arti/
onion_proxy.rs

1//! Configure and implement onion service reverse-proxy feature.
2
3use std::{
4    collections::{btree_map::Entry, BTreeMap, HashSet},
5    sync::{Arc, Mutex},
6};
7
8use arti_client::config::onion_service::{OnionServiceConfig, OnionServiceConfigBuilder};
9use futures::{StreamExt as _};
10use tor_config::{
11    define_list_builder_helper, impl_standard_builder, ConfigBuildError, Flatten, Reconfigure,
12    ReconfigureError,
13};
14use tor_error::warn_report;
15use tor_hsrproxy::{config::ProxyConfigBuilder, OnionServiceReverseProxy, ProxyConfig};
16use tor_hsservice::{HsNickname, RunningOnionService};
17use tor_rtcompat::{Runtime, SpawnExt};
18use tracing::debug;
19
20/// Configuration for running an onion service from `arti`.
21///
22/// This onion service will forward incoming connections to one or more local
23/// ports, depending on its configuration.  If you need it to do something else
24/// with incoming connections, or if you need finer-grained control over its
25/// behavior, consider using
26/// [`TorClient::launch_onion_service`](arti_client::TorClient::launch_onion_service).
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct OnionServiceProxyConfig {
29    /// Configuration for the onion service itself.
30    pub(crate) svc_cfg: OnionServiceConfig,
31    /// Configuration for the reverse proxy that handles incoming connections
32    /// from the onion service.
33    pub(crate) proxy_cfg: ProxyConfig,
34}
35
36/// Builder object to construct an [`OnionServiceProxyConfig`].
37//
38// We cannot easily use derive_builder on this builder type, since we want it to be a
39// "Flatten<>" internally.  Fortunately, it's easy enough to implement the
40// pieces that we need.
41#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
42#[serde(transparent)]
43pub struct OnionServiceProxyConfigBuilder(Flatten<OnionServiceConfigBuilder, ProxyConfigBuilder>);
44
45impl OnionServiceProxyConfigBuilder {
46    /// Try to construct an [`OnionServiceProxyConfig`].
47    ///
48    /// Returns an error if any part of this builder is invalid.
49    pub fn build(&self) -> Result<OnionServiceProxyConfig, ConfigBuildError> {
50        let svc_cfg = self.0 .0.build()?;
51        let proxy_cfg = self.0 .1.build()?;
52        Ok(OnionServiceProxyConfig { svc_cfg, proxy_cfg })
53    }
54
55    /// Return a mutable reference to an onion-service configuration sub-builder.
56    pub fn service(&mut self) -> &mut OnionServiceConfigBuilder {
57        &mut self.0 .0
58    }
59
60    /// Return a mutable reference to a proxy configuration sub-builder.
61    pub fn proxy(&mut self) -> &mut ProxyConfigBuilder {
62        &mut self.0 .1
63    }
64}
65
66impl_standard_builder! { OnionServiceProxyConfig: !Default }
67
68/// Alias for a `BTreeMap` of [`OnionServiceProxyConfig`]; used to make [`derive_builder`] happy.
69#[cfg(feature = "onion-service-service")]
70pub(crate) type OnionServiceProxyConfigMap = BTreeMap<HsNickname, OnionServiceProxyConfig>;
71
72/// The serialized format of an [`OnionServiceProxyConfigMapBuilder`]:
73/// a map from [`HsNickname`] to [`OnionServiceConfigBuilder`].
74type ProxyBuilderMap = BTreeMap<HsNickname, OnionServiceProxyConfigBuilder>;
75
76// TODO: Someday we might want to have an API for a MapBuilder that is distinct
77// from that of a ListBuilder.  It would have to enforce that everything has a
78// key, and that keys are distinct.
79#[cfg(feature = "onion-service-service")]
80define_list_builder_helper! {
81    pub struct OnionServiceProxyConfigMapBuilder {
82        services: [OnionServiceProxyConfigBuilder],
83    }
84    built: OnionServiceProxyConfigMap = build_list(services)?;
85    default = vec![];
86    #[serde(try_from="ProxyBuilderMap", into="ProxyBuilderMap")]
87}
88
89/// Construct a [`OnionServiceProxyConfigMap`] from a `Vec` of [`OnionServiceProxyConfig`];
90/// enforce that [`HsNickname`]s are unique.
91fn build_list(
92    services: Vec<OnionServiceProxyConfig>,
93) -> Result<OnionServiceProxyConfigMap, ConfigBuildError> {
94    // It *is* reachable from OnionServiceProxyConfigMapBuilder::build(), since
95    // that builder's API uses push() to add OnionServiceProxyConfigBuilders to
96    // an internal _list_.  Alternatively, we might want to have a distinct
97    // MapBuilder type.
98
99    let mut map = BTreeMap::new();
100    for service in services {
101        if let Some(previous_value) = map.insert(service.svc_cfg.nickname().clone(), service) {
102            return Err(ConfigBuildError::Inconsistent {
103                fields: vec!["nickname".into()],
104                problem: format!(
105                    "Multiple onion services with the nickname {}",
106                    previous_value.svc_cfg.nickname()
107                ),
108            });
109        };
110    }
111    Ok(map)
112}
113
114impl TryFrom<ProxyBuilderMap> for OnionServiceProxyConfigMapBuilder {
115    type Error = ConfigBuildError;
116
117    fn try_from(value: ProxyBuilderMap) -> Result<Self, Self::Error> {
118        let mut list_builder = OnionServiceProxyConfigMapBuilder::default();
119        for (nickname, mut cfg) in value {
120            match cfg.0 .0.peek_nickname() {
121                Some(n) if n == &nickname => (),
122                None => (),
123                Some(other) => {
124                    return Err(ConfigBuildError::Inconsistent {
125                        fields: vec![nickname.to_string(), format!("{nickname}.{other}")],
126                        problem: "mismatched nicknames on onion service.".into(),
127                    });
128                }
129            }
130            cfg.0 .0.nickname(nickname);
131            list_builder.access().push(cfg);
132        }
133        Ok(list_builder)
134    }
135}
136
137impl From<OnionServiceProxyConfigMapBuilder> for ProxyBuilderMap {
138    /// Convert our Builder representation of a set of onion services into the
139    /// format that serde will serialize.
140    ///
141    /// Note: This is a potentially lossy conversion, since the serialized format
142    /// can't represent partially-built services without a nickname, or
143    /// a collection of services with duplicate nicknames.
144    fn from(value: OnionServiceProxyConfigMapBuilder) -> Self {
145        let mut map = BTreeMap::new();
146        for cfg in value.services.into_iter().flatten() {
147            let nickname = cfg.0 .0.peek_nickname().cloned().unwrap_or_else(|| {
148                "Unnamed"
149                    .to_string()
150                    .try_into()
151                    .expect("'Unnamed' was not a valid nickname")
152            });
153            map.insert(nickname, cfg);
154        }
155        map
156    }
157}
158
159/// A running onion service and an associated reverse proxy.
160///
161/// This is what a user configures when they add an onion service to their
162/// configuration.
163#[must_use = "a hidden service Proxy object will terminate the service when dropped"]
164struct Proxy {
165    /// The onion service.
166    ///
167    /// This is launched and running.
168    svc: Arc<RunningOnionService>,
169    /// The reverse proxy that accepts connections from the onion service.
170    ///
171    /// This is also launched and running.
172    proxy: Arc<OnionServiceReverseProxy>,
173}
174
175impl Proxy {
176    /// Create and launch a new onion service proxy, using a given `client`,
177    /// to handle connections according to `config`.
178    ///
179    /// Returns `Ok(None)` if the service specified is disabled in the config.
180    pub(crate) fn launch_new<R: Runtime>(
181        client: &arti_client::TorClient<R>,
182        config: OnionServiceProxyConfig,
183    ) -> anyhow::Result<Option<Self>> {
184        let OnionServiceProxyConfig { svc_cfg, proxy_cfg } = config;
185        let nickname = svc_cfg.nickname().clone();
186
187        let (svc, request_stream) = match client.launch_onion_service(svc_cfg)? {
188            Some(running_service) => running_service,
189            None => {
190                debug!(
191                    "Onion service {} didn't start (disabled in config)",
192                    nickname
193                );
194                return Ok(None);
195            }
196        };
197        let proxy = OnionServiceReverseProxy::new(proxy_cfg);
198
199        {
200            let proxy = proxy.clone();
201            let runtime_clone = client.runtime().clone();
202            let nickname_clone = nickname.clone();
203            client.runtime().spawn(async move {
204                match proxy
205                    .handle_requests(runtime_clone, nickname.clone(), request_stream)
206                    .await
207                {
208                    Ok(()) => {
209                        debug!("Onion service {} exited cleanly.", nickname);
210                    }
211                    Err(e) => {
212                        warn_report!(e, "Onion service {} exited with an error", nickname);
213                    }
214                }
215            })?;
216
217            let mut status_stream = svc.status_events();
218            client.runtime().spawn(async move {
219                while let Some(status) = status_stream.next().await {
220                    debug!(
221                        nickname=%nickname_clone,
222                        status=?status.state(),
223                        problem=?status.current_problem(),
224                        "Onion service status change",
225                    );
226                }
227            })?;
228        }
229
230        Ok(Some(Proxy { svc, proxy }))
231    }
232
233    /// Reconfigure this proxy, using the new configuration `config` and the
234    /// rules in `how`.
235    fn reconfigure(
236        &mut self,
237        config: OnionServiceProxyConfig,
238        how: Reconfigure,
239    ) -> Result<(), ReconfigureError> {
240        if matches!(how, Reconfigure::AllOrNothing) {
241            self.reconfigure_inner(config.clone(), Reconfigure::CheckAllOrNothing)?;
242        }
243
244        self.reconfigure_inner(config, how)
245    }
246
247    /// Helper for `reconfigure`: Run `reconfigure` on each part of this `Proxy`.
248    fn reconfigure_inner(
249        &mut self,
250        config: OnionServiceProxyConfig,
251        how: Reconfigure,
252    ) -> Result<(), ReconfigureError> {
253        let OnionServiceProxyConfig { svc_cfg, proxy_cfg } = config;
254
255        self.svc.reconfigure(svc_cfg, how)?;
256        self.proxy.reconfigure(proxy_cfg, how)?;
257
258        Ok(())
259    }
260}
261
262/// A set of configured onion service proxies.
263#[must_use = "a hidden service ProxySet object will terminate the services when dropped"]
264pub(crate) struct ProxySet<R: Runtime> {
265    /// The arti_client that we use to launch proxies.
266    client: arti_client::TorClient<R>,
267    /// The proxies themselves, indexed by nickname.
268    proxies: Mutex<BTreeMap<HsNickname, Proxy>>,
269}
270
271impl<R: Runtime> ProxySet<R> {
272    /// Create and launch a set of onion service proxies.
273    pub(crate) fn launch_new(
274        client: &arti_client::TorClient<R>,
275        config_list: OnionServiceProxyConfigMap,
276    ) -> anyhow::Result<Self> {
277        let proxies: BTreeMap<_, _> = config_list
278            .into_iter()
279            .filter_map(|(nickname, cfg)| {
280                // Filter out services which are disabled in the config
281                match Proxy::launch_new(client, cfg) {
282                    Ok(Some(running_service)) => Some(Ok((nickname, running_service))),
283                    Err(error) => Some(Err(error)),
284                    Ok(None) => None,
285                }
286            })
287            .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
288
289        Ok(Self {
290            client: client.clone(),
291            proxies: Mutex::new(proxies),
292        })
293    }
294
295    /// Try to reconfigure the set of onion proxies according to the
296    /// configuration in `new_config`.
297    ///
298    /// Launches or closes proxies as necessary.  Does not close existing
299    /// connections.
300    pub(crate) fn reconfigure(
301        &self,
302        new_config: OnionServiceProxyConfigMap,
303        // TODO: this should probably take `how: Reconfigure` and implement an all-or-nothing mode.
304        // See #1156.
305    ) -> Result<(), anyhow::Error> {
306        let mut proxy_map = self.proxies.lock().expect("lock poisoned");
307
308        // Set of the nicknames of defunct proxies.
309        let mut defunct_nicknames: HashSet<_> = proxy_map.keys().map(Clone::clone).collect();
310
311        for cfg in new_config.into_values() {
312            let nickname = cfg.svc_cfg.nickname().clone();
313            // This proxy is still configured, so remove it from the list of
314            // defunct proxies.
315            defunct_nicknames.remove(&nickname);
316
317            match proxy_map.entry(nickname) {
318                Entry::Occupied(mut existing_proxy) => {
319                    // We already have a proxy by this name, so we try to
320                    // reconfigure it.
321                    existing_proxy
322                        .get_mut()
323                        .reconfigure(cfg, Reconfigure::WarnOnFailures)?;
324                }
325                Entry::Vacant(ent) => {
326                    // We do not have a proxy by this name, so we try to launch
327                    // one.
328                    match Proxy::launch_new(&self.client, cfg) {
329                        Ok(Some(new_proxy)) => {
330                            ent.insert(new_proxy);
331                        }
332                        Ok(None) => {
333                            debug!(
334                                "Onion service {} didn't start (disabled in config)",
335                                ent.key()
336                            );
337                        }
338                        Err(err) => {
339                            warn_report!(err, "Unable to launch onion service {}", ent.key());
340                        }
341                    }
342                }
343            }
344        }
345
346        for nickname in defunct_nicknames {
347            // We no longer have any configuration for this proxy, so we remove
348            // it from our map.
349            let defunct_proxy = proxy_map
350                .remove(&nickname)
351                .expect("Somehow a proxy disappeared from the map");
352            // This "drop" should shut down the proxy.
353            drop(defunct_proxy);
354        }
355
356        Ok(())
357    }
358
359    /// Whether this `ProxySet` is empty.
360    pub(crate) fn is_empty(&self) -> bool {
361        self.proxies.lock().expect("lock poisoned").is_empty()
362    }
363}
364
365impl<R: Runtime> crate::reload_cfg::ReconfigurableModule for ProxySet<R> {
366    fn reconfigure(&self, new: &crate::ArtiCombinedConfig) -> anyhow::Result<()> {
367        ProxySet::reconfigure(self, new.0.onion_services.clone())?;
368        Ok(())
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    // @@ begin test lint list maintained by maint/add_warning @@
375    #![allow(clippy::bool_assert_comparison)]
376    #![allow(clippy::clone_on_copy)]
377    #![allow(clippy::dbg_macro)]
378    #![allow(clippy::mixed_attributes_style)]
379    #![allow(clippy::print_stderr)]
380    #![allow(clippy::print_stdout)]
381    #![allow(clippy::single_char_pattern)]
382    #![allow(clippy::unwrap_used)]
383    #![allow(clippy::unchecked_time_subtraction)]
384    #![allow(clippy::useless_vec)]
385    #![allow(clippy::needless_pass_by_value)]
386    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
387    use super::*;
388
389    use tor_config::ConfigBuildError;
390    use tor_hsservice::HsNickname;
391
392    /// Get an [`OnionServiceProxyConfig`] with its `svc_cfg` field having the nickname `nick`.
393    fn get_onion_service_proxy_config(nick: &HsNickname) -> OnionServiceProxyConfig {
394        let mut builder = OnionServiceProxyConfigBuilder::default();
395        builder.service().nickname(nick.clone());
396        builder.build().unwrap()
397    }
398
399    /// Test `super::build_list` with unique and duplicate [`HsNickname`]s.
400    #[test]
401    fn fn_build_list() {
402        let nick_1 = HsNickname::new("nick_1".to_string()).unwrap();
403        let nick_2 = HsNickname::new("nick_2".to_string()).unwrap();
404
405        let proxy_configs: Vec<OnionServiceProxyConfig> = [&nick_1, &nick_2]
406            .into_iter()
407            .map(get_onion_service_proxy_config)
408            .collect();
409        let actual = build_list(proxy_configs.clone()).unwrap();
410
411        let expected =
412            OnionServiceProxyConfigMap::from_iter([nick_1, nick_2].into_iter().zip(proxy_configs));
413
414        assert_eq!(actual, expected);
415
416        let nick = HsNickname::new("nick".to_string()).unwrap();
417        let proxy_configs_dup: Vec<OnionServiceProxyConfig> = [&nick, &nick]
418            .into_iter()
419            .map(get_onion_service_proxy_config)
420            .collect();
421        let actual = build_list(proxy_configs_dup).unwrap_err();
422        let ConfigBuildError::Inconsistent { fields, problem } = actual else {
423            panic!("Unexpected error from `build_list`: {actual:?}");
424        };
425
426        assert_eq!(fields, vec!["nickname".to_string()]);
427        assert_eq!(
428            problem,
429            format!("Multiple onion services with the nickname {nick}")
430        );
431    }
432}