tachyon_i2p/router.rs
1//! [`I2pRouter`]: the process-wide libi2pd instance.
2
3use crate::destination::Destination;
4use crate::error::I2pError;
5use std::ffi::CString;
6use std::os::raw::c_int;
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10
11/// Set while a router is live, cleared once the last [`I2pRouter`] clone has been dropped and
12/// libi2pd has been torn down.
13///
14/// libi2pd's router context is a process-wide global, so at most one router can be live at a
15/// time. Sequentially is fine: i2pd-sys 0.0.5's shim holds one mutex across all four lifecycle
16/// calls and has `i2pd_terminate` stop a still-running router itself, making
17/// `init -> start -> terminate -> init -> start` a supported cycle.
18///
19/// Latched permanently in one case: a panic inside the worker running `i2pd_init`/`i2pd_start`
20/// leaves libi2pd's globals in an unknown state, and a fresh `init` over that is what the shim's
21/// mutex cannot make safe.
22static ROUTER_RUNNING: AtomicBool = AtomicBool::new(false);
23
24/// The signature algorithm for a destination's identity -- see [`I2pRouter::generate_keys`].
25///
26/// Values are libi2pd's protocol-level `SigningKeyType` (`libi2pd/Identity.h`). Two families
27/// from the I2P spec are deliberately absent: RSA, which is a `su3` file-signing type that
28/// libi2pd's `GenerateSigningKeyPair` silently substitutes EdDSA for; and RedDSA-SHA512-Ed25519,
29/// which is for LeaseSet2 blinding, not identity signing.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31#[non_exhaustive]
32pub enum SigType {
33 /// DSA-SHA1. Superseded as the network default in 2015; for old persisted keys only.
34 DsaSha1,
35 /// ECDSA-SHA256 on the P-256 curve.
36 EcdsaP256,
37 /// ECDSA-SHA384 on the P-384 curve. Not widely used on the I2P network.
38 EcdsaP384,
39 /// ECDSA-SHA512 on the P-521 curve. Not widely used on the I2P network.
40 EcdsaP521,
41 /// Ed25519 (EdDSA-SHA512). The I2P network default since 0.9.15; use this for new
42 /// destinations.
43 #[default]
44 Eddsa25519,
45}
46
47impl SigType {
48 pub(crate) const fn as_raw(self) -> c_int {
49 match self {
50 Self::DsaSha1 => 0,
51 Self::EcdsaP256 => 1,
52 Self::EcdsaP384 => 2,
53 Self::EcdsaP521 => 3,
54 Self::Eddsa25519 => 7,
55 }
56 }
57}
58
59/// An encryption algorithm a destination's LeaseSet2 can advertise as usable -- see
60/// [`I2pRouter::create_persistent_destination`]'s `encryption_types` parameter.
61///
62/// Values are libi2pd's protocol-level `CryptoKeyType` (`libi2pd/Identity.h`). This covers only
63/// the advertised encryption capability, never the identity certificate, which is ElGamal either
64/// way (see [`I2pRouter::generate_keys`]).
65///
66/// An empty `encryption_types` slice already gets libi2pd's hybrid default
67/// (ElGamal + ECIES-X25519, plus ML-KEM-768 on a post-quantum-capable backend); explicit values
68/// only ever *narrow* that. The `EciesMlkem*` variants are new enough that older peers may not
69/// understand a destination advertising one, so they trade reachability for long-term
70/// confidentiality.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72#[non_exhaustive]
73pub enum CryptoType {
74 /// The original ElGamal scheme. For old persisted keys only.
75 ElGamal,
76 /// ECIES on the P-256 curve with AES-256-CBC. Not widely used on the I2P network.
77 EciesP256,
78 /// ECIES-X25519-AEAD (ChaCha20/Poly1305). The I2P network's current default.
79 #[default]
80 EciesX25519,
81 /// ECIES-X25519-AEAD hybridized with ML-KEM-512 (NIST PQC category 1).
82 EciesMlkem512X25519,
83 /// ECIES-X25519-AEAD hybridized with ML-KEM-768 (NIST PQC category 3).
84 EciesMlkem768X25519,
85 /// ECIES-X25519-AEAD hybridized with ML-KEM-1024 (NIST PQC category 5).
86 EciesMlkem1024X25519,
87}
88
89impl CryptoType {
90 pub(crate) const fn as_raw(self) -> c_int {
91 match self {
92 Self::ElGamal => 0,
93 Self::EciesP256 => 1,
94 Self::EciesX25519 => 4,
95 Self::EciesMlkem512X25519 => 5,
96 Self::EciesMlkem768X25519 => 6,
97 Self::EciesMlkem1024X25519 => 7,
98 }
99 }
100
101 /// Whether the linked crypto backend can actually perform this algorithm. Always `true` for
102 /// the classical types; for the `EciesMlkem*` ones it runs a real
103 /// generate/encapsulate/decapsulate round trip through libi2pd's `MLKEMKeys` rather than
104 /// checking a feature flag. Sub-millisecond but not free -- cache it, don't call it per
105 /// connection. Needs no running router.
106 ///
107 /// Check this before naming an `EciesMlkem*` type in
108 /// [`create_persistent_destination`](I2pRouter::create_persistent_destination): a LeaseSet2
109 /// advertising only algorithms this router cannot perform leaves the destination
110 /// unreachable, with nothing in the address or the creation result to say so.
111 #[must_use]
112 pub fn is_supported(self) -> bool {
113 let mlkem_variant = match self {
114 Self::ElGamal | Self::EciesP256 | Self::EciesX25519 => return true,
115 Self::EciesMlkem512X25519 => 0,
116 Self::EciesMlkem768X25519 => 1,
117 Self::EciesMlkem1024X25519 => 2,
118 };
119 // SAFETY: the shim validates the variant number itself (returning 0 for anything outside
120 // 0-2), touches only AWS-LC's own key machinery rather than libi2pd's router globals, and
121 // takes no pointers -- so this is safe at any point, including before `i2pd_init`.
122 unsafe { i2pd_sys::i2pd_test_mlkem_roundtrip(mlkem_variant) != 0 }
123 }
124}
125
126/// How this router participates in the wider I2P network -- see
127/// [`I2pRouter::start_with_config`].
128///
129/// libi2pd normally reads all of this from an `i2pd.conf`, which the library entry point never
130/// parses (only upstream's daemon did), so these settings are the only way to reach them. They
131/// apply between libi2pd's `init` and `start`; nothing here can be changed on a running router.
132///
133/// [`Default`] mirrors i2pd-sys's own: transit on, 256 KB/s, no share limit, libi2pd's own
134/// transit tunnel ceiling, floodfill off.
135///
136/// ```
137/// use tachyon_i2p::RouterConfig;
138///
139/// // Keep carrying transit (the anonymity-preserving default), but bound what it costs.
140/// let config = RouterConfig::default()
141/// .bandwidth_limit_kbps(512)
142/// .transit_share_percent(25)
143/// .max_transit_tunnels(500);
144/// ```
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct RouterConfig {
148 accepts_transit: bool,
149 bandwidth_limit_kbps: Option<u32>,
150 transit_share_percent: u8,
151 max_transit_tunnels: Option<u32>,
152 floodfill: bool,
153}
154
155impl Default for RouterConfig {
156 fn default() -> Self {
157 Self {
158 accepts_transit: true,
159 bandwidth_limit_kbps: None,
160 transit_share_percent: 100,
161 max_transit_tunnels: None,
162 floodfill: false,
163 }
164 }
165}
166
167impl RouterConfig {
168 /// Whether to carry *other* users' tunnels (default: `true`).
169 ///
170 /// Transit is unrelated to your own destinations, which work either way. It defaults to on
171 /// because a router that relays nothing gives an observer no cover traffic: every byte
172 /// crossing the link is then yours, and the refusal is itself a fingerprint. Turn it off when
173 /// bandwidth is metered, or when the risk you care about is a memory-safety bug in libi2pd
174 /// rather than traffic analysis.
175 ///
176 /// Building without the default `transit` feature is the stronger form of `false`: libi2pd's
177 /// tunnel build-request path is compiled out, so `true` here is silently ignored and
178 /// [`I2pRouter::supports_transit`] reports `false`.
179 #[must_use]
180 pub const fn accepts_transit(mut self, enabled: bool) -> Self {
181 self.accepts_transit = enabled;
182 self
183 }
184
185 /// Whole-router bandwidth ceiling in KB/s (default: 256).
186 ///
187 /// A limit is always in force, and 0 is not "unlimited" -- it falls back to the same 256 KB/s
188 /// default as never calling this. At a genuine 0 the router publishes itself as permanently
189 /// congested and refuses transit outright, so i2pd-sys applies 256 KB/s at init, below
190 /// upstream's 2048 KB/s daemon default (which assumes a dedicated router, not a library
191 /// sharing a server's uplink).
192 #[must_use]
193 pub const fn bandwidth_limit_kbps(mut self, kbps: u32) -> Self {
194 self.bandwidth_limit_kbps = Some(kbps);
195 self
196 }
197
198 /// What percentage of [`bandwidth_limit_kbps`](Self::bandwidth_limit_kbps) transit tunnels
199 /// may use (default: 100). Values above 100 are clamped.
200 #[must_use]
201 pub const fn transit_share_percent(mut self, percent: u8) -> Self {
202 self.transit_share_percent = percent;
203 self
204 }
205
206 /// Ceiling on concurrently-carried transit tunnels (default: libi2pd's own 25000, a
207 /// daemon-scale figure worth lowering for a library embedding).
208 #[must_use]
209 pub const fn max_transit_tunnels(mut self, max: u32) -> Self {
210 self.max_transit_tunnels = Some(max);
211 self
212 }
213
214 /// Whether to serve the distributed netDb (default: `false`).
215 ///
216 /// A floodfill router stores and answers lookups for the whole network's lease sets: a lot of
217 /// extra traffic, and a lot of extra attacker-supplied input parsed in-process. Leave it off
218 /// unless running a floodfill is the point.
219 #[must_use]
220 pub const fn floodfill(mut self, enabled: bool) -> Self {
221 self.floodfill = enabled;
222 self
223 }
224
225 /// Must run between `i2pd_init` and `i2pd_start`; anything outside that window is a no-op.
226 fn apply(&self) {
227 // SAFETY: every setter here takes plain integers, tolerates any value (percentages are
228 // clamped shim-side, non-positive limits fall back to a default), and is called from the
229 // same `spawn_blocking` worker as `i2pd_init`/`i2pd_start`, in between the two.
230 unsafe {
231 i2pd_sys::i2pd_set_accepts_transit(c_int::from(self.accepts_transit));
232 i2pd_sys::i2pd_set_bandwidth_limit(clamp_to_c_int(self.bandwidth_limit_kbps));
233 i2pd_sys::i2pd_set_share_percent(c_int::from(self.transit_share_percent));
234 i2pd_sys::i2pd_set_max_transit_tunnels(clamp_to_c_int(self.max_transit_tunnels));
235 i2pd_sys::i2pd_set_floodfill(c_int::from(self.floodfill));
236 }
237 }
238}
239
240/// `0` is the shim's "leave at the default". Oversized values saturate rather than wrapping
241/// negative, which the shim would read as "restore the default" (bandwidth) or "ignore" (transit
242/// tunnels) -- silently discarding a caller's very high limit.
243const fn clamp_to_c_int(value: Option<u32>) -> c_int {
244 match value {
245 None => 0,
246 Some(v) if v > c_int::MAX as u32 => c_int::MAX,
247 Some(v) => v as c_int,
248 }
249}
250
251/// Owns a destination's serialized private keys and scrubs them on drop.
252///
253/// These bytes are the destination's identity: anyone holding a copy can impersonate the
254/// eepsite. A plain `Vec<u8>` would leave them in freed heap memory after every load/generate,
255/// recoverable by a later heap disclosure bug, a core dump, or swap.
256struct SecretBytes(Vec<u8>);
257
258impl Drop for SecretBytes {
259 fn drop(&mut self) {
260 for b in &mut self.0 {
261 // SAFETY: `b` is a live, uniquely-borrowed, properly-aligned `u8`. Volatile so the
262 // write survives an optimizer that can see the buffer is dead afterwards.
263 unsafe { std::ptr::write_volatile(b, 0) };
264 }
265 std::sync::atomic::compiler_fence(Ordering::SeqCst);
266 }
267}
268
269impl std::fmt::Debug for SecretBytes {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 // Never render the key material itself.
272 write!(f, "SecretBytes({} bytes)", self.0.len())
273 }
274}
275
276/// Writes `bytes` to `path` as private key material: owner-only, and atomically, so an
277/// interrupted write can't leave a half-written keys file in place of a good one.
278///
279/// The temporary goes in the *same directory* as `path` (a rename is only atomic within one
280/// filesystem) and is created with the restrictive mode, so the key material is never even
281/// momentarily world-readable.
282async fn write_keys_file(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
283 let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
284 if !parent.as_os_str().is_empty() {
285 tokio::fs::create_dir_all(parent).await?;
286 }
287
288 let file_name = path.file_name().ok_or_else(|| {
289 std::io::Error::new(
290 std::io::ErrorKind::InvalidInput,
291 "keys file path has no file name",
292 )
293 })?;
294 // Unique per process and per call, so two racing writers can't clobber each other's
295 // temporary. The rename is still last-writer-wins, but neither ever observes a torn file.
296 static TMP_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
297 let unique = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
298 let tmp_path = path.with_file_name(format!(
299 ".{}.{}.{unique}.tmp",
300 file_name.to_string_lossy(),
301 std::process::id(),
302 ));
303
304 let mut opts = tokio::fs::OpenOptions::new();
305 opts.write(true).create_new(true);
306 #[cfg(unix)]
307 opts.mode(0o600);
308
309 let write_result = async {
310 let mut file = opts.open(&tmp_path).await?;
311 tokio::io::AsyncWriteExt::write_all(&mut file, bytes).await?;
312 // Durable before the rename: a crash otherwise leaves the renamed-into-place file present
313 // but empty, indistinguishable from a corrupt keys file on restart.
314 file.sync_all().await?;
315 drop(file);
316 tokio::fs::rename(&tmp_path, path).await
317 }
318 .await;
319
320 if write_result.is_err() {
321 // Best effort: don't leave the temporary behind, and don't mask the original error.
322 drop(tokio::fs::remove_file(&tmp_path).await);
323 }
324 write_result
325}
326
327#[derive(Debug)]
328struct RouterInner {
329 /// Serializes [`I2pRouter::destination_from_keys_file`]'s read-check-generate-write sequence.
330 /// Without it, two tasks racing to create the same first-time destination each generate and
331 /// persist a different keypair, leaving the loser holding an identity that doesn't match
332 /// what's on disk. Shared across every clone; does not cover two separate processes racing
333 /// on the same path.
334 keys_file_lock: tokio::sync::Mutex<()>,
335}
336
337impl Drop for RouterInner {
338 fn drop(&mut self) {
339 // SAFETY: `i2pd_terminate` is safe to call unconditionally once `i2pd_init` has run
340 // (guaranteed here -- `RouterInner` is only ever constructed after `start` completed it),
341 // and stops a still-running router itself, so no separate `i2pd_stop` is needed.
342 unsafe {
343 i2pd_sys::i2pd_terminate();
344 }
345 // Only now, with libi2pd fully torn down, may another `start` run `i2pd_init` again.
346 // Releasing rather than latching keeps the ordering: any `start` that observes `false`
347 // here is guaranteed to see the effects of the `terminate` above.
348 ROUTER_RUNNING.store(false, Ordering::Release);
349 }
350}
351
352/// A running libi2pd router instance.
353///
354/// Cheaply [`Clone`]-able: the router is torn down once the last clone drops. Only one may run
355/// per process at a time (see [`I2pError::AlreadyRunning`]), though a new one may be started once
356/// the previous is gone.
357///
358/// Dropping the *last* clone runs libi2pd's network-wide shutdown synchronously on whatever
359/// thread drops it -- there is no async `shutdown()` yet. That includes implicit drops, e.g. a
360/// [`Destination`] or [`crate::I2pStream`] holding the only remaining clone going out of scope at
361/// the end of a request handler, which blocks that runtime thread for the length of the shutdown.
362/// Drop the last clone from a `spawn_blocking` context, or accept the stall.
363#[derive(Clone, Debug)]
364pub struct I2pRouter {
365 /// Held only for its `Drop` side effect (stopping/terminating libi2pd once the last clone
366 /// goes away) -- never read directly.
367 _inner: Arc<RouterInner>,
368}
369
370impl I2pRouter {
371 /// Initializes and starts libi2pd's transport/tunnel/netDb subsystems. `app_name` names the
372 /// data directory libi2pd uses for its own files (router keys, netDb cache) -- unrelated to
373 /// the destination keys file in
374 /// [`destination_from_keys_file`](Self::destination_from_keys_file).
375 ///
376 /// Returns before the network bootstrap finishes, which continues on libi2pd's own threads.
377 /// Creating destinations and connecting streams meanwhile just takes longer; it doesn't fail.
378 ///
379 /// Network participation is left at [`RouterConfig::default`], which carries transit tunnels
380 /// at up to 256 KB/s router-wide. [`start_with_config`](Self::start_with_config) is the only
381 /// opportunity to change that.
382 ///
383 /// # Errors
384 /// Returns [`I2pError::InvalidAppName`] if `app_name` contains an interior NUL byte, or
385 /// [`I2pError::AlreadyRunning`] if an `I2pRouter` is already running in this process.
386 pub async fn start(app_name: impl Into<String>) -> Result<Self, I2pError> {
387 Self::start_with_config(app_name, RouterConfig::default()).await
388 }
389
390 /// [`start`](Self::start), with explicit control over transit tunnels, bandwidth and
391 /// floodfill. See [`RouterConfig`].
392 ///
393 /// # Errors
394 /// As [`start`](Self::start).
395 pub async fn start_with_config(
396 app_name: impl Into<String>,
397 config: RouterConfig,
398 ) -> Result<Self, I2pError> {
399 // Validate before claiming the flag: `app_name` is entirely caller-side, so rejecting a
400 // bad one must not lock out a concurrent, well-formed start.
401 let c_name = CString::new(app_name.into()).map_err(|_| I2pError::InvalidAppName)?;
402
403 if ROUTER_RUNNING.swap(true, Ordering::AcqRel) {
404 return Err(I2pError::AlreadyRunning);
405 }
406 // From here on the flag stays set unless this call produces a `RouterInner` whose `Drop`
407 // clears it. `spawn_blocking` runs its closure even if this future is dropped, so a
408 // cancelled `start` may well have reached `i2pd_init`; clearing the flag on the panic
409 // path would let a later `start` run `i2pd_init` over half-initialized globals.
410 let result = tokio::task::spawn_blocking(move || {
411 // SAFETY: `i2pd_init` must precede every other i2pd-sys call, and no other router is
412 // live (the `ROUTER_RUNNING` swap above, cleared only after `i2pd_terminate` has
413 // returned). The setters in between are exactly where the shim documents them as
414 // taking effect: after init, before start.
415 unsafe {
416 i2pd_sys::i2pd_init(c_name.as_ptr());
417 config.apply();
418 i2pd_sys::i2pd_start();
419 }
420 })
421 .await;
422
423 match result {
424 Ok(()) => Ok(Self {
425 _inner: Arc::new(RouterInner {
426 keys_file_lock: tokio::sync::Mutex::new(()),
427 }),
428 }),
429 Err(_) => Err(I2pError::WorkerPanicked),
430 }
431 }
432
433 /// Whether this build was compiled with the default `transit` feature. When `false`,
434 /// libi2pd's tunnel build-request path is compiled out and
435 /// [`RouterConfig::accepts_transit`]`(true)` is silently ignored.
436 #[must_use]
437 pub fn supports_transit() -> bool {
438 // SAFETY: reads a compile-time constant in the shim; takes no arguments and touches no
439 // router state, so it is safe at any point in the lifecycle, including before `init`.
440 unsafe { i2pd_sys::i2pd_accepts_transit() != 0 }
441 }
442
443 /// Creates a transient destination: a fresh keypair, published to the netDb for the lifetime
444 /// of the returned [`Destination`] only, at a different `.b32.i2p` address every call.
445 ///
446 /// # Errors
447 /// Returns [`I2pError::DestinationCreationFailed`] if libi2pd fails to create it.
448 pub async fn create_transient_destination(&self) -> Result<Destination, I2pError> {
449 let router = self.clone();
450 tokio::task::spawn_blocking(move || {
451 // SAFETY: the router is running (this `I2pRouter` handle proves it); the returned
452 // pointer (possibly null on failure) is immediately handed to `Destination::from_raw`,
453 // which takes ownership and never touches it again on this thread.
454 let ptr = unsafe { i2pd_sys::i2pd_create_transient_destination() };
455 Destination::from_raw(router, ptr)
456 })
457 .await
458 .map_err(|_| I2pError::WorkerPanicked)?
459 }
460
461 /// Generates a persistent-destination keypair as an opaque byte buffer, for
462 /// [`create_persistent_destination`](Self::create_persistent_destination) or for writing
463 /// straight to disk. Most callers want
464 /// [`destination_from_keys_file`](Self::destination_from_keys_file), which does both.
465 ///
466 /// # Errors
467 /// Returns [`I2pError::KeyGenerationFailed`] if libi2pd fails to generate the keypair.
468 pub async fn generate_keys(&self, sig: SigType) -> Result<Vec<u8>, I2pError> {
469 tokio::task::spawn_blocking(move || {
470 let mut buf: *mut u8 = std::ptr::null_mut();
471 let mut len: usize = 0;
472 // SAFETY: `out_buf`/`out_len` are valid, distinct, writable local variables; on
473 // success the returned buffer is immediately copied out and freed via
474 // `i2pd_free_buffer`, matching the shim's ownership contract. The crypto-type arg is
475 // ignored by the shim (see its doc comment) -- ElGamal is passed only as a clear,
476 // self-documenting placeholder.
477 let ok = unsafe {
478 i2pd_sys::i2pd_generate_keys(
479 sig.as_raw(),
480 CryptoType::ElGamal.as_raw(),
481 &raw mut buf,
482 &raw mut len,
483 )
484 };
485 if ok == 0 || buf.is_null() {
486 return Err(I2pError::KeyGenerationFailed);
487 }
488 // SAFETY: `buf`/`len` were just populated by a successful `i2pd_generate_keys` call.
489 let bytes = unsafe { std::slice::from_raw_parts(buf, len) }.to_vec();
490 // SAFETY: `buf` was allocated by `i2pd_generate_keys`, is not freed yet, and `len` is
491 // the length that call reported -- which is what the shim wipes before freeing, so
492 // the private key material does not stay readable in the heap afterwards. Passing a
493 // length that did not come from `i2pd_generate_keys` is what would be unsound here.
494 unsafe { i2pd_sys::i2pd_free_buffer(buf, len) };
495 Ok(bytes)
496 })
497 .await
498 .map_err(|_| I2pError::WorkerPanicked)?
499 }
500
501 /// Creates a destination from a keys buffer produced by
502 /// [`generate_keys`](Self::generate_keys), or read back from wherever it was persisted. Most
503 /// callers want [`destination_from_keys_file`](Self::destination_from_keys_file) instead.
504 ///
505 /// `is_public` publishes this destination's lease set to the netDb, which is what makes it
506 /// findable and so reachable by inbound [`accept`](Destination::accept). Pass `false` only
507 /// for a destination that will exclusively make outbound [`connect`](Destination::connect)
508 /// calls; it can never receive an inbound stream.
509 ///
510 /// `encryption_types` is the set this destination's LeaseSet2 advertises -- see
511 /// [`CryptoType`]. An empty slice gets libi2pd's automatic hybrid default; passing types
512 /// publishes exactly those, first entry preferred, with no automatic extras.
513 ///
514 /// # Errors
515 /// Returns [`I2pError::DestinationCreationFailed`] if `keys` is malformed or libi2pd
516 /// otherwise fails to create the destination.
517 pub async fn create_persistent_destination(
518 &self,
519 keys: Vec<u8>,
520 is_public: bool,
521 encryption_types: &[CryptoType],
522 ) -> Result<Destination, I2pError> {
523 let router = self.clone();
524 let csv = if encryption_types.is_empty() {
525 None
526 } else {
527 Some(
528 encryption_types
529 .iter()
530 .map(|c| c.as_raw().to_string())
531 .collect::<Vec<_>>()
532 .join(","),
533 )
534 };
535 // Infallible: digits and commas only. A `None` degrades to libi2pd's default set rather
536 // than to a silently-empty selection.
537 let csv = csv.and_then(|s| std::ffi::CString::new(s).ok());
538 // Scrubbed on drop however this call ends, including the FFI-failure path.
539 let keys = SecretBytes(keys);
540 tokio::task::spawn_blocking(move || {
541 let csv_ptr = csv.as_deref().map_or(std::ptr::null(), |c| c.as_ptr());
542 // SAFETY: `keys` is a valid, non-empty (checked by the shim) byte buffer alive for
543 // the duration of this call; `csv` (if present) is a valid NUL-terminated C string
544 // alive for the duration of this call too; the returned pointer is handed to
545 // `Destination::from_raw`.
546 let ptr = unsafe {
547 i2pd_sys::i2pd_create_persistent_destination(
548 keys.0.as_ptr(),
549 keys.0.len(),
550 c_int::from(is_public),
551 csv_ptr,
552 )
553 };
554 Destination::from_raw(router, ptr)
555 })
556 .await
557 .map_err(|_| I2pError::WorkerPanicked)?
558 }
559
560 /// Loads a persistent destination's keys from `path`, generating and saving a fresh keypair
561 /// there first if the file doesn't exist. `sig` applies to that first-time generation only;
562 /// an existing file keeps the algorithm its keys were generated with. Reusing the same path
563 /// across restarts keeps the same `.b32.i2p` address.
564 ///
565 /// The file format carries no guarantees beyond what this crate's own version wrote: treat it
566 /// as an opaque blob, don't hand-edit it, and back it up as private key material -- anyone
567 /// who obtains it can impersonate this destination. First-time generation creates it `0600`
568 /// on Unix and writes it atomically. An existing file's permissions are left alone, so this
569 /// won't tighten one another tool created more permissively.
570 ///
571 /// `is_public`/`encryption_types` pass through to
572 /// [`create_persistent_destination`](Self::create_persistent_destination) on *every* load,
573 /// not just first-time generation: reloading with a different `encryption_types` re-publishes
574 /// the LeaseSet2 with the new set. The keys, and so the address, are unaffected either way.
575 ///
576 /// Concurrent calls for the same `path` on this router or its clones are serialized, so only
577 /// one keypair is ever generated per path. Two separate processes racing on the same path can
578 /// still each generate and write their own.
579 ///
580 /// # Errors
581 /// Returns [`I2pError::Io`] if the file can't be read or written, or if it exists but is
582 /// empty -- a corrupt keys file, reported rather than regenerated, since regenerating would
583 /// permanently change this destination's `.b32.i2p` address. Otherwise
584 /// [`I2pError::DestinationCreationFailed`] or [`I2pError::KeyGenerationFailed`], per
585 /// [`create_persistent_destination`](Self::create_persistent_destination) and
586 /// [`generate_keys`](Self::generate_keys).
587 pub async fn destination_from_keys_file(
588 &self,
589 path: impl Into<PathBuf>,
590 is_public: bool,
591 sig: SigType,
592 encryption_types: &[CryptoType],
593 ) -> Result<Destination, I2pError> {
594 let path = path.into();
595 let mut keys = {
596 let _guard = self._inner.keys_file_lock.lock().await;
597 match tokio::fs::read(&path).await {
598 // An empty file is corrupt, not a valid identity. Never regenerate over it: that
599 // would quietly change the destination's permanent `.b32.i2p` address.
600 Ok(bytes) if bytes.is_empty() => {
601 return Err(I2pError::Io(std::io::Error::new(
602 std::io::ErrorKind::InvalidData,
603 "keys file is empty",
604 )));
605 }
606 Ok(bytes) => SecretBytes(bytes),
607 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
608 let generated = SecretBytes(self.generate_keys(sig).await?);
609 write_keys_file(&path, &generated.0)
610 .await
611 .map_err(I2pError::Io)?;
612 generated
613 }
614 Err(e) => return Err(I2pError::Io(e)),
615 }
616 };
617 // `create_persistent_destination` takes ownership and re-wraps it, so the bytes stay
618 // scrubbed-on-drop across the hand-off.
619 let bytes = std::mem::take(&mut keys.0);
620 self.create_persistent_destination(bytes, is_public, encryption_types)
621 .await
622 }
623}
624
625#[cfg(test)]
626#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
627mod tests {
628 use super::{CryptoType, RouterConfig, SecretBytes, SigType, clamp_to_c_int, write_keys_file};
629 use std::os::raw::c_int;
630
631 /// Protocol values baked into every persisted keys file and published identity. Pinned so
632 /// inserting an enum variant can't silently repurpose them.
633 #[test]
634 fn sig_type_raw_values() {
635 assert_eq!(SigType::DsaSha1.as_raw(), 0);
636 assert_eq!(SigType::EcdsaP256.as_raw(), 1);
637 assert_eq!(SigType::EcdsaP384.as_raw(), 2);
638 assert_eq!(SigType::EcdsaP521.as_raw(), 3);
639 // 4-6 are the RSA types, deliberately not exposed -- see `SigType`'s docs.
640 assert_eq!(SigType::Eddsa25519.as_raw(), 7);
641 assert_eq!(SigType::default(), SigType::Eddsa25519);
642 }
643
644 /// As above, for `CryptoKeyType`: a wrong number means peers negotiate the wrong algorithm.
645 /// The gap between `EciesP256` (1) and `EciesX25519` (4) is real.
646 #[test]
647 fn crypto_type_raw_values() {
648 assert_eq!(CryptoType::ElGamal.as_raw(), 0);
649 assert_eq!(CryptoType::EciesP256.as_raw(), 1);
650 assert_eq!(CryptoType::EciesX25519.as_raw(), 4);
651 assert_eq!(CryptoType::EciesMlkem512X25519.as_raw(), 5);
652 assert_eq!(CryptoType::EciesMlkem768X25519.as_raw(), 6);
653 assert_eq!(CryptoType::EciesMlkem1024X25519.as_raw(), 7);
654 assert_eq!(CryptoType::default(), CryptoType::EciesX25519);
655 }
656
657 /// A caller who never touches [`RouterConfig`] gets a router that carries transit for
658 /// strangers and does not serve the netDb. Both are deliberate; pin them against drift.
659 #[test]
660 fn router_config_defaults() {
661 let config = RouterConfig::default();
662 assert!(config.accepts_transit);
663 assert!(!config.floodfill);
664 // `None` defers to i2pd-sys/libi2pd (256 KB/s, 25000 tunnels) rather than duplicating
665 // figures this crate would have to keep in sync.
666 assert_eq!(config.bandwidth_limit_kbps, None);
667 assert_eq!(config.max_transit_tunnels, None);
668 assert_eq!(config.transit_share_percent, 100);
669 }
670
671 #[test]
672 fn router_config_builder_applies_each_setting() {
673 let config = RouterConfig::default()
674 .accepts_transit(false)
675 .bandwidth_limit_kbps(512)
676 .transit_share_percent(25)
677 .max_transit_tunnels(500)
678 .floodfill(true);
679 assert!(!config.accepts_transit);
680 assert_eq!(config.bandwidth_limit_kbps, Some(512));
681 assert_eq!(config.transit_share_percent, 25);
682 assert_eq!(config.max_transit_tunnels, Some(500));
683 assert!(config.floodfill);
684 }
685
686 /// Wrapping a past-`c_int::MAX` limit negative would reach the shim as "restore the default"
687 /// or "ignore this", turning the highest expressible limit into no limit at all.
688 #[test]
689 fn oversized_limits_saturate() {
690 assert_eq!(clamp_to_c_int(None), 0);
691 assert_eq!(clamp_to_c_int(Some(512)), 512);
692 assert_eq!(clamp_to_c_int(Some(u32::MAX)), c_int::MAX);
693 assert!(clamp_to_c_int(Some(u32::MAX)) > 0);
694 }
695
696 /// A keys file *is* the destination's identity. `File::create` would leave it world-readable
697 /// under a typical umask, and a temporary left behind by the atomic write would be a second,
698 /// unmanaged copy of the private key sitting next to the real one.
699 #[tokio::test]
700 async fn keys_file_is_owner_only_and_leaves_no_temporary() {
701 let dir = tempfile::tempdir().unwrap();
702 let path = dir.path().join("server.keys");
703 write_keys_file(&path, b"secret key material")
704 .await
705 .unwrap();
706
707 assert_eq!(
708 tokio::fs::read(&path).await.unwrap(),
709 b"secret key material"
710 );
711
712 #[cfg(unix)]
713 {
714 use std::os::unix::fs::PermissionsExt as _;
715 let mode = tokio::fs::metadata(&path)
716 .await
717 .unwrap()
718 .permissions()
719 .mode();
720 assert_eq!(mode & 0o777, 0o600, "got {:o}", mode & 0o777);
721 }
722
723 let mut entries = tokio::fs::read_dir(dir.path()).await.unwrap();
724 let mut names = Vec::new();
725 while let Some(entry) = entries.next_entry().await.unwrap() {
726 names.push(entry.file_name().to_string_lossy().into_owned());
727 }
728 assert_eq!(names, vec!["server.keys".to_string()]);
729 }
730
731 /// The documented example passes a bare filename, whose parent is the empty path -- that must
732 /// not be mistaken for a directory named "".
733 #[tokio::test]
734 async fn keys_file_creates_missing_parent_directories() {
735 let dir = tempfile::tempdir().unwrap();
736 let path = dir.path().join("nested/deeper/server.keys");
737 write_keys_file(&path, b"secret key material")
738 .await
739 .unwrap();
740 assert!(path.exists());
741 }
742
743 /// A derived `Debug` would dump every key byte into whatever log or panic message formatted
744 /// it.
745 #[test]
746 fn secret_bytes_debug_hides_contents() {
747 let secret = SecretBytes(vec![0xAB; 4]);
748 let rendered = format!("{secret:?}");
749 assert_eq!(rendered, "SecretBytes(4 bytes)");
750 assert!(!rendered.contains("171") && !rendered.to_lowercase().contains("ab"));
751 }
752}