1pub 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
29const HW_MTU: usize = 1064;
31
32const BITRATE_GUESS: u64 = 256_000;
34
35const RECONNECT_WAIT: Duration = Duration::from_secs(15);
37
38#[derive(Debug, Clone)]
40pub struct I2pConfig {
41 pub name: String,
42 pub interface_id: InterfaceId,
43 pub sam_host: String,
45 pub sam_port: u16,
47 pub peers: Vec<String>,
49 pub connectable: bool,
51 pub storage_dir: PathBuf,
53 pub ingress_control: rns_core::transport::types::IngressControlConfig,
54 pub underlay_mark: Option<u32>,
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 underlay_mark: None,
90 runtime: Arc::new(Mutex::new(I2pRuntime {
91 reconnect_wait: RECONNECT_WAIT,
92 })),
93 };
94 let startup = I2pRuntime::from_config(&config);
95 config.runtime = Arc::new(Mutex::new(startup));
96 config
97 }
98}
99
100struct I2pWriter {
102 stream: std::net::TcpStream,
103}
104
105impl Writer for I2pWriter {
106 fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
107 self.stream.write_all(&hdlc::frame(data))
108 }
109}
110
111fn sanitize_name(name: &str) -> String {
113 name.chars()
114 .map(|c| {
115 if c.is_alphanumeric() || c == '-' || c == '_' {
116 c
117 } else {
118 '_'
119 }
120 })
121 .collect()
122}
123
124fn key_file_path(storage_dir: &Path, name: &str) -> PathBuf {
126 storage_dir.join(format!("i2p_{}.key", sanitize_name(name)))
127}
128
129fn load_or_generate_keypair(
132 sam_addr: &SocketAddr,
133 storage_dir: &Path,
134 name: &str,
135 underlay_mark: Option<u32>,
136) -> Result<sam::KeyPair, SamError> {
137 let key_path = key_file_path(storage_dir, name);
138
139 if key_path.exists() {
140 let priv_data = std::fs::read(&key_path).map_err(SamError::Io)?;
142
143 Ok(sam::KeyPair {
162 destination: Destination { data: Vec::new() }, private_key: priv_data,
164 })
165 } else {
166 log::info!("[{}] generating new I2P destination keypair", name);
168 let keypair = sam::dest_generate_with_mark(sam_addr, underlay_mark)?;
169
170 if let Some(parent) = key_path.parent() {
172 std::fs::create_dir_all(parent).map_err(SamError::Io)?;
173 }
174 std::fs::write(&key_path, &keypair.private_key).map_err(SamError::Io)?;
175 log::info!("[{}] saved I2P key to {:?}", name, key_path);
176
177 Ok(keypair)
178 }
179}
180
181pub fn start(config: I2pConfig, tx: EventSender, next_id: Arc<AtomicU64>) -> io::Result<()> {
184 start_with_template(config, tx, next_id, None)
185}
186
187fn start_with_template(
188 config: I2pConfig,
189 tx: EventSender,
190 next_id: Arc<AtomicU64>,
191 dynamic_template: Option<super::DynamicInterfaceTemplate>,
192) -> io::Result<()> {
193 let name = config.name.clone();
194
195 thread::Builder::new()
196 .name(format!("i2p-coord-{}", config.interface_id.0))
197 .spawn(move || {
198 if let Err(e) = coordinator(config, tx, next_id, dynamic_template) {
199 log::error!("[{}] I2P coordinator failed: {}", name, e);
200 }
201 })?;
202
203 Ok(())
204}
205
206fn coordinator(
208 config: I2pConfig,
209 tx: EventSender,
210 next_id: Arc<AtomicU64>,
211 dynamic_template: Option<super::DynamicInterfaceTemplate>,
212) -> Result<(), SamError> {
213 let ingress_control = config.ingress_control;
214 let sam_addr: SocketAddr = format!("{}:{}", config.sam_host, config.sam_port)
215 .parse()
216 .map_err(|e| SamError::Io(io::Error::new(io::ErrorKind::InvalidInput, e)))?;
217
218 let keypair = load_or_generate_keypair(
220 &sam_addr,
221 &config.storage_dir,
222 &config.name,
223 config.underlay_mark,
224 )?;
225 let priv_b64 = sam::i2p_base64_encode(&keypair.private_key);
226
227 let session_id = sanitize_name(&config.name);
230
231 log::info!("[{}] creating SAM session (id={})", config.name, session_id);
232 let mut control_socket =
233 sam::session_create_with_mark(&sam_addr, &session_id, &priv_b64, config.underlay_mark)?;
234
235 match sam::naming_lookup_on(&mut control_socket, "ME") {
238 Ok(our_dest) => {
239 let b32 = our_dest.base32_address();
240 log::info!("[{}] I2P address: {}", config.name, b32);
241 }
242 Err(e) => {
243 log::warn!("[{}] could not look up own destination: {}", config.name, e);
244 }
245 }
246
247 for peer_addr in &config.peers {
249 let peer_addr = peer_addr.trim().to_string();
250 if peer_addr.is_empty() {
251 continue;
252 }
253
254 let tx2 = tx.clone();
255 let next_id2 = next_id.clone();
256 let sam_addr2 = sam_addr;
257 let session_id2 = session_id.clone();
258 let iface_name = config.name.clone();
259 let runtime = Arc::clone(&config.runtime);
260 let dynamic_template = dynamic_template.clone();
261
262 thread::Builder::new()
263 .name(format!("i2p-out-{}", peer_addr))
264 .spawn(move || {
265 outbound_peer_loop(OutboundPeerContext {
266 sam_addr: sam_addr2,
267 session_id: session_id2,
268 peer_addr,
269 iface_name,
270 tx2,
271 next_id2,
272 runtime,
273 ingress_control,
274 dynamic_template,
275 underlay_mark: config.underlay_mark,
276 });
277 })
278 .ok();
279 }
280
281 if config.connectable {
283 let tx2 = tx.clone();
284 let next_id2 = next_id.clone();
285 let sam_addr2 = sam_addr;
286 let session_id2 = session_id.clone();
287 let iface_name = config.name.clone();
288 let dynamic_template = dynamic_template.clone();
289
290 thread::Builder::new()
291 .name("i2p-acceptor".into())
292 .spawn(move || {
293 acceptor_loop(AcceptorContext {
294 sam_addr: sam_addr2,
295 session_id: session_id2,
296 iface_name,
297 tx: tx2,
298 next_id: next_id2,
299 ingress_control,
300 dynamic_template,
301 underlay_mark: config.underlay_mark,
302 });
303 })
304 .ok();
305 }
306
307 let _keep_alive = control_socket;
310 loop {
311 thread::sleep(Duration::from_secs(3600));
312 }
313}
314
315struct OutboundPeerContext {
318 sam_addr: SocketAddr,
319 session_id: String,
320 peer_addr: String,
321 iface_name: String,
322 tx2: EventSender,
323 next_id2: Arc<AtomicU64>,
324 runtime: Arc<Mutex<I2pRuntime>>,
325 ingress_control: rns_core::transport::types::IngressControlConfig,
326 dynamic_template: Option<super::DynamicInterfaceTemplate>,
327 underlay_mark: Option<u32>,
328}
329
330fn outbound_peer_loop(context: OutboundPeerContext) {
331 let OutboundPeerContext {
332 sam_addr,
333 session_id,
334 peer_addr,
335 iface_name,
336 tx2: tx,
337 next_id2: next_id,
338 runtime,
339 ingress_control,
340 dynamic_template,
341 underlay_mark,
342 } = context;
343 loop {
344 log::info!("[{}] connecting to I2P peer {}", iface_name, peer_addr);
345
346 let destination = if peer_addr.ends_with(".i2p") {
348 match sam::naming_lookup_with_mark(&sam_addr, &peer_addr, underlay_mark) {
349 Ok(dest) => dest.to_i2p_base64(),
350 Err(e) => {
351 log::warn!("[{}] failed to resolve {}: {}", iface_name, peer_addr, e);
352 thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
353 continue;
354 }
355 }
356 } else {
357 peer_addr.to_string()
359 };
360
361 match sam::stream_connect_with_mark(&sam_addr, &session_id, &destination, underlay_mark) {
363 Ok(stream) => {
364 let client_id = InterfaceId(next_id.fetch_add(1, Ordering::Relaxed));
365
366 log::info!(
367 "[{}] connected to I2P peer {} → id {}",
368 iface_name,
369 peer_addr,
370 client_id.0
371 );
372
373 let writer_stream = match stream.try_clone() {
375 Ok(s) => s,
376 Err(e) => {
377 log::warn!("[{}] failed to clone stream: {}", iface_name, e);
378 thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
379 continue;
380 }
381 };
382
383 let writer: Box<dyn Writer> = Box::new(I2pWriter {
384 stream: writer_stream,
385 });
386
387 let info = InterfaceInfo {
388 id: client_id,
389 name: format!("I2PInterface/{}", peer_addr),
390 mode: constants::MODE_FULL,
391 gravity: 0,
392 recursive_prs: false,
393 announces_from_internal: true,
394 announces_to_internal: None,
395 out_capable: true,
396 in_capable: true,
397 bitrate: Some(BITRATE_GUESS),
398 airtime_profile: None,
399 announce_rate_target: None,
400 announce_rate_grace: 0,
401 announce_rate_penalty: 0.0,
402 announce_cap: constants::ANNOUNCE_CAP,
403 is_local_client: false,
404 wants_tunnel: false,
405 tunnel_id: None,
406 mtu: 65535,
407 ia_freq: 0.0,
408 ip_freq: 0.0,
409 op_freq: 0.0,
410 op_samples: 0,
411 started: 0.0,
412 ingress_control,
413 };
414
415 let event = if let Some(template) = &dynamic_template {
417 Event::DynamicInterfaceUp {
418 id: client_id,
419 writer,
420 registration: Box::new(template.registration(info)),
421 }
422 } else {
423 Event::InterfaceUp(client_id, Some(writer), Some(Box::new(info)))
424 };
425 if tx.send(event).is_err() {
426 return; }
428
429 peer_reader_loop(
431 stream,
432 client_id,
433 &iface_name,
434 &tx,
435 template_ifac_size(dynamic_template.as_ref()),
436 );
437
438 let _ = tx.send(Event::InterfaceDown(client_id));
440 log::warn!(
441 "[{}] I2P peer {} disconnected, reconnecting in {}s",
442 iface_name,
443 peer_addr,
444 lock_or_recover(&runtime, "i2p runtime")
445 .reconnect_wait
446 .as_secs()
447 );
448 }
449 Err(e) => {
450 log::warn!(
451 "[{}] failed to connect to I2P peer {}: {}",
452 iface_name,
453 peer_addr,
454 e
455 );
456 }
457 }
458
459 thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
460 }
461}
462
463struct AcceptorContext {
465 sam_addr: SocketAddr,
466 session_id: String,
467 iface_name: String,
468 tx: EventSender,
469 next_id: Arc<AtomicU64>,
470 ingress_control: rns_core::transport::types::IngressControlConfig,
471 dynamic_template: Option<super::DynamicInterfaceTemplate>,
472 underlay_mark: Option<u32>,
473}
474
475fn acceptor_loop(context: AcceptorContext) {
476 let AcceptorContext {
477 sam_addr,
478 session_id,
479 iface_name,
480 tx,
481 next_id,
482 ingress_control,
483 dynamic_template,
484 underlay_mark,
485 } = context;
486
487 loop {
488 match sam::stream_accept_with_mark(&sam_addr, &session_id, underlay_mark) {
489 Ok((stream, remote_dest)) => {
490 let client_id = InterfaceId(next_id.fetch_add(1, Ordering::Relaxed));
491 let remote_b32 = remote_dest.base32_address();
492
493 log::info!(
494 "[{}] accepted I2P connection from {} → id {}",
495 iface_name,
496 remote_b32,
497 client_id.0
498 );
499
500 let writer_stream = match stream.try_clone() {
502 Ok(s) => s,
503 Err(e) => {
504 log::warn!("[{}] failed to clone accepted stream: {}", iface_name, e);
505 continue;
506 }
507 };
508
509 let writer: Box<dyn Writer> = Box::new(I2pWriter {
510 stream: writer_stream,
511 });
512
513 let info = InterfaceInfo {
514 id: client_id,
515 name: format!("I2PInterface/{}", remote_b32),
516 mode: constants::MODE_FULL,
517 gravity: 0,
518 recursive_prs: false,
519 announces_from_internal: true,
520 announces_to_internal: None,
521 out_capable: true,
522 in_capable: true,
523 bitrate: Some(BITRATE_GUESS),
524 airtime_profile: None,
525 announce_rate_target: None,
526 announce_rate_grace: 0,
527 announce_rate_penalty: 0.0,
528 announce_cap: constants::ANNOUNCE_CAP,
529 is_local_client: false,
530 wants_tunnel: false,
531 tunnel_id: None,
532 mtu: 65535,
533 ia_freq: 0.0,
534 ip_freq: 0.0,
535 op_freq: 0.0,
536 op_samples: 0,
537 started: 0.0,
538 ingress_control,
539 };
540
541 let event = if let Some(template) = &dynamic_template {
543 Event::DynamicInterfaceUp {
544 id: client_id,
545 writer,
546 registration: Box::new(template.registration(info)),
547 }
548 } else {
549 Event::InterfaceUp(client_id, Some(writer), Some(Box::new(info)))
550 };
551 if tx.send(event).is_err() {
552 return; }
554
555 let client_tx = tx.clone();
557 let client_name = iface_name.to_string();
558 let ifac_size = template_ifac_size(dynamic_template.as_ref());
559 thread::Builder::new()
560 .name(format!("i2p-client-{}", client_id.0))
561 .spawn(move || {
562 peer_reader_loop(stream, client_id, &client_name, &client_tx, ifac_size);
563 let _ = client_tx.send(Event::InterfaceDown(client_id));
564 })
565 .ok();
566 }
567 Err(e) => {
568 log::warn!("[{}] I2P accept failed: {}, retrying", iface_name, e);
569 thread::sleep(Duration::from_secs(1));
570 }
571 }
572 }
573}
574
575fn template_ifac_size(template: Option<&super::DynamicInterfaceTemplate>) -> usize {
577 template
578 .and_then(|template| template.ifac.as_ref())
579 .map_or(0, |ifac| ifac.size)
580}
581
582fn frame_decoder(ifac_size: usize) -> hdlc::Decoder {
584 hdlc::Decoder::reticulum(HW_MTU, ifac_size)
585}
586
587fn peer_reader_loop(
590 mut stream: std::net::TcpStream,
591 id: InterfaceId,
592 name: &str,
593 tx: &EventSender,
594 ifac_size: usize,
595) {
596 let mut decoder = frame_decoder(ifac_size);
597 let mut buf = [0u8; 4096];
598
599 loop {
600 match stream.read(&mut buf) {
601 Ok(0) => {
602 log::info!("[{}] I2P peer {} disconnected", name, id.0);
603 return;
604 }
605 Ok(n) => {
606 for frame in decoder.feed(&buf[..n]) {
607 if tx
608 .send(Event::Frame {
609 interface_id: id,
610 data: frame,
611 rssi: None,
612 snr: None,
613 })
614 .is_err()
615 {
616 return; }
618 }
619 }
620 Err(e) => {
621 log::warn!("[{}] I2P peer {} read error: {}", name, id.0, e);
622 return;
623 }
624 }
625 }
626}
627
628use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
631use std::collections::HashMap;
632
633pub struct I2pFactory;
635
636impl InterfaceFactory for I2pFactory {
637 fn type_name(&self) -> &str {
638 "I2PInterface"
639 }
640
641 fn parse_config(
642 &self,
643 name: &str,
644 id: InterfaceId,
645 params: &HashMap<String, String>,
646 ) -> Result<Box<dyn InterfaceConfigData>, String> {
647 let sam_host = params
648 .get("sam_host")
649 .cloned()
650 .unwrap_or_else(|| "127.0.0.1".into());
651
652 let sam_port = params
653 .get("sam_port")
654 .and_then(|v| v.parse::<u16>().ok())
655 .unwrap_or(7656);
656
657 let connectable = params
658 .get("connectable")
659 .and_then(|v| crate::config::parse_bool_pub(v))
660 .unwrap_or(false);
661
662 let peers = params
663 .get("peers")
664 .map(|v| {
665 v.split(',')
666 .map(|s| s.trim().to_string())
667 .filter(|s| !s.is_empty())
668 .collect::<Vec<String>>()
669 })
670 .unwrap_or_default();
671
672 let storage_dir = params
673 .get("storage_dir")
674 .map(PathBuf::from)
675 .unwrap_or_else(|| PathBuf::from("/tmp/rns-i2p"));
676
677 Ok(Box::new(I2pConfig {
678 name: name.to_string(),
679 interface_id: id,
680 sam_host,
681 sam_port,
682 connectable,
683 peers,
684 storage_dir,
685 ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
686 underlay_mark: None,
687 runtime: Arc::new(Mutex::new(I2pRuntime {
688 reconnect_wait: RECONNECT_WAIT,
689 })),
690 }))
691 }
692
693 fn start(
694 &self,
695 config: Box<dyn InterfaceConfigData>,
696 ctx: StartContext,
697 ) -> io::Result<StartResult> {
698 let mut cfg = *config
699 .into_any()
700 .downcast::<I2pConfig>()
701 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
702 cfg.ingress_control = ctx.ingress_control;
703 cfg.underlay_mark = ctx.underlay_mark;
704 let parent_id = cfg.interface_id;
705 start_with_template(
706 cfg,
707 ctx.tx,
708 ctx.next_dynamic_id,
709 Some(super::DynamicInterfaceTemplate {
710 parent_id,
711 interface_type: "I2PInterfacePeer".into(),
712 ifac: ctx.ifac,
713 mode: ctx.mode,
714 gravity: ctx.gravity,
715 recursive_prs: ctx.recursive_prs,
716 announces_from_internal: ctx.announces_from_internal,
717 announces_to_internal: ctx.announces_to_internal,
718 }),
719 )?;
720 Ok(StartResult::Listener { control: None })
721 }
722}
723
724pub(crate) fn i2p_runtime_handle_from_config(config: &I2pConfig) -> I2pRuntimeConfigHandle {
725 I2pRuntimeConfigHandle {
726 interface_name: config.name.clone(),
727 runtime: Arc::clone(&config.runtime),
728 startup: I2pRuntime::from_config(config),
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 #[test]
737 fn frame_decoder_accepts_frames_up_to_hw_mtu_plus_ifac() {
738 let ifac_size = 16;
739 let mut decoder = frame_decoder(ifac_size);
740 let payload = vec![0x5a; HW_MTU + ifac_size];
741 let decoded = decoder.feed_with_diagnostics(&hdlc::frame(&payload));
742 assert_eq!(decoded.frames, vec![payload]);
743 assert!(decoded.invalid_frame_lengths.is_empty());
744 }
745
746 #[test]
747 fn frame_decoder_drops_frame_above_hw_mtu_as_invalid() {
748 let mut decoder = frame_decoder(0);
749 let decoded = decoder.feed_with_diagnostics(&hdlc::frame(&vec![0x5a; HW_MTU + 1]));
750 assert!(decoded.frames.is_empty());
751 assert_eq!(decoded.invalid_frame_lengths, vec![HW_MTU + 1]);
752 }
753}