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