Skip to main content

rns_net/interface/i2p/
mod.rs

1//! I2P interface using the SAM v3.1 protocol.
2//!
3//! Provides anonymous transport over the I2P network. The interface manages
4//! both outbound peer connections and inbound connection acceptance.
5//! Each peer connection becomes a dynamic interface with HDLC framing,
6//! similar to TCP server client connections.
7//!
8//! Matches Python `I2PInterface` from `I2PInterface.py`.
9
10mod folded;
11pub mod sam;
12
13use std::io::{self, Read, Write};
14use std::net::SocketAddr;
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, Mutex};
18use std::thread;
19use std::time::Duration;
20
21use rns_core::constants;
22use rns_core::transport::types::{InterfaceId, InterfaceInfo};
23
24use crate::event::{Event, EventSender};
25use crate::hdlc;
26use crate::interface::{lock_or_recover, Writer};
27
28use self::sam::{Destination, SamError};
29
30/// Hardware MTU for I2P streams (matches Python I2PInterface).
31const HW_MTU: usize = 1064;
32
33/// Estimated bitrate for I2P tunnels (256 kbps, matches Python).
34const BITRATE_GUESS: u64 = 256_000;
35
36/// Wait time before reconnecting to an outbound peer.
37const RECONNECT_WAIT: Duration = Duration::from_secs(15);
38
39/// Configuration for the I2P interface.
40#[derive(Debug, Clone)]
41pub struct I2pConfig {
42    pub name: String,
43    pub interface_id: InterfaceId,
44    /// SAM bridge host (default "127.0.0.1").
45    pub sam_host: String,
46    /// SAM bridge port (default 7656).
47    pub sam_port: u16,
48    /// List of .b32.i2p peer addresses (or full base64 destinations) to connect to.
49    pub peers: Vec<String>,
50    /// Whether to accept inbound connections.
51    pub connectable: bool,
52    /// Directory for key persistence (typically `{config_dir}/storage`).
53    pub storage_dir: PathBuf,
54    pub ingress_control: rns_core::transport::types::IngressControlConfig,
55    pub underlay_mark: Option<u32>,
56    pub runtime: Arc<Mutex<I2pRuntime>>,
57}
58
59#[derive(Debug, Clone)]
60pub struct I2pRuntime {
61    pub reconnect_wait: Duration,
62}
63
64impl I2pRuntime {
65    pub fn from_config(_config: &I2pConfig) -> Self {
66        Self {
67            reconnect_wait: RECONNECT_WAIT,
68        }
69    }
70}
71
72#[derive(Debug, Clone)]
73pub struct I2pRuntimeConfigHandle {
74    pub interface_name: String,
75    pub runtime: Arc<Mutex<I2pRuntime>>,
76    pub startup: I2pRuntime,
77}
78
79impl Default for I2pConfig {
80    fn default() -> Self {
81        let mut config = I2pConfig {
82            name: String::new(),
83            interface_id: InterfaceId(0),
84            sam_host: "127.0.0.1".into(),
85            sam_port: 7656,
86            peers: Vec::new(),
87            connectable: false,
88            storage_dir: PathBuf::from("."),
89            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
90            underlay_mark: None,
91            runtime: Arc::new(Mutex::new(I2pRuntime {
92                reconnect_wait: RECONNECT_WAIT,
93            })),
94        };
95        let startup = I2pRuntime::from_config(&config);
96        config.runtime = Arc::new(Mutex::new(startup));
97        config
98    }
99}
100
101/// Writer that sends HDLC-framed data over an I2P SAM stream.
102struct I2pWriter {
103    stream: std::net::TcpStream,
104}
105
106impl Writer for I2pWriter {
107    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
108        self.stream.write_all(&hdlc::frame(data))
109    }
110
111    fn loop_socket(&self) -> Option<std::os::fd::RawFd> {
112        // With interface I/O folded the driver writes the stream itself.
113        use std::os::fd::AsRawFd;
114        crate::engine::FOLD_IO.then(|| self.stream.as_raw_fd())
115    }
116}
117
118/// The interface a connected peer gets, named after `remote`.
119fn peer_info(
120    client_id: InterfaceId,
121    remote: &str,
122    ingress_control: rns_core::transport::types::IngressControlConfig,
123) -> InterfaceInfo {
124    InterfaceInfo {
125        stamp_costs: Default::default(),
126        id: client_id,
127        name: format!("I2PInterface/{}", remote),
128        mode: constants::MODE_FULL,
129        gravity: 0,
130        recursive_prs: false,
131        announces_from_internal: true,
132        announces_to_internal: None,
133        out_capable: true,
134        in_capable: true,
135        bitrate: Some(BITRATE_GUESS),
136        airtime_profile: None,
137        announce_rate_target: None,
138        announce_rate_grace: 0,
139        announce_rate_penalty: 0.0,
140        announce_cap: constants::ANNOUNCE_CAP,
141        is_local_client: false,
142        wants_tunnel: false,
143        tunnel_id: None,
144        mtu: 65535,
145        ia_freq: 0.0,
146        ip_freq: 0.0,
147        op_freq: 0.0,
148        op_samples: 0,
149        started: 0.0,
150        ingress_control,
151    }
152}
153
154/// The event that brings up a connected peer's interface.
155fn peer_up(
156    client_id: InterfaceId,
157    writer: Box<dyn Writer>,
158    info: InterfaceInfo,
159    template: Option<&super::DynamicInterfaceTemplate>,
160) -> Event {
161    match template {
162        Some(template) => Event::DynamicInterfaceUp {
163            id: client_id,
164            writer,
165            registration: Box::new(template.registration(info)),
166        },
167        None => Event::InterfaceUp(client_id, Some(writer), Some(Box::new(info))),
168    }
169}
170
171/// Sanitize a name for use in filenames and SAM session IDs.
172fn sanitize_name(name: &str) -> String {
173    name.chars()
174        .map(|c| {
175            if c.is_alphanumeric() || c == '-' || c == '_' {
176                c
177            } else {
178                '_'
179            }
180        })
181        .collect()
182}
183
184/// Key file path for the given interface name.
185fn key_file_path(storage_dir: &Path, name: &str) -> PathBuf {
186    storage_dir.join(format!("i2p_{}.key", sanitize_name(name)))
187}
188
189/// Load or generate an I2P keypair. Persists the private key to disk
190/// so the node keeps a stable I2P address across restarts.
191fn load_or_generate_keypair(
192    sam_addr: &SocketAddr,
193    storage_dir: &Path,
194    name: &str,
195    underlay_mark: Option<u32>,
196) -> Result<sam::KeyPair, SamError> {
197    let key_path = key_file_path(storage_dir, name);
198
199    if key_path.exists() {
200        // Load existing private key
201        let priv_data = std::fs::read(&key_path).map_err(SamError::Io)?;
202
203        // We need the public destination too. Create a temporary session to extract it,
204        // or we can derive it by creating a session and reading back the destination.
205        // Simpler: store the full keypair. Let's re-generate from the private key
206        // by creating a session (the destination is embedded in the private key).
207        // Actually, the I2P private key blob contains the destination as a prefix.
208        // We can extract the destination from the private key.
209        // The destination is the first portion of the private key blob.
210        // For simplicity, store the full PRIV blob (which includes the destination).
211
212        // The PRIV blob from DEST GENERATE contains both the destination and private keys.
213        // The destination is extractable as the first ~387 bytes, but the exact size
214        // depends on the key type. For Ed25519 (sig type 7), the destination is:
215        //   256 bytes (encryption public key) + 128 bytes (signing public key) + 3 bytes (certificate)
216        // = 387 bytes. But this can vary with certificate content.
217
218        // Rather than parsing, we pass the full PRIV to SESSION CREATE (which accepts it)
219        // and then do a NAMING LOOKUP on "ME" to get our destination.
220
221        Ok(sam::KeyPair {
222            destination: Destination { data: Vec::new() }, // filled in later
223            private_key: priv_data,
224        })
225    } else {
226        // Generate new keypair
227        log::info!("[{}] generating new I2P destination keypair", name);
228        let keypair = sam::dest_generate_with_mark(sam_addr, underlay_mark)?;
229
230        // Save private key to disk
231        if let Some(parent) = key_path.parent() {
232            std::fs::create_dir_all(parent).map_err(SamError::Io)?;
233        }
234        std::fs::write(&key_path, &keypair.private_key).map_err(SamError::Io)?;
235        log::info!("[{}] saved I2P key to {:?}", name, key_path);
236
237        Ok(keypair)
238    }
239}
240
241/// Start the I2P interface coordinator. All peer connections are registered
242/// as dynamic interfaces via InterfaceUp events.
243pub fn start(config: I2pConfig, tx: EventSender, next_id: Arc<AtomicU64>) -> io::Result<()> {
244    start_with_template(config, tx, next_id, None)
245}
246
247fn start_with_template(
248    config: I2pConfig,
249    tx: EventSender,
250    next_id: Arc<AtomicU64>,
251    dynamic_template: Option<super::DynamicInterfaceTemplate>,
252) -> io::Result<()> {
253    let name = config.name.clone();
254
255    if crate::engine::FOLD_IO {
256        thread::Builder::new()
257            .name(format!("i2p-setup-{}", config.interface_id.0))
258            .spawn(move || {
259                if let Err(e) = folded::setup(config, tx, next_id, dynamic_template) {
260                    log::error!("[{}] I2P setup failed: {}", name, e);
261                }
262            })?;
263        return Ok(());
264    }
265
266    thread::Builder::new()
267        .name(format!("i2p-coord-{}", config.interface_id.0))
268        .spawn(move || {
269            if let Err(e) = coordinator(config, tx, next_id, dynamic_template) {
270                log::error!("[{}] I2P coordinator failed: {}", name, e);
271            }
272        })?;
273
274    Ok(())
275}
276
277/// Coordinator thread: sets up SAM sessions and spawns peer threads.
278fn coordinator(
279    config: I2pConfig,
280    tx: EventSender,
281    next_id: Arc<AtomicU64>,
282    dynamic_template: Option<super::DynamicInterfaceTemplate>,
283) -> Result<(), SamError> {
284    let ingress_control = config.ingress_control;
285    let sam_addr: SocketAddr = format!("{}:{}", config.sam_host, config.sam_port)
286        .parse()
287        .map_err(|e| SamError::Io(io::Error::new(io::ErrorKind::InvalidInput, e)))?;
288
289    // Load or generate keypair
290    let keypair = load_or_generate_keypair(
291        &sam_addr,
292        &config.storage_dir,
293        &config.name,
294        config.underlay_mark,
295    )?;
296    let priv_b64 = sam::i2p_base64_encode(&keypair.private_key);
297
298    // We use a single session for all streams (outbound + inbound).
299    // Session ID must be unique; use the interface name sanitized.
300    let session_id = sanitize_name(&config.name);
301
302    log::info!("[{}] creating SAM session (id={})", config.name, session_id);
303    let mut control_socket =
304        sam::session_create_with_mark(&sam_addr, &session_id, &priv_b64, config.underlay_mark)?;
305
306    // Look up our own destination via NAMING LOOKUP "ME" on the session control socket.
307    // "ME" requires a session context on the same connection.
308    match sam::naming_lookup_on(&mut control_socket, "ME") {
309        Ok(our_dest) => {
310            let b32 = our_dest.base32_address();
311            log::info!("[{}] I2P address: {}", config.name, b32);
312        }
313        Err(e) => {
314            log::warn!("[{}] could not look up own destination: {}", config.name, e);
315        }
316    }
317
318    // Spawn outbound peer threads
319    for peer_addr in &config.peers {
320        let peer_addr = peer_addr.trim().to_string();
321        if peer_addr.is_empty() {
322            continue;
323        }
324
325        let tx2 = tx.clone();
326        let next_id2 = next_id.clone();
327        let sam_addr2 = sam_addr;
328        let session_id2 = session_id.clone();
329        let iface_name = config.name.clone();
330        let runtime = Arc::clone(&config.runtime);
331        let dynamic_template = dynamic_template.clone();
332
333        thread::Builder::new()
334            .name(format!("i2p-out-{}", peer_addr))
335            .spawn(move || {
336                outbound_peer_loop(OutboundPeerContext {
337                    sam_addr: sam_addr2,
338                    session_id: session_id2,
339                    peer_addr,
340                    iface_name,
341                    tx2,
342                    next_id2,
343                    runtime,
344                    ingress_control,
345                    dynamic_template,
346                    underlay_mark: config.underlay_mark,
347                });
348            })
349            .ok();
350    }
351
352    // Spawn acceptor thread if connectable
353    if config.connectable {
354        let tx2 = tx.clone();
355        let next_id2 = next_id.clone();
356        let sam_addr2 = sam_addr;
357        let session_id2 = session_id.clone();
358        let iface_name = config.name.clone();
359        let dynamic_template = dynamic_template.clone();
360
361        thread::Builder::new()
362            .name("i2p-acceptor".into())
363            .spawn(move || {
364                acceptor_loop(AcceptorContext {
365                    sam_addr: sam_addr2,
366                    session_id: session_id2,
367                    iface_name,
368                    tx: tx2,
369                    next_id: next_id2,
370                    ingress_control,
371                    dynamic_template,
372                    underlay_mark: config.underlay_mark,
373                });
374            })
375            .ok();
376    }
377
378    // The control socket (and this thread) must remain alive for the session's lifetime.
379    // We park the coordinator thread here. If control_socket drops, the session ends.
380    let _keep_alive = control_socket;
381    loop {
382        thread::sleep(Duration::from_secs(3600));
383    }
384}
385
386/// Outbound peer thread: connects to a remote I2P destination, runs HDLC reader loop.
387/// Reconnects on failure.
388struct OutboundPeerContext {
389    sam_addr: SocketAddr,
390    session_id: String,
391    peer_addr: String,
392    iface_name: String,
393    tx2: EventSender,
394    next_id2: Arc<AtomicU64>,
395    runtime: Arc<Mutex<I2pRuntime>>,
396    ingress_control: rns_core::transport::types::IngressControlConfig,
397    dynamic_template: Option<super::DynamicInterfaceTemplate>,
398    underlay_mark: Option<u32>,
399}
400
401fn outbound_peer_loop(context: OutboundPeerContext) {
402    let OutboundPeerContext {
403        sam_addr,
404        session_id,
405        peer_addr,
406        iface_name,
407        tx2: tx,
408        next_id2: next_id,
409        runtime,
410        ingress_control,
411        dynamic_template,
412        underlay_mark,
413    } = context;
414    loop {
415        log::info!("[{}] connecting to I2P peer {}", iface_name, peer_addr);
416
417        // Resolve .b32.i2p address if needed
418        let destination = if peer_addr.ends_with(".i2p") {
419            match sam::naming_lookup_with_mark(&sam_addr, &peer_addr, underlay_mark) {
420                Ok(dest) => dest.to_i2p_base64(),
421                Err(e) => {
422                    log::warn!("[{}] failed to resolve {}: {}", iface_name, peer_addr, e);
423                    thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
424                    continue;
425                }
426            }
427        } else {
428            // Assume it's already a full base64 destination
429            peer_addr.to_string()
430        };
431
432        // Connect via SAM
433        match sam::stream_connect_with_mark(&sam_addr, &session_id, &destination, underlay_mark) {
434            Ok(stream) => {
435                let client_id = InterfaceId(next_id.fetch_add(1, Ordering::Relaxed));
436
437                log::info!(
438                    "[{}] connected to I2P peer {} → id {}",
439                    iface_name,
440                    peer_addr,
441                    client_id.0
442                );
443
444                // Clone stream for writer/reader split
445                let writer_stream = match stream.try_clone() {
446                    Ok(s) => s,
447                    Err(e) => {
448                        log::warn!("[{}] failed to clone stream: {}", iface_name, e);
449                        thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
450                        continue;
451                    }
452                };
453
454                let writer: Box<dyn Writer> = Box::new(I2pWriter {
455                    stream: writer_stream,
456                });
457
458                let event = peer_up(
459                    client_id,
460                    writer,
461                    peer_info(client_id, &peer_addr, ingress_control),
462                    dynamic_template.as_ref(),
463                );
464                if tx.send(event).is_err() {
465                    return; // Driver shut down
466                }
467
468                // Run HDLC reader loop (blocks until disconnect)
469                peer_reader_loop(
470                    stream,
471                    client_id,
472                    &iface_name,
473                    &tx,
474                    template_ifac_size(dynamic_template.as_ref()),
475                );
476
477                // Disconnected
478                let _ = tx.send(Event::InterfaceDown(client_id));
479                log::warn!(
480                    "[{}] I2P peer {} disconnected, reconnecting in {}s",
481                    iface_name,
482                    peer_addr,
483                    lock_or_recover(&runtime, "i2p runtime")
484                        .reconnect_wait
485                        .as_secs()
486                );
487            }
488            Err(e) => {
489                log::warn!(
490                    "[{}] failed to connect to I2P peer {}: {}",
491                    iface_name,
492                    peer_addr,
493                    e
494                );
495            }
496        }
497
498        thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
499    }
500}
501
502/// Acceptor thread: loops accepting inbound connections on the session.
503struct AcceptorContext {
504    sam_addr: SocketAddr,
505    session_id: String,
506    iface_name: String,
507    tx: EventSender,
508    next_id: Arc<AtomicU64>,
509    ingress_control: rns_core::transport::types::IngressControlConfig,
510    dynamic_template: Option<super::DynamicInterfaceTemplate>,
511    underlay_mark: Option<u32>,
512}
513
514fn acceptor_loop(context: AcceptorContext) {
515    let AcceptorContext {
516        sam_addr,
517        session_id,
518        iface_name,
519        tx,
520        next_id,
521        ingress_control,
522        dynamic_template,
523        underlay_mark,
524    } = context;
525
526    loop {
527        match sam::stream_accept_with_mark(&sam_addr, &session_id, underlay_mark) {
528            Ok((stream, remote_dest)) => {
529                let client_id = InterfaceId(next_id.fetch_add(1, Ordering::Relaxed));
530                let remote_b32 = remote_dest.base32_address();
531
532                log::info!(
533                    "[{}] accepted I2P connection from {} → id {}",
534                    iface_name,
535                    remote_b32,
536                    client_id.0
537                );
538
539                // Clone stream for writer/reader split
540                let writer_stream = match stream.try_clone() {
541                    Ok(s) => s,
542                    Err(e) => {
543                        log::warn!("[{}] failed to clone accepted stream: {}", iface_name, e);
544                        continue;
545                    }
546                };
547
548                let writer: Box<dyn Writer> = Box::new(I2pWriter {
549                    stream: writer_stream,
550                });
551
552                let event = peer_up(
553                    client_id,
554                    writer,
555                    peer_info(client_id, &remote_b32, ingress_control),
556                    dynamic_template.as_ref(),
557                );
558                if tx.send(event).is_err() {
559                    return; // Driver shut down
560                }
561
562                // Spawn per-client reader thread (accepted peers don't reconnect)
563                let client_tx = tx.clone();
564                let client_name = iface_name.to_string();
565                let ifac_size = template_ifac_size(dynamic_template.as_ref());
566                thread::Builder::new()
567                    .name(format!("i2p-client-{}", client_id.0))
568                    .spawn(move || {
569                        peer_reader_loop(stream, client_id, &client_name, &client_tx, ifac_size);
570                        let _ = client_tx.send(Event::InterfaceDown(client_id));
571                    })
572                    .ok();
573            }
574            Err(e) => {
575                log::warn!("[{}] I2P accept failed: {}, retrying", iface_name, e);
576                thread::sleep(Duration::from_secs(1));
577            }
578        }
579    }
580}
581
582/// IFAC bytes a spawned peer connection may carry on the wire.
583fn template_ifac_size(template: Option<&super::DynamicInterfaceTemplate>) -> usize {
584    template
585        .and_then(|template| template.ifac.as_ref())
586        .map_or(0, |ifac| ifac.size)
587}
588
589/// HDLC decoder bounded by the I2P hardware MTU plus on-wire IFAC bytes.
590fn frame_decoder(ifac_size: usize) -> hdlc::Decoder {
591    hdlc::Decoder::reticulum(HW_MTU, ifac_size)
592}
593
594/// Per-peer HDLC reader loop. Reads from the SAM data stream, decodes HDLC
595/// frames, and sends them to the driver. Returns when the stream is closed.
596fn peer_reader_loop(
597    mut stream: std::net::TcpStream,
598    id: InterfaceId,
599    name: &str,
600    tx: &EventSender,
601    ifac_size: usize,
602) {
603    let mut decoder = frame_decoder(ifac_size);
604    let mut buf = [0u8; 4096];
605
606    loop {
607        match stream.read(&mut buf) {
608            Ok(0) => {
609                log::info!("[{}] I2P peer {} disconnected", name, id.0);
610                return;
611            }
612            Ok(n) => {
613                for frame in decoder.feed(&buf[..n]) {
614                    if tx
615                        .send(Event::Frame {
616                            interface_id: id,
617                            data: frame.into(),
618                            rssi: None,
619                            snr: None,
620                        })
621                        .is_err()
622                    {
623                        return; // Driver shut down
624                    }
625                }
626            }
627            Err(e) => {
628                log::warn!("[{}] I2P peer {} read error: {}", name, id.0, e);
629                return;
630            }
631        }
632    }
633}
634
635// --- Factory implementation ---
636
637use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
638use std::collections::HashMap;
639
640/// Factory for `I2PInterface`.
641pub struct I2pFactory;
642
643impl InterfaceFactory for I2pFactory {
644    fn type_name(&self) -> &str {
645        "I2PInterface"
646    }
647
648    fn parse_config(
649        &self,
650        name: &str,
651        id: InterfaceId,
652        params: &HashMap<String, String>,
653    ) -> Result<Box<dyn InterfaceConfigData>, String> {
654        let sam_host = params
655            .get("sam_host")
656            .cloned()
657            .unwrap_or_else(|| "127.0.0.1".into());
658
659        let sam_port = params
660            .get("sam_port")
661            .and_then(|v| v.parse::<u16>().ok())
662            .unwrap_or(7656);
663
664        let connectable = params
665            .get("connectable")
666            .and_then(|v| crate::config::parse_bool_pub(v))
667            .unwrap_or(false);
668
669        let peers = params
670            .get("peers")
671            .map(|v| {
672                v.split(',')
673                    .map(|s| s.trim().to_string())
674                    .filter(|s| !s.is_empty())
675                    .collect::<Vec<String>>()
676            })
677            .unwrap_or_default();
678
679        let storage_dir = params
680            .get("storage_dir")
681            .map(PathBuf::from)
682            .unwrap_or_else(|| PathBuf::from("/tmp/rns-i2p"));
683
684        Ok(Box::new(I2pConfig {
685            name: name.to_string(),
686            interface_id: id,
687            sam_host,
688            sam_port,
689            connectable,
690            peers,
691            storage_dir,
692            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
693            underlay_mark: None,
694            runtime: Arc::new(Mutex::new(I2pRuntime {
695                reconnect_wait: RECONNECT_WAIT,
696            })),
697        }))
698    }
699
700    fn start(
701        &self,
702        config: Box<dyn InterfaceConfigData>,
703        ctx: StartContext,
704    ) -> io::Result<StartResult> {
705        let mut cfg = *config
706            .into_any()
707            .downcast::<I2pConfig>()
708            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
709        cfg.ingress_control = ctx.ingress_control;
710        cfg.underlay_mark = ctx.underlay_mark;
711        let parent_id = cfg.interface_id;
712        start_with_template(
713            cfg,
714            ctx.tx,
715            ctx.next_dynamic_id,
716            Some(super::DynamicInterfaceTemplate {
717                parent_id,
718                interface_type: "I2PInterfacePeer".into(),
719                ifac: ctx.ifac,
720                mode: ctx.mode,
721                gravity: ctx.gravity,
722                recursive_prs: ctx.recursive_prs,
723                announces_from_internal: ctx.announces_from_internal,
724                announces_to_internal: ctx.announces_to_internal,
725                stamp_costs: ctx.stamp_costs,
726            }),
727        )?;
728        Ok(StartResult::Listener { control: None })
729    }
730}
731
732pub(crate) fn i2p_runtime_handle_from_config(config: &I2pConfig) -> I2pRuntimeConfigHandle {
733    I2pRuntimeConfigHandle {
734        interface_name: config.name.clone(),
735        runtime: Arc::clone(&config.runtime),
736        startup: I2pRuntime::from_config(config),
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn frame_decoder_accepts_frames_up_to_hw_mtu_plus_ifac() {
746        let ifac_size = 16;
747        let mut decoder = frame_decoder(ifac_size);
748        let payload = vec![0x5a; HW_MTU + ifac_size];
749        let decoded = decoder.feed_with_diagnostics(&hdlc::frame(&payload));
750        assert_eq!(decoded.frames, vec![payload]);
751        assert!(decoded.invalid_frame_lengths.is_empty());
752    }
753
754    #[test]
755    fn frame_decoder_drops_frame_above_hw_mtu_as_invalid() {
756        let mut decoder = frame_decoder(0);
757        let decoded = decoder.feed_with_diagnostics(&hdlc::frame(&vec![0x5a; HW_MTU + 1]));
758        assert!(decoded.frames.is_empty());
759        assert_eq!(decoded.invalid_frame_lengths, vec![HW_MTU + 1]);
760    }
761}