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
29#[allow(dead_code)]
31const HW_MTU: usize = 1064;
32
33const BITRATE_GUESS: u64 = 256_000;
35
36const RECONNECT_WAIT: Duration = Duration::from_secs(15);
38
39#[derive(Debug, Clone)]
41pub struct I2pConfig {
42 pub name: String,
43 pub interface_id: InterfaceId,
44 pub sam_host: String,
46 pub sam_port: u16,
48 pub peers: Vec<String>,
50 pub connectable: bool,
52 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
101struct 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
112fn 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
125fn key_file_path(storage_dir: &Path, name: &str) -> PathBuf {
127 storage_dir.join(format!("i2p_{}.key", sanitize_name(name)))
128}
129
130fn 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 let priv_data = std::fs::read(&key_path).map_err(SamError::Io)?;
143
144 Ok(sam::KeyPair {
163 destination: Destination { data: Vec::new() }, private_key: priv_data,
165 })
166 } else {
167 log::info!("[{}] generating new I2P destination keypair", name);
169 let keypair = sam::dest_generate_with_mark(sam_addr, underlay_mark)?;
170
171 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
182pub 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
207fn 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 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 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 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 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 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 let _keep_alive = control_socket;
311 loop {
312 thread::sleep(Duration::from_secs(3600));
313 }
314}
315
316struct 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 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 peer_addr.to_string()
360 };
361
362 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 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 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; }
429
430 peer_reader_loop(stream, client_id, &iface_name, &tx);
432
433 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
458struct 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 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 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; }
549
550 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
569fn 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; }
599 }
600 }
601 Err(e) => {
602 log::warn!("[{}] I2P peer {} read error: {}", name, id.0, e);
603 return;
604 }
605 }
606 }
607}
608
609use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
612use std::collections::HashMap;
613
614pub 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}