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