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