1use std::{
4 collections::{BTreeMap, HashSet, btree_map::Entry},
5 sync::{Arc, Mutex},
6};
7
8use arti_client::config::onion_service::{OnionServiceConfig, OnionServiceConfigBuilder};
9use futures::StreamExt as _;
10use tor_config::{
11 ConfigBuildError, Flatten, Reconfigure, ReconfigureError, define_list_builder_helper,
12 impl_standard_builder,
13};
14use tor_error::warn_report;
15use tor_hsrproxy::{OnionServiceReverseProxy, ProxyConfig, config::ProxyConfigBuilder};
16use tor_hsservice::{HsNickname, RunningOnionService};
17use tor_rtcompat::{Runtime, SpawnExt};
18use tracing::debug;
19
20#[derive(Clone, Debug, Eq, PartialEq)]
28#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
29pub(crate) struct OnionServiceProxyConfig {
30 pub(crate) svc_cfg: OnionServiceConfig,
32 pub(crate) proxy_cfg: ProxyConfig,
35}
36
37#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
43#[serde(transparent)]
44#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
45pub(crate) struct OnionServiceProxyConfigBuilder(
46 Flatten<OnionServiceConfigBuilder, ProxyConfigBuilder>,
47);
48
49impl OnionServiceProxyConfigBuilder {
50 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
54 pub(crate) fn build(&self) -> Result<OnionServiceProxyConfig, ConfigBuildError> {
55 let svc_cfg = self.0.0.build()?;
56 let proxy_cfg = self.0.1.build()?;
57 Ok(OnionServiceProxyConfig { svc_cfg, proxy_cfg })
58 }
59
60 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
62 pub(crate) fn service(&mut self) -> &mut OnionServiceConfigBuilder {
63 &mut self.0.0
64 }
65
66 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
68 pub(crate) fn proxy(&mut self) -> &mut ProxyConfigBuilder {
69 &mut self.0.1
70 }
71}
72
73impl_standard_builder! { OnionServiceProxyConfig: !Default }
74
75#[cfg(feature = "onion-service-service")]
77pub(crate) type OnionServiceProxyConfigMap = BTreeMap<HsNickname, OnionServiceProxyConfig>;
78
79type ProxyBuilderMap = BTreeMap<HsNickname, OnionServiceProxyConfigBuilder>;
82
83#[cfg(feature = "onion-service-service")]
87define_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
97fn build_list(
100 services: Vec<OnionServiceProxyConfig>,
101) -> Result<OnionServiceProxyConfigMap, ConfigBuildError> {
102 let mut map = BTreeMap::new();
108 for service in services {
109 if let Some(previous_value) = map.insert(service.svc_cfg.nickname().clone(), service) {
110 return Err(ConfigBuildError::Inconsistent {
111 fields: vec!["nickname".into()],
112 problem: format!(
113 "Multiple onion services with the nickname {}",
114 previous_value.svc_cfg.nickname()
115 ),
116 });
117 };
118 }
119 Ok(map)
120}
121
122impl TryFrom<ProxyBuilderMap> for OnionServiceProxyConfigMapBuilder {
123 type Error = ConfigBuildError;
124
125 fn try_from(value: ProxyBuilderMap) -> Result<Self, Self::Error> {
126 let mut list_builder = OnionServiceProxyConfigMapBuilder::default();
127 for (nickname, mut cfg) in value {
128 match cfg.0.0.peek_nickname() {
129 Some(n) if n == &nickname => (),
130 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 cfg.0.0.nickname(nickname);
139 list_builder.access().push(cfg);
140 }
141 Ok(list_builder)
142 }
143}
144
145impl From<OnionServiceProxyConfigMapBuilder> for ProxyBuilderMap {
146 fn from(value: OnionServiceProxyConfigMapBuilder) -> Self {
153 let mut map = BTreeMap::new();
154 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 map
164 }
165}
166
167#[must_use = "a hidden service Proxy object will terminate the service when dropped"]
172struct Proxy {
173 svc: Arc<RunningOnionService>,
177 proxy: Arc<OnionServiceReverseProxy>,
181}
182
183impl Proxy {
184 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 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 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#[must_use = "a hidden service ProxySet object will terminate the services when dropped"]
272pub(crate) struct ProxySet<R: Runtime> {
273 client: Arc<arti_client::TorClient<R>>,
275 proxies: Mutex<BTreeMap<HsNickname, Proxy>>,
277}
278
279impl<R: Runtime> ProxySet<R> {
280 pub(crate) fn new_deferred(client: Arc<arti_client::TorClient<R>>) -> Self {
285 Self {
286 client,
287 proxies: Mutex::new(BTreeMap::new()),
288 }
289 }
290
291 pub(crate) fn launch_new(
293 client: Arc<arti_client::TorClient<R>>,
294 config_list: OnionServiceProxyConfigMap,
295 ) -> anyhow::Result<Self> {
296 let proxies: BTreeMap<_, _> = config_list
297 .into_iter()
298 .filter_map(|(nickname, cfg)| {
299 match Proxy::launch_new(&client, cfg) {
301 Ok(Some(running_service)) => Some(Ok((nickname, running_service))),
302 Err(error) => Some(Err(error)),
303 Ok(None) => None,
304 }
305 })
306 .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
307
308 Ok(Self {
309 client,
310 proxies: Mutex::new(proxies),
311 })
312 }
313
314 pub(crate) fn reconfigure(
320 &self,
321 new_config: OnionServiceProxyConfigMap,
322 ) -> Result<(), anyhow::Error> {
325 let mut proxy_map = self.proxies.lock().expect("lock poisoned");
326
327 let mut defunct_nicknames: HashSet<_> = proxy_map.keys().map(Clone::clone).collect();
329
330 for cfg in new_config.into_values() {
331 let nickname = cfg.svc_cfg.nickname().clone();
332 defunct_nicknames.remove(&nickname);
335
336 match proxy_map.entry(nickname) {
337 Entry::Occupied(mut existing_proxy) => {
338 existing_proxy
341 .get_mut()
342 .reconfigure(cfg, Reconfigure::WarnOnFailures)?;
343 }
344 Entry::Vacant(ent) => {
345 match Proxy::launch_new(&self.client, cfg) {
348 Ok(Some(new_proxy)) => {
349 ent.insert(new_proxy);
350 }
351 Ok(None) => {
352 debug!(
353 "Onion service {} didn't start (disabled in config)",
354 ent.key()
355 );
356 }
357 Err(err) => {
358 warn_report!(err, "Unable to launch onion service {}", ent.key());
359 }
360 }
361 }
362 }
363 }
364
365 for nickname in defunct_nicknames {
366 let defunct_proxy = proxy_map
369 .remove(&nickname)
370 .expect("Somehow a proxy disappeared from the map");
371 drop(defunct_proxy);
373 }
374
375 Ok(())
376 }
377
378 pub(crate) fn is_empty(&self) -> bool {
380 self.proxies.lock().expect("lock poisoned").is_empty()
381 }
382}
383
384impl<R: Runtime> crate::reload_cfg::ReconfigurableModule for ProxySet<R> {
385 fn reconfigure(&self, new: &crate::ArtiCombinedConfig) -> anyhow::Result<()> {
386 if new.0.application().defer_bootstrap {
387 return Ok(());
390 }
391
392 ProxySet::reconfigure(self, new.0.onion_services.clone())?;
393 Ok(())
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 #![allow(clippy::bool_assert_comparison)]
401 #![allow(clippy::clone_on_copy)]
402 #![allow(clippy::dbg_macro)]
403 #![allow(clippy::mixed_attributes_style)]
404 #![allow(clippy::print_stderr)]
405 #![allow(clippy::print_stdout)]
406 #![allow(clippy::single_char_pattern)]
407 #![allow(clippy::unwrap_used)]
408 #![allow(clippy::unchecked_time_subtraction)]
409 #![allow(clippy::useless_vec)]
410 #![allow(clippy::needless_pass_by_value)]
411 #![allow(clippy::string_slice)] use super::*;
414
415 use tor_config::ConfigBuildError;
416 use tor_hsservice::HsNickname;
417
418 fn get_onion_service_proxy_config(nick: &HsNickname) -> OnionServiceProxyConfig {
420 let mut builder = OnionServiceProxyConfigBuilder::default();
421 builder.service().nickname(nick.clone());
422 builder.build().unwrap()
423 }
424
425 #[test]
427 fn fn_build_list() {
428 let nick_1 = HsNickname::new("nick_1".to_string()).unwrap();
429 let nick_2 = HsNickname::new("nick_2".to_string()).unwrap();
430
431 let proxy_configs: Vec<OnionServiceProxyConfig> = [&nick_1, &nick_2]
432 .into_iter()
433 .map(get_onion_service_proxy_config)
434 .collect();
435 let actual = build_list(proxy_configs.clone()).unwrap();
436
437 let expected =
438 OnionServiceProxyConfigMap::from_iter([nick_1, nick_2].into_iter().zip(proxy_configs));
439
440 assert_eq!(actual, expected);
441
442 let nick = HsNickname::new("nick".to_string()).unwrap();
443 let proxy_configs_dup: Vec<OnionServiceProxyConfig> = [&nick, &nick]
444 .into_iter()
445 .map(get_onion_service_proxy_config)
446 .collect();
447 let actual = build_list(proxy_configs_dup).unwrap_err();
448 let ConfigBuildError::Inconsistent { fields, problem } = actual else {
449 panic!("Unexpected error from `build_list`: {actual:?}");
450 };
451
452 assert_eq!(fields, vec!["nickname".to_string()]);
453 assert_eq!(
454 problem,
455 format!("Multiple onion services with the nickname {nick}")
456 );
457 }
458}