1use rns_core::msgpack::{self, Value};
9use rns_core::stamp::{stamp_valid, stamp_value, stamp_workblock};
10use rns_crypto::sha256::sha256;
11
12use super::time;
13
14pub const NAME: u8 = 0xFF;
20pub const TRANSPORT_ID: u8 = 0xFE;
21pub const INTERFACE_TYPE: u8 = 0x00;
22pub const TRANSPORT: u8 = 0x01;
23pub const REACHABLE_ON: u8 = 0x02;
24pub const LATITUDE: u8 = 0x03;
25pub const LONGITUDE: u8 = 0x04;
26pub const HEIGHT: u8 = 0x05;
27pub const PORT: u8 = 0x06;
28pub const IFAC_NETNAME: u8 = 0x07;
29pub const IFAC_NETKEY: u8 = 0x08;
30pub const FREQUENCY: u8 = 0x09;
31pub const BANDWIDTH: u8 = 0x0A;
32pub const SPREADINGFACTOR: u8 = 0x0B;
33pub const CODINGRATE: u8 = 0x0C;
34pub const MODULATION: u8 = 0x0D;
35pub const CHANNEL: u8 = 0x0E;
36
37pub const APP_NAME: &str = "rnstransport";
39
40pub const DEFAULT_STAMP_VALUE: u8 = 14;
42
43pub const WORKBLOCK_EXPAND_ROUNDS: u32 = 20;
45
46pub const STAMP_SIZE: usize = 32;
48
49pub const DISCOVERABLE_TYPES: [&str; 6] = [
51 "BackboneInterface",
52 "TCPServerInterface",
53 "I2PInterface",
54 "RNodeInterface",
55 "WeaveInterface",
56 "KISSInterface",
57];
58
59pub const THRESHOLD_UNKNOWN: f64 = 24.0 * 60.0 * 60.0;
62pub const THRESHOLD_STALE: f64 = 3.0 * 24.0 * 60.0 * 60.0;
64pub const THRESHOLD_REMOVE: f64 = 7.0 * 24.0 * 60.0 * 60.0;
66
67const STATUS_STALE: i32 = 0;
69const STATUS_UNKNOWN: i32 = 100;
70const STATUS_AVAILABLE: i32 = 1000;
71
72#[derive(Debug, Clone)]
78pub struct DiscoveryConfig {
79 pub discovery_name: String,
81 pub announce_interval: u64,
83 pub stamp_value: u8,
85 pub reachable_on: Option<String>,
87 pub interface_type: String,
89 pub listen_port: Option<u16>,
91 pub latitude: Option<f64>,
93 pub longitude: Option<f64>,
95 pub height: Option<f64>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum DiscoveredStatus {
106 Available,
107 Unknown,
108 Stale,
109}
110
111impl DiscoveredStatus {
112 pub fn code(&self) -> i32 {
114 match self {
115 DiscoveredStatus::Available => STATUS_AVAILABLE,
116 DiscoveredStatus::Unknown => STATUS_UNKNOWN,
117 DiscoveredStatus::Stale => STATUS_STALE,
118 }
119 }
120
121 pub fn as_str(&self) -> &'static str {
123 match self {
124 DiscoveredStatus::Available => "available",
125 DiscoveredStatus::Unknown => "unknown",
126 DiscoveredStatus::Stale => "stale",
127 }
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct DiscoveredInterface {
134 pub interface_type: String,
136 pub transport: bool,
138 pub name: String,
140 pub discovered: f64,
142 pub last_heard: f64,
144 pub heard_count: u32,
146 pub status: DiscoveredStatus,
148 pub stamp: Vec<u8>,
150 pub stamp_value: u32,
152 pub transport_id: [u8; 16],
154 pub network_id: [u8; 16],
156 pub hops: u8,
158
159 pub latitude: Option<f64>,
161 pub longitude: Option<f64>,
162 pub height: Option<f64>,
163
164 pub reachable_on: Option<String>,
166 pub port: Option<u16>,
167
168 pub frequency: Option<u32>,
170 pub bandwidth: Option<u32>,
171 pub spreading_factor: Option<u8>,
172 pub coding_rate: Option<u8>,
173 pub modulation: Option<String>,
174 pub channel: Option<u8>,
175
176 pub ifac_netname: Option<String>,
178 pub ifac_netkey: Option<String>,
179
180 pub config_entry: Option<String>,
182
183 pub discovery_hash: [u8; 32],
185}
186
187impl DiscoveredInterface {
188 pub fn compute_status(&self) -> DiscoveredStatus {
190 let delta = time::now() - self.last_heard;
191 if delta > THRESHOLD_STALE {
192 DiscoveredStatus::Stale
193 } else if delta > THRESHOLD_UNKNOWN {
194 DiscoveredStatus::Unknown
195 } else {
196 DiscoveredStatus::Available
197 }
198 }
199}
200
201pub fn parse_interface_announce(
212 app_data: &[u8],
213 announced_identity_hash: &[u8; 16],
214 hops: u8,
215 required_stamp_value: u8,
216) -> Option<DiscoveredInterface> {
217 if app_data.len() <= STAMP_SIZE + 1 {
219 return None;
220 }
221
222 let flags = app_data[0];
224 let payload = &app_data[1..];
225
226 let encrypted = (flags & 0x02) != 0;
228 if encrypted {
229 log::debug!("Ignoring encrypted discovered interface (not supported)");
230 return None;
231 }
232
233 let stamp = &payload[payload.len() - STAMP_SIZE..];
235 let packed = &payload[..payload.len() - STAMP_SIZE];
236
237 let infohash = sha256(packed);
239 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
240
241 if !stamp_valid(stamp, required_stamp_value, &workblock) {
243 log::debug!("Ignoring discovered interface with invalid stamp");
244 return None;
245 }
246
247 let stamp_value = stamp_value(&workblock, stamp);
249
250 let (value, _) = msgpack::unpack(packed).ok()?;
252 let map = value.as_map()?;
253
254 let get_u8_val = |key: u8| -> Option<Value> {
256 for (k, v) in map {
257 if k.as_uint()? as u8 == key {
258 return Some(v.clone());
259 }
260 }
261 None
262 };
263
264 let interface_type = match get_u8_val(INTERFACE_TYPE)? {
266 Value::Str(value) => value,
267 _ => return None,
268 };
269 if !is_discoverable_type(&interface_type) {
270 log::debug!(
271 "Ignoring discovered interface with unsupported type '{}'",
272 interface_type
273 );
274 return None;
275 }
276
277 let transport = match get_u8_val(TRANSPORT)? {
278 Value::Bool(value) => value,
279 _ => return None,
280 };
281 let raw_name = match get_u8_val(NAME) {
282 Some(Value::Str(value)) => value,
283 Some(_) | None => String::new(),
284 };
285 let name = sanitize_discovered_name(&raw_name)
286 .unwrap_or_else(|| format!("Discovered {}", interface_type));
287
288 let transport_id_val = get_u8_val(TRANSPORT_ID)?;
289 let transport_id_bytes = transport_id_val.as_bin()?;
290 if transport_id_bytes.len() != 16 {
291 log::debug!("Ignoring discovered interface with invalid transport_id length");
292 return None;
293 }
294 let mut transport_id = [0u8; 16];
295 transport_id.copy_from_slice(transport_id_bytes);
296
297 let latitude = optional_f64_field(get_u8_val(LATITUDE))?;
299 let longitude = optional_f64_field(get_u8_val(LONGITUDE))?;
300 let height = optional_f64_field(get_u8_val(HEIGHT))?;
301 let reachable_on = match get_u8_val(REACHABLE_ON) {
302 None | Some(Value::Nil) => None,
303 Some(Value::Str(value)) => Some(value),
304 Some(_) => return None,
305 };
306 if let Some(ref reachable_on) = reachable_on {
307 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
308 log::debug!(
309 "Ignoring discovered interface with invalid reachable_on '{}'",
310 reachable_on
311 );
312 return None;
313 }
314 }
315
316 let port = get_u8_val(PORT).and_then(|v| v.as_uint().map(|n| n as u16));
317 let frequency = get_u8_val(FREQUENCY).and_then(|v| v.as_uint().map(|n| n as u32));
318 let bandwidth = get_u8_val(BANDWIDTH).and_then(|v| v.as_uint().map(|n| n as u32));
319 let spreading_factor = get_u8_val(SPREADINGFACTOR).and_then(|v| v.as_uint().map(|n| n as u8));
320 let coding_rate = get_u8_val(CODINGRATE).and_then(|v| v.as_uint().map(|n| n as u8));
321 let modulation = get_u8_val(MODULATION).and_then(|v| v.as_str().map(|s| s.to_string()));
322 let channel = get_u8_val(CHANNEL).and_then(|v| v.as_uint().map(|n| n as u8));
323 let ifac_netname = get_u8_val(IFAC_NETNAME).map(|v| discovery_value_to_string(&v));
324 let ifac_netkey = get_u8_val(IFAC_NETKEY).map(|v| discovery_value_to_string(&v));
325
326 let discovery_hash = compute_discovery_hash(&transport_id, &name);
328
329 let config_entry = generate_config_entry(
331 &interface_type,
332 &name,
333 &transport_id,
334 reachable_on.as_deref(),
335 port,
336 frequency,
337 bandwidth,
338 spreading_factor,
339 coding_rate,
340 modulation.as_deref(),
341 ifac_netname.as_deref(),
342 ifac_netkey.as_deref(),
343 );
344
345 let now = time::now();
346
347 Some(DiscoveredInterface {
348 interface_type,
349 transport,
350 name,
351 discovered: now,
352 last_heard: now,
353 heard_count: 0,
354 status: DiscoveredStatus::Available,
355 stamp: stamp.to_vec(),
356 stamp_value,
357 transport_id,
358 network_id: *announced_identity_hash,
359 hops,
360 latitude,
361 longitude,
362 height,
363 reachable_on,
364 port,
365 frequency,
366 bandwidth,
367 spreading_factor,
368 coding_rate,
369 modulation,
370 channel,
371 ifac_netname,
372 ifac_netkey,
373 config_entry,
374 discovery_hash,
375 })
376}
377
378pub fn compute_discovery_hash(transport_id: &[u8; 16], name: &str) -> [u8; 32] {
380 let mut material = Vec::with_capacity(16 + name.len());
381 material.extend_from_slice(transport_id);
382 material.extend_from_slice(name.as_bytes());
383 sha256(&material)
384}
385
386pub fn apply_transport_autoconnect_mode(
388 iface: &mut DiscoveredInterface,
389 local_transport_enabled: bool,
390) {
391 if !local_transport_enabled || !iface.transport {
392 return;
393 }
394 let Some(config_entry) = iface.config_entry.as_mut() else {
395 return;
396 };
397 if config_entry
398 .lines()
399 .any(|line| line.trim_start().starts_with("interface_mode"))
400 {
401 return;
402 }
403 if let Some(pos) = config_entry.find(" enabled = yes\n") {
404 let insert_at = pos + " enabled = yes\n".len();
405 config_entry.insert_str(insert_at, " interface_mode = gateway\n");
406 } else {
407 config_entry.push_str("\n interface_mode = gateway");
408 }
409}
410
411fn generate_config_entry(
413 interface_type: &str,
414 name: &str,
415 transport_id: &[u8; 16],
416 reachable_on: Option<&str>,
417 port: Option<u16>,
418 frequency: Option<u32>,
419 bandwidth: Option<u32>,
420 spreading_factor: Option<u8>,
421 coding_rate: Option<u8>,
422 modulation: Option<&str>,
423 ifac_netname: Option<&str>,
424 ifac_netkey: Option<&str>,
425) -> Option<String> {
426 if reachable_on.is_some_and(is_ygg_ipv6) {
427 return None;
428 }
429
430 let transport_id_hex = hex_encode(transport_id);
431 let netname_str = ifac_netname
432 .map(|n| format!("\n network_name = {}", n))
433 .unwrap_or_default();
434 let netkey_str = ifac_netkey
435 .map(|k| format!("\n passphrase = {}", k))
436 .unwrap_or_default();
437 let identity_str = format!("\n transport_identity = {}", transport_id_hex);
438
439 match interface_type {
440 "BackboneInterface" | "TCPServerInterface" => {
441 let reachable = reachable_on.unwrap_or("unknown");
442 let port_val = port.unwrap_or(4242);
443 Some(format!(
444 "[[{}]]\n type = BackboneInterface\n enabled = yes\n remote = {}\n target_port = {}{}{}{}",
445 name, reachable, port_val, identity_str, netname_str, netkey_str
446 ))
447 }
448 "I2PInterface" => {
449 let reachable = reachable_on.unwrap_or("unknown");
450 Some(format!(
451 "[[{}]]\n type = I2PInterface\n enabled = yes\n peers = {}{}{}{}",
452 name, reachable, identity_str, netname_str, netkey_str
453 ))
454 }
455 "RNodeInterface" => {
456 let freq_str = frequency
457 .map(|f| format!("\n frequency = {}", f))
458 .unwrap_or_default();
459 let bw_str = bandwidth
460 .map(|b| format!("\n bandwidth = {}", b))
461 .unwrap_or_default();
462 let sf_str = spreading_factor
463 .map(|s| format!("\n spreadingfactor = {}", s))
464 .unwrap_or_default();
465 let cr_str = coding_rate
466 .map(|c| format!("\n codingrate = {}", c))
467 .unwrap_or_default();
468 Some(format!(
469 "[[{}]]\n type = RNodeInterface\n enabled = yes\n port = {}{}{}{}{}{}{}{}",
470 name, "", freq_str, bw_str, sf_str, cr_str, identity_str, netname_str, netkey_str
471 ))
472 }
473 "KISSInterface" => {
474 let freq_str = frequency
475 .map(|f| format!("\n # Frequency: {}", f))
476 .unwrap_or_default();
477 let bw_str = bandwidth
478 .map(|b| format!("\n # Bandwidth: {}", b))
479 .unwrap_or_default();
480 let mod_str = modulation
481 .map(|m| format!("\n # Modulation: {}", m))
482 .unwrap_or_default();
483 Some(format!(
484 "[[{}]]\n type = KISSInterface\n enabled = yes\n port = {}{}{}{}{}{}{}",
485 name, "", freq_str, bw_str, mod_str, identity_str, netname_str, netkey_str
486 ))
487 }
488 "WeaveInterface" => Some(format!(
489 "[[{}]]\n type = WeaveInterface\n enabled = yes\n port = {}{}{}{}",
490 name, "", identity_str, netname_str, netkey_str
491 )),
492 _ => None,
493 }
494}
495
496fn optional_f64_field(value: Option<Value>) -> Option<Option<f64>> {
501 match value {
502 None | Some(Value::Nil) => Some(None),
503 Some(Value::Float(value)) if value.is_finite() => Some(Some(value)),
504 Some(_) => None,
505 }
506}
507
508fn discovery_value_to_string(value: &Value) -> String {
509 match value {
510 Value::Nil => "None".to_string(),
511 Value::Bool(value) => value.to_string(),
512 Value::UInt(value) => value.to_string(),
513 Value::Int(value) => value.to_string(),
514 Value::Float(value) => value.to_string(),
515 Value::Bin(value) => hex_encode(value),
516 Value::Str(value) => value.clone(),
517 Value::Array(_) => "[]".to_string(),
518 Value::Map(_) => "{}".to_string(),
519 }
520}
521
522pub fn sanitize_discovered_name(name: &str) -> Option<String> {
524 let ascii: String = name.chars().filter(|ch| ch.is_ascii()).collect();
525 let mut sanitized = ascii.trim().to_string();
526 while sanitized.contains(" ") {
527 sanitized = sanitized.replace(" ", " ");
528 }
529
530 let start = sanitized
531 .char_indices()
532 .find(|(_, ch)| ch.is_ascii_alphanumeric())
533 .map(|(idx, _)| idx)?;
534 let end = sanitized
535 .char_indices()
536 .rev()
537 .find(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == ')')
538 .map(|(idx, ch)| idx + ch.len_utf8())?;
539
540 if start >= end {
541 return None;
542 }
543
544 let sanitized = sanitized[start..end].to_string();
545 (!sanitized.is_empty()).then_some(sanitized)
546}
547
548pub fn hex_encode(bytes: &[u8]) -> String {
550 bytes.iter().map(|b| format!("{:02x}", b)).collect()
551}
552
553pub fn is_ip_address(s: &str) -> bool {
555 s.parse::<std::net::IpAddr>().is_ok()
556}
557
558pub fn is_ygg_ipv6(s: &str) -> bool {
560 match s.parse::<std::net::IpAddr>() {
561 Ok(std::net::IpAddr::V6(addr)) => {
562 let segments = addr.segments();
563 (segments[0] & 0xfe00) == 0x0200
564 }
565 _ => false,
566 }
567}
568
569pub fn is_hostname(s: &str) -> bool {
571 let s = s.strip_suffix('.').unwrap_or(s);
572 if s.len() > 253 {
573 return false;
574 }
575 let components: Vec<&str> = s.split('.').collect();
576 if components.is_empty() {
577 return false;
578 }
579 if components
581 .last()
582 .map(|c| c.chars().all(|ch| ch.is_ascii_digit()))
583 .unwrap_or(false)
584 {
585 return false;
586 }
587 components.iter().all(|c| {
588 !c.is_empty()
589 && c.len() <= 63
590 && !c.starts_with('-')
591 && !c.ends_with('-')
592 && c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
593 })
594}
595
596pub fn is_discoverable_type(interface_type: &str) -> bool {
598 DISCOVERABLE_TYPES.contains(&interface_type)
599}
600
601pub fn filter_and_sort_interfaces(
603 interfaces: &mut Vec<DiscoveredInterface>,
604 only_available: bool,
605 only_transport: bool,
606) {
607 let now = time::now();
608
609 interfaces.retain(|iface| {
611 if !is_discoverable_type(&iface.interface_type) {
612 return false;
613 }
614 if let Some(ref reachable_on) = iface.reachable_on {
615 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
616 return false;
617 }
618 }
619
620 let delta = now - iface.last_heard;
621
622 if delta > THRESHOLD_REMOVE {
624 return false;
625 }
626
627 let status = iface.compute_status();
629
630 if only_available && status != DiscoveredStatus::Available {
632 return false;
633 }
634 if only_transport && !iface.transport {
635 return false;
636 }
637
638 true
639 });
640
641 interfaces.sort_by(|a, b| {
643 let status_cmp = b.compute_status().code().cmp(&a.compute_status().code());
644 if status_cmp != std::cmp::Ordering::Equal {
645 return status_cmp;
646 }
647 let value_cmp = b.stamp_value.cmp(&a.stamp_value);
648 if value_cmp != std::cmp::Ordering::Equal {
649 return value_cmp;
650 }
651 b.last_heard
652 .partial_cmp(&a.last_heard)
653 .unwrap_or(std::cmp::Ordering::Equal)
654 });
655}
656
657pub fn discovery_name_hash() -> [u8; 10] {
662 rns_core::destination::name_hash(APP_NAME, &["discovery", "interface"])
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 fn pack_discovery_entries(entries: Vec<(Value, Value)>) -> Vec<u8> {
670 let packed = msgpack::pack(&Value::Map(entries));
671 let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
672 app_data.push(0x00);
673 app_data.extend_from_slice(&packed);
674 app_data.extend_from_slice(&[0u8; STAMP_SIZE]);
675 app_data
676 }
677
678 fn discovery_entries(interface_type: &str, reachable_on: Option<&str>) -> Vec<(Value, Value)> {
679 let mut entries = vec![
680 (
681 Value::UInt(INTERFACE_TYPE as u64),
682 Value::Str(interface_type.to_string()),
683 ),
684 (Value::UInt(TRANSPORT as u64), Value::Bool(true)),
685 (
686 Value::UInt(NAME as u64),
687 Value::Str(format!("test-{interface_type}")),
688 ),
689 (Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 16])),
690 ];
691
692 if let Some(reachable_on) = reachable_on {
693 entries.push((
694 Value::UInt(REACHABLE_ON as u64),
695 Value::Str(reachable_on.to_string()),
696 ));
697 }
698
699 entries
700 }
701
702 fn build_discovery_app_data(interface_type: &str, reachable_on: Option<&str>) -> Vec<u8> {
703 pack_discovery_entries(discovery_entries(interface_type, reachable_on))
704 }
705
706 #[test]
707 fn parse_rejects_unsupported_discovered_interface_type() {
708 let app_data = build_discovery_app_data("BogusInterface", None);
709
710 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
711
712 assert!(
713 parsed.is_none(),
714 "unsupported discovered interface types must be ignored"
715 );
716 }
717
718 #[test]
719 fn parse_rejects_invalid_reachable_on_address() {
720 let app_data = build_discovery_app_data("BackboneInterface", Some("-not a host-"));
721
722 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
723
724 assert!(
725 parsed.is_none(),
726 "discovered interfaces with invalid reachable_on values must be ignored"
727 );
728 }
729
730 #[test]
731 fn parse_sanitizes_discovered_interface_name() {
732 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
733 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
734 entries.push((
735 Value::UInt(NAME as u64),
736 Value::Str("\t**Alpha Beta!!!\n".to_string()),
737 ));
738 let app_data = pack_discovery_entries(entries);
739
740 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
741
742 assert_eq!(parsed.name, "Alpha Beta");
743 assert!(parsed.config_entry.unwrap().starts_with("[[Alpha Beta]]"));
744 }
745
746 #[test]
747 fn parse_falls_back_when_discovered_interface_name_sanitizes_empty() {
748 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
749 entries.retain(|(key, _)| key.as_uint() != Some(NAME as u64));
750 entries.push((Value::UInt(NAME as u64), Value::Str("!!!".to_string())));
751 let app_data = pack_discovery_entries(entries);
752
753 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
754
755 assert_eq!(parsed.name, "Discovered BackboneInterface");
756 }
757
758 #[test]
759 fn parse_rejects_invalid_discovered_interface_field_types() {
760 for (field, replacement) in [
761 (TRANSPORT, Value::Str("yes".to_string())),
762 (LATITUDE, Value::Str("45.0".to_string())),
763 (LONGITUDE, Value::Str("9.0".to_string())),
764 (HEIGHT, Value::Str("100".to_string())),
765 (INTERFACE_TYPE, Value::UInt(123)),
766 (REACHABLE_ON, Value::UInt(123)),
767 ] {
768 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
769 entries.retain(|(key, _)| key.as_uint() != Some(field as u64));
770 entries.push((Value::UInt(field as u64), replacement));
771 let app_data = pack_discovery_entries(entries);
772
773 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
774
775 assert!(parsed.is_none(), "field {field} should reject invalid type");
776 }
777 }
778
779 #[test]
780 fn parse_rejects_invalid_transport_id_length() {
781 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
782 entries.retain(|(key, _)| key.as_uint() != Some(TRANSPORT_ID as u64));
783 entries.push((Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 15])));
784 let app_data = pack_discovery_entries(entries);
785
786 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
787
788 assert!(parsed.is_none());
789 }
790
791 #[test]
792 fn parse_converts_ifac_fields_to_strings() {
793 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
794 entries.push((Value::UInt(IFAC_NETNAME as u64), Value::UInt(123)));
795 entries.push((Value::UInt(IFAC_NETKEY as u64), Value::Bool(true)));
796 let app_data = pack_discovery_entries(entries);
797
798 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
799
800 assert_eq!(parsed.ifac_netname.as_deref(), Some("123"));
801 assert_eq!(parsed.ifac_netkey.as_deref(), Some("true"));
802 }
803
804 #[test]
805 fn transport_autoconnect_mode_marks_transport_discovery_config_as_gateway() {
806 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
807 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
808
809 apply_transport_autoconnect_mode(&mut parsed, true);
810
811 let config_entry = parsed.config_entry.unwrap();
812 assert!(config_entry.contains(" enabled = yes\n interface_mode = gateway\n"));
813 }
814
815 #[test]
816 fn transport_autoconnect_mode_does_not_modify_non_transport_contexts() {
817 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
818 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
819 let original = parsed.config_entry.clone();
820
821 apply_transport_autoconnect_mode(&mut parsed, false);
822
823 assert_eq!(parsed.config_entry, original);
824 }
825
826 #[test]
827 fn transport_autoconnect_mode_does_not_modify_non_transport_announces() {
828 let mut entries = discovery_entries("BackboneInterface", Some("example.com"));
829 entries.retain(|(key, _)| key.as_uint() != Some(TRANSPORT as u64));
830 entries.push((Value::UInt(TRANSPORT as u64), Value::Bool(false)));
831 let app_data = pack_discovery_entries(entries);
832 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
833 let original = parsed.config_entry.clone();
834
835 apply_transport_autoconnect_mode(&mut parsed, true);
836
837 assert_eq!(parsed.config_entry, original);
838 }
839
840 #[test]
841 fn transport_autoconnect_mode_preserves_existing_interface_mode() {
842 let app_data = build_discovery_app_data("BackboneInterface", Some("example.com"));
843 let mut parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
844 let config_entry = parsed.config_entry.as_mut().unwrap();
845 config_entry.push_str("\n interface_mode = access_point");
846 let original = parsed.config_entry.clone();
847
848 apply_transport_autoconnect_mode(&mut parsed, true);
849
850 assert_eq!(parsed.config_entry, original);
851 }
852
853 #[test]
854 fn parse_yggdrasil_reachable_on_keeps_record_without_config_entry() {
855 let app_data = build_discovery_app_data("BackboneInterface", Some("200:1234::1"));
856
857 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0).unwrap();
858
859 assert_eq!(parsed.reachable_on.as_deref(), Some("200:1234::1"));
860 assert!(parsed.config_entry.is_none());
861 }
862
863 #[test]
864 fn parse_accepts_supported_discovered_interface_types() {
865 for interface_type in [
866 "BackboneInterface",
867 "TCPServerInterface",
868 "I2PInterface",
869 "RNodeInterface",
870 "WeaveInterface",
871 "KISSInterface",
872 ] {
873 let app_data = build_discovery_app_data(interface_type, Some("example.com"));
874
875 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
876
877 assert!(
878 parsed.is_some(),
879 "{interface_type} should be accepted as a discoverable interface type"
880 );
881 }
882 }
883}