1pub use crate::common::discovery::*;
13
14use std::fs;
15use std::io;
16use std::path::PathBuf;
17
18use rns_core::msgpack::{self, Value};
19use rns_core::stamp::{stamp_valid, stamp_workblock};
20use rns_crypto::sha256::sha256;
21
22use crate::time;
23
24pub struct DiscoveredInterfaceStorage {
30 base_path: PathBuf,
31}
32
33impl DiscoveredInterfaceStorage {
34 pub fn new(base_path: PathBuf) -> Self {
36 Self { base_path }
37 }
38
39 pub fn store(&self, iface: &DiscoveredInterface) -> io::Result<()> {
41 let filename = hex_encode(&iface.discovery_hash);
42 let filepath = self.base_path.join(filename);
43
44 let data = self.serialize_interface(iface)?;
45 fs::write(&filepath, &data)
46 }
47
48 pub fn store_received(&self, iface: &mut DiscoveredInterface) -> io::Result<()> {
50 match self.load(&iface.discovery_hash) {
51 Ok(Some(existing)) => {
52 iface.discovered = existing.discovered;
53 iface.heard_count = existing.heard_count.saturating_add(1);
54 }
55 Ok(None) => {
56 iface.discovered = iface.last_heard;
57 iface.heard_count = 1;
58 }
59 Err(err) => {
60 log::error!(
61 "Error while reading existing data for discovered interface, re-creating data: {}",
62 err
63 );
64 iface.discovered = iface.last_heard;
65 iface.heard_count = 1;
66 }
67 }
68
69 self.store(iface)
70 }
71
72 pub fn load(&self, discovery_hash: &[u8; 32]) -> io::Result<Option<DiscoveredInterface>> {
74 let filename = hex_encode(discovery_hash);
75 let filepath = self.base_path.join(filename);
76
77 if !filepath.exists() {
78 return Ok(None);
79 }
80
81 let data = fs::read(&filepath)?;
82 self.deserialize_interface(&data).map(Some)
83 }
84
85 pub fn list(&self) -> io::Result<Vec<DiscoveredInterface>> {
87 let mut interfaces = Vec::new();
88
89 let entries = match fs::read_dir(&self.base_path) {
90 Ok(e) => e,
91 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(interfaces),
92 Err(e) => return Err(e),
93 };
94
95 for entry in entries {
96 let entry = entry?;
97 let path = entry.path();
98
99 if !path.is_file() {
100 continue;
101 }
102
103 match fs::read(&path) {
104 Ok(data) => {
105 if let Ok(iface) = self.deserialize_interface(&data) {
106 interfaces.push(iface);
107 }
108 }
109 Err(_) => continue,
110 }
111 }
112
113 Ok(interfaces)
114 }
115
116 pub fn remove(&self, discovery_hash: &[u8; 32]) -> io::Result<()> {
118 let filename = hex_encode(discovery_hash);
119 let filepath = self.base_path.join(filename);
120
121 if filepath.exists() {
122 fs::remove_file(&filepath)?;
123 }
124 Ok(())
125 }
126
127 pub fn cleanup(&self) -> io::Result<usize> {
130 let mut removed = 0;
131 let now = time::now();
132
133 let interfaces = self.list()?;
134 for iface in interfaces {
135 let invalid_reachable_on = iface
136 .reachable_on
137 .as_ref()
138 .map(|reachable_on| !(is_ip_address(reachable_on) || is_hostname(reachable_on)))
139 .unwrap_or(false);
140
141 if !is_discoverable_type(&iface.interface_type)
142 || invalid_reachable_on
143 || now - iface.last_heard > THRESHOLD_REMOVE
144 {
145 self.remove(&iface.discovery_hash)?;
146 removed += 1;
147 }
148 }
149
150 Ok(removed)
151 }
152
153 fn serialize_interface(&self, iface: &DiscoveredInterface) -> io::Result<Vec<u8>> {
155 let mut entries: Vec<(Value, Value)> = Vec::new();
156
157 entries.push((
158 Value::Str("type".into()),
159 Value::Str(iface.interface_type.clone()),
160 ));
161 entries.push((Value::Str("transport".into()), Value::Bool(iface.transport)));
162 entries.push((Value::Str("name".into()), Value::Str(iface.name.clone())));
163 entries.push((
164 Value::Str("discovered".into()),
165 Value::Float(iface.discovered),
166 ));
167 entries.push((
168 Value::Str("last_heard".into()),
169 Value::Float(iface.last_heard),
170 ));
171 entries.push((
172 Value::Str("heard_count".into()),
173 Value::UInt(iface.heard_count as u64),
174 ));
175 entries.push((
176 Value::Str("status".into()),
177 Value::Str(iface.status.as_str().into()),
178 ));
179 entries.push((Value::Str("stamp".into()), Value::Bin(iface.stamp.clone())));
180 entries.push((
181 Value::Str("value".into()),
182 Value::UInt(iface.stamp_value as u64),
183 ));
184 entries.push((
185 Value::Str("transport_id".into()),
186 Value::Bin(iface.transport_id.to_vec()),
187 ));
188 entries.push((
189 Value::Str("network_id".into()),
190 Value::Bin(iface.network_id.to_vec()),
191 ));
192 entries.push((Value::Str("hops".into()), Value::UInt(iface.hops as u64)));
193
194 if let Some(v) = iface.latitude {
195 entries.push((Value::Str("latitude".into()), Value::Float(v)));
196 }
197 if let Some(v) = iface.longitude {
198 entries.push((Value::Str("longitude".into()), Value::Float(v)));
199 }
200 if let Some(v) = iface.height {
201 entries.push((Value::Str("height".into()), Value::Float(v)));
202 }
203 if let Some(ref v) = iface.reachable_on {
204 entries.push((Value::Str("reachable_on".into()), Value::Str(v.clone())));
205 }
206 if let Some(v) = iface.port {
207 entries.push((Value::Str("port".into()), Value::UInt(v as u64)));
208 }
209 if let Some(v) = iface.frequency {
210 entries.push((Value::Str("frequency".into()), Value::UInt(v as u64)));
211 }
212 if let Some(v) = iface.bandwidth {
213 entries.push((Value::Str("bandwidth".into()), Value::UInt(v as u64)));
214 }
215 if let Some(v) = iface.spreading_factor {
216 entries.push((Value::Str("sf".into()), Value::UInt(v as u64)));
217 }
218 if let Some(v) = iface.coding_rate {
219 entries.push((Value::Str("cr".into()), Value::UInt(v as u64)));
220 }
221 if let Some(ref v) = iface.modulation {
222 entries.push((Value::Str("modulation".into()), Value::Str(v.clone())));
223 }
224 if let Some(v) = iface.channel {
225 entries.push((Value::Str("channel".into()), Value::UInt(v as u64)));
226 }
227 if let Some(ref v) = iface.ifac_netname {
228 entries.push((Value::Str("ifac_netname".into()), Value::Str(v.clone())));
229 }
230 if let Some(ref v) = iface.ifac_netkey {
231 entries.push((Value::Str("ifac_netkey".into()), Value::Str(v.clone())));
232 }
233 if let Some(ref v) = iface.config_entry {
234 entries.push((Value::Str("config_entry".into()), Value::Str(v.clone())));
235 }
236
237 entries.push((
238 Value::Str("discovery_hash".into()),
239 Value::Bin(iface.discovery_hash.to_vec()),
240 ));
241
242 Ok(msgpack::pack(&Value::Map(entries)))
243 }
244
245 fn deserialize_interface(&self, data: &[u8]) -> io::Result<DiscoveredInterface> {
247 let (value, _) = msgpack::unpack(data).map_err(|e| {
248 io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e))
249 })?;
250
251 let get_str = |v: &Value, key: &str| -> io::Result<String> {
253 v.map_get(key)
254 .and_then(|val| val.as_str())
255 .map(|s| s.to_string())
256 .ok_or_else(|| {
257 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a string", key))
258 })
259 };
260
261 let get_opt_str = |v: &Value, key: &str| -> Option<String> {
262 v.map_get(key)
263 .and_then(|val| val.as_str().map(|s| s.to_string()))
264 };
265
266 let get_bool = |v: &Value, key: &str| -> io::Result<bool> {
267 v.map_get(key).and_then(|val| val.as_bool()).ok_or_else(|| {
268 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a bool", key))
269 })
270 };
271
272 let get_float = |v: &Value, key: &str| -> io::Result<f64> {
273 v.map_get(key)
274 .and_then(|val| val.as_float())
275 .ok_or_else(|| {
276 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a float", key))
277 })
278 };
279
280 let get_opt_float =
281 |v: &Value, key: &str| -> Option<f64> { v.map_get(key).and_then(|val| val.as_float()) };
282
283 let get_uint = |v: &Value, key: &str| -> io::Result<u64> {
284 v.map_get(key).and_then(|val| val.as_uint()).ok_or_else(|| {
285 io::Error::new(io::ErrorKind::InvalidData, format!("{} not a uint", key))
286 })
287 };
288
289 let get_opt_uint =
290 |v: &Value, key: &str| -> Option<u64> { v.map_get(key).and_then(|val| val.as_uint()) };
291
292 let get_bytes = |v: &Value, key: &str| -> io::Result<Vec<u8>> {
293 v.map_get(key)
294 .and_then(|val| val.as_bin())
295 .map(|b| b.to_vec())
296 .ok_or_else(|| {
297 io::Error::new(io::ErrorKind::InvalidData, format!("{} not bytes", key))
298 })
299 };
300
301 let fixed_bytes = |key: &str, expected_len: usize| -> io::Result<Vec<u8>> {
302 let bytes = get_bytes(&value, key)?;
303 if bytes.len() != expected_len {
304 return Err(io::Error::new(
305 io::ErrorKind::InvalidData,
306 format!("{} must be {} bytes", key, expected_len),
307 ));
308 }
309 Ok(bytes)
310 };
311
312 let transport_id_bytes = fixed_bytes("transport_id", 16)?;
313 let mut transport_id = [0u8; 16];
314 transport_id.copy_from_slice(&transport_id_bytes);
315
316 let network_id_bytes = fixed_bytes("network_id", 16)?;
317 let mut network_id = [0u8; 16];
318 network_id.copy_from_slice(&network_id_bytes);
319
320 let discovery_hash_bytes = fixed_bytes("discovery_hash", 32)?;
321 let mut discovery_hash = [0u8; 32];
322 discovery_hash.copy_from_slice(&discovery_hash_bytes);
323
324 let status_str = get_str(&value, "status")?;
325 let status = match status_str.as_str() {
326 "available" => DiscoveredStatus::Available,
327 "unknown" => DiscoveredStatus::Unknown,
328 "stale" => DiscoveredStatus::Stale,
329 _ => DiscoveredStatus::Unknown,
330 };
331
332 let interface_type = get_str(&value, "type")?;
333 let raw_name = get_str(&value, "name")?;
334 let name = sanitize_discovered_name(&raw_name)
335 .unwrap_or_else(|| format!("Discovered {}", interface_type));
336
337 Ok(DiscoveredInterface {
338 interface_type,
339 transport: get_bool(&value, "transport")?,
340 name,
341 discovered: get_float(&value, "discovered")?,
342 last_heard: get_float(&value, "last_heard")?,
343 heard_count: get_uint(&value, "heard_count")? as u32,
344 status,
345 stamp: get_bytes(&value, "stamp")?,
346 stamp_value: get_uint(&value, "value")? as u32,
347 transport_id,
348 network_id,
349 hops: get_uint(&value, "hops")? as u8,
350 latitude: get_opt_float(&value, "latitude"),
351 longitude: get_opt_float(&value, "longitude"),
352 height: get_opt_float(&value, "height"),
353 reachable_on: get_opt_str(&value, "reachable_on"),
354 port: get_opt_uint(&value, "port").map(|v| v as u16),
355 frequency: get_opt_uint(&value, "frequency").map(|v| v as u32),
356 bandwidth: get_opt_uint(&value, "bandwidth").map(|v| v as u32),
357 spreading_factor: get_opt_uint(&value, "sf").map(|v| v as u8),
358 coding_rate: get_opt_uint(&value, "cr").map(|v| v as u8),
359 modulation: get_opt_str(&value, "modulation"),
360 channel: get_opt_uint(&value, "channel").map(|v| v as u8),
361 ifac_netname: get_opt_str(&value, "ifac_netname"),
362 ifac_netkey: get_opt_str(&value, "ifac_netkey"),
363 config_entry: get_opt_str(&value, "config_entry"),
364 discovery_hash,
365 })
366 }
367}
368
369pub fn generate_discovery_stamp(packed_data: &[u8], stamp_cost: u8) -> ([u8; STAMP_SIZE], u32) {
377 use rns_crypto::{OsRng, Rng};
378 use std::sync::atomic::{AtomicBool, Ordering};
379 use std::sync::{Arc, Mutex};
380
381 let infohash = sha256(packed_data);
382 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
383
384 let found: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
385 let result: Arc<Mutex<Option<[u8; STAMP_SIZE]>>> = Arc::new(Mutex::new(None));
386
387 let num_threads = rayon::current_num_threads();
388
389 rayon::scope(|s| {
390 for _ in 0..num_threads {
391 let found = found.clone();
392 let result = result.clone();
393 let workblock = &workblock;
394 s.spawn(move |_| {
395 let mut rng = OsRng;
396 let mut nonce = [0u8; STAMP_SIZE];
397 loop {
398 if found.load(Ordering::Relaxed) {
399 return;
400 }
401 rng.fill_bytes(&mut nonce);
402 if stamp_valid(&nonce, stamp_cost, workblock) {
403 let mut r = match result.lock() {
404 Ok(guard) => guard,
405 Err(poisoned) => {
406 log::error!(
407 "recovering from poisoned discovery stamp result buffer"
408 );
409 poisoned.into_inner()
410 }
411 };
412 if r.is_none() {
413 *r = Some(nonce);
414 }
415 found.store(true, Ordering::Relaxed);
416 return;
417 }
418 }
419 });
420 }
421 });
422
423 let stamp = match result.lock() {
424 Ok(mut guard) => guard.take(),
425 Err(poisoned) => {
426 log::error!("recovering from poisoned discovery stamp result buffer");
427 poisoned.into_inner().take()
428 }
429 }
430 .unwrap_or_else(|| {
431 log::error!("parallel discovery stamp search returned no result; retrying synchronously");
432 let mut rng = OsRng;
433 let mut nonce = [0u8; STAMP_SIZE];
434 loop {
435 rng.fill_bytes(&mut nonce);
436 if stamp_valid(&nonce, stamp_cost, &workblock) {
437 return nonce;
438 }
439 }
440 });
441 let value = rns_core::stamp::stamp_value(&workblock, &stamp);
442 (stamp, value)
443}
444
445#[derive(Debug, Clone)]
451pub struct DiscoverableInterface {
452 pub interface_name: String,
454 pub config: DiscoveryConfig,
455 pub transport_enabled: bool,
457 pub ifac_netname: Option<String>,
459 pub ifac_netkey: Option<String>,
461}
462
463pub struct StampResult {
465 pub interface_name: String,
467 pub app_data: Vec<u8>,
469}
470
471pub struct InterfaceAnnouncer {
477 transport_id: [u8; 16],
479 interfaces: Vec<DiscoverableInterface>,
481 last_announced: Vec<f64>,
483 stamp_rx: std::sync::mpsc::Receiver<StampResult>,
485 stamp_tx: std::sync::mpsc::Sender<StampResult>,
487 stamp_pending: bool,
489}
490
491impl InterfaceAnnouncer {
492 pub fn new(transport_id: [u8; 16], interfaces: Vec<DiscoverableInterface>) -> Self {
494 let n = interfaces.len();
495 let (stamp_tx, stamp_rx) = std::sync::mpsc::channel();
496 InterfaceAnnouncer {
497 transport_id,
498 interfaces,
499 last_announced: vec![0.0; n],
500 stamp_rx,
501 stamp_tx,
502 stamp_pending: false,
503 }
504 }
505
506 pub fn maybe_start(&mut self, now: f64) {
510 if self.stamp_pending {
511 return;
512 }
513 let due_index = self.interfaces.iter().enumerate().find_map(|(i, iface)| {
514 let elapsed = now - self.last_announced[i];
515 if elapsed >= iface.config.announce_interval as f64 {
516 Some(i)
517 } else {
518 None
519 }
520 });
521
522 if let Some(idx) = due_index {
523 let packed = self.pack_interface_info(idx);
524 let stamp_cost = self.interfaces[idx].config.stamp_value;
525 let name = self.interfaces[idx].config.discovery_name.clone();
526 let interface_name = self.interfaces[idx].interface_name.clone();
527 let tx = self.stamp_tx.clone();
528
529 log::info!(
530 "Spawning discovery stamp generation (cost={}) for '{}'...",
531 stamp_cost,
532 name,
533 );
534
535 self.stamp_pending = true;
536 self.last_announced[idx] = now;
537
538 std::thread::spawn(move || {
539 let (stamp, value) = generate_discovery_stamp(&packed, stamp_cost);
540 log::info!("Discovery stamp generated (value={}) for '{}'", value, name,);
541
542 let flags: u8 = 0x00; let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
544 app_data.push(flags);
545 app_data.extend_from_slice(&packed);
546 app_data.extend_from_slice(&stamp);
547
548 let _ = tx.send(StampResult {
549 interface_name,
550 app_data,
551 });
552 });
553 }
554 }
555
556 pub fn poll_ready(&mut self) -> Option<StampResult> {
559 match self.stamp_rx.try_recv() {
560 Ok(result) => {
561 self.stamp_pending = false;
562 Some(result)
563 }
564 Err(_) => None,
565 }
566 }
567
568 pub fn contains_interface(&self, interface_name: &str) -> bool {
570 self.interfaces
571 .iter()
572 .any(|iface| iface.interface_name == interface_name)
573 }
574
575 pub fn upsert_interface(&mut self, iface: DiscoverableInterface) {
577 if let Some(index) = self
578 .interfaces
579 .iter()
580 .position(|existing| existing.interface_name == iface.interface_name)
581 {
582 self.interfaces[index] = iface;
583 return;
584 }
585 self.interfaces.push(iface);
586 self.last_announced.push(0.0);
587 }
588
589 pub fn remove_interface(&mut self, interface_name: &str) -> bool {
591 if let Some(index) = self
592 .interfaces
593 .iter()
594 .position(|iface| iface.interface_name == interface_name)
595 {
596 self.interfaces.remove(index);
597 self.last_announced.remove(index);
598 true
599 } else {
600 false
601 }
602 }
603
604 pub fn is_empty(&self) -> bool {
606 self.interfaces.is_empty()
607 }
608
609 fn pack_interface_info(&self, index: usize) -> Vec<u8> {
611 let iface = &self.interfaces[index];
612 let mut entries: Vec<(msgpack::Value, msgpack::Value)> = Vec::new();
613
614 entries.push((
615 msgpack::Value::UInt(INTERFACE_TYPE as u64),
616 msgpack::Value::Str(iface.config.interface_type.clone()),
617 ));
618 entries.push((
619 msgpack::Value::UInt(TRANSPORT as u64),
620 msgpack::Value::Bool(iface.transport_enabled),
621 ));
622 entries.push((
623 msgpack::Value::UInt(NAME as u64),
624 msgpack::Value::Str(iface.config.discovery_name.clone()),
625 ));
626 entries.push((
627 msgpack::Value::UInt(TRANSPORT_ID as u64),
628 msgpack::Value::Bin(self.transport_id.to_vec()),
629 ));
630 if let Some(ref reachable) = iface.config.reachable_on {
631 entries.push((
632 msgpack::Value::UInt(REACHABLE_ON as u64),
633 msgpack::Value::Str(reachable.clone()),
634 ));
635 }
636 if let Some(port) = iface.config.listen_port {
637 entries.push((
638 msgpack::Value::UInt(PORT as u64),
639 msgpack::Value::UInt(port as u64),
640 ));
641 }
642 if let Some(lat) = iface.config.latitude {
643 entries.push((
644 msgpack::Value::UInt(LATITUDE as u64),
645 msgpack::Value::Float(lat),
646 ));
647 }
648 if let Some(lon) = iface.config.longitude {
649 entries.push((
650 msgpack::Value::UInt(LONGITUDE as u64),
651 msgpack::Value::Float(lon),
652 ));
653 }
654 if let Some(h) = iface.config.height {
655 entries.push((
656 msgpack::Value::UInt(HEIGHT as u64),
657 msgpack::Value::Float(h),
658 ));
659 }
660 if let Some(ref netname) = iface.ifac_netname {
661 entries.push((
662 msgpack::Value::UInt(IFAC_NETNAME as u64),
663 msgpack::Value::Str(netname.clone()),
664 ));
665 }
666 if let Some(ref netkey) = iface.ifac_netkey {
667 entries.push((
668 msgpack::Value::UInt(IFAC_NETKEY as u64),
669 msgpack::Value::Str(netkey.clone()),
670 ));
671 }
672
673 msgpack::pack(&msgpack::Value::Map(entries))
674 }
675}
676
677#[cfg(test)]
682mod tests {
683 use super::*;
684
685 #[test]
686 fn test_hex_encode() {
687 assert_eq!(hex_encode(&[0x00, 0xff, 0x12]), "00ff12");
688 assert_eq!(hex_encode(&[]), "");
689 }
690
691 #[test]
692 fn test_compute_discovery_hash() {
693 let transport_id = [0x42u8; 16];
694 let name = "TestInterface";
695 let hash = compute_discovery_hash(&transport_id, name);
696
697 let hash2 = compute_discovery_hash(&transport_id, name);
699 assert_eq!(hash, hash2);
700
701 let hash3 = compute_discovery_hash(&transport_id, "OtherInterface");
703 assert_ne!(hash, hash3);
704 }
705
706 #[test]
707 fn test_is_ip_address() {
708 assert!(is_ip_address("192.168.1.1"));
709 assert!(is_ip_address("::1"));
710 assert!(is_ip_address("2001:db8::1"));
711 assert!(!is_ip_address("not-an-ip"));
712 assert!(!is_ip_address("hostname.example.com"));
713 }
714
715 #[test]
716 fn test_is_hostname() {
717 assert!(is_hostname("example.com"));
718 assert!(is_hostname("sub.example.com"));
719 assert!(is_hostname("my-node"));
720 assert!(is_hostname("my-node.example.com"));
721 assert!(!is_hostname(""));
722 assert!(!is_hostname("-invalid"));
723 assert!(!is_hostname("invalid-"));
724 assert!(!is_hostname("a".repeat(300).as_str()));
725 }
726
727 #[test]
728 fn test_discovered_status() {
729 let now = time::now();
730
731 let mut iface = DiscoveredInterface {
732 interface_type: "TestInterface".into(),
733 transport: true,
734 name: "Test".into(),
735 discovered: now,
736 last_heard: now,
737 heard_count: 0,
738 status: DiscoveredStatus::Available,
739 stamp: vec![],
740 stamp_value: 14,
741 transport_id: [0u8; 16],
742 network_id: [0u8; 16],
743 hops: 0,
744 latitude: None,
745 longitude: None,
746 height: None,
747 reachable_on: None,
748 port: None,
749 frequency: None,
750 bandwidth: None,
751 spreading_factor: None,
752 coding_rate: None,
753 modulation: None,
754 channel: None,
755 ifac_netname: None,
756 ifac_netkey: None,
757 config_entry: None,
758 discovery_hash: [0u8; 32],
759 };
760
761 assert_eq!(iface.compute_status(), DiscoveredStatus::Available);
763
764 iface.last_heard = now - THRESHOLD_UNKNOWN - 3600.0;
766 assert_eq!(iface.compute_status(), DiscoveredStatus::Unknown);
767
768 iface.last_heard = now - THRESHOLD_STALE - 3600.0;
770 assert_eq!(iface.compute_status(), DiscoveredStatus::Stale);
771 }
772
773 fn test_discovered_interface(name: &str) -> DiscoveredInterface {
774 DiscoveredInterface {
775 interface_type: "BackboneInterface".into(),
776 transport: true,
777 name: name.into(),
778 discovered: 1700000000.0,
779 last_heard: 1700001000.0,
780 heard_count: 5,
781 status: DiscoveredStatus::Available,
782 stamp: vec![0x42u8; 64],
783 stamp_value: 18,
784 transport_id: [0x01u8; 16],
785 network_id: [0x02u8; 16],
786 hops: 2,
787 latitude: Some(45.0),
788 longitude: Some(9.0),
789 height: Some(100.0),
790 reachable_on: Some("example.com".into()),
791 port: Some(4242),
792 frequency: None,
793 bandwidth: None,
794 spreading_factor: None,
795 coding_rate: None,
796 modulation: None,
797 channel: None,
798 ifac_netname: Some("mynetwork".into()),
799 ifac_netkey: Some("secretkey".into()),
800 config_entry: Some("test config".into()),
801 discovery_hash: compute_discovery_hash(&[0x01u8; 16], name),
802 }
803 }
804
805 #[test]
806 fn test_storage_roundtrip() {
807 use std::sync::atomic::{AtomicU64, Ordering};
808 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
809
810 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
811 let dir =
812 std::env::temp_dir().join(format!("rns-discovery-test-{}-{}", std::process::id(), id));
813 let _ = fs::remove_dir_all(&dir);
814 fs::create_dir_all(&dir).unwrap();
815
816 let storage = DiscoveredInterfaceStorage::new(dir.clone());
817
818 let iface = test_discovered_interface("TestNode");
819
820 storage.store(&iface).unwrap();
822
823 let loaded = storage.load(&iface.discovery_hash).unwrap().unwrap();
825
826 assert_eq!(loaded.interface_type, iface.interface_type);
827 assert_eq!(loaded.name, iface.name);
828 assert_eq!(loaded.stamp_value, iface.stamp_value);
829 assert_eq!(loaded.transport_id, iface.transport_id);
830 assert_eq!(loaded.hops, iface.hops);
831 assert_eq!(loaded.latitude, iface.latitude);
832 assert_eq!(loaded.reachable_on, iface.reachable_on);
833 assert_eq!(loaded.port, iface.port);
834
835 let list = storage.list().unwrap();
837 assert_eq!(list.len(), 1);
838
839 storage.remove(&iface.discovery_hash).unwrap();
841 let list = storage.list().unwrap();
842 assert!(list.is_empty());
843
844 let _ = fs::remove_dir_all(&dir);
845 }
846
847 #[test]
848 fn storage_load_sanitizes_cached_interface_names() {
849 let dir = std::env::temp_dir().join(format!(
850 "rns-discovery-sanitize-test-{}",
851 std::process::id()
852 ));
853 let _ = fs::remove_dir_all(&dir);
854 fs::create_dir_all(&dir).unwrap();
855 let storage = DiscoveredInterfaceStorage::new(dir.clone());
856 let iface = test_discovered_interface("\t**Cached Name!!!\n");
857
858 storage.store(&iface).unwrap();
859
860 let loaded = storage.load(&iface.discovery_hash).unwrap().unwrap();
861 let listed = storage.list().unwrap();
862
863 assert_eq!(loaded.name, "Cached Name");
864 assert_eq!(listed[0].name, "Cached Name");
865
866 let _ = fs::remove_dir_all(&dir);
867 }
868
869 #[test]
870 fn storage_rejects_cached_transport_id_with_invalid_length() {
871 let storage = DiscoveredInterfaceStorage::new(std::env::temp_dir());
872 let iface = test_discovered_interface("BadTransportId");
873 let mut data = storage.serialize_interface(&iface).unwrap();
874 let (mut value, _) = msgpack::unpack(&data).unwrap();
875 if let Value::Map(ref mut entries) = value {
876 for (key, val) in entries {
877 if key.as_str() == Some("transport_id") {
878 *val = Value::Bin(vec![0x01; 15]);
879 }
880 }
881 }
882 data = msgpack::pack(&value);
883
884 let err = storage.deserialize_interface(&data).unwrap_err();
885
886 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
887 assert!(err.to_string().contains("transport_id"));
888 }
889
890 #[test]
891 fn store_received_preserves_existing_first_seen_and_increments_heard_count() {
892 let dir = std::env::temp_dir().join(format!(
893 "rns-discovery-received-preserve-test-{}",
894 std::process::id()
895 ));
896 let _ = fs::remove_dir_all(&dir);
897 fs::create_dir_all(&dir).unwrap();
898 let storage = DiscoveredInterfaceStorage::new(dir.clone());
899
900 let mut existing = test_discovered_interface("ExistingDiscovery");
901 existing.discovered = 1000.0;
902 existing.last_heard = 1100.0;
903 existing.heard_count = 7;
904 storage.store(&existing).unwrap();
905
906 let mut received = existing.clone();
907 received.discovered = 2000.0;
908 received.last_heard = 3000.0;
909 received.heard_count = 0;
910 storage.store_received(&mut received).unwrap();
911
912 let loaded = storage.load(&received.discovery_hash).unwrap().unwrap();
913 assert_eq!(received.discovered, 1000.0);
914 assert_eq!(received.last_heard, 3000.0);
915 assert_eq!(received.heard_count, 8);
916 assert_eq!(loaded.discovered, 1000.0);
917 assert_eq!(loaded.last_heard, 3000.0);
918 assert_eq!(loaded.heard_count, 8);
919
920 let _ = fs::remove_dir_all(&dir);
921 }
922
923 #[test]
924 fn store_received_recreates_corrupt_cache_with_received_time_as_first_seen() {
925 let dir = std::env::temp_dir().join(format!(
926 "rns-discovery-corrupt-recreate-test-{}",
927 std::process::id()
928 ));
929 let _ = fs::remove_dir_all(&dir);
930 fs::create_dir_all(&dir).unwrap();
931 let storage = DiscoveredInterfaceStorage::new(dir.clone());
932
933 let mut received = test_discovered_interface("CorruptDiscovery");
934 received.discovered = 1234.0;
935 received.last_heard = 5678.0;
936 received.heard_count = 0;
937 let filepath = dir.join(hex_encode(&received.discovery_hash));
938 fs::write(&filepath, b"not msgpack").unwrap();
939
940 storage.store_received(&mut received).unwrap();
941
942 let loaded = storage.load(&received.discovery_hash).unwrap().unwrap();
943 assert_eq!(received.discovered, 5678.0);
944 assert_eq!(received.heard_count, 1);
945 assert_eq!(loaded.discovered, 5678.0);
946 assert_eq!(loaded.last_heard, 5678.0);
947 assert_eq!(loaded.heard_count, 1);
948 assert_eq!(loaded.name, "CorruptDiscovery");
949
950 let _ = fs::remove_dir_all(&dir);
951 }
952
953 #[test]
954 fn test_filter_and_sort() {
955 let now = time::now();
956
957 let ifaces = vec![
958 DiscoveredInterface {
959 interface_type: "BackboneInterface".into(),
960 transport: true,
961 name: "high-value-stale".into(),
962 discovered: now,
963 last_heard: now - THRESHOLD_STALE - 100.0, heard_count: 0,
965 status: DiscoveredStatus::Stale,
966 stamp: vec![],
967 stamp_value: 20,
968 transport_id: [0u8; 16],
969 network_id: [0u8; 16],
970 hops: 0,
971 latitude: None,
972 longitude: None,
973 height: None,
974 reachable_on: None,
975 port: None,
976 frequency: None,
977 bandwidth: None,
978 spreading_factor: None,
979 coding_rate: None,
980 modulation: None,
981 channel: None,
982 ifac_netname: None,
983 ifac_netkey: None,
984 config_entry: None,
985 discovery_hash: [0u8; 32],
986 },
987 DiscoveredInterface {
988 interface_type: "TCPServerInterface".into(),
989 transport: true,
990 name: "low-value-available".into(),
991 discovered: now,
992 last_heard: now - 10.0, heard_count: 0,
994 status: DiscoveredStatus::Available,
995 stamp: vec![],
996 stamp_value: 10,
997 transport_id: [0u8; 16],
998 network_id: [0u8; 16],
999 hops: 0,
1000 latitude: None,
1001 longitude: None,
1002 height: None,
1003 reachable_on: None,
1004 port: None,
1005 frequency: None,
1006 bandwidth: None,
1007 spreading_factor: None,
1008 coding_rate: None,
1009 modulation: None,
1010 channel: None,
1011 ifac_netname: None,
1012 ifac_netkey: None,
1013 config_entry: None,
1014 discovery_hash: [1u8; 32],
1015 },
1016 DiscoveredInterface {
1017 interface_type: "I2PInterface".into(),
1018 transport: false,
1019 name: "high-value-available".into(),
1020 discovered: now,
1021 last_heard: now - 10.0, heard_count: 0,
1023 status: DiscoveredStatus::Available,
1024 stamp: vec![],
1025 stamp_value: 20,
1026 transport_id: [0u8; 16],
1027 network_id: [0u8; 16],
1028 hops: 0,
1029 latitude: None,
1030 longitude: None,
1031 height: None,
1032 reachable_on: None,
1033 port: None,
1034 frequency: None,
1035 bandwidth: None,
1036 spreading_factor: None,
1037 coding_rate: None,
1038 modulation: None,
1039 channel: None,
1040 ifac_netname: None,
1041 ifac_netkey: None,
1042 config_entry: None,
1043 discovery_hash: [2u8; 32],
1044 },
1045 ];
1046
1047 let mut result = ifaces.clone();
1049 filter_and_sort_interfaces(&mut result, false, false);
1050 assert_eq!(result.len(), 3);
1051 assert_eq!(result[0].name, "high-value-available");
1053 assert_eq!(result[1].name, "low-value-available");
1054 assert_eq!(result[2].name, "high-value-stale");
1055
1056 let mut result = ifaces.clone();
1058 filter_and_sort_interfaces(&mut result, true, false);
1059 assert_eq!(result.len(), 2); let mut result = ifaces.clone();
1063 filter_and_sort_interfaces(&mut result, false, true);
1064 assert_eq!(result.len(), 2); }
1066
1067 #[test]
1068 fn test_discovery_name_hash_deterministic() {
1069 let h1 = discovery_name_hash();
1070 let h2 = discovery_name_hash();
1071 assert_eq!(h1, h2);
1072 assert_ne!(h1, [0u8; 10]); }
1074}