Skip to main content

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}