1use std::collections::{BTreeMap, BTreeSet, VecDeque};
9
10use rns_core::constants;
11use rns_core::msgpack::{self, Value};
12use rns_core::stamp::{stamp_valid, stamp_value, stamp_workblock, StampCost, StampValue};
13use rns_crypto::sha256::sha256;
14
15use super::time;
16
17pub const NAME: u8 = 0xFF;
23pub const TRANSPORT_ID: u8 = 0xFE;
24pub const TRANSPORT_IMPL: u8 = 0xFD;
25pub const TRANSPORT_VERS: u8 = 0xFC;
26pub const DST_COST: u8 = 0xFB;
28pub const PR_COST: u8 = 0xFA;
30pub const INTERFACE_TYPE: u8 = 0x00;
31pub const TRANSPORT: u8 = 0x01;
32pub const REACHABLE_ON: u8 = 0x02;
33pub const LATITUDE: u8 = 0x03;
34pub const LONGITUDE: u8 = 0x04;
35pub const HEIGHT: u8 = 0x05;
36pub const PORT: u8 = 0x06;
37pub const IFAC_NETNAME: u8 = 0x07;
38pub const IFAC_NETKEY: u8 = 0x08;
39pub const FREQUENCY: u8 = 0x09;
40pub const BANDWIDTH: u8 = 0x0A;
41pub const SPREADINGFACTOR: u8 = 0x0B;
42pub const CODINGRATE: u8 = 0x0C;
43pub const MODULATION: u8 = 0x0D;
44pub const CHANNEL: u8 = 0x0E;
45pub const OP_ADDR: u8 = 0xF0;
46
47pub const TRANSPORT_IMPLEMENTATION_NAME: &str = "rns-rs";
49
50pub const TRANSPORT_IMPLEMENTATION_VERSION: &str = env!("CARGO_PKG_VERSION");
52
53pub const APP_NAME: &str = "rnstransport";
55
56pub const DEFAULT_STAMP_VALUE: StampCost = StampCost::from_whole(16);
58
59pub const WORKBLOCK_EXPAND_ROUNDS: u32 = 20;
61
62pub const STAMP_SIZE: usize = 32;
64
65pub const DISCOVERY_STAMP_CACHE_MAX_ENTRIES: usize = 2048;
67
68pub const DISCOVERABLE_TYPES: [&str; 6] = [
70 "BackboneInterface",
71 "TCPServerInterface",
72 "I2PInterface",
73 "RNodeInterface",
74 "WeaveInterface",
75 "KISSInterface",
76];
77
78pub const THRESHOLD_UNKNOWN: f64 = 24.0 * 60.0 * 60.0;
81pub const THRESHOLD_STALE: f64 = 3.0 * 24.0 * 60.0 * 60.0;
83pub const THRESHOLD_REMOVE: f64 = 7.0 * 24.0 * 60.0 * 60.0;
85
86const STATUS_STALE: i32 = 0;
88const STATUS_UNKNOWN: i32 = 100;
89const STATUS_AVAILABLE: i32 = 1000;
90
91#[derive(Debug, Clone)]
97pub struct DiscoveryConfig {
98 pub discovery_name: String,
100 pub announce_interval: u64,
102 pub stamp_value: StampCost,
105 pub reachable_on: Option<String>,
107 pub interface_type: String,
109 pub listen_port: Option<u16>,
111 pub location_cmd: Option<String>,
113 pub latitude: Option<f64>,
115 pub longitude: Option<f64>,
117 pub height: Option<f64>,
119 pub operator_lxmf_address: Option<[u8; 16]>,
121 pub stamp_costs: rns_core::transport::types::InterfaceStampCosts,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum DiscoveredStatus {
133 Available,
134 Unknown,
135 Stale,
136}
137
138impl DiscoveredStatus {
139 pub fn code(&self) -> i32 {
141 match self {
142 DiscoveredStatus::Available => STATUS_AVAILABLE,
143 DiscoveredStatus::Unknown => STATUS_UNKNOWN,
144 DiscoveredStatus::Stale => STATUS_STALE,
145 }
146 }
147
148 pub fn as_str(&self) -> &'static str {
150 match self {
151 DiscoveredStatus::Available => "available",
152 DiscoveredStatus::Unknown => "unknown",
153 DiscoveredStatus::Stale => "stale",
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct DiscoveredInterface {
161 pub interface_type: String,
163 pub impl_name: Option<String>,
165 pub impl_version: Option<String>,
166 pub transport: bool,
168 pub name: String,
170 pub discovered: f64,
172 pub last_heard: f64,
174 pub heard_count: u32,
176 pub status: DiscoveredStatus,
178 pub stamp: Vec<u8>,
180 pub stamp_value: StampValue,
182 pub transport_id: [u8; 16],
184 pub network_id: [u8; 16],
186 pub hops: u8,
188
189 pub latitude: Option<f64>,
191 pub longitude: Option<f64>,
192 pub height: Option<f64>,
193 pub operator_lxmf_address: Option<[u8; 16]>,
195
196 pub reachable_on: Option<String>,
198 pub port: Option<u16>,
199
200 pub frequency: Option<u32>,
202 pub bandwidth: Option<u32>,
203 pub spreading_factor: Option<u8>,
204 pub coding_rate: Option<u8>,
205 pub modulation: Option<String>,
206 pub channel: Option<u8>,
207
208 pub ifac_netname: Option<String>,
210 pub ifac_netkey: Option<String>,
211
212 pub config_entry: Option<String>,
214
215 pub discovery_hash: [u8; 32],
217
218 pub dst_cost: Option<StampCost>,
221 pub pr_cost: Option<StampCost>,
222}
223
224fn insufficient_stamp_diagnostic(value: StampValue) -> String {
227 format!(
228 "Ignored discovered interface with insufficient stamp value {}",
229 rns_crypto::stamp_value::pretty_cost(value.as_f64())
230 )
231}
232
233#[derive(Debug, Clone)]
239pub struct DiscoveryStampCache {
240 capacity: usize,
241 valid_order: VecDeque<[u8; 32]>,
242 valid_entries: BTreeMap<[u8; 32], ValidStampEntry>,
243 invalid_order: VecDeque<[u8; 32]>,
244 invalid_entries: BTreeSet<[u8; 32]>,
245}
246
247#[derive(Debug, Clone)]
248struct ValidStampEntry {
249 stamp: Vec<u8>,
250 packed: Vec<u8>,
251 value: StampValue,
252}
253
254impl Default for DiscoveryStampCache {
255 fn default() -> Self {
256 Self::with_capacity(DISCOVERY_STAMP_CACHE_MAX_ENTRIES)
257 }
258}
259
260impl DiscoveryStampCache {
261 pub fn with_capacity(capacity: usize) -> Self {
262 Self {
263 capacity,
264 valid_order: VecDeque::with_capacity(capacity),
265 valid_entries: BTreeMap::new(),
266 invalid_order: VecDeque::with_capacity(capacity),
267 invalid_entries: BTreeSet::new(),
268 }
269 }
270
271 fn payload_hash(app_data: &[u8]) -> Option<[u8; 32]> {
272 app_data
273 .get(1..)
274 .filter(|payload| !payload.is_empty())
275 .map(sha256)
276 }
277
278 fn contains_invalid_hash(&self, payload_hash: &[u8; 32]) -> bool {
279 self.invalid_entries.contains(payload_hash)
280 }
281
282 fn valid_entry(&self, payload_hash: &[u8; 32]) -> Option<&ValidStampEntry> {
283 self.valid_entries.get(payload_hash)
284 }
285
286 fn insert_valid_hash(&mut self, payload_hash: [u8; 32], entry: ValidStampEntry) {
287 if self.capacity == 0 || self.valid_entries.contains_key(&payload_hash) {
288 return;
289 }
290 if self.valid_order.len() == self.capacity {
291 if let Some(evicted) = self.valid_order.pop_front() {
292 self.valid_entries.remove(&evicted);
293 }
294 }
295 self.valid_order.push_back(payload_hash);
296 self.valid_entries.insert(payload_hash, entry);
297 }
298
299 fn insert_invalid_hash(&mut self, payload_hash: [u8; 32]) {
300 if self.capacity == 0 || self.invalid_entries.contains(&payload_hash) {
301 return;
302 }
303 if self.invalid_order.len() == self.capacity {
304 if let Some(evicted) = self.invalid_order.pop_front() {
305 self.invalid_entries.remove(&evicted);
306 }
307 }
308 self.invalid_order.push_back(payload_hash);
309 self.invalid_entries.insert(payload_hash);
310 }
311
312 pub fn invalid_len(&self) -> usize {
313 self.invalid_order.len()
314 }
315
316 pub fn valid_len(&self) -> usize {
317 self.valid_order.len()
318 }
319
320 pub fn contains_invalid_app_data(&self, app_data: &[u8]) -> bool {
321 Self::payload_hash(app_data)
322 .is_some_and(|payload_hash| self.contains_invalid_hash(&payload_hash))
323 }
324
325 pub fn contains_valid_app_data(&self, app_data: &[u8]) -> bool {
326 Self::payload_hash(app_data)
327 .is_some_and(|payload_hash| self.valid_entries.contains_key(&payload_hash))
328 }
329}
330
331impl DiscoveredInterface {
332 pub fn compute_status(&self) -> DiscoveredStatus {
334 let delta = time::now() - self.last_heard;
335 if delta > THRESHOLD_STALE {
336 DiscoveredStatus::Stale
337 } else if delta > THRESHOLD_UNKNOWN {
338 DiscoveredStatus::Unknown
339 } else {
340 DiscoveredStatus::Available
341 }
342 }
343}
344
345pub fn parse_interface_announce(
356 app_data: &[u8],
357 announced_identity_hash: &[u8; 16],
358 hops: u8,
359 required_stamp_value: StampCost,
360) -> Option<DiscoveredInterface> {
361 let mut cache = DiscoveryStampCache::with_capacity(0);
362 parse_interface_announce_with_cache(
363 app_data,
364 announced_identity_hash,
365 hops,
366 required_stamp_value,
367 &mut cache,
368 )
369}
370
371pub fn parse_interface_announce_with_cache(
374 app_data: &[u8],
375 announced_identity_hash: &[u8; 16],
376 hops: u8,
377 required_stamp_value: StampCost,
378 cache: &mut DiscoveryStampCache,
379) -> Option<DiscoveredInterface> {
380 if app_data.len() <= STAMP_SIZE + 1 {
382 return None;
383 }
384
385 if hops > constants::PATHFINDER_M - 1 {
386 log::trace!(
387 target: rns_core::logging::PATHING_LOG_TARGET,
388 "Ignored discovered interface with invalid hop count"
389 );
390 return None;
391 }
392
393 let flags = app_data[0];
395 let payload = &app_data[1..];
396 let payload_hash = sha256(payload);
397
398 if cache.contains_invalid_hash(&payload_hash) {
399 log::trace!(
400 target: rns_core::logging::PATHING_LOG_TARGET,
401 "Ignored previously discovered interface with insufficient stamp value"
402 );
403 return None;
404 }
405
406 let encrypted = (flags & 0x02) != 0;
408 if encrypted {
409 log::debug!("Ignoring encrypted discovered interface (not supported)");
410 return None;
411 }
412
413 let (stamp, packed, stamp_value) =
414 if let Some(entry) = cache.valid_entry(&payload_hash).cloned() {
415 log::trace!(
416 target: rns_core::logging::PATHING_LOG_TARGET,
417 "Discovery announce cache hit"
418 );
419 if !entry.value.meets(required_stamp_value) {
420 log::debug!("{}", insufficient_stamp_diagnostic(entry.value));
421 return None;
422 }
423 (entry.stamp, entry.packed, entry.value)
424 } else {
425 let stamp = payload[payload.len() - STAMP_SIZE..].to_vec();
428 let packed = payload[..payload.len() - STAMP_SIZE].to_vec();
429 let infohash = sha256(&packed);
430 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
431 let value = stamp_value(&workblock, &stamp);
432
433 if !stamp_valid(&stamp, required_stamp_value, &workblock) {
434 log::debug!("{}", insufficient_stamp_diagnostic(value));
435 cache.insert_invalid_hash(payload_hash);
436 return None;
437 }
438 cache.insert_valid_hash(
439 payload_hash,
440 ValidStampEntry {
441 stamp: stamp.clone(),
442 packed: packed.clone(),
443 value,
444 },
445 );
446 (stamp, packed, value)
447 };
448
449 let (value, _) = msgpack::unpack(&packed).ok()?;
451 let map = value.as_map()?;
452
453 let get_u8_val = |key: u8| -> Option<Value> {
455 for (k, v) in map {
456 if k.as_uint()? as u8 == key {
457 return Some(v.clone());
458 }
459 }
460 None
461 };
462
463 let interface_type = match get_u8_val(INTERFACE_TYPE)? {
465 Value::Str(value) => value,
466 _ => return None,
467 };
468 if !is_discoverable_type(&interface_type) {
469 log::debug!(
470 "Ignoring discovered interface with unsupported type '{}'",
471 interface_type
472 );
473 return None;
474 }
475
476 let transport = match get_u8_val(TRANSPORT)? {
477 Value::Bool(value) => value,
478 _ => return None,
479 };
480 let impl_name = get_u8_val(TRANSPORT_IMPL).and_then(|value| value.as_str().map(str::to_owned));
481 let impl_version =
482 get_u8_val(TRANSPORT_VERS).and_then(|value| value.as_str().map(str::to_owned));
483 let announced_cost = |key: u8| -> Result<Option<StampCost>, ()> {
486 match get_u8_val(key) {
487 None => Ok(None),
488 Some(value) => value
489 .as_number()
490 .filter(|cost| cost.is_finite())
491 .map(|cost| Some(StampCost::from_f64(cost)))
492 .ok_or(()),
493 }
494 };
495 let Ok(dst_cost) = announced_cost(DST_COST) else {
496 log::debug!("Ignoring discovered interface with an invalid destination cost");
497 return None;
498 };
499 let Ok(pr_cost) = announced_cost(PR_COST) else {
500 log::debug!("Ignoring discovered interface with an invalid path request cost");
501 return None;
502 };
503 let raw_name = match get_u8_val(NAME) {
504 Some(Value::Str(value)) => value,
505 Some(_) | None => String::new(),
506 };
507 let name = sanitize_discovered_name(&raw_name)
508 .unwrap_or_else(|| format!("Discovered {}", interface_type));
509
510 let transport_id_val = get_u8_val(TRANSPORT_ID)?;
511 let transport_id_bytes = transport_id_val.as_bin()?;
512 if transport_id_bytes.len() != 16 {
513 log::debug!("Ignoring discovered interface with invalid transport_id length");
514 return None;
515 }
516 let mut transport_id = [0u8; 16];
517 transport_id.copy_from_slice(transport_id_bytes);
518
519 let latitude = optional_f64_field(get_u8_val(LATITUDE))?;
521 let longitude = optional_f64_field(get_u8_val(LONGITUDE))?;
522 let height = optional_f64_field(get_u8_val(HEIGHT))?;
523 let reachable_on = match get_u8_val(REACHABLE_ON) {
524 None | Some(Value::Nil) => None,
525 Some(Value::Str(value)) => Some(value),
526 Some(_) => return None,
527 };
528 if let Some(ref reachable_on) = reachable_on {
529 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
530 log::debug!(
531 "Ignoring discovered interface with invalid reachable_on '{}'",
532 reachable_on
533 );
534 return None;
535 }
536 }
537
538 let port = get_u8_val(PORT).and_then(|v| v.as_uint().map(|n| n as u16));
539 let frequency = get_u8_val(FREQUENCY).and_then(|v| v.as_uint().map(|n| n as u32));
540 let bandwidth = get_u8_val(BANDWIDTH).and_then(|v| v.as_uint().map(|n| n as u32));
541 let spreading_factor = get_u8_val(SPREADINGFACTOR).and_then(|v| v.as_uint().map(|n| n as u8));
542 let coding_rate = get_u8_val(CODINGRATE).and_then(|v| v.as_uint().map(|n| n as u8));
543 let modulation = get_u8_val(MODULATION).and_then(|v| v.as_str().map(|s| s.to_string()));
544 let channel = get_u8_val(CHANNEL).and_then(|v| v.as_uint().map(|n| n as u8));
545 let ifac_netname = get_u8_val(IFAC_NETNAME).and_then(sanitize_ifac_announce_string);
546 let ifac_netkey = get_u8_val(IFAC_NETKEY).and_then(sanitize_ifac_announce_string);
547 let operator_lxmf_address = match get_u8_val(OP_ADDR) {
548 None | Some(Value::Nil) => None,
549 Some(Value::Bin(value)) if value.len() == 16 => {
550 let mut address = [0u8; 16];
551 address.copy_from_slice(&value);
552 Some(address)
553 }
554 Some(Value::Bin(_)) => None,
555 Some(_) => return None,
556 };
557
558 let discovery_hash = compute_discovery_hash(&transport_id, &name);
560
561 let config_entry = generate_config_entry(ConfigEntryParams {
563 interface_type: &interface_type,
564 name: &name,
565 transport_id: &transport_id,
566 reachable_on: reachable_on.as_deref(),
567 port,
568 frequency,
569 bandwidth,
570 spreading_factor,
571 coding_rate,
572 modulation: modulation.as_deref(),
573 ifac_netname: ifac_netname.as_deref(),
574 ifac_netkey: ifac_netkey.as_deref(),
575 });
576
577 let now = time::now();
578
579 Some(DiscoveredInterface {
580 interface_type,
581 impl_name,
582 impl_version,
583 transport,
584 name,
585 discovered: now,
586 last_heard: now,
587 heard_count: 0,
588 status: DiscoveredStatus::Available,
589 stamp,
590 stamp_value,
591 transport_id,
592 network_id: *announced_identity_hash,
593 hops,
594 latitude,
595 longitude,
596 height,
597 operator_lxmf_address,
598 reachable_on,
599 port,
600 frequency,
601 bandwidth,
602 spreading_factor,
603 coding_rate,
604 modulation,
605 channel,
606 ifac_netname,
607 ifac_netkey,
608 config_entry,
609 discovery_hash,
610 dst_cost,
611 pr_cost,
612 })
613}
614
615pub fn compute_discovery_hash(transport_id: &[u8; 16], name: &str) -> [u8; 32] {
617 let mut material = Vec::with_capacity(16 + name.len());
618 material.extend_from_slice(transport_id);
619 material.extend_from_slice(name.as_bytes());
620 sha256(&material)
621}
622
623pub fn apply_transport_autoconnect_mode(
625 iface: &mut DiscoveredInterface,
626 local_transport_enabled: bool,
627 configured_mode: Option<u8>,
628) {
629 let mode = configured_mode.or_else(|| {
630 (local_transport_enabled && iface.transport).then_some(rns_core::constants::MODE_GATEWAY)
631 });
632 let Some(mode) = mode else {
633 return;
634 };
635 let Some(config_entry) = iface.config_entry.as_mut() else {
636 return;
637 };
638 if config_entry
639 .lines()
640 .any(|line| line.trim_start().starts_with("interface_mode"))
641 {
642 return;
643 }
644 if let Some(pos) = config_entry.find(" enabled = yes\n") {
645 let insert_at = pos + " enabled = yes\n".len();
646 config_entry.insert_str(
647 insert_at,
648 &format!(" interface_mode = {}\n", interface_mode_name(mode)),
649 );
650 } else {
651 config_entry.push_str(&format!(
652 "\n interface_mode = {}",
653 interface_mode_name(mode)
654 ));
655 }
656}
657
658fn interface_mode_name(mode: u8) -> &'static str {
659 match mode {
660 rns_core::constants::MODE_ACCESS_POINT => "access_point",
661 rns_core::constants::MODE_POINT_TO_POINT => "pointtopoint",
662 rns_core::constants::MODE_ROAMING => "roaming",
663 rns_core::constants::MODE_BOUNDARY => "boundary",
664 rns_core::constants::MODE_GATEWAY => "gateway",
665 rns_core::constants::MODE_INTERNAL => "internal",
666 _ => "full",
667 }
668}
669
670struct ConfigEntryParams<'a> {
672 interface_type: &'a str,
673 name: &'a str,
674 transport_id: &'a [u8; 16],
675 reachable_on: Option<&'a str>,
676 port: Option<u16>,
677 frequency: Option<u32>,
678 bandwidth: Option<u32>,
679 spreading_factor: Option<u8>,
680 coding_rate: Option<u8>,
681 modulation: Option<&'a str>,
682 ifac_netname: Option<&'a str>,
683 ifac_netkey: Option<&'a str>,
684}
685
686fn generate_config_entry(params: ConfigEntryParams<'_>) -> Option<String> {
687 let ConfigEntryParams {
688 interface_type,
689 name,
690 transport_id,
691 reachable_on,
692 port,
693 frequency,
694 bandwidth,
695 spreading_factor,
696 coding_rate,
697 modulation,
698 ifac_netname,
699 ifac_netkey,
700 } = params;
701 if reachable_on.is_some_and(|address| {
702 is_ygg_ipv6(address) || is_onion_address(address) || is_invalid_ip_address(address)
703 }) {
704 return None;
705 }
706
707 let transport_id_hex = hex_encode(transport_id);
708 let netname_str = ifac_netname
709 .map(|n| format!("\n network_name = {}", n))
710 .unwrap_or_default();
711 let netkey_str = ifac_netkey
712 .map(|k| format!("\n passphrase = {}", k))
713 .unwrap_or_default();
714 let identity_str = format!("\n transport_identity = {}", transport_id_hex);
715
716 match interface_type {
717 "BackboneInterface" | "TCPServerInterface" => {
718 let reachable = reachable_on.unwrap_or("unknown");
719 let port_val = port.unwrap_or(4242);
720 Some(format!(
721 "[[{}]]\n type = BackboneInterface\n enabled = yes\n remote = {}\n target_port = {}{}{}{}",
722 name, reachable, port_val, identity_str, netname_str, netkey_str
723 ))
724 }
725 "I2PInterface" => {
726 let reachable = reachable_on.unwrap_or("unknown");
727 Some(format!(
728 "[[{}]]\n type = I2PInterface\n enabled = yes\n peers = {}.b32.i2p{}{}{}",
729 name, reachable, identity_str, netname_str, netkey_str
730 ))
731 }
732 "RNodeInterface" => {
733 let freq_str = frequency
734 .map(|f| format!("\n frequency = {}", f))
735 .unwrap_or_default();
736 let bw_str = bandwidth
737 .map(|b| format!("\n bandwidth = {}", b))
738 .unwrap_or_default();
739 let sf_str = spreading_factor
740 .map(|s| format!("\n spreadingfactor = {}", s))
741 .unwrap_or_default();
742 let cr_str = coding_rate
743 .map(|c| format!("\n codingrate = {}", c))
744 .unwrap_or_default();
745 Some(format!(
746 "[[{}]]\n type = RNodeInterface\n enabled = yes\n port = {}{}{}{}{}{}{}{}",
747 name, "", freq_str, bw_str, sf_str, cr_str, identity_str, netname_str, netkey_str
748 ))
749 }
750 "KISSInterface" => {
751 let freq_str = frequency
752 .map(|f| format!("\n # Frequency: {}", f))
753 .unwrap_or_default();
754 let bw_str = bandwidth
755 .map(|b| format!("\n # Bandwidth: {}", b))
756 .unwrap_or_default();
757 let mod_str = modulation
758 .map(|m| format!("\n # Modulation: {}", m))
759 .unwrap_or_default();
760 Some(format!(
761 "[[{}]]\n type = KISSInterface\n enabled = yes\n port = {}{}{}{}{}{}{}",
762 name, "", freq_str, bw_str, mod_str, identity_str, netname_str, netkey_str
763 ))
764 }
765 "WeaveInterface" => Some(format!(
766 "[[{}]]\n type = WeaveInterface\n enabled = yes\n port = {}{}{}{}",
767 name, "", identity_str, netname_str, netkey_str
768 )),
769 _ => None,
770 }
771}
772
773fn optional_f64_field(value: Option<Value>) -> Option<Option<f64>> {
778 match value {
779 None | Some(Value::Nil) => Some(None),
780 Some(Value::Float(value)) if value.is_finite() => Some(Some(value)),
781 Some(_) => None,
782 }
783}
784
785fn sanitize_ifac_announce_string(value: Value) -> Option<String> {
791 match value {
792 Value::Str(text) if !text.is_empty() && text != "None" => Some(text),
793 _ => None,
794 }
795}
796
797pub fn sanitize_persisted_ifac(value: Option<String>) -> Option<String> {
800 value.filter(|text| !text.is_empty() && text != "None")
801}
802
803pub fn sanitize_discovered_name(name: &str) -> Option<String> {
805 let ascii: String = name.chars().filter(|ch| ch.is_ascii()).collect();
806 let mut sanitized = ascii.trim().to_string();
807 while sanitized.contains(" ") {
808 sanitized = sanitized.replace(" ", " ");
809 }
810
811 let start = sanitized
812 .char_indices()
813 .find(|(_, ch)| ch.is_ascii_alphanumeric())
814 .map(|(idx, _)| idx)?;
815 let end = sanitized
816 .char_indices()
817 .rev()
818 .find(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == ')')
819 .map(|(idx, ch)| idx + ch.len_utf8())?;
820
821 if start >= end {
822 return None;
823 }
824
825 let sanitized = sanitized[start..end].to_string();
826 (!sanitized.is_empty()).then_some(sanitized)
827}
828
829pub fn hex_encode(bytes: &[u8]) -> String {
831 bytes.iter().map(|b| format!("{:02x}", b)).collect()
832}
833
834pub fn is_ip_address(s: &str) -> bool {
836 s.parse::<std::net::IpAddr>().is_ok()
837}
838
839pub fn is_ygg_ipv6(s: &str) -> bool {
841 match s.parse::<std::net::IpAddr>() {
842 Ok(std::net::IpAddr::V6(addr)) => {
843 let segments = addr.segments();
844 (segments[0] & 0xfe00) == 0x0200
845 }
846 _ => false,
847 }
848}
849
850pub fn is_onion_address(s: &str) -> bool {
852 s.to_ascii_lowercase().ends_with(".onion")
853}
854
855pub fn is_invalid_ip_address(s: &str) -> bool {
857 matches!(s, "127.0.0.1" | "0.0.0.0")
858}
859
860pub fn is_hostname(s: &str) -> bool {
862 let s = s.strip_suffix('.').unwrap_or(s);
863 if s.len() > 253 {
864 return false;
865 }
866 let components: Vec<&str> = s.split('.').collect();
867 if components.is_empty() {
868 return false;
869 }
870 if components
872 .last()
873 .map(|c| c.chars().all(|ch| ch.is_ascii_digit()))
874 .unwrap_or(false)
875 {
876 return false;
877 }
878 components.iter().all(|c| {
879 !c.is_empty()
880 && c.len() <= 63
881 && !c.starts_with('-')
882 && !c.ends_with('-')
883 && c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
884 })
885}
886
887pub fn is_discoverable_type(interface_type: &str) -> bool {
889 DISCOVERABLE_TYPES.contains(&interface_type)
890}
891
892pub fn filter_and_sort_interfaces(
894 interfaces: &mut Vec<DiscoveredInterface>,
895 only_available: bool,
896 only_transport: bool,
897) {
898 let now = time::now();
899
900 interfaces.retain(|iface| {
902 if !is_discoverable_type(&iface.interface_type) {
903 return false;
904 }
905 if let Some(ref reachable_on) = iface.reachable_on {
906 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
907 return false;
908 }
909 }
910
911 let delta = now - iface.last_heard;
912
913 if delta > THRESHOLD_REMOVE {
915 return false;
916 }
917
918 let status = iface.compute_status();
920
921 if only_available && status != DiscoveredStatus::Available {
923 return false;
924 }
925 if only_transport && !iface.transport {
926 return false;
927 }
928
929 true
930 });
931
932 interfaces.sort_by(|a, b| {
934 let status_cmp = b.compute_status().code().cmp(&a.compute_status().code());
935 if status_cmp != std::cmp::Ordering::Equal {
936 return status_cmp;
937 }
938 let value_cmp = b.stamp_value.cmp(&a.stamp_value);
939 if value_cmp != std::cmp::Ordering::Equal {
940 return value_cmp;
941 }
942 b.last_heard
943 .partial_cmp(&a.last_heard)
944 .unwrap_or(std::cmp::Ordering::Equal)
945 });
946}
947
948pub fn discovery_name_hash() -> [u8; 10] {
953 rns_core::destination::name_hash(APP_NAME, &["discovery", "interface"])
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959
960 fn pack_discovery_entries(entries: Vec<(Value, Value)>) -> Vec<u8> {
961 let packed = msgpack::pack(&Value::Map(entries));
962 let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
963 app_data.push(0x00);
964 app_data.extend_from_slice(&packed);
965 app_data.extend_from_slice(&[0u8; STAMP_SIZE]);
966 app_data
967 }
968
969 fn discovery_entries(interface_type: &str, reachable_on: Option<&str>) -> Vec<(Value, Value)> {
970 let mut entries = vec![
971 (
972 Value::UInt(INTERFACE_TYPE as u64),
973 Value::Str(interface_type.to_string()),
974 ),
975 (Value::UInt(TRANSPORT as u64), Value::Bool(true)),
976 (
977 Value::UInt(NAME as u64),
978 Value::Str(format!("test-{interface_type}")),
979 ),
980 (Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 16])),
981 ];
982
983 if let Some(reachable_on) = reachable_on {
984 entries.push((
985 Value::UInt(REACHABLE_ON as u64),
986 Value::Str(reachable_on.to_string()),
987 ));
988 }
989
990 entries
991 }
992
993 fn build_discovery_app_data(interface_type: &str, reachable_on: Option<&str>) -> Vec<u8> {
994 pack_discovery_entries(discovery_entries(interface_type, reachable_on))
995 }
996
997 #[test]
998 fn discovery_rejects_hop_count_at_pathfinder_limit() {
999 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1000 assert!(parse_interface_announce(
1001 &app_data,
1002 &[0x11; 16],
1003 constants::PATHFINDER_M - 1,
1004 rns_core::stamp::StampCost::ZERO
1005 )
1006 .is_some());
1007 assert!(parse_interface_announce(
1008 &app_data,
1009 &[0x11; 16],
1010 constants::PATHFINDER_M,
1011 rns_core::stamp::StampCost::ZERO
1012 )
1013 .is_none());
1014 }
1015
1016 #[test]
1017 fn insufficient_stamp_diagnostic_includes_calculated_value() {
1018 assert_eq!(
1019 insufficient_stamp_diagnostic(StampValue::from_fp(7 * 256 + 64)),
1020 "Ignored discovered interface with insufficient stamp value 7.250"
1021 );
1022 }
1023
1024 #[test]
1025 fn default_discovery_stamp_value_matches_upstream_1_4_0() {
1026 assert_eq!(DEFAULT_STAMP_VALUE, StampCost::from_whole(16));
1027 }
1028
1029 #[test]
1030 fn invalid_stamp_cache_short_circuits_repeat_validation() {
1031 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1032 let mut cache = DiscoveryStampCache::with_capacity(8);
1033
1034 assert!(parse_interface_announce_with_cache(
1035 &app_data,
1036 &[0x11; 16],
1037 1,
1038 rns_core::stamp::StampCost::from_whole(100),
1039 &mut cache,
1040 )
1041 .is_none());
1042 assert_eq!(cache.invalid_len(), 1);
1043
1044 assert!(parse_interface_announce_with_cache(
1047 &app_data,
1048 &[0x11; 16],
1049 1,
1050 rns_core::stamp::StampCost::ZERO,
1051 &mut cache,
1052 )
1053 .is_none());
1054 assert!(parse_interface_announce(
1055 &app_data,
1056 &[0x11; 16],
1057 1,
1058 rns_core::stamp::StampCost::ZERO
1059 )
1060 .is_some());
1061 assert_eq!(
1062 cache.invalid_len(),
1063 1,
1064 "cache hits must not duplicate entries"
1065 );
1066 }
1067
1068 #[test]
1069 fn invalid_stamp_cache_is_bounded_fifo() {
1070 let first = build_discovery_app_data("BackboneInterface", Some("first.example"));
1071 let second = build_discovery_app_data("BackboneInterface", Some("second.example"));
1072 let third = build_discovery_app_data("BackboneInterface", Some("third.example"));
1073 let mut cache = DiscoveryStampCache::with_capacity(2);
1074
1075 for app_data in [&first, &second, &third] {
1076 assert!(parse_interface_announce_with_cache(
1077 app_data,
1078 &[0x11; 16],
1079 1,
1080 rns_core::stamp::StampCost::from_whole(100),
1081 &mut cache,
1082 )
1083 .is_none());
1084 }
1085
1086 assert_eq!(cache.invalid_len(), 2);
1087 assert!(!cache.contains_invalid_app_data(&first));
1088 assert!(cache.contains_invalid_app_data(&second));
1089 assert!(cache.contains_invalid_app_data(&third));
1090 }
1091
1092 #[test]
1093 fn valid_stamp_cache_short_circuits_repeat_work_and_preserves_dynamic_metadata() {
1094 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1095 let mut cache = DiscoveryStampCache::with_capacity(8);
1096 let first = parse_interface_announce_with_cache(
1097 &app_data,
1098 &[0x11; 16],
1099 1,
1100 rns_core::stamp::StampCost::ZERO,
1101 &mut cache,
1102 )
1103 .unwrap();
1104 assert_eq!(cache.valid_len(), 1);
1105
1106 let second = parse_interface_announce_with_cache(
1107 &app_data,
1108 &[0x22; 16],
1109 7,
1110 rns_core::stamp::StampCost::ZERO,
1111 &mut cache,
1112 )
1113 .unwrap();
1114 assert_eq!(cache.valid_len(), 1);
1115 assert_eq!(second.stamp, first.stamp);
1116 assert_eq!(second.stamp_value, first.stamp_value);
1117 assert_eq!(second.network_id, [0x22; 16]);
1118 assert_eq!(second.hops, 7);
1119 }
1120
1121 #[test]
1122 fn valid_stamp_cache_is_bounded_fifo_and_keys_payload_like_invalid_cache() {
1123 let first = build_discovery_app_data("BackboneInterface", Some("first.example"));
1124 let second = build_discovery_app_data("BackboneInterface", Some("second.example"));
1125 let third = build_discovery_app_data("BackboneInterface", Some("third.example"));
1126 let mut cache = DiscoveryStampCache::with_capacity(2);
1127
1128 for app_data in [&first, &second, &third] {
1129 assert!(parse_interface_announce_with_cache(
1130 app_data,
1131 &[0x11; 16],
1132 1,
1133 rns_core::stamp::StampCost::ZERO,
1134 &mut cache,
1135 )
1136 .is_some());
1137 }
1138 assert_eq!(cache.valid_len(), 2);
1139 assert!(!cache.contains_valid_app_data(&first));
1140 assert!(cache.contains_valid_app_data(&second));
1141 assert!(cache.contains_valid_app_data(&third));
1142
1143 let mut different_flags = second.clone();
1144 different_flags[0] ^= 0x01;
1145 assert!(cache.contains_valid_app_data(&different_flags));
1146 let mut different_stamp = second.clone();
1147 *different_stamp.last_mut().unwrap() ^= 0x01;
1148 assert!(!cache.contains_valid_app_data(&different_stamp));
1149 }
1150
1151 #[test]
1152 fn cached_stamp_value_is_rechecked_if_requirement_increases() {
1153 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1154 let mut cache = DiscoveryStampCache::with_capacity(8);
1155 assert!(parse_interface_announce_with_cache(
1156 &app_data,
1157 &[0x11; 16],
1158 1,
1159 rns_core::stamp::StampCost::ZERO,
1160 &mut cache,
1161 )
1162 .is_some());
1163 assert!(parse_interface_announce_with_cache(
1164 &app_data,
1165 &[0x11; 16],
1166 1,
1167 rns_core::stamp::StampCost::from_whole(100),
1168 &mut cache,
1169 )
1170 .is_none());
1171 assert_eq!(cache.valid_len(), 1);
1172 assert_eq!(cache.invalid_len(), 0);
1173 }
1174
1175 #[test]
1176 fn invalid_stamp_cache_keys_payload_including_stamp_but_excluding_flags() {
1177 let original = build_discovery_app_data("BackboneInterface", Some("example.com"));
1178 let mut cache = DiscoveryStampCache::with_capacity(8);
1179 assert!(parse_interface_announce_with_cache(
1180 &original,
1181 &[0x11; 16],
1182 1,
1183 rns_core::stamp::StampCost::from_whole(100),
1184 &mut cache,
1185 )
1186 .is_none());
1187
1188 let mut different_flags = original.clone();
1189 different_flags[0] ^= 0x01;
1190 assert!(cache.contains_invalid_app_data(&different_flags));
1191
1192 let mut different_stamp = original.clone();
1193 *different_stamp.last_mut().unwrap() ^= 0x01;
1194 assert!(!cache.contains_invalid_app_data(&different_stamp));
1195 }
1196
1197 #[test]
1198 fn malformed_discovery_payload_is_not_added_to_invalid_stamp_cache() {
1199 let mut cache = DiscoveryStampCache::with_capacity(8);
1200 assert!(parse_interface_announce_with_cache(
1201 &[0x00, 0x01, 0x02],
1202 &[0x11; 16],
1203 1,
1204 rns_core::stamp::StampCost::from_whole(100),
1205 &mut cache,
1206 )
1207 .is_none());
1208 assert_eq!(cache.invalid_len(), 0);
1209 }
1210
1211 #[test]
1212 fn parse_rejects_unsupported_discovered_interface_type() {
1213 let app_data = build_discovery_app_data("BogusInterface", None);
1214
1215 let parsed =
1216 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO);
1217
1218 assert!(
1219 parsed.is_none(),
1220 "unsupported discovered interface types must be ignored"
1221 );
1222 }
1223
1224 #[test]
1225 fn parse_rejects_invalid_reachable_on_address() {
1226 let app_data = build_discovery_app_data("BackboneInterface", Some("-not a host-"));
1227
1228 let parsed =
1229 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO);
1230
1231 assert!(
1232 parsed.is_none(),
1233 "discovered interfaces with invalid reachable_on values must be ignored"
1234 );
1235 }
1236
1237 #[test]
1238 fn parse_sanitizes_discovered_interface_name() {
1239 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1240 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
1241 entries.push((
1242 Value::UInt(NAME as u64),
1243 Value::Str("\t**Alpha Beta!!!\n".to_string()),
1244 ));
1245 let app_data = pack_discovery_entries(entries);
1246
1247 let parsed =
1248 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1249 .unwrap();
1250
1251 assert_eq!(parsed.name, "Alpha Beta");
1252 assert!(parsed.config_entry.unwrap().starts_with("[[Alpha Beta]]"));
1253 }
1254
1255 #[test]
1256 fn parse_retains_optional_stack_implementation_and_version() {
1257 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1258 entries.push((
1259 Value::UInt(TRANSPORT_IMPL as u64),
1260 Value::Str("Reticulum".into()),
1261 ));
1262 entries.push((
1263 Value::UInt(TRANSPORT_VERS as u64),
1264 Value::Str("1.5.5".into()),
1265 ));
1266 let parsed = parse_interface_announce(
1267 &pack_discovery_entries(entries),
1268 &[0x11; 16],
1269 1,
1270 rns_core::stamp::StampCost::ZERO,
1271 )
1272 .unwrap();
1273 assert_eq!(parsed.impl_name.as_deref(), Some("Reticulum"));
1274 assert_eq!(parsed.impl_version.as_deref(), Some("1.5.5"));
1275
1276 let legacy = parse_interface_announce(
1277 &build_discovery_app_data("BackboneInterface", Some("example.com")),
1278 &[0x11; 16],
1279 1,
1280 rns_core::stamp::StampCost::ZERO,
1281 )
1282 .unwrap();
1283 assert_eq!(legacy.impl_name, None);
1284 assert_eq!(legacy.impl_version, None);
1285 }
1286
1287 #[test]
1288 fn parse_reads_announced_stamp_costs() {
1289 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1291 entries.push((Value::UInt(DST_COST as u64), Value::Float(23.5)));
1292 entries.push((Value::UInt(PR_COST as u64), Value::UInt(15)));
1293 let parsed = parse_interface_announce(
1294 &pack_discovery_entries(entries),
1295 &[0x11; 16],
1296 1,
1297 StampCost::ZERO,
1298 )
1299 .unwrap();
1300 assert_eq!(parsed.dst_cost, Some(StampCost::from_f64(23.5)));
1301 assert_eq!(parsed.pr_cost, Some(StampCost::from_whole(15)));
1302
1303 let legacy = parse_interface_announce(
1304 &build_discovery_app_data("BackboneInterface", Some("example.com")),
1305 &[0x11; 16],
1306 1,
1307 StampCost::ZERO,
1308 )
1309 .unwrap();
1310 assert_eq!((legacy.dst_cost, legacy.pr_cost), (None, None));
1311
1312 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1314 entries.push((Value::UInt(PR_COST as u64), Value::Str("cheap".into())));
1315 assert!(parse_interface_announce(
1316 &pack_discovery_entries(entries),
1317 &[0x11; 16],
1318 1,
1319 StampCost::ZERO,
1320 )
1321 .is_none());
1322 }
1323
1324 #[test]
1325 fn parse_falls_back_when_discovered_interface_name_sanitizes_empty() {
1326 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1327 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
1328 entries.push((Value::UInt(NAME as u64), Value::Str("!!!".to_string())));
1329 let app_data = pack_discovery_entries(entries);
1330
1331 let parsed =
1332 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1333 .unwrap();
1334
1335 assert_eq!(parsed.name, "Discovered BackboneInterface");
1336 }
1337
1338 #[test]
1339 fn parse_falls_back_when_discovered_interface_name_is_nil() {
1340 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1341 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
1342 entries.push((Value::UInt(NAME as u64), Value::Nil));
1343 let app_data = pack_discovery_entries(entries);
1344
1345 let parsed =
1346 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1347 .unwrap();
1348
1349 assert_eq!(parsed.name, "Discovered BackboneInterface");
1350 }
1351
1352 #[test]
1353 fn parse_rejects_invalid_discovered_interface_field_types() {
1354 for (field, replacement) in [
1355 (TRANSPORT, Value::Str("yes".to_string())),
1356 (LATITUDE, Value::Str("45.0".to_string())),
1357 (LONGITUDE, Value::Str("9.0".to_string())),
1358 (HEIGHT, Value::Str("100".to_string())),
1359 (INTERFACE_TYPE, Value::UInt(123)),
1360 (REACHABLE_ON, Value::UInt(123)),
1361 ] {
1362 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1363 entries.retain(|(key, _)| key.as_uint() != Some(field as u64));
1364 entries.push((Value::UInt(field as u64), replacement));
1365 let app_data = pack_discovery_entries(entries);
1366
1367 let parsed = parse_interface_announce(
1368 &app_data,
1369 &[0x11; 16],
1370 1,
1371 rns_core::stamp::StampCost::ZERO,
1372 );
1373
1374 assert!(parsed.is_none(), "field {field} should reject invalid type");
1375 }
1376 }
1377
1378 #[test]
1379 fn parse_rejects_invalid_transport_id_length() {
1380 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1381 entries.retain(|(key, _)| key.as_uint() != Some(TRANSPORT_ID as u64));
1382 entries.push((Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 15])));
1383 let app_data = pack_discovery_entries(entries);
1384
1385 let parsed =
1386 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO);
1387
1388 assert!(parsed.is_none());
1389 }
1390
1391 #[test]
1392 fn parse_accepts_valid_string_ifac_fields() {
1393 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1394 entries.push((Value::UInt(IFAC_NETNAME as u64), Value::Str("mynet".into())));
1395 entries.push((Value::UInt(IFAC_NETKEY as u64), Value::Str("secret".into())));
1396 let app_data = pack_discovery_entries(entries);
1397
1398 let parsed =
1399 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1400 .unwrap();
1401
1402 assert_eq!(parsed.ifac_netname.as_deref(), Some("mynet"));
1403 assert_eq!(parsed.ifac_netkey.as_deref(), Some("secret"));
1404 }
1405
1406 #[test]
1407 fn parse_drops_nonsensical_ifac_fields() {
1408 for value in [
1409 Value::UInt(123),
1410 Value::Bool(true),
1411 Value::Nil,
1412 Value::Str("None".into()),
1413 Value::Str(String::new()),
1414 ] {
1415 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1416 entries.push((Value::UInt(IFAC_NETNAME as u64), value.clone()));
1417 entries.push((Value::UInt(IFAC_NETKEY as u64), value));
1418 let app_data = pack_discovery_entries(entries);
1419
1420 let parsed = parse_interface_announce(
1421 &app_data,
1422 &[0x11; 16],
1423 1,
1424 rns_core::stamp::StampCost::ZERO,
1425 )
1426 .unwrap();
1427
1428 assert_eq!(parsed.ifac_netname, None);
1429 assert_eq!(parsed.ifac_netkey, None);
1430 }
1431 }
1432
1433 #[test]
1434 fn sanitize_persisted_ifac_drops_none_and_empty() {
1435 assert_eq!(
1436 sanitize_persisted_ifac(Some("net".into())),
1437 Some("net".into())
1438 );
1439 assert_eq!(sanitize_persisted_ifac(Some("None".into())), None);
1440 assert_eq!(sanitize_persisted_ifac(Some(String::new())), None);
1441 assert_eq!(sanitize_persisted_ifac(None), None);
1442 }
1443
1444 #[test]
1445 fn parse_accepts_exact_operator_lxmf_address() {
1446 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1447 entries.push((Value::UInt(OP_ADDR as u64), Value::Bin(vec![0x5a; 16])));
1448 let app_data = pack_discovery_entries(entries);
1449
1450 let parsed =
1451 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1452 .unwrap();
1453
1454 assert_eq!(parsed.operator_lxmf_address, Some([0x5a; 16]));
1455 }
1456
1457 #[test]
1458 fn parse_rejects_non_binary_operator_lxmf_address() {
1459 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1460 entries.push((Value::UInt(OP_ADDR as u64), Value::Str("5a".repeat(16))));
1461 let app_data = pack_discovery_entries(entries);
1462
1463 assert!(parse_interface_announce(
1464 &app_data,
1465 &[0x11; 16],
1466 1,
1467 rns_core::stamp::StampCost::ZERO
1468 )
1469 .is_none());
1470 }
1471
1472 #[test]
1473 fn parse_ignores_operator_lxmf_address_with_wrong_length() {
1474 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1475 entries.push((Value::UInt(OP_ADDR as u64), Value::Bin(vec![0x5a; 15])));
1476 let app_data = pack_discovery_entries(entries);
1477
1478 let parsed =
1479 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1480 .unwrap();
1481
1482 assert_eq!(parsed.operator_lxmf_address, None);
1483 }
1484
1485 #[test]
1486 fn unsafe_discovery_endpoints_are_retained_without_autoconnect_config() {
1487 for address in ["example.onion", "127.0.0.1", "0.0.0.0"] {
1488 let app_data = build_discovery_app_data("BackboneInterface", Some(address));
1489 let parsed = parse_interface_announce(
1490 &app_data,
1491 &[0x11; 16],
1492 1,
1493 rns_core::stamp::StampCost::ZERO,
1494 )
1495 .unwrap();
1496
1497 assert_eq!(parsed.reachable_on.as_deref(), Some(address));
1498 assert_eq!(parsed.config_entry, None, "{address} must not autoconnect");
1499 }
1500 }
1501
1502 #[test]
1503 fn transport_autoconnect_mode_marks_transport_discovery_config_as_gateway() {
1504 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1505 let mut parsed =
1506 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1507 .unwrap();
1508
1509 apply_transport_autoconnect_mode(&mut parsed, true, None);
1510
1511 let config_entry = parsed.config_entry.unwrap();
1512 assert!(config_entry.contains(" enabled = yes\n interface_mode = gateway\n"));
1513 }
1514
1515 #[test]
1516 fn configured_autoconnect_mode_overrides_transport_fallback() {
1517 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1518 let mut parsed =
1519 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1520 .unwrap();
1521
1522 apply_transport_autoconnect_mode(
1523 &mut parsed,
1524 true,
1525 Some(rns_core::constants::MODE_BOUNDARY),
1526 );
1527
1528 let config_entry = parsed.config_entry.unwrap();
1529 assert!(config_entry.contains(" enabled = yes\n interface_mode = boundary\n"));
1530 }
1531
1532 #[test]
1533 fn transport_autoconnect_mode_does_not_modify_non_transport_contexts() {
1534 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1535 let mut parsed =
1536 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1537 .unwrap();
1538 let original = parsed.config_entry.clone();
1539
1540 apply_transport_autoconnect_mode(&mut parsed, false, None);
1541
1542 assert_eq!(parsed.config_entry, original);
1543 }
1544
1545 #[test]
1546 fn transport_autoconnect_mode_does_not_modify_non_transport_announces() {
1547 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
1548 entries.retain(|(key, _)| key.as_uint() != Some(TRANSPORT as u64));
1549 entries.push((Value::UInt(TRANSPORT as u64), Value::Bool(false)));
1550 let app_data = pack_discovery_entries(entries);
1551 let mut parsed =
1552 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1553 .unwrap();
1554 let original = parsed.config_entry.clone();
1555
1556 apply_transport_autoconnect_mode(&mut parsed, true, None);
1557
1558 assert_eq!(parsed.config_entry, original);
1559 }
1560
1561 #[test]
1562 fn transport_autoconnect_mode_preserves_existing_interface_mode() {
1563 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
1564 let mut parsed =
1565 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1566 .unwrap();
1567 let config_entry = parsed.config_entry.as_mut().unwrap();
1568 config_entry.push_str("\n interface_mode = access_point");
1569 let original = parsed.config_entry.clone();
1570
1571 apply_transport_autoconnect_mode(&mut parsed, true, None);
1572
1573 assert_eq!(parsed.config_entry, original);
1574 }
1575
1576 #[test]
1577 fn parse_yggdrasil_reachable_on_keeps_record_without_config_entry() {
1578 let app_data = build_discovery_app_data("BackboneInterface", Some("200:1234::1"));
1579
1580 let parsed =
1581 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1582 .unwrap();
1583
1584 assert_eq!(parsed.reachable_on.as_deref(), Some("200:1234::1"));
1585 assert!(parsed.config_entry.is_none());
1586 }
1587
1588 #[test]
1589 fn parse_accepts_supported_discovered_interface_types() {
1590 for interface_type in [
1591 "BackboneInterface",
1592 "TCPServerInterface",
1593 "I2PInterface",
1594 "RNodeInterface",
1595 "WeaveInterface",
1596 "KISSInterface",
1597 ] {
1598 let app_data = build_discovery_app_data(interface_type, Some("example.com"));
1599
1600 let parsed = parse_interface_announce(
1601 &app_data,
1602 &[0x11; 16],
1603 1,
1604 rns_core::stamp::StampCost::ZERO,
1605 );
1606
1607 assert!(
1608 parsed.is_some(),
1609 "{interface_type} should be accepted as a discoverable interface type"
1610 );
1611 }
1612 }
1613
1614 #[test]
1615 fn i2p_discovery_config_entry_uses_b32_hostname() {
1616 let app_data = build_discovery_app_data("I2PInterface", Some("example"));
1617 let parsed =
1618 parse_interface_announce(&app_data, &[0x11; 16], 1, rns_core::stamp::StampCost::ZERO)
1619 .unwrap();
1620
1621 assert_eq!(parsed.reachable_on.as_deref(), Some("example"));
1622 assert!(parsed
1623 .config_entry
1624 .as_deref()
1625 .unwrap()
1626 .contains("peers = example.b32.i2p\n"));
1627 }
1628}