tachyon_web/server/i2p.rs
1//! Native I2P `.b32.i2p` eepsite support (see the `i2p` feature).
2//!
3//! Wraps [`tachyon_i2p`] (itself a safe wrapper around the vendored `libi2pd` router, see that
4//! crate's docs) so a Tachyon [`Server`] can be published directly as an I2P eepsite — no
5//! external `i2pd`/Java-I2P process, no SAM/BOB bridge — with the same `serve_*` ergonomics as
6//! [`Server::serve_tor`](crate::server::Server::serve_tor).
7//!
8//! # ⚠️ This feature does not honor `tachyon-web`'s `forbid(unsafe_code)` guarantee
9//!
10//! `tachyon-web` itself has `#![forbid(unsafe_code)]` at its crate root, same as always. But
11//! `libi2pd` is a C++ library with no stable C ABI, so reaching it at all requires an FFI
12//! boundary — that boundary is [`i2pd-sys`](https://docs.rs/i2pd-sys) (a hand-written `extern
13//! "C"` shim over `libi2pd`, vendored from [PurpleI2P/i2pd](https://github.com/PurpleI2P/i2pd))
14//! and [`tachyon-i2p`](https://docs.rs/tachyon-i2p) (the safe wrapper crate built on top of it),
15//! both **written for this project**, not a long-established, independently-audited pure-Rust
16//! dependency the way `arti-client`/`tor-hsservice` are for the `tor` feature. Enabling `i2p`
17//! pulls that FFI layer — and the statically-linked `libi2pd`/Boost/AWS-LC C/C++ code it
18//! compiles from vendored source — into your binary.
19//!
20//! Concretely, this means:
21//! - Memory safety for everything reachable through this feature rests on this project's own
22//! review of `libi2pd`'s threading/ownership contracts (documented inline in
23//! `i2pd-sys/shim/shim.h` and `tachyon-i2p`'s source), not on the Rust compiler.
24//! - A memory-safety bug in `libi2pd` itself, or in the shim/wrapper glue, is a bug in *your*
25//! process — there is no separate-process/SAM-bridge isolation boundary the way there would
26//! be running a standalone `i2pd` daemon.
27//! - This is meaningfully newer and less battle-tested than the `tor` feature. Treat it
28//! accordingly for anything security-sensitive: review `tachyon-i2p`'s source yourself, keep
29//! the crate updated, and don't expose it to hostile input without the same caution you'd
30//! apply to any other C/C++ dependency compiled into your binary.
31//!
32//! None of this is a knock on `libi2pd` itself (it's the reference I2P router implementation and
33//! plenty battle-tested on its own), but *this specific FFI boundary* is new, project-specific
34//! code, not something with years of independent scrutiny the way `arti`'s pure-Rust stack has.
35//!
36//! # Two entry points
37//!
38//! - [`Server::serve_i2p`] — the simplest possible eepsite: a persistent destination (keys
39//! stored under a data directory, so the address survives restarts), plaintext only.
40//! - [`Server::serve_i2p_config`], driven by an [`I2pConfig`] — adds an `on_ready` hook and a
41//! custom keys-file location, always available under the `i2p` feature alone. Optional TLS
42//! ([`I2pConfig::tls_config`], and [`I2pConfig::self_signed_tls`] specifically) additionally
43//! requires enabling `tls` (and `cert-gen` for the self-signed convenience) alongside `i2p` —
44//! see the [module docs](self) below and the `i2p` feature's own docs in `Cargo.toml`.
45//!
46//! # Crypto backend: `aws-lc` (default) vs FIPS
47//!
48//! `i2pd-sys` (via `tachyon-i2p`) links one of two crypto backends: regular AWS-LC (the default,
49//! selected here by the `i2p` feature) or the FIPS 140-3-validated AWS-LC-FIPS module. There's no
50//! separate `i2p-fips` feature — this crate's single top-level `fips` feature reaches into
51//! `tachyon-i2p/fips` too (taking priority over `i2p`'s `aws-lc` pick, harmlessly — see
52//! `i2pd-sys`'s crate docs for why), so enabling `i2p` and `fips` together is all it takes to
53//! publish this eepsite with FIPS-validated crypto everywhere, including the optional TLS layer
54//! ([`I2pConfig::tls_config`]/[`I2pConfig::self_signed_tls`]). See
55//! [`i2pd-sys`'s README](https://docs.rs/i2pd-sys) ("FIPS" section) for what this does and does
56//! not get you before reaching for it to satisfy a compliance requirement.
57//!
58//! # Why there's no `redirect_http`/dual-stack option like `tor`'s `OnionConfig`
59//!
60//! A Tor onion service multiplexes plaintext (virtual port 80) and TLS (virtual port 443) over
61//! the *same* `.onion` address, because Tor's rendezvous protocol carries a virtual port per
62//! stream. I2P's streaming protocol has no equivalent convention actually wired up here — one
63//! [`Destination`](tachyon_i2p::Destination) is one address serving *one* mode. Pick plaintext
64//! (the default, and by far the more common real-world eepsite setup) or TLS
65//! ([`I2pConfig::tls_config`]) up front; there's no in-band redirect between them the way there
66//! is for Tor.
67//!
68//! # Example
69//!
70//! ```rust,no_run
71//! use tachyon_web::{Router, Server, get};
72//!
73//! async fn hello() -> &'static str { "Hello from an eepsite!" }
74//!
75//! #[tokio::main]
76//! async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
77//! let app = Router::new().route("/", get(hello));
78//!
79//! // Publishes the service, prints its `.b32.i2p` address as soon as the destination is
80//! // created (not necessarily reachable on the network yet), then blocks serving requests
81//! // arriving over I2P streams.
82//! Server::new(app).serve_i2p("my-eepsite").await?;
83//! Ok(())
84//! }
85//! ```
86//!
87//! # Custom data directory and an `on_ready` hook
88//!
89//! ```rust,no_run
90//! use tachyon_web::{Router, Server, get};
91//! use tachyon_web::server::i2p::I2pConfig;
92//!
93//! async fn hello() -> &'static str { "Hello, eepsite world!" }
94//!
95//! #[tokio::main]
96//! async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
97//! let app = Router::new().route("/", get(hello));
98//!
99//! let config = I2pConfig::new("my-eepsite")
100//! .data_dir("/var/lib/tachyon/i2p")
101//! .on_ready(|addr| tracing::info!("reachable at http://{addr}"));
102//!
103//! Server::new(app).serve_i2p_config(config).await?;
104//! Ok(())
105//! }
106//! ```
107//!
108//! Reusing the same `nickname` (and data directory) across restarts keeps the same `.b32.i2p`
109//! address — the destination's keys file is created on first run and reused after that.
110//!
111//! # Choosing the identity's signature algorithm, and the destination's encryption capability
112//!
113//! [`I2pConfig::signature_type`] controls the identity's signature algorithm, used **the first
114//! time** a destination's keys are generated (irrelevant once a keys file already exists — an
115//! existing destination keeps whatever it was originally created with); it defaults to
116//! [`tachyon_i2p::SigType::Eddsa25519`], the I2P network's own current default.
117//!
118//! [`I2pConfig::crypto_type`] is different: it controls which encryption algorithm(s) the
119//! destination's `LeaseSet2` *advertises*, and applies on every run, not just first-time
120//! generation (the identity's own certificate is always plain `ElGamal` regardless — that's a
121//! hard requirement of real I2P clients, not something this crate exposes a choice over). Not
122//! calling it at all (the default) already publishes a hybrid `ElGamal` + ECIES-X25519 set, plus
123//! the post-quantum `ML-KEM-768` hybrid variant too if this was built against a
124//! post-quantum-capable crypto backend — maximizing both reachability and, when available,
125//! "harvest now, decrypt later" resistance, with no explicit opt-in needed. Call it only to
126//! *narrow* that down to one specific algorithm, e.g. for a smaller `LeaseSet2` or to deliberately
127//! exclude the post-quantum component:
128//!
129//! ```rust,no_run
130//! use tachyon_web::server::i2p::I2pConfig;
131//! use tachyon_i2p::{CryptoType, SigType};
132//!
133//! let config = I2pConfig::new("my-eepsite")
134//! .signature_type(SigType::Eddsa25519)
135//! .crypto_type(CryptoType::EciesX25519); // classical-only, no ML-KEM component
136//! ```
137
138use crate::server::Server;
139use crate::server::conn::{NO_PEER_ADDR as I2P_PEER_ADDR, serve_connection};
140use crate::server::http::hyper_handler;
141use std::path::PathBuf;
142use std::sync::Arc;
143use tachyon_i2p::{CryptoType, I2pRouter, SigType};
144#[cfg(feature = "tls")]
145use tokio_rustls::TlsAcceptor;
146
147/// How (or whether) an eepsite published via [`I2pConfig`] terminates TLS.
148#[derive(Clone)]
149enum I2pTls {
150 /// Plaintext only. This is the default — see the [module docs](self) for why, unlike Tor's
151 /// onion services, this isn't just cosmetic defense-in-depth: I2P's own transport is already
152 /// end-to-end encrypted, so TLS on top mainly matters if you specifically want the
153 /// destination itself to present a certificate.
154 None,
155 /// TLS using an ephemeral self-signed certificate, generated for the eepsite's `.b32.i2p`
156 /// address once it's known. Requires the `cert-gen` feature.
157 #[cfg(feature = "cert-gen")]
158 SelfSigned,
159 /// TLS using a caller-supplied config — e.g. the same `rustls::ServerConfig` used for a
160 /// clearnet [`Server::serve_https_config`](crate::server::Server::serve_https_config)
161 /// listener. Requires the `tls` feature.
162 #[cfg(feature = "tls")]
163 Custom(Arc<rustls::ServerConfig>),
164}
165
166impl std::fmt::Debug for I2pTls {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 Self::None => f.write_str("None"),
170 #[cfg(feature = "cert-gen")]
171 Self::SelfSigned => f.write_str("SelfSigned"),
172 #[cfg(feature = "tls")]
173 Self::Custom(_) => f.write_str("Custom(..)"),
174 }
175 }
176}
177
178/// Callback invoked with the published `.b32.i2p` address — see [`I2pConfig::on_ready`].
179type OnReadyHook = Box<dyn FnOnce(&str) + Send>;
180
181/// Configuration for publishing an I2P eepsite via [`Server::serve_i2p_config`].
182///
183/// See the [module docs](self) for a full example, and — importantly — for the
184/// `forbid(unsafe_code)` disclosure that applies to this whole feature.
185pub struct I2pConfig {
186 nickname: String,
187 data_dir: Option<PathBuf>,
188 sig_type: SigType,
189 encryption_types: Vec<CryptoType>,
190 tls: I2pTls,
191 on_ready: Option<OnReadyHook>,
192}
193
194impl std::fmt::Debug for I2pConfig {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("I2pConfig")
197 .field("nickname", &self.nickname)
198 .field("data_dir", &self.data_dir)
199 .field("sig_type", &self.sig_type)
200 .field("encryption_types", &self.encryption_types)
201 .field("tls", &self.tls)
202 .finish_non_exhaustive()
203 }
204}
205
206impl I2pConfig {
207 /// Creates a new configuration for a service published under `nickname`. `nickname` also
208 /// seeds libi2pd's own default data directory name (its router keys/netDb cache, separate
209 /// from this eepsite's own persistent destination keys — see [`data_dir`](Self::data_dir)).
210 ///
211 /// Defaults: plaintext only (see the [module docs](self) for why TLS defaults off here,
212 /// unlike Tor's `OnionConfig`), no `on_ready` hook, destination keys stored under
213 /// `./.tachyon-i2p/<nickname>.keys` relative to the current working directory,
214 /// [`SigType::default`] for the identity's signature algorithm (only used the first time
215 /// this destination's keys are generated — see
216 /// [`signature_type`](Self::signature_type)), and no explicit
217 /// [`crypto_type`](Self::crypto_type) override — which means the destination publishes
218 /// libi2pd's own automatic hybrid encryption set rather than a single fixed algorithm; see
219 /// [`crypto_type`](Self::crypto_type)'s docs before assuming a specific one is always used.
220 #[must_use]
221 pub fn new(nickname: impl Into<String>) -> Self {
222 Self {
223 nickname: nickname.into(),
224 data_dir: None,
225 sig_type: SigType::default(),
226 encryption_types: Vec::new(),
227 tls: I2pTls::None,
228 on_ready: None,
229 }
230 }
231
232 /// Overrides the directory this eepsite's persistent destination keys file is stored under
233 /// (as `<data_dir>/<nickname>.keys`). Reusing the same directory (and `nickname`) across
234 /// restarts keeps the same `.b32.i2p` address. Defaults to `./.tachyon-i2p` relative to the
235 /// current working directory.
236 #[must_use]
237 pub fn data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
238 self.data_dir = Some(dir.into());
239 self
240 }
241
242 /// Overrides the signature algorithm used **the first time** this destination's keys are
243 /// generated — irrelevant if a keys file already exists at the resolved path (an existing
244 /// destination keeps whatever algorithm it was originally created with). See
245 /// [`tachyon_i2p::SigType`]'s own docs for what's available and why RSA isn't one of the
246 /// options; defaults to [`SigType::default`] (`Eddsa25519`, the I2P network's own default).
247 #[must_use]
248 pub const fn signature_type(mut self, sig: SigType) -> Self {
249 self.sig_type = sig;
250 self
251 }
252
253 /// Overrides which encryption algorithm this destination's `LeaseSet2` advertises as usable —
254 /// convenience for the common single-algorithm case; see
255 /// [`encryption_types`](Self::encryption_types) (which this is built on) for the general
256 /// case, including "prefer post-quantum but still accept classical" multi-algorithm setups.
257 /// See [`tachyon_i2p::CryptoType`]'s own docs for the available options.
258 ///
259 /// Not calling this at all (the default) publishes libi2pd's own automatic hybrid set —
260 /// `ElGamal` + ECIES-X25519, plus ML-KEM-768+X25519 if this was built against a
261 /// post-quantum-capable crypto backend — which is what most callers want. Call this to
262 /// *narrow* that down to exactly one algorithm instead, e.g. for a smaller `LeaseSet2` or to
263 /// deliberately exclude the post-quantum component.
264 #[must_use]
265 pub fn crypto_type(mut self, crypto: CryptoType) -> Self {
266 self.encryption_types = vec![crypto];
267 self
268 }
269
270 /// Overrides which encryption algorithm(s) this destination's `LeaseSet2` advertises as usable
271 /// — unlike [`signature_type`](Self::signature_type), this applies on *every* run, not just
272 /// first-time key generation (the identity's own certificate is always plain `ElGamal`
273 /// regardless of this setting, per real I2P clients' requirements — this only controls the
274 /// destination's advertised encryption capability).
275 ///
276 /// Order matters: the **first** entry becomes the preferred type (published first in the
277 /// actual `LeaseSet2`, and what a peer that understands multiple of the listed types will
278 /// choose), with every later entry a fallback for peers that don't recognize it — publishing
279 /// something a given peer doesn't understand at all is harmless, not an error, since it
280 /// simply skips entries it can't use and tries the next one. This is how to express "prefer
281 /// post-quantum, but still reachable by peers that don't support it yet":
282 ///
283 /// ```rust,no_run
284 /// use tachyon_web::server::i2p::I2pConfig;
285 /// use tachyon_i2p::CryptoType;
286 ///
287 /// let config = I2pConfig::new("my-eepsite").encryption_types(&[
288 /// CryptoType::EciesMlkem1024X25519, // preferred: strongest post-quantum option
289 /// CryptoType::EciesX25519, // fallback: peers that don't understand ML-KEM yet
290 /// ]);
291 /// ```
292 ///
293 /// An empty slice restores the default automatic hybrid set described on
294 /// [`crypto_type`](Self::crypto_type)'s docs.
295 #[must_use]
296 pub fn encryption_types(mut self, types: &[CryptoType]) -> Self {
297 self.encryption_types = types.to_vec();
298 self
299 }
300
301 /// Enables TLS using a caller-supplied `rustls::ServerConfig` instead of the plaintext
302 /// default — for example, the exact same config passed to
303 /// [`Server::serve_https_config`](crate::server::Server::serve_https_config) for a clearnet
304 /// listener. Requires the `tls` feature.
305 #[cfg(feature = "tls")]
306 #[must_use]
307 pub fn tls_config(mut self, config: rustls::ServerConfig) -> Self {
308 self.tls = I2pTls::Custom(Arc::new(config));
309 self
310 }
311
312 /// Enables TLS using a freshly generated self-signed certificate for the eepsite's
313 /// `.b32.i2p` address, instead of the plaintext default. Requires the `cert-gen` feature.
314 #[cfg(feature = "cert-gen")]
315 #[must_use]
316 pub fn self_signed_tls(mut self) -> Self {
317 self.tls = I2pTls::SelfSigned;
318 self
319 }
320
321 /// Disables TLS (the default) after a prior [`tls_config`](Self::tls_config)/
322 /// [`self_signed_tls`](Self::self_signed_tls) call.
323 // Only `const`-eligible when neither `Custom`/`SelfSigned` variant exists (their
324 // non-trivial `Drop` glue can't run in a `const fn`), i.e. only without `tls`/`cert-gen` —
325 // not worth splitting this method's signature across features for.
326 #[cfg_attr(not(feature = "tls"), allow(clippy::missing_const_for_fn))]
327 #[must_use]
328 pub fn no_tls(mut self) -> Self {
329 self.tls = I2pTls::None;
330 self
331 }
332
333 /// Registers a callback invoked exactly once — with the published `.b32.i2p` address (no
334 /// scheme, e.g. `"abcd...xyz.b32.i2p"`) — as soon as the destination is created, just before
335 /// requests start being served. This is the only way to observe the address
336 /// programmatically, since [`serve_i2p_config`](Server::serve_i2p_config) blocks for the
337 /// lifetime of the service; the address is also always logged via `tracing` at `info` level.
338 #[must_use]
339 pub fn on_ready(mut self, f: impl FnOnce(&str) + Send + 'static) -> Self {
340 self.on_ready = Some(Box::new(f));
341 self
342 }
343
344 /// The nickname this service will be published under.
345 #[must_use]
346 pub fn nickname(&self) -> &str {
347 &self.nickname
348 }
349
350 /// Whether TLS is enabled — `false` (the default) unless
351 /// [`tls_config`](Self::tls_config)/[`self_signed_tls`](Self::self_signed_tls) was called.
352 #[must_use]
353 pub const fn tls_enabled(&self) -> bool {
354 !matches!(self.tls, I2pTls::None)
355 }
356
357 /// The keys-file path this configuration resolves to (`<data_dir>/<nickname>.keys`).
358 fn keys_path(&self) -> PathBuf {
359 self.data_dir
360 .clone()
361 .unwrap_or_else(|| PathBuf::from(".tachyon-i2p"))
362 .join(format!("{}.keys", self.nickname))
363 }
364}
365
366impl<S> Server<S>
367where
368 S: Clone + Send + Sync + 'static,
369{
370 /// Publishes this router as an I2P eepsite and serves requests arriving over it, blocking
371 /// indefinitely — the accept loop retries forever on error and has no graceful-stop
372 /// mechanism today; abort the surrounding task (e.g. via `JoinHandle::abort`) to end it.
373 ///
374 /// Starts a fresh [`I2pRouter`] and a persistent destination under
375 /// `./.tachyon-i2p/<nickname>.keys` — plaintext only, no other configuration. For a custom
376 /// data directory, TLS, or an `on_ready` hook, use [`serve_i2p_config`](Self::serve_i2p_config)
377 /// instead.
378 ///
379 /// **See the [module docs](crate::server::i2p) for why this feature does not honor
380 /// `tachyon-web`'s `forbid(unsafe_code)` guarantee.**
381 ///
382 /// # Errors
383 /// Returns an error if the I2P router fails to start (most commonly:
384 /// [`tachyon_i2p::I2pError::AlreadyRunning`] if another [`I2pRouter`] is already running in
385 /// this process — only one may exist per process) or the destination fails to load/create.
386 pub async fn serve_i2p(
387 self,
388 nickname: &str,
389 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
390 self.serve_i2p_config(I2pConfig::new(nickname)).await
391 }
392
393 /// Publishes this router as an I2P eepsite according to `config`, starting a fresh
394 /// [`I2pRouter`], and serves requests arriving over it, blocking indefinitely — the accept
395 /// loop retries forever on error and has no graceful-stop mechanism today; abort the
396 /// surrounding task (e.g. via `JoinHandle::abort`) to end it.
397 ///
398 /// **See the [module docs](crate::server::i2p) for why this feature does not honor
399 /// `tachyon-web`'s `forbid(unsafe_code)` guarantee.**
400 ///
401 /// # Errors
402 /// Returns an error if the I2P router fails to start (most commonly:
403 /// [`tachyon_i2p::I2pError::AlreadyRunning`] if another [`I2pRouter`] is already running in
404 /// this process — only one may exist per process; use
405 /// [`serve_i2p_config_with_router`](Self::serve_i2p_config_with_router) to reuse one instead),
406 /// the destination fails to load/create, or (when TLS is enabled) the TLS configuration is
407 /// invalid.
408 pub async fn serve_i2p_config(
409 self,
410 config: I2pConfig,
411 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
412 let router = I2pRouter::start(config.nickname.clone()).await?;
413 self.serve_i2p_config_with_router(&router, config).await
414 }
415
416 /// Publishes this router as an I2P eepsite according to `config`, using an already-started
417 /// [`I2pRouter`] (only one may run per process — this is how a second eepsite, or a second
418 /// destination used purely as an outbound client, shares the same router instead of hitting
419 /// [`tachyon_i2p::I2pError::AlreadyRunning`]), and serves requests arriving over it, blocking
420 /// indefinitely — the accept loop retries forever on error and has no graceful-stop
421 /// mechanism today; abort the surrounding task (e.g. via `JoinHandle::abort`) to end it.
422 ///
423 /// **See the [module docs](crate::server::i2p) for why this feature does not honor
424 /// `tachyon-web`'s `forbid(unsafe_code)` guarantee.**
425 ///
426 /// The self-signed certificate (when [`I2pConfig::self_signed_tls`] is used) shares this
427 /// server's crypto/TLS policy — see [`Server::tls_policy`].
428 ///
429 /// # Errors
430 /// Returns an error if `nickname` contains path separators or `..` (it's used verbatim to
431 /// build the destination keys file path, as `<data_dir>/<nickname>.keys`), the destination
432 /// fails to load/create, or (when TLS is enabled) the TLS configuration is invalid.
433 pub async fn serve_i2p_config_with_router(
434 self,
435 router: &I2pRouter,
436 config: I2pConfig,
437 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
438 crate::server::enforce_fips_compliance()?;
439 validate_nickname(&config.nickname)?;
440
441 let keys_path = config.keys_path();
442 let is_public = true;
443 let mut destination = router
444 .destination_from_keys_file(
445 keys_path,
446 is_public,
447 config.sig_type,
448 &config.encryption_types,
449 )
450 .await?;
451
452 let address = destination.b32_address().to_string();
453 tracing::info!("[i2p] eepsite published at {address}");
454 if let Some(on_ready) = config.on_ready {
455 on_ready(&address);
456 }
457
458 #[cfg(feature = "tls")]
459 {
460 let tls_acceptor = match &config.tls {
461 I2pTls::None => None,
462 #[cfg(feature = "cert-gen")]
463 I2pTls::SelfSigned => {
464 let cert = crate::tls::generate_self_signed_cert(vec![address.clone()])?;
465 let server_config = self.effective_tls_policy().server_config_from_pem(
466 cert.cert_pem.as_bytes(),
467 cert.key_pem.as_bytes(),
468 )?;
469 Some(TlsAcceptor::from(Arc::new(server_config)))
470 }
471 I2pTls::Custom(server_config) => Some(TlsAcceptor::from(server_config.clone())),
472 };
473
474 let state = Arc::new(self);
475 loop {
476 let stream = match destination.accept().await {
477 Ok(s) => s,
478 Err(e) => {
479 // Unlike the TCP `accept()` loops (`serve_http`/`serve_https`), a
480 // persistently failing `destination.accept()` (e.g. the underlying I2P
481 // tunnel is down) has no OS-level resource-exhaustion signal to detect —
482 // so back off unconditionally rather than risk a tight, CPU-spinning
483 // retry loop if every future `accept()` keeps failing immediately.
484 tracing::debug!("[i2p] accept error: {e}");
485 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
486 continue;
487 }
488 };
489 let state = state.clone();
490 let tls_acceptor = tls_acceptor.clone();
491 drop(tokio::spawn(async move {
492 if let Err(e) = handle_i2p_stream(state, stream, tls_acceptor).await {
493 tracing::debug!("[i2p] connection error: {e}");
494 }
495 }));
496 }
497 }
498
499 // No `tls` feature compiled in at all: `config.tls` can only ever be `I2pTls::None`
500 // (the only variant that exists in this build), so this is unconditionally plaintext.
501 #[cfg(not(feature = "tls"))]
502 {
503 let state = Arc::new(self);
504 loop {
505 let stream = match destination.accept().await {
506 Ok(s) => s,
507 Err(e) => {
508 tracing::debug!("[i2p] accept error: {e}");
509 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
510 continue;
511 }
512 };
513 let state = state.clone();
514 drop(tokio::spawn(async move {
515 if let Err(e) = handle_i2p_stream_plaintext(state, stream).await {
516 tracing::debug!("[i2p] connection error: {e}");
517 }
518 }));
519 }
520 }
521 }
522}
523
524/// Rejects nicknames that could escape [`I2pConfig::data_dir`] when used to build the
525/// destination keys file path (`<data_dir>/<nickname>.keys`) — unlike the Tor `nickname`, which
526/// is validated as a typed `HsNickname` before any file I/O, I2P has no equivalent typed
527/// nickname to lean on, so it's checked directly here.
528fn validate_nickname(nickname: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
529 if nickname.is_empty() || nickname.contains(['/', '\\']) || nickname == "." || nickname == ".."
530 {
531 return Err(format!("invalid I2P eepsite nickname {nickname:?}").into());
532 }
533 Ok(())
534}
535
536/// Handles a single accepted I2P stream when the `tls` feature is off: plaintext HTTP dispatch
537/// only, sharing the same [`serve_connection`] helper (and thus HTTP/1.1-vs-HTTP/2 negotiation
538/// logic) `tor.rs` uses.
539#[cfg(not(feature = "tls"))]
540async fn handle_i2p_stream_plaintext<S>(
541 state: Arc<Server<S>>,
542 stream: tachyon_i2p::I2pStream,
543) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
544where
545 S: Clone + Send + Sync + 'static,
546{
547 let svc =
548 hyper::service::service_fn(move |req| hyper_handler(state.clone(), req, I2P_PEER_ADDR));
549 serve_connection(stream, svc).await
550}
551
552/// Handles a single accepted I2P stream: TLS (if configured) then HTTP dispatch, sharing the
553/// same [`serve_connection`] helper (and thus HTTP/1.1-vs-HTTP/2 negotiation logic) `tor.rs` uses.
554/// Requires the `tls` feature (see [`handle_i2p_stream_plaintext`] for the non-TLS build).
555#[cfg(feature = "tls")]
556async fn handle_i2p_stream<S>(
557 state: Arc<Server<S>>,
558 stream: tachyon_i2p::I2pStream,
559 tls_acceptor: Option<TlsAcceptor>,
560) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
561where
562 S: Clone + Send + Sync + 'static,
563{
564 match tls_acceptor {
565 None => {
566 let svc = hyper::service::service_fn(move |req| {
567 hyper_handler(state.clone(), req, I2P_PEER_ADDR)
568 });
569 serve_connection(stream, svc).await
570 }
571 Some(acceptor) => {
572 let tls_stream = tokio::time::timeout(
573 crate::server::TLS_HANDSHAKE_TIMEOUT,
574 acceptor.accept(stream),
575 )
576 .await
577 .map_err(|_| "TLS handshake timed out")??;
578 let svc = hyper::service::service_fn(move |req| {
579 hyper_handler(state.clone(), req, I2P_PEER_ADDR)
580 });
581 serve_connection(tls_stream, svc).await
582 }
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::{I2pConfig, validate_nickname};
589
590 #[test]
591 fn validate_nickname_accepts_a_normal_name() {
592 assert!(validate_nickname("my-eepsite").is_ok());
593 }
594
595 #[test]
596 fn validate_nickname_rejects_path_traversal() {
597 assert!(validate_nickname("..").is_err());
598 assert!(validate_nickname(".").is_err());
599 assert!(validate_nickname("").is_err());
600 assert!(validate_nickname("../../etc/passwd").is_err());
601 assert!(validate_nickname("a/b").is_err());
602 assert!(validate_nickname("a\\b").is_err());
603 }
604
605 #[test]
606 fn i2p_config_defaults_are_sensible() {
607 let config = I2pConfig::new("test-nickname");
608 assert_eq!(config.nickname(), "test-nickname");
609 assert!(!config.tls_enabled());
610 assert_eq!(
611 config.keys_path(),
612 std::path::Path::new(".tachyon-i2p/test-nickname.keys")
613 );
614 }
615
616 #[cfg(feature = "cert-gen")]
617 #[test]
618 fn i2p_config_builder_methods_are_chainable() {
619 let config = I2pConfig::new("nick")
620 .data_dir("/tmp/i2p-data")
621 .self_signed_tls();
622 assert_eq!(
623 config.keys_path(),
624 std::path::Path::new("/tmp/i2p-data/nick.keys")
625 );
626 assert!(config.tls_enabled());
627
628 let config = config.no_tls();
629 assert!(!config.tls_enabled());
630 }
631
632 #[cfg(not(feature = "cert-gen"))]
633 #[test]
634 fn i2p_config_data_dir_is_chainable_without_cert_gen() {
635 let config = I2pConfig::new("nick").data_dir("/tmp/i2p-data");
636 assert_eq!(
637 config.keys_path(),
638 std::path::Path::new("/tmp/i2p-data/nick.keys")
639 );
640 assert!(!config.tls_enabled());
641 }
642
643 #[test]
644 fn i2p_config_signature_and_crypto_type_defaults_and_overrides() {
645 let config = I2pConfig::new("nick");
646 assert_eq!(config.sig_type, tachyon_i2p::SigType::default());
647 assert!(
648 config.encryption_types.is_empty(),
649 "no explicit crypto_type() call should mean \"use libi2pd's automatic hybrid set\""
650 );
651
652 let config = config
653 .signature_type(tachyon_i2p::SigType::EcdsaP521)
654 .crypto_type(tachyon_i2p::CryptoType::EciesMlkem768X25519);
655 assert_eq!(config.sig_type, tachyon_i2p::SigType::EcdsaP521);
656 assert_eq!(
657 config.encryption_types,
658 vec![tachyon_i2p::CryptoType::EciesMlkem768X25519]
659 );
660 }
661
662 #[test]
663 fn i2p_config_encryption_types_preserves_preference_order() {
664 let config = I2pConfig::new("nick").encryption_types(&[
665 tachyon_i2p::CryptoType::EciesMlkem1024X25519,
666 tachyon_i2p::CryptoType::EciesX25519,
667 ]);
668 assert_eq!(
669 config.encryption_types,
670 vec![
671 tachyon_i2p::CryptoType::EciesMlkem1024X25519,
672 tachyon_i2p::CryptoType::EciesX25519,
673 ],
674 "the preferred type must stay first -- it's what libi2pd publishes as preferred"
675 );
676
677 // A later crypto_type()/encryption_types() call replaces, rather than appends to, the
678 // previous one -- confirms these two builder methods share one underlying field.
679 let config = config.crypto_type(tachyon_i2p::CryptoType::EciesX25519);
680 assert_eq!(
681 config.encryption_types,
682 vec![tachyon_i2p::CryptoType::EciesX25519]
683 );
684 }
685
686 #[test]
687 fn i2p_config_debug_does_not_panic() {
688 let debug = format!("{:?}", I2pConfig::new("nick"));
689 assert!(debug.contains("I2pConfig"));
690 assert!(debug.contains("nick"));
691 }
692
693 #[cfg(all(feature = "tls", feature = "cert-gen"))]
694 #[test]
695 fn tls_config_switches_to_a_custom_server_config() {
696 let policy = crate::tls::TlsPolicy::hardened();
697 let cert = crate::tls::generate_self_signed_cert(vec!["nick.b32.i2p".to_string()])
698 .expect("generate self-signed cert");
699 let server_config = policy
700 .server_config_from_pem(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes())
701 .expect("build server config");
702
703 let config = I2pConfig::new("nick").tls_config(server_config);
704 assert!(matches!(config.tls, super::I2pTls::Custom(_)));
705 assert!(config.tls_enabled());
706 }
707
708 #[test]
709 fn on_ready_stores_the_callback() {
710 let config = I2pConfig::new("nick").on_ready(|_addr| {});
711 assert!(config.on_ready.is_some());
712 }
713}