tachyon_web/server/tor.rs
1//! Native Tor `.onion` hidden-service support (see the `tor` feature).
2//!
3//! Wraps [`arti-client`](https://docs.rs/arti-client) and
4//! [`tor-hsservice`](https://docs.rs/tor-hsservice) so a Tachyon [`Server`] can be published
5//! directly as a v3 Tor hidden service — no external `tor` daemon, no reverse proxy — with the
6//! same `serve_*` ergonomics as [`Server::serve_https`](crate::server::Server::serve_https).
7//!
8//! Two entry points are available:
9//!
10//! - [`Server::serve_tor`] / [`Server::serve_tor_with_client`] — the simplest possible onion
11//! service: plaintext HTTP on virtual port 80, nothing else configurable. Always available
12//! under the `tor` feature alone — no TLS stack required.
13//! - [`Server::serve_onion`] / [`Server::serve_onion_with_client`], driven by an [`OnionConfig`] —
14//! adds custom state/cache directories, a vanguards toggle, and an `on_ready` hook for reading
15//! the published `.onion` address, all available under `tor` alone. Native HTTPS (virtual port
16//! 443, terminated with the *same* `rustls::ServerConfig` type used by
17//! [`Server::serve_https_config`](crate::server::Server::serve_https_config), so a
18//! FIPS-constrained crypto provider or custom cert chain can be shared between the clearnet and
19//! onion listeners) is additionally available when the `tls` feature is enabled alongside
20//! `tor` — the self-signed-certificate convenience ([`OnionConfig::self_signed_tls`], the
21//! default whenever it's available) further requires `cert-gen`.
22//!
23//! # Example
24//!
25//! ```rust,no_run
26//! use tachyon_web::{Router, Server, get};
27//!
28//! async fn hello() -> &'static str { "Hello from an onion service!" }
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
32//! let app = Router::new().route("/", get(hello));
33//!
34//! // Publishes the service, prints its `.onion` address once reachable, then
35//! // blocks serving requests arriving over Tor rendezvous circuits.
36//! Server::new(app).serve_tor("my-hidden-service").await?;
37//! Ok(())
38//! }
39//! ```
40//!
41//! # HTTPS, custom directories, and vanguards
42//!
43//! HTTPS support (this example) requires enabling `tls` (and `cert-gen` for the self-signed
44//! certificate shown here) alongside `tor` — see the [module docs](self) above.
45//!
46//! ```rust,no_run
47//! use tachyon_web::{Router, Server, get};
48//! use tachyon_web::server::tor::OnionConfig;
49//!
50//! async fn hello() -> &'static str { "Hello, secure onion world!" }
51//!
52//! #[tokio::main]
53//! async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
54//! let app = Router::new().route("/", get(hello));
55//!
56//! let config = OnionConfig::new("my-hidden-service")
57//! .state_dir("/var/lib/tachyon/tor/state")
58//! .cache_dir("/var/lib/tachyon/tor/cache")
59//! // Default (with `cert-gen` enabled) is a self-signed cert for the onion address;
60//! // pass your own instead:
61//! // .tls_config(my_rustls_server_config)
62//! .redirect_http(false) // dual-stack by default: plaintext AND TLS both work
63//! .on_ready(|addr| tracing::info!("reachable at https://{addr}"));
64//!
65//! Server::new(app).serve_onion(config).await?;
66//! Ok(())
67//! }
68//! ```
69//!
70//! Persistent onion service keys and Arti's own state/cache are stored under Arti's default
71//! state directory unless overridden via [`OnionConfig::state_dir`]/[`OnionConfig::cache_dir`];
72//! reusing the same `nickname` (and directories) across restarts keeps the same `.onion` address.
73//! Pass an already-bootstrapped client (e.g. one configured with custom bridges) via
74//! [`Server::serve_onion_with_client`]/[`Server::serve_tor_with_client`] instead of bootstrapping
75//! a fresh one per service.
76//!
77//! # Vanguards
78//!
79//! [Vanguards](https://blog.torproject.org/vanguards-onion-services/) harden onion services
80//! against guard-discovery attacks and are enabled by default (arti's own "lite" mode).
81//! [`OnionConfig::vanguards`] overrides this at runtime, e.g. `OnionConfig::new(nickname).vanguards(false)`.
82
83#[cfg(feature = "tls")]
84use crate::http::response::Body;
85use crate::server::Server;
86use crate::server::conn::{NO_PEER_ADDR as ONION_PEER_ADDR, serve_connection};
87use crate::server::http::hyper_handler;
88use arti_client::config::{CfgPath, TorClientConfigBuilder};
89use arti_client::{TorClient, TorClientConfig};
90use futures_util::StreamExt as _;
91#[cfg(feature = "tls")]
92use hyper::{Request, Response};
93use safelog::DisplayRedacted as _;
94use std::path::PathBuf;
95use std::sync::Arc;
96#[cfg(feature = "tls")]
97use tokio_rustls::TlsAcceptor;
98use tor_cell::relaycell::msg::Connected;
99use tor_config::ExplicitOrAuto;
100use tor_guardmgr::VanguardMode;
101use tor_hsservice::config::OnionServiceConfigBuilder;
102use tor_hsservice::{HsNickname, StreamRequest};
103use tor_proto::stream::IncomingStreamRequest;
104use tor_rtcompat::Runtime;
105
106/// The virtual port plaintext HTTP clients connect to — mirrors how a clearnet browser assumes
107/// port 80 for a bare `http://` URL, regardless of what port the service actually listens on
108/// inside the Tor network.
109const ONION_HTTP_PORT: u16 = 80;
110/// The virtual port HTTPS clients connect to, matching the clearnet `https://` convention.
111/// Only meaningful when the `tls` feature is enabled.
112#[cfg(feature = "tls")]
113const ONION_HTTPS_PORT: u16 = 443;
114
115/// How (or whether) an onion service published via [`OnionConfig`] terminates TLS.
116#[derive(Clone)]
117enum OnionTls {
118 /// Plaintext only — no virtual port 443 listener.
119 None,
120 /// TLS on virtual port 443 using an ephemeral self-signed certificate, generated for the
121 /// onion address once it's known. This is the default whenever `cert-gen` is enabled.
122 /// Requires the `cert-gen` feature.
123 #[cfg(feature = "cert-gen")]
124 SelfSigned,
125 /// TLS on virtual port 443 using a caller-supplied config — e.g. the same
126 /// `rustls::ServerConfig` used for a clearnet [`Server::serve_https_config`] listener, so a
127 /// custom crypto provider or FIPS constraints carry over unchanged. Requires the `tls`
128 /// feature.
129 #[cfg(feature = "tls")]
130 Custom(Arc<rustls::ServerConfig>),
131}
132
133impl std::fmt::Debug for OnionTls {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 Self::None => f.write_str("None"),
137 #[cfg(feature = "cert-gen")]
138 Self::SelfSigned => f.write_str("SelfSigned"),
139 #[cfg(feature = "tls")]
140 Self::Custom(_) => f.write_str("Custom(..)"),
141 }
142 }
143}
144
145/// Callback invoked with the published `.onion` address — see [`OnionConfig::on_ready`].
146type OnReadyHook = Box<dyn FnOnce(&str) + Send>;
147
148/// Configuration for publishing a Tor `.onion` hidden service via
149/// [`Server::serve_onion`]/[`Server::serve_onion_with_client`].
150///
151/// See the [module docs](self) for a full example.
152pub struct OnionConfig {
153 nickname: String,
154 state_dir: Option<PathBuf>,
155 cache_dir: Option<PathBuf>,
156 tls: OnionTls,
157 redirect_http: bool,
158 vanguards: bool,
159 on_ready: Option<OnReadyHook>,
160}
161
162impl std::fmt::Debug for OnionConfig {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("OnionConfig")
165 .field("nickname", &self.nickname)
166 .field("state_dir", &self.state_dir)
167 .field("cache_dir", &self.cache_dir)
168 .field("tls", &self.tls)
169 .field("redirect_http", &self.redirect_http)
170 .field("vanguards", &self.vanguards)
171 .finish_non_exhaustive()
172 }
173}
174
175impl OnionConfig {
176 /// Creates a new configuration for a service published under `nickname`.
177 ///
178 /// Defaults: TLS enabled with a self-signed certificate if the `cert-gen` feature is
179 /// enabled (virtual port 443, alongside plaintext on virtual port 80 — see
180 /// [`redirect_http`](Self::redirect_http)); plaintext-only otherwise (or always, if the
181 /// `tls` feature isn't enabled at all — see the [module docs](self)). No forced
182 /// HTTP→HTTPS redirect, and vanguards on — see [`vanguards`](Self::vanguards) to change it.
183 /// `nickname` is validated (as an [`HsNickname`]) when the service is actually launched.
184 #[must_use]
185 pub fn new(nickname: impl Into<String>) -> Self {
186 Self {
187 nickname: nickname.into(),
188 state_dir: None,
189 cache_dir: None,
190 #[cfg(feature = "cert-gen")]
191 tls: OnionTls::SelfSigned,
192 #[cfg(not(feature = "cert-gen"))]
193 tls: OnionTls::None,
194 redirect_http: false,
195 vanguards: true,
196 on_ready: None,
197 }
198 }
199
200 /// Overrides the directory Arti uses for persistent state — including this service's onion
201 /// keys. Reusing the same directory (and `nickname`) across restarts keeps the same `.onion`
202 /// address. Defaults to Arti's own platform-specific state directory.
203 #[must_use]
204 pub fn state_dir(mut self, dir: impl Into<PathBuf>) -> Self {
205 self.state_dir = Some(dir.into());
206 self
207 }
208
209 /// Overrides the directory Arti uses for cached network directory information. Defaults to
210 /// Arti's own platform-specific cache directory.
211 #[must_use]
212 pub fn cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
213 self.cache_dir = Some(dir.into());
214 self
215 }
216
217 /// Disables HTTPS entirely — only plaintext HTTP on virtual port 80 is served, matching
218 /// [`Server::serve_tor`].
219 // Only `const`-eligible when neither `Custom`/`SelfSigned` variant exists (their non-trivial
220 // `Drop` glue — an `Arc<rustls::ServerConfig>` — can't run in a `const fn`), i.e. only
221 // without `tls`/`cert-gen` — not worth splitting this method's signature across features
222 // for.
223 #[cfg_attr(not(feature = "tls"), allow(clippy::missing_const_for_fn))]
224 #[must_use]
225 pub fn no_tls(mut self) -> Self {
226 self.tls = OnionTls::None;
227 self
228 }
229
230 /// Re-enables HTTPS with a freshly generated self-signed certificate (the default when this
231 /// feature is available), after a prior [`no_tls`](Self::no_tls) or
232 /// [`tls_config`](Self::tls_config) call. Requires the `cert-gen` feature.
233 #[cfg(feature = "cert-gen")]
234 #[must_use]
235 pub fn self_signed_tls(mut self) -> Self {
236 self.tls = OnionTls::SelfSigned;
237 self
238 }
239
240 /// Enables HTTPS using a caller-supplied `rustls::ServerConfig` instead of the default
241 /// self-signed certificate — for example, the exact same config passed to
242 /// [`Server::serve_https_config`](crate::server::Server::serve_https_config) for the clearnet
243 /// listener, so a custom crypto provider, FIPS constraints, or a real cert chain carry over
244 /// unchanged. Requires the `tls` feature.
245 #[cfg(feature = "tls")]
246 #[must_use]
247 pub fn tls_config(mut self, config: rustls::ServerConfig) -> Self {
248 self.tls = OnionTls::Custom(Arc::new(config));
249 self
250 }
251
252 /// Controls what happens to plaintext HTTP (virtual port 80) requests when TLS is enabled.
253 ///
254 /// `false` (the default): plaintext and TLS are both served — a dual-stack onion service,
255 /// same as browsing a clearnet site over either `http://` or `https://`. `true`: port 80
256 /// instead issues a `308 Permanent Redirect` to the equivalent `https://` URL, forcing all
257 /// traffic onto TLS. Has no effect when TLS is disabled ([`no_tls`](Self::no_tls)) or the
258 /// `tls` feature isn't enabled.
259 #[must_use]
260 pub const fn redirect_http(mut self, enable: bool) -> Self {
261 self.redirect_http = enable;
262 self
263 }
264
265 /// Controls whether [vanguards](https://blog.torproject.org/vanguards-onion-services/) are
266 /// used for this service. Defaults to `true`.
267 #[must_use]
268 pub const fn vanguards(mut self, enabled: bool) -> Self {
269 self.vanguards = enabled;
270 self
271 }
272
273 /// Registers a callback invoked exactly once — with the published `.onion` address (no
274 /// scheme, e.g. `"abcd...xyz.onion"`) — as soon as the service is fully reachable, just
275 /// before requests start being served. This is the only way to observe the address
276 /// programmatically, since [`serve_onion`](Server::serve_onion) blocks for the lifetime of
277 /// the service; the address is also always logged via `tracing` at `info` level.
278 #[must_use]
279 pub fn on_ready(mut self, f: impl FnOnce(&str) + Send + 'static) -> Self {
280 self.on_ready = Some(Box::new(f));
281 self
282 }
283
284 /// The nickname this service will be published under.
285 #[must_use]
286 pub fn nickname(&self) -> &str {
287 &self.nickname
288 }
289
290 /// Whether HTTPS is enabled (via the default self-signed cert or a custom
291 /// [`tls_config`](Self::tls_config)) — `false` after [`no_tls`](Self::no_tls), and always
292 /// `false` when the `tls` feature isn't enabled.
293 #[must_use]
294 pub const fn tls_enabled(&self) -> bool {
295 !matches!(self.tls, OnionTls::None)
296 }
297
298 /// Whether plaintext HTTP is forced to redirect to HTTPS — see
299 /// [`redirect_http`](Self::redirect_http).
300 #[must_use]
301 pub const fn redirect_http_enabled(&self) -> bool {
302 self.redirect_http
303 }
304
305 /// Whether vanguards will be requested for this service — see
306 /// [`vanguards`](Self::vanguards).
307 #[must_use]
308 pub const fn vanguards_enabled(&self) -> bool {
309 self.vanguards
310 }
311}
312
313impl<S> Server<S>
314where
315 S: Clone + Send + Sync + 'static,
316{
317 /// Publishes this router as a Tor `.onion` hidden service and serves requests arriving
318 /// over it, blocking until the service stops.
319 ///
320 /// Bootstraps a fresh [`TorClient`] with [`TorClientConfig::default`] — this alone can
321 /// take from several seconds up to a minute or more, since it involves connecting to and
322 /// syncing with the live Tor network — then behaves like
323 /// [`serve_tor_with_client`](Server::serve_tor_with_client). Reuse a [`TorClient`] across
324 /// calls (via `serve_tor_with_client`) rather than bootstrapping one per service.
325 ///
326 /// This is the plaintext-only entry point (virtual port 80 only, no HTTPS, no
327 /// configuration) — available under the `tor` feature alone, no TLS stack required. For
328 /// native onion HTTPS (needs the `tls` feature too), custom state/cache directories, or the
329 /// other [`OnionConfig`] options, use [`serve_onion`](Server::serve_onion) instead.
330 ///
331 /// # Errors
332 /// Returns an error if the Tor client fails to bootstrap, `nickname` is not a valid
333 /// [`HsNickname`], or the onion service fails to launch.
334 pub async fn serve_tor(
335 self,
336 nickname: &str,
337 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
338 // Install this server's crypto/TLS policy as rustls's process-wide default *before*
339 // bootstrapping — arti reads this global default for its own relay/channel TLS
340 // connections (it has no API to accept a `ClientConfig` directly). See `TlsPolicy`'s
341 // docs. Idempotent: a no-op if something already installed a default. Only relevant
342 // (and only compiled) when this crate's own `tls` feature is enabled — without it,
343 // arti simply falls back to whatever crypto provider it installs on its own.
344 #[cfg(feature = "tls")]
345 self.effective_tls_policy().install_as_process_default();
346 let client = TorClient::create_bootstrapped(TorClientConfig::default()).await?;
347 self.serve_tor_with_client(&client, nickname).await
348 }
349
350 /// Publishes this router as a Tor `.onion` hidden service using an already-bootstrapped
351 /// [`TorClient`], and serves requests arriving over it, blocking until the service stops.
352 ///
353 /// Only rendezvous requests targeting virtual port 80 are accepted (the port every `.onion`
354 /// HTTP client expects); anything else has its circuit shut down immediately. Requests are
355 /// dispatched through the same handling pipeline as [`serve_http`](Server::serve_http) —
356 /// HTTP/1.1, plus h2c with the `http2` feature — one Tokio task per stream.
357 ///
358 /// Since `client` is already bootstrapped, arti has already constructed its internal
359 /// relay/channel TLS provider from whatever `rustls::crypto::CryptoProvider` was installed
360 /// process-wide *before this call* — install one yourself (e.g.
361 /// `server.effective_tls_policy()`, or simply `TlsPolicy::hardened().install_as_process_default()`,
362 /// both requiring this crate's `tls` feature) before bootstrapping `client` if that matters
363 /// to you; it's too late to affect `client` by the time this function runs.
364 /// [`serve_tor`](Server::serve_tor) does this for you because it owns the bootstrap.
365 ///
366 /// # Errors
367 /// Returns an error if `nickname` is not a valid [`HsNickname`], or the onion service
368 /// fails to launch (for example, if onion services are disabled in `client`'s config).
369 pub async fn serve_tor_with_client<R>(
370 self,
371 client: &TorClient<R>,
372 nickname: &str,
373 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
374 where
375 R: Runtime,
376 {
377 crate::server::enforce_fips_compliance()?;
378
379 let hs_nickname = parse_nickname(nickname)?;
380 let svc_cfg = OnionServiceConfigBuilder::default()
381 .nickname(hs_nickname)
382 .build()?;
383
384 let Some((service, request_stream)) = client.launch_onion_service(svc_cfg)? else {
385 return Err("onion services are disabled in this TorClient's config".into());
386 };
387
388 if let Some(addr) = service.onion_address() {
389 tracing::info!(
390 "[tor] onion service published at {}",
391 addr.display_unredacted()
392 );
393 }
394
395 wait_until_reachable(&service).await;
396
397 let state = Arc::new(self);
398 let stream_requests = tor_hsservice::handle_rend_requests(request_stream);
399 tokio::pin!(stream_requests);
400
401 while let Some(stream_request) = stream_requests.next().await {
402 let state = state.clone();
403 drop(tokio::spawn(async move {
404 if let Err(e) = handle_plaintext_only_stream(state, stream_request).await {
405 tracing::debug!("[tor] connection error: {e}");
406 }
407 }));
408 }
409
410 drop(service);
411 Ok(())
412 }
413
414 /// Publishes this router as a Tor `.onion` hidden service according to `config`
415 /// (state/cache directories, vanguards) and serves requests arriving over it, blocking
416 /// until the service stops. Bootstraps a fresh [`TorClient`] — see the bootstrap-time note
417 /// on [`serve_tor`](Server::serve_tor); prefer [`serve_onion_with_client`](Server::serve_onion_with_client)
418 /// to reuse one across services.
419 ///
420 /// # Errors
421 /// Returns an error if the Tor client fails to bootstrap, `config.nickname` is not a valid
422 /// [`HsNickname`], or the onion service fails to launch.
423 pub async fn serve_onion(
424 self,
425 config: OnionConfig,
426 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
427 let mut builder = TorClientConfigBuilder::default();
428
429 if let Some(state_dir) = &config.state_dir {
430 builder
431 .storage()
432 .state_dir(CfgPath::new_literal(state_dir.clone()));
433 }
434 if let Some(cache_dir) = &config.cache_dir {
435 builder
436 .storage()
437 .cache_dir(CfgPath::new_literal(cache_dir.clone()));
438 }
439 if !config.vanguards {
440 builder
441 .vanguards()
442 .mode(ExplicitOrAuto::Explicit(VanguardMode::Disabled));
443 }
444
445 // See the equivalent comment in `serve_tor` — must happen before bootstrapping.
446 #[cfg(feature = "tls")]
447 self.effective_tls_policy().install_as_process_default();
448 let client_config = builder.build()?;
449 let client = TorClient::create_bootstrapped(client_config).await?;
450 self.serve_onion_with_client(&client, config).await
451 }
452
453 /// Publishes this router as a Tor `.onion` hidden service according to `config`, using an
454 /// already-bootstrapped [`TorClient`], and serves requests arriving over it, blocking until
455 /// the service stops.
456 ///
457 /// Dispatch depends on `config` and on which features are compiled in: virtual port 80
458 /// serves plaintext HTTP unless [`redirect_http`](OnionConfig::redirect_http) is enabled
459 /// (in which case it issues a `308` to the `https://` equivalent) — both only possible with
460 /// the `tls` feature enabled; virtual port 443 terminates TLS — self-signed by default with
461 /// `cert-gen`, or a caller-supplied config via [`tls_config`](OnionConfig::tls_config) with
462 /// just `tls` — and is only listened on if the `tls` feature is enabled and
463 /// [`no_tls`](OnionConfig::no_tls) wasn't called. Without the `tls` feature at all, this
464 /// behaves exactly like [`serve_tor_with_client`](Server::serve_tor_with_client): plaintext
465 /// on virtual port 80 only. Anything else has its circuit shut down immediately.
466 ///
467 /// Since `client` is already bootstrapped, install a `CryptoProvider` process-wide
468 /// yourself *before* bootstrapping it if you want arti's relay/channel TLS to share this
469 /// server's policy — see the equivalent note on
470 /// [`serve_tor_with_client`](Server::serve_tor_with_client). The self-signed certificate on
471 /// virtual port 443 always uses this server's [`TlsPolicy`](crate::tls::TlsPolicy)
472 /// (see [`Server::tls_policy`]), regardless of what's installed process-wide.
473 ///
474 /// # Errors
475 /// Returns an error if `config.nickname` is not a valid [`HsNickname`], the onion service
476 /// fails to launch, or (when TLS is enabled) the TLS configuration is invalid.
477 pub async fn serve_onion_with_client<R>(
478 self,
479 client: &TorClient<R>,
480 config: OnionConfig,
481 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
482 where
483 R: Runtime,
484 {
485 crate::server::enforce_fips_compliance()?;
486
487 let hs_nickname = parse_nickname(&config.nickname)?;
488 let svc_cfg = OnionServiceConfigBuilder::default()
489 .nickname(hs_nickname)
490 .build()?;
491
492 let Some((service, request_stream)) = client.launch_onion_service(svc_cfg)? else {
493 return Err("onion services are disabled in this TorClient's config".into());
494 };
495
496 let onion_host = service
497 .onion_address()
498 .map(|addr| addr.display_unredacted().to_string());
499 if let Some(host) = &onion_host {
500 tracing::info!("[tor] onion service published at {host}");
501 }
502 tracing::info!(
503 vanguards = config.vanguards,
504 tls = config.tls_enabled(),
505 "[tor] hardening posture: vanguards={}, tls={}",
506 if config.vanguards { "on" } else { "off" },
507 if config.tls_enabled() { "on" } else { "off" },
508 );
509
510 wait_until_reachable(&service).await;
511
512 if let Some(on_ready) = config.on_ready
513 && let Some(host) = &onion_host
514 {
515 on_ready(host);
516 }
517
518 #[cfg(feature = "tls")]
519 {
520 let tls_acceptor = match &config.tls {
521 OnionTls::None => None,
522 #[cfg(feature = "cert-gen")]
523 OnionTls::SelfSigned => {
524 let domain = onion_host
525 .clone()
526 .unwrap_or_else(|| "onion-service.invalid".to_string());
527 let cert = crate::tls::generate_self_signed_cert(vec![domain])?;
528 // Shares this server's crypto/TLS policy (see `Server::tls_policy`) rather
529 // than stock rustls defaults, so a hardened/FIPS/custom provider set for
530 // clearnet applies here too.
531 let server_config = self.effective_tls_policy().server_config_from_pem(
532 cert.cert_pem.as_bytes(),
533 cert.key_pem.as_bytes(),
534 )?;
535 Some(TlsAcceptor::from(Arc::new(server_config)))
536 }
537 OnionTls::Custom(server_config) => Some(TlsAcceptor::from(server_config.clone())),
538 };
539 let onion_host: Arc<str> = Arc::from(onion_host.unwrap_or_default());
540 let redirect_http = config.redirect_http;
541
542 let state = Arc::new(self);
543 let stream_requests = tor_hsservice::handle_rend_requests(request_stream);
544 tokio::pin!(stream_requests);
545
546 while let Some(stream_request) = stream_requests.next().await {
547 let state = state.clone();
548 let tls_acceptor = tls_acceptor.clone();
549 let onion_host = onion_host.clone();
550 drop(tokio::spawn(async move {
551 if let Err(e) = handle_onion_stream(
552 state,
553 stream_request,
554 tls_acceptor,
555 redirect_http,
556 onion_host,
557 )
558 .await
559 {
560 tracing::debug!("[tor] connection error: {e}");
561 }
562 }));
563 }
564
565 drop(service);
566 Ok(())
567 }
568
569 // No `tls` feature compiled in at all: `config.tls` can only ever be `OnionTls::None`
570 // (the only variant that exists in this build), so this is functionally identical to
571 // `serve_tor_with_client`'s plaintext-only dispatch.
572 #[cfg(not(feature = "tls"))]
573 {
574 let state = Arc::new(self);
575 let stream_requests = tor_hsservice::handle_rend_requests(request_stream);
576 tokio::pin!(stream_requests);
577
578 while let Some(stream_request) = stream_requests.next().await {
579 let state = state.clone();
580 drop(tokio::spawn(async move {
581 if let Err(e) = handle_plaintext_only_stream(state, stream_request).await {
582 tracing::debug!("[tor] connection error: {e}");
583 }
584 }));
585 }
586
587 drop(service);
588 Ok(())
589 }
590 }
591}
592
593/// Validates `nickname` as an [`HsNickname`], wrapping the error with the offending value —
594/// [`HsNickname::from_str`]'s own error doesn't otherwise echo it back.
595fn parse_nickname(nickname: &str) -> Result<HsNickname, Box<dyn std::error::Error + Send + Sync>> {
596 nickname
597 .parse()
598 .map_err(|e| format!("invalid onion service nickname {nickname:?}: {e}").into())
599}
600
601/// Awaits `service`'s status stream until it reports full reachability. This tracks real Tor
602/// network activity (introduction points built, descriptor accepted by `HsDirs`) with no built-in
603/// timeout, so it can legitimately take minutes on a slow or first-run bootstrap — every state
604/// transition is logged so that wait doesn't look hung.
605async fn wait_until_reachable(service: &tor_hsservice::RunningOnionService) {
606 let mut status_events = service.status_events();
607 let mut last_state = None;
608 loop {
609 let Some(status) = status_events.next().await else {
610 tracing::warn!(
611 "[tor] onion service status stream ended before reporting full reachability"
612 );
613 return;
614 };
615 let state = status.state();
616 if last_state != Some(state) {
617 tracing::info!("[tor] onion service status: {state:?}");
618 last_state = Some(state);
619 }
620 if state.is_fully_reachable() {
621 break;
622 }
623 }
624 tracing::info!("[tor] onion service is fully reachable");
625}
626
627/// What to do with an incoming onion-service rendezvous request, given the virtual port it
628/// targeted and the service's current TLS/redirect configuration. Kept as a pure function
629/// (see the `tests` module below) independent of arti's stream types so the dispatch rules can
630/// be unit-tested without a live Tor connection.
631///
632/// `Redirect`/`ServeTls` only exist when the `tls` feature is enabled — without it, an onion
633/// service can only ever be plaintext, so [`route_onion_request`] never has a reason to produce
634/// them.
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636enum OnionAction {
637 /// Shut the circuit down — not a port this service answers on.
638 Reject,
639 /// Serve the app directly over plaintext HTTP.
640 ServePlaintext,
641 /// Issue a `308 Permanent Redirect` to the `https://` equivalent. Requires the `tls`
642 /// feature.
643 #[cfg(feature = "tls")]
644 Redirect,
645 /// Perform a TLS handshake, then serve the app over it. Requires the `tls` feature.
646 #[cfg(feature = "tls")]
647 ServeTls,
648}
649
650#[cfg_attr(not(feature = "tls"), allow(unused_variables))]
651const fn route_onion_request(port: u16, tls_enabled: bool, redirect_http: bool) -> OnionAction {
652 match port {
653 #[cfg(feature = "tls")]
654 ONION_HTTP_PORT if tls_enabled && redirect_http => OnionAction::Redirect,
655 ONION_HTTP_PORT => OnionAction::ServePlaintext,
656 #[cfg(feature = "tls")]
657 ONION_HTTPS_PORT if tls_enabled => OnionAction::ServeTls,
658 _ => OnionAction::Reject,
659 }
660}
661
662/// Builds the `Location` header value for a plaintext→TLS onion redirect. Requires the `tls`
663/// feature.
664#[cfg(feature = "tls")]
665fn redirect_location(onion_host: &str, path_and_query: &str) -> String {
666 format!("https://{onion_host}{path_and_query}")
667}
668
669/// Handles a single rendezvous stream for [`Server::serve_tor_with_client`] — plaintext HTTP on
670/// virtual port 80 only, everything else rejected.
671async fn handle_plaintext_only_stream<S>(
672 state: Arc<Server<S>>,
673 stream_request: StreamRequest,
674) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
675where
676 S: Clone + Send + Sync + 'static,
677{
678 let IncomingStreamRequest::Begin(begin) = stream_request.request() else {
679 stream_request.shutdown_circuit()?;
680 return Ok(());
681 };
682
683 if route_onion_request(begin.port(), false, false) != OnionAction::ServePlaintext {
684 stream_request.shutdown_circuit()?;
685 return Ok(());
686 }
687
688 let onion_stream = stream_request.accept(Connected::new_empty()).await?;
689 let svc =
690 hyper::service::service_fn(move |req| hyper_handler(state.clone(), req, ONION_PEER_ADDR));
691 serve_connection(onion_stream, svc).await
692}
693
694/// Handles a single rendezvous stream for [`Server::serve_onion_with_client`], dispatching per
695/// [`route_onion_request`]. Requires the `tls` feature (see [`OnionAction`]'s docs for why the
696/// non-TLS case never needs this — it reuses [`handle_plaintext_only_stream`] instead).
697#[cfg(feature = "tls")]
698async fn handle_onion_stream<S>(
699 state: Arc<Server<S>>,
700 stream_request: StreamRequest,
701 tls_acceptor: Option<TlsAcceptor>,
702 redirect_http: bool,
703 onion_host: Arc<str>,
704) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
705where
706 S: Clone + Send + Sync + 'static,
707{
708 let IncomingStreamRequest::Begin(begin) = stream_request.request() else {
709 stream_request.shutdown_circuit()?;
710 return Ok(());
711 };
712
713 match route_onion_request(begin.port(), tls_acceptor.is_some(), redirect_http) {
714 OnionAction::Reject => {
715 stream_request.shutdown_circuit()?;
716 Ok(())
717 }
718 OnionAction::ServePlaintext => {
719 let onion_stream = stream_request.accept(Connected::new_empty()).await?;
720 let svc = hyper::service::service_fn(move |req| {
721 hyper_handler(state.clone(), req, ONION_PEER_ADDR)
722 });
723 serve_connection(onion_stream, svc).await
724 }
725 OnionAction::Redirect => {
726 let onion_stream = stream_request.accept(Connected::new_empty()).await?;
727 let svc = hyper::service::service_fn(move |req: Request<hyper::body::Incoming>| {
728 let onion_host = onion_host.clone();
729 async move { Ok::<_, std::io::Error>(redirect_response(&req, &onion_host)) }
730 });
731 serve_connection(onion_stream, svc).await
732 }
733 OnionAction::ServeTls => {
734 let Some(acceptor) = tls_acceptor else {
735 stream_request.shutdown_circuit()?;
736 return Ok(());
737 };
738 let onion_stream = stream_request.accept(Connected::new_empty()).await?;
739 let tls_stream = tokio::time::timeout(
740 crate::server::TLS_HANDSHAKE_TIMEOUT,
741 acceptor.accept(onion_stream),
742 )
743 .await
744 .map_err(|_| "TLS handshake timed out")??;
745 let svc = hyper::service::service_fn(move |req| {
746 hyper_handler(state.clone(), req, ONION_PEER_ADDR)
747 });
748 serve_connection(tls_stream, svc).await
749 }
750 }
751}
752
753/// Builds the `308 Permanent Redirect` response for a plaintext request when
754/// [`OnionConfig::redirect_http`] is enabled. Requires the `tls` feature.
755#[cfg(feature = "tls")]
756fn redirect_response(req: &Request<hyper::body::Incoming>, onion_host: &str) -> Response<Body> {
757 let path_and_query = req
758 .uri()
759 .path_and_query()
760 .map_or("/", hyper::http::uri::PathAndQuery::as_str);
761 let location = redirect_location(onion_host, path_and_query);
762 Response::builder()
763 .status(308) // preserves the HTTP method, unlike 301/302
764 .header("location", location)
765 .body(Body::empty())
766 .unwrap_or_else(|_| Response::new(Body::empty()))
767}
768
769#[cfg(test)]
770mod tests {
771 #[cfg(feature = "tls")]
772 use super::redirect_location;
773 use super::{OnionAction, OnionConfig, parse_nickname, route_onion_request};
774
775 #[test]
776 fn plaintext_serves_port_80_and_rejects_everything_else() {
777 assert_eq!(
778 route_onion_request(80, false, false),
779 OnionAction::ServePlaintext
780 );
781 assert_eq!(route_onion_request(443, false, false), OnionAction::Reject);
782 assert_eq!(route_onion_request(22, false, false), OnionAction::Reject);
783 }
784
785 #[cfg(feature = "tls")]
786 #[test]
787 fn tls_dual_stack_serves_both_ports_without_redirect() {
788 assert_eq!(
789 route_onion_request(80, true, false),
790 OnionAction::ServePlaintext
791 );
792 assert_eq!(route_onion_request(443, true, false), OnionAction::ServeTls);
793 }
794
795 #[cfg(feature = "tls")]
796 #[test]
797 fn tls_with_redirect_forces_port_80_to_redirect() {
798 assert_eq!(route_onion_request(80, true, true), OnionAction::Redirect);
799 assert_eq!(route_onion_request(443, true, true), OnionAction::ServeTls);
800 }
801
802 #[test]
803 fn redirect_only_applies_when_tls_is_enabled() {
804 // Requesting a redirect without TLS enabled is meaningless — plaintext still wins.
805 assert_eq!(
806 route_onion_request(80, false, true),
807 OnionAction::ServePlaintext
808 );
809 }
810
811 #[test]
812 fn unknown_ports_are_always_rejected() {
813 assert_eq!(route_onion_request(8080, false, false), OnionAction::Reject);
814 assert_eq!(route_onion_request(8080, true, true), OnionAction::Reject);
815 }
816
817 #[cfg(feature = "tls")]
818 #[test]
819 fn redirect_location_builds_the_https_equivalent_url() {
820 assert_eq!(
821 redirect_location("abcd1234.onion", "/foo?x=1"),
822 "https://abcd1234.onion/foo?x=1"
823 );
824 assert_eq!(
825 redirect_location("abcd1234.onion", "/"),
826 "https://abcd1234.onion/"
827 );
828 }
829
830 #[cfg(feature = "cert-gen")]
831 #[test]
832 fn onion_config_defaults_to_self_signed_tls_when_cert_gen_is_enabled() {
833 let config = OnionConfig::new("test-nickname");
834 assert_eq!(config.nickname, "test-nickname");
835 assert!(config.vanguards);
836 assert!(!config.redirect_http);
837 assert!(matches!(config.tls, super::OnionTls::SelfSigned));
838 }
839
840 #[cfg(not(feature = "cert-gen"))]
841 #[test]
842 fn onion_config_defaults_to_no_tls_without_cert_gen() {
843 let config = OnionConfig::new("test-nickname");
844 assert_eq!(config.nickname, "test-nickname");
845 assert!(config.vanguards);
846 assert!(!config.redirect_http);
847 assert!(matches!(config.tls, super::OnionTls::None));
848 }
849
850 #[test]
851 fn onion_config_builder_methods_are_chainable() {
852 let config = OnionConfig::new("nick")
853 .state_dir("/tmp/state")
854 .cache_dir("/tmp/cache")
855 .redirect_http(true)
856 .vanguards(false)
857 .no_tls();
858 assert_eq!(
859 config.state_dir.as_deref(),
860 Some(std::path::Path::new("/tmp/state"))
861 );
862 assert_eq!(
863 config.cache_dir.as_deref(),
864 Some(std::path::Path::new("/tmp/cache"))
865 );
866 assert!(config.redirect_http);
867 assert!(!config.vanguards);
868 assert!(matches!(config.tls, super::OnionTls::None));
869 }
870
871 #[test]
872 fn parse_nickname_accepts_a_valid_name() {
873 assert!(parse_nickname("valid-nickname").is_ok());
874 }
875
876 #[test]
877 fn parse_nickname_rejects_an_invalid_name_and_echoes_it_back() {
878 // Onion service nicknames are restricted (e.g. no spaces) — `HsNickname::from_str`
879 // rejects this, and `parse_nickname` wraps that error with the offending value since
880 // the underlying error doesn't otherwise include it.
881 let err = parse_nickname("not a valid nickname!!").unwrap_err();
882 assert!(err.to_string().contains("not a valid nickname!!"));
883 }
884
885 #[test]
886 fn onion_config_debug_does_not_panic() {
887 let debug = format!("{:?}", OnionConfig::new("nick"));
888 assert!(debug.contains("OnionConfig"));
889 assert!(debug.contains("nick"));
890 }
891
892 #[cfg(all(feature = "tls", feature = "cert-gen"))]
893 #[test]
894 fn tls_config_switches_to_a_custom_server_config() {
895 let policy = crate::tls::TlsPolicy::hardened();
896 let cert = crate::tls::generate_self_signed_cert(vec!["nick.onion".to_string()])
897 .expect("generate self-signed cert");
898 let server_config = policy
899 .server_config_from_pem(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes())
900 .expect("build server config");
901
902 let config = OnionConfig::new("nick").tls_config(server_config);
903 assert!(matches!(config.tls, super::OnionTls::Custom(_)));
904 // `OnionTls::Custom`'s `Debug` impl deliberately doesn't dump the whole
905 // `rustls::ServerConfig` — just proves the variant is reachable and formats.
906 assert!(format!("{config:?}").contains("nickname"));
907 }
908
909 #[test]
910 fn on_ready_stores_the_callback() {
911 let config = OnionConfig::new("nick").on_ready(|_addr| {});
912 assert!(config.on_ready.is_some());
913 }
914
915 #[cfg(all(feature = "tls", feature = "http1"))]
916 #[tokio::test]
917 async fn redirect_response_builds_a_308_to_the_https_equivalent() {
918 use hyper::Request;
919 use hyper::service::service_fn;
920 use tokio::io::{AsyncReadExt, AsyncWriteExt};
921
922 let (mut client_io, server_io) = tokio::io::duplex(8 * 1024);
923 let onion_host: std::sync::Arc<str> = std::sync::Arc::from("abcd1234.onion");
924
925 let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
926 let onion_host = onion_host.clone();
927 async move { Ok::<_, std::io::Error>(super::redirect_response(&req, &onion_host)) }
928 });
929 let server = tokio::spawn(async move {
930 hyper::server::conn::http1::Builder::new()
931 .serve_connection(hyper_util::rt::TokioIo::new(server_io), svc)
932 .await
933 });
934
935 client_io
936 .write_all(b"GET /foo?x=1 HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n")
937 .await
938 .expect("write request");
939
940 let mut buf = Vec::new();
941 client_io
942 .read_to_end(&mut buf)
943 .await
944 .expect("read response");
945 let response = String::from_utf8_lossy(&buf);
946
947 assert!(response.contains("308"), "unexpected response: {response}");
948 assert!(
949 response.contains("location: https://abcd1234.onion/foo?x=1"),
950 "unexpected response: {response}"
951 );
952
953 server
954 .await
955 .expect("server task join")
956 .expect("serve_connection ok");
957 }
958}