Skip to main content

rns_net/
management.rs

1//! Remote management destinations for the Reticulum transport node.
2//!
3//! Implements the server-side handlers for:
4//! - `/status` on `rnstransport.remote.management` destination
5//! - `/path` on `rnstransport.remote.management` destination
6//! - `/list` on `rnstransport.info.blackhole` destination
7//!
8//! Python reference: Transport.py:220-241, 2591-2643, 3243-3249
9
10use std::collections::HashMap;
11
12use rns_core::constants;
13use rns_core::destination::destination_hash;
14use rns_core::hash::truncated_hash;
15use rns_core::msgpack::{self, Value};
16use rns_core::transport::TransportEngine;
17
18use crate::interface::InterfaceEntry;
19use crate::time;
20
21/// Get the path hash for "/status".
22pub fn status_path_hash() -> [u8; 16] {
23    truncated_hash(b"/status")
24}
25
26/// Get the path hash for "/path".
27pub fn path_path_hash() -> [u8; 16] {
28    truncated_hash(b"/path")
29}
30
31/// Get the path hash for "/list".
32pub fn list_path_hash() -> [u8; 16] {
33    truncated_hash(b"/list")
34}
35
36/// Check if a path hash matches a known management path.
37pub fn is_management_path(path_hash: &[u8; 16]) -> bool {
38    *path_hash == status_path_hash()
39        || *path_hash == path_path_hash()
40        || *path_hash == list_path_hash()
41}
42
43/// Compute the remote management destination hash.
44///
45/// Destination: `rnstransport.remote.management` with transport identity.
46pub fn management_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
47    destination_hash("rnstransport", &["remote", "management"], Some(transport_identity_hash))
48}
49
50/// Compute the blackhole info destination hash.
51///
52/// Destination: `rnstransport.info.blackhole` with transport identity.
53pub fn blackhole_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
54    destination_hash("rnstransport", &["info", "blackhole"], Some(transport_identity_hash))
55}
56
57/// Compute the probe responder destination hash.
58///
59/// Destination: `rnstransport.probe` with transport identity.
60pub fn probe_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
61    destination_hash("rnstransport", &["probe"], Some(transport_identity_hash))
62}
63
64/// Build an announce packet for the probe responder destination.
65///
66/// Returns raw packet bytes ready for `engine.handle_outbound()`.
67pub fn build_probe_announce(
68    identity: &rns_crypto::identity::Identity,
69    rng: &mut dyn rns_crypto::Rng,
70) -> Option<Vec<u8>> {
71    let identity_hash = *identity.hash();
72    let dest_hash = probe_dest_hash(&identity_hash);
73    let name_hash = rns_core::destination::name_hash("rnstransport", &["probe"]);
74    let mut random_hash = [0u8; 10];
75    rng.fill_bytes(&mut random_hash);
76
77    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
78        identity,
79        &dest_hash,
80        &name_hash,
81        &random_hash,
82        None,
83        None,
84    )
85    .ok()?;
86
87    let flags = rns_core::packet::PacketFlags {
88        header_type: constants::HEADER_1,
89        context_flag: constants::FLAG_UNSET,
90        transport_type: constants::TRANSPORT_BROADCAST,
91        destination_type: constants::DESTINATION_SINGLE,
92        packet_type: constants::PACKET_TYPE_ANNOUNCE,
93    };
94
95    let packet = rns_core::packet::RawPacket::pack(
96        flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data,
97    )
98    .ok()?;
99
100    Some(packet.raw)
101}
102
103/// Management configuration.
104#[derive(Debug, Clone)]
105pub struct ManagementConfig {
106    /// Enable remote management destination.
107    pub enable_remote_management: bool,
108    /// Identity hashes allowed to query management.
109    pub remote_management_allowed: Vec<[u8; 16]>,
110    /// Enable blackhole list publication.
111    pub publish_blackhole: bool,
112}
113
114impl Default for ManagementConfig {
115    fn default() -> Self {
116        ManagementConfig {
117            enable_remote_management: false,
118            remote_management_allowed: Vec::new(),
119            publish_blackhole: false,
120        }
121    }
122}
123
124/// Handle a `/status` request.
125///
126/// Request data: msgpack([include_lstats]) where include_lstats is bool.
127/// Response: msgpack([interface_stats_dict, link_count?]) matching Python format.
128pub fn handle_status_request(
129    data: &[u8],
130    engine: &TransportEngine,
131    interfaces: &HashMap<rns_core::transport::types::InterfaceId, InterfaceEntry>,
132    started: f64,
133    probe_responder_hash: Option<[u8; 16]>,
134) -> Option<Vec<u8>> {
135    // Decode request data
136    let include_lstats = match msgpack::unpack_exact(data) {
137        Ok(Value::Array(arr)) if !arr.is_empty() => {
138            arr[0].as_bool().unwrap_or(false)
139        }
140        _ => false,
141    };
142
143    // Build interface stats
144    let mut iface_list = Vec::new();
145    let mut total_rxb: u64 = 0;
146    let mut total_txb: u64 = 0;
147
148    for (id, entry) in interfaces {
149        total_rxb += entry.stats.rxb;
150        total_txb += entry.stats.txb;
151
152        let mut ifstats: Vec<(&str, Value)> = Vec::new();
153        ifstats.push(("name", Value::Str(entry.info.name.clone())));
154        ifstats.push(("short_name", Value::Str(entry.info.name.clone())));
155        ifstats.push(("status", Value::Bool(entry.online)));
156        ifstats.push(("mode", Value::UInt(entry.info.mode as u64)));
157        ifstats.push(("rxb", Value::UInt(entry.stats.rxb)));
158        ifstats.push(("txb", Value::UInt(entry.stats.txb)));
159        if let Some(br) = entry.info.bitrate {
160            ifstats.push(("bitrate", Value::UInt(br)));
161        } else {
162            ifstats.push(("bitrate", Value::Nil));
163        }
164        ifstats.push(("incoming_announce_freq", Value::Float(entry.stats.incoming_announce_freq())));
165        ifstats.push(("outgoing_announce_freq", Value::Float(entry.stats.outgoing_announce_freq())));
166        ifstats.push(("held_announces", Value::UInt(engine.held_announce_count(id) as u64)));
167
168        // IFAC info
169        ifstats.push(("ifac_signature", Value::Nil));
170        ifstats.push(("ifac_size", if entry.info.bitrate.is_some() {
171            Value::UInt(0)
172        } else {
173            Value::Nil
174        }));
175        ifstats.push(("ifac_netname", Value::Nil));
176
177        // Unused by Rust but expected by Python clients
178        ifstats.push(("clients", Value::Nil));
179        ifstats.push(("announce_queue", Value::Nil));
180        ifstats.push(("rxs", Value::UInt(0)));
181        ifstats.push(("txs", Value::UInt(0)));
182
183        // Build as map
184        let map = ifstats.into_iter()
185            .map(|(k, v)| (Value::Str(k.into()), v))
186            .collect();
187        iface_list.push(Value::Map(map));
188    }
189
190    // Build top-level stats dict
191    let mut stats: Vec<(&str, Value)> = Vec::new();
192    stats.push(("interfaces", Value::Array(iface_list)));
193    stats.push(("rxb", Value::UInt(total_rxb)));
194    stats.push(("txb", Value::UInt(total_txb)));
195    stats.push(("rxs", Value::UInt(0)));
196    stats.push(("txs", Value::UInt(0)));
197
198    if let Some(identity_hash) = engine.config().identity_hash {
199        stats.push(("transport_id", Value::Bin(identity_hash.to_vec())));
200        stats.push(("transport_uptime", Value::Float(time::now() - started)));
201    }
202    stats.push(("probe_responder", match probe_responder_hash {
203        Some(hash) => Value::Bin(hash.to_vec()),
204        None => Value::Nil,
205    }));
206    stats.push(("rss", Value::Nil));
207
208    let stats_map = stats.into_iter()
209        .map(|(k, v)| (Value::Str(k.into()), v))
210        .collect();
211
212    // Build response: [stats_dict] or [stats_dict, link_count]
213    let mut response = vec![Value::Map(stats_map)];
214    if include_lstats {
215        let link_count = engine.link_table_count();
216        response.push(Value::UInt(link_count as u64));
217    }
218
219    Some(msgpack::pack(&Value::Array(response)))
220}
221
222/// Handle a `/path` request.
223///
224/// Request data: msgpack([command, destination_hash?, max_hops?])
225/// - command = "table" → returns path table entries
226/// - command = "rates" → returns rate table entries
227pub fn handle_path_request(
228    data: &[u8],
229    engine: &TransportEngine,
230) -> Option<Vec<u8>> {
231    let arr = match msgpack::unpack_exact(data) {
232        Ok(Value::Array(arr)) if !arr.is_empty() => arr,
233        _ => return None,
234    };
235
236    let command = match &arr[0] {
237        Value::Str(s) => s.as_str(),
238        _ => return None,
239    };
240
241    let dest_filter: Option<[u8; 16]> = if arr.len() > 1 {
242        match &arr[1] {
243            Value::Bin(b) if b.len() == 16 => {
244                let mut h = [0u8; 16];
245                h.copy_from_slice(b);
246                Some(h)
247            }
248            _ => None,
249        }
250    } else {
251        None
252    };
253
254    let max_hops: Option<u8> = if arr.len() > 2 {
255        arr[2].as_uint().map(|v| v as u8)
256    } else {
257        None
258    };
259
260    match command {
261        "table" => {
262            let paths = engine.get_path_table(max_hops);
263            let mut entries = Vec::new();
264            for p in &paths {
265                if let Some(ref filter) = dest_filter {
266                    if p.0 != *filter {
267                        continue;
268                    }
269                }
270                // p = (dest_hash, timestamp, next_hop, hops, expires, interface)
271                let entry = vec![
272                    (Value::Str("hash".into()), Value::Bin(p.0.to_vec())),
273                    (Value::Str("timestamp".into()), Value::Float(p.1)),
274                    (Value::Str("via".into()), Value::Bin(p.2.to_vec())),
275                    (Value::Str("hops".into()), Value::UInt(p.3 as u64)),
276                    (Value::Str("expires".into()), Value::Float(p.4)),
277                    (Value::Str("interface".into()), Value::Str(p.5.clone())),
278                ];
279                entries.push(Value::Map(entry));
280            }
281            Some(msgpack::pack(&Value::Array(entries)))
282        }
283        "rates" => {
284            let rates = engine.get_rate_table();
285            let mut entries = Vec::new();
286            for r in &rates {
287                if let Some(ref filter) = dest_filter {
288                    if r.0 != *filter {
289                        continue;
290                    }
291                }
292                // r = (dest_hash, last, rate_violations, blocked_until, timestamps)
293                let timestamps: Vec<Value> = r.4.iter().map(|t| Value::Float(*t)).collect();
294                let entry = vec![
295                    (Value::Str("hash".into()), Value::Bin(r.0.to_vec())),
296                    (Value::Str("last".into()), Value::Float(r.1)),
297                    (Value::Str("rate_violations".into()), Value::UInt(r.2 as u64)),
298                    (Value::Str("blocked_until".into()), Value::Float(r.3)),
299                    (Value::Str("timestamps".into()), Value::Array(timestamps)),
300                ];
301                entries.push(Value::Map(entry));
302            }
303            Some(msgpack::pack(&Value::Array(entries)))
304        }
305        _ => None,
306    }
307}
308
309/// Handle a `/list` (blackhole list) request.
310///
311/// Returns the blackholed_identities dict as msgpack.
312pub fn handle_blackhole_list_request(
313    engine: &TransportEngine,
314) -> Option<Vec<u8>> {
315    let blackholed = engine.get_blackholed();
316    let mut map_entries = Vec::new();
317    for (hash, created, expires, reason) in &blackholed {
318        let mut entry = vec![
319            (Value::Str("created".into()), Value::Float(*created)),
320            (Value::Str("expires".into()), Value::Float(*expires)),
321        ];
322        if let Some(r) = reason {
323            entry.push((Value::Str("reason".into()), Value::Str(r.clone())));
324        }
325        map_entries.push((Value::Bin(hash.to_vec()), Value::Map(entry)));
326    }
327    Some(msgpack::pack(&Value::Map(map_entries)))
328}
329
330/// Build an announce packet for the management destination.
331///
332/// Returns raw packet bytes ready for `engine.handle_outbound()`.
333pub fn build_management_announce(
334    identity: &rns_crypto::identity::Identity,
335    rng: &mut dyn rns_crypto::Rng,
336) -> Option<Vec<u8>> {
337    let identity_hash = *identity.hash();
338    let dest_hash = management_dest_hash(&identity_hash);
339    let name_hash = rns_core::destination::name_hash("rnstransport", &["remote", "management"]);
340    let mut random_hash = [0u8; 10];
341    rng.fill_bytes(&mut random_hash);
342
343    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
344        identity,
345        &dest_hash,
346        &name_hash,
347        &random_hash,
348        None, // no ratchet
349        None, // no app_data
350    )
351    .ok()?;
352
353    let flags = rns_core::packet::PacketFlags {
354        header_type: constants::HEADER_1,
355        context_flag: constants::FLAG_UNSET,
356        transport_type: constants::TRANSPORT_BROADCAST,
357        destination_type: constants::DESTINATION_SINGLE,
358        packet_type: constants::PACKET_TYPE_ANNOUNCE,
359    };
360
361    let packet = rns_core::packet::RawPacket::pack(
362        flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data,
363    )
364    .ok()?;
365
366    Some(packet.raw)
367}
368
369/// Build an announce packet for the blackhole info destination.
370///
371/// Returns raw packet bytes ready for `engine.handle_outbound()`.
372pub fn build_blackhole_announce(
373    identity: &rns_crypto::identity::Identity,
374    rng: &mut dyn rns_crypto::Rng,
375) -> Option<Vec<u8>> {
376    let identity_hash = *identity.hash();
377    let dest_hash = blackhole_dest_hash(&identity_hash);
378    let name_hash = rns_core::destination::name_hash("rnstransport", &["info", "blackhole"]);
379    let mut random_hash = [0u8; 10];
380    rng.fill_bytes(&mut random_hash);
381
382    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
383        identity,
384        &dest_hash,
385        &name_hash,
386        &random_hash,
387        None,
388        None,
389    )
390    .ok()?;
391
392    let flags = rns_core::packet::PacketFlags {
393        header_type: constants::HEADER_1,
394        context_flag: constants::FLAG_UNSET,
395        transport_type: constants::TRANSPORT_BROADCAST,
396        destination_type: constants::DESTINATION_SINGLE,
397        packet_type: constants::PACKET_TYPE_ANNOUNCE,
398    };
399
400    let packet = rns_core::packet::RawPacket::pack(
401        flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data,
402    )
403    .ok()?;
404
405    Some(packet.raw)
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::interface::{InterfaceStats, Writer};
412    use crate::ifac::IfacState;
413    use rns_core::transport::types::{InterfaceId, InterfaceInfo, TransportConfig};
414    use std::io;
415
416    struct NullWriter;
417    impl Writer for NullWriter {
418        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
419            Ok(())
420        }
421    }
422
423    fn make_engine() -> TransportEngine {
424        TransportEngine::new(TransportConfig {
425            transport_enabled: true,
426            identity_hash: Some([0xAA; 16]),
427        })
428    }
429
430    fn make_interfaces() -> HashMap<InterfaceId, InterfaceEntry> {
431        let mut map = HashMap::new();
432        let id = InterfaceId(1);
433        let info = InterfaceInfo {
434            id,
435            name: "TestInterface".into(),
436            mode: constants::MODE_FULL,
437            out_capable: true,
438            in_capable: true,
439            bitrate: Some(115200),
440            announce_rate_target: None,
441            announce_rate_grace: 0,
442            announce_rate_penalty: 0.0,
443            announce_cap: constants::ANNOUNCE_CAP,
444            is_local_client: false,
445            wants_tunnel: false,
446            tunnel_id: None,
447            mtu: rns_core::constants::MTU as u32,
448            ia_freq: 0.0,
449            started: 0.0,
450            ingress_control: false,
451        };
452        map.insert(id, InterfaceEntry {
453            id,
454            info,
455            writer: Box::new(NullWriter),
456            online: true,
457            dynamic: false,
458            ifac: None,
459            stats: InterfaceStats {
460                rxb: 1234,
461                txb: 5678,
462                rx_packets: 10,
463                tx_packets: 20,
464                started: 1000.0,
465                ia_timestamps: vec![],
466                oa_timestamps: vec![],
467            },
468            interface_type: "TestInterface".to_string(),
469        });
470        map
471    }
472
473    #[test]
474    fn test_management_dest_hash() {
475        let id_hash = [0x42; 16];
476        let dh = management_dest_hash(&id_hash);
477        // Should be deterministic
478        assert_eq!(dh, management_dest_hash(&id_hash));
479        // Different identity → different hash
480        assert_ne!(dh, management_dest_hash(&[0x43; 16]));
481    }
482
483    #[test]
484    fn test_blackhole_dest_hash() {
485        let id_hash = [0x42; 16];
486        let dh = blackhole_dest_hash(&id_hash);
487        assert_eq!(dh, blackhole_dest_hash(&id_hash));
488        // Different from management dest
489        assert_ne!(dh, management_dest_hash(&id_hash));
490    }
491
492    #[test]
493    fn test_path_hashes_distinct() {
494        let s = status_path_hash();
495        let p = path_path_hash();
496        let l = list_path_hash();
497        assert_ne!(s, p);
498        assert_ne!(s, l);
499        assert_ne!(p, l);
500        // Non-zero
501        assert_ne!(s, [0u8; 16]);
502    }
503
504    #[test]
505    fn test_management_config_default() {
506        let config = ManagementConfig::default();
507        assert!(!config.enable_remote_management);
508        assert!(config.remote_management_allowed.is_empty());
509        assert!(!config.publish_blackhole);
510    }
511
512    #[test]
513    fn test_is_management_path() {
514        assert!(is_management_path(&status_path_hash()));
515        assert!(is_management_path(&path_path_hash()));
516        assert!(is_management_path(&list_path_hash()));
517        assert!(!is_management_path(&[0u8; 16]));
518    }
519
520    #[test]
521    fn test_status_request_basic() {
522        let engine = make_engine();
523        let interfaces = make_interfaces();
524        let started = time::now() - 100.0; // 100 seconds ago
525
526        // Request with include_lstats = false
527        let request = msgpack::pack(&Value::Array(vec![Value::Bool(false)]));
528        let response = handle_status_request(&request, &engine, &interfaces, started, None).unwrap();
529
530        // Decode response
531        let val = msgpack::unpack_exact(&response).unwrap();
532        match val {
533            Value::Array(arr) => {
534                assert_eq!(arr.len(), 1); // no link stats
535                match &arr[0] {
536                    Value::Map(map) => {
537                        // Check that transport_id is present
538                        let transport_id = map.iter()
539                            .find(|(k, _)| *k == Value::Str("transport_id".into()))
540                            .map(|(_, v)| v);
541                        assert!(transport_id.is_some());
542
543                        // Check rxb/txb totals
544                        let rxb = map.iter()
545                            .find(|(k, _)| *k == Value::Str("rxb".into()))
546                            .map(|(_, v)| v.as_uint().unwrap());
547                        assert_eq!(rxb, Some(1234));
548
549                        let txb = map.iter()
550                            .find(|(k, _)| *k == Value::Str("txb".into()))
551                            .map(|(_, v)| v.as_uint().unwrap());
552                        assert_eq!(txb, Some(5678));
553
554                        // Check interfaces array
555                        let ifaces = map.iter()
556                            .find(|(k, _)| *k == Value::Str("interfaces".into()))
557                            .map(|(_, v)| v);
558                        match ifaces {
559                            Some(Value::Array(iface_arr)) => {
560                                assert_eq!(iface_arr.len(), 1);
561                            }
562                            _ => panic!("Expected interfaces array"),
563                        }
564
565                        // Check uptime
566                        let uptime = map.iter()
567                            .find(|(k, _)| *k == Value::Str("transport_uptime".into()))
568                            .and_then(|(_, v)| v.as_float());
569                        assert!(uptime.unwrap() >= 100.0);
570                    }
571                    _ => panic!("Expected map in response"),
572                }
573            }
574            _ => panic!("Expected array response"),
575        }
576    }
577
578    #[test]
579    fn test_status_request_with_lstats() {
580        let engine = make_engine();
581        let interfaces = make_interfaces();
582        let started = time::now();
583
584        let request = msgpack::pack(&Value::Array(vec![Value::Bool(true)]));
585        let response = handle_status_request(&request, &engine, &interfaces, started, None).unwrap();
586
587        let val = msgpack::unpack_exact(&response).unwrap();
588        match val {
589            Value::Array(arr) => {
590                assert_eq!(arr.len(), 2); // stats + link count
591                assert_eq!(arr[1].as_uint(), Some(0)); // no links
592            }
593            _ => panic!("Expected array response"),
594        }
595    }
596
597    #[test]
598    fn test_status_request_empty_data() {
599        let engine = make_engine();
600        let interfaces = make_interfaces();
601        let started = time::now();
602
603        // Empty data should still work (include_lstats defaults to false)
604        let response = handle_status_request(&[], &engine, &interfaces, started, None).unwrap();
605        let val = msgpack::unpack_exact(&response).unwrap();
606        match val {
607            Value::Array(arr) => assert_eq!(arr.len(), 1),
608            _ => panic!("Expected array response"),
609        }
610    }
611
612    #[test]
613    fn test_path_request_table() {
614        let engine = make_engine();
615
616        // Request table with no entries
617        let request = msgpack::pack(&Value::Array(vec![Value::Str("table".into())]));
618        let response = handle_path_request(&request, &engine).unwrap();
619        let val = msgpack::unpack_exact(&response).unwrap();
620        match val {
621            Value::Array(arr) => assert_eq!(arr.len(), 0),
622            _ => panic!("Expected array"),
623        }
624    }
625
626    #[test]
627    fn test_path_request_rates() {
628        let engine = make_engine();
629
630        let request = msgpack::pack(&Value::Array(vec![Value::Str("rates".into())]));
631        let response = handle_path_request(&request, &engine).unwrap();
632        let val = msgpack::unpack_exact(&response).unwrap();
633        match val {
634            Value::Array(arr) => assert_eq!(arr.len(), 0),
635            _ => panic!("Expected array"),
636        }
637    }
638
639    #[test]
640    fn test_path_request_unknown_command() {
641        let engine = make_engine();
642
643        let request = msgpack::pack(&Value::Array(vec![Value::Str("unknown".into())]));
644        let response = handle_path_request(&request, &engine);
645        assert!(response.is_none());
646    }
647
648    #[test]
649    fn test_path_request_invalid_data() {
650        let engine = make_engine();
651        let response = handle_path_request(&[], &engine);
652        assert!(response.is_none());
653    }
654
655    #[test]
656    fn test_blackhole_list_empty() {
657        let engine = make_engine();
658        let response = handle_blackhole_list_request(&engine).unwrap();
659        let val = msgpack::unpack_exact(&response).unwrap();
660        match val {
661            Value::Map(entries) => assert_eq!(entries.len(), 0),
662            _ => panic!("Expected map"),
663        }
664    }
665
666    // Phase 8c: Announce building tests
667
668    #[test]
669    fn test_build_management_announce() {
670        use rns_crypto::identity::Identity;
671        use rns_crypto::OsRng;
672
673        let identity = Identity::new(&mut OsRng);
674        let raw = build_management_announce(&identity, &mut OsRng);
675        assert!(raw.is_some(), "Should build management announce");
676
677        let raw = raw.unwrap();
678        // Parse it as a valid packet
679        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
680        assert_eq!(pkt.flags.packet_type, constants::PACKET_TYPE_ANNOUNCE);
681        assert_eq!(pkt.flags.destination_type, constants::DESTINATION_SINGLE);
682        assert_eq!(pkt.destination_hash, management_dest_hash(identity.hash()));
683    }
684
685    #[test]
686    fn test_build_blackhole_announce() {
687        use rns_crypto::identity::Identity;
688        use rns_crypto::OsRng;
689
690        let identity = Identity::new(&mut OsRng);
691        let raw = build_blackhole_announce(&identity, &mut OsRng);
692        assert!(raw.is_some(), "Should build blackhole announce");
693
694        let raw = raw.unwrap();
695        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
696        assert_eq!(pkt.flags.packet_type, constants::PACKET_TYPE_ANNOUNCE);
697        assert_eq!(pkt.destination_hash, blackhole_dest_hash(identity.hash()));
698    }
699
700    #[test]
701    fn test_management_announce_validates() {
702        use rns_crypto::identity::Identity;
703        use rns_crypto::OsRng;
704
705        let identity = Identity::new(&mut OsRng);
706        let raw = build_management_announce(&identity, &mut OsRng).unwrap();
707
708        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
709
710        // Validate the announce data
711        let validated = rns_core::announce::AnnounceData::unpack(&pkt.data, false);
712        assert!(validated.is_ok(), "Announce data should unpack");
713
714        let ann = validated.unwrap();
715        let result = ann.validate(&pkt.destination_hash);
716        assert!(result.is_ok(), "Announce should validate: {:?}", result.err());
717    }
718
719    #[test]
720    fn test_blackhole_announce_validates() {
721        use rns_crypto::identity::Identity;
722        use rns_crypto::OsRng;
723
724        let identity = Identity::new(&mut OsRng);
725        let raw = build_blackhole_announce(&identity, &mut OsRng).unwrap();
726
727        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
728        let ann = rns_core::announce::AnnounceData::unpack(&pkt.data, false).unwrap();
729        let result = ann.validate(&pkt.destination_hash);
730        assert!(result.is_ok(), "Blackhole announce should validate: {:?}", result.err());
731    }
732
733    #[test]
734    fn test_probe_dest_hash() {
735        let id_hash = [0x42; 16];
736        let dh = probe_dest_hash(&id_hash);
737        // Should be deterministic
738        assert_eq!(dh, probe_dest_hash(&id_hash));
739        // Different identity → different hash
740        assert_ne!(dh, probe_dest_hash(&[0x43; 16]));
741        // Different from management and blackhole dests
742        assert_ne!(dh, management_dest_hash(&id_hash));
743        assert_ne!(dh, blackhole_dest_hash(&id_hash));
744    }
745
746    #[test]
747    fn test_build_probe_announce() {
748        use rns_crypto::identity::Identity;
749        use rns_crypto::OsRng;
750
751        let identity = Identity::new(&mut OsRng);
752        let raw = build_probe_announce(&identity, &mut OsRng);
753        assert!(raw.is_some(), "Should build probe announce");
754
755        let raw = raw.unwrap();
756        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
757        assert_eq!(pkt.flags.packet_type, constants::PACKET_TYPE_ANNOUNCE);
758        assert_eq!(pkt.flags.destination_type, constants::DESTINATION_SINGLE);
759        assert_eq!(pkt.destination_hash, probe_dest_hash(identity.hash()));
760    }
761
762    #[test]
763    fn test_probe_announce_validates() {
764        use rns_crypto::identity::Identity;
765        use rns_crypto::OsRng;
766
767        let identity = Identity::new(&mut OsRng);
768        let raw = build_probe_announce(&identity, &mut OsRng).unwrap();
769        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
770        let ann = rns_core::announce::AnnounceData::unpack(&pkt.data, false).unwrap();
771        let result = ann.validate(&pkt.destination_hash);
772        assert!(result.is_ok(), "Probe announce should validate: {:?}", result.err());
773    }
774
775    #[test]
776    fn test_management_announce_different_from_blackhole() {
777        use rns_crypto::identity::Identity;
778        use rns_crypto::OsRng;
779
780        let identity = Identity::new(&mut OsRng);
781        let mgmt_raw = build_management_announce(&identity, &mut OsRng).unwrap();
782        let bh_raw = build_blackhole_announce(&identity, &mut OsRng).unwrap();
783
784        let mgmt_pkt = rns_core::packet::RawPacket::unpack(&mgmt_raw).unwrap();
785        let bh_pkt = rns_core::packet::RawPacket::unpack(&bh_raw).unwrap();
786
787        assert_ne!(mgmt_pkt.destination_hash, bh_pkt.destination_hash,
788            "Management and blackhole should have different dest hashes");
789    }
790}