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 let name = config.name.clone();
183
184 thread::Builder::new()
185 .name(format!("i2p-coord-{}", config.interface_id.0))
186 .spawn(move || {
187 if let Err(e) = coordinator(config, tx, next_id) {
188 log::error!("[{}] I2P coordinator failed: {}", name, e);
189 }
190 })?;
191
192 Ok(())
193}
194
195fn coordinator(
197 config: I2pConfig,
198 tx: EventSender,
199 next_id: Arc<AtomicU64>,
200) -> Result<(), SamError> {
201 let ingress_control = config.ingress_control;
202 let sam_addr: SocketAddr = format!("{}:{}", config.sam_host, config.sam_port)
203 .parse()
204 .map_err(|e| SamError::Io(io::Error::new(io::ErrorKind::InvalidInput, e)))?;
205
206 let keypair = load_or_generate_keypair(&sam_addr, &config.storage_dir, &config.name)?;
208 let priv_b64 = sam::i2p_base64_encode(&keypair.private_key);
209
210 let session_id = sanitize_name(&config.name);
213
214 log::info!("[{}] creating SAM session (id={})", config.name, session_id);
215 let mut control_socket = sam::session_create(&sam_addr, &session_id, &priv_b64)?;
216
217 match sam::naming_lookup_on(&mut control_socket, "ME") {
220 Ok(our_dest) => {
221 let b32 = our_dest.base32_address();
222 log::info!("[{}] I2P address: {}", config.name, b32);
223 }
224 Err(e) => {
225 log::warn!("[{}] could not look up own destination: {}", config.name, e);
226 }
227 }
228
229 for peer_addr in &config.peers {
231 let peer_addr = peer_addr.trim().to_string();
232 if peer_addr.is_empty() {
233 continue;
234 }
235
236 let tx2 = tx.clone();
237 let next_id2 = next_id.clone();
238 let sam_addr2 = sam_addr;
239 let session_id2 = session_id.clone();
240 let iface_name = config.name.clone();
241 let runtime = Arc::clone(&config.runtime);
242
243 thread::Builder::new()
244 .name(format!("i2p-out-{}", peer_addr))
245 .spawn(move || {
246 outbound_peer_loop(
247 sam_addr2,
248 &session_id2,
249 &peer_addr,
250 &iface_name,
251 tx2,
252 next_id2,
253 runtime,
254 ingress_control,
255 );
256 })
257 .ok();
258 }
259
260 if config.connectable {
262 let tx2 = tx.clone();
263 let next_id2 = next_id.clone();
264 let sam_addr2 = sam_addr;
265 let session_id2 = session_id.clone();
266 let iface_name = config.name.clone();
267
268 thread::Builder::new()
269 .name("i2p-acceptor".into())
270 .spawn(move || {
271 acceptor_loop(
272 sam_addr2,
273 &session_id2,
274 &iface_name,
275 tx2,
276 next_id2,
277 ingress_control,
278 );
279 })
280 .ok();
281 }
282
283 let _keep_alive = control_socket;
286 loop {
287 thread::sleep(Duration::from_secs(3600));
288 }
289}
290
291fn outbound_peer_loop(
294 sam_addr: SocketAddr,
295 session_id: &str,
296 peer_addr: &str,
297 iface_name: &str,
298 tx: EventSender,
299 next_id: Arc<AtomicU64>,
300 runtime: Arc<Mutex<I2pRuntime>>,
301 ingress_control: rns_core::transport::types::IngressControlConfig,
302) {
303 loop {
304 log::info!("[{}] connecting to I2P peer {}", iface_name, peer_addr);
305
306 let destination = if peer_addr.ends_with(".i2p") {
308 match sam::naming_lookup(&sam_addr, peer_addr) {
309 Ok(dest) => dest.to_i2p_base64(),
310 Err(e) => {
311 log::warn!("[{}] failed to resolve {}: {}", iface_name, peer_addr, e);
312 thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
313 continue;
314 }
315 }
316 } else {
317 peer_addr.to_string()
319 };
320
321 match sam::stream_connect(&sam_addr, session_id, &destination) {
323 Ok(stream) => {
324 let client_id = InterfaceId(next_id.fetch_add(1, Ordering::Relaxed));
325
326 log::info!(
327 "[{}] connected to I2P peer {} → id {}",
328 iface_name,
329 peer_addr,
330 client_id.0
331 );
332
333 let writer_stream = match stream.try_clone() {
335 Ok(s) => s,
336 Err(e) => {
337 log::warn!("[{}] failed to clone stream: {}", iface_name, e);
338 thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
339 continue;
340 }
341 };
342
343 let writer: Box<dyn Writer> = Box::new(I2pWriter {
344 stream: writer_stream,
345 });
346
347 let info = InterfaceInfo {
348 id: client_id,
349 name: format!("I2PInterface/{}", peer_addr),
350 mode: constants::MODE_FULL,
351 recursive_prs: false,
352 announces_from_internal: true,
353 out_capable: true,
354 in_capable: true,
355 bitrate: Some(BITRATE_GUESS),
356 airtime_profile: None,
357 announce_rate_target: None,
358 announce_rate_grace: 0,
359 announce_rate_penalty: 0.0,
360 announce_cap: constants::ANNOUNCE_CAP,
361 is_local_client: false,
362 wants_tunnel: false,
363 tunnel_id: None,
364 mtu: 65535,
365 ia_freq: 0.0,
366 ip_freq: 0.0,
367 op_freq: 0.0,
368 op_samples: 0,
369 started: 0.0,
370 ingress_control,
371 };
372
373 if tx
375 .send(Event::InterfaceUp(client_id, Some(writer), Some(info)))
376 .is_err()
377 {
378 return; }
380
381 peer_reader_loop(stream, client_id, iface_name, &tx);
383
384 let _ = tx.send(Event::InterfaceDown(client_id));
386 log::warn!(
387 "[{}] I2P peer {} disconnected, reconnecting in {}s",
388 iface_name,
389 peer_addr,
390 lock_or_recover(&runtime, "i2p runtime")
391 .reconnect_wait
392 .as_secs()
393 );
394 }
395 Err(e) => {
396 log::warn!(
397 "[{}] failed to connect to I2P peer {}: {}",
398 iface_name,
399 peer_addr,
400 e
401 );
402 }
403 }
404
405 thread::sleep(lock_or_recover(&runtime, "i2p runtime").reconnect_wait);
406 }
407}
408
409fn acceptor_loop(
411 sam_addr: SocketAddr,
412 session_id: &str,
413 iface_name: &str,
414 tx: EventSender,
415 next_id: Arc<AtomicU64>,
416 ingress_control: rns_core::transport::types::IngressControlConfig,
417) {
418 loop {
419 match sam::stream_accept(&sam_addr, session_id) {
420 Ok((stream, remote_dest)) => {
421 let client_id = InterfaceId(next_id.fetch_add(1, Ordering::Relaxed));
422 let remote_b32 = remote_dest.base32_address();
423
424 log::info!(
425 "[{}] accepted I2P connection from {} → id {}",
426 iface_name,
427 remote_b32,
428 client_id.0
429 );
430
431 let writer_stream = match stream.try_clone() {
433 Ok(s) => s,
434 Err(e) => {
435 log::warn!("[{}] failed to clone accepted stream: {}", iface_name, e);
436 continue;
437 }
438 };
439
440 let writer: Box<dyn Writer> = Box::new(I2pWriter {
441 stream: writer_stream,
442 });
443
444 let info = InterfaceInfo {
445 id: client_id,
446 name: format!("I2PInterface/{}", remote_b32),
447 mode: constants::MODE_FULL,
448 recursive_prs: false,
449 announces_from_internal: true,
450 out_capable: true,
451 in_capable: true,
452 bitrate: Some(BITRATE_GUESS),
453 airtime_profile: None,
454 announce_rate_target: None,
455 announce_rate_grace: 0,
456 announce_rate_penalty: 0.0,
457 announce_cap: constants::ANNOUNCE_CAP,
458 is_local_client: false,
459 wants_tunnel: false,
460 tunnel_id: None,
461 mtu: 65535,
462 ia_freq: 0.0,
463 ip_freq: 0.0,
464 op_freq: 0.0,
465 op_samples: 0,
466 started: 0.0,
467 ingress_control,
468 };
469
470 if tx
472 .send(Event::InterfaceUp(client_id, Some(writer), Some(info)))
473 .is_err()
474 {
475 return; }
477
478 let client_tx = tx.clone();
480 let client_name = iface_name.to_string();
481 thread::Builder::new()
482 .name(format!("i2p-client-{}", client_id.0))
483 .spawn(move || {
484 peer_reader_loop(stream, client_id, &client_name, &client_tx);
485 let _ = client_tx.send(Event::InterfaceDown(client_id));
486 })
487 .ok();
488 }
489 Err(e) => {
490 log::warn!("[{}] I2P accept failed: {}, retrying", iface_name, e);
491 thread::sleep(Duration::from_secs(1));
492 }
493 }
494 }
495}
496
497fn peer_reader_loop(
500 mut stream: std::net::TcpStream,
501 id: InterfaceId,
502 name: &str,
503 tx: &EventSender,
504) {
505 let mut decoder = hdlc::Decoder::new();
506 let mut buf = [0u8; 4096];
507
508 loop {
509 match stream.read(&mut buf) {
510 Ok(0) => {
511 log::info!("[{}] I2P peer {} disconnected", name, id.0);
512 return;
513 }
514 Ok(n) => {
515 for frame in decoder.feed(&buf[..n]) {
516 if tx
517 .send(Event::Frame {
518 interface_id: id,
519 data: frame,
520 rssi: None,
521 snr: None,
522 })
523 .is_err()
524 {
525 return; }
527 }
528 }
529 Err(e) => {
530 log::warn!("[{}] I2P peer {} read error: {}", name, id.0, e);
531 return;
532 }
533 }
534 }
535}
536
537use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
540use std::collections::HashMap;
541
542pub struct I2pFactory;
544
545impl InterfaceFactory for I2pFactory {
546 fn type_name(&self) -> &str {
547 "I2PInterface"
548 }
549
550 fn parse_config(
551 &self,
552 name: &str,
553 id: InterfaceId,
554 params: &HashMap<String, String>,
555 ) -> Result<Box<dyn InterfaceConfigData>, String> {
556 let sam_host = params
557 .get("sam_host")
558 .cloned()
559 .unwrap_or_else(|| "127.0.0.1".into());
560
561 let sam_port = params
562 .get("sam_port")
563 .and_then(|v| v.parse::<u16>().ok())
564 .unwrap_or(7656);
565
566 let connectable = params
567 .get("connectable")
568 .and_then(|v| crate::config::parse_bool_pub(v))
569 .unwrap_or(false);
570
571 let peers = params
572 .get("peers")
573 .map(|v| {
574 v.split(',')
575 .map(|s| s.trim().to_string())
576 .filter(|s| !s.is_empty())
577 .collect::<Vec<String>>()
578 })
579 .unwrap_or_default();
580
581 let storage_dir = params
582 .get("storage_dir")
583 .map(PathBuf::from)
584 .unwrap_or_else(|| PathBuf::from("/tmp/rns-i2p"));
585
586 Ok(Box::new(I2pConfig {
587 name: name.to_string(),
588 interface_id: id,
589 sam_host,
590 sam_port,
591 connectable,
592 peers,
593 storage_dir,
594 ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
595 runtime: Arc::new(Mutex::new(I2pRuntime {
596 reconnect_wait: RECONNECT_WAIT,
597 })),
598 }))
599 }
600
601 fn start(
602 &self,
603 config: Box<dyn InterfaceConfigData>,
604 ctx: StartContext,
605 ) -> io::Result<StartResult> {
606 let mut cfg = *config
607 .into_any()
608 .downcast::<I2pConfig>()
609 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
610 cfg.ingress_control = ctx.ingress_control;
611 start(cfg, ctx.tx, ctx.next_dynamic_id)?;
612 Ok(StartResult::Listener { control: None })
613 }
614}
615
616pub(crate) fn i2p_runtime_handle_from_config(config: &I2pConfig) -> I2pRuntimeConfigHandle {
617 I2pRuntimeConfigHandle {
618 interface_name: config.name.clone(),
619 runtime: Arc::clone(&config.runtime),
620 startup: I2pRuntime::from_config(config),
621 }
622}