1use std::collections::{BTreeMap, BTreeSet, VecDeque};
9
10use rns_core::msgpack::{self, Value};
11use rns_core::stamp::{stamp_valid, stamp_value, stamp_workblock};
12use rns_crypto::sha256::sha256;
13
14use super::time;
15
16pub const NAME: u8 = 0xFF;
22pub const TRANSPORT_ID: u8 = 0xFE;
23pub const TRANSPORT_IMPL: u8 = 0xFD;
24pub const TRANSPORT_VERS: u8 = 0xFC;
25pub const INTERFACE_TYPE: u8 = 0x00;
26pub const TRANSPORT: u8 = 0x01;
27pub const REACHABLE_ON: u8 = 0x02;
28pub const LATITUDE: u8 = 0x03;
29pub const LONGITUDE: u8 = 0x04;
30pub const HEIGHT: u8 = 0x05;
31pub const PORT: u8 = 0x06;
32pub const IFAC_NETNAME: u8 = 0x07;
33pub const IFAC_NETKEY: u8 = 0x08;
34pub const FREQUENCY: u8 = 0x09;
35pub const BANDWIDTH: u8 = 0x0A;
36pub const SPREADINGFACTOR: u8 = 0x0B;
37pub const CODINGRATE: u8 = 0x0C;
38pub const MODULATION: u8 = 0x0D;
39pub const CHANNEL: u8 = 0x0E;
40pub const OP_ADDR: u8 = 0xF0;
41
42pub const TRANSPORT_IMPLEMENTATION_NAME: &str = "rns-rs";
44
45pub const TRANSPORT_IMPLEMENTATION_VERSION: &str = env!("CARGO_PKG_VERSION");
47
48pub const APP_NAME: &str = "rnstransport";
50
51pub const DEFAULT_STAMP_VALUE: u8 = 16;
53
54pub const WORKBLOCK_EXPAND_ROUNDS: u32 = 20;
56
57pub const STAMP_SIZE: usize = 32;
59
60pub const DISCOVERY_STAMP_CACHE_MAX_ENTRIES: usize = 2048;
62
63pub const DISCOVERABLE_TYPES: [&str; 6] = [
65 "BackboneInterface",
66 "TCPServerInterface",
67 "I2PInterface",
68 "RNodeInterface",
69 "WeaveInterface",
70 "KISSInterface",
71];
72
73pub const THRESHOLD_UNKNOWN: f64 = 24.0 * 60.0 * 60.0;
76pub const THRESHOLD_STALE: f64 = 3.0 * 24.0 * 60.0 * 60.0;
78pub const THRESHOLD_REMOVE: f64 = 7.0 * 24.0 * 60.0 * 60.0;
80
81const STATUS_STALE: i32 = 0;
83const STATUS_UNKNOWN: i32 = 100;
84const STATUS_AVAILABLE: i32 = 1000;
85
86#[derive(Debug, Clone)]
92pub struct DiscoveryConfig {
93 pub discovery_name: String,
95 pub announce_interval: u64,
97 pub stamp_value: u8,
99 pub reachable_on: Option<String>,
101 pub interface_type: String,
103 pub listen_port: Option<u16>,
105 pub location_cmd: Option<String>,
107 pub latitude: Option<f64>,
109 pub longitude: Option<f64>,
111 pub height: Option<f64>,
113 pub operator_lxmf_address: Option<[u8; 16]>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum DiscoveredStatus {
124 Available,
125 Unknown,
126 Stale,
127}
128
129impl DiscoveredStatus {
130 pub fn code(&self) -> i32 {
132 match self {
133 DiscoveredStatus::Available => STATUS_AVAILABLE,
134 DiscoveredStatus::Unknown => STATUS_UNKNOWN,
135 DiscoveredStatus::Stale => STATUS_STALE,
136 }
137 }
138
139 pub fn as_str(&self) -> &'static str {
141 match self {
142 DiscoveredStatus::Available => "available",
143 DiscoveredStatus::Unknown => "unknown",
144 DiscoveredStatus::Stale => "stale",
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct DiscoveredInterface {
152 pub interface_type: String,
154 pub transport: bool,
156 pub name: String,
158 pub discovered: f64,
160 pub last_heard: f64,
162 pub heard_count: u32,
164 pub status: DiscoveredStatus,
166 pub stamp: Vec<u8>,
168 pub stamp_value: u32,
170 pub transport_id: [u8; 16],
172 pub network_id: [u8; 16],
174 pub hops: u8,
176
177 pub latitude: Option<f64>,
179 pub longitude: Option<f64>,
180 pub height: Option<f64>,
181 pub operator_lxmf_address: Option<[u8; 16]>,
183
184 pub reachable_on: Option<String>,
186 pub port: Option<u16>,
187
188 pub frequency: Option<u32>,
190 pub bandwidth: Option<u32>,
191 pub spreading_factor: Option<u8>,
192 pub coding_rate: Option<u8>,
193 pub modulation: Option<String>,
194 pub channel: Option<u8>,
195
196 pub ifac_netname: Option<String>,
198 pub ifac_netkey: Option<String>,
199
200 pub config_entry: Option<String>,
202
203 pub discovery_hash: [u8; 32],
205}
206
207fn insufficient_stamp_diagnostic(value: u32) -> String {
208 format!("Ignored discovered interface with insufficient stamp value {value}")
209}
210
211#[derive(Debug, Clone)]
217pub struct DiscoveryStampCache {
218 capacity: usize,
219 valid_order: VecDeque<[u8; 32]>,
220 valid_entries: BTreeMap<[u8; 32], ValidStampEntry>,
221 invalid_order: VecDeque<[u8; 32]>,
222 invalid_entries: BTreeSet<[u8; 32]>,
223}
224
225#[derive(Debug, Clone)]
226struct ValidStampEntry {
227 stamp: Vec<u8>,
228 packed: Vec<u8>,
229 value: u32,
230}
231
232impl Default for DiscoveryStampCache {
233 fn default() -> Self {
234 Self::with_capacity(DISCOVERY_STAMP_CACHE_MAX_ENTRIES)
235 }
236}
237
238impl DiscoveryStampCache {
239 pub fn with_capacity(capacity: usize) -> Self {
240 Self {
241 capacity,
242 valid_order: VecDeque::with_capacity(capacity),
243 valid_entries: BTreeMap::new(),
244 invalid_order: VecDeque::with_capacity(capacity),
245 invalid_entries: BTreeSet::new(),
246 }
247 }
248
249 fn payload_hash(app_data: &[u8]) -> Option<[u8; 32]> {
250 app_data
251 .get(1..)
252 .filter(|payload| !payload.is_empty())
253 .map(sha256)
254 }
255
256 fn contains_invalid_hash(&self, payload_hash: &[u8; 32]) -> bool {
257 self.invalid_entries.contains(payload_hash)
258 }
259
260 fn valid_entry(&self, payload_hash: &[u8; 32]) -> Option<&ValidStampEntry> {
261 self.valid_entries.get(payload_hash)
262 }
263
264 fn insert_valid_hash(&mut self, payload_hash: [u8; 32], entry: ValidStampEntry) {
265 if self.capacity == 0 || self.valid_entries.contains_key(&payload_hash) {
266 return;
267 }
268 if self.valid_order.len() == self.capacity {
269 if let Some(evicted) = self.valid_order.pop_front() {
270 self.valid_entries.remove(&evicted);
271 }
272 }
273 self.valid_order.push_back(payload_hash);
274 self.valid_entries.insert(payload_hash, entry);
275 }
276
277 fn insert_invalid_hash(&mut self, payload_hash: [u8; 32]) {
278 if self.capacity == 0 || self.invalid_entries.contains(&payload_hash) {
279 return;
280 }
281 if self.invalid_order.len() == self.capacity {
282 if let Some(evicted) = self.invalid_order.pop_front() {
283 self.invalid_entries.remove(&evicted);
284 }
285 }
286 self.invalid_order.push_back(payload_hash);
287 self.invalid_entries.insert(payload_hash);
288 }
289
290 pub fn invalid_len(&self) -> usize {
291 self.invalid_order.len()
292 }
293
294 pub fn valid_len(&self) -> usize {
295 self.valid_order.len()
296 }
297
298 pub fn contains_invalid_app_data(&self, app_data: &[u8]) -> bool {
299 Self::payload_hash(app_data)
300 .is_some_and(|payload_hash| self.contains_invalid_hash(&payload_hash))
301 }
302
303 pub fn contains_valid_app_data(&self, app_data: &[u8]) -> bool {
304 Self::payload_hash(app_data)
305 .is_some_and(|payload_hash| self.valid_entries.contains_key(&payload_hash))
306 }
307}
308
309impl DiscoveredInterface {
310 pub fn compute_status(&self) -> DiscoveredStatus {
312 let delta = time::now() - self.last_heard;
313 if delta > THRESHOLD_STALE {
314 DiscoveredStatus::Stale
315 } else if delta > THRESHOLD_UNKNOWN {
316 DiscoveredStatus::Unknown
317 } else {
318 DiscoveredStatus::Available
319 }
320 }
321}
322
323pub fn parse_interface_announce(
334 app_data: &[u8],
335 announced_identity_hash: &[u8; 16],
336 hops: u8,
337 required_stamp_value: u8,
338) -> Option<DiscoveredInterface> {
339 let mut cache = DiscoveryStampCache::with_capacity(0);
340 parse_interface_announce_with_cache(
341 app_data,
342 announced_identity_hash,
343 hops,
344 required_stamp_value,
345 &mut cache,
346 )
347}
348
349pub fn parse_interface_announce_with_cache(
352 app_data: &[u8],
353 announced_identity_hash: &[u8; 16],
354 hops: u8,
355 required_stamp_value: u8,
356 cache: &mut DiscoveryStampCache,
357) -> Option<DiscoveredInterface> {
358 if app_data.len() <= STAMP_SIZE + 1 {
360 return None;
361 }
362
363 let flags = app_data[0];
365 let payload = &app_data[1..];
366 let payload_hash = sha256(payload);
367
368 if cache.contains_invalid_hash(&payload_hash) {
369 log::trace!(
370 target: rns_core::logging::PATHING_LOG_TARGET,
371 "Ignored previously discovered interface with insufficient stamp value"
372 );
373 return None;
374 }
375
376 let encrypted = (flags & 0x02) != 0;
378 if encrypted {
379 log::debug!("Ignoring encrypted discovered interface (not supported)");
380 return None;
381 }
382
383 let (stamp, packed, stamp_value) =
384 if let Some(entry) = cache.valid_entry(&payload_hash).cloned() {
385 log::trace!(
386 target: rns_core::logging::PATHING_LOG_TARGET,
387 "Discovery announce cache hit"
388 );
389 if entry.value < required_stamp_value as u32 {
390 log::debug!("{}", insufficient_stamp_diagnostic(entry.value));
391 return None;
392 }
393 (entry.stamp, entry.packed, entry.value)
394 } else {
395 let stamp = payload[payload.len() - STAMP_SIZE..].to_vec();
398 let packed = payload[..payload.len() - STAMP_SIZE].to_vec();
399 let infohash = sha256(&packed);
400 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
401 let value = stamp_value(&workblock, &stamp);
402
403 if !stamp_valid(&stamp, required_stamp_value, &workblock) {
404 log::debug!("{}", insufficient_stamp_diagnostic(value));
405 cache.insert_invalid_hash(payload_hash);
406 return None;
407 }
408 cache.insert_valid_hash(
409 payload_hash,
410 ValidStampEntry {
411 stamp: stamp.clone(),
412 packed: packed.clone(),
413 value,
414 },
415 );
416 (stamp, packed, value)
417 };
418
419 let (value, _) = msgpack::unpack(&packed).ok()?;
421 let map = value.as_map()?;
422
423 let get_u8_val = |key: u8| -> Option<Value> {
425 for (k, v) in map {
426 if k.as_uint()? as u8 == key {
427 return Some(v.clone());
428 }
429 }
430 None
431 };
432
433 let interface_type = match get_u8_val(INTERFACE_TYPE)? {
435 Value::Str(value) => value,
436 _ => return None,
437 };
438 if !is_discoverable_type(&interface_type) {
439 log::debug!(
440 "Ignoring discovered interface with unsupported type '{}'",
441 interface_type
442 );
443 return None;
444 }
445
446 let transport = match get_u8_val(TRANSPORT)? {
447 Value::Bool(value) => value,
448 _ => return None,
449 };
450 let raw_name = match get_u8_val(NAME) {
451 Some(Value::Str(value)) => value,
452 Some(_) | None => String::new(),
453 };
454 let name = sanitize_discovered_name(&raw_name)
455 .unwrap_or_else(|| format!("Discovered {}", interface_type));
456
457 let transport_id_val = get_u8_val(TRANSPORT_ID)?;
458 let transport_id_bytes = transport_id_val.as_bin()?;
459 if transport_id_bytes.len() != 16 {
460 log::debug!("Ignoring discovered interface with invalid transport_id length");
461 return None;
462 }
463 let mut transport_id = [0u8; 16];
464 transport_id.copy_from_slice(transport_id_bytes);
465
466 let latitude = optional_f64_field(get_u8_val(LATITUDE))?;
468 let longitude = optional_f64_field(get_u8_val(LONGITUDE))?;
469 let height = optional_f64_field(get_u8_val(HEIGHT))?;
470 let reachable_on = match get_u8_val(REACHABLE_ON) {
471 None | Some(Value::Nil) => None,
472 Some(Value::Str(value)) => Some(value),
473 Some(_) => return None,
474 };
475 if let Some(ref reachable_on) = reachable_on {
476 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
477 log::debug!(
478 "Ignoring discovered interface with invalid reachable_on '{}'",
479 reachable_on
480 );
481 return None;
482 }
483 }
484
485 let port = get_u8_val(PORT).and_then(|v| v.as_uint().map(|n| n as u16));
486 let frequency = get_u8_val(FREQUENCY).and_then(|v| v.as_uint().map(|n| n as u32));
487 let bandwidth = get_u8_val(BANDWIDTH).and_then(|v| v.as_uint().map(|n| n as u32));
488 let spreading_factor = get_u8_val(SPREADINGFACTOR).and_then(|v| v.as_uint().map(|n| n as u8));
489 let coding_rate = get_u8_val(CODINGRATE).and_then(|v| v.as_uint().map(|n| n as u8));
490 let modulation = get_u8_val(MODULATION).and_then(|v| v.as_str().map(|s| s.to_string()));
491 let channel = get_u8_val(CHANNEL).and_then(|v| v.as_uint().map(|n| n as u8));
492 let ifac_netname = get_u8_val(IFAC_NETNAME).map(|v| discovery_value_to_string(&v));
493 let ifac_netkey = get_u8_val(IFAC_NETKEY).map(|v| discovery_value_to_string(&v));
494 let operator_lxmf_address = match get_u8_val(OP_ADDR) {
495 None | Some(Value::Nil) => None,
496 Some(Value::Bin(value)) if value.len() == 16 => {
497 let mut address = [0u8; 16];
498 address.copy_from_slice(&value);
499 Some(address)
500 }
501 Some(Value::Bin(_)) => None,
502 Some(_) => return None,
503 };
504
505 let discovery_hash = compute_discovery_hash(&transport_id, &name);
507
508 let config_entry = generate_config_entry(ConfigEntryParams {
510 interface_type: &interface_type,
511 name: &name,
512 transport_id: &transport_id,
513 reachable_on: reachable_on.as_deref(),
514 port,
515 frequency,
516 bandwidth,
517 spreading_factor,
518 coding_rate,
519 modulation: modulation.as_deref(),
520 ifac_netname: ifac_netname.as_deref(),
521 ifac_netkey: ifac_netkey.as_deref(),
522 });
523
524 let now = time::now();
525
526 Some(DiscoveredInterface {
527 interface_type,
528 transport,
529 name,
530 discovered: now,
531 last_heard: now,
532 heard_count: 0,
533 status: DiscoveredStatus::Available,
534 stamp,
535 stamp_value,
536 transport_id,
537 network_id: *announced_identity_hash,
538 hops,
539 latitude,
540 longitude,
541 height,
542 operator_lxmf_address,
543 reachable_on,
544 port,
545 frequency,
546 bandwidth,
547 spreading_factor,
548 coding_rate,
549 modulation,
550 channel,
551 ifac_netname,
552 ifac_netkey,
553 config_entry,
554 discovery_hash,
555 })
556}
557
558pub fn compute_discovery_hash(transport_id: &[u8; 16], name: &str) -> [u8; 32] {
560 let mut material = Vec::with_capacity(16 + name.len());
561 material.extend_from_slice(transport_id);
562 material.extend_from_slice(name.as_bytes());
563 sha256(&material)
564}
565
566pub fn apply_transport_autoconnect_mode(
568 iface: &mut DiscoveredInterface,
569 local_transport_enabled: bool,
570 configured_mode: Option<u8>,
571) {
572 let mode = configured_mode.or_else(|| {
573 (local_transport_enabled && iface.transport).then_some(rns_core::constants::MODE_GATEWAY)
574 });
575 let Some(mode) = mode else {
576 return;
577 };
578 let Some(config_entry) = iface.config_entry.as_mut() else {
579 return;
580 };
581 if config_entry
582 .lines()
583 .any(|line| line.trim_start().starts_with("interface_mode"))
584 {
585 return;
586 }
587 if let Some(pos) = config_entry.find(" enabled = yes\n") {
588 let insert_at = pos + " enabled = yes\n".len();
589 config_entry.insert_str(
590 insert_at,
591 &format!(" interface_mode = {}\n", interface_mode_name(mode)),
592 );
593 } else {
594 config_entry.push_str(&format!(
595 "\n interface_mode = {}",
596 interface_mode_name(mode)
597 ));
598 }
599}
600
601fn interface_mode_name(mode: u8) -> &'static str {
602 match mode {
603 rns_core::constants::MODE_ACCESS_POINT => "access_point",
604 rns_core::constants::MODE_POINT_TO_POINT => "pointtopoint",
605 rns_core::constants::MODE_ROAMING => "roaming",
606 rns_core::constants::MODE_BOUNDARY => "boundary",
607 rns_core::constants::MODE_GATEWAY => "gateway",
608 rns_core::constants::MODE_INTERNAL => "internal",
609 _ => "full",
610 }
611}
612
613struct ConfigEntryParams<'a> {
615 interface_type: &'a str,
616 name: &'a str,
617 transport_id: &'a [u8; 16],
618 reachable_on: Option<&'a str>,
619 port: Option<u16>,
620 frequency: Option<u32>,
621 bandwidth: Option<u32>,
622 spreading_factor: Option<u8>,
623 coding_rate: Option<u8>,
624 modulation: Option<&'a str>,
625 ifac_netname: Option<&'a str>,
626 ifac_netkey: Option<&'a str>,
627}
628
629fn generate_config_entry(params: ConfigEntryParams<'_>) -> Option<String> {
630 let ConfigEntryParams {
631 interface_type,
632 name,
633 transport_id,
634 reachable_on,
635 port,
636 frequency,
637 bandwidth,
638 spreading_factor,
639 coding_rate,
640 modulation,
641 ifac_netname,
642 ifac_netkey,
643 } = params;
644 if reachable_on.is_some_and(|address| {
645 is_ygg_ipv6(address) || is_onion_address(address) || is_invalid_ip_address(address)
646 }) {
647 return None;
648 }
649
650 let transport_id_hex = hex_encode(transport_id);
651 let netname_str = ifac_netname
652 .map(|n| format!("\n network_name = {}", n))
653 .unwrap_or_default();
654 let netkey_str = ifac_netkey
655 .map(|k| format!("\n passphrase = {}", k))
656 .unwrap_or_default();
657 let identity_str = format!("\n transport_identity = {}", transport_id_hex);
658
659 match interface_type {
660 "BackboneInterface" | "TCPServerInterface" => {
661 let reachable = reachable_on.unwrap_or("unknown");
662 let port_val = port.unwrap_or(4242);
663 Some(format!(
664 "[[{}]]\n type = BackboneInterface\n enabled = yes\n remote = {}\n target_port = {}{}{}{}",
665 name, reachable, port_val, identity_str, netname_str, netkey_str
666 ))
667 }
668 "I2PInterface" => {
669 let reachable = reachable_on.unwrap_or("unknown");
670 Some(format!(
671 "[[{}]]\n type = I2PInterface\n enabled = yes\n peers = {}{}{}{}",
672 name, reachable, identity_str, netname_str, netkey_str
673 ))
674 }
675 "RNodeInterface" => {
676 let freq_str = frequency
677 .map(|f| format!("\n frequency = {}", f))
678 .unwrap_or_default();
679 let bw_str = bandwidth
680 .map(|b| format!("\n bandwidth = {}", b))
681 .unwrap_or_default();
682 let sf_str = spreading_factor
683 .map(|s| format!("\n spreadingfactor = {}", s))
684 .unwrap_or_default();
685 let cr_str = coding_rate
686 .map(|c| format!("\n codingrate = {}", c))
687 .unwrap_or_default();
688 Some(format!(
689 "[[{}]]\n type = RNodeInterface\n enabled = yes\n port = {}{}{}{}{}{}{}{}",
690 name, "", freq_str, bw_str, sf_str, cr_str, identity_str, netname_str, netkey_str
691 ))
692 }
693 "KISSInterface" => {
694 let freq_str = frequency
695 .map(|f| format!("\n # Frequency: {}", f))
696 .unwrap_or_default();
697 let bw_str = bandwidth
698 .map(|b| format!("\n # Bandwidth: {}", b))
699 .unwrap_or_default();
700 let mod_str = modulation
701 .map(|m| format!("\n # Modulation: {}", m))
702 .unwrap_or_default();
703 Some(format!(
704 "[[{}]]\n type = KISSInterface\n enabled = yes\n port = {}{}{}{}{}{}{}",
705 name, "", freq_str, bw_str, mod_str, identity_str, netname_str, netkey_str
706 ))
707 }
708 "WeaveInterface" => Some(format!(
709 "[[{}]]\n type = WeaveInterface\n enabled = yes\n port = {}{}{}{}",
710 name, "", identity_str, netname_str, netkey_str
711 )),
712 _ => None,
713 }
714}
715
716fn optional_f64_field(value: Option<Value>) -> Option<Option<f64>> {
721 match value {
722 None | Some(Value::Nil) => Some(None),
723 Some(Value::Float(value)) if value.is_finite() => Some(Some(value)),
724 Some(_) => None,
725 }
726}
727
728fn discovery_value_to_string(value: &Value) -> String {
729 match value {
730 Value::Nil => "None".to_string(),
731 Value::Bool(value) => value.to_string(),
732 Value::UInt(value) => value.to_string(),
733 Value::Int(value) => value.to_string(),
734 Value::Float(value) => value.to_string(),
735 Value::Bin(value) => hex_encode(value),
736 Value::Str(value) => value.clone(),
737 Value::Array(_) => "[]".to_string(),
738 Value::Map(_) => "{}".to_string(),
739 }
740}
741
742pub fn sanitize_discovered_name(name: &str) -> Option<String> {
744 let ascii: String = name.chars().filter(|ch| ch.is_ascii()).collect();
745 let mut sanitized = ascii.trim().to_string();
746 while sanitized.contains(" ") {
747 sanitized = sanitized.replace(" ", " ");
748 }
749
750 let start = sanitized
751 .char_indices()
752 .find(|(_, ch)| ch.is_ascii_alphanumeric())
753 .map(|(idx, _)| idx)?;
754 let end = sanitized
755 .char_indices()
756 .rev()
757 .find(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == ')')
758 .map(|(idx, ch)| idx + ch.len_utf8())?;
759
760 if start >= end {
761 return None;
762 }
763
764 let sanitized = sanitized[start..end].to_string();
765 (!sanitized.is_empty()).then_some(sanitized)
766}
767
768pub fn hex_encode(bytes: &[u8]) -> String {
770 bytes.iter().map(|b| format!("{:02x}", b)).collect()
771}
772
773pub fn is_ip_address(s: &str) -> bool {
775 s.parse::<std::net::IpAddr>().is_ok()
776}
777
778pub fn is_ygg_ipv6(s: &str) -> bool {
780 match s.parse::<std::net::IpAddr>() {
781 Ok(std::net::IpAddr::V6(addr)) => {
782 let segments = addr.segments();
783 (segments[0] & 0xfe00) == 0x0200
784 }
785 _ => false,
786 }
787}
788
789pub fn is_onion_address(s: &str) -> bool {
791 s.to_ascii_lowercase().ends_with(".onion")
792}
793
794pub fn is_invalid_ip_address(s: &str) -> bool {
796 matches!(s, "127.0.0.1" | "0.0.0.0")
797}
798
799pub fn is_hostname(s: &str) -> bool {
801 let s = s.strip_suffix('.').unwrap_or(s);
802 if s.len() > 253 {
803 return false;
804 }
805 let components: Vec<&str> = s.split('.').collect();
806 if components.is_empty() {
807 return false;
808 }
809 if components
811 .last()
812 .map(|c| c.chars().all(|ch| ch.is_ascii_digit()))
813 .unwrap_or(false)
814 {
815 return false;
816 }
817 components.iter().all(|c| {
818 !c.is_empty()
819 && c.len() <= 63
820 && !c.starts_with('-')
821 && !c.ends_with('-')
822 && c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
823 })
824}
825
826pub fn is_discoverable_type(interface_type: &str) -> bool {
828 DISCOVERABLE_TYPES.contains(&interface_type)
829}
830
831pub fn filter_and_sort_interfaces(
833 interfaces: &mut Vec<DiscoveredInterface>,
834 only_available: bool,
835 only_transport: bool,
836) {
837 let now = time::now();
838
839 interfaces.retain(|iface| {
841 if !is_discoverable_type(&iface.interface_type) {
842 return false;
843 }
844 if let Some(ref reachable_on) = iface.reachable_on {
845 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
846 return false;
847 }
848 }
849
850 let delta = now - iface.last_heard;
851
852 if delta > THRESHOLD_REMOVE {
854 return false;
855 }
856
857 let status = iface.compute_status();
859
860 if only_available && status != DiscoveredStatus::Available {
862 return false;
863 }
864 if only_transport && !iface.transport {
865 return false;
866 }
867
868 true
869 });
870
871 interfaces.sort_by(|a, b| {
873 let status_cmp = b.compute_status().code().cmp(&a.compute_status().code());
874 if status_cmp != std::cmp::Ordering::Equal {
875 return status_cmp;
876 }
877 let value_cmp = b.stamp_value.cmp(&a.stamp_value);
878 if value_cmp != std::cmp::Ordering::Equal {
879 return value_cmp;
880 }
881 b.last_heard
882 .partial_cmp(&a.last_heard)
883 .unwrap_or(std::cmp::Ordering::Equal)
884 });
885}
886
887pub fn discovery_name_hash() -> [u8; 10] {
892 rns_core::destination::name_hash(APP_NAME, &["discovery", "interface"])
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898
899 fn pack_discovery_entries(entries: Vec<(Value, Value)>) -> Vec<u8> {
900 let packed = msgpack::pack(&Value::Map(entries));
901 let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
902 app_data.push(0x00);
903 app_data.extend_from_slice(&packed);
904 app_data.extend_from_slice(&[0u8; STAMP_SIZE]);
905 app_data
906 }
907
908 fn discovery_entries(interface_type: &str, reachable_on: Option<&str>) -> Vec<(Value, Value)> {
909 let mut entries = vec![
910 (
911 Value::UInt(INTERFACE_TYPE as u64),
912 Value::Str(interface_type.to_string()),
913 ),
914 (Value::UInt(TRANSPORT as u64), Value::Bool(true)),
915 (
916 Value::UInt(NAME as u64),
917 Value::Str(format!("test-{interface_type}")),
918 ),
919 (Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 16])),
920 ];
921
922 if let Some(reachable_on) = reachable_on {
923 entries.push((
924 Value::UInt(REACHABLE_ON as u64),
925 Value::Str(reachable_on.to_string()),
926 ));
927 }
928
929 entries
930 }
931
932 fn build_discovery_app_data(interface_type: &str, reachable_on: Option<&str>) -> Vec<u8> {
933 pack_discovery_entries(discovery_entries(interface_type, reachable_on))
934 }
935
936 #[test]
937 fn insufficient_stamp_diagnostic_includes_calculated_value() {
938 assert_eq!(
939 insufficient_stamp_diagnostic(7),
940 "Ignored discovered interface with insufficient stamp value 7"
941 );
942 }
943
944 #[test]
945 fn default_discovery_stamp_value_matches_upstream_1_4_0() {
946 assert_eq!(DEFAULT_STAMP_VALUE, 16);
947 }
948
949 #[test]
950 fn invalid_stamp_cache_short_circuits_repeat_validation() {
951 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
952 let mut cache = DiscoveryStampCache::with_capacity(8);
953
954 assert!(parse_interface_announce_with_cache(
955 &app_data,
956 &[0x11; 16],
957 1,
958 u8::MAX,
959 &mut cache,
960 )
961 .is_none());
962 assert_eq!(cache.invalid_len(), 1);
963
964 assert!(
967 parse_interface_announce_with_cache(&app_data, &[0x11; 16], 1, 0, &mut cache,)
968 .is_none()
969 );
970 assert!(parse_interface_announce(&app_data, &[0x11; 16], 1, 0).is_some());
971 assert_eq!(
972 cache.invalid_len(),
973 1,
974 "cache hits must not duplicate entries"
975 );
976 }
977
978 #[test]
979 fn invalid_stamp_cache_is_bounded_fifo() {
980 let first = build_discovery_app_data("BackboneInterface", Some("first.example"));
981 let second = build_discovery_app_data("BackboneInterface", Some("second.example"));
982 let third = build_discovery_app_data("BackboneInterface", Some("third.example"));
983 let mut cache = DiscoveryStampCache::with_capacity(2);
984
985 for app_data in [&first, &second, &third] {
986 assert!(parse_interface_announce_with_cache(
987 app_data,
988 &[0x11; 16],
989 1,
990 u8::MAX,
991 &mut cache,
992 )
993 .is_none());
994 }
995
996 assert_eq!(cache.invalid_len(), 2);
997 assert!(!cache.contains_invalid_app_data(&first));
998 assert!(cache.contains_invalid_app_data(&second));
999 assert!(cache.contains_invalid_app_data(&third));
1000 }
1001
1002 #[test]
1003 fn valid_stamp_cache_short_circuits_repeat_work_and_preserves_dynamic_metadata() {
1004 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1005 let mut cache = DiscoveryStampCache::with_capacity(8);
1006 let first =
1007 parse_interface_announce_with_cache(&app_data, &[0x11; 16], 1, 0, &mut cache).unwrap();
1008 assert_eq!(cache.valid_len(), 1);
1009
1010 let second =
1011 parse_interface_announce_with_cache(&app_data, &[0x22; 16], 7, 0, &mut cache).unwrap();
1012 assert_eq!(cache.valid_len(), 1);
1013 assert_eq!(second.stamp, first.stamp);
1014 assert_eq!(second.stamp_value, first.stamp_value);
1015 assert_eq!(second.network_id, [0x22; 16]);
1016 assert_eq!(second.hops, 7);
1017 }
1018
1019 #[test]
1020 fn valid_stamp_cache_is_bounded_fifo_and_keys_payload_like_invalid_cache() {
1021 let first = build_discovery_app_data("BackboneInterface", Some("first.example"));
1022 let second = build_discovery_app_data("BackboneInterface", Some("second.example"));
1023 let third = build_discovery_app_data("BackboneInterface", Some("third.example"));
1024 let mut cache = DiscoveryStampCache::with_capacity(2);
1025
1026 for app_data in [&first, &second, &third] {
1027 assert!(
1028 parse_interface_announce_with_cache(app_data, &[0x11; 16], 1, 0, &mut cache,)
1029 .is_some()
1030 );
1031 }
1032 assert_eq!(cache.valid_len(), 2);
1033 assert!(!cache.contains_valid_app_data(&first));
1034 assert!(cache.contains_valid_app_data(&second));
1035 assert!(cache.contains_valid_app_data(&third));
1036
1037 let mut different_flags = second.clone();
1038 different_flags[0] ^= 0x01;
1039 assert!(cache.contains_valid_app_data(&different_flags));
1040 let mut different_stamp = second.clone();
1041 *different_stamp.last_mut().unwrap() ^= 0x01;
1042 assert!(!cache.contains_valid_app_data(&different_stamp));
1043 }
1044
1045 #[test]
1046 fn cached_stamp_value_is_rechecked_if_requirement_increases() {
1047 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1048 let mut cache = DiscoveryStampCache::with_capacity(8);
1049 assert!(
1050 parse_interface_announce_with_cache(&app_data, &[0x11; 16], 1, 0, &mut cache,)
1051 .is_some()
1052 );
1053 assert!(parse_interface_announce_with_cache(
1054 &app_data,
1055 &[0x11; 16],
1056 1,
1057 u8::MAX,
1058 &mut cache,
1059 )
1060 .is_none());
1061 assert_eq!(cache.valid_len(), 1);
1062 assert_eq!(cache.invalid_len(), 0);
1063 }
1064
1065 #[test]
1066 fn invalid_stamp_cache_keys_payload_including_stamp_but_excluding_flags() {
1067 let original = build_discovery_app_data("BackboneInterface", Some("example.com"));
1068 let mut cache = DiscoveryStampCache::with_capacity(8);
1069 assert!(parse_interface_announce_with_cache(
1070 &original,
1071 &[0x11; 16],
1072 1,
1073 u8::MAX,
1074 &mut cache,
1075 )
1076 .is_none());
1077
1078 let mut different_flags = original.clone();
1079 different_flags[0] ^= 0x01;
1080 assert!(cache.contains_invalid_app_data(&different_flags));
1081
1082 let mut different_stamp = original.clone();
1083 *different_stamp.last_mut().unwrap() ^= 0x01;
1084 assert!(!cache.contains_invalid_app_data(&different_stamp));
1085 }
1086
1087 #[test]
1088 fn malformed_discovery_payload_is_not_added_to_invalid_stamp_cache() {
1089 let mut cache = DiscoveryStampCache::with_capacity(8);
1090 assert!(parse_interface_announce_with_cache(
1091 &[0x00, 0x01, 0x02],
1092 &[0x11; 16],
1093 1,
1094 u8::MAX,
1095 &mut cache,
1096 )
1097 .is_none());
1098 assert_eq!(cache.invalid_len(), 0);
1099 }
1100
1101 #[test]
1102 fn parse_rejects_unsupported_discovered_interface_type() {
1103 let app_data = build_discovery_app_data("BogusInterface", None);
1104
1105 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
1106
1107 assert!(
1108 parsed.is_none(),
1109 "unsupported discovered interface types must be ignored"
1110 );
1111 }
1112
1113 #[test]
1114 fn parse_rejects_invalid_reachable_on_address() {
1115 let app_data = build_discovery_app_data("BackboneInterface", Some("-not a host-"));
1116
1117 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
1118
1119 assert!(
1120 parsed.is_none(),
1121 "discovered interfaces with invalid reachable_on values must be ignored"
1122 );
1123 }
1124
1125 #[test]
1126 fn parse_sanitizes_discovered_interface_name() {
1127 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1128 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
1129 entries.push((
1130 Value::UInt(NAME as u64),
1131 Value::Str("\t**Alpha Beta!!!\n".to_string()),
1132 ));
1133 let app_data = pack_discovery_entries(entries);
1134
1135 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1136
1137 assert_eq!(parsed.name, "Alpha Beta");
1138 assert!(parsed.config_entry.unwrap().starts_with("[[Alpha Beta]]"));
1139 }
1140
1141 #[test]
1142 fn parse_falls_back_when_discovered_interface_name_sanitizes_empty() {
1143 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1144 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
1145 entries.push((Value::UInt(NAME as u64), Value::Str("!!!".to_string())));
1146 let app_data = pack_discovery_entries(entries);
1147
1148 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1149
1150 assert_eq!(parsed.name, "Discovered BackboneInterface");
1151 }
1152
1153 #[test]
1154 fn parse_falls_back_when_discovered_interface_name_is_nil() {
1155 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1156 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
1157 entries.push((Value::UInt(NAME as u64), Value::Nil));
1158 let app_data = pack_discovery_entries(entries);
1159
1160 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1161
1162 assert_eq!(parsed.name, "Discovered BackboneInterface");
1163 }
1164
1165 #[test]
1166 fn parse_rejects_invalid_discovered_interface_field_types() {
1167 for (field, replacement) in [
1168 (TRANSPORT, Value::Str("yes".to_string())),
1169 (LATITUDE, Value::Str("45.0".to_string())),
1170 (LONGITUDE, Value::Str("9.0".to_string())),
1171 (HEIGHT, Value::Str("100".to_string())),
1172 (INTERFACE_TYPE, Value::UInt(123)),
1173 (REACHABLE_ON, Value::UInt(123)),
1174 ] {
1175 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1176 entries.retain(|(key, _)| key.as_uint() != Some(field as u64));
1177 entries.push((Value::UInt(field as u64), replacement));
1178 let app_data = pack_discovery_entries(entries);
1179
1180 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
1181
1182 assert!(parsed.is_none(), "field {field} should reject invalid type");
1183 }
1184 }
1185
1186 #[test]
1187 fn parse_rejects_invalid_transport_id_length() {
1188 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1189 entries.retain(|(key, _)| key.as_uint() != Some(TRANSPORT_ID as u64));
1190 entries.push((Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 15])));
1191 let app_data = pack_discovery_entries(entries);
1192
1193 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
1194
1195 assert!(parsed.is_none());
1196 }
1197
1198 #[test]
1199 fn parse_converts_ifac_fields_to_strings() {
1200 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1201 entries.push((Value::UInt(IFAC_NETNAME as u64), Value::UInt(123)));
1202 entries.push((Value::UInt(IFAC_NETKEY as u64), Value::Bool(true)));
1203 let app_data = pack_discovery_entries(entries);
1204
1205 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1206
1207 assert_eq!(parsed.ifac_netname.as_deref(), Some("123"));
1208 assert_eq!(parsed.ifac_netkey.as_deref(), Some("true"));
1209 }
1210
1211 #[test]
1212 fn parse_accepts_exact_operator_lxmf_address() {
1213 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1214 entries.push((Value::UInt(OP_ADDR as u64), Value::Bin(vec![0x5a; 16])));
1215 let app_data = pack_discovery_entries(entries);
1216
1217 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1218
1219 assert_eq!(parsed.operator_lxmf_address, Some([0x5a; 16]));
1220 }
1221
1222 #[test]
1223 fn parse_rejects_non_binary_operator_lxmf_address() {
1224 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1225 entries.push((Value::UInt(OP_ADDR as u64), Value::Str("5a".repeat(16))));
1226 let app_data = pack_discovery_entries(entries);
1227
1228 assert!(parse_interface_announce(&app_data, &[0x11; 16], 1, 0).is_none());
1229 }
1230
1231 #[test]
1232 fn parse_ignores_operator_lxmf_address_with_wrong_length() {
1233 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1234 entries.push((Value::UInt(OP_ADDR as u64), Value::Bin(vec![0x5a; 15])));
1235 let app_data = pack_discovery_entries(entries);
1236
1237 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1238
1239 assert_eq!(parsed.operator_lxmf_address, None);
1240 }
1241
1242 #[test]
1243 fn unsafe_discovery_endpoints_are_retained_without_autoconnect_config() {
1244 for address in ["example.onion", "127.0.0.1", "0.0.0.0"] {
1245 let app_data = build_discovery_app_data("BackboneInterface", Some(address));
1246 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1247
1248 assert_eq!(parsed.reachable_on.as_deref(), Some(address));
1249 assert_eq!(parsed.config_entry, None, "{address} must not autoconnect");
1250 }
1251 }
1252
1253 #[test]
1254 fn transport_autoconnect_mode_marks_transport_discovery_config_as_gateway() {
1255 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1256 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1257
1258 apply_transport_autoconnect_mode(&mut parsed, true, None);
1259
1260 let config_entry = parsed.config_entry.unwrap();
1261 assert!(config_entry.contains(" enabled = yes\n interface_mode = gateway\n"));
1262 }
1263
1264 #[test]
1265 fn configured_autoconnect_mode_overrides_transport_fallback() {
1266 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1267 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1268
1269 apply_transport_autoconnect_mode(
1270 &mut parsed,
1271 true,
1272 Some(rns_core::constants::MODE_BOUNDARY),
1273 );
1274
1275 let config_entry = parsed.config_entry.unwrap();
1276 assert!(config_entry.contains(" enabled = yes\n interface_mode = boundary\n"));
1277 }
1278
1279 #[test]
1280 fn transport_autoconnect_mode_does_not_modify_non_transport_contexts() {
1281 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1282 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1283 let original = parsed.config_entry.clone();
1284
1285 apply_transport_autoconnect_mode(&mut parsed, false, None);
1286
1287 assert_eq!(parsed.config_entry, original);
1288 }
1289
1290 #[test]
1291 fn transport_autoconnect_mode_does_not_modify_non_transport_announces() {
1292 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1293 entries.retain(|(key, _)| key.as_uint() != Some(TRANSPORT as u64));
1294 entries.push((Value::UInt(TRANSPORT as u64), Value::Bool(false)));
1295 let app_data = pack_discovery_entries(entries);
1296 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1297 let original = parsed.config_entry.clone();
1298
1299 apply_transport_autoconnect_mode(&mut parsed, true, None);
1300
1301 assert_eq!(parsed.config_entry, original);
1302 }
1303
1304 #[test]
1305 fn transport_autoconnect_mode_preserves_existing_interface_mode() {
1306 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1307 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1308 let config_entry = parsed.config_entry.as_mut().unwrap();
1309 config_entry.push_str("\n interface_mode = access_point");
1310 let original = parsed.config_entry.clone();
1311
1312 apply_transport_autoconnect_mode(&mut parsed, true, None);
1313
1314 assert_eq!(parsed.config_entry, original);
1315 }
1316
1317 #[test]
1318 fn parse_yggdrasil_reachable_on_keeps_record_without_config_entry() {
1319 let app_data = build_discovery_app_data("BackboneInterface", Some("200:1234::1"));
1320
1321 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
1322
1323 assert_eq!(parsed.reachable_on.as_deref(), Some("200:1234::1"));
1324 assert!(parsed.config_entry.is_none());
1325 }
1326
1327 #[test]
1328 fn parse_accepts_supported_discovered_interface_types() {
1329 for interface_type in [
1330 "BackboneInterface",
1331 "TCPServerInterface",
1332 "I2PInterface",
1333 "RNodeInterface",
1334 "WeaveInterface",
1335 "KISSInterface",
1336 ] {
1337 let app_data = build_discovery_app_data(interface_type, Some("example.com"));
1338
1339 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
1340
1341 assert!(
1342 parsed.is_some(),
1343 "{interface_type} should be accepted as a discoverable interface type"
1344 );
1345 }
1346 }
1347}