tachyon-i2p 0.0.4

Safe async wrapper around i2pd-sys (native I2P `.b32.i2p` eepsite support)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
//! [`I2pRouter`]: the process-wide libi2pd instance.

use crate::destination::Destination;
use crate::error::I2pError;
use std::ffi::CString;
use std::os::raw::c_int;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

/// Set while a router is live, cleared once the last [`I2pRouter`] clone has been dropped and
/// libi2pd has been torn down.
///
/// libi2pd's router context is a process-wide global, so at most one router can be live at a
/// time. Sequentially is fine: i2pd-sys 0.0.5's shim holds one mutex across all four lifecycle
/// calls and has `i2pd_terminate` stop a still-running router itself, making
/// `init -> start -> terminate -> init -> start` a supported cycle.
///
/// Latched permanently in one case: a panic inside the worker running `i2pd_init`/`i2pd_start`
/// leaves libi2pd's globals in an unknown state, and a fresh `init` over that is what the shim's
/// mutex cannot make safe.
static ROUTER_RUNNING: AtomicBool = AtomicBool::new(false);

/// The signature algorithm for a destination's identity -- see [`I2pRouter::generate_keys`].
///
/// Values are libi2pd's protocol-level `SigningKeyType` (`libi2pd/Identity.h`). Two families
/// from the I2P spec are deliberately absent: RSA, which is a `su3` file-signing type that
/// libi2pd's `GenerateSigningKeyPair` silently substitutes EdDSA for; and RedDSA-SHA512-Ed25519,
/// which is for LeaseSet2 blinding, not identity signing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum SigType {
    /// DSA-SHA1. Superseded as the network default in 2015; for old persisted keys only.
    DsaSha1,
    /// ECDSA-SHA256 on the P-256 curve.
    EcdsaP256,
    /// ECDSA-SHA384 on the P-384 curve. Not widely used on the I2P network.
    EcdsaP384,
    /// ECDSA-SHA512 on the P-521 curve. Not widely used on the I2P network.
    EcdsaP521,
    /// Ed25519 (EdDSA-SHA512). The I2P network default since 0.9.15; use this for new
    /// destinations.
    #[default]
    Eddsa25519,
}

impl SigType {
    pub(crate) const fn as_raw(self) -> c_int {
        match self {
            Self::DsaSha1 => 0,
            Self::EcdsaP256 => 1,
            Self::EcdsaP384 => 2,
            Self::EcdsaP521 => 3,
            Self::Eddsa25519 => 7,
        }
    }
}

/// An encryption algorithm a destination's LeaseSet2 can advertise as usable -- see
/// [`I2pRouter::create_persistent_destination`]'s `encryption_types` parameter.
///
/// Values are libi2pd's protocol-level `CryptoKeyType` (`libi2pd/Identity.h`). This covers only
/// the advertised encryption capability, never the identity certificate, which is ElGamal either
/// way (see [`I2pRouter::generate_keys`]).
///
/// An empty `encryption_types` slice already gets libi2pd's hybrid default
/// (ElGamal + ECIES-X25519, plus ML-KEM-768 on a post-quantum-capable backend); explicit values
/// only ever *narrow* that. The `EciesMlkem*` variants are new enough that older peers may not
/// understand a destination advertising one, so they trade reachability for long-term
/// confidentiality.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CryptoType {
    /// The original ElGamal scheme. For old persisted keys only.
    ElGamal,
    /// ECIES on the P-256 curve with AES-256-CBC. Not widely used on the I2P network.
    EciesP256,
    /// ECIES-X25519-AEAD (ChaCha20/Poly1305). The I2P network's current default.
    #[default]
    EciesX25519,
    /// ECIES-X25519-AEAD hybridized with ML-KEM-512 (NIST PQC category 1).
    EciesMlkem512X25519,
    /// ECIES-X25519-AEAD hybridized with ML-KEM-768 (NIST PQC category 3).
    EciesMlkem768X25519,
    /// ECIES-X25519-AEAD hybridized with ML-KEM-1024 (NIST PQC category 5).
    EciesMlkem1024X25519,
}

impl CryptoType {
    pub(crate) const fn as_raw(self) -> c_int {
        match self {
            Self::ElGamal => 0,
            Self::EciesP256 => 1,
            Self::EciesX25519 => 4,
            Self::EciesMlkem512X25519 => 5,
            Self::EciesMlkem768X25519 => 6,
            Self::EciesMlkem1024X25519 => 7,
        }
    }

    /// Whether the linked crypto backend can actually perform this algorithm. Always `true` for
    /// the classical types; for the `EciesMlkem*` ones it runs a real
    /// generate/encapsulate/decapsulate round trip through libi2pd's `MLKEMKeys` rather than
    /// checking a feature flag. Sub-millisecond but not free -- cache it, don't call it per
    /// connection. Needs no running router.
    ///
    /// Check this before naming an `EciesMlkem*` type in
    /// [`create_persistent_destination`](I2pRouter::create_persistent_destination): a LeaseSet2
    /// advertising only algorithms this router cannot perform leaves the destination
    /// unreachable, with nothing in the address or the creation result to say so.
    #[must_use]
    pub fn is_supported(self) -> bool {
        let mlkem_variant = match self {
            Self::ElGamal | Self::EciesP256 | Self::EciesX25519 => return true,
            Self::EciesMlkem512X25519 => 0,
            Self::EciesMlkem768X25519 => 1,
            Self::EciesMlkem1024X25519 => 2,
        };
        // SAFETY: the shim validates the variant number itself (returning 0 for anything outside
        // 0-2), touches only AWS-LC's own key machinery rather than libi2pd's router globals, and
        // takes no pointers -- so this is safe at any point, including before `i2pd_init`.
        unsafe { i2pd_sys::i2pd_test_mlkem_roundtrip(mlkem_variant) != 0 }
    }
}

/// How this router participates in the wider I2P network -- see
/// [`I2pRouter::start_with_config`].
///
/// libi2pd normally reads all of this from an `i2pd.conf`, which the library entry point never
/// parses (only upstream's daemon did), so these settings are the only way to reach them. They
/// apply between libi2pd's `init` and `start`; nothing here can be changed on a running router.
///
/// [`Default`] mirrors i2pd-sys's own: transit on, 256 KB/s, no share limit, libi2pd's own
/// transit tunnel ceiling, floodfill off.
///
/// ```
/// use tachyon_i2p::RouterConfig;
///
/// // Keep carrying transit (the anonymity-preserving default), but bound what it costs.
/// let config = RouterConfig::default()
///     .bandwidth_limit_kbps(512)
///     .transit_share_percent(25)
///     .max_transit_tunnels(500);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RouterConfig {
    accepts_transit: bool,
    bandwidth_limit_kbps: Option<u32>,
    transit_share_percent: u8,
    max_transit_tunnels: Option<u32>,
    floodfill: bool,
}

impl Default for RouterConfig {
    fn default() -> Self {
        Self {
            accepts_transit: true,
            bandwidth_limit_kbps: None,
            transit_share_percent: 100,
            max_transit_tunnels: None,
            floodfill: false,
        }
    }
}

impl RouterConfig {
    /// Whether to carry *other* users' tunnels (default: `true`).
    ///
    /// Transit is unrelated to your own destinations, which work either way. It defaults to on
    /// because a router that relays nothing gives an observer no cover traffic: every byte
    /// crossing the link is then yours, and the refusal is itself a fingerprint. Turn it off when
    /// bandwidth is metered, or when the risk you care about is a memory-safety bug in libi2pd
    /// rather than traffic analysis.
    ///
    /// Building without the default `transit` feature is the stronger form of `false`: libi2pd's
    /// tunnel build-request path is compiled out, so `true` here is silently ignored and
    /// [`I2pRouter::supports_transit`] reports `false`.
    #[must_use]
    pub const fn accepts_transit(mut self, enabled: bool) -> Self {
        self.accepts_transit = enabled;
        self
    }

    /// Whole-router bandwidth ceiling in KB/s (default: 256).
    ///
    /// A limit is always in force, and 0 is not "unlimited" -- it falls back to the same 256 KB/s
    /// default as never calling this. At a genuine 0 the router publishes itself as permanently
    /// congested and refuses transit outright, so i2pd-sys applies 256 KB/s at init, below
    /// upstream's 2048 KB/s daemon default (which assumes a dedicated router, not a library
    /// sharing a server's uplink).
    #[must_use]
    pub const fn bandwidth_limit_kbps(mut self, kbps: u32) -> Self {
        self.bandwidth_limit_kbps = Some(kbps);
        self
    }

    /// What percentage of [`bandwidth_limit_kbps`](Self::bandwidth_limit_kbps) transit tunnels
    /// may use (default: 100). Values above 100 are clamped.
    #[must_use]
    pub const fn transit_share_percent(mut self, percent: u8) -> Self {
        self.transit_share_percent = percent;
        self
    }

    /// Ceiling on concurrently-carried transit tunnels (default: libi2pd's own 25000, a
    /// daemon-scale figure worth lowering for a library embedding).
    #[must_use]
    pub const fn max_transit_tunnels(mut self, max: u32) -> Self {
        self.max_transit_tunnels = Some(max);
        self
    }

    /// Whether to serve the distributed netDb (default: `false`).
    ///
    /// A floodfill router stores and answers lookups for the whole network's lease sets: a lot of
    /// extra traffic, and a lot of extra attacker-supplied input parsed in-process. Leave it off
    /// unless running a floodfill is the point.
    #[must_use]
    pub const fn floodfill(mut self, enabled: bool) -> Self {
        self.floodfill = enabled;
        self
    }

    /// Must run between `i2pd_init` and `i2pd_start`; anything outside that window is a no-op.
    fn apply(&self) {
        // SAFETY: every setter here takes plain integers, tolerates any value (percentages are
        // clamped shim-side, non-positive limits fall back to a default), and is called from the
        // same `spawn_blocking` worker as `i2pd_init`/`i2pd_start`, in between the two.
        unsafe {
            i2pd_sys::i2pd_set_accepts_transit(c_int::from(self.accepts_transit));
            i2pd_sys::i2pd_set_bandwidth_limit(clamp_to_c_int(self.bandwidth_limit_kbps));
            i2pd_sys::i2pd_set_share_percent(c_int::from(self.transit_share_percent));
            i2pd_sys::i2pd_set_max_transit_tunnels(clamp_to_c_int(self.max_transit_tunnels));
            i2pd_sys::i2pd_set_floodfill(c_int::from(self.floodfill));
        }
    }
}

/// `0` is the shim's "leave at the default". Oversized values saturate rather than wrapping
/// negative, which the shim would read as "restore the default" (bandwidth) or "ignore" (transit
/// tunnels) -- silently discarding a caller's very high limit.
const fn clamp_to_c_int(value: Option<u32>) -> c_int {
    match value {
        None => 0,
        Some(v) if v > c_int::MAX as u32 => c_int::MAX,
        Some(v) => v as c_int,
    }
}

/// Owns a destination's serialized private keys and scrubs them on drop.
///
/// These bytes are the destination's identity: anyone holding a copy can impersonate the
/// eepsite. A plain `Vec<u8>` would leave them in freed heap memory after every load/generate,
/// recoverable by a later heap disclosure bug, a core dump, or swap.
struct SecretBytes(Vec<u8>);

impl Drop for SecretBytes {
    fn drop(&mut self) {
        for b in &mut self.0 {
            // SAFETY: `b` is a live, uniquely-borrowed, properly-aligned `u8`. Volatile so the
            // write survives an optimizer that can see the buffer is dead afterwards.
            unsafe { std::ptr::write_volatile(b, 0) };
        }
        std::sync::atomic::compiler_fence(Ordering::SeqCst);
    }
}

impl std::fmt::Debug for SecretBytes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Never render the key material itself.
        write!(f, "SecretBytes({} bytes)", self.0.len())
    }
}

/// Writes `bytes` to `path` as private key material: owner-only, and atomically, so an
/// interrupted write can't leave a half-written keys file in place of a good one.
///
/// The temporary goes in the *same directory* as `path` (a rename is only atomic within one
/// filesystem) and is created with the restrictive mode, so the key material is never even
/// momentarily world-readable.
async fn write_keys_file(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
    let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
    if !parent.as_os_str().is_empty() {
        tokio::fs::create_dir_all(parent).await?;
    }

    let file_name = path.file_name().ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "keys file path has no file name",
        )
    })?;
    // Unique per process and per call, so two racing writers can't clobber each other's
    // temporary. The rename is still last-writer-wins, but neither ever observes a torn file.
    static TMP_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let unique = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
    let tmp_path = path.with_file_name(format!(
        ".{}.{}.{unique}.tmp",
        file_name.to_string_lossy(),
        std::process::id(),
    ));

    let mut opts = tokio::fs::OpenOptions::new();
    opts.write(true).create_new(true);
    #[cfg(unix)]
    opts.mode(0o600);

    let write_result = async {
        let mut file = opts.open(&tmp_path).await?;
        tokio::io::AsyncWriteExt::write_all(&mut file, bytes).await?;
        // Durable before the rename: a crash otherwise leaves the renamed-into-place file present
        // but empty, indistinguishable from a corrupt keys file on restart.
        file.sync_all().await?;
        drop(file);
        tokio::fs::rename(&tmp_path, path).await
    }
    .await;

    if write_result.is_err() {
        // Best effort: don't leave the temporary behind, and don't mask the original error.
        drop(tokio::fs::remove_file(&tmp_path).await);
    }
    write_result
}

#[derive(Debug)]
struct RouterInner {
    /// Serializes [`I2pRouter::destination_from_keys_file`]'s read-check-generate-write sequence.
    /// Without it, two tasks racing to create the same first-time destination each generate and
    /// persist a different keypair, leaving the loser holding an identity that doesn't match
    /// what's on disk. Shared across every clone; does not cover two separate processes racing
    /// on the same path.
    keys_file_lock: tokio::sync::Mutex<()>,
}

impl Drop for RouterInner {
    fn drop(&mut self) {
        // SAFETY: `i2pd_terminate` is safe to call unconditionally once `i2pd_init` has run
        // (guaranteed here -- `RouterInner` is only ever constructed after `start` completed it),
        // and stops a still-running router itself, so no separate `i2pd_stop` is needed.
        unsafe {
            i2pd_sys::i2pd_terminate();
        }
        // Only now, with libi2pd fully torn down, may another `start` run `i2pd_init` again.
        // Releasing rather than latching keeps the ordering: any `start` that observes `false`
        // here is guaranteed to see the effects of the `terminate` above.
        ROUTER_RUNNING.store(false, Ordering::Release);
    }
}

/// A running libi2pd router instance.
///
/// Cheaply [`Clone`]-able: the router is torn down once the last clone drops. Only one may run
/// per process at a time (see [`I2pError::AlreadyRunning`]), though a new one may be started once
/// the previous is gone.
///
/// Dropping the *last* clone runs libi2pd's network-wide shutdown synchronously on whatever
/// thread drops it -- there is no async `shutdown()` yet. That includes implicit drops, e.g. a
/// [`Destination`] or [`crate::I2pStream`] holding the only remaining clone going out of scope at
/// the end of a request handler, which blocks that runtime thread for the length of the shutdown.
/// Drop the last clone from a `spawn_blocking` context, or accept the stall.
#[derive(Clone, Debug)]
pub struct I2pRouter {
    /// Held only for its `Drop` side effect (stopping/terminating libi2pd once the last clone
    /// goes away) -- never read directly.
    _inner: Arc<RouterInner>,
}

impl I2pRouter {
    /// Initializes and starts libi2pd's transport/tunnel/netDb subsystems. `app_name` names the
    /// data directory libi2pd uses for its own files (router keys, netDb cache) -- unrelated to
    /// the destination keys file in
    /// [`destination_from_keys_file`](Self::destination_from_keys_file).
    ///
    /// Returns before the network bootstrap finishes, which continues on libi2pd's own threads.
    /// Creating destinations and connecting streams meanwhile just takes longer; it doesn't fail.
    ///
    /// Network participation is left at [`RouterConfig::default`], which carries transit tunnels
    /// at up to 256 KB/s router-wide. [`start_with_config`](Self::start_with_config) is the only
    /// opportunity to change that.
    ///
    /// # Errors
    /// Returns [`I2pError::InvalidAppName`] if `app_name` contains an interior NUL byte, or
    /// [`I2pError::AlreadyRunning`] if an `I2pRouter` is already running in this process.
    pub async fn start(app_name: impl Into<String>) -> Result<Self, I2pError> {
        Self::start_with_config(app_name, RouterConfig::default()).await
    }

    /// [`start`](Self::start), with explicit control over transit tunnels, bandwidth and
    /// floodfill. See [`RouterConfig`].
    ///
    /// # Errors
    /// As [`start`](Self::start).
    pub async fn start_with_config(
        app_name: impl Into<String>,
        config: RouterConfig,
    ) -> Result<Self, I2pError> {
        // Validate before claiming the flag: `app_name` is entirely caller-side, so rejecting a
        // bad one must not lock out a concurrent, well-formed start.
        let c_name = CString::new(app_name.into()).map_err(|_| I2pError::InvalidAppName)?;

        if ROUTER_RUNNING.swap(true, Ordering::AcqRel) {
            return Err(I2pError::AlreadyRunning);
        }
        // From here on the flag stays set unless this call produces a `RouterInner` whose `Drop`
        // clears it. `spawn_blocking` runs its closure even if this future is dropped, so a
        // cancelled `start` may well have reached `i2pd_init`; clearing the flag on the panic
        // path would let a later `start` run `i2pd_init` over half-initialized globals.
        let result = tokio::task::spawn_blocking(move || {
            // SAFETY: `i2pd_init` must precede every other i2pd-sys call, and no other router is
            // live (the `ROUTER_RUNNING` swap above, cleared only after `i2pd_terminate` has
            // returned). The setters in between are exactly where the shim documents them as
            // taking effect: after init, before start.
            unsafe {
                i2pd_sys::i2pd_init(c_name.as_ptr());
                config.apply();
                i2pd_sys::i2pd_start();
            }
        })
        .await;

        match result {
            Ok(()) => Ok(Self {
                _inner: Arc::new(RouterInner {
                    keys_file_lock: tokio::sync::Mutex::new(()),
                }),
            }),
            Err(_) => Err(I2pError::WorkerPanicked),
        }
    }

    /// Whether this build was compiled with the default `transit` feature. When `false`,
    /// libi2pd's tunnel build-request path is compiled out and
    /// [`RouterConfig::accepts_transit`]`(true)` is silently ignored.
    #[must_use]
    pub fn supports_transit() -> bool {
        // SAFETY: reads a compile-time constant in the shim; takes no arguments and touches no
        // router state, so it is safe at any point in the lifecycle, including before `init`.
        unsafe { i2pd_sys::i2pd_accepts_transit() != 0 }
    }

    /// Creates a transient destination: a fresh keypair, published to the netDb for the lifetime
    /// of the returned [`Destination`] only, at a different `.b32.i2p` address every call.
    ///
    /// # Errors
    /// Returns [`I2pError::DestinationCreationFailed`] if libi2pd fails to create it.
    pub async fn create_transient_destination(&self) -> Result<Destination, I2pError> {
        let router = self.clone();
        tokio::task::spawn_blocking(move || {
            // SAFETY: the router is running (this `I2pRouter` handle proves it); the returned
            // pointer (possibly null on failure) is immediately handed to `Destination::from_raw`,
            // which takes ownership and never touches it again on this thread.
            let ptr = unsafe { i2pd_sys::i2pd_create_transient_destination() };
            Destination::from_raw(router, ptr)
        })
        .await
        .map_err(|_| I2pError::WorkerPanicked)?
    }

    /// Generates a persistent-destination keypair as an opaque byte buffer, for
    /// [`create_persistent_destination`](Self::create_persistent_destination) or for writing
    /// straight to disk. Most callers want
    /// [`destination_from_keys_file`](Self::destination_from_keys_file), which does both.
    ///
    /// # Errors
    /// Returns [`I2pError::KeyGenerationFailed`] if libi2pd fails to generate the keypair.
    pub async fn generate_keys(&self, sig: SigType) -> Result<Vec<u8>, I2pError> {
        tokio::task::spawn_blocking(move || {
            let mut buf: *mut u8 = std::ptr::null_mut();
            let mut len: usize = 0;
            // SAFETY: `out_buf`/`out_len` are valid, distinct, writable local variables; on
            // success the returned buffer is immediately copied out and freed via
            // `i2pd_free_buffer`, matching the shim's ownership contract. The crypto-type arg is
            // ignored by the shim (see its doc comment) -- ElGamal is passed only as a clear,
            // self-documenting placeholder.
            let ok = unsafe {
                i2pd_sys::i2pd_generate_keys(
                    sig.as_raw(),
                    CryptoType::ElGamal.as_raw(),
                    &raw mut buf,
                    &raw mut len,
                )
            };
            if ok == 0 || buf.is_null() {
                return Err(I2pError::KeyGenerationFailed);
            }
            // SAFETY: `buf`/`len` were just populated by a successful `i2pd_generate_keys` call.
            let bytes = unsafe { std::slice::from_raw_parts(buf, len) }.to_vec();
            // SAFETY: `buf` was allocated by `i2pd_generate_keys`, is not freed yet, and `len` is
            // the length that call reported -- which is what the shim wipes before freeing, so
            // the private key material does not stay readable in the heap afterwards. Passing a
            // length that did not come from `i2pd_generate_keys` is what would be unsound here.
            unsafe { i2pd_sys::i2pd_free_buffer(buf, len) };
            Ok(bytes)
        })
        .await
        .map_err(|_| I2pError::WorkerPanicked)?
    }

    /// Creates a destination from a keys buffer produced by
    /// [`generate_keys`](Self::generate_keys), or read back from wherever it was persisted. Most
    /// callers want [`destination_from_keys_file`](Self::destination_from_keys_file) instead.
    ///
    /// `is_public` publishes this destination's lease set to the netDb, which is what makes it
    /// findable and so reachable by inbound [`accept`](Destination::accept). Pass `false` only
    /// for a destination that will exclusively make outbound [`connect`](Destination::connect)
    /// calls; it can never receive an inbound stream.
    ///
    /// `encryption_types` is the set this destination's LeaseSet2 advertises -- see
    /// [`CryptoType`]. An empty slice gets libi2pd's automatic hybrid default; passing types
    /// publishes exactly those, first entry preferred, with no automatic extras.
    ///
    /// # Errors
    /// Returns [`I2pError::DestinationCreationFailed`] if `keys` is malformed or libi2pd
    /// otherwise fails to create the destination.
    pub async fn create_persistent_destination(
        &self,
        keys: Vec<u8>,
        is_public: bool,
        encryption_types: &[CryptoType],
    ) -> Result<Destination, I2pError> {
        let router = self.clone();
        let csv = if encryption_types.is_empty() {
            None
        } else {
            Some(
                encryption_types
                    .iter()
                    .map(|c| c.as_raw().to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            )
        };
        // Infallible: digits and commas only. A `None` degrades to libi2pd's default set rather
        // than to a silently-empty selection.
        let csv = csv.and_then(|s| std::ffi::CString::new(s).ok());
        // Scrubbed on drop however this call ends, including the FFI-failure path.
        let keys = SecretBytes(keys);
        tokio::task::spawn_blocking(move || {
            let csv_ptr = csv.as_deref().map_or(std::ptr::null(), |c| c.as_ptr());
            // SAFETY: `keys` is a valid, non-empty (checked by the shim) byte buffer alive for
            // the duration of this call; `csv` (if present) is a valid NUL-terminated C string
            // alive for the duration of this call too; the returned pointer is handed to
            // `Destination::from_raw`.
            let ptr = unsafe {
                i2pd_sys::i2pd_create_persistent_destination(
                    keys.0.as_ptr(),
                    keys.0.len(),
                    c_int::from(is_public),
                    csv_ptr,
                )
            };
            Destination::from_raw(router, ptr)
        })
        .await
        .map_err(|_| I2pError::WorkerPanicked)?
    }

    /// Loads a persistent destination's keys from `path`, generating and saving a fresh keypair
    /// there first if the file doesn't exist. `sig` applies to that first-time generation only;
    /// an existing file keeps the algorithm its keys were generated with. Reusing the same path
    /// across restarts keeps the same `.b32.i2p` address.
    ///
    /// The file format carries no guarantees beyond what this crate's own version wrote: treat it
    /// as an opaque blob, don't hand-edit it, and back it up as private key material -- anyone
    /// who obtains it can impersonate this destination. First-time generation creates it `0600`
    /// on Unix and writes it atomically. An existing file's permissions are left alone, so this
    /// won't tighten one another tool created more permissively.
    ///
    /// `is_public`/`encryption_types` pass through to
    /// [`create_persistent_destination`](Self::create_persistent_destination) on *every* load,
    /// not just first-time generation: reloading with a different `encryption_types` re-publishes
    /// the LeaseSet2 with the new set. The keys, and so the address, are unaffected either way.
    ///
    /// Concurrent calls for the same `path` on this router or its clones are serialized, so only
    /// one keypair is ever generated per path. Two separate processes racing on the same path can
    /// still each generate and write their own.
    ///
    /// # Errors
    /// Returns [`I2pError::Io`] if the file can't be read or written, or if it exists but is
    /// empty -- a corrupt keys file, reported rather than regenerated, since regenerating would
    /// permanently change this destination's `.b32.i2p` address. Otherwise
    /// [`I2pError::DestinationCreationFailed`] or [`I2pError::KeyGenerationFailed`], per
    /// [`create_persistent_destination`](Self::create_persistent_destination) and
    /// [`generate_keys`](Self::generate_keys).
    pub async fn destination_from_keys_file(
        &self,
        path: impl Into<PathBuf>,
        is_public: bool,
        sig: SigType,
        encryption_types: &[CryptoType],
    ) -> Result<Destination, I2pError> {
        let path = path.into();
        let mut keys = {
            let _guard = self._inner.keys_file_lock.lock().await;
            match tokio::fs::read(&path).await {
                // An empty file is corrupt, not a valid identity. Never regenerate over it: that
                // would quietly change the destination's permanent `.b32.i2p` address.
                Ok(bytes) if bytes.is_empty() => {
                    return Err(I2pError::Io(std::io::Error::new(
                        std::io::ErrorKind::InvalidData,
                        "keys file is empty",
                    )));
                }
                Ok(bytes) => SecretBytes(bytes),
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
                    let generated = SecretBytes(self.generate_keys(sig).await?);
                    write_keys_file(&path, &generated.0)
                        .await
                        .map_err(I2pError::Io)?;
                    generated
                }
                Err(e) => return Err(I2pError::Io(e)),
            }
        };
        // `create_persistent_destination` takes ownership and re-wraps it, so the bytes stay
        // scrubbed-on-drop across the hand-off.
        let bytes = std::mem::take(&mut keys.0);
        self.create_persistent_destination(bytes, is_public, encryption_types)
            .await
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::{CryptoType, RouterConfig, SecretBytes, SigType, clamp_to_c_int, write_keys_file};
    use std::os::raw::c_int;

    /// Protocol values baked into every persisted keys file and published identity. Pinned so
    /// inserting an enum variant can't silently repurpose them.
    #[test]
    fn sig_type_raw_values() {
        assert_eq!(SigType::DsaSha1.as_raw(), 0);
        assert_eq!(SigType::EcdsaP256.as_raw(), 1);
        assert_eq!(SigType::EcdsaP384.as_raw(), 2);
        assert_eq!(SigType::EcdsaP521.as_raw(), 3);
        // 4-6 are the RSA types, deliberately not exposed -- see `SigType`'s docs.
        assert_eq!(SigType::Eddsa25519.as_raw(), 7);
        assert_eq!(SigType::default(), SigType::Eddsa25519);
    }

    /// As above, for `CryptoKeyType`: a wrong number means peers negotiate the wrong algorithm.
    /// The gap between `EciesP256` (1) and `EciesX25519` (4) is real.
    #[test]
    fn crypto_type_raw_values() {
        assert_eq!(CryptoType::ElGamal.as_raw(), 0);
        assert_eq!(CryptoType::EciesP256.as_raw(), 1);
        assert_eq!(CryptoType::EciesX25519.as_raw(), 4);
        assert_eq!(CryptoType::EciesMlkem512X25519.as_raw(), 5);
        assert_eq!(CryptoType::EciesMlkem768X25519.as_raw(), 6);
        assert_eq!(CryptoType::EciesMlkem1024X25519.as_raw(), 7);
        assert_eq!(CryptoType::default(), CryptoType::EciesX25519);
    }

    /// A caller who never touches [`RouterConfig`] gets a router that carries transit for
    /// strangers and does not serve the netDb. Both are deliberate; pin them against drift.
    #[test]
    fn router_config_defaults() {
        let config = RouterConfig::default();
        assert!(config.accepts_transit);
        assert!(!config.floodfill);
        // `None` defers to i2pd-sys/libi2pd (256 KB/s, 25000 tunnels) rather than duplicating
        // figures this crate would have to keep in sync.
        assert_eq!(config.bandwidth_limit_kbps, None);
        assert_eq!(config.max_transit_tunnels, None);
        assert_eq!(config.transit_share_percent, 100);
    }

    #[test]
    fn router_config_builder_applies_each_setting() {
        let config = RouterConfig::default()
            .accepts_transit(false)
            .bandwidth_limit_kbps(512)
            .transit_share_percent(25)
            .max_transit_tunnels(500)
            .floodfill(true);
        assert!(!config.accepts_transit);
        assert_eq!(config.bandwidth_limit_kbps, Some(512));
        assert_eq!(config.transit_share_percent, 25);
        assert_eq!(config.max_transit_tunnels, Some(500));
        assert!(config.floodfill);
    }

    /// Wrapping a past-`c_int::MAX` limit negative would reach the shim as "restore the default"
    /// or "ignore this", turning the highest expressible limit into no limit at all.
    #[test]
    fn oversized_limits_saturate() {
        assert_eq!(clamp_to_c_int(None), 0);
        assert_eq!(clamp_to_c_int(Some(512)), 512);
        assert_eq!(clamp_to_c_int(Some(u32::MAX)), c_int::MAX);
        assert!(clamp_to_c_int(Some(u32::MAX)) > 0);
    }

    /// A keys file *is* the destination's identity. `File::create` would leave it world-readable
    /// under a typical umask, and a temporary left behind by the atomic write would be a second,
    /// unmanaged copy of the private key sitting next to the real one.
    #[tokio::test]
    async fn keys_file_is_owner_only_and_leaves_no_temporary() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("server.keys");
        write_keys_file(&path, b"secret key material")
            .await
            .unwrap();

        assert_eq!(
            tokio::fs::read(&path).await.unwrap(),
            b"secret key material"
        );

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt as _;
            let mode = tokio::fs::metadata(&path)
                .await
                .unwrap()
                .permissions()
                .mode();
            assert_eq!(mode & 0o777, 0o600, "got {:o}", mode & 0o777);
        }

        let mut entries = tokio::fs::read_dir(dir.path()).await.unwrap();
        let mut names = Vec::new();
        while let Some(entry) = entries.next_entry().await.unwrap() {
            names.push(entry.file_name().to_string_lossy().into_owned());
        }
        assert_eq!(names, vec!["server.keys".to_string()]);
    }

    /// The documented example passes a bare filename, whose parent is the empty path -- that must
    /// not be mistaken for a directory named "".
    #[tokio::test]
    async fn keys_file_creates_missing_parent_directories() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("nested/deeper/server.keys");
        write_keys_file(&path, b"secret key material")
            .await
            .unwrap();
        assert!(path.exists());
    }

    /// A derived `Debug` would dump every key byte into whatever log or panic message formatted
    /// it.
    #[test]
    fn secret_bytes_debug_hides_contents() {
        let secret = SecretBytes(vec![0xAB; 4]);
        let rendered = format!("{secret:?}");
        assert_eq!(rendered, "SecretBytes(4 bytes)");
        assert!(!rendered.contains("171") && !rendered.to_lowercase().contains("ab"));
    }
}