Skip to main content

rns_net/common/
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 rns_core::constants;
11use rns_core::destination::destination_hash;
12use rns_core::hash::truncated_hash;
13use rns_core::msgpack::{self, Value};
14use rns_core::transport::types::{InterfaceId, InterfaceInfo};
15use rns_core::transport::TransportEngine;
16
17use super::interface_stats::InterfaceStats;
18use super::time;
19
20/// View trait for interface status, decoupling management from InterfaceEntry.
21pub trait InterfaceStatusView {
22    fn id(&self) -> InterfaceId;
23    fn info(&self) -> &InterfaceInfo;
24    fn online(&self) -> bool;
25    fn stats(&self) -> &InterfaceStats;
26    fn tx_drops(&self) -> u64 {
27        0
28    }
29    fn tx_dropped_bytes(&self) -> u64 {
30        0
31    }
32    fn tx_stalled(&self) -> bool {
33        false
34    }
35    fn tx_buffered(&self) -> usize {
36        0
37    }
38}
39
40/// Get the path hash for "/status".
41pub fn status_path_hash() -> [u8; 16] {
42    truncated_hash(b"/status")
43}
44
45/// Get the path hash for "/path".
46pub fn path_path_hash() -> [u8; 16] {
47    truncated_hash(b"/path")
48}
49
50/// Get the path hash for "/list".
51pub fn list_path_hash() -> [u8; 16] {
52    truncated_hash(b"/list")
53}
54
55/// Check if a path hash matches a known management path.
56pub fn is_management_path(path_hash: &[u8; 16]) -> bool {
57    *path_hash == status_path_hash()
58        || *path_hash == path_path_hash()
59        || *path_hash == list_path_hash()
60}
61
62/// Compute the remote management destination hash.
63///
64/// Destination: `rnstransport.remote.management` with transport identity.
65pub fn management_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
66    destination_hash(
67        "rnstransport",
68        &["remote", "management"],
69        Some(transport_identity_hash),
70    )
71}
72
73/// Compute the blackhole info destination hash.
74///
75/// Destination: `rnstransport.info.blackhole` with transport identity.
76pub fn blackhole_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
77    destination_hash(
78        "rnstransport",
79        &["info", "blackhole"],
80        Some(transport_identity_hash),
81    )
82}
83
84/// Compute the probe responder destination hash.
85///
86/// Destination: `rnstransport.probe` with transport identity.
87pub fn probe_dest_hash(transport_identity_hash: &[u8; 16]) -> [u8; 16] {
88    destination_hash("rnstransport", &["probe"], Some(transport_identity_hash))
89}
90
91/// Build an announce packet for the probe responder destination.
92///
93/// Returns raw packet bytes ready for `engine.handle_outbound()`.
94pub fn build_probe_announce(
95    identity: &rns_crypto::identity::Identity,
96    rng: &mut dyn rns_crypto::Rng,
97) -> Option<Vec<u8>> {
98    let identity_hash = *identity.hash();
99    let dest_hash = probe_dest_hash(&identity_hash);
100    let name_hash = rns_core::destination::name_hash("rnstransport", &["probe"]);
101    let mut random_hash = [0u8; 10];
102    rng.fill_bytes(&mut random_hash);
103
104    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
105        identity,
106        &dest_hash,
107        &name_hash,
108        &random_hash,
109        None,
110        None,
111    )
112    .ok()?;
113
114    let flags = rns_core::packet::PacketFlags {
115        header_type: constants::HEADER_1,
116        context_flag: constants::FLAG_UNSET,
117        transport_type: constants::TRANSPORT_BROADCAST,
118        destination_type: constants::DESTINATION_SINGLE,
119        packet_type: constants::PACKET_TYPE_ANNOUNCE,
120    };
121
122    let packet = rns_core::packet::RawPacket::pack(
123        flags,
124        0,
125        &dest_hash,
126        None,
127        constants::CONTEXT_NONE,
128        &announce_data,
129    )
130    .ok()?;
131
132    Some(packet.raw)
133}
134
135/// Management configuration.
136#[derive(Debug, Clone, Default)]
137pub struct ManagementConfig {
138    /// Enable remote management destination.
139    pub enable_remote_management: bool,
140    /// Identity hashes allowed to query management.
141    pub remote_management_allowed: Vec<[u8; 16]>,
142    /// Enable blackhole list publication.
143    pub publish_blackhole: bool,
144}
145
146/// Handle a `/status` request.
147///
148/// Request data: msgpack([include_lstats, include_profiling]) where both are bool.
149/// Response: msgpack([interface_stats_dict, link_count?, profiling_results?])
150/// matching Python format.
151pub fn handle_status_request(
152    data: &[u8],
153    engine: &TransportEngine,
154    interfaces: &[&dyn InterfaceStatusView],
155    started: f64,
156    probe_responder_hash: Option<[u8; 16]>,
157) -> Option<Vec<u8>> {
158    // Decode request data
159    let (include_lstats, include_profiling) = match msgpack::unpack_exact(data) {
160        Ok(Value::Array(arr)) if !arr.is_empty() => (
161            arr[0].as_bool().unwrap_or(false),
162            arr.get(1).and_then(Value::as_bool).unwrap_or(false),
163        ),
164        _ => (false, false),
165    };
166
167    // Build interface stats
168    let mut iface_list = Vec::new();
169    let mut total_rxb: u64 = 0;
170    let mut total_txb: u64 = 0;
171    let mut total_rxpps = 0.0;
172    let mut total_txpps = 0.0;
173    let mut total_arxb: u64 = 0;
174    let mut total_atxb: u64 = 0;
175    let mut total_arxc: u64 = 0;
176    let mut total_atxc: u64 = 0;
177    let mut total_prxb: u64 = 0;
178    let mut total_ptxb: u64 = 0;
179    let mut total_prxc: u64 = 0;
180    let mut total_ptxc: u64 = 0;
181    let mut total_rxs = 0.0;
182    let mut total_txs = 0.0;
183    let mut total_arxs = 0.0;
184    let mut total_atxs = 0.0;
185    let mut total_prxs = 0.0;
186    let mut total_ptxs = 0.0;
187    let mut total_arxf = 0.0;
188    let mut total_atxf = 0.0;
189    let mut total_prxf = 0.0;
190    let mut total_ptxf = 0.0;
191
192    for entry in interfaces {
193        let id = entry.id();
194        let info = entry.info();
195        let stats = entry.stats();
196
197        if !info.is_local_client {
198            total_rxb += stats.rxb;
199            total_txb += stats.txb;
200            total_rxpps += stats.traffic_rates.rxpps;
201            total_txpps += stats.traffic_rates.txpps;
202            total_arxb += stats.arxb;
203            total_atxb += stats.atxb;
204            total_arxc += stats.arxc;
205            total_atxc += stats.atxc;
206            total_prxb += stats.prxb;
207            total_ptxb += stats.ptxb;
208            total_prxc += stats.prxc;
209            total_ptxc += stats.ptxc;
210            total_rxs += stats.traffic_rates.rxs;
211            total_txs += stats.traffic_rates.txs;
212            total_arxs += stats.traffic_rates.arxs;
213            total_atxs += stats.traffic_rates.atxs;
214            total_prxs += stats.traffic_rates.prxs;
215            total_ptxs += stats.traffic_rates.ptxs;
216            total_arxf += stats.incoming_announce_freq();
217            total_atxf += stats.outgoing_announce_freq();
218            total_prxf += stats.incoming_path_request_freq();
219            total_ptxf += stats.outgoing_path_request_freq();
220        }
221
222        let mut ifstats: Vec<(&str, Value)> = vec![
223            ("name", Value::Str(info.name.clone())),
224            ("short_name", Value::Str(info.name.clone())),
225            ("status", Value::Bool(entry.online())),
226            ("mode", Value::UInt(info.mode as u64)),
227            ("rxb", Value::UInt(stats.rxb)),
228            ("txb", Value::UInt(stats.txb)),
229            ("txdrp", Value::UInt(entry.tx_drops())),
230            ("txdrb", Value::UInt(entry.tx_dropped_bytes())),
231            ("txstalled", Value::Bool(entry.tx_stalled())),
232            ("txbuffered", Value::UInt(entry.tx_buffered() as u64)),
233            ("rxs", Value::Float(stats.traffic_rates.rxs)),
234            ("txs", Value::Float(stats.traffic_rates.txs)),
235            ("arxb", Value::UInt(stats.arxb)),
236            ("atxb", Value::UInt(stats.atxb)),
237            ("arxc", Value::UInt(stats.arxc)),
238            ("atxc", Value::UInt(stats.atxc)),
239            ("arxs", Value::Float(stats.traffic_rates.arxs)),
240            ("atxs", Value::Float(stats.traffic_rates.atxs)),
241            ("prxb", Value::UInt(stats.prxb)),
242            ("ptxb", Value::UInt(stats.ptxb)),
243            ("prxc", Value::UInt(stats.prxc)),
244            ("ptxc", Value::UInt(stats.ptxc)),
245            ("prxs", Value::Float(stats.traffic_rates.prxs)),
246            ("ptxs", Value::Float(stats.traffic_rates.ptxs)),
247            (
248                "protocol_violations",
249                Value::UInt(stats.protocol_violations),
250            ),
251            ("ifac_violations", Value::UInt(stats.ifac_violations)),
252            ("packet_filter_hits", Value::UInt(stats.packet_filter_hits)),
253        ];
254        if let Some(br) = info.bitrate {
255            ifstats.push(("bitrate", Value::UInt(br)));
256        } else {
257            ifstats.push(("bitrate", Value::Nil));
258        }
259        ifstats.push((
260            "incoming_announce_freq",
261            Value::Float(stats.incoming_announce_freq()),
262        ));
263        ifstats.push((
264            "outgoing_announce_freq",
265            Value::Float(stats.outgoing_announce_freq()),
266        ));
267        ifstats.push((
268            "held_announces",
269            Value::UInt(engine.held_announce_count(&id) as u64),
270        ));
271
272        // IFAC info
273        ifstats.push(("ifac_signature", Value::Nil));
274        ifstats.push((
275            "ifac_size",
276            if info.bitrate.is_some() {
277                Value::UInt(0)
278            } else {
279                Value::Nil
280            },
281        ));
282        ifstats.push(("ifac_netname", Value::Nil));
283
284        // Unused by Rust but expected by Python clients
285        ifstats.push(("clients", Value::Nil));
286        ifstats.push(("announce_queue", Value::Nil));
287
288        // Build as map
289        let map = ifstats
290            .into_iter()
291            .map(|(k, v)| (Value::Str(k.into()), v))
292            .collect();
293        iface_list.push(Value::Map(map));
294    }
295
296    // Build top-level stats dict
297    let mut stats: Vec<(&str, Value)> = vec![
298        ("interfaces", Value::Array(iface_list)),
299        ("rxb", Value::UInt(total_rxb)),
300        ("txb", Value::UInt(total_txb)),
301        ("rxpps", Value::Float(total_rxpps)),
302        ("txpps", Value::Float(total_txpps)),
303        ("rxs", Value::Float(total_rxs)),
304        ("txs", Value::Float(total_txs)),
305        ("arxb", Value::UInt(total_arxb)),
306        ("atxb", Value::UInt(total_atxb)),
307        ("arxc", Value::UInt(total_arxc)),
308        ("atxc", Value::UInt(total_atxc)),
309        ("arxs", Value::Float(total_arxs)),
310        ("atxs", Value::Float(total_atxs)),
311        ("prxb", Value::UInt(total_prxb)),
312        ("ptxb", Value::UInt(total_ptxb)),
313        ("prxc", Value::UInt(total_prxc)),
314        ("ptxc", Value::UInt(total_ptxc)),
315        ("prxs", Value::Float(total_prxs)),
316        ("ptxs", Value::Float(total_ptxs)),
317        ("arxf", Value::Float(total_arxf)),
318        ("atxf", Value::Float(total_atxf)),
319        ("prxf", Value::Float(total_prxf)),
320        ("ptxf", Value::Float(total_ptxf)),
321    ];
322
323    if let Some(identity_hash) = engine.config().identity_hash {
324        stats.push(("transport_id", Value::Bin(identity_hash.to_vec())));
325        stats.push(("transport_uptime", Value::Float(time::now() - started)));
326    }
327    stats.push((
328        "probe_responder",
329        match probe_responder_hash {
330            Some(hash) => Value::Bin(hash.to_vec()),
331            None => Value::Nil,
332        },
333    ));
334    stats.push(("rss", Value::Nil));
335
336    let stats_map = stats
337        .into_iter()
338        .map(|(k, v)| (Value::Str(k.into()), v))
339        .collect();
340
341    // Build response: [stats_dict] or [stats_dict, link_count]
342    let mut response = vec![Value::Map(stats_map)];
343    if include_lstats {
344        let link_count = engine.link_table_count();
345        response.push(Value::UInt(link_count as u64));
346    }
347    if include_profiling {
348        response.push(crate::profiling::results_msgpack());
349    }
350
351    Some(msgpack::pack(&Value::Array(response)))
352}
353
354/// Handle a `/path` request.
355///
356/// Request data: msgpack([command, destination_hash?, max_hops?])
357/// - command = "table" → returns path table entries
358/// - command = "rates" → returns rate table entries
359pub fn handle_path_request(data: &[u8], engine: &TransportEngine) -> Option<Vec<u8>> {
360    let arr = match msgpack::unpack_exact(data) {
361        Ok(Value::Array(arr)) if !arr.is_empty() => arr,
362        _ => return None,
363    };
364
365    let command = match &arr[0] {
366        Value::Str(s) => s.as_str(),
367        _ => return None,
368    };
369
370    let dest_filter: Option<[u8; 16]> = if arr.len() > 1 {
371        match &arr[1] {
372            Value::Bin(b) if b.len() == 16 => {
373                let mut h = [0u8; 16];
374                h.copy_from_slice(b);
375                Some(h)
376            }
377            _ => None,
378        }
379    } else {
380        None
381    };
382
383    let max_hops: Option<u8> = if arr.len() > 2 {
384        arr[2].as_uint().map(|v| v as u8)
385    } else {
386        None
387    };
388
389    match command {
390        "table" => {
391            let paths = engine.get_path_table(max_hops);
392            let mut entries = Vec::new();
393            for p in &paths {
394                if let Some(ref filter) = dest_filter {
395                    if p.0 != *filter {
396                        continue;
397                    }
398                }
399                // p = (dest_hash, timestamp, next_hop, hops, expires, interface)
400                let entry = vec![
401                    (Value::Str("hash".into()), Value::Bin(p.0.to_vec())),
402                    (Value::Str("timestamp".into()), Value::Float(p.1)),
403                    (Value::Str("via".into()), Value::Bin(p.2.to_vec())),
404                    (Value::Str("hops".into()), Value::UInt(p.3 as u64)),
405                    (Value::Str("expires".into()), Value::Float(p.4)),
406                    (Value::Str("interface".into()), Value::Str(p.5.clone())),
407                ];
408                entries.push(Value::Map(entry));
409            }
410            Some(msgpack::pack(&Value::Array(entries)))
411        }
412        "rates" => {
413            let rates = engine.get_rate_table();
414            let mut entries = Vec::new();
415            for r in &rates {
416                if let Some(ref filter) = dest_filter {
417                    if r.0 != *filter {
418                        continue;
419                    }
420                }
421                // r = (dest_hash, last, rate_violations, blocked_until, timestamps)
422                let timestamps: Vec<Value> = r.4.iter().map(|t| Value::Float(*t)).collect();
423                let entry = vec![
424                    (Value::Str("hash".into()), Value::Bin(r.0.to_vec())),
425                    (Value::Str("last".into()), Value::Float(r.1)),
426                    (
427                        Value::Str("rate_violations".into()),
428                        Value::UInt(r.2 as u64),
429                    ),
430                    (Value::Str("blocked_until".into()), Value::Float(r.3)),
431                    (Value::Str("timestamps".into()), Value::Array(timestamps)),
432                ];
433                entries.push(Value::Map(entry));
434            }
435            Some(msgpack::pack(&Value::Array(entries)))
436        }
437        _ => None,
438    }
439}
440
441/// Handle a `/list` (blackhole list) request.
442///
443/// Returns the blackholed_identities dict as msgpack.
444pub fn handle_blackhole_list_request(engine: &TransportEngine) -> Option<Vec<u8>> {
445    let blackholed = engine.get_blackholed();
446    let mut map_entries = Vec::new();
447    for (hash, created, expires, reason) in &blackholed {
448        let mut entry = vec![
449            (Value::Str("created".into()), Value::Float(*created)),
450            (Value::Str("expires".into()), Value::Float(*expires)),
451        ];
452        if let Some(r) = reason {
453            entry.push((Value::Str("reason".into()), Value::Str(r.clone())));
454        }
455        map_entries.push((Value::Bin(hash.to_vec()), Value::Map(entry)));
456    }
457    Some(msgpack::pack(&Value::Map(map_entries)))
458}
459
460/// Build an announce packet for the management destination.
461///
462/// Returns raw packet bytes ready for `engine.handle_outbound()`.
463pub fn build_management_announce(
464    identity: &rns_crypto::identity::Identity,
465    rng: &mut dyn rns_crypto::Rng,
466) -> Option<Vec<u8>> {
467    let identity_hash = *identity.hash();
468    let dest_hash = management_dest_hash(&identity_hash);
469    let name_hash = rns_core::destination::name_hash("rnstransport", &["remote", "management"]);
470    let mut random_hash = [0u8; 10];
471    rng.fill_bytes(&mut random_hash);
472
473    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
474        identity,
475        &dest_hash,
476        &name_hash,
477        &random_hash,
478        None, // no ratchet
479        None, // no app_data
480    )
481    .ok()?;
482
483    let flags = rns_core::packet::PacketFlags {
484        header_type: constants::HEADER_1,
485        context_flag: constants::FLAG_UNSET,
486        transport_type: constants::TRANSPORT_BROADCAST,
487        destination_type: constants::DESTINATION_SINGLE,
488        packet_type: constants::PACKET_TYPE_ANNOUNCE,
489    };
490
491    let packet = rns_core::packet::RawPacket::pack(
492        flags,
493        0,
494        &dest_hash,
495        None,
496        constants::CONTEXT_NONE,
497        &announce_data,
498    )
499    .ok()?;
500
501    Some(packet.raw)
502}
503
504/// Build an announce packet for the blackhole info destination.
505///
506/// Returns raw packet bytes ready for `engine.handle_outbound()`.
507pub fn build_blackhole_announce(
508    identity: &rns_crypto::identity::Identity,
509    rng: &mut dyn rns_crypto::Rng,
510) -> Option<Vec<u8>> {
511    let identity_hash = *identity.hash();
512    let dest_hash = blackhole_dest_hash(&identity_hash);
513    let name_hash = rns_core::destination::name_hash("rnstransport", &["info", "blackhole"]);
514    let mut random_hash = [0u8; 10];
515    rng.fill_bytes(&mut random_hash);
516
517    let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
518        identity,
519        &dest_hash,
520        &name_hash,
521        &random_hash,
522        None,
523        None,
524    )
525    .ok()?;
526
527    let flags = rns_core::packet::PacketFlags {
528        header_type: constants::HEADER_1,
529        context_flag: constants::FLAG_UNSET,
530        transport_type: constants::TRANSPORT_BROADCAST,
531        destination_type: constants::DESTINATION_SINGLE,
532        packet_type: constants::PACKET_TYPE_ANNOUNCE,
533    };
534
535    let packet = rns_core::packet::RawPacket::pack(
536        flags,
537        0,
538        &dest_hash,
539        None,
540        constants::CONTEXT_NONE,
541        &announce_data,
542    )
543    .ok()?;
544
545    Some(packet.raw)
546}