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