1
//! Configure and implement onion service reverse-proxy feature.
2

            
3
use std::{
4
    collections::{BTreeMap, HashSet, btree_map::Entry},
5
    sync::{Arc, Mutex},
6
};
7

            
8
use arti_client::config::onion_service::{OnionServiceConfig, OnionServiceConfigBuilder};
9
use futures::StreamExt as _;
10
use tor_config::{
11
    ConfigBuildError, Flatten, Reconfigure, ReconfigureError, define_list_builder_helper,
12
    impl_standard_builder,
13
};
14
use tor_error::warn_report;
15
use tor_hsrproxy::{OnionServiceReverseProxy, ProxyConfig, config::ProxyConfigBuilder};
16
use tor_hsservice::{HsNickname, RunningOnionService};
17
use tor_rtcompat::{Runtime, SpawnExt};
18
use tracing::debug;
19

            
20
use crate::reload_cfg::ReconfigurableModule;
21

            
22
/// Configuration for running an onion service from `arti`.
23
///
24
/// This onion service will forward incoming connections to one or more local
25
/// ports, depending on its configuration.  If you need it to do something else
26
/// with incoming connections, or if you need finer-grained control over its
27
/// behavior, consider using
28
/// [`TorClient::launch_onion_service`](arti_client::TorClient::launch_onion_service).
29
#[derive(Clone, Debug, Eq, PartialEq)]
30
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
31
pub(crate) struct OnionServiceProxyConfig {
32
    /// Configuration for the onion service itself.
33
    pub(crate) svc_cfg: OnionServiceConfig,
34
    /// Configuration for the reverse proxy that handles incoming connections
35
    /// from the onion service.
36
    pub(crate) proxy_cfg: ProxyConfig,
37
}
38

            
39
/// Builder object to construct an [`OnionServiceProxyConfig`].
40
//
41
// We cannot easily use derive_builder on this builder type, since we want it to be a
42
// "Flatten<>" internally.  Fortunately, it's easy enough to implement the
43
// pieces that we need.
44
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
45
#[serde(transparent)]
46
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
47
pub(crate) struct OnionServiceProxyConfigBuilder(
48
    Flatten<OnionServiceConfigBuilder, ProxyConfigBuilder>,
49
);
50

            
51
impl OnionServiceProxyConfigBuilder {
52
    /// Try to construct an [`OnionServiceProxyConfig`].
53
    ///
54
    /// Returns an error if any part of this builder is invalid.
55
150
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
56
150
    pub(crate) fn build(&self) -> Result<OnionServiceProxyConfig, ConfigBuildError> {
57
150
        let svc_cfg = self.0.0.build()?;
58
150
        let proxy_cfg = self.0.1.build()?;
59
150
        Ok(OnionServiceProxyConfig { svc_cfg, proxy_cfg })
60
150
    }
61

            
62
    /// Return a mutable reference to an onion-service configuration sub-builder.
63
16
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
64
16
    pub(crate) fn service(&mut self) -> &mut OnionServiceConfigBuilder {
65
16
        &mut self.0.0
66
16
    }
67

            
68
    /// Return a mutable reference to a proxy configuration sub-builder.
69
10
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
70
10
    pub(crate) fn proxy(&mut self) -> &mut ProxyConfigBuilder {
71
10
        &mut self.0.1
72
10
    }
73
}
74

            
75
impl_standard_builder! { OnionServiceProxyConfig: !Default }
76

            
77
/// Alias for a `BTreeMap` of [`OnionServiceProxyConfig`]; used to make [`derive_builder`] happy.
78
#[cfg(feature = "onion-service-service")]
79
pub(crate) type OnionServiceProxyConfigMap = BTreeMap<HsNickname, OnionServiceProxyConfig>;
80

            
81
/// The serialized format of an [`OnionServiceProxyConfigMapBuilder`]:
82
/// a map from [`HsNickname`] to [`OnionServiceConfigBuilder`].
83
type ProxyBuilderMap = BTreeMap<HsNickname, OnionServiceProxyConfigBuilder>;
84

            
85
// TODO: Someday we might want to have an API for a MapBuilder that is distinct
86
// from that of a ListBuilder.  It would have to enforce that everything has a
87
// key, and that keys are distinct.
88
#[cfg(feature = "onion-service-service")]
89
define_list_builder_helper! {
90
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
91
    pub(crate) struct OnionServiceProxyConfigMapBuilder {
92
        services: [OnionServiceProxyConfigBuilder],
93
    }
94
    built: OnionServiceProxyConfigMap = build_list(services)?;
95
    default = vec![];
96
    #[serde(try_from="ProxyBuilderMap", into="ProxyBuilderMap")]
97
}
98

            
99
/// Construct a [`OnionServiceProxyConfigMap`] from a `Vec` of [`OnionServiceProxyConfig`];
100
/// enforce that [`HsNickname`]s are unique.
101
352
fn build_list(
102
352
    services: Vec<OnionServiceProxyConfig>,
103
352
) -> Result<OnionServiceProxyConfigMap, ConfigBuildError> {
104
    // It *is* reachable from OnionServiceProxyConfigMapBuilder::build(), since
105
    // that builder's API uses push() to add OnionServiceProxyConfigBuilders to
106
    // an internal _list_.  Alternatively, we might want to have a distinct
107
    // MapBuilder type.
108

            
109
352
    let mut map = BTreeMap::new();
110
352
    for service in services {
111
148
        if let Some(previous_value) = map.insert(service.svc_cfg.nickname().clone(), service) {
112
2
            return Err(ConfigBuildError::Inconsistent {
113
2
                fields: vec!["nickname".into()],
114
2
                problem: format!(
115
2
                    "Multiple onion services with the nickname {}",
116
2
                    previous_value.svc_cfg.nickname()
117
2
                ),
118
2
            });
119
146
        };
120
    }
121
350
    Ok(map)
122
352
}
123

            
124
impl TryFrom<ProxyBuilderMap> for OnionServiceProxyConfigMapBuilder {
125
    type Error = ConfigBuildError;
126

            
127
128
    fn try_from(value: ProxyBuilderMap) -> Result<Self, Self::Error> {
128
128
        let mut list_builder = OnionServiceProxyConfigMapBuilder::default();
129
146
        for (nickname, mut cfg) in value {
130
140
            match cfg.0.0.peek_nickname() {
131
                Some(n) if n == &nickname => (),
132
140
                None => (),
133
                Some(other) => {
134
                    return Err(ConfigBuildError::Inconsistent {
135
                        fields: vec![nickname.to_string(), format!("{nickname}.{other}")],
136
                        problem: "mismatched nicknames on onion service.".into(),
137
                    });
138
                }
139
            }
140
140
            cfg.0.0.nickname(nickname);
141
140
            list_builder.access().push(cfg);
142
        }
143
128
        Ok(list_builder)
144
128
    }
145
}
146

            
147
impl From<OnionServiceProxyConfigMapBuilder> for ProxyBuilderMap {
148
    /// Convert our Builder representation of a set of onion services into the
149
    /// format that serde will serialize.
150
    ///
151
    /// Note: This is a potentially lossy conversion, since the serialized format
152
    /// can't represent partially-built services without a nickname, or
153
    /// a collection of services with duplicate nicknames.
154
22
    fn from(value: OnionServiceProxyConfigMapBuilder) -> Self {
155
22
        let mut map = BTreeMap::new();
156
22
        for cfg in value.services.into_iter().flatten() {
157
            let nickname = cfg.0.0.peek_nickname().cloned().unwrap_or_else(|| {
158
                "Unnamed"
159
                    .to_string()
160
                    .try_into()
161
                    .expect("'Unnamed' was not a valid nickname")
162
            });
163
            map.insert(nickname, cfg);
164
        }
165
22
        map
166
22
    }
167
}
168

            
169
/// A running onion service and an associated reverse proxy.
170
///
171
/// This is what a user configures when they add an onion service to their
172
/// configuration.
173
#[must_use = "a hidden service Proxy object will terminate the service when dropped"]
174
struct Proxy {
175
    /// The onion service.
176
    ///
177
    /// This is launched and running.
178
    svc: Arc<RunningOnionService>,
179
    /// The reverse proxy that accepts connections from the onion service.
180
    ///
181
    /// This is also launched and running.
182
    proxy: Arc<OnionServiceReverseProxy>,
183
}
184

            
185
impl Proxy {
186
    /// Create and launch a new onion service proxy, using a given `client`,
187
    /// to handle connections according to `config`.
188
    ///
189
    /// Returns `Ok(None)` if the service specified is disabled in the config.
190
    pub(crate) fn launch_new<R: Runtime>(
191
        client: &arti_client::TorClient<R>,
192
        config: OnionServiceProxyConfig,
193
    ) -> anyhow::Result<Option<Self>> {
194
        let OnionServiceProxyConfig { svc_cfg, proxy_cfg } = config;
195
        let nickname = svc_cfg.nickname().clone();
196

            
197
        let (svc, request_stream) = match client.launch_onion_service(svc_cfg)? {
198
            Some(running_service) => running_service,
199
            None => {
200
                debug!(
201
                    "Onion service {} didn't start (disabled in config)",
202
                    nickname
203
                );
204
                return Ok(None);
205
            }
206
        };
207
        let proxy = OnionServiceReverseProxy::new(proxy_cfg);
208

            
209
        {
210
            let proxy = proxy.clone();
211
            let runtime_clone = client.runtime().clone();
212
            let nickname_clone = nickname.clone();
213
            client.runtime().spawn(async move {
214
                match proxy
215
                    .handle_requests(runtime_clone, nickname.clone(), request_stream)
216
                    .await
217
                {
218
                    Ok(()) => {
219
                        debug!("Onion service {} exited cleanly.", nickname);
220
                    }
221
                    Err(e) => {
222
                        warn_report!(e, "Onion service {} exited with an error", nickname);
223
                    }
224
                }
225
            })?;
226

            
227
            let mut status_stream = svc.status_events();
228
            client.runtime().spawn(async move {
229
                while let Some(status) = status_stream.next().await {
230
                    debug!(
231
                        nickname=%nickname_clone,
232
                        status=?status.state(),
233
                        problem=?status.current_problem(),
234
                        "Onion service status change",
235
                    );
236
                }
237
            })?;
238
        }
239

            
240
        Ok(Some(Proxy { svc, proxy }))
241
    }
242

            
243
    /// Reconfigure this proxy, using the new configuration `config` and the
244
    /// rules in `how`.
245
    fn reconfigure(
246
        &mut self,
247
        config: OnionServiceProxyConfig,
248
        how: Reconfigure,
249
    ) -> Result<(), ReconfigureError> {
250
        if matches!(how, Reconfigure::AllOrNothing) {
251
            self.reconfigure_inner(config.clone(), Reconfigure::CheckAllOrNothing)?;
252
        }
253

            
254
        self.reconfigure_inner(config, how)
255
    }
256

            
257
    /// Helper for `reconfigure`: Run `reconfigure` on each part of this `Proxy`.
258
    fn reconfigure_inner(
259
        &mut self,
260
        config: OnionServiceProxyConfig,
261
        how: Reconfigure,
262
    ) -> Result<(), ReconfigureError> {
263
        let OnionServiceProxyConfig { svc_cfg, proxy_cfg } = config;
264

            
265
        self.svc.reconfigure(svc_cfg, how)?;
266
        self.proxy.reconfigure(proxy_cfg, how)?;
267

            
268
        Ok(())
269
    }
270
}
271

            
272
/// A set of configured onion service proxies.
273
#[must_use = "a hidden service ProxySet object will terminate the services when dropped"]
274
pub(crate) struct ProxySet<R: Runtime> {
275
    /// The arti_client that we use to launch proxies.
276
    client: Arc<arti_client::TorClient<R>>,
277
    /// The proxies themselves, indexed by nickname.
278
    proxies: Mutex<BTreeMap<HsNickname, Proxy>>,
279
}
280

            
281
impl<R: Runtime> ProxySet<R> {
282
    /// Create a new empty onion service proxy set.
283
    ///
284
    /// We do this when we are running with deferred bootstrapping,
285
    /// since we can't launch an onion service on an unbootstrapped client.
286
    pub(crate) fn new_deferred(client: Arc<arti_client::TorClient<R>>) -> Self {
287
        Self {
288
            client,
289
            proxies: Mutex::new(BTreeMap::new()),
290
        }
291
    }
292

            
293
    /// Create and launch a set of onion service proxies.
294
    pub(crate) fn launch_new(
295
        client: Arc<arti_client::TorClient<R>>,
296
        config_list: OnionServiceProxyConfigMap,
297
    ) -> anyhow::Result<Self> {
298
        let proxies: BTreeMap<_, _> = config_list
299
            .into_iter()
300
            .filter_map(|(nickname, cfg)| {
301
                // Filter out services which are disabled in the config
302
                match Proxy::launch_new(&client, cfg) {
303
                    Ok(Some(running_service)) => Some(Ok((nickname, running_service))),
304
                    Err(error) => Some(Err(error)),
305
                    Ok(None) => None,
306
                }
307
            })
308
            .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
309

            
310
        Ok(Self {
311
            client,
312
            proxies: Mutex::new(proxies),
313
        })
314
    }
315

            
316
    /// Try to reconfigure the set of onion proxies according to the
317
    /// configuration in `new_config`.
318
    ///
319
    /// Launches or closes proxies as necessary.  Does not close existing
320
    /// connections.
321
    pub(crate) fn reconfigure(
322
        &self,
323
        new_config: OnionServiceProxyConfigMap,
324
        how: Reconfigure,
325
    ) -> Result<(), ReconfigureError> {
326
        if how == Reconfigure::AllOrNothing {
327
            self.reconfigure(new_config.clone(), Reconfigure::CheckAllOrNothing)?;
328
        }
329
        let dry_run = how == Reconfigure::CheckAllOrNothing;
330

            
331
        let mut proxy_map = self.proxies.lock().expect("lock poisoned");
332

            
333
        // Set of the nicknames of defunct proxies.
334
        let mut defunct_nicknames: HashSet<_> = proxy_map.keys().map(Clone::clone).collect();
335

            
336
        for cfg in new_config.into_values() {
337
            let nickname = cfg.svc_cfg.nickname().clone();
338
            // This proxy is still configured, so remove it from the list of
339
            // defunct proxies.
340
            defunct_nicknames.remove(&nickname);
341

            
342
            match proxy_map.entry(nickname) {
343
                Entry::Occupied(mut existing_proxy) => {
344
                    // We already have a proxy by this name, so we try to
345
                    // reconfigure it.
346
                    existing_proxy.get_mut().reconfigure(cfg, how)?;
347
                }
348
                Entry::Vacant(ent) => {
349
                    // We do not have a proxy by this name, so we try to launch
350
                    // one.
351
                    if !dry_run {
352
                        match Proxy::launch_new(&self.client, cfg) {
353
                            Ok(Some(new_proxy)) => {
354
                                ent.insert(new_proxy);
355
                            }
356
                            Ok(None) => {
357
                                debug!(
358
                                    "Onion service {} didn't start (disabled in config)",
359
                                    ent.key()
360
                                );
361
                            }
362
                            Err(err) => {
363
                                warn_report!(err, "Unable to launch onion service {}", ent.key());
364
                            }
365
                        }
366
                    }
367
                }
368
            }
369
        }
370

            
371
        if !dry_run {
372
            for nickname in defunct_nicknames {
373
                // We no longer have any configuration for this proxy, so we remove
374
                // it from our map.
375
                let defunct_proxy = proxy_map
376
                    .remove(&nickname)
377
                    .expect("Somehow a proxy disappeared from the map");
378
                // This "drop" should shut down the proxy.
379
                drop(defunct_proxy);
380
            }
381
        }
382

            
383
        Ok(())
384
    }
385

            
386
    /// Whether this `ProxySet` is empty.
387
    pub(crate) fn is_empty(&self) -> bool {
388
        self.proxies.lock().expect("lock poisoned").is_empty()
389
    }
390
}
391

            
392
impl<R: Runtime> ReconfigurableModule for ProxySet<R> {
393
    fn reconfigure(
394
        &self,
395
        new: &crate::ArtiCombinedConfig,
396
        how: Reconfigure,
397
    ) -> Result<(), ReconfigureError> {
398
        if new.0.application().defer_bootstrap {
399
            // Do not actually launch any onion services unless we are trying
400
            // to bootstrap the client.
401
            return Ok(());
402
        }
403

            
404
        ProxySet::reconfigure(self, new.0.onion_services.clone(), how)?;
405
        Ok(())
406
    }
407
}
408

            
409
#[cfg(test)]
410
mod tests {
411
    // @@ begin test lint list maintained by maint/add_warning @@
412
    #![allow(clippy::bool_assert_comparison)]
413
    #![allow(clippy::clone_on_copy)]
414
    #![allow(clippy::dbg_macro)]
415
    #![allow(clippy::mixed_attributes_style)]
416
    #![allow(clippy::print_stderr)]
417
    #![allow(clippy::print_stdout)]
418
    #![allow(clippy::single_char_pattern)]
419
    #![allow(clippy::unwrap_used)]
420
    #![allow(clippy::unchecked_time_subtraction)]
421
    #![allow(clippy::useless_vec)]
422
    #![allow(clippy::needless_pass_by_value)]
423
    #![allow(clippy::string_slice)] // See arti#2571
424
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
425
    use super::*;
426

            
427
    use tor_config::ConfigBuildError;
428
    use tor_hsservice::HsNickname;
429

            
430
    /// Get an [`OnionServiceProxyConfig`] with its `svc_cfg` field having the nickname `nick`.
431
    fn get_onion_service_proxy_config(nick: &HsNickname) -> OnionServiceProxyConfig {
432
        let mut builder = OnionServiceProxyConfigBuilder::default();
433
        builder.service().nickname(nick.clone());
434
        builder.build().unwrap()
435
    }
436

            
437
    /// Test `super::build_list` with unique and duplicate [`HsNickname`]s.
438
    #[test]
439
    fn fn_build_list() {
440
        let nick_1 = HsNickname::new("nick_1".to_string()).unwrap();
441
        let nick_2 = HsNickname::new("nick_2".to_string()).unwrap();
442

            
443
        let proxy_configs: Vec<OnionServiceProxyConfig> = [&nick_1, &nick_2]
444
            .into_iter()
445
            .map(get_onion_service_proxy_config)
446
            .collect();
447
        let actual = build_list(proxy_configs.clone()).unwrap();
448

            
449
        let expected =
450
            OnionServiceProxyConfigMap::from_iter([nick_1, nick_2].into_iter().zip(proxy_configs));
451

            
452
        assert_eq!(actual, expected);
453

            
454
        let nick = HsNickname::new("nick".to_string()).unwrap();
455
        let proxy_configs_dup: Vec<OnionServiceProxyConfig> = [&nick, &nick]
456
            .into_iter()
457
            .map(get_onion_service_proxy_config)
458
            .collect();
459
        let actual = build_list(proxy_configs_dup).unwrap_err();
460
        let ConfigBuildError::Inconsistent { fields, problem } = actual else {
461
            panic!("Unexpected error from `build_list`: {actual:?}");
462
        };
463

            
464
        assert_eq!(fields, vec!["nickname".to_string()]);
465
        assert_eq!(
466
            problem,
467
            format!("Multiple onion services with the nickname {nick}")
468
        );
469
    }
470
}