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(engine.held_announce_count(id) as u64)));
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            mtu: rns_core::constants::MTU as u32,
398            ia_freq: 0.0,
399            started: 0.0,
400            ingress_control: false,
401        };
402        map.insert(id, InterfaceEntry {
403            id,
404            info,
405            writer: Box::new(NullWriter),
406            online: true,
407            dynamic: false,
408            ifac: None,
409            stats: InterfaceStats {
410                rxb: 1234,
411                txb: 5678,
412                rx_packets: 10,
413                tx_packets: 20,
414                started: 1000.0,
415                ia_timestamps: vec![],
416                oa_timestamps: vec![],
417            },
418            interface_type: "TestInterface".to_string(),
419        });
420        map
421    }
422
423    #[test]
424    fn test_management_dest_hash() {
425        let id_hash = [0x42; 16];
426        let dh = management_dest_hash(&id_hash);
427        // Should be deterministic
428        assert_eq!(dh, management_dest_hash(&id_hash));
429        // Different identity → different hash
430        assert_ne!(dh, management_dest_hash(&[0x43; 16]));
431    }
432
433    #[test]
434    fn test_blackhole_dest_hash() {
435        let id_hash = [0x42; 16];
436        let dh = blackhole_dest_hash(&id_hash);
437        assert_eq!(dh, blackhole_dest_hash(&id_hash));
438        // Different from management dest
439        assert_ne!(dh, management_dest_hash(&id_hash));
440    }
441
442    #[test]
443    fn test_path_hashes_distinct() {
444        let s = status_path_hash();
445        let p = path_path_hash();
446        let l = list_path_hash();
447        assert_ne!(s, p);
448        assert_ne!(s, l);
449        assert_ne!(p, l);
450        // Non-zero
451        assert_ne!(s, [0u8; 16]);
452    }
453
454    #[test]
455    fn test_management_config_default() {
456        let config = ManagementConfig::default();
457        assert!(!config.enable_remote_management);
458        assert!(config.remote_management_allowed.is_empty());
459        assert!(!config.publish_blackhole);
460    }
461
462    #[test]
463    fn test_is_management_path() {
464        assert!(is_management_path(&status_path_hash()));
465        assert!(is_management_path(&path_path_hash()));
466        assert!(is_management_path(&list_path_hash()));
467        assert!(!is_management_path(&[0u8; 16]));
468    }
469
470    #[test]
471    fn test_status_request_basic() {
472        let engine = make_engine();
473        let interfaces = make_interfaces();
474        let started = time::now() - 100.0; // 100 seconds ago
475
476        // Request with include_lstats = false
477        let request = msgpack::pack(&Value::Array(vec![Value::Bool(false)]));
478        let response = handle_status_request(&request, &engine, &interfaces, started).unwrap();
479
480        // Decode response
481        let val = msgpack::unpack_exact(&response).unwrap();
482        match val {
483            Value::Array(arr) => {
484                assert_eq!(arr.len(), 1); // no link stats
485                match &arr[0] {
486                    Value::Map(map) => {
487                        // Check that transport_id is present
488                        let transport_id = map.iter()
489                            .find(|(k, _)| *k == Value::Str("transport_id".into()))
490                            .map(|(_, v)| v);
491                        assert!(transport_id.is_some());
492
493                        // Check rxb/txb totals
494                        let rxb = map.iter()
495                            .find(|(k, _)| *k == Value::Str("rxb".into()))
496                            .map(|(_, v)| v.as_uint().unwrap());
497                        assert_eq!(rxb, Some(1234));
498
499                        let txb = map.iter()
500                            .find(|(k, _)| *k == Value::Str("txb".into()))
501                            .map(|(_, v)| v.as_uint().unwrap());
502                        assert_eq!(txb, Some(5678));
503
504                        // Check interfaces array
505                        let ifaces = map.iter()
506                            .find(|(k, _)| *k == Value::Str("interfaces".into()))
507                            .map(|(_, v)| v);
508                        match ifaces {
509                            Some(Value::Array(iface_arr)) => {
510                                assert_eq!(iface_arr.len(), 1);
511                            }
512                            _ => panic!("Expected interfaces array"),
513                        }
514
515                        // Check uptime
516                        let uptime = map.iter()
517                            .find(|(k, _)| *k == Value::Str("transport_uptime".into()))
518                            .and_then(|(_, v)| v.as_float());
519                        assert!(uptime.unwrap() >= 100.0);
520                    }
521                    _ => panic!("Expected map in response"),
522                }
523            }
524            _ => panic!("Expected array response"),
525        }
526    }
527
528    #[test]
529    fn test_status_request_with_lstats() {
530        let engine = make_engine();
531        let interfaces = make_interfaces();
532        let started = time::now();
533
534        let request = msgpack::pack(&Value::Array(vec![Value::Bool(true)]));
535        let response = handle_status_request(&request, &engine, &interfaces, started).unwrap();
536
537        let val = msgpack::unpack_exact(&response).unwrap();
538        match val {
539            Value::Array(arr) => {
540                assert_eq!(arr.len(), 2); // stats + link count
541                assert_eq!(arr[1].as_uint(), Some(0)); // no links
542            }
543            _ => panic!("Expected array response"),
544        }
545    }
546
547    #[test]
548    fn test_status_request_empty_data() {
549        let engine = make_engine();
550        let interfaces = make_interfaces();
551        let started = time::now();
552
553        // Empty data should still work (include_lstats defaults to false)
554        let response = handle_status_request(&[], &engine, &interfaces, started).unwrap();
555        let val = msgpack::unpack_exact(&response).unwrap();
556        match val {
557            Value::Array(arr) => assert_eq!(arr.len(), 1),
558            _ => panic!("Expected array response"),
559        }
560    }
561
562    #[test]
563    fn test_path_request_table() {
564        let engine = make_engine();
565
566        // Request table with no entries
567        let request = msgpack::pack(&Value::Array(vec![Value::Str("table".into())]));
568        let response = handle_path_request(&request, &engine).unwrap();
569        let val = msgpack::unpack_exact(&response).unwrap();
570        match val {
571            Value::Array(arr) => assert_eq!(arr.len(), 0),
572            _ => panic!("Expected array"),
573        }
574    }
575
576    #[test]
577    fn test_path_request_rates() {
578        let engine = make_engine();
579
580        let request = msgpack::pack(&Value::Array(vec![Value::Str("rates".into())]));
581        let response = handle_path_request(&request, &engine).unwrap();
582        let val = msgpack::unpack_exact(&response).unwrap();
583        match val {
584            Value::Array(arr) => assert_eq!(arr.len(), 0),
585            _ => panic!("Expected array"),
586        }
587    }
588
589    #[test]
590    fn test_path_request_unknown_command() {
591        let engine = make_engine();
592
593        let request = msgpack::pack(&Value::Array(vec![Value::Str("unknown".into())]));
594        let response = handle_path_request(&request, &engine);
595        assert!(response.is_none());
596    }
597
598    #[test]
599    fn test_path_request_invalid_data() {
600        let engine = make_engine();
601        let response = handle_path_request(&[], &engine);
602        assert!(response.is_none());
603    }
604
605    #[test]
606    fn test_blackhole_list_empty() {
607        let engine = make_engine();
608        let response = handle_blackhole_list_request(&engine).unwrap();
609        let val = msgpack::unpack_exact(&response).unwrap();
610        match val {
611            Value::Map(entries) => assert_eq!(entries.len(), 0),
612            _ => panic!("Expected map"),
613        }
614    }
615
616    // Phase 8c: Announce building tests
617
618    #[test]
619    fn test_build_management_announce() {
620        use rns_crypto::identity::Identity;
621        use rns_crypto::OsRng;
622
623        let identity = Identity::new(&mut OsRng);
624        let raw = build_management_announce(&identity, &mut OsRng);
625        assert!(raw.is_some(), "Should build management announce");
626
627        let raw = raw.unwrap();
628        // Parse it as a valid packet
629        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
630        assert_eq!(pkt.flags.packet_type, constants::PACKET_TYPE_ANNOUNCE);
631        assert_eq!(pkt.flags.destination_type, constants::DESTINATION_SINGLE);
632        assert_eq!(pkt.destination_hash, management_dest_hash(identity.hash()));
633    }
634
635    #[test]
636    fn test_build_blackhole_announce() {
637        use rns_crypto::identity::Identity;
638        use rns_crypto::OsRng;
639
640        let identity = Identity::new(&mut OsRng);
641        let raw = build_blackhole_announce(&identity, &mut OsRng);
642        assert!(raw.is_some(), "Should build blackhole announce");
643
644        let raw = raw.unwrap();
645        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
646        assert_eq!(pkt.flags.packet_type, constants::PACKET_TYPE_ANNOUNCE);
647        assert_eq!(pkt.destination_hash, blackhole_dest_hash(identity.hash()));
648    }
649
650    #[test]
651    fn test_management_announce_validates() {
652        use rns_crypto::identity::Identity;
653        use rns_crypto::OsRng;
654
655        let identity = Identity::new(&mut OsRng);
656        let raw = build_management_announce(&identity, &mut OsRng).unwrap();
657
658        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
659
660        // Validate the announce data
661        let validated = rns_core::announce::AnnounceData::unpack(&pkt.data, false);
662        assert!(validated.is_ok(), "Announce data should unpack");
663
664        let ann = validated.unwrap();
665        let result = ann.validate(&pkt.destination_hash);
666        assert!(result.is_ok(), "Announce should validate: {:?}", result.err());
667    }
668
669    #[test]
670    fn test_blackhole_announce_validates() {
671        use rns_crypto::identity::Identity;
672        use rns_crypto::OsRng;
673
674        let identity = Identity::new(&mut OsRng);
675        let raw = build_blackhole_announce(&identity, &mut OsRng).unwrap();
676
677        let pkt = rns_core::packet::RawPacket::unpack(&raw).unwrap();
678        let ann = rns_core::announce::AnnounceData::unpack(&pkt.data, false).unwrap();
679        let result = ann.validate(&pkt.destination_hash);
680        assert!(result.is_ok(), "Blackhole announce should validate: {:?}", result.err());
681    }
682
683    #[test]
684    fn test_management_announce_different_from_blackhole() {
685        use rns_crypto::identity::Identity;
686        use rns_crypto::OsRng;
687
688        let identity = Identity::new(&mut OsRng);
689        let mgmt_raw = build_management_announce(&identity, &mut OsRng).unwrap();
690        let bh_raw = build_blackhole_announce(&identity, &mut OsRng).unwrap();
691
692        let mgmt_pkt = rns_core::packet::RawPacket::unpack(&mgmt_raw).unwrap();
693        let bh_pkt = rns_core::packet::RawPacket::unpack(&bh_raw).unwrap();
694
695        assert_ne!(mgmt_pkt.destination_hash, bh_pkt.destination_hash,
696            "Management and blackhole should have different dest hashes");
697    }
698}