1use 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#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct OnionServiceProxyConfig {
29 pub(crate) svc_cfg: OnionServiceConfig,
31 pub(crate) proxy_cfg: ProxyConfig,
34}
35
36#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
42#[serde(transparent)]
43pub struct OnionServiceProxyConfigBuilder(Flatten<OnionServiceConfigBuilder, ProxyConfigBuilder>);
44
45impl OnionServiceProxyConfigBuilder {
46 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 pub fn service(&mut self) -> &mut OnionServiceConfigBuilder {
57 &mut self.0 .0
58 }
59
60 pub fn proxy(&mut self) -> &mut ProxyConfigBuilder {
62 &mut self.0 .1
63 }
64}
65
66impl_standard_builder! { OnionServiceProxyConfig: !Default }
67
68#[cfg(feature = "onion-service-service")]
70pub(crate) type OnionServiceProxyConfigMap = BTreeMap<HsNickname, OnionServiceProxyConfig>;
71
72type ProxyBuilderMap = BTreeMap<HsNickname, OnionServiceProxyConfigBuilder>;
75
76#[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
89fn build_list(
92 services: Vec<OnionServiceProxyConfig>,
93) -> Result<OnionServiceProxyConfigMap, ConfigBuildError> {
94 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 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#[must_use = "a hidden service Proxy object will terminate the service when dropped"]
164struct Proxy {
165 svc: Arc<RunningOnionService>,
169 proxy: Arc<OnionServiceReverseProxy>,
173}
174
175impl Proxy {
176 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 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 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#[must_use = "a hidden service ProxySet object will terminate the services when dropped"]
264pub(crate) struct ProxySet<R: Runtime> {
265 client: arti_client::TorClient<R>,
267 proxies: Mutex<BTreeMap<HsNickname, Proxy>>,
269}
270
271impl<R: Runtime> ProxySet<R> {
272 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 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 pub(crate) fn reconfigure(
301 &self,
302 new_config: OnionServiceProxyConfigMap,
303 ) -> Result<(), anyhow::Error> {
306 let mut proxy_map = self.proxies.lock().expect("lock poisoned");
307
308 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 defunct_nicknames.remove(&nickname);
316
317 match proxy_map.entry(nickname) {
318 Entry::Occupied(mut existing_proxy) => {
319 existing_proxy
322 .get_mut()
323 .reconfigure(cfg, Reconfigure::WarnOnFailures)?;
324 }
325 Entry::Vacant(ent) => {
326 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 let defunct_proxy = proxy_map
350 .remove(&nickname)
351 .expect("Somehow a proxy disappeared from the map");
352 drop(defunct_proxy);
354 }
355
356 Ok(())
357 }
358
359 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 #![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 use super::*;
388
389 use tor_config::ConfigBuildError;
390 use tor_hsservice::HsNickname;
391
392 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]
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}