1pub 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#[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 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
99struct 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
110fn 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
123fn key_file_path(storage_dir: &PathBuf, name: &str) -> PathBuf {
125 storage_dir.join(format!("i2p_{}.key", sanitize_name(name)))
126}
127
128fn 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 let priv_data = std::fs::read(&key_path).map_err(SamError::Io)?;
140
141 Ok(sam::KeyPair {
160 destination: Destination { data: Vec::new() }, private_key: priv_data,
162 })
163 } else {
164 log::info!("[{}] generating new I2P destination keypair", name);
166 let keypair = sam::dest_generate(sam_addr)?;
167
168 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
179pub 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
204fn 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 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 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 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 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 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 let _keep_alive = control_socket;
300 loop {
301 thread::sleep(Duration::from_secs(3600));
302 }
303}
304
305fn 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 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 peer_addr.to_string()
334 };
335
336 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 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 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; }
403
404 peer_reader_loop(stream, client_id, iface_name, &tx);
406
407 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
432fn 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 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 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; }
509
510 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
529fn 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; }
559 }
560 }
561 Err(e) => {
562 log::warn!("[{}] I2P peer {} read error: {}", name, id.0, e);
563 return;
564 }
565 }
566 }
567}
568
569use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
572use std::collections::HashMap;
573
574pub 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}