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
20use crate::reload_cfg::ReconfigurableModule;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
30#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
31pub(crate) struct OnionServiceProxyConfig {
32 pub(crate) svc_cfg: OnionServiceConfig,
34 pub(crate) proxy_cfg: ProxyConfig,
37}
38
39#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
45#[serde(transparent)]
46#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
47pub(crate) struct OnionServiceProxyConfigBuilder(
48 Flatten<OnionServiceConfigBuilder, ProxyConfigBuilder>,
49);
50
51impl OnionServiceProxyConfigBuilder {
52 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
56 pub(crate) fn build(&self) -> Result<OnionServiceProxyConfig, ConfigBuildError> {
57 let svc_cfg = self.0.0.build()?;
58 let proxy_cfg = self.0.1.build()?;
59 Ok(OnionServiceProxyConfig { svc_cfg, proxy_cfg })
60 }
61
62 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
64 pub(crate) fn service(&mut self) -> &mut OnionServiceConfigBuilder {
65 &mut self.0.0
66 }
67
68 #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
70 pub(crate) fn proxy(&mut self) -> &mut ProxyConfigBuilder {
71 &mut self.0.1
72 }
73}
74
75impl_standard_builder! { OnionServiceProxyConfig: !Default }
76
77#[cfg(feature = "onion-service-service")]
79pub(crate) type OnionServiceProxyConfigMap = BTreeMap<HsNickname, OnionServiceProxyConfig>;
80
81type ProxyBuilderMap = BTreeMap<HsNickname, OnionServiceProxyConfigBuilder>;
84
85#[cfg(feature = "onion-service-service")]
89define_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
99fn build_list(
102 services: Vec<OnionServiceProxyConfig>,
103) -> Result<OnionServiceProxyConfigMap, ConfigBuildError> {
104 let mut map = BTreeMap::new();
110 for service in services {
111 if let Some(previous_value) = map.insert(service.svc_cfg.nickname().clone(), service) {
112 return Err(ConfigBuildError::Inconsistent {
113 fields: vec!["nickname".into()],
114 problem: format!(
115 "Multiple onion services with the nickname {}",
116 previous_value.svc_cfg.nickname()
117 ),
118 });
119 };
120 }
121 Ok(map)
122}
123
124impl TryFrom<ProxyBuilderMap> for OnionServiceProxyConfigMapBuilder {
125 type Error = ConfigBuildError;
126
127 fn try_from(value: ProxyBuilderMap) -> Result<Self, Self::Error> {
128 let mut list_builder = OnionServiceProxyConfigMapBuilder::default();
129 for (nickname, mut cfg) in value {
130 match cfg.0.0.peek_nickname() {
131 Some(n) if n == &nickname => (),
132 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 cfg.0.0.nickname(nickname);
141 list_builder.access().push(cfg);
142 }
143 Ok(list_builder)
144 }
145}
146
147impl From<OnionServiceProxyConfigMapBuilder> for ProxyBuilderMap {
148 fn from(value: OnionServiceProxyConfigMapBuilder) -> Self {
155 let mut map = BTreeMap::new();
156 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 map
166 }
167}
168
169#[must_use = "a hidden service Proxy object will terminate the service when dropped"]
174struct Proxy {
175 svc: Arc<RunningOnionService>,
179 proxy: Arc<OnionServiceReverseProxy>,
183}
184
185impl Proxy {
186 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 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 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#[must_use = "a hidden service ProxySet object will terminate the services when dropped"]
274pub(crate) struct ProxySet<R: Runtime> {
275 client: Arc<arti_client::TorClient<R>>,
277 proxies: Mutex<BTreeMap<HsNickname, Proxy>>,
279}
280
281impl<R: Runtime> ProxySet<R> {
282 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 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 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 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 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 defunct_nicknames.remove(&nickname);
341
342 match proxy_map.entry(nickname) {
343 Entry::Occupied(mut existing_proxy) => {
344 existing_proxy.get_mut().reconfigure(cfg, how)?;
347 }
348 Entry::Vacant(ent) => {
349 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 let defunct_proxy = proxy_map
376 .remove(&nickname)
377 .expect("Somehow a proxy disappeared from the map");
378 drop(defunct_proxy);
380 }
381 }
382
383 Ok(())
384 }
385
386 pub(crate) fn is_empty(&self) -> bool {
388 self.proxies.lock().expect("lock poisoned").is_empty()
389 }
390}
391
392impl<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 return Ok(());
402 }
403
404 ProxySet::reconfigure(self, new.0.onion_services.clone(), how)?;
405 Ok(())
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 #![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)] use super::*;
426
427 use tor_config::ConfigBuildError;
428 use tor_hsservice::HsNickname;
429
430 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]
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}