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
/// 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)]
28
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
29
pub(crate) struct OnionServiceProxyConfig {
30
    /// Configuration for the onion service itself.
31
    pub(crate) svc_cfg: OnionServiceConfig,
32
    /// Configuration for the reverse proxy that handles incoming connections
33
    /// from the onion service.
34
    pub(crate) proxy_cfg: ProxyConfig,
35
}
36

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

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

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

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

            
73
impl_standard_builder! { OnionServiceProxyConfig: !Default }
74

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

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

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

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

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

            
122
impl TryFrom<ProxyBuilderMap> for OnionServiceProxyConfigMapBuilder {
123
    type Error = ConfigBuildError;
124

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

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

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

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

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

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

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

            
238
        Ok(Some(Proxy { svc, proxy }))
239
    }
240

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

            
252
        self.reconfigure_inner(config, how)
253
    }
254

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

            
263
        self.svc.reconfigure(svc_cfg, how)?;
264
        self.proxy.reconfigure(proxy_cfg, how)?;
265

            
266
        Ok(())
267
    }
268
}
269

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

            
279
impl<R: Runtime> ProxySet<R> {
280
    /// Create and launch a set of onion service proxies.
281
    pub(crate) fn launch_new(
282
        client: &arti_client::TorClient<R>,
283
        config_list: OnionServiceProxyConfigMap,
284
    ) -> anyhow::Result<Self> {
285
        let proxies: BTreeMap<_, _> = config_list
286
            .into_iter()
287
            .filter_map(|(nickname, cfg)| {
288
                // Filter out services which are disabled in the config
289
                match Proxy::launch_new(client, cfg) {
290
                    Ok(Some(running_service)) => Some(Ok((nickname, running_service))),
291
                    Err(error) => Some(Err(error)),
292
                    Ok(None) => None,
293
                }
294
            })
295
            .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
296

            
297
        Ok(Self {
298
            client: client.clone(),
299
            proxies: Mutex::new(proxies),
300
        })
301
    }
302

            
303
    /// Try to reconfigure the set of onion proxies according to the
304
    /// configuration in `new_config`.
305
    ///
306
    /// Launches or closes proxies as necessary.  Does not close existing
307
    /// connections.
308
    pub(crate) fn reconfigure(
309
        &self,
310
        new_config: OnionServiceProxyConfigMap,
311
        // TODO: this should probably take `how: Reconfigure` and implement an all-or-nothing mode.
312
        // See #1156.
313
    ) -> Result<(), anyhow::Error> {
314
        let mut proxy_map = self.proxies.lock().expect("lock poisoned");
315

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

            
319
        for cfg in new_config.into_values() {
320
            let nickname = cfg.svc_cfg.nickname().clone();
321
            // This proxy is still configured, so remove it from the list of
322
            // defunct proxies.
323
            defunct_nicknames.remove(&nickname);
324

            
325
            match proxy_map.entry(nickname) {
326
                Entry::Occupied(mut existing_proxy) => {
327
                    // We already have a proxy by this name, so we try to
328
                    // reconfigure it.
329
                    existing_proxy
330
                        .get_mut()
331
                        .reconfigure(cfg, Reconfigure::WarnOnFailures)?;
332
                }
333
                Entry::Vacant(ent) => {
334
                    // We do not have a proxy by this name, so we try to launch
335
                    // one.
336
                    match Proxy::launch_new(&self.client, cfg) {
337
                        Ok(Some(new_proxy)) => {
338
                            ent.insert(new_proxy);
339
                        }
340
                        Ok(None) => {
341
                            debug!(
342
                                "Onion service {} didn't start (disabled in config)",
343
                                ent.key()
344
                            );
345
                        }
346
                        Err(err) => {
347
                            warn_report!(err, "Unable to launch onion service {}", ent.key());
348
                        }
349
                    }
350
                }
351
            }
352
        }
353

            
354
        for nickname in defunct_nicknames {
355
            // We no longer have any configuration for this proxy, so we remove
356
            // it from our map.
357
            let defunct_proxy = proxy_map
358
                .remove(&nickname)
359
                .expect("Somehow a proxy disappeared from the map");
360
            // This "drop" should shut down the proxy.
361
            drop(defunct_proxy);
362
        }
363

            
364
        Ok(())
365
    }
366

            
367
    /// Whether this `ProxySet` is empty.
368
    pub(crate) fn is_empty(&self) -> bool {
369
        self.proxies.lock().expect("lock poisoned").is_empty()
370
    }
371
}
372

            
373
impl<R: Runtime> crate::reload_cfg::ReconfigurableModule for ProxySet<R> {
374
    fn reconfigure(&self, new: &crate::ArtiCombinedConfig) -> anyhow::Result<()> {
375
        ProxySet::reconfigure(self, new.0.onion_services.clone())?;
376
        Ok(())
377
    }
378
}
379

            
380
#[cfg(test)]
381
mod tests {
382
    // @@ begin test lint list maintained by maint/add_warning @@
383
    #![allow(clippy::bool_assert_comparison)]
384
    #![allow(clippy::clone_on_copy)]
385
    #![allow(clippy::dbg_macro)]
386
    #![allow(clippy::mixed_attributes_style)]
387
    #![allow(clippy::print_stderr)]
388
    #![allow(clippy::print_stdout)]
389
    #![allow(clippy::single_char_pattern)]
390
    #![allow(clippy::unwrap_used)]
391
    #![allow(clippy::unchecked_time_subtraction)]
392
    #![allow(clippy::useless_vec)]
393
    #![allow(clippy::needless_pass_by_value)]
394
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
395
    use super::*;
396

            
397
    use tor_config::ConfigBuildError;
398
    use tor_hsservice::HsNickname;
399

            
400
    /// Get an [`OnionServiceProxyConfig`] with its `svc_cfg` field having the nickname `nick`.
401
    fn get_onion_service_proxy_config(nick: &HsNickname) -> OnionServiceProxyConfig {
402
        let mut builder = OnionServiceProxyConfigBuilder::default();
403
        builder.service().nickname(nick.clone());
404
        builder.build().unwrap()
405
    }
406

            
407
    /// Test `super::build_list` with unique and duplicate [`HsNickname`]s.
408
    #[test]
409
    fn fn_build_list() {
410
        let nick_1 = HsNickname::new("nick_1".to_string()).unwrap();
411
        let nick_2 = HsNickname::new("nick_2".to_string()).unwrap();
412

            
413
        let proxy_configs: Vec<OnionServiceProxyConfig> = [&nick_1, &nick_2]
414
            .into_iter()
415
            .map(get_onion_service_proxy_config)
416
            .collect();
417
        let actual = build_list(proxy_configs.clone()).unwrap();
418

            
419
        let expected =
420
            OnionServiceProxyConfigMap::from_iter([nick_1, nick_2].into_iter().zip(proxy_configs));
421

            
422
        assert_eq!(actual, expected);
423

            
424
        let nick = HsNickname::new("nick".to_string()).unwrap();
425
        let proxy_configs_dup: Vec<OnionServiceProxyConfig> = [&nick, &nick]
426
            .into_iter()
427
            .map(get_onion_service_proxy_config)
428
            .collect();
429
        let actual = build_list(proxy_configs_dup).unwrap_err();
430
        let ConfigBuildError::Inconsistent { fields, problem } = actual else {
431
            panic!("Unexpected error from `build_list`: {actual:?}");
432
        };
433

            
434
        assert_eq!(fields, vec!["nickname".to_string()]);
435
        assert_eq!(
436
            problem,
437
            format!("Multiple onion services with the nickname {nick}")
438
        );
439
    }
440
}