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