Skip to main content

rns_net/
rpc.rs

1//! RPC server and client for cross-process daemon communication.
2//!
3//! Implements Python `multiprocessing.connection` wire protocol:
4//! - 4-byte big-endian signed i32 length prefix + payload
5//! - HMAC-SHA256 challenge-response authentication
6//! - msgpack serialization for request/response dictionaries
7//!
8//! Server translates RPC dicts into [`QueryRequest`] events, sends
9//! them through the driver event channel, and returns RPC responses.
10
11use std::io::{self, Read, Write};
12use std::net::{IpAddr, TcpListener, TcpStream};
13#[cfg(target_os = "linux")]
14use std::os::linux::net::SocketAddrExt;
15#[cfg(target_os = "linux")]
16use std::os::unix::net::{SocketAddr, UnixListener};
17use std::sync::mpsc;
18use std::sync::{
19    atomic::{AtomicBool, Ordering},
20    Arc,
21};
22use std::thread;
23use std::time::Duration;
24
25use rns_core::msgpack::{self, Value as MsgpackValue};
26use rns_core::transport::types::InterfaceId;
27use rns_crypto::hmac::hmac_sha256;
28use rns_crypto::sha256::sha256;
29
30#[cfg(test)]
31use crate::event::TrafficDetail;
32use crate::event::{
33    BackboneInterfaceEntry, BackbonePeerStateEntry, BlackholeInfo, BurstClassStats, DrainStatus,
34    DynamicBurstStats, Event, EventSender, HookInfo, InterfaceStatsResponse, KnownDestinationEntry,
35    LifecycleState, PathTableEntry, ProviderBridgeStats, QueryRequest, QueryResponse,
36    RateTableEntry, RuntimeConfigApplyMode, RuntimeConfigEntry, RuntimeConfigError,
37    RuntimeConfigErrorCode, RuntimeConfigSource, RuntimeConfigValue, SingleInterfaceStat,
38};
39use crate::md5::hmac_md5;
40use crate::pickle::{self, PickleValue};
41
42const CHALLENGE_PREFIX: &[u8] = b"#CHALLENGE#";
43const WELCOME: &[u8] = b"#WELCOME#";
44const FAILURE: &[u8] = b"#FAILURE#";
45const CHALLENGE_LEN: usize = 40;
46const MAX_CONCURRENT_RPC_CONNECTIONS: usize = 32;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum RpcCodec {
50    Msgpack,
51    Pickle,
52}
53
54/// RPC address types.
55#[derive(Debug, Clone)]
56pub enum RpcAddr {
57    Tcp(String, u16),
58}
59
60/// RPC server that listens for incoming connections and handles queries.
61pub struct RpcServer {
62    shutdown: Arc<AtomicBool>,
63    thread: Option<thread::JoinHandle<()>>,
64}
65
66impl RpcServer {
67    /// Start the RPC server on the given address.
68    pub fn start(addr: &RpcAddr, auth_key: [u8; 32], event_tx: EventSender) -> io::Result<Self> {
69        let shutdown = Arc::new(AtomicBool::new(false));
70        let shutdown2 = shutdown.clone();
71
72        let listener = match addr {
73            RpcAddr::Tcp(host, port) => {
74                let l = TcpListener::bind((host.as_str(), *port))?;
75                // Non-blocking so we can check shutdown flag
76                l.set_nonblocking(true)?;
77                l
78            }
79        };
80
81        let thread = thread::Builder::new()
82            .name("rpc-server".into())
83            .spawn(move || {
84                rpc_server_loop(listener, auth_key, event_tx, shutdown2);
85            })
86            .map_err(io::Error::other)?;
87
88        Ok(RpcServer {
89            shutdown,
90            thread: Some(thread),
91        })
92    }
93
94    /// Listen on the abstract socket used by Python Reticulum on Linux.
95    #[cfg(target_os = "linux")]
96    pub fn start_abstract_unix(
97        instance_name: &str,
98        auth_key: [u8; 32],
99        event_tx: EventSender,
100    ) -> io::Result<Self> {
101        let addr = SocketAddr::from_abstract_name(format!("rns/{instance_name}/rpc"))?;
102        let listener = UnixListener::bind_addr(&addr)?;
103        listener.set_nonblocking(true)?;
104        let shutdown = Arc::new(AtomicBool::new(false));
105        let shutdown2 = shutdown.clone();
106        let thread = thread::Builder::new()
107            .name("rpc-unix-server".into())
108            .spawn(move || rpc_server_loop_unix(listener, auth_key, event_tx, shutdown2))
109            .map_err(io::Error::other)?;
110        Ok(Self {
111            shutdown,
112            thread: Some(thread),
113        })
114    }
115
116    /// Stop the RPC server.
117    pub fn stop(&mut self) {
118        self.shutdown.store(true, Ordering::Relaxed);
119        if let Some(handle) = self.thread.take() {
120            let _ = handle.join();
121        }
122    }
123}
124
125#[cfg(target_os = "linux")]
126fn rpc_server_loop_unix(
127    listener: UnixListener,
128    auth_key: [u8; 32],
129    event_tx: EventSender,
130    shutdown: Arc<AtomicBool>,
131) {
132    let mut workers = Vec::new();
133    loop {
134        reap_finished_rpc_workers(&mut workers);
135        if shutdown.load(Ordering::Relaxed) {
136            break;
137        }
138        match listener.accept() {
139            Ok((stream, _)) => {
140                if workers.len() >= MAX_CONCURRENT_RPC_CONNECTIONS {
141                    log::warn!("rejecting Unix RPC connection: {MAX_CONCURRENT_RPC_CONNECTIONS} handlers already active");
142                    continue;
143                }
144                let _ = stream.set_read_timeout(Some(Duration::from_secs(10)));
145                let _ = stream.set_write_timeout(Some(Duration::from_secs(10)));
146                let worker_event_tx = event_tx.clone();
147                match thread::Builder::new()
148                    .name("rpc-unix-client".into())
149                    .spawn(move || {
150                        if let Err(e) = handle_connection(stream, &auth_key, &worker_event_tx) {
151                            log::debug!("Unix RPC connection error: {e}");
152                        }
153                    }) {
154                    Ok(worker) => workers.push(worker),
155                    Err(e) => log::error!("failed to start Unix RPC connection handler: {e}"),
156                }
157            }
158            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
159                thread::sleep(Duration::from_millis(10))
160            }
161            Err(e) => {
162                log::error!("Unix RPC accept error: {e}");
163                thread::sleep(Duration::from_millis(100));
164            }
165        }
166    }
167    for worker in workers {
168        let _ = worker.join();
169    }
170}
171
172impl Drop for RpcServer {
173    fn drop(&mut self) {
174        self.stop();
175    }
176}
177
178fn rpc_server_loop(
179    listener: TcpListener,
180    auth_key: [u8; 32],
181    event_tx: EventSender,
182    shutdown: Arc<AtomicBool>,
183) {
184    let mut workers = Vec::new();
185
186    loop {
187        reap_finished_rpc_workers(&mut workers);
188
189        if shutdown.load(Ordering::Relaxed) {
190            break;
191        }
192
193        match listener.accept() {
194            Ok((stream, addr)) => {
195                if workers.len() >= MAX_CONCURRENT_RPC_CONNECTIONS {
196                    log::warn!(
197                        "rejecting RPC connection from {addr}: {MAX_CONCURRENT_RPC_CONNECTIONS} handlers already active"
198                    );
199                    continue;
200                }
201
202                // Set blocking for this connection
203                let _ = stream.set_nonblocking(false);
204                let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(10)));
205                let _ = stream.set_write_timeout(Some(std::time::Duration::from_secs(10)));
206
207                let worker_event_tx = event_tx.clone();
208                match thread::Builder::new()
209                    .name("rpc-client".into())
210                    .spawn(move || {
211                        if let Err(e) = handle_connection(stream, &auth_key, &worker_event_tx) {
212                            log::debug!("RPC connection error: {e}");
213                        }
214                    }) {
215                    Ok(worker) => workers.push(worker),
216                    Err(e) => log::error!("failed to start RPC connection handler: {e}"),
217                }
218            }
219            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
220                // No pending connection, sleep briefly and retry
221                thread::sleep(std::time::Duration::from_millis(10));
222            }
223            Err(e) => {
224                log::error!("RPC accept error: {}", e);
225                thread::sleep(std::time::Duration::from_millis(100));
226            }
227        }
228    }
229
230    for worker in workers {
231        let _ = worker.join();
232    }
233}
234
235fn reap_finished_rpc_workers(workers: &mut Vec<thread::JoinHandle<()>>) {
236    let mut index = 0;
237    while index < workers.len() {
238        if workers[index].is_finished() {
239            let worker = workers.swap_remove(index);
240            let _ = worker.join();
241        } else {
242            index += 1;
243        }
244    }
245}
246
247fn handle_connection<S: Read + Write>(
248    mut stream: S,
249    auth_key: &[u8; 32],
250    event_tx: &EventSender,
251) -> io::Result<()> {
252    // Authentication: send challenge, verify response
253    server_auth(&mut stream, auth_key)?;
254    // multiprocessing.connection authenticates both peers before requests.
255    answer_challenge(&mut stream, auth_key)?;
256
257    // Read request.
258    let request_bytes = recv_bytes(&mut stream)?;
259    let (request, codec) = decode_rpc_payload(&request_bytes)?;
260
261    // Translate request dict to query, send to driver, get response.
262    let response = handle_rpc_request(&request, event_tx)?;
263
264    // Encode response and send
265    let response_bytes = encode_rpc_payload(&response, codec);
266    send_bytes(&mut stream, &response_bytes)?;
267
268    Ok(())
269}
270
271fn decode_rpc_payload(data: &[u8]) -> io::Result<(PickleValue, RpcCodec)> {
272    if let Ok(value) = msgpack::unpack_exact(data) {
273        return Ok((msgpack_to_pickle(&value), RpcCodec::Msgpack));
274    }
275    pickle::decode(data)
276        .map(|value| (value, RpcCodec::Pickle))
277        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
278}
279
280fn encode_rpc_payload(value: &PickleValue, codec: RpcCodec) -> Vec<u8> {
281    match codec {
282        RpcCodec::Msgpack => msgpack::pack(&pickle_to_msgpack(value)),
283        RpcCodec::Pickle => pickle::encode(value),
284    }
285}
286
287fn pickle_to_msgpack(value: &PickleValue) -> MsgpackValue {
288    match value {
289        PickleValue::None => MsgpackValue::Nil,
290        PickleValue::Bool(v) => MsgpackValue::Bool(*v),
291        PickleValue::Int(v) if *v >= 0 => MsgpackValue::UInt(*v as u64),
292        PickleValue::Int(v) => MsgpackValue::Int(*v),
293        PickleValue::Float(v) => MsgpackValue::Float(*v),
294        PickleValue::String(v) => MsgpackValue::Str(v.clone()),
295        PickleValue::Bytes(v) => MsgpackValue::Bin(v.clone()),
296        PickleValue::List(values) => {
297            MsgpackValue::Array(values.iter().map(pickle_to_msgpack).collect())
298        }
299        PickleValue::Dict(values) => MsgpackValue::Map(
300            values
301                .iter()
302                .map(|(key, value)| (pickle_to_msgpack(key), pickle_to_msgpack(value)))
303                .collect(),
304        ),
305    }
306}
307
308fn msgpack_to_pickle(value: &MsgpackValue) -> PickleValue {
309    match value {
310        MsgpackValue::Nil => PickleValue::None,
311        MsgpackValue::Bool(v) => PickleValue::Bool(*v),
312        MsgpackValue::UInt(v) if *v <= i64::MAX as u64 => PickleValue::Int(*v as i64),
313        MsgpackValue::UInt(v) => PickleValue::Float(*v as f64),
314        MsgpackValue::Int(v) => PickleValue::Int(*v),
315        MsgpackValue::Float(v) => PickleValue::Float(*v),
316        MsgpackValue::Bin(v) => PickleValue::Bytes(v.clone()),
317        MsgpackValue::Str(v) => PickleValue::String(v.clone()),
318        MsgpackValue::Array(values) => {
319            PickleValue::List(values.iter().map(msgpack_to_pickle).collect())
320        }
321        MsgpackValue::Map(values) => PickleValue::Dict(
322            values
323                .iter()
324                .map(|(key, value)| (msgpack_to_pickle(key), msgpack_to_pickle(value)))
325                .collect(),
326        ),
327    }
328}
329
330/// Server-side authentication: challenge-response.
331fn server_auth<S: Read + Write>(stream: &mut S, auth_key: &[u8; 32]) -> io::Result<()> {
332    // Generate challenge: #CHALLENGE#{sha256}<40 random bytes>
333    let mut random_bytes = [0u8; CHALLENGE_LEN];
334    // Use /dev/urandom for randomness
335    {
336        let mut f = std::fs::File::open("/dev/urandom")?;
337        f.read_exact(&mut random_bytes)?;
338    }
339
340    let mut challenge_message = Vec::with_capacity(CHALLENGE_PREFIX.len() + 8 + CHALLENGE_LEN);
341    challenge_message.extend_from_slice(CHALLENGE_PREFIX);
342    challenge_message.extend_from_slice(b"{sha256}");
343    challenge_message.extend_from_slice(&random_bytes);
344
345    send_bytes(stream, &challenge_message)?;
346
347    // Read response (max 256 bytes)
348    let response = recv_bytes(stream)?;
349
350    // Verify response
351    // The message to HMAC is everything after #CHALLENGE# (i.e. {sha256}<random>)
352    let message = &challenge_message[CHALLENGE_PREFIX.len()..];
353
354    if verify_response(auth_key, message, &response) {
355        send_bytes(stream, WELCOME)?;
356        Ok(())
357    } else {
358        send_bytes(stream, FAILURE)?;
359        Err(io::Error::new(
360            io::ErrorKind::PermissionDenied,
361            "auth failed",
362        ))
363    }
364}
365
366/// Verify a client's HMAC response.
367fn verify_response(auth_key: &[u8; 32], message: &[u8], response: &[u8]) -> bool {
368    // Modern protocol: response = {sha256}<hmac-sha256 digest>
369    if response.starts_with(b"{sha256}") {
370        let digest = &response[8..];
371        let expected = hmac_sha256(auth_key, message);
372        constant_time_eq(digest, &expected)
373    }
374    // Legacy protocol: response = raw 16-byte HMAC-MD5 digest
375    else if response.len() == 16 {
376        let expected = hmac_md5(auth_key, message);
377        constant_time_eq(response, &expected)
378    }
379    // Legacy with {md5} prefix
380    else if response.starts_with(b"{md5}") {
381        let digest = &response[5..];
382        let expected = hmac_md5(auth_key, message);
383        constant_time_eq(digest, &expected)
384    } else {
385        false
386    }
387}
388
389/// Constant-time byte comparison.
390fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
391    if a.len() != b.len() {
392        return false;
393    }
394    let mut diff = 0u8;
395    for (x, y) in a.iter().zip(b.iter()) {
396        diff |= x ^ y;
397    }
398    diff == 0
399}
400
401/// Send bytes with 4-byte big-endian length prefix.
402fn send_bytes<S: Write>(stream: &mut S, data: &[u8]) -> io::Result<()> {
403    let len = data.len() as i32;
404    stream.write_all(&len.to_be_bytes())?;
405    stream.write_all(data)?;
406    stream.flush()
407}
408
409/// Receive bytes with 4-byte big-endian length prefix.
410fn recv_bytes<S: Read>(stream: &mut S) -> io::Result<Vec<u8>> {
411    let mut len_buf = [0u8; 4];
412    stream.read_exact(&mut len_buf)?;
413    let len = i32::from_be_bytes(len_buf);
414
415    if len < 0 {
416        // Extended format: 8-byte length
417        let mut len8_buf = [0u8; 8];
418        stream.read_exact(&mut len8_buf)?;
419        let len = u64::from_be_bytes(len8_buf) as usize;
420        if len > 64 * 1024 * 1024 {
421            return Err(io::Error::new(
422                io::ErrorKind::InvalidData,
423                "message too large",
424            ));
425        }
426        let mut buf = vec![0u8; len];
427        stream.read_exact(&mut buf)?;
428        Ok(buf)
429    } else {
430        let len = len as usize;
431        if len > 64 * 1024 * 1024 {
432            return Err(io::Error::new(
433                io::ErrorKind::InvalidData,
434                "message too large",
435            ));
436        }
437        let mut buf = vec![0u8; len];
438        stream.read_exact(&mut buf)?;
439        Ok(buf)
440    }
441}
442
443/// Default probe payload size when the request omits `size`.
444const DEFAULT_PROBE_PAYLOAD_SIZE: usize = 16;
445
446/// Encode a failed `send_probe` for RPC clients.
447///
448/// `UnknownIdentity` stays `None` for compatibility with older clients. Other
449/// failures return `{"error": <code>, ...}` so clients can tell them apart.
450fn send_probe_error_pickle(err: crate::event::SendProbeError) -> PickleValue {
451    use crate::event::SendProbeError;
452    let code = match err {
453        SendProbeError::UnknownIdentity => return PickleValue::None,
454        SendProbeError::PayloadTooLarge { .. } => "payload_too_large",
455        SendProbeError::EncryptFailed => "encrypt_failed",
456        SendProbeError::PackFailed => "pack_failed",
457    };
458    let mut entries = vec![(
459        PickleValue::String("error".into()),
460        PickleValue::String(code.into()),
461    )];
462    if let SendProbeError::PayloadTooLarge { max } = err {
463        entries.push((
464            PickleValue::String("max_size".into()),
465            PickleValue::Int(max as i64),
466        ));
467    }
468    PickleValue::Dict(entries)
469}
470
471/// Translate an RPC request dict to a query event and get response.
472fn handle_rpc_request(request: &PickleValue, event_tx: &EventSender) -> io::Result<PickleValue> {
473    if let Some(operation) = request.get("manage").and_then(PickleValue::as_str) {
474        let operation = match operation {
475            "attach_interface" => crate::event::InterfaceManagementOperation::Attach,
476            "detach_interface" => crate::event::InterfaceManagementOperation::Detach,
477            "reload_interface" => crate::event::InterfaceManagementOperation::Reload,
478            _ => return Ok(PickleValue::None),
479        };
480        let name = request
481            .get("name")
482            .and_then(PickleValue::as_str)
483            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing interface name"))?;
484        let result = send_query(
485            event_tx,
486            QueryRequest::ManageInterface {
487                operation,
488                name: name.to_string(),
489            },
490        )?;
491        return Ok(match result {
492            QueryResponse::InterfaceManagement(Some(value)) => PickleValue::Bool(value),
493            _ => PickleValue::None,
494        });
495    }
496    // Handle "get" requests
497    if let Some(get_val) = request.get("get") {
498        if let Some(path) = get_val.as_str() {
499            return match path {
500                "interface_stats" => {
501                    let resp = send_query(event_tx, QueryRequest::InterfaceStats)?;
502                    if let QueryResponse::InterfaceStats(stats) = resp {
503                        let mut response = interface_stats_to_pickle(&stats);
504                        add_inbound_runtime_stats(&mut response, &stats, event_tx);
505                        Ok(response)
506                    } else {
507                        Ok(PickleValue::None)
508                    }
509                }
510                "profiling_results" => {
511                    Ok(crate::profiling::results_pickle().unwrap_or(PickleValue::None))
512                }
513                "path_table" => {
514                    let max_hops = request
515                        .get("max_hops")
516                        .and_then(|v| v.as_int().map(|n| n as u8));
517                    let resp = send_query(event_tx, QueryRequest::PathTable { max_hops })?;
518                    if let QueryResponse::PathTable(entries) = resp {
519                        Ok(path_table_to_pickle(&entries))
520                    } else {
521                        Ok(PickleValue::None)
522                    }
523                }
524                "rate_table" => {
525                    let resp = send_query(event_tx, QueryRequest::RateTable)?;
526                    if let QueryResponse::RateTable(entries) = resp {
527                        Ok(rate_table_to_pickle(&entries))
528                    } else {
529                        Ok(PickleValue::None)
530                    }
531                }
532                "lowest_interface_bitrate" => {
533                    let resp = send_query(event_tx, QueryRequest::LowestInterfaceBitrate)?;
534                    if let QueryResponse::LowestInterfaceBitrate(Some(bitrate)) = resp {
535                        Ok(PickleValue::Int(bitrate as i64))
536                    } else {
537                        Ok(PickleValue::None)
538                    }
539                }
540                "medium_path_timeout" => {
541                    let resp = send_query(event_tx, QueryRequest::MediumPathTimeout)?;
542                    if let QueryResponse::MediumPathTimeout(timeout) = resp {
543                        Ok(PickleValue::Float(timeout))
544                    } else {
545                        Ok(PickleValue::Int(0))
546                    }
547                }
548                "first_hop_timeout" => {
549                    let hash = extract_dest_hash(request, "destination_hash")?;
550                    let resp =
551                        send_query(event_tx, QueryRequest::FirstHopTimeout { dest_hash: hash })?;
552                    if let QueryResponse::FirstHopTimeout(timeout) = resp {
553                        Ok(PickleValue::Float(timeout))
554                    } else {
555                        Ok(PickleValue::Float(
556                            rns_core::constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
557                        ))
558                    }
559                }
560                "next_hop" => {
561                    let hash = extract_dest_hash(request, "destination_hash")?;
562                    let resp = send_query(event_tx, QueryRequest::NextHop { dest_hash: hash })?;
563                    if let QueryResponse::NextHop(Some(nh)) = resp {
564                        Ok(PickleValue::Bytes(nh.next_hop.to_vec()))
565                    } else {
566                        Ok(PickleValue::None)
567                    }
568                }
569                "next_hop_if_name" => {
570                    let hash = extract_dest_hash(request, "destination_hash")?;
571                    let resp =
572                        send_query(event_tx, QueryRequest::NextHopIfName { dest_hash: hash })?;
573                    if let QueryResponse::NextHopIfName(Some(name)) = resp {
574                        Ok(PickleValue::String(name))
575                    } else {
576                        Ok(PickleValue::None)
577                    }
578                }
579                "link_count" => {
580                    let resp = send_query(event_tx, QueryRequest::LinkCount)?;
581                    if let QueryResponse::LinkCount(n) = resp {
582                        Ok(PickleValue::Int(n as i64))
583                    } else {
584                        Ok(PickleValue::None)
585                    }
586                }
587                "active_link_count" => {
588                    let resp = send_query(event_tx, QueryRequest::ActiveLinkCount)?;
589                    if let QueryResponse::ActiveLinkCount(n) = resp {
590                        Ok(PickleValue::Int(n as i64))
591                    } else {
592                        Ok(PickleValue::None)
593                    }
594                }
595                "transport_identity" => {
596                    let resp = send_query(event_tx, QueryRequest::TransportIdentity)?;
597                    if let QueryResponse::TransportIdentity(Some(hash)) = resp {
598                        Ok(PickleValue::Bytes(hash.to_vec()))
599                    } else {
600                        Ok(PickleValue::None)
601                    }
602                }
603                "blackholed" => {
604                    let resp = send_query(event_tx, QueryRequest::GetBlackholed)?;
605                    if let QueryResponse::Blackholed(entries) = resp {
606                        Ok(blackholed_to_pickle(&entries))
607                    } else {
608                        Ok(PickleValue::None)
609                    }
610                }
611                "discovered_interfaces" => {
612                    let only_available = request
613                        .get("only_available")
614                        .and_then(|v| v.as_bool())
615                        .unwrap_or(false);
616                    let only_transport = request
617                        .get("only_transport")
618                        .and_then(|v| v.as_bool())
619                        .unwrap_or(false);
620                    let resp = send_query(
621                        event_tx,
622                        QueryRequest::DiscoveredInterfaces {
623                            only_available,
624                            only_transport,
625                        },
626                    )?;
627                    if let QueryResponse::DiscoveredInterfaces(interfaces) = resp {
628                        Ok(discovered_interfaces_to_pickle(&interfaces))
629                    } else {
630                        Ok(PickleValue::None)
631                    }
632                }
633                "hooks" => {
634                    let (response_tx, response_rx) = mpsc::channel();
635                    try_send_event(event_tx, Event::ListHooks { response_tx })?;
636                    let hooks = response_rx
637                        .recv_timeout(std::time::Duration::from_secs(5))
638                        .map_err(|_| {
639                            io::Error::new(io::ErrorKind::TimedOut, "list hooks timed out")
640                        })?;
641                    Ok(hooks_to_pickle(&hooks))
642                }
643                "runtime_config" => {
644                    let resp = send_query(event_tx, QueryRequest::ListRuntimeConfig)?;
645                    if let QueryResponse::RuntimeConfigList(entries) = resp {
646                        Ok(runtime_config_list_to_pickle(&entries))
647                    } else {
648                        Ok(PickleValue::None)
649                    }
650                }
651                "known_destinations" => {
652                    let resp = send_query(event_tx, QueryRequest::KnownDestinations)?;
653                    if let QueryResponse::KnownDestinations(entries) = resp {
654                        Ok(known_destinations_to_pickle(&entries))
655                    } else {
656                        Ok(PickleValue::None)
657                    }
658                }
659                "runtime_config_entry" => {
660                    let key = request
661                        .get("key")
662                        .and_then(|v| v.as_str())
663                        .unwrap_or_default()
664                        .to_string();
665                    let resp = send_query(event_tx, QueryRequest::GetRuntimeConfig { key })?;
666                    if let QueryResponse::RuntimeConfigEntry(entry) = resp {
667                        Ok(entry
668                            .as_ref()
669                            .map(runtime_config_entry_to_pickle)
670                            .unwrap_or(PickleValue::None))
671                    } else {
672                        Ok(PickleValue::None)
673                    }
674                }
675                "backbone_peer_state" => {
676                    let interface_name = request
677                        .get("interface")
678                        .and_then(|v| v.as_str())
679                        .map(|s| s.to_string());
680                    let resp =
681                        send_query(event_tx, QueryRequest::BackbonePeerState { interface_name })?;
682                    if let QueryResponse::BackbonePeerState(entries) = resp {
683                        Ok(backbone_peer_state_to_pickle(&entries))
684                    } else {
685                        Ok(PickleValue::None)
686                    }
687                }
688                "backbone_interfaces" => {
689                    let resp = send_query(event_tx, QueryRequest::BackboneInterfaces)?;
690                    if let QueryResponse::BackboneInterfaces(entries) = resp {
691                        Ok(backbone_interfaces_to_pickle(&entries))
692                    } else {
693                        Ok(PickleValue::None)
694                    }
695                }
696                "provider_bridge_stats" => {
697                    let resp = send_query(event_tx, QueryRequest::ProviderBridgeStats)?;
698                    if let QueryResponse::ProviderBridgeStats(stats) = resp {
699                        Ok(stats
700                            .as_ref()
701                            .map(provider_bridge_stats_to_pickle)
702                            .unwrap_or(PickleValue::None))
703                    } else {
704                        Ok(PickleValue::None)
705                    }
706                }
707                "drain_status" => {
708                    let resp = send_query(event_tx, QueryRequest::DrainStatus)?;
709                    if let QueryResponse::DrainStatus(status) = resp {
710                        Ok(drain_status_to_pickle(&status))
711                    } else {
712                        Ok(PickleValue::None)
713                    }
714                }
715                _ => Ok(PickleValue::None),
716            };
717        }
718    }
719
720    if let Some(begin_val) = request.get("begin_drain") {
721        let timeout_secs = begin_val
722            .as_float()
723            .or_else(|| begin_val.as_int().map(|value| value as f64))
724            .unwrap_or(0.0)
725            .max(0.0);
726        let timeout = Duration::from_secs_f64(timeout_secs);
727        try_send_event(event_tx, Event::BeginDrain { timeout })?;
728        return Ok(PickleValue::Bool(true));
729    }
730
731    if let Some(set_val) = request.get("set").and_then(|v| v.as_str()) {
732        if set_val == "known_destination_retained" {
733            let dest_hash = extract_dest_hash(request, "dest_hash")?;
734            let resp = send_query(event_tx, QueryRequest::RetainKnownDestination { dest_hash })?;
735            return if let QueryResponse::RetainKnownDestination(ok) = resp {
736                Ok(PickleValue::Bool(ok))
737            } else {
738                Ok(PickleValue::None)
739            };
740        }
741        if set_val == "identity_retained" {
742            let identity_hash = extract_dest_hash(request, "identity_hash")?;
743            let resp = send_query(event_tx, QueryRequest::RetainIdentity { identity_hash })?;
744            return if let QueryResponse::RetainIdentity(ok) = resp {
745                Ok(PickleValue::Bool(ok))
746            } else {
747                Ok(PickleValue::None)
748            };
749        }
750        if set_val == "known_destination_used" {
751            let dest_hash = extract_dest_hash(request, "dest_hash")?;
752            let resp = send_query(
753                event_tx,
754                QueryRequest::MarkKnownDestinationUsed { dest_hash },
755            )?;
756            return if let QueryResponse::MarkKnownDestinationUsed(ok) = resp {
757                Ok(PickleValue::Bool(ok))
758            } else {
759                Ok(PickleValue::None)
760            };
761        }
762        if set_val == "runtime_config" {
763            let key = request
764                .get("key")
765                .and_then(|v| v.as_str())
766                .unwrap_or_default()
767                .to_string();
768            let Some(value) = request
769                .get("value")
770                .and_then(runtime_config_value_from_pickle)
771            else {
772                return Ok(runtime_config_error_to_pickle(&RuntimeConfigError {
773                    code: RuntimeConfigErrorCode::InvalidType,
774                    message: "runtime-config set requires a scalar value".into(),
775                }));
776            };
777            let resp = send_query(event_tx, QueryRequest::SetRuntimeConfig { key, value })?;
778            return if let QueryResponse::RuntimeConfigSet(result) = resp {
779                Ok(runtime_config_result_to_pickle(result))
780            } else {
781                Ok(PickleValue::None)
782            };
783        }
784    }
785
786    if let Some(reset_val) = request.get("reset").and_then(|v| v.as_str()) {
787        if reset_val == "runtime_config" {
788            let key = request
789                .get("key")
790                .and_then(|v| v.as_str())
791                .unwrap_or_default()
792                .to_string();
793            let resp = send_query(event_tx, QueryRequest::ResetRuntimeConfig { key })?;
794            return if let QueryResponse::RuntimeConfigReset(result) = resp {
795                Ok(runtime_config_result_to_pickle(result))
796            } else {
797                Ok(PickleValue::None)
798            };
799        }
800    }
801
802    if let Some(clear_val) = request.get("clear").and_then(|v| v.as_str()) {
803        if clear_val == "known_destination_retained" {
804            let dest_hash = extract_dest_hash(request, "dest_hash")?;
805            let resp = send_query(
806                event_tx,
807                QueryRequest::UnretainKnownDestination { dest_hash },
808            )?;
809            return if let QueryResponse::UnretainKnownDestination(ok) = resp {
810                Ok(PickleValue::Bool(ok))
811            } else {
812                Ok(PickleValue::None)
813            };
814        }
815        if clear_val == "backbone_peer_state" {
816            let interface_name = required_string(request, "interface")?;
817            let peer_ip = required_string(request, "ip")?;
818            let peer_ip = peer_ip
819                .parse()
820                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid peer IP"))?;
821            let resp = send_query(
822                event_tx,
823                QueryRequest::ClearBackbonePeerState {
824                    interface_name,
825                    peer_ip,
826                },
827            )?;
828            return if let QueryResponse::ClearBackbonePeerState(ok) = resp {
829                Ok(PickleValue::Bool(ok))
830            } else {
831                Ok(PickleValue::None)
832            };
833        }
834    }
835
836    if let Some(set_val) = request.get("set").and_then(|v| v.as_str()) {
837        if set_val == "backbone_peer_blacklist" {
838            let interface_name = required_string(request, "interface")?;
839            let peer_ip = required_string(request, "ip")?;
840            let peer_ip: IpAddr = peer_ip
841                .parse()
842                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid peer IP"))?;
843            let duration_secs = request
844                .get("duration_secs")
845                .and_then(|v| v.as_int())
846                .ok_or_else(|| {
847                    io::Error::new(io::ErrorKind::InvalidInput, "missing duration_secs")
848                })?;
849            let reason = request
850                .get("reason")
851                .and_then(|v| v.as_str())
852                .unwrap_or("sentinel blacklist")
853                .to_string();
854            let penalty_level = request
855                .get("penalty_level")
856                .and_then(|v| v.as_int())
857                .unwrap_or(0)
858                .clamp(0, u8::MAX as i64) as u8;
859            let resp = send_query(
860                event_tx,
861                QueryRequest::BlacklistBackbonePeer {
862                    interface_name,
863                    peer_ip,
864                    duration: Duration::from_secs(duration_secs as u64),
865                    reason,
866                    penalty_level,
867                },
868            )?;
869            return if let QueryResponse::BlacklistBackbonePeer(ok) = resp {
870                Ok(PickleValue::Bool(ok))
871            } else {
872                Ok(PickleValue::None)
873            };
874        }
875    }
876
877    // Handle "request_path" -- trigger a path request to the network
878    if let Some(hash_val) = request.get("request_path") {
879        if let Some(hash_bytes) = hash_val.as_bytes() {
880            if hash_bytes.len() >= 16 {
881                let mut dest_hash = [0u8; 16];
882                dest_hash.copy_from_slice(&hash_bytes[..16]);
883                try_send_event(event_tx, crate::event::Event::RequestPath { dest_hash })?;
884                return Ok(PickleValue::Bool(true));
885            }
886        }
887    }
888
889    // Handle "send_probe" requests
890    if let Some(hash_val) = request.get("send_probe") {
891        if let Some(hash_bytes) = hash_val.as_bytes() {
892            if hash_bytes.len() >= 16 {
893                let mut dest_hash = [0u8; 16];
894                dest_hash.copy_from_slice(&hash_bytes[..16]);
895                // Missing or non-positive sizes fall back to the default;
896                // oversize requests are rejected rather than silently
897                // replaced, so the caller learns the real limit.
898                let payload_size = match request.get("size").and_then(|v| v.as_int()) {
899                    Some(n) if n > 0 => usize::try_from(n).unwrap_or(usize::MAX),
900                    _ => DEFAULT_PROBE_PAYLOAD_SIZE,
901                };
902                if payload_size > crate::event::MAX_PROBE_PAYLOAD_SIZE {
903                    return Ok(send_probe_error_pickle(
904                        crate::event::SendProbeError::PayloadTooLarge {
905                            max: crate::event::MAX_PROBE_PAYLOAD_SIZE,
906                        },
907                    ));
908                }
909                let resp = send_query(
910                    event_tx,
911                    QueryRequest::SendProbe {
912                        dest_hash,
913                        payload_size,
914                    },
915                )?;
916                return Ok(match resp {
917                    QueryResponse::SendProbe(Ok((packet_hash, hops))) => PickleValue::Dict(vec![
918                        (
919                            PickleValue::String("packet_hash".into()),
920                            PickleValue::Bytes(packet_hash.to_vec()),
921                        ),
922                        (
923                            PickleValue::String("hops".into()),
924                            PickleValue::Int(hops as i64),
925                        ),
926                    ]),
927                    QueryResponse::SendProbe(Err(err)) => send_probe_error_pickle(err),
928                    _ => PickleValue::None,
929                });
930            }
931        }
932    }
933
934    // Handle "check_proof" requests
935    if let Some(hash_val) = request.get("check_proof") {
936        if let Some(hash_bytes) = hash_val.as_bytes() {
937            if hash_bytes.len() >= 32 {
938                let mut packet_hash = [0u8; 32];
939                packet_hash.copy_from_slice(&hash_bytes[..32]);
940                let resp = send_query(event_tx, QueryRequest::CheckProof { packet_hash })?;
941                if let QueryResponse::CheckProof(Some(rtt)) = resp {
942                    return Ok(PickleValue::Float(rtt));
943                } else {
944                    return Ok(PickleValue::None);
945                }
946            }
947        }
948    }
949
950    // Handle "blackhole" requests
951    if let Some(hash_val) = request.get("blackhole") {
952        if let Some(hash_bytes) = hash_val.as_bytes() {
953            if hash_bytes.len() >= 16 {
954                let mut identity_hash = [0u8; 16];
955                identity_hash.copy_from_slice(&hash_bytes[..16]);
956                let duration_hours = request.get("duration").and_then(|v| v.as_float());
957                let reason = request
958                    .get("reason")
959                    .and_then(|v| v.as_str())
960                    .map(|s| s.to_string());
961                let resp = send_query(
962                    event_tx,
963                    QueryRequest::BlackholeIdentity {
964                        identity_hash,
965                        duration_hours,
966                        reason,
967                    },
968                )?;
969                return Ok(PickleValue::Bool(matches!(
970                    resp,
971                    QueryResponse::BlackholeResult(true)
972                )));
973            }
974        }
975    }
976
977    // Handle "unblackhole" requests
978    if let Some(hash_val) = request.get("unblackhole") {
979        if let Some(hash_bytes) = hash_val.as_bytes() {
980            if hash_bytes.len() >= 16 {
981                let mut identity_hash = [0u8; 16];
982                identity_hash.copy_from_slice(&hash_bytes[..16]);
983                let resp = send_query(
984                    event_tx,
985                    QueryRequest::UnblackholeIdentity { identity_hash },
986                )?;
987                return Ok(PickleValue::Bool(matches!(
988                    resp,
989                    QueryResponse::UnblackholeResult(true)
990                )));
991            }
992        }
993    }
994
995    // Handle "drop" requests
996    if let Some(drop_val) = request.get("drop") {
997        if let Some(path) = drop_val.as_str() {
998            return match path {
999                "path" => {
1000                    let hash = extract_dest_hash(request, "destination_hash")?;
1001                    let resp = send_query(event_tx, QueryRequest::DropPath { dest_hash: hash })?;
1002                    if let QueryResponse::DropPath(ok) = resp {
1003                        Ok(PickleValue::Bool(ok))
1004                    } else {
1005                        Ok(PickleValue::None)
1006                    }
1007                }
1008                "all_via" => {
1009                    let hash = extract_dest_hash(request, "destination_hash")?;
1010                    let resp = send_query(
1011                        event_tx,
1012                        QueryRequest::DropAllVia {
1013                            transport_hash: hash,
1014                        },
1015                    )?;
1016                    if let QueryResponse::DropAllVia(n) = resp {
1017                        Ok(PickleValue::Int(n as i64))
1018                    } else {
1019                        Ok(PickleValue::None)
1020                    }
1021                }
1022                "announce_queues" => {
1023                    let resp = send_query(event_tx, QueryRequest::DropAnnounceQueues)?;
1024                    if let QueryResponse::DropAnnounceQueues = resp {
1025                        Ok(PickleValue::Bool(true))
1026                    } else {
1027                        Ok(PickleValue::None)
1028                    }
1029                }
1030                _ => Ok(PickleValue::None),
1031            };
1032        }
1033    }
1034
1035    if let Some(hook_val) = request.get("hook").and_then(|v| v.as_str()) {
1036        return handle_hook_rpc_request(hook_val, request, event_tx);
1037    }
1038
1039    Ok(PickleValue::None)
1040}
1041
1042fn handle_hook_rpc_request(
1043    op: &str,
1044    request: &PickleValue,
1045    event_tx: &EventSender,
1046) -> io::Result<PickleValue> {
1047    match op {
1048        "load" => {
1049            let name = required_string(request, "name")?;
1050            let attach_point = required_string(request, "attach_point")?;
1051            let priority = request
1052                .get("priority")
1053                .and_then(|v| v.as_int())
1054                .unwrap_or(0) as i32;
1055            let wasm = request
1056                .get("wasm")
1057                .and_then(|v| v.as_bytes())
1058                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing wasm"))?
1059                .to_vec();
1060            let (response_tx, response_rx) = mpsc::channel();
1061            try_send_event(
1062                event_tx,
1063                Event::LoadHook {
1064                    name,
1065                    wasm_bytes: wasm,
1066                    attach_point,
1067                    priority,
1068                    response_tx,
1069                },
1070            )?;
1071            let response = response_rx
1072                .recv_timeout(std::time::Duration::from_secs(5))
1073                .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "hook load timed out"))?;
1074            Ok(hook_result_to_pickle(response))
1075        }
1076        "load_builtin" => {
1077            let name = required_string(request, "name")?;
1078            let attach_point = required_string(request, "attach_point")?;
1079            let builtin_id = required_string(request, "builtin_id")?;
1080            let priority = request
1081                .get("priority")
1082                .and_then(|v| v.as_int())
1083                .unwrap_or(0) as i32;
1084            let (response_tx, response_rx) = mpsc::channel();
1085            try_send_event(
1086                event_tx,
1087                Event::LoadBuiltinHook {
1088                    name,
1089                    builtin_id,
1090                    attach_point,
1091                    priority,
1092                    response_tx,
1093                },
1094            )?;
1095            let response = response_rx
1096                .recv_timeout(std::time::Duration::from_secs(5))
1097                .map_err(|_| {
1098                    io::Error::new(io::ErrorKind::TimedOut, "built-in hook load timed out")
1099                })?;
1100            Ok(hook_result_to_pickle(response))
1101        }
1102        "unload" => {
1103            let name = required_string(request, "name")?;
1104            let attach_point = required_string(request, "attach_point")?;
1105            let (response_tx, response_rx) = mpsc::channel();
1106            try_send_event(
1107                event_tx,
1108                Event::UnloadHook {
1109                    name,
1110                    attach_point,
1111                    response_tx,
1112                },
1113            )?;
1114            let response = response_rx
1115                .recv_timeout(std::time::Duration::from_secs(5))
1116                .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "hook unload timed out"))?;
1117            Ok(hook_result_to_pickle(response))
1118        }
1119        "enable" | "disable" => {
1120            let name = required_string(request, "name")?;
1121            let attach_point = required_string(request, "attach_point")?;
1122            let enabled = op == "enable";
1123            let (response_tx, response_rx) = mpsc::channel();
1124            try_send_event(
1125                event_tx,
1126                Event::SetHookEnabled {
1127                    name,
1128                    attach_point,
1129                    enabled,
1130                    response_tx,
1131                },
1132            )?;
1133            let response = response_rx
1134                .recv_timeout(std::time::Duration::from_secs(5))
1135                .map_err(|_| {
1136                    io::Error::new(io::ErrorKind::TimedOut, "hook enable/disable timed out")
1137                })?;
1138            Ok(hook_result_to_pickle(response))
1139        }
1140        "set_priority" => {
1141            let name = required_string(request, "name")?;
1142            let attach_point = required_string(request, "attach_point")?;
1143            let priority = request
1144                .get("priority")
1145                .and_then(|v| v.as_int())
1146                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing priority"))?
1147                as i32;
1148            let (response_tx, response_rx) = mpsc::channel();
1149            try_send_event(
1150                event_tx,
1151                Event::SetHookPriority {
1152                    name,
1153                    attach_point,
1154                    priority,
1155                    response_tx,
1156                },
1157            )?;
1158            let response = response_rx
1159                .recv_timeout(std::time::Duration::from_secs(5))
1160                .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "hook priority timed out"))?;
1161            Ok(hook_result_to_pickle(response))
1162        }
1163        _ => Ok(PickleValue::None),
1164    }
1165}
1166
1167/// Send a query to the driver and wait for the response.
1168fn send_query(event_tx: &EventSender, request: QueryRequest) -> io::Result<QueryResponse> {
1169    let (resp_tx, resp_rx) = mpsc::channel();
1170    try_send_event(event_tx, Event::query(request, resp_tx))?;
1171    resp_rx
1172        .recv_timeout(std::time::Duration::from_secs(5))
1173        .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "query timed out"))
1174}
1175
1176fn try_send_event(event_tx: &EventSender, event: Event) -> io::Result<()> {
1177    event_tx.try_send(event).map_err(|error| match error {
1178        mpsc::TrySendError::Full(_) => {
1179            io::Error::new(io::ErrorKind::WouldBlock, "driver event queue is full")
1180        }
1181        mpsc::TrySendError::Disconnected(_) => {
1182            io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down")
1183        }
1184    })
1185}
1186
1187/// Extract a 16-byte destination hash from an RPC dict field.
1188fn extract_dest_hash(request: &PickleValue, key: &str) -> io::Result<[u8; 16]> {
1189    let bytes = request
1190        .get(key)
1191        .and_then(|v| v.as_bytes())
1192        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing destination_hash"))?;
1193    if bytes.len() < 16 {
1194        return Err(io::Error::new(io::ErrorKind::InvalidData, "hash too short"));
1195    }
1196    let mut hash = [0u8; 16];
1197    hash.copy_from_slice(&bytes[..16]);
1198    Ok(hash)
1199}
1200
1201fn required_string(request: &PickleValue, key: &str) -> io::Result<String> {
1202    request
1203        .get(key)
1204        .and_then(|v| v.as_str())
1205        .map(|s| s.to_string())
1206        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("missing {}", key)))
1207}
1208
1209fn hook_result_to_pickle(result: Result<(), String>) -> PickleValue {
1210    match result {
1211        Ok(()) => PickleValue::Dict(vec![(
1212            PickleValue::String("ok".into()),
1213            PickleValue::Bool(true),
1214        )]),
1215        Err(error) => PickleValue::Dict(vec![
1216            (PickleValue::String("ok".into()), PickleValue::Bool(false)),
1217            (
1218                PickleValue::String("error".into()),
1219                PickleValue::String(error),
1220            ),
1221        ]),
1222    }
1223}
1224
1225// --- Pickle response builders ---
1226
1227fn interface_stats_to_pickle(stats: &InterfaceStatsResponse) -> PickleValue {
1228    let mut ifaces = Vec::new();
1229    for iface in &stats.interfaces {
1230        ifaces.push(single_iface_to_pickle(iface));
1231    }
1232
1233    let mut dict = vec![
1234        (
1235            PickleValue::String("interfaces".into()),
1236            PickleValue::List(ifaces),
1237        ),
1238        (
1239            PickleValue::String("transport_enabled".into()),
1240            PickleValue::Bool(stats.transport_enabled),
1241        ),
1242        (
1243            PickleValue::String("transport_uptime".into()),
1244            PickleValue::Float(stats.transport_uptime),
1245        ),
1246        (
1247            PickleValue::String("rxb".into()),
1248            PickleValue::Int(stats.total_rxb as i64),
1249        ),
1250        (
1251            PickleValue::String("txb".into()),
1252            PickleValue::Int(stats.total_txb as i64),
1253        ),
1254        (
1255            PickleValue::String("rxpps".into()),
1256            PickleValue::Float(stats.rx_pps),
1257        ),
1258        (
1259            PickleValue::String("txpps".into()),
1260            PickleValue::Float(stats.tx_pps),
1261        ),
1262        (
1263            PickleValue::String("rxs".into()),
1264            PickleValue::Float(stats.traffic.rxs),
1265        ),
1266        (
1267            PickleValue::String("txs".into()),
1268            PickleValue::Float(stats.traffic.txs),
1269        ),
1270        (
1271            PickleValue::String("arxb".into()),
1272            PickleValue::Int(stats.traffic.arxb as i64),
1273        ),
1274        (
1275            PickleValue::String("atxb".into()),
1276            PickleValue::Int(stats.traffic.atxb as i64),
1277        ),
1278        (
1279            PickleValue::String("arxc".into()),
1280            PickleValue::Int(stats.traffic.arxc as i64),
1281        ),
1282        (
1283            PickleValue::String("atxc".into()),
1284            PickleValue::Int(stats.traffic.atxc as i64),
1285        ),
1286        (
1287            PickleValue::String("arxs".into()),
1288            PickleValue::Float(stats.traffic.arxs),
1289        ),
1290        (
1291            PickleValue::String("atxs".into()),
1292            PickleValue::Float(stats.traffic.atxs),
1293        ),
1294        (
1295            PickleValue::String("prxb".into()),
1296            PickleValue::Int(stats.traffic.prxb as i64),
1297        ),
1298        (
1299            PickleValue::String("ptxb".into()),
1300            PickleValue::Int(stats.traffic.ptxb as i64),
1301        ),
1302        (
1303            PickleValue::String("prxc".into()),
1304            PickleValue::Int(stats.traffic.prxc as i64),
1305        ),
1306        (
1307            PickleValue::String("ptxc".into()),
1308            PickleValue::Int(stats.traffic.ptxc as i64),
1309        ),
1310        (
1311            PickleValue::String("prxs".into()),
1312            PickleValue::Float(stats.traffic.prxs),
1313        ),
1314        (
1315            PickleValue::String("ptxs".into()),
1316            PickleValue::Float(stats.traffic.ptxs),
1317        ),
1318        (
1319            PickleValue::String("arxf".into()),
1320            PickleValue::Float(stats.traffic.arxf),
1321        ),
1322        (
1323            PickleValue::String("atxf".into()),
1324            PickleValue::Float(stats.traffic.atxf),
1325        ),
1326        (
1327            PickleValue::String("prxf".into()),
1328            PickleValue::Float(stats.traffic.prxf),
1329        ),
1330        (
1331            PickleValue::String("ptxf".into()),
1332            PickleValue::Float(stats.traffic.ptxf),
1333        ),
1334    ];
1335
1336    if let Some(tid) = stats.transport_id {
1337        dict.push((
1338            PickleValue::String("transport_id".into()),
1339            PickleValue::Bytes(tid.to_vec()),
1340        ));
1341    } else {
1342        dict.push((
1343            PickleValue::String("transport_id".into()),
1344            PickleValue::None,
1345        ));
1346    }
1347
1348    if let Some(pr) = stats.probe_responder {
1349        dict.push((
1350            PickleValue::String("probe_responder".into()),
1351            PickleValue::Bytes(pr.to_vec()),
1352        ));
1353    } else {
1354        dict.push((
1355            PickleValue::String("probe_responder".into()),
1356            PickleValue::None,
1357        ));
1358    }
1359
1360    if let Some(pool) = &stats.backbone_peer_pool {
1361        let members = pool
1362            .members
1363            .iter()
1364            .map(|member| {
1365                let mut member_dict = vec![
1366                    (
1367                        PickleValue::String("name".into()),
1368                        PickleValue::String(member.name.clone()),
1369                    ),
1370                    (
1371                        PickleValue::String("remote".into()),
1372                        PickleValue::String(member.remote.clone()),
1373                    ),
1374                    (
1375                        PickleValue::String("source".into()),
1376                        PickleValue::String(member.source.clone()),
1377                    ),
1378                    (
1379                        PickleValue::String("priority".into()),
1380                        PickleValue::Int(member.priority as i64),
1381                    ),
1382                    (
1383                        PickleValue::String("state".into()),
1384                        PickleValue::String(member.state.clone()),
1385                    ),
1386                    (
1387                        PickleValue::String("failure_count".into()),
1388                        PickleValue::Int(member.failure_count as i64),
1389                    ),
1390                ];
1391                member_dict.push((
1392                    PickleValue::String("interface_id".into()),
1393                    member
1394                        .interface_id
1395                        .map(|id| PickleValue::Int(id as i64))
1396                        .unwrap_or(PickleValue::None),
1397                ));
1398                member_dict.push((
1399                    PickleValue::String("last_error".into()),
1400                    member
1401                        .last_error
1402                        .as_ref()
1403                        .map(|err| PickleValue::String(err.clone()))
1404                        .unwrap_or(PickleValue::None),
1405                ));
1406                member_dict.push((
1407                    PickleValue::String("cooldown_remaining_seconds".into()),
1408                    member
1409                        .cooldown_remaining_seconds
1410                        .map(PickleValue::Float)
1411                        .unwrap_or(PickleValue::None),
1412                ));
1413                PickleValue::Dict(member_dict)
1414            })
1415            .collect();
1416        dict.push((
1417            PickleValue::String("backbone_peer_pool".into()),
1418            PickleValue::Dict(vec![
1419                (
1420                    PickleValue::String("max_connected".into()),
1421                    PickleValue::Int(pool.max_connected as i64),
1422                ),
1423                (
1424                    PickleValue::String("active_count".into()),
1425                    PickleValue::Int(pool.active_count as i64),
1426                ),
1427                (
1428                    PickleValue::String("standby_count".into()),
1429                    PickleValue::Int(pool.standby_count as i64),
1430                ),
1431                (
1432                    PickleValue::String("cooldown_count".into()),
1433                    PickleValue::Int(pool.cooldown_count as i64),
1434                ),
1435                (
1436                    PickleValue::String("members".into()),
1437                    PickleValue::List(members),
1438                ),
1439            ]),
1440        ));
1441    } else {
1442        dict.push((
1443            PickleValue::String("backbone_peer_pool".into()),
1444            PickleValue::None,
1445        ));
1446    }
1447
1448    PickleValue::Dict(dict)
1449}
1450
1451fn add_inbound_runtime_stats(
1452    response: &mut PickleValue,
1453    stats: &InterfaceStatsResponse,
1454    event_tx: &EventSender,
1455) {
1456    let PickleValue::Dict(dict) = response else {
1457        return;
1458    };
1459    if let Some((_, PickleValue::List(ifaces))) = dict
1460        .iter_mut()
1461        .find(|(key, _)| key.as_str() == Some("interfaces"))
1462    {
1463        for (iface, stat) in ifaces.iter_mut().zip(&stats.interfaces) {
1464            let PickleValue::Dict(iface_dict) = iface else {
1465                continue;
1466            };
1467            let burst_stats = if stat.interface_type == "BackboneInterface" {
1468                event_tx
1469                    .dynamic_burst_stats(InterfaceId(stat.id))
1470                    .or(Some(DynamicBurstStats {
1471                        announce: BurstClassStats {
1472                            count: 0,
1473                            activated: None,
1474                        },
1475                        path_request: BurstClassStats {
1476                            count: 0,
1477                            activated: None,
1478                        },
1479                    }))
1480            } else {
1481                None
1482            };
1483            iface_dict.push((
1484                PickleValue::String("burst_count".into()),
1485                burst_stats
1486                    .map(|stats| PickleValue::Int(stats.announce.count as i64))
1487                    .unwrap_or(PickleValue::None),
1488            ));
1489            iface_dict.push((
1490                PickleValue::String("pr_burst_count".into()),
1491                burst_stats
1492                    .map(|stats| PickleValue::Int(stats.path_request.count as i64))
1493                    .unwrap_or(PickleValue::None),
1494            ));
1495            if let Some(stats) = burst_stats {
1496                set_pickle_dict_entry(
1497                    iface_dict,
1498                    "burst_active",
1499                    PickleValue::Bool(stats.announce.count > 0),
1500                );
1501                set_pickle_dict_entry(
1502                    iface_dict,
1503                    "burst_activated",
1504                    stats
1505                        .announce
1506                        .activated
1507                        .map(PickleValue::Float)
1508                        .unwrap_or(PickleValue::None),
1509                );
1510                set_pickle_dict_entry(
1511                    iface_dict,
1512                    "pr_burst_active",
1513                    PickleValue::Bool(stats.path_request.count > 0),
1514                );
1515                set_pickle_dict_entry(
1516                    iface_dict,
1517                    "pr_burst_activated",
1518                    stats
1519                        .path_request
1520                        .activated
1521                        .map(PickleValue::Float)
1522                        .unwrap_or(PickleValue::None),
1523                );
1524            }
1525        }
1526    }
1527
1528    let snapshot = event_tx.inbound_queue_snapshot();
1529    let total_height = snapshot.total_height();
1530    let total_capacity = snapshot.total_capacity();
1531    let total_dropped = snapshot.total_dropped();
1532    let pressure = |height: usize, capacity: usize| {
1533        if height == 0 {
1534            0.0
1535        } else {
1536            height as f64 / capacity as f64
1537        }
1538    };
1539    dict.extend([
1540        (
1541            PickleValue::String("rxqt".into()),
1542            PickleValue::Int(total_height as i64),
1543        ),
1544        (
1545            PickleValue::String("rxqd".into()),
1546            PickleValue::Int(snapshot.heights[0] as i64),
1547        ),
1548        (
1549            PickleValue::String("rxqa".into()),
1550            PickleValue::Int(snapshot.heights[1] as i64),
1551        ),
1552        (
1553            PickleValue::String("rxqp".into()),
1554            PickleValue::Int(snapshot.heights[2] as i64),
1555        ),
1556        (
1557            PickleValue::String("rxqil".into()),
1558            PickleValue::Int(snapshot.heights[3] as i64),
1559        ),
1560        (
1561            PickleValue::String("rxqtd".into()),
1562            PickleValue::Int(total_dropped as i64),
1563        ),
1564        (
1565            PickleValue::String("rxqdd".into()),
1566            PickleValue::Int(snapshot.dropped[0] as i64),
1567        ),
1568        (
1569            PickleValue::String("rxqad".into()),
1570            PickleValue::Int(snapshot.dropped[1] as i64),
1571        ),
1572        (
1573            PickleValue::String("rxqpd".into()),
1574            PickleValue::Int(snapshot.dropped[2] as i64),
1575        ),
1576        (
1577            PickleValue::String("rxqild".into()),
1578            PickleValue::Int(snapshot.dropped[3] as i64),
1579        ),
1580        (
1581            PickleValue::String("tqpressure".into()),
1582            PickleValue::Float(pressure(total_height, total_capacity)),
1583        ),
1584        (
1585            PickleValue::String("dqpressure".into()),
1586            PickleValue::Float(pressure(snapshot.heights[0], snapshot.capacities[0])),
1587        ),
1588        (
1589            PickleValue::String("aqpressure".into()),
1590            PickleValue::Float(pressure(snapshot.heights[1], snapshot.capacities[1])),
1591        ),
1592        (
1593            PickleValue::String("pqpressure".into()),
1594            PickleValue::Float(pressure(snapshot.heights[2], snapshot.capacities[2])),
1595        ),
1596        (
1597            PickleValue::String("ilqpressure".into()),
1598            PickleValue::Float(pressure(snapshot.heights[3], snapshot.capacities[3])),
1599        ),
1600        (PickleValue::String("txq".into()), PickleValue::None),
1601    ]);
1602}
1603
1604fn set_pickle_dict_entry(
1605    dict: &mut Vec<(PickleValue, PickleValue)>,
1606    key: &str,
1607    value: PickleValue,
1608) {
1609    if let Some((_, existing)) = dict
1610        .iter_mut()
1611        .find(|(candidate, _)| candidate.as_str() == Some(key))
1612    {
1613        *existing = value;
1614    } else {
1615        dict.push((PickleValue::String(key.into()), value));
1616    }
1617}
1618
1619fn single_iface_to_pickle(s: &SingleInterfaceStat) -> PickleValue {
1620    let mut dict = vec![
1621        (
1622            PickleValue::String("id".into()),
1623            PickleValue::Int(s.id as i64),
1624        ),
1625        (
1626            PickleValue::String("name".into()),
1627            PickleValue::String(s.name.clone()),
1628        ),
1629        (
1630            PickleValue::String("status".into()),
1631            PickleValue::Bool(s.status),
1632        ),
1633        (
1634            PickleValue::String("mode".into()),
1635            PickleValue::Int(s.mode as i64),
1636        ),
1637        (
1638            PickleValue::String("gravity".into()),
1639            PickleValue::Int(s.gravity),
1640        ),
1641        (
1642            PickleValue::String("announces_to_internal".into()),
1643            s.announces_to_internal
1644                .map(PickleValue::Bool)
1645                .unwrap_or(PickleValue::None),
1646        ),
1647        (
1648            PickleValue::String("rxb".into()),
1649            PickleValue::Int(s.rxb as i64),
1650        ),
1651        (
1652            PickleValue::String("txb".into()),
1653            PickleValue::Int(s.txb as i64),
1654        ),
1655        (
1656            PickleValue::String("txdrp".into()),
1657            PickleValue::Int(s.tx_drops as i64),
1658        ),
1659        (
1660            PickleValue::String("txdrb".into()),
1661            PickleValue::Int(s.tx_dropped_bytes as i64),
1662        ),
1663        (
1664            PickleValue::String("txstalled".into()),
1665            PickleValue::Bool(s.tx_stalled),
1666        ),
1667        (
1668            PickleValue::String("txbuffered".into()),
1669            PickleValue::Int(s.tx_buffered as i64),
1670        ),
1671        (
1672            PickleValue::String("rxs".into()),
1673            PickleValue::Float(s.traffic.rxs),
1674        ),
1675        (
1676            PickleValue::String("txs".into()),
1677            PickleValue::Float(s.traffic.txs),
1678        ),
1679        (
1680            PickleValue::String("arxb".into()),
1681            PickleValue::Int(s.traffic.arxb as i64),
1682        ),
1683        (
1684            PickleValue::String("atxb".into()),
1685            PickleValue::Int(s.traffic.atxb as i64),
1686        ),
1687        (
1688            PickleValue::String("arxc".into()),
1689            PickleValue::Int(s.traffic.arxc as i64),
1690        ),
1691        (
1692            PickleValue::String("atxc".into()),
1693            PickleValue::Int(s.traffic.atxc as i64),
1694        ),
1695        (
1696            PickleValue::String("arxs".into()),
1697            PickleValue::Float(s.traffic.arxs),
1698        ),
1699        (
1700            PickleValue::String("atxs".into()),
1701            PickleValue::Float(s.traffic.atxs),
1702        ),
1703        (
1704            PickleValue::String("prxb".into()),
1705            PickleValue::Int(s.traffic.prxb as i64),
1706        ),
1707        (
1708            PickleValue::String("ptxb".into()),
1709            PickleValue::Int(s.traffic.ptxb as i64),
1710        ),
1711        (
1712            PickleValue::String("prxc".into()),
1713            PickleValue::Int(s.traffic.prxc as i64),
1714        ),
1715        (
1716            PickleValue::String("ptxc".into()),
1717            PickleValue::Int(s.traffic.ptxc as i64),
1718        ),
1719        (
1720            PickleValue::String("prxs".into()),
1721            PickleValue::Float(s.traffic.prxs),
1722        ),
1723        (
1724            PickleValue::String("ptxs".into()),
1725            PickleValue::Float(s.traffic.ptxs),
1726        ),
1727        (
1728            PickleValue::String("protocol_violations".into()),
1729            PickleValue::Int(s.protocol_violations as i64),
1730        ),
1731        (
1732            PickleValue::String("ifac_violations".into()),
1733            PickleValue::Int(s.ifac_violations as i64),
1734        ),
1735        (
1736            PickleValue::String("packet_filter_hits".into()),
1737            PickleValue::Int(s.packet_filter_hits as i64),
1738        ),
1739        (
1740            PickleValue::String("rx_packets".into()),
1741            PickleValue::Int(s.rx_packets as i64),
1742        ),
1743        (
1744            PickleValue::String("tx_packets".into()),
1745            PickleValue::Int(s.tx_packets as i64),
1746        ),
1747        (
1748            PickleValue::String("cpu_load".into()),
1749            s.cpu_load
1750                .map(PickleValue::Float)
1751                .unwrap_or(PickleValue::None),
1752        ),
1753        (
1754            PickleValue::String("mem_load".into()),
1755            s.mem_load
1756                .map(PickleValue::Float)
1757                .unwrap_or(PickleValue::None),
1758        ),
1759        (
1760            PickleValue::String("switch_id".into()),
1761            s.switch_id
1762                .map(|v| PickleValue::Bytes(v.to_vec()))
1763                .unwrap_or(PickleValue::None),
1764        ),
1765        (
1766            PickleValue::String("endpoint_id".into()),
1767            s.endpoint_id
1768                .map(|v| PickleValue::Bytes(v.to_vec()))
1769                .unwrap_or(PickleValue::None),
1770        ),
1771        (
1772            PickleValue::String("via_switch_id".into()),
1773            s.via_switch_id
1774                .map(|v| PickleValue::Bytes(v.to_vec()))
1775                .unwrap_or(PickleValue::None),
1776        ),
1777        (
1778            PickleValue::String("peers".into()),
1779            s.peers
1780                .map(|v| PickleValue::Int(v as i64))
1781                .unwrap_or(PickleValue::None),
1782        ),
1783        (
1784            PickleValue::String("started".into()),
1785            PickleValue::Float(s.started),
1786        ),
1787        (
1788            PickleValue::String("ia_freq".into()),
1789            PickleValue::Float(s.ia_freq),
1790        ),
1791        (
1792            PickleValue::String("oa_freq".into()),
1793            PickleValue::Float(s.oa_freq),
1794        ),
1795        (
1796            PickleValue::String("ip_freq".into()),
1797            PickleValue::Float(s.ip_freq),
1798        ),
1799        (
1800            PickleValue::String("op_freq".into()),
1801            PickleValue::Float(s.op_freq),
1802        ),
1803        (
1804            PickleValue::String("burst_active".into()),
1805            PickleValue::Bool(s.burst_active),
1806        ),
1807        (
1808            PickleValue::String("burst_activated".into()),
1809            PickleValue::Float(s.burst_activated),
1810        ),
1811        (
1812            PickleValue::String("pr_burst_active".into()),
1813            PickleValue::Bool(s.pr_burst_active),
1814        ),
1815        (
1816            PickleValue::String("pr_burst_activated".into()),
1817            PickleValue::Float(s.pr_burst_activated),
1818        ),
1819        (
1820            PickleValue::String("clients".into()),
1821            s.clients
1822                .map(|clients| PickleValue::Int(clients as i64))
1823                .unwrap_or(PickleValue::None),
1824        ),
1825        (
1826            PickleValue::String("blocked_ips".into()),
1827            s.blocked_ips
1828                .map(|blocked| PickleValue::Int(blocked as i64))
1829                .unwrap_or(PickleValue::None),
1830        ),
1831        (
1832            PickleValue::String("blocked_ip_list".into()),
1833            s.blocked_ip_list
1834                .as_ref()
1835                .map(|blocked| {
1836                    PickleValue::List(blocked.iter().cloned().map(PickleValue::String).collect())
1837                })
1838                .unwrap_or(PickleValue::None),
1839        ),
1840        (
1841            PickleValue::String("announce_rate_grace".into()),
1842            PickleValue::Int(s.announce_rate_grace as i64),
1843        ),
1844        (
1845            PickleValue::String("announce_rate_penalty".into()),
1846            PickleValue::Float(s.announce_rate_penalty),
1847        ),
1848    ];
1849
1850    match s.announce_rate_target {
1851        Some(target) => dict.push((
1852            PickleValue::String("announce_rate_target".into()),
1853            PickleValue::Float(target),
1854        )),
1855        None => dict.push((
1856            PickleValue::String("announce_rate_target".into()),
1857            PickleValue::None,
1858        )),
1859    }
1860
1861    match s.bitrate {
1862        Some(br) => dict.push((
1863            PickleValue::String("bitrate".into()),
1864            PickleValue::Int(br as i64),
1865        )),
1866        None => dict.push((PickleValue::String("bitrate".into()), PickleValue::None)),
1867    }
1868
1869    match s.mtu {
1870        Some(mtu) => dict.push((
1871            PickleValue::String("mtu".into()),
1872            PickleValue::Int(mtu as i64),
1873        )),
1874        None => dict.push((PickleValue::String("mtu".into()), PickleValue::None)),
1875    }
1876
1877    match s.ifac_size {
1878        Some(sz) => dict.push((
1879            PickleValue::String("ifac_size".into()),
1880            PickleValue::Int(sz as i64),
1881        )),
1882        None => dict.push((PickleValue::String("ifac_size".into()), PickleValue::None)),
1883    }
1884
1885    PickleValue::Dict(dict)
1886}
1887
1888fn path_table_to_pickle(entries: &[PathTableEntry]) -> PickleValue {
1889    let list: Vec<PickleValue> = entries
1890        .iter()
1891        .map(|e| {
1892            PickleValue::Dict(vec![
1893                (
1894                    PickleValue::String("hash".into()),
1895                    PickleValue::Bytes(e.hash.to_vec()),
1896                ),
1897                (
1898                    PickleValue::String("timestamp".into()),
1899                    PickleValue::Float(e.timestamp),
1900                ),
1901                (
1902                    PickleValue::String("via".into()),
1903                    PickleValue::Bytes(e.via.to_vec()),
1904                ),
1905                (
1906                    PickleValue::String("hops".into()),
1907                    PickleValue::Int(e.hops as i64),
1908                ),
1909                (
1910                    PickleValue::String("expires".into()),
1911                    PickleValue::Float(e.expires),
1912                ),
1913                (
1914                    PickleValue::String("interface".into()),
1915                    PickleValue::String(e.interface_name.clone()),
1916                ),
1917            ])
1918        })
1919        .collect();
1920    PickleValue::List(list)
1921}
1922
1923fn rate_table_to_pickle(entries: &[RateTableEntry]) -> PickleValue {
1924    let list: Vec<PickleValue> = entries
1925        .iter()
1926        .map(|e| {
1927            PickleValue::Dict(vec![
1928                (
1929                    PickleValue::String("hash".into()),
1930                    PickleValue::Bytes(e.hash.to_vec()),
1931                ),
1932                (
1933                    PickleValue::String("last".into()),
1934                    PickleValue::Float(e.last),
1935                ),
1936                (
1937                    PickleValue::String("rate_violations".into()),
1938                    PickleValue::Int(e.rate_violations as i64),
1939                ),
1940                (
1941                    PickleValue::String("blocked_until".into()),
1942                    PickleValue::Float(e.blocked_until),
1943                ),
1944                (
1945                    PickleValue::String("timestamps".into()),
1946                    PickleValue::List(
1947                        e.timestamps
1948                            .iter()
1949                            .map(|&t| PickleValue::Float(t))
1950                            .collect(),
1951                    ),
1952                ),
1953            ])
1954        })
1955        .collect();
1956    PickleValue::List(list)
1957}
1958
1959fn blackholed_to_pickle(entries: &[BlackholeInfo]) -> PickleValue {
1960    let list: Vec<PickleValue> = entries
1961        .iter()
1962        .map(|e| {
1963            let mut dict = vec![
1964                (
1965                    PickleValue::String("identity_hash".into()),
1966                    PickleValue::Bytes(e.identity_hash.to_vec()),
1967                ),
1968                (
1969                    PickleValue::String("created".into()),
1970                    PickleValue::Float(e.created),
1971                ),
1972                (
1973                    PickleValue::String("expires".into()),
1974                    PickleValue::Float(e.expires),
1975                ),
1976            ];
1977            if let Some(ref reason) = e.reason {
1978                dict.push((
1979                    PickleValue::String("reason".into()),
1980                    PickleValue::String(reason.clone()),
1981                ));
1982            } else {
1983                dict.push((PickleValue::String("reason".into()), PickleValue::None));
1984            }
1985            PickleValue::Dict(dict)
1986        })
1987        .collect();
1988    PickleValue::List(list)
1989}
1990
1991fn discovered_interfaces_to_pickle(
1992    interfaces: &[crate::discovery::DiscoveredInterface],
1993) -> PickleValue {
1994    let list: Vec<PickleValue> = interfaces
1995        .iter()
1996        .map(|iface| {
1997            let mut dict = vec![
1998                (
1999                    PickleValue::String("type".into()),
2000                    PickleValue::String(iface.interface_type.clone()),
2001                ),
2002                (
2003                    PickleValue::String("impl_name".into()),
2004                    iface
2005                        .impl_name
2006                        .clone()
2007                        .map(PickleValue::String)
2008                        .unwrap_or(PickleValue::None),
2009                ),
2010                (
2011                    PickleValue::String("version".into()),
2012                    iface
2013                        .impl_version
2014                        .clone()
2015                        .map(PickleValue::String)
2016                        .unwrap_or(PickleValue::None),
2017                ),
2018                (
2019                    PickleValue::String("transport".into()),
2020                    PickleValue::Bool(iface.transport),
2021                ),
2022                (
2023                    PickleValue::String("name".into()),
2024                    PickleValue::String(iface.name.clone()),
2025                ),
2026                (
2027                    PickleValue::String("discovered".into()),
2028                    PickleValue::Float(iface.discovered),
2029                ),
2030                (
2031                    PickleValue::String("last_heard".into()),
2032                    PickleValue::Float(iface.last_heard),
2033                ),
2034                (
2035                    PickleValue::String("heard_count".into()),
2036                    PickleValue::Int(iface.heard_count as i64),
2037                ),
2038                (
2039                    PickleValue::String("status".into()),
2040                    PickleValue::String(iface.status.as_str().into()),
2041                ),
2042                (
2043                    PickleValue::String("stamp".into()),
2044                    PickleValue::Bytes(iface.stamp.clone()),
2045                ),
2046                (
2047                    PickleValue::String("value".into()),
2048                    PickleValue::Int(iface.stamp_value as i64),
2049                ),
2050                (
2051                    PickleValue::String("transport_id".into()),
2052                    PickleValue::Bytes(iface.transport_id.to_vec()),
2053                ),
2054                (
2055                    PickleValue::String("network_id".into()),
2056                    PickleValue::Bytes(iface.network_id.to_vec()),
2057                ),
2058                (
2059                    PickleValue::String("hops".into()),
2060                    PickleValue::Int(iface.hops as i64),
2061                ),
2062            ];
2063
2064            // Optional location fields
2065            if let Some(v) = iface.latitude {
2066                dict.push((
2067                    PickleValue::String("latitude".into()),
2068                    PickleValue::Float(v),
2069                ));
2070            } else {
2071                dict.push((PickleValue::String("latitude".into()), PickleValue::None));
2072            }
2073            if let Some(v) = iface.longitude {
2074                dict.push((
2075                    PickleValue::String("longitude".into()),
2076                    PickleValue::Float(v),
2077                ));
2078            } else {
2079                dict.push((PickleValue::String("longitude".into()), PickleValue::None));
2080            }
2081            if let Some(v) = iface.height {
2082                dict.push((PickleValue::String("height".into()), PickleValue::Float(v)));
2083            } else {
2084                dict.push((PickleValue::String("height".into()), PickleValue::None));
2085            }
2086            if let Some(v) = iface.operator_lxmf_address {
2087                dict.push((
2088                    PickleValue::String("operator_lxmf_address".into()),
2089                    PickleValue::String(crate::discovery::hex_encode(&v)),
2090                ));
2091            }
2092
2093            // Connection info
2094            if let Some(ref v) = iface.reachable_on {
2095                dict.push((
2096                    PickleValue::String("reachable_on".into()),
2097                    PickleValue::String(v.clone()),
2098                ));
2099            } else {
2100                dict.push((
2101                    PickleValue::String("reachable_on".into()),
2102                    PickleValue::None,
2103                ));
2104            }
2105            if let Some(v) = iface.port {
2106                dict.push((
2107                    PickleValue::String("port".into()),
2108                    PickleValue::Int(v as i64),
2109                ));
2110            } else {
2111                dict.push((PickleValue::String("port".into()), PickleValue::None));
2112            }
2113
2114            // RNode/RF specific
2115            if let Some(v) = iface.frequency {
2116                dict.push((
2117                    PickleValue::String("frequency".into()),
2118                    PickleValue::Int(v as i64),
2119                ));
2120            } else {
2121                dict.push((PickleValue::String("frequency".into()), PickleValue::None));
2122            }
2123            if let Some(v) = iface.bandwidth {
2124                dict.push((
2125                    PickleValue::String("bandwidth".into()),
2126                    PickleValue::Int(v as i64),
2127                ));
2128            } else {
2129                dict.push((PickleValue::String("bandwidth".into()), PickleValue::None));
2130            }
2131            if let Some(v) = iface.spreading_factor {
2132                dict.push((PickleValue::String("sf".into()), PickleValue::Int(v as i64)));
2133            } else {
2134                dict.push((PickleValue::String("sf".into()), PickleValue::None));
2135            }
2136            if let Some(v) = iface.coding_rate {
2137                dict.push((PickleValue::String("cr".into()), PickleValue::Int(v as i64)));
2138            } else {
2139                dict.push((PickleValue::String("cr".into()), PickleValue::None));
2140            }
2141            if let Some(ref v) = iface.modulation {
2142                dict.push((
2143                    PickleValue::String("modulation".into()),
2144                    PickleValue::String(v.clone()),
2145                ));
2146            } else {
2147                dict.push((PickleValue::String("modulation".into()), PickleValue::None));
2148            }
2149            if let Some(v) = iface.channel {
2150                dict.push((
2151                    PickleValue::String("channel".into()),
2152                    PickleValue::Int(v as i64),
2153                ));
2154            } else {
2155                dict.push((PickleValue::String("channel".into()), PickleValue::None));
2156            }
2157
2158            // IFAC info
2159            if let Some(ref v) = iface.ifac_netname {
2160                dict.push((
2161                    PickleValue::String("ifac_netname".into()),
2162                    PickleValue::String(v.clone()),
2163                ));
2164            } else {
2165                dict.push((
2166                    PickleValue::String("ifac_netname".into()),
2167                    PickleValue::None,
2168                ));
2169            }
2170            if let Some(ref v) = iface.ifac_netkey {
2171                dict.push((
2172                    PickleValue::String("ifac_netkey".into()),
2173                    PickleValue::String(v.clone()),
2174                ));
2175            } else {
2176                dict.push((PickleValue::String("ifac_netkey".into()), PickleValue::None));
2177            }
2178
2179            // Config entry
2180            if let Some(ref v) = iface.config_entry {
2181                dict.push((
2182                    PickleValue::String("config_entry".into()),
2183                    PickleValue::String(v.clone()),
2184                ));
2185            } else {
2186                dict.push((
2187                    PickleValue::String("config_entry".into()),
2188                    PickleValue::None,
2189                ));
2190            }
2191
2192            dict.push((
2193                PickleValue::String("discovery_hash".into()),
2194                PickleValue::Bytes(iface.discovery_hash.to_vec()),
2195            ));
2196
2197            PickleValue::Dict(dict)
2198        })
2199        .collect();
2200    PickleValue::List(list)
2201}
2202
2203fn hooks_to_pickle(hooks: &[HookInfo]) -> PickleValue {
2204    PickleValue::List(
2205        hooks
2206            .iter()
2207            .map(|hook| {
2208                PickleValue::Dict(vec![
2209                    (
2210                        PickleValue::String("name".into()),
2211                        PickleValue::String(hook.name.clone()),
2212                    ),
2213                    (
2214                        PickleValue::String("type".into()),
2215                        PickleValue::String(hook.hook_type.clone()),
2216                    ),
2217                    (
2218                        PickleValue::String("attach_point".into()),
2219                        PickleValue::String(hook.attach_point.clone()),
2220                    ),
2221                    (
2222                        PickleValue::String("priority".into()),
2223                        PickleValue::Int(hook.priority as i64),
2224                    ),
2225                    (
2226                        PickleValue::String("enabled".into()),
2227                        PickleValue::Bool(hook.enabled),
2228                    ),
2229                    (
2230                        PickleValue::String("consecutive_traps".into()),
2231                        PickleValue::Int(hook.consecutive_traps as i64),
2232                    ),
2233                ])
2234            })
2235            .collect(),
2236    )
2237}
2238
2239fn backbone_peer_state_to_pickle(entries: &[BackbonePeerStateEntry]) -> PickleValue {
2240    PickleValue::List(
2241        entries
2242            .iter()
2243            .map(|entry| {
2244                PickleValue::Dict(vec![
2245                    (
2246                        PickleValue::String("interface".into()),
2247                        PickleValue::String(entry.interface_name.clone()),
2248                    ),
2249                    (
2250                        PickleValue::String("ip".into()),
2251                        PickleValue::String(entry.peer_ip.to_string()),
2252                    ),
2253                    (
2254                        PickleValue::String("connected_count".into()),
2255                        PickleValue::Int(entry.connected_count as i64),
2256                    ),
2257                    (
2258                        PickleValue::String("blacklisted_remaining_secs".into()),
2259                        entry
2260                            .blacklisted_remaining_secs
2261                            .map(PickleValue::Float)
2262                            .unwrap_or(PickleValue::None),
2263                    ),
2264                    (
2265                        PickleValue::String("blacklist_reason".into()),
2266                        entry
2267                            .blacklist_reason
2268                            .as_ref()
2269                            .map(|v: &String| PickleValue::String(v.clone()))
2270                            .unwrap_or(PickleValue::None),
2271                    ),
2272                    (
2273                        PickleValue::String("reject_count".into()),
2274                        PickleValue::Int(entry.reject_count as i64),
2275                    ),
2276                ])
2277            })
2278            .collect(),
2279    )
2280}
2281
2282fn backbone_interfaces_to_pickle(entries: &[BackboneInterfaceEntry]) -> PickleValue {
2283    PickleValue::List(
2284        entries
2285            .iter()
2286            .map(|entry| {
2287                PickleValue::Dict(vec![
2288                    (
2289                        PickleValue::String("id".into()),
2290                        PickleValue::Int(entry.interface_id.0 as i64),
2291                    ),
2292                    (
2293                        PickleValue::String("name".into()),
2294                        PickleValue::String(entry.interface_name.clone()),
2295                    ),
2296                ])
2297            })
2298            .collect(),
2299    )
2300}
2301
2302fn provider_bridge_stats_to_pickle(stats: &ProviderBridgeStats) -> PickleValue {
2303    PickleValue::Dict(vec![
2304        (
2305            PickleValue::String("connected".into()),
2306            PickleValue::Bool(stats.connected),
2307        ),
2308        (
2309            PickleValue::String("consumer_count".into()),
2310            PickleValue::Int(stats.consumer_count as i64),
2311        ),
2312        (
2313            PickleValue::String("queue_max_events".into()),
2314            PickleValue::Int(stats.queue_max_events as i64),
2315        ),
2316        (
2317            PickleValue::String("queue_max_bytes".into()),
2318            PickleValue::Int(stats.queue_max_bytes as i64),
2319        ),
2320        (
2321            PickleValue::String("backlog_len".into()),
2322            PickleValue::Int(stats.backlog_len as i64),
2323        ),
2324        (
2325            PickleValue::String("backlog_bytes".into()),
2326            PickleValue::Int(stats.backlog_bytes as i64),
2327        ),
2328        (
2329            PickleValue::String("backlog_dropped_pending".into()),
2330            PickleValue::Int(stats.backlog_dropped_pending as i64),
2331        ),
2332        (
2333            PickleValue::String("backlog_dropped_total".into()),
2334            PickleValue::Int(stats.backlog_dropped_total as i64),
2335        ),
2336        (
2337            PickleValue::String("total_disconnect_count".into()),
2338            PickleValue::Int(stats.total_disconnect_count as i64),
2339        ),
2340        (
2341            PickleValue::String("consumers".into()),
2342            PickleValue::List(
2343                stats
2344                    .consumers
2345                    .iter()
2346                    .map(|consumer| {
2347                        PickleValue::Dict(vec![
2348                            (
2349                                PickleValue::String("id".into()),
2350                                PickleValue::Int(consumer.id as i64),
2351                            ),
2352                            (
2353                                PickleValue::String("connected".into()),
2354                                PickleValue::Bool(consumer.connected),
2355                            ),
2356                            (
2357                                PickleValue::String("queue_len".into()),
2358                                PickleValue::Int(consumer.queue_len as i64),
2359                            ),
2360                            (
2361                                PickleValue::String("queued_bytes".into()),
2362                                PickleValue::Int(consumer.queued_bytes as i64),
2363                            ),
2364                            (
2365                                PickleValue::String("dropped_pending".into()),
2366                                PickleValue::Int(consumer.dropped_pending as i64),
2367                            ),
2368                            (
2369                                PickleValue::String("dropped_total".into()),
2370                                PickleValue::Int(consumer.dropped_total as i64),
2371                            ),
2372                            (
2373                                PickleValue::String("queue_max_events".into()),
2374                                PickleValue::Int(consumer.queue_max_events as i64),
2375                            ),
2376                            (
2377                                PickleValue::String("queue_max_bytes".into()),
2378                                PickleValue::Int(consumer.queue_max_bytes as i64),
2379                            ),
2380                        ])
2381                    })
2382                    .collect(),
2383            ),
2384        ),
2385    ])
2386}
2387
2388fn lifecycle_state_name(state: LifecycleState) -> &'static str {
2389    match state {
2390        LifecycleState::Active => "active",
2391        LifecycleState::Draining => "draining",
2392        LifecycleState::Stopping => "stopping",
2393        LifecycleState::Stopped => "stopped",
2394    }
2395}
2396
2397fn drain_status_to_pickle(status: &DrainStatus) -> PickleValue {
2398    PickleValue::Dict(vec![
2399        (
2400            PickleValue::String("state".into()),
2401            PickleValue::String(lifecycle_state_name(status.state).into()),
2402        ),
2403        (
2404            PickleValue::String("drain_age_seconds".into()),
2405            status
2406                .drain_age_seconds
2407                .map(PickleValue::Float)
2408                .unwrap_or(PickleValue::None),
2409        ),
2410        (
2411            PickleValue::String("deadline_remaining_seconds".into()),
2412            status
2413                .deadline_remaining_seconds
2414                .map(PickleValue::Float)
2415                .unwrap_or(PickleValue::None),
2416        ),
2417        (
2418            PickleValue::String("drain_complete".into()),
2419            PickleValue::Bool(status.drain_complete),
2420        ),
2421        (
2422            PickleValue::String("interface_writer_queued_frames".into()),
2423            PickleValue::Int(status.interface_writer_queued_frames as i64),
2424        ),
2425        (
2426            PickleValue::String("provider_backlog_events".into()),
2427            PickleValue::Int(status.provider_backlog_events as i64),
2428        ),
2429        (
2430            PickleValue::String("provider_consumer_queued_events".into()),
2431            PickleValue::Int(status.provider_consumer_queued_events as i64),
2432        ),
2433        (
2434            PickleValue::String("detail".into()),
2435            status
2436                .detail
2437                .as_ref()
2438                .map(|detail| PickleValue::String(detail.clone()))
2439                .unwrap_or(PickleValue::None),
2440        ),
2441    ])
2442}
2443
2444fn runtime_config_value_to_pickle(value: &RuntimeConfigValue) -> PickleValue {
2445    match value {
2446        RuntimeConfigValue::Int(v) => PickleValue::Int(*v),
2447        RuntimeConfigValue::Float(v) => PickleValue::Float(*v),
2448        RuntimeConfigValue::Bool(v) => PickleValue::Bool(*v),
2449        RuntimeConfigValue::String(v) => PickleValue::String(v.clone()),
2450        RuntimeConfigValue::Null => PickleValue::None,
2451    }
2452}
2453
2454fn runtime_config_value_from_pickle(value: &PickleValue) -> Option<RuntimeConfigValue> {
2455    match value {
2456        PickleValue::Int(v) => Some(RuntimeConfigValue::Int(*v)),
2457        PickleValue::Float(v) => Some(RuntimeConfigValue::Float(*v)),
2458        PickleValue::Bool(v) => Some(RuntimeConfigValue::Bool(*v)),
2459        PickleValue::String(v) => Some(RuntimeConfigValue::String(v.clone())),
2460        PickleValue::None => Some(RuntimeConfigValue::Null),
2461        _ => None,
2462    }
2463}
2464
2465fn runtime_config_entry_to_pickle(entry: &RuntimeConfigEntry) -> PickleValue {
2466    PickleValue::Dict(vec![
2467        (
2468            PickleValue::String("key".into()),
2469            PickleValue::String(entry.key.clone()),
2470        ),
2471        (
2472            PickleValue::String("value".into()),
2473            runtime_config_value_to_pickle(&entry.value),
2474        ),
2475        (
2476            PickleValue::String("default".into()),
2477            runtime_config_value_to_pickle(&entry.default),
2478        ),
2479        (
2480            PickleValue::String("source".into()),
2481            PickleValue::String(match entry.source {
2482                RuntimeConfigSource::Startup => "startup".into(),
2483                RuntimeConfigSource::RuntimeOverride => "runtime_override".into(),
2484            }),
2485        ),
2486        (
2487            PickleValue::String("apply_mode".into()),
2488            PickleValue::String(match entry.apply_mode {
2489                RuntimeConfigApplyMode::Immediate => "immediate".into(),
2490                RuntimeConfigApplyMode::NewConnectionsOnly => "new_connections_only".into(),
2491                RuntimeConfigApplyMode::NextReconnect => "next_reconnect".into(),
2492                RuntimeConfigApplyMode::RestartRequired => "restart_required".into(),
2493            }),
2494        ),
2495        (
2496            PickleValue::String("description".into()),
2497            entry
2498                .description
2499                .as_ref()
2500                .map(|v| PickleValue::String(v.clone()))
2501                .unwrap_or(PickleValue::None),
2502        ),
2503    ])
2504}
2505
2506fn runtime_config_list_to_pickle(entries: &[RuntimeConfigEntry]) -> PickleValue {
2507    PickleValue::List(entries.iter().map(runtime_config_entry_to_pickle).collect())
2508}
2509
2510fn runtime_config_error_to_pickle(error: &RuntimeConfigError) -> PickleValue {
2511    PickleValue::Dict(vec![
2512        (
2513            PickleValue::String("error".into()),
2514            PickleValue::String(match error.code {
2515                RuntimeConfigErrorCode::UnknownKey => "unknown_key".into(),
2516                RuntimeConfigErrorCode::InvalidType => "invalid_type".into(),
2517                RuntimeConfigErrorCode::InvalidValue => "invalid_value".into(),
2518                RuntimeConfigErrorCode::Unsupported => "unsupported".into(),
2519                RuntimeConfigErrorCode::NotFound => "not_found".into(),
2520                RuntimeConfigErrorCode::ApplyFailed => "apply_failed".into(),
2521            }),
2522        ),
2523        (
2524            PickleValue::String("message".into()),
2525            PickleValue::String(error.message.clone()),
2526        ),
2527    ])
2528}
2529
2530fn runtime_config_result_to_pickle(
2531    result: Result<RuntimeConfigEntry, RuntimeConfigError>,
2532) -> PickleValue {
2533    match result {
2534        Ok(entry) => runtime_config_entry_to_pickle(&entry),
2535        Err(error) => runtime_config_error_to_pickle(&error),
2536    }
2537}
2538
2539fn known_destination_entry_to_pickle(entry: &KnownDestinationEntry) -> PickleValue {
2540    PickleValue::Dict(vec![
2541        (
2542            PickleValue::String("dest_hash".into()),
2543            PickleValue::Bytes(entry.dest_hash.to_vec()),
2544        ),
2545        (
2546            PickleValue::String("identity_hash".into()),
2547            PickleValue::Bytes(entry.identity_hash.to_vec()),
2548        ),
2549        (
2550            PickleValue::String("public_key".into()),
2551            PickleValue::Bytes(entry.public_key.to_vec()),
2552        ),
2553        (
2554            PickleValue::String("app_data".into()),
2555            entry
2556                .app_data
2557                .as_ref()
2558                .map(|data: &Vec<u8>| PickleValue::Bytes(data.clone()))
2559                .unwrap_or(PickleValue::None),
2560        ),
2561        (
2562            PickleValue::String("hops".into()),
2563            PickleValue::Int(entry.hops as i64),
2564        ),
2565        (
2566            PickleValue::String("received_at".into()),
2567            PickleValue::Float(entry.received_at),
2568        ),
2569        (
2570            PickleValue::String("receiving_interface".into()),
2571            PickleValue::Int(entry.receiving_interface.0 as i64),
2572        ),
2573        (
2574            PickleValue::String("was_used".into()),
2575            PickleValue::Bool(entry.was_used),
2576        ),
2577        (
2578            PickleValue::String("last_used_at".into()),
2579            entry
2580                .last_used_at
2581                .map(PickleValue::Float)
2582                .unwrap_or(PickleValue::None),
2583        ),
2584        (
2585            PickleValue::String("retained".into()),
2586            PickleValue::Bool(entry.retained),
2587        ),
2588    ])
2589}
2590
2591fn known_destinations_to_pickle(entries: &[KnownDestinationEntry]) -> PickleValue {
2592    PickleValue::List(
2593        entries
2594            .iter()
2595            .map(known_destination_entry_to_pickle)
2596            .collect(),
2597    )
2598}
2599
2600fn parse_known_destination_entry(value: &PickleValue) -> io::Result<KnownDestinationEntry> {
2601    let get_bytes = |key: &str, len: usize| -> io::Result<Vec<u8>> {
2602        let value = value.get(key).ok_or_else(|| {
2603            io::Error::new(io::ErrorKind::InvalidData, format!("missing {}", key))
2604        })?;
2605        let bytes = value.as_bytes().ok_or_else(|| {
2606            io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key))
2607        })?;
2608        if bytes.len() != len {
2609            return Err(io::Error::new(
2610                io::ErrorKind::InvalidData,
2611                format!("invalid {} length", key),
2612            ));
2613        }
2614        Ok(bytes.to_vec())
2615    };
2616    let get_int = |key: &str| -> io::Result<i64> {
2617        value
2618            .get(key)
2619            .and_then(|v| v.as_int())
2620            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key)))
2621    };
2622    let get_float = |key: &str| -> io::Result<f64> {
2623        value
2624            .get(key)
2625            .and_then(|v| v.as_float())
2626            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key)))
2627    };
2628    let get_bool = |key: &str| -> io::Result<bool> {
2629        value
2630            .get(key)
2631            .and_then(|v| v.as_bool())
2632            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key)))
2633    };
2634
2635    let mut dest_hash = [0u8; 16];
2636    dest_hash.copy_from_slice(&get_bytes("dest_hash", 16)?);
2637    let mut identity_hash = [0u8; 16];
2638    identity_hash.copy_from_slice(&get_bytes("identity_hash", 16)?);
2639    let mut public_key = [0u8; 64];
2640    public_key.copy_from_slice(&get_bytes("public_key", 64)?);
2641    let app_data = value
2642        .get("app_data")
2643        .and_then(|v| v.as_bytes())
2644        .map(|bytes| bytes.to_vec());
2645    let last_used_at = value.get("last_used_at").and_then(|v| v.as_float());
2646
2647    Ok(KnownDestinationEntry {
2648        dest_hash,
2649        identity_hash,
2650        public_key,
2651        app_data,
2652        hops: get_int("hops")? as u8,
2653        received_at: get_float("received_at")?,
2654        receiving_interface: rns_core::transport::types::InterfaceId(
2655            get_int("receiving_interface")? as u64,
2656        ),
2657        was_used: get_bool("was_used")?,
2658        last_used_at,
2659        retained: get_bool("retained")?,
2660    })
2661}
2662
2663fn parse_known_destination_list(value: &PickleValue) -> io::Result<Vec<KnownDestinationEntry>> {
2664    let list = value
2665        .as_list()
2666        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "expected list"))?;
2667    list.iter().map(parse_known_destination_entry).collect()
2668}
2669
2670// --- RPC Client ---
2671
2672/// RPC client for connecting to a running daemon.
2673pub struct RpcClient {
2674    stream: TcpStream,
2675}
2676
2677impl RpcClient {
2678    /// Connect to an RPC server and authenticate.
2679    pub fn connect(addr: &RpcAddr, auth_key: &[u8; 32]) -> io::Result<Self> {
2680        let mut stream = match addr {
2681            RpcAddr::Tcp(host, port) => TcpStream::connect((host.as_str(), *port))?,
2682        };
2683
2684        stream.set_read_timeout(Some(std::time::Duration::from_secs(10)))?;
2685        stream.set_write_timeout(Some(std::time::Duration::from_secs(10)))?;
2686
2687        // Client-side authentication
2688        client_auth(&mut stream, auth_key)?;
2689
2690        Ok(RpcClient { stream })
2691    }
2692
2693    /// Send an RPC request and receive a response.
2694    pub fn call(&mut self, request: &PickleValue) -> io::Result<PickleValue> {
2695        let request_bytes = encode_rpc_payload(request, RpcCodec::Msgpack);
2696        send_bytes(&mut self.stream, &request_bytes)?;
2697
2698        let response_bytes = recv_bytes(&mut self.stream)?;
2699        decode_rpc_payload(&response_bytes).map(|(value, _)| value)
2700    }
2701
2702    pub fn list_hooks(&mut self) -> io::Result<Vec<HookInfo>> {
2703        let response = self.call(&PickleValue::Dict(vec![(
2704            PickleValue::String("get".into()),
2705            PickleValue::String("hooks".into()),
2706        )]))?;
2707        parse_hook_list(&response)
2708    }
2709
2710    pub fn begin_drain(&mut self, timeout: Duration) -> io::Result<bool> {
2711        let response = self.call(&PickleValue::Dict(vec![(
2712            PickleValue::String("begin_drain".into()),
2713            PickleValue::Float(timeout.as_secs_f64()),
2714        )]))?;
2715        Ok(response.as_bool().unwrap_or(false))
2716    }
2717
2718    pub fn drain_status(&mut self) -> io::Result<Option<DrainStatus>> {
2719        let response = self.call(&PickleValue::Dict(vec![(
2720            PickleValue::String("get".into()),
2721            PickleValue::String("drain_status".into()),
2722        )]))?;
2723        parse_drain_status(&response)
2724    }
2725
2726    pub fn provider_bridge_stats(&mut self) -> io::Result<PickleValue> {
2727        self.call(&PickleValue::Dict(vec![(
2728            PickleValue::String("get".into()),
2729            PickleValue::String("provider_bridge_stats".into()),
2730        )]))
2731    }
2732
2733    pub fn load_hook(
2734        &mut self,
2735        name: &str,
2736        attach_point: &str,
2737        priority: i32,
2738        wasm: &[u8],
2739    ) -> io::Result<Result<(), String>> {
2740        let response = self.call(&PickleValue::Dict(vec![
2741            (
2742                PickleValue::String("hook".into()),
2743                PickleValue::String("load".into()),
2744            ),
2745            (
2746                PickleValue::String("name".into()),
2747                PickleValue::String(name.to_string()),
2748            ),
2749            (
2750                PickleValue::String("attach_point".into()),
2751                PickleValue::String(attach_point.to_string()),
2752            ),
2753            (
2754                PickleValue::String("priority".into()),
2755                PickleValue::Int(priority as i64),
2756            ),
2757            (
2758                PickleValue::String("wasm".into()),
2759                PickleValue::Bytes(wasm.to_vec()),
2760            ),
2761        ]))?;
2762        parse_hook_result(&response)
2763    }
2764
2765    pub fn load_builtin_hook(
2766        &mut self,
2767        name: &str,
2768        attach_point: &str,
2769        priority: i32,
2770        builtin_id: &str,
2771    ) -> io::Result<Result<(), String>> {
2772        let response = self.call(&PickleValue::Dict(vec![
2773            (
2774                PickleValue::String("hook".into()),
2775                PickleValue::String("load_builtin".into()),
2776            ),
2777            (
2778                PickleValue::String("name".into()),
2779                PickleValue::String(name.to_string()),
2780            ),
2781            (
2782                PickleValue::String("attach_point".into()),
2783                PickleValue::String(attach_point.to_string()),
2784            ),
2785            (
2786                PickleValue::String("priority".into()),
2787                PickleValue::Int(priority as i64),
2788            ),
2789            (
2790                PickleValue::String("builtin_id".into()),
2791                PickleValue::String(builtin_id.to_string()),
2792            ),
2793        ]))?;
2794        parse_hook_result(&response)
2795    }
2796
2797    pub fn unload_hook(
2798        &mut self,
2799        name: &str,
2800        attach_point: &str,
2801    ) -> io::Result<Result<(), String>> {
2802        let response = self.call(&PickleValue::Dict(vec![
2803            (
2804                PickleValue::String("hook".into()),
2805                PickleValue::String("unload".into()),
2806            ),
2807            (
2808                PickleValue::String("name".into()),
2809                PickleValue::String(name.to_string()),
2810            ),
2811            (
2812                PickleValue::String("attach_point".into()),
2813                PickleValue::String(attach_point.to_string()),
2814            ),
2815        ]))?;
2816        parse_hook_result(&response)
2817    }
2818
2819    pub fn set_hook_enabled(
2820        &mut self,
2821        name: &str,
2822        attach_point: &str,
2823        enabled: bool,
2824    ) -> io::Result<Result<(), String>> {
2825        let op = if enabled { "enable" } else { "disable" };
2826        let response = self.call(&PickleValue::Dict(vec![
2827            (
2828                PickleValue::String("hook".into()),
2829                PickleValue::String(op.into()),
2830            ),
2831            (
2832                PickleValue::String("name".into()),
2833                PickleValue::String(name.to_string()),
2834            ),
2835            (
2836                PickleValue::String("attach_point".into()),
2837                PickleValue::String(attach_point.to_string()),
2838            ),
2839        ]))?;
2840        parse_hook_result(&response)
2841    }
2842
2843    pub fn set_hook_priority(
2844        &mut self,
2845        name: &str,
2846        attach_point: &str,
2847        priority: i32,
2848    ) -> io::Result<Result<(), String>> {
2849        let response = self.call(&PickleValue::Dict(vec![
2850            (
2851                PickleValue::String("hook".into()),
2852                PickleValue::String("set_priority".into()),
2853            ),
2854            (
2855                PickleValue::String("name".into()),
2856                PickleValue::String(name.to_string()),
2857            ),
2858            (
2859                PickleValue::String("attach_point".into()),
2860                PickleValue::String(attach_point.to_string()),
2861            ),
2862            (
2863                PickleValue::String("priority".into()),
2864                PickleValue::Int(priority as i64),
2865            ),
2866        ]))?;
2867        parse_hook_result(&response)
2868    }
2869
2870    pub fn blacklist_backbone_peer(
2871        &mut self,
2872        interface: &str,
2873        ip: &str,
2874        duration_secs: u64,
2875        reason: Option<&str>,
2876        penalty_level: Option<u8>,
2877    ) -> io::Result<bool> {
2878        let mut request = vec![
2879            (
2880                PickleValue::String("set".into()),
2881                PickleValue::String("backbone_peer_blacklist".into()),
2882            ),
2883            (
2884                PickleValue::String("interface".into()),
2885                PickleValue::String(interface.to_string()),
2886            ),
2887            (
2888                PickleValue::String("ip".into()),
2889                PickleValue::String(ip.to_string()),
2890            ),
2891            (
2892                PickleValue::String("duration_secs".into()),
2893                PickleValue::Int(duration_secs as i64),
2894            ),
2895        ];
2896        if let Some(reason) = reason {
2897            request.push((
2898                PickleValue::String("reason".into()),
2899                PickleValue::String(reason.to_string()),
2900            ));
2901        }
2902        if let Some(level) = penalty_level {
2903            request.push((
2904                PickleValue::String("penalty_level".into()),
2905                PickleValue::Int(level as i64),
2906            ));
2907        }
2908        let response = self.call(&PickleValue::Dict(request))?;
2909        Ok(response.as_bool().unwrap_or(false))
2910    }
2911
2912    pub fn known_destinations(&mut self) -> io::Result<Vec<KnownDestinationEntry>> {
2913        let response = self.call(&PickleValue::Dict(vec![(
2914            PickleValue::String("get".into()),
2915            PickleValue::String("known_destinations".into()),
2916        )]))?;
2917        parse_known_destination_list(&response)
2918    }
2919
2920    pub fn retain_known_destination(&mut self, dest_hash: [u8; 16]) -> io::Result<bool> {
2921        let response = self.call(&PickleValue::Dict(vec![
2922            (
2923                PickleValue::String("set".into()),
2924                PickleValue::String("known_destination_retained".into()),
2925            ),
2926            (
2927                PickleValue::String("dest_hash".into()),
2928                PickleValue::Bytes(dest_hash.to_vec()),
2929            ),
2930        ]))?;
2931        Ok(response.as_bool().unwrap_or(false))
2932    }
2933
2934    pub fn retain_identity(&mut self, identity_hash: [u8; 16]) -> io::Result<bool> {
2935        let response = self.call(&PickleValue::Dict(vec![
2936            (
2937                PickleValue::String("set".into()),
2938                PickleValue::String("identity_retained".into()),
2939            ),
2940            (
2941                PickleValue::String("identity_hash".into()),
2942                PickleValue::Bytes(identity_hash.to_vec()),
2943            ),
2944        ]))?;
2945        Ok(response.as_bool().unwrap_or(false))
2946    }
2947
2948    pub fn unretain_known_destination(&mut self, dest_hash: [u8; 16]) -> io::Result<bool> {
2949        let response = self.call(&PickleValue::Dict(vec![
2950            (
2951                PickleValue::String("clear".into()),
2952                PickleValue::String("known_destination_retained".into()),
2953            ),
2954            (
2955                PickleValue::String("dest_hash".into()),
2956                PickleValue::Bytes(dest_hash.to_vec()),
2957            ),
2958        ]))?;
2959        Ok(response.as_bool().unwrap_or(false))
2960    }
2961
2962    pub fn mark_known_destination_used(&mut self, dest_hash: [u8; 16]) -> io::Result<bool> {
2963        let response = self.call(&PickleValue::Dict(vec![
2964            (
2965                PickleValue::String("set".into()),
2966                PickleValue::String("known_destination_used".into()),
2967            ),
2968            (
2969                PickleValue::String("dest_hash".into()),
2970                PickleValue::Bytes(dest_hash.to_vec()),
2971            ),
2972        ]))?;
2973        Ok(response.as_bool().unwrap_or(false))
2974    }
2975}
2976
2977fn parse_lifecycle_state(value: &str) -> Option<LifecycleState> {
2978    match value {
2979        "active" => Some(LifecycleState::Active),
2980        "draining" => Some(LifecycleState::Draining),
2981        "stopping" => Some(LifecycleState::Stopping),
2982        "stopped" => Some(LifecycleState::Stopped),
2983        _ => None,
2984    }
2985}
2986
2987fn parse_drain_status(value: &PickleValue) -> io::Result<Option<DrainStatus>> {
2988    if !matches!(value, PickleValue::Dict(_)) {
2989        return Ok(None);
2990    }
2991    let state = value
2992        .get("state")
2993        .and_then(|entry| entry.as_str())
2994        .and_then(parse_lifecycle_state)
2995        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing drain state"))?;
2996    let drain_age_seconds = value.get("drain_age_seconds").and_then(|entry| {
2997        entry
2998            .as_float()
2999            .or_else(|| entry.as_int().map(|v| v as f64))
3000    });
3001    let deadline_remaining_seconds = value.get("deadline_remaining_seconds").and_then(|entry| {
3002        entry
3003            .as_float()
3004            .or_else(|| entry.as_int().map(|v| v as f64))
3005    });
3006    let drain_complete = value
3007        .get("drain_complete")
3008        .and_then(|entry| entry.as_bool())
3009        .unwrap_or(false);
3010    let interface_writer_queued_frames = value
3011        .get("interface_writer_queued_frames")
3012        .and_then(|entry| entry.as_int())
3013        .unwrap_or(0)
3014        .max(0) as usize;
3015    let provider_backlog_events = value
3016        .get("provider_backlog_events")
3017        .and_then(|entry| entry.as_int())
3018        .unwrap_or(0)
3019        .max(0) as usize;
3020    let provider_consumer_queued_events = value
3021        .get("provider_consumer_queued_events")
3022        .and_then(|entry| entry.as_int())
3023        .unwrap_or(0)
3024        .max(0) as usize;
3025    let detail = value
3026        .get("detail")
3027        .and_then(|entry| entry.as_str().map(|v| v.to_string()));
3028    Ok(Some(DrainStatus {
3029        state,
3030        drain_age_seconds,
3031        deadline_remaining_seconds,
3032        drain_complete,
3033        interface_writer_queued_frames,
3034        provider_backlog_events,
3035        provider_consumer_queued_events,
3036        detail,
3037    }))
3038}
3039
3040fn parse_hook_result(response: &PickleValue) -> io::Result<Result<(), String>> {
3041    let ok = response
3042        .get("ok")
3043        .and_then(|v| v.as_bool())
3044        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid hook response"))?;
3045    if ok {
3046        Ok(Ok(()))
3047    } else {
3048        Ok(Err(response
3049            .get("error")
3050            .and_then(|v| v.as_str())
3051            .unwrap_or("unknown hook error")
3052            .to_string()))
3053    }
3054}
3055
3056fn parse_hook_list(response: &PickleValue) -> io::Result<Vec<HookInfo>> {
3057    let list = response
3058        .as_list()
3059        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid hooks response"))?;
3060    let mut hooks = Vec::with_capacity(list.len());
3061    for item in list {
3062        hooks.push(HookInfo {
3063            name: item
3064                .get("name")
3065                .and_then(|v| v.as_str())
3066                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing hook name"))?
3067                .to_string(),
3068            hook_type: item
3069                .get("type")
3070                .and_then(|v| v.as_str())
3071                .unwrap_or(default_hook_type())
3072                .to_string(),
3073            attach_point: item
3074                .get("attach_point")
3075                .and_then(|v| v.as_str())
3076                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing attach_point"))?
3077                .to_string(),
3078            priority: item
3079                .get("priority")
3080                .and_then(|v| v.as_int())
3081                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing priority"))?
3082                as i32,
3083            enabled: item
3084                .get("enabled")
3085                .and_then(|v| v.as_bool())
3086                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing enabled"))?,
3087            consecutive_traps: item
3088                .get("consecutive_traps")
3089                .and_then(|v| v.as_int())
3090                .ok_or_else(|| {
3091                    io::Error::new(io::ErrorKind::InvalidData, "missing consecutive_traps")
3092                })? as u32,
3093        });
3094    }
3095    Ok(hooks)
3096}
3097
3098fn default_hook_type() -> &'static str {
3099    #[cfg(feature = "rns-hooks-native")]
3100    {
3101        "native"
3102    }
3103    #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
3104    {
3105        return "wasm";
3106    }
3107    #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
3108    {
3109        "wasm"
3110    }
3111}
3112
3113/// Client-side authentication: answer the server's challenge.
3114fn client_auth(stream: &mut TcpStream, auth_key: &[u8; 32]) -> io::Result<()> {
3115    answer_challenge(stream, auth_key)?;
3116    server_auth(stream, auth_key)
3117}
3118
3119fn answer_challenge<S: Read + Write>(stream: &mut S, auth_key: &[u8; 32]) -> io::Result<()> {
3120    // Read challenge
3121    let challenge = recv_bytes(stream)?;
3122
3123    if !challenge.starts_with(CHALLENGE_PREFIX) {
3124        return Err(io::Error::new(
3125            io::ErrorKind::InvalidData,
3126            "expected challenge",
3127        ));
3128    }
3129
3130    let message = &challenge[CHALLENGE_PREFIX.len()..];
3131
3132    // Create HMAC response
3133    let response = create_response(auth_key, message);
3134    send_bytes(stream, &response)?;
3135
3136    // Read welcome/failure
3137    let result = recv_bytes(stream)?;
3138    if result == WELCOME {
3139        Ok(())
3140    } else {
3141        Err(io::Error::new(
3142            io::ErrorKind::PermissionDenied,
3143            "authentication failed",
3144        ))
3145    }
3146}
3147
3148/// Create an HMAC response to a challenge message.
3149fn create_response(auth_key: &[u8; 32], message: &[u8]) -> Vec<u8> {
3150    // Check if message has {sha256} prefix (modern protocol)
3151    if message.starts_with(b"{sha256}") || message.len() > 20 {
3152        // Modern protocol: use HMAC-SHA256 with {sha256} prefix
3153        let digest = hmac_sha256(auth_key, message);
3154        let mut response = Vec::with_capacity(8 + 32);
3155        response.extend_from_slice(b"{sha256}");
3156        response.extend_from_slice(&digest);
3157        response
3158    } else {
3159        // Legacy protocol: raw HMAC-MD5
3160        let digest = hmac_md5(auth_key, message);
3161        digest.to_vec()
3162    }
3163}
3164
3165/// Derive the RPC auth key from transport identity private key.
3166pub fn derive_auth_key(private_key: &[u8]) -> [u8; 32] {
3167    sha256(private_key)
3168}
3169
3170#[cfg(test)]
3171mod tests {
3172    use super::*;
3173
3174    #[test]
3175    fn discovered_interface_rpc_includes_operator_lxmf_address() {
3176        let interface = crate::discovery::DiscoveredInterface {
3177            interface_type: "BackboneInterface".into(),
3178            impl_name: Some("Reticulum".into()),
3179            impl_version: Some("1.5.5".into()),
3180            transport: true,
3181            name: "Operator Test".into(),
3182            discovered: 1.0,
3183            last_heard: 2.0,
3184            heard_count: 1,
3185            status: crate::discovery::DiscoveredStatus::Available,
3186            stamp: vec![0; crate::discovery::STAMP_SIZE],
3187            stamp_value: 16,
3188            transport_id: [0x11; 16],
3189            network_id: [0x22; 16],
3190            hops: 1,
3191            latitude: None,
3192            longitude: None,
3193            height: None,
3194            operator_lxmf_address: Some([0xa5; 16]),
3195            reachable_on: Some("example.com".into()),
3196            port: Some(4242),
3197            frequency: None,
3198            bandwidth: None,
3199            spreading_factor: None,
3200            coding_rate: None,
3201            modulation: None,
3202            channel: None,
3203            ifac_netname: None,
3204            ifac_netkey: None,
3205            config_entry: None,
3206            discovery_hash: [0x33; 32],
3207        };
3208
3209        let encoded = discovered_interfaces_to_pickle(&[interface]);
3210        let first = &encoded.as_list().unwrap()[0];
3211
3212        assert_eq!(
3213            first.get("impl_name").and_then(PickleValue::as_str),
3214            Some("Reticulum")
3215        );
3216        assert_eq!(
3217            first.get("version").and_then(PickleValue::as_str),
3218            Some("1.5.5")
3219        );
3220
3221        assert_eq!(
3222            first
3223                .get("operator_lxmf_address")
3224                .and_then(PickleValue::as_str),
3225            Some("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5")
3226        );
3227    }
3228
3229    #[test]
3230    fn send_recv_bytes_roundtrip() {
3231        let (mut c1, mut c2) = tcp_pair();
3232        let data = b"hello world";
3233        send_bytes(&mut c1, data).unwrap();
3234        let received = recv_bytes(&mut c2).unwrap();
3235        assert_eq!(&received, data);
3236    }
3237
3238    #[test]
3239    fn send_recv_empty() {
3240        let (mut c1, mut c2) = tcp_pair();
3241        send_bytes(&mut c1, b"").unwrap();
3242        let received = recv_bytes(&mut c2).unwrap();
3243        assert!(received.is_empty());
3244    }
3245
3246    #[test]
3247    fn auth_success() {
3248        let key = derive_auth_key(b"test-private-key");
3249        let (mut server, mut client) = tcp_pair();
3250
3251        let key2 = key;
3252        let t = thread::spawn(move || {
3253            client_auth(&mut client, &key2).unwrap();
3254        });
3255
3256        server_auth(&mut server, &key).unwrap();
3257        answer_challenge(&mut server, &key).unwrap();
3258        t.join().unwrap();
3259    }
3260
3261    #[test]
3262    fn auth_failure_wrong_key() {
3263        let server_key = derive_auth_key(b"server-key");
3264        let client_key = derive_auth_key(b"wrong-key");
3265        let (mut server, mut client) = tcp_pair();
3266
3267        let t = thread::spawn(move || {
3268            let result = client_auth(&mut client, &client_key);
3269            assert!(result.is_err());
3270        });
3271
3272        let result = server_auth(&mut server, &server_key);
3273        assert!(result.is_err());
3274        t.join().unwrap();
3275    }
3276
3277    #[test]
3278    fn verify_sha256_response() {
3279        let key = derive_auth_key(b"mykey");
3280        let message = b"{sha256}abcdefghijklmnopqrstuvwxyz0123456789ABCD";
3281        let response = create_response(&key, message);
3282        assert!(response.starts_with(b"{sha256}"));
3283        assert!(verify_response(&key, message, &response));
3284    }
3285
3286    #[test]
3287    fn verify_legacy_md5_response() {
3288        let key = derive_auth_key(b"mykey");
3289        // Legacy message: 20 bytes, no prefix
3290        let message = b"01234567890123456789";
3291        // Create legacy response (raw HMAC-MD5)
3292        let digest = hmac_md5(&key, message);
3293        assert!(verify_response(&key, message, &digest));
3294    }
3295
3296    #[test]
3297    fn constant_time_eq_works() {
3298        assert!(constant_time_eq(b"hello", b"hello"));
3299        assert!(!constant_time_eq(b"hello", b"world"));
3300        assert!(!constant_time_eq(b"hello", b"hell"));
3301    }
3302
3303    #[test]
3304    fn rpc_payload_accepts_msgpack_and_legacy_pickle() {
3305        let request = PickleValue::Dict(vec![
3306            (
3307                PickleValue::String("get".into()),
3308                PickleValue::String("link_count".into()),
3309            ),
3310            (PickleValue::String("max_hops".into()), PickleValue::Int(3)),
3311        ]);
3312
3313        let msgpack_bytes = encode_rpc_payload(&request, RpcCodec::Msgpack);
3314        let (decoded_msgpack, msgpack_codec) = decode_rpc_payload(&msgpack_bytes).unwrap();
3315        assert_eq!(decoded_msgpack, request);
3316        assert_eq!(msgpack_codec, RpcCodec::Msgpack);
3317
3318        let pickle_bytes = encode_rpc_payload(&request, RpcCodec::Pickle);
3319        let (decoded_pickle, pickle_codec) = decode_rpc_payload(&pickle_bytes).unwrap();
3320        assert_eq!(decoded_pickle, request);
3321        assert_eq!(pickle_codec, RpcCodec::Pickle);
3322    }
3323
3324    #[test]
3325    fn rpc_roundtrip() {
3326        let key = derive_auth_key(b"test-key");
3327        let (event_tx, event_rx) = crate::event::channel();
3328
3329        // Start server
3330        // Bind manually to get the actual port
3331        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3332        let port = listener.local_addr().unwrap().port();
3333        listener.set_nonblocking(true).unwrap();
3334
3335        let shutdown = Arc::new(AtomicBool::new(false));
3336        let shutdown2 = shutdown.clone();
3337
3338        // Driver thread that handles queries
3339        let driver_thread = thread::spawn(move || loop {
3340            match event_rx.recv_timeout(std::time::Duration::from_secs(5)) {
3341                Ok(Event::Query(request, resp_tx))
3342                    if matches!(*request, QueryRequest::LinkCount) =>
3343                {
3344                    let _ = resp_tx.send(QueryResponse::LinkCount(42));
3345                }
3346                Ok(Event::Query(request, resp_tx))
3347                    if matches!(*request, QueryRequest::ActiveLinkCount) =>
3348                {
3349                    let _ = resp_tx.send(QueryResponse::ActiveLinkCount(7));
3350                }
3351                Ok(Event::Query(request, resp_tx))
3352                    if matches!(*request, QueryRequest::LowestInterfaceBitrate) =>
3353                {
3354                    let _ = resp_tx.send(QueryResponse::LowestInterfaceBitrate(Some(400)));
3355                }
3356                Ok(Event::Query(request, resp_tx))
3357                    if matches!(*request, QueryRequest::MediumPathTimeout) =>
3358                {
3359                    let _ = resp_tx.send(QueryResponse::MediumPathTimeout(26.0));
3360                }
3361                Ok(Event::Query(request, resp_tx))
3362                    if matches!(*request, QueryRequest::FirstHopTimeout { .. }) =>
3363                {
3364                    let QueryRequest::FirstHopTimeout { dest_hash } = *request else {
3365                        unreachable!()
3366                    };
3367                    assert_eq!(dest_hash, [0x77; 16]);
3368                    let _ = resp_tx.send(QueryResponse::FirstHopTimeout(6.4));
3369                }
3370                Ok(Event::Query(request, resp_tx))
3371                    if matches!(*request, QueryRequest::InterfaceStats) =>
3372                {
3373                    let _ = resp_tx.send(QueryResponse::InterfaceStats(InterfaceStatsResponse {
3374                        interfaces: vec![SingleInterfaceStat {
3375                            id: 7,
3376                            name: "TestInterface".into(),
3377                            status: true,
3378                            mode: 1,
3379                            gravity: -2,
3380                            announces_to_internal: Some(true),
3381                            rxb: 1000,
3382                            txb: 2000,
3383                            tx_drops: 0,
3384                            tx_dropped_bytes: 0,
3385                            tx_stalled: false,
3386                            tx_buffered: 0,
3387                            traffic: TrafficDetail::default(),
3388                            protocol_violations: 0,
3389                            ifac_violations: 0,
3390                            packet_filter_hits: 0,
3391                            rx_packets: 10,
3392                            tx_packets: 20,
3393                            cpu_load: None,
3394                            mem_load: None,
3395                            switch_id: None,
3396                            endpoint_id: None,
3397                            via_switch_id: None,
3398                            peers: None,
3399                            bitrate: Some(10_000_000),
3400                            mtu: Some(262_144),
3401                            ifac_size: None,
3402                            started: 1000.0,
3403                            ia_freq: 0.0,
3404                            ip_freq: 0.0,
3405                            op_freq: 0.0,
3406                            op_samples: 0,
3407                            burst_active: false,
3408                            burst_activated: 0.0,
3409                            pr_burst_active: false,
3410                            pr_burst_activated: 0.0,
3411                            oa_freq: 0.0,
3412                            clients: Some(2),
3413                            blocked_ips: None,
3414                            blocked_ip_list: None,
3415                            announce_rate_target: Some(3600.0),
3416                            announce_rate_grace: 5,
3417                            announce_rate_penalty: 0.0,
3418                            interface_type: "TestInterface".into(),
3419                        }],
3420                        transport_id: None,
3421                        transport_enabled: true,
3422                        transport_uptime: 3600.0,
3423                        total_rxb: 1000,
3424                        total_txb: 2000,
3425                        rx_pps: 3.0,
3426                        tx_pps: 4.0,
3427                        traffic: TrafficDetail::default(),
3428                        probe_responder: None,
3429                        backbone_peer_pool: None,
3430                    }));
3431                }
3432                _ => break,
3433            }
3434        });
3435
3436        let key2 = key;
3437        let shutdown3 = shutdown2.clone();
3438        let server_thread = thread::spawn(move || {
3439            rpc_server_loop(listener, key2, event_tx, shutdown3);
3440        });
3441
3442        // Give server time to start
3443        thread::sleep(std::time::Duration::from_millis(50));
3444
3445        // Client: connect and query link count
3446        let server_addr = RpcAddr::Tcp("127.0.0.1".into(), port);
3447        let mut client = RpcClient::connect(&server_addr, &key).unwrap();
3448        let response = client
3449            .call(&PickleValue::Dict(vec![(
3450                PickleValue::String("get".into()),
3451                PickleValue::String("link_count".into()),
3452            )]))
3453            .unwrap();
3454        assert_eq!(response.as_int().unwrap(), 42);
3455        drop(client);
3456
3457        let mut active_client = RpcClient::connect(&server_addr, &key).unwrap();
3458        let active_response = active_client
3459            .call(&PickleValue::Dict(vec![(
3460                PickleValue::String("get".into()),
3461                PickleValue::String("active_link_count".into()),
3462            )]))
3463            .unwrap();
3464        assert_eq!(active_response.as_int().unwrap(), 7);
3465        drop(active_client);
3466
3467        let mut timeout_client = RpcClient::connect(&server_addr, &key).unwrap();
3468        let bitrate = timeout_client
3469            .call(&PickleValue::Dict(vec![(
3470                PickleValue::String("get".into()),
3471                PickleValue::String("lowest_interface_bitrate".into()),
3472            )]))
3473            .unwrap();
3474        assert_eq!(bitrate.as_int(), Some(400));
3475        drop(timeout_client);
3476        let mut timeout_client = RpcClient::connect(&server_addr, &key).unwrap();
3477        let timeout = timeout_client
3478            .call(&PickleValue::Dict(vec![(
3479                PickleValue::String("get".into()),
3480                PickleValue::String("medium_path_timeout".into()),
3481            )]))
3482            .unwrap();
3483        assert_eq!(timeout.as_float(), Some(26.0));
3484        drop(timeout_client);
3485        let mut timeout_client = RpcClient::connect(&server_addr, &key).unwrap();
3486        let first_hop = timeout_client
3487            .call(&PickleValue::Dict(vec![
3488                (
3489                    PickleValue::String("get".into()),
3490                    PickleValue::String("first_hop_timeout".into()),
3491                ),
3492                (
3493                    PickleValue::String("destination_hash".into()),
3494                    PickleValue::Bytes(vec![0x77; 16]),
3495                ),
3496            ]))
3497            .unwrap();
3498        assert_eq!(first_hop.as_float(), Some(6.4));
3499        drop(timeout_client);
3500
3501        // Client: query interface stats
3502        let mut client2 = RpcClient::connect(&server_addr, &key).unwrap();
3503        let response2 = client2
3504            .call(&PickleValue::Dict(vec![(
3505                PickleValue::String("get".into()),
3506                PickleValue::String("interface_stats".into()),
3507            )]))
3508            .unwrap();
3509        let ifaces = response2.get("interfaces").unwrap().as_list().unwrap();
3510        assert_eq!(ifaces.len(), 1);
3511        let iface = &ifaces[0];
3512        assert_eq!(
3513            iface.get("name").unwrap().as_str().unwrap(),
3514            "TestInterface"
3515        );
3516        assert_eq!(iface.get("rxb").unwrap().as_int().unwrap(), 1000);
3517        assert_eq!(iface.get("mtu").unwrap().as_int(), Some(262_144));
3518        drop(client2);
3519
3520        // Shutdown
3521        shutdown2.store(true, Ordering::Relaxed);
3522        server_thread.join().unwrap();
3523        driver_thread.join().unwrap();
3524    }
3525
3526    #[test]
3527    fn full_event_queue_fails_rpc_query_without_blocking() {
3528        let (event_tx, event_rx) = crate::event::channel_with_capacity(1);
3529        event_tx.send(Event::Tick).unwrap();
3530
3531        let (result_tx, result_rx) = mpsc::channel();
3532        let query_thread = thread::spawn(move || {
3533            let _ = result_tx.send(send_query(&event_tx, QueryRequest::LinkCount));
3534        });
3535
3536        let completed_promptly = match result_rx.recv_timeout(Duration::from_millis(250)) {
3537            Ok(result) => {
3538                assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
3539                true
3540            }
3541            Err(mpsc::RecvTimeoutError::Timeout) => false,
3542            Err(mpsc::RecvTimeoutError::Disconnected) => {
3543                panic!("RPC query thread disconnected without a result")
3544            }
3545        };
3546
3547        // Let the old blocking implementation finish so a regression failure does
3548        // not leak a thread into the rest of the test process.
3549        assert!(matches!(event_rx.recv().unwrap(), Event::Tick));
3550        if !completed_promptly {
3551            match event_rx.recv_timeout(Duration::from_secs(1)).unwrap() {
3552                Event::Query(request, response_tx)
3553                    if matches!(*request, QueryRequest::LinkCount) =>
3554                {
3555                    response_tx.send(QueryResponse::LinkCount(1)).unwrap();
3556                }
3557                other => panic!("unexpected event after releasing queue: {other:?}"),
3558            }
3559            let _ = result_rx.recv_timeout(Duration::from_secs(1)).unwrap();
3560        }
3561        query_thread.join().unwrap();
3562
3563        assert!(
3564            completed_promptly,
3565            "RPC query blocked while the driver event queue was full"
3566        );
3567    }
3568
3569    #[test]
3570    fn stalled_rpc_request_does_not_block_later_clients() {
3571        let key = derive_auth_key(b"concurrent-rpc-test");
3572        let (event_tx, event_rx) = crate::event::channel();
3573
3574        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3575        let port = listener.local_addr().unwrap().port();
3576        listener.set_nonblocking(true).unwrap();
3577
3578        let shutdown = Arc::new(AtomicBool::new(false));
3579        let server_shutdown = shutdown.clone();
3580        let server_thread = thread::spawn(move || {
3581            rpc_server_loop(listener, key, event_tx, server_shutdown);
3582        });
3583
3584        let (first_seen_tx, first_seen_rx) = mpsc::channel();
3585        let (concurrent_tx, concurrent_rx) = mpsc::channel();
3586        let driver_thread = thread::spawn(move || {
3587            let first_response = match event_rx.recv_timeout(Duration::from_secs(1)).unwrap() {
3588                Event::Query(request, response_tx)
3589                    if matches!(*request, QueryRequest::LinkCount) =>
3590                {
3591                    response_tx
3592                }
3593                other => panic!("unexpected first RPC event: {other:?}"),
3594            };
3595            first_seen_tx.send(()).unwrap();
3596
3597            let second = event_rx.recv_timeout(Duration::from_millis(500));
3598            let handled_concurrently = match second {
3599                Ok(Event::Query(request, response_tx))
3600                    if matches!(*request, QueryRequest::TransportIdentity) =>
3601                {
3602                    response_tx
3603                        .send(QueryResponse::TransportIdentity(Some([7; 16])))
3604                        .unwrap();
3605                    true
3606                }
3607                Ok(other) => panic!("unexpected second RPC event: {other:?}"),
3608                Err(mpsc::RecvTimeoutError::Timeout) => false,
3609                Err(mpsc::RecvTimeoutError::Disconnected) => {
3610                    panic!("RPC event channel disconnected")
3611                }
3612            };
3613
3614            first_response.send(QueryResponse::LinkCount(1)).unwrap();
3615
3616            if !handled_concurrently {
3617                match event_rx.recv_timeout(Duration::from_secs(1)).unwrap() {
3618                    Event::Query(request, response_tx)
3619                        if matches!(*request, QueryRequest::TransportIdentity) =>
3620                    {
3621                        response_tx
3622                            .send(QueryResponse::TransportIdentity(Some([7; 16])))
3623                            .unwrap();
3624                    }
3625                    other => panic!("unexpected delayed RPC event: {other:?}"),
3626                }
3627            }
3628            concurrent_tx.send(handled_concurrently).unwrap();
3629        });
3630
3631        let first_addr = RpcAddr::Tcp("127.0.0.1".into(), port);
3632        let first_client = thread::spawn(move || {
3633            let mut client = RpcClient::connect(&first_addr, &key).unwrap();
3634            client
3635                .call(&PickleValue::Dict(vec![(
3636                    PickleValue::String("get".into()),
3637                    PickleValue::String("link_count".into()),
3638                )]))
3639                .unwrap()
3640        });
3641
3642        first_seen_rx.recv_timeout(Duration::from_secs(1)).unwrap();
3643
3644        let second_addr = RpcAddr::Tcp("127.0.0.1".into(), port);
3645        let second_client = thread::spawn(move || {
3646            let mut client = RpcClient::connect(&second_addr, &key).unwrap();
3647            client
3648                .call(&PickleValue::Dict(vec![(
3649                    PickleValue::String("get".into()),
3650                    PickleValue::String("transport_identity".into()),
3651                )]))
3652                .unwrap()
3653        });
3654
3655        let handled_concurrently = concurrent_rx.recv_timeout(Duration::from_secs(2)).unwrap();
3656        assert_eq!(first_client.join().unwrap().as_int(), Some(1));
3657        assert_eq!(
3658            second_client.join().unwrap().as_bytes(),
3659            Some([7; 16].as_slice())
3660        );
3661
3662        shutdown.store(true, Ordering::Relaxed);
3663        server_thread.join().unwrap();
3664        driver_thread.join().unwrap();
3665
3666        assert!(
3667            handled_concurrently,
3668            "a stalled RPC request prevented the server from handling a later client"
3669        );
3670    }
3671
3672    #[test]
3673    fn derive_auth_key_deterministic() {
3674        let key1 = derive_auth_key(b"test");
3675        let key2 = derive_auth_key(b"test");
3676        assert_eq!(key1, key2);
3677        // Different input → different key
3678        let key3 = derive_auth_key(b"other");
3679        assert_ne!(key1, key3);
3680    }
3681
3682    #[test]
3683    fn pickle_request_handling() {
3684        // Test the request → query translation without networking
3685        let (event_tx, event_rx) = crate::event::channel();
3686
3687        let driver = thread::spawn(move || {
3688            if let Some((QueryRequest::DropPath { dest_hash }, resp_tx)) = event_rx
3689                .recv()
3690                .ok()
3691                .and_then(|event| event.into_query().ok())
3692            {
3693                assert_eq!(dest_hash, [1u8; 16]);
3694                let _ = resp_tx.send(QueryResponse::DropPath(true));
3695            }
3696        });
3697
3698        let request = PickleValue::Dict(vec![
3699            (
3700                PickleValue::String("drop".into()),
3701                PickleValue::String("path".into()),
3702            ),
3703            (
3704                PickleValue::String("destination_hash".into()),
3705                PickleValue::Bytes(vec![1u8; 16]),
3706            ),
3707        ]);
3708
3709        let response = handle_rpc_request(&request, &event_tx).unwrap();
3710        assert_eq!(response, PickleValue::Bool(true));
3711        driver.join().unwrap();
3712    }
3713
3714    #[test]
3715    fn hook_list_request_handling() {
3716        let (event_tx, event_rx) = crate::event::channel();
3717
3718        let driver = thread::spawn(move || {
3719            if let Ok(Event::ListHooks { response_tx }) = event_rx.recv() {
3720                let _ = response_tx.send(vec![HookInfo {
3721                    name: "stats".into(),
3722                    hook_type: "wasm".into(),
3723                    attach_point: "PreIngress".into(),
3724                    priority: 7,
3725                    enabled: true,
3726                    consecutive_traps: 0,
3727                }]);
3728            }
3729        });
3730
3731        let request = PickleValue::Dict(vec![(
3732            PickleValue::String("get".into()),
3733            PickleValue::String("hooks".into()),
3734        )]);
3735        let response = handle_rpc_request(&request, &event_tx).unwrap();
3736        let hooks = parse_hook_list(&response).unwrap();
3737        assert_eq!(hooks.len(), 1);
3738        assert_eq!(hooks[0].name, "stats");
3739        driver.join().unwrap();
3740    }
3741
3742    #[test]
3743    fn hook_load_request_handling() {
3744        let (event_tx, event_rx) = crate::event::channel();
3745
3746        let driver = thread::spawn(move || {
3747            if let Ok(Event::LoadHook {
3748                name,
3749                wasm_bytes,
3750                attach_point,
3751                priority,
3752                response_tx,
3753            }) = event_rx.recv()
3754            {
3755                assert_eq!(name, "stats");
3756                assert_eq!(attach_point, "PreIngress");
3757                assert_eq!(priority, 11);
3758                assert_eq!(wasm_bytes, vec![1, 2, 3]);
3759                let _ = response_tx.send(Ok(()));
3760            }
3761        });
3762
3763        let request = PickleValue::Dict(vec![
3764            (
3765                PickleValue::String("hook".into()),
3766                PickleValue::String("load".into()),
3767            ),
3768            (
3769                PickleValue::String("name".into()),
3770                PickleValue::String("stats".into()),
3771            ),
3772            (
3773                PickleValue::String("attach_point".into()),
3774                PickleValue::String("PreIngress".into()),
3775            ),
3776            (PickleValue::String("priority".into()), PickleValue::Int(11)),
3777            (
3778                PickleValue::String("wasm".into()),
3779                PickleValue::Bytes(vec![1, 2, 3]),
3780            ),
3781        ]);
3782        let response = handle_rpc_request(&request, &event_tx).unwrap();
3783        assert_eq!(parse_hook_result(&response).unwrap(), Ok(()));
3784        driver.join().unwrap();
3785    }
3786
3787    #[test]
3788    fn interface_stats_pickle_format() {
3789        let traffic = TrafficDetail {
3790            rxs: 800.0,
3791            txs: 1600.0,
3792            arxb: 11,
3793            atxb: 22,
3794            arxc: 1,
3795            atxc: 2,
3796            arxs: 88.0,
3797            atxs: 176.0,
3798            prxb: 33,
3799            ptxb: 44,
3800            prxc: 3,
3801            ptxc: 4,
3802            prxs: 264.0,
3803            ptxs: 352.0,
3804            arxf: 1.0,
3805            atxf: 2.0,
3806            prxf: 3.0,
3807            ptxf: 4.0,
3808        };
3809        let mut stats = InterfaceStatsResponse {
3810            interfaces: vec![SingleInterfaceStat {
3811                id: 1,
3812                name: "TCP".into(),
3813                status: true,
3814                mode: 1,
3815                gravity: -2,
3816                announces_to_internal: Some(true),
3817                rxb: 100,
3818                txb: 200,
3819                tx_drops: 3,
3820                tx_dropped_bytes: 4096,
3821                tx_stalled: true,
3822                tx_buffered: 8192,
3823                traffic,
3824                protocol_violations: 11,
3825                ifac_violations: 12,
3826                packet_filter_hits: 13,
3827                rx_packets: 5,
3828                tx_packets: 10,
3829                cpu_load: Some(12.0),
3830                mem_load: Some(34.5),
3831                switch_id: Some([1, 2, 3, 4]),
3832                endpoint_id: Some([5; 8]),
3833                via_switch_id: Some([6; 4]),
3834                peers: Some(2),
3835                bitrate: Some(1000000),
3836                mtu: Some(131_072),
3837                ifac_size: Some(16),
3838                started: 1000.0,
3839                ia_freq: 1.0,
3840                ip_freq: 2.0,
3841                op_freq: 3.0,
3842                op_samples: 0,
3843                burst_active: true,
3844                burst_activated: 1200.0,
3845                pr_burst_active: true,
3846                pr_burst_activated: 1300.0,
3847                oa_freq: 4.0,
3848                clients: Some(3),
3849                blocked_ips: Some(2),
3850                blocked_ip_list: Some(vec!["192.0.2.1".into(), "203.0.113.2".into()]),
3851                announce_rate_target: Some(3600.0),
3852                announce_rate_grace: 5,
3853                announce_rate_penalty: 0.0,
3854                interface_type: "TCPClientInterface".into(),
3855            }],
3856            transport_id: Some([0xAB; 16]),
3857            transport_enabled: true,
3858            transport_uptime: 3600.0,
3859            total_rxb: 100,
3860            total_txb: 200,
3861            rx_pps: 3.0,
3862            tx_pps: 4.0,
3863            traffic,
3864            probe_responder: None,
3865            backbone_peer_pool: None,
3866        };
3867
3868        let pickle = interface_stats_to_pickle(&stats);
3869
3870        // Verify it round-trips through encode/decode
3871        let encoded = pickle::encode(&pickle);
3872        let decoded = pickle::decode(&encoded).unwrap();
3873        assert!(decoded.get("transport_enabled").unwrap().as_bool().unwrap());
3874        let ifaces = decoded.get("interfaces").unwrap().as_list().unwrap();
3875        assert_eq!(decoded.get("arxf").unwrap().as_float(), Some(1.0));
3876        assert_eq!(decoded.get("atxf").unwrap().as_float(), Some(2.0));
3877        assert_eq!(decoded.get("prxf").unwrap().as_float(), Some(3.0));
3878        assert_eq!(decoded.get("ptxf").unwrap().as_float(), Some(4.0));
3879        assert_eq!(decoded.get("rxpps").unwrap().as_float(), Some(3.0));
3880        assert_eq!(decoded.get("txpps").unwrap().as_float(), Some(4.0));
3881        assert_eq!(ifaces[0].get("id").unwrap().as_int().unwrap(), 1);
3882        assert_eq!(ifaces[0].get("name").unwrap().as_str().unwrap(), "TCP");
3883        assert_eq!(ifaces[0].get("mtu").unwrap().as_int(), Some(131_072));
3884        assert_eq!(ifaces[0].get("gravity").unwrap().as_int().unwrap(), -2);
3885        assert_eq!(ifaces[0].get("txdrp").unwrap().as_int(), Some(3));
3886        assert_eq!(ifaces[0].get("txdrb").unwrap().as_int(), Some(4096));
3887        assert_eq!(ifaces[0].get("txstalled").unwrap().as_bool(), Some(true));
3888        assert_eq!(ifaces[0].get("txbuffered").unwrap().as_int(), Some(8192));
3889        assert!(ifaces[0]
3890            .get("announces_to_internal")
3891            .unwrap()
3892            .as_bool()
3893            .unwrap());
3894        assert_eq!(ifaces[0].get("ia_freq").unwrap().as_float().unwrap(), 1.0);
3895        assert_eq!(ifaces[0].get("ip_freq").unwrap().as_float().unwrap(), 2.0);
3896        assert_eq!(ifaces[0].get("op_freq").unwrap().as_float().unwrap(), 3.0);
3897        assert_eq!(ifaces[0].get("oa_freq").unwrap().as_float().unwrap(), 4.0);
3898        assert_eq!(ifaces[0].get("arxb").unwrap().as_int().unwrap(), 11);
3899        assert_eq!(ifaces[0].get("atxb").unwrap().as_int().unwrap(), 22);
3900        assert_eq!(ifaces[0].get("prxb").unwrap().as_int().unwrap(), 33);
3901        assert_eq!(ifaces[0].get("ptxb").unwrap().as_int().unwrap(), 44);
3902        assert_eq!(ifaces[0].get("arxs").unwrap().as_float().unwrap(), 88.0);
3903        assert_eq!(ifaces[0].get("ptxs").unwrap().as_float().unwrap(), 352.0);
3904        assert_eq!(
3905            ifaces[0]
3906                .get("protocol_violations")
3907                .unwrap()
3908                .as_int()
3909                .unwrap(),
3910            11
3911        );
3912        assert_eq!(ifaces[0].get("ifac_violations").unwrap().as_int(), Some(12));
3913        assert_eq!(
3914            ifaces[0].get("packet_filter_hits").unwrap().as_int(),
3915            Some(13)
3916        );
3917        assert_eq!(decoded.get("arxb").unwrap().as_int().unwrap(), 11);
3918        assert_eq!(decoded.get("prxb").unwrap().as_int().unwrap(), 33);
3919        assert!(ifaces[0].get("burst_active").unwrap().as_bool().unwrap());
3920        assert_eq!(
3921            ifaces[0]
3922                .get("burst_activated")
3923                .unwrap()
3924                .as_float()
3925                .unwrap(),
3926            1200.0
3927        );
3928        assert!(ifaces[0].get("pr_burst_active").unwrap().as_bool().unwrap());
3929        assert_eq!(
3930            ifaces[0]
3931                .get("pr_burst_activated")
3932                .unwrap()
3933                .as_float()
3934                .unwrap(),
3935            1300.0
3936        );
3937        assert_eq!(
3938            ifaces[0]
3939                .get("announce_rate_target")
3940                .unwrap()
3941                .as_float()
3942                .unwrap(),
3943            3600.0
3944        );
3945        assert_eq!(
3946            ifaces[0]
3947                .get("announce_rate_grace")
3948                .unwrap()
3949                .as_int()
3950                .unwrap(),
3951            5
3952        );
3953        assert_eq!(
3954            ifaces[0]
3955                .get("announce_rate_penalty")
3956                .unwrap()
3957                .as_float()
3958                .unwrap(),
3959            0.0
3960        );
3961        assert_eq!(ifaces[0].get("clients").unwrap().as_int().unwrap(), 3);
3962        assert_eq!(ifaces[0].get("blocked_ips").unwrap().as_int().unwrap(), 2);
3963        assert_eq!(
3964            ifaces[0]
3965                .get("blocked_ip_list")
3966                .unwrap()
3967                .as_list()
3968                .unwrap()
3969                .iter()
3970                .map(|value| value.as_str().unwrap())
3971                .collect::<Vec<_>>(),
3972            vec!["192.0.2.1", "203.0.113.2"]
3973        );
3974
3975        stats.interfaces[0].interface_type = "BackboneInterface".into();
3976        let (event_tx, _event_rx) = crate::event::channel_with_capacity(1);
3977        event_tx.register_dynamic_parent(InterfaceId(2), InterfaceId(1));
3978        event_tx.set_ingress_bursts(InterfaceId(2), Some(10.0), Some(20.0));
3979        event_tx
3980            .try_send(Event::Frame {
3981                interface_id: InterfaceId(2),
3982                data: vec![0],
3983                rssi: None,
3984                snr: None,
3985            })
3986            .unwrap();
3987        assert!(event_tx
3988            .try_send(Event::Frame {
3989                interface_id: InterfaceId(2),
3990                data: vec![0],
3991                rssi: None,
3992                snr: None,
3993            })
3994            .is_err());
3995        // Reader `send` now waits for space in the data class instead of
3996        // dropping, so a second refused `try_send` provides the second drop.
3997        assert!(event_tx
3998            .try_send(Event::Frame {
3999                interface_id: InterfaceId(2),
4000                data: vec![0],
4001                rssi: None,
4002                snr: None,
4003            })
4004            .is_err());
4005        let mut runtime_pickle = interface_stats_to_pickle(&stats);
4006        add_inbound_runtime_stats(&mut runtime_pickle, &stats, &event_tx);
4007        let runtime_ifaces = runtime_pickle.get("interfaces").unwrap().as_list().unwrap();
4008        assert_eq!(
4009            runtime_ifaces[0]
4010                .get("burst_count")
4011                .unwrap()
4012                .as_int()
4013                .unwrap(),
4014            1
4015        );
4016        assert_eq!(
4017            runtime_ifaces[0]
4018                .get("pr_burst_count")
4019                .unwrap()
4020                .as_int()
4021                .unwrap(),
4022            1
4023        );
4024        assert!(runtime_ifaces[0]
4025            .get("burst_active")
4026            .unwrap()
4027            .as_bool()
4028            .unwrap());
4029        assert_eq!(
4030            runtime_ifaces[0]
4031                .get("burst_activated")
4032                .unwrap()
4033                .as_float()
4034                .unwrap(),
4035            10.0
4036        );
4037        assert!(runtime_ifaces[0]
4038            .get("pr_burst_active")
4039            .unwrap()
4040            .as_bool()
4041            .unwrap());
4042        assert_eq!(
4043            runtime_ifaces[0]
4044                .get("pr_burst_activated")
4045                .unwrap()
4046                .as_float()
4047                .unwrap(),
4048            20.0
4049        );
4050        assert_eq!(runtime_pickle.get("rxqt").unwrap().as_int().unwrap(), 1);
4051        assert_eq!(runtime_pickle.get("rxqd").unwrap().as_int().unwrap(), 1);
4052        assert_eq!(runtime_pickle.get("rxqtd").unwrap().as_int().unwrap(), 2);
4053        assert_eq!(runtime_pickle.get("rxqdd").unwrap().as_int().unwrap(), 2);
4054        assert_eq!(runtime_pickle.get("rxqad").unwrap().as_int().unwrap(), 0);
4055        assert_eq!(runtime_pickle.get("rxqpd").unwrap().as_int().unwrap(), 0);
4056        assert_eq!(runtime_pickle.get("rxqild").unwrap().as_int().unwrap(), 0);
4057        assert_eq!(
4058            runtime_pickle
4059                .get("dqpressure")
4060                .unwrap()
4061                .as_float()
4062                .unwrap(),
4063            1.0
4064        );
4065        assert_eq!(
4066            runtime_pickle
4067                .get("tqpressure")
4068                .unwrap()
4069                .as_float()
4070                .unwrap(),
4071            0.25
4072        );
4073    }
4074
4075    #[test]
4076    fn profiling_results_rpc_exposes_completed_samples() {
4077        {
4078            let _sample = crate::profiling::profile("entry110.rpc");
4079        }
4080        let (event_tx, _event_rx) = crate::event::channel();
4081        let request = PickleValue::Dict(vec![(
4082            PickleValue::String("get".into()),
4083            PickleValue::String("profiling_results".into()),
4084        )]);
4085        let response = handle_rpc_request(&request, &event_tx).unwrap();
4086        let sample = response.get("entry110.rpc").unwrap();
4087        assert_eq!(
4088            sample.get("name").and_then(PickleValue::as_str),
4089            Some("entry110.rpc")
4090        );
4091        assert_eq!(
4092            sample
4093                .get("stats_all")
4094                .and_then(|stats| stats.get("count"))
4095                .and_then(PickleValue::as_int),
4096            Some(1)
4097        );
4098    }
4099
4100    #[test]
4101    fn interface_stats_pickle_includes_backbone_peer_pool_priority() {
4102        let stats = InterfaceStatsResponse {
4103            interfaces: vec![],
4104            transport_id: None,
4105            transport_enabled: true,
4106            transport_uptime: 1.0,
4107            total_rxb: 0,
4108            total_txb: 0,
4109            rx_pps: 0.0,
4110            tx_pps: 0.0,
4111            traffic: TrafficDetail::default(),
4112            probe_responder: None,
4113            backbone_peer_pool: Some(crate::event::BackbonePeerPoolStatus {
4114                max_connected: 1,
4115                active_count: 1,
4116                standby_count: 0,
4117                cooldown_count: 0,
4118                members: vec![crate::event::BackbonePeerPoolMemberStatus {
4119                    name: "peer".into(),
4120                    remote: "127.0.0.1:4242".into(),
4121                    source: "configured".into(),
4122                    priority: 77,
4123                    state: "active".into(),
4124                    interface_id: Some(42),
4125                    failure_count: 0,
4126                    last_error: None,
4127                    cooldown_remaining_seconds: None,
4128                }],
4129            }),
4130        };
4131
4132        let pickle = interface_stats_to_pickle(&stats);
4133        let encoded = pickle::encode(&pickle);
4134        let decoded = pickle::decode(&encoded).unwrap();
4135        let pool = decoded.get("backbone_peer_pool").unwrap();
4136        let members = pool.get("members").unwrap().as_list().unwrap();
4137        assert_eq!(members[0].get("priority").unwrap().as_int().unwrap(), 77);
4138    }
4139
4140    #[test]
4141    fn send_probe_rpc_unknown_dest() {
4142        let (event_tx, event_rx) = crate::event::channel();
4143
4144        let driver = thread::spawn(move || {
4145            if let Some((
4146                QueryRequest::SendProbe {
4147                    dest_hash,
4148                    payload_size,
4149                },
4150                resp_tx,
4151            )) = event_rx
4152                .recv()
4153                .ok()
4154                .and_then(|event| event.into_query().ok())
4155            {
4156                assert_eq!(dest_hash, [0xAA; 16]);
4157                assert_eq!(payload_size, 16); // default
4158                let _ = resp_tx.send(QueryResponse::SendProbe(Err(
4159                    crate::event::SendProbeError::UnknownIdentity,
4160                )));
4161            }
4162        });
4163
4164        let request = PickleValue::Dict(vec![(
4165            PickleValue::String("send_probe".into()),
4166            PickleValue::Bytes(vec![0xAA; 16]),
4167        )]);
4168
4169        let response = handle_rpc_request(&request, &event_tx).unwrap();
4170        assert_eq!(response, PickleValue::None);
4171        driver.join().unwrap();
4172    }
4173
4174    #[test]
4175    fn send_probe_rpc_with_result() {
4176        let (event_tx, event_rx) = crate::event::channel();
4177
4178        let packet_hash = [0xBB; 32];
4179        let driver = thread::spawn(move || {
4180            if let Some((
4181                QueryRequest::SendProbe {
4182                    dest_hash,
4183                    payload_size,
4184                },
4185                resp_tx,
4186            )) = event_rx
4187                .recv()
4188                .ok()
4189                .and_then(|event| event.into_query().ok())
4190            {
4191                assert_eq!(dest_hash, [0xCC; 16]);
4192                assert_eq!(payload_size, 32);
4193                let _ = resp_tx.send(QueryResponse::SendProbe(Ok((packet_hash, 3))));
4194            }
4195        });
4196
4197        let request = PickleValue::Dict(vec![
4198            (
4199                PickleValue::String("send_probe".into()),
4200                PickleValue::Bytes(vec![0xCC; 16]),
4201            ),
4202            (PickleValue::String("size".into()), PickleValue::Int(32)),
4203        ]);
4204
4205        let response = handle_rpc_request(&request, &event_tx).unwrap();
4206        let ph = response.get("packet_hash").unwrap().as_bytes().unwrap();
4207        assert_eq!(ph, &[0xBB; 32]);
4208        assert_eq!(response.get("hops").unwrap().as_int().unwrap(), 3);
4209        driver.join().unwrap();
4210    }
4211
4212    #[test]
4213    fn retain_identity_rpc_sends_identity_hash_query() {
4214        let (event_tx, event_rx) = crate::event::channel();
4215        let identity_hash = [0x44; 16];
4216
4217        let driver = thread::spawn(move || {
4218            if let Some((
4219                QueryRequest::RetainIdentity {
4220                    identity_hash: hash,
4221                },
4222                resp_tx,
4223            )) = event_rx
4224                .recv()
4225                .ok()
4226                .and_then(|event| event.into_query().ok())
4227            {
4228                assert_eq!(hash, identity_hash);
4229                let _ = resp_tx.send(QueryResponse::RetainIdentity(true));
4230            }
4231        });
4232
4233        let request = PickleValue::Dict(vec![
4234            (
4235                PickleValue::String("set".into()),
4236                PickleValue::String("identity_retained".into()),
4237            ),
4238            (
4239                PickleValue::String("identity_hash".into()),
4240                PickleValue::Bytes(identity_hash.to_vec()),
4241            ),
4242        ]);
4243
4244        let response = handle_rpc_request(&request, &event_tx).unwrap();
4245        assert_eq!(response, PickleValue::Bool(true));
4246        driver.join().unwrap();
4247    }
4248
4249    #[test]
4250    fn send_probe_rpc_size_validation() {
4251        let (event_tx, event_rx) = crate::event::channel();
4252
4253        // Negative size should be clamped to default (16)
4254        let driver = thread::spawn(move || {
4255            if let Some((QueryRequest::SendProbe { payload_size, .. }, resp_tx)) = event_rx
4256                .recv()
4257                .ok()
4258                .and_then(|event| event.into_query().ok())
4259            {
4260                assert_eq!(payload_size, 16); // default, not negative
4261                let _ = resp_tx.send(QueryResponse::SendProbe(Err(
4262                    crate::event::SendProbeError::UnknownIdentity,
4263                )));
4264            }
4265        });
4266
4267        let request = PickleValue::Dict(vec![
4268            (
4269                PickleValue::String("send_probe".into()),
4270                PickleValue::Bytes(vec![0xDD; 16]),
4271            ),
4272            (PickleValue::String("size".into()), PickleValue::Int(-1)),
4273        ]);
4274
4275        let response = handle_rpc_request(&request, &event_tx).unwrap();
4276        assert_eq!(response, PickleValue::None);
4277        driver.join().unwrap();
4278    }
4279
4280    #[test]
4281    fn send_probe_rpc_size_too_large() {
4282        let (event_tx, _event_rx) = crate::event::channel();
4283
4284        // Oversize requests are rejected before reaching the driver instead
4285        // of being silently replaced by the default size.
4286        let request = PickleValue::Dict(vec![
4287            (
4288                PickleValue::String("send_probe".into()),
4289                PickleValue::Bytes(vec![0xDD; 16]),
4290            ),
4291            (
4292                PickleValue::String("size".into()),
4293                PickleValue::Int(crate::event::MAX_PROBE_PAYLOAD_SIZE as i64 + 1),
4294            ),
4295        ]);
4296
4297        let response = handle_rpc_request(&request, &event_tx).unwrap();
4298        assert_eq!(
4299            response.get("error").and_then(|v| v.as_str()),
4300            Some("payload_too_large")
4301        );
4302        assert_eq!(
4303            response.get("max_size").and_then(|v| v.as_int()),
4304            Some(crate::event::MAX_PROBE_PAYLOAD_SIZE as i64)
4305        );
4306    }
4307
4308    #[test]
4309    fn send_probe_rpc_max_size_reaches_driver() {
4310        let (event_tx, event_rx) = crate::event::channel();
4311
4312        let driver = thread::spawn(move || {
4313            if let Some((QueryRequest::SendProbe { payload_size, .. }, resp_tx)) = event_rx
4314                .recv()
4315                .ok()
4316                .and_then(|event| event.into_query().ok())
4317            {
4318                assert_eq!(payload_size, crate::event::MAX_PROBE_PAYLOAD_SIZE);
4319                let _ = resp_tx.send(QueryResponse::SendProbe(Ok(([0xEE; 32], 2))));
4320            }
4321        });
4322
4323        let request = PickleValue::Dict(vec![
4324            (
4325                PickleValue::String("send_probe".into()),
4326                PickleValue::Bytes(vec![0xDD; 16]),
4327            ),
4328            (
4329                PickleValue::String("size".into()),
4330                PickleValue::Int(crate::event::MAX_PROBE_PAYLOAD_SIZE as i64),
4331            ),
4332        ]);
4333
4334        let response = handle_rpc_request(&request, &event_tx).unwrap();
4335        assert_eq!(response.get("hops").unwrap().as_int().unwrap(), 2);
4336        driver.join().unwrap();
4337    }
4338
4339    #[test]
4340    fn send_probe_rpc_driver_payload_too_large_is_reported() {
4341        let (event_tx, event_rx) = crate::event::channel();
4342
4343        let driver = thread::spawn(move || {
4344            if let Some((QueryRequest::SendProbe { .. }, resp_tx)) = event_rx
4345                .recv()
4346                .ok()
4347                .and_then(|event| event.into_query().ok())
4348            {
4349                let _ = resp_tx.send(QueryResponse::SendProbe(Err(
4350                    crate::event::SendProbeError::PayloadTooLarge { max: 123 },
4351                )));
4352            }
4353        });
4354
4355        let request = PickleValue::Dict(vec![(
4356            PickleValue::String("send_probe".into()),
4357            PickleValue::Bytes(vec![0xDD; 16]),
4358        )]);
4359
4360        let response = handle_rpc_request(&request, &event_tx).unwrap();
4361        assert_eq!(
4362            response.get("error").and_then(|v| v.as_str()),
4363            Some("payload_too_large")
4364        );
4365        assert_eq!(response.get("max_size").and_then(|v| v.as_int()), Some(123));
4366        driver.join().unwrap();
4367    }
4368
4369    #[test]
4370    fn check_proof_rpc_not_found() {
4371        let (event_tx, event_rx) = crate::event::channel();
4372
4373        let driver = thread::spawn(move || {
4374            if let Some((QueryRequest::CheckProof { packet_hash }, resp_tx)) = event_rx
4375                .recv()
4376                .ok()
4377                .and_then(|event| event.into_query().ok())
4378            {
4379                assert_eq!(packet_hash, [0xEE; 32]);
4380                let _ = resp_tx.send(QueryResponse::CheckProof(None));
4381            }
4382        });
4383
4384        let request = PickleValue::Dict(vec![(
4385            PickleValue::String("check_proof".into()),
4386            PickleValue::Bytes(vec![0xEE; 32]),
4387        )]);
4388
4389        let response = handle_rpc_request(&request, &event_tx).unwrap();
4390        assert_eq!(response, PickleValue::None);
4391        driver.join().unwrap();
4392    }
4393
4394    #[test]
4395    fn check_proof_rpc_found() {
4396        let (event_tx, event_rx) = crate::event::channel();
4397
4398        let driver = thread::spawn(move || {
4399            if let Some((QueryRequest::CheckProof { packet_hash }, resp_tx)) = event_rx
4400                .recv()
4401                .ok()
4402                .and_then(|event| event.into_query().ok())
4403            {
4404                assert_eq!(packet_hash, [0xFF; 32]);
4405                let _ = resp_tx.send(QueryResponse::CheckProof(Some(0.352)));
4406            }
4407        });
4408
4409        let request = PickleValue::Dict(vec![(
4410            PickleValue::String("check_proof".into()),
4411            PickleValue::Bytes(vec![0xFF; 32]),
4412        )]);
4413
4414        let response = handle_rpc_request(&request, &event_tx).unwrap();
4415        if let PickleValue::Float(rtt) = response {
4416            assert!((rtt - 0.352).abs() < 0.001);
4417        } else {
4418            panic!("Expected Float, got {:?}", response);
4419        }
4420        driver.join().unwrap();
4421    }
4422
4423    #[test]
4424    fn request_path_rpc() {
4425        let (event_tx, event_rx) = crate::event::channel();
4426
4427        let driver =
4428            thread::spawn(
4429                move || match event_rx.recv_timeout(std::time::Duration::from_secs(5)) {
4430                    Ok(Event::RequestPath { dest_hash }) => {
4431                        assert_eq!(dest_hash, [0x11; 16]);
4432                    }
4433                    other => panic!("Expected RequestPath event, got {:?}", other),
4434                },
4435            );
4436
4437        let request = PickleValue::Dict(vec![(
4438            PickleValue::String("request_path".into()),
4439            PickleValue::Bytes(vec![0x11; 16]),
4440        )]);
4441
4442        let response = handle_rpc_request(&request, &event_tx).unwrap();
4443        assert_eq!(response, PickleValue::Bool(true));
4444        driver.join().unwrap();
4445    }
4446
4447    #[test]
4448    fn begin_drain_rpc_emits_event() {
4449        let (event_tx, event_rx) = crate::event::channel();
4450
4451        let driver = thread::spawn(
4452            move || match event_rx.recv_timeout(Duration::from_secs(5)) {
4453                Ok(Event::BeginDrain { timeout }) => {
4454                    assert!((timeout.as_secs_f64() - 1.5).abs() < 0.001);
4455                }
4456                other => panic!("Expected BeginDrain event, got {:?}", other),
4457            },
4458        );
4459
4460        let request = PickleValue::Dict(vec![(
4461            PickleValue::String("begin_drain".into()),
4462            PickleValue::Float(1.5),
4463        )]);
4464
4465        let response = handle_rpc_request(&request, &event_tx).unwrap();
4466        assert_eq!(response, PickleValue::Bool(true));
4467        driver.join().unwrap();
4468    }
4469
4470    #[test]
4471    fn drain_status_rpc_roundtrips_fields() {
4472        let (event_tx, event_rx) = crate::event::channel();
4473
4474        let driver = thread::spawn(move || {
4475            if let Some((QueryRequest::DrainStatus, resp_tx)) = event_rx
4476                .recv()
4477                .ok()
4478                .and_then(|event| event.into_query().ok())
4479            {
4480                let _ = resp_tx.send(QueryResponse::DrainStatus(DrainStatus {
4481                    state: LifecycleState::Draining,
4482                    drain_age_seconds: Some(0.75),
4483                    deadline_remaining_seconds: Some(2.25),
4484                    drain_complete: false,
4485                    interface_writer_queued_frames: 3,
4486                    provider_backlog_events: 4,
4487                    provider_consumer_queued_events: 5,
4488                    detail: Some("node is draining existing work".into()),
4489                }));
4490            }
4491        });
4492
4493        let request = PickleValue::Dict(vec![(
4494            PickleValue::String("get".into()),
4495            PickleValue::String("drain_status".into()),
4496        )]);
4497
4498        let response = handle_rpc_request(&request, &event_tx).unwrap();
4499        assert_eq!(response.get("state").unwrap().as_str(), Some("draining"));
4500        assert_eq!(
4501            response.get("drain_complete").unwrap().as_bool(),
4502            Some(false)
4503        );
4504        assert_eq!(
4505            response
4506                .get("deadline_remaining_seconds")
4507                .unwrap()
4508                .as_float(),
4509            Some(2.25)
4510        );
4511        assert_eq!(
4512            response
4513                .get("interface_writer_queued_frames")
4514                .unwrap()
4515                .as_int(),
4516            Some(3)
4517        );
4518        assert_eq!(
4519            response.get("provider_backlog_events").unwrap().as_int(),
4520            Some(4)
4521        );
4522        assert_eq!(
4523            response
4524                .get("provider_consumer_queued_events")
4525                .unwrap()
4526                .as_int(),
4527            Some(5)
4528        );
4529        assert_eq!(
4530            response.get("detail").unwrap().as_str(),
4531            Some("node is draining existing work")
4532        );
4533        driver.join().unwrap();
4534    }
4535
4536    #[test]
4537    fn interface_stats_with_probe_responder() {
4538        let probe_hash = [0x42; 16];
4539        let stats = InterfaceStatsResponse {
4540            interfaces: vec![],
4541            transport_id: None,
4542            transport_enabled: true,
4543            transport_uptime: 100.0,
4544            total_rxb: 0,
4545            total_txb: 0,
4546            rx_pps: 0.0,
4547            tx_pps: 0.0,
4548            traffic: TrafficDetail::default(),
4549            probe_responder: Some(probe_hash),
4550            backbone_peer_pool: None,
4551        };
4552
4553        let pickle = interface_stats_to_pickle(&stats);
4554        let encoded = pickle::encode(&pickle);
4555        let decoded = pickle::decode(&encoded).unwrap();
4556
4557        let pr = decoded.get("probe_responder").unwrap().as_bytes().unwrap();
4558        assert_eq!(pr, &probe_hash);
4559    }
4560
4561    #[test]
4562    fn runtime_config_get_and_set_rpc() {
4563        let (event_tx, event_rx) = crate::event::channel();
4564
4565        let driver = thread::spawn(move || {
4566            if let Some((QueryRequest::GetRuntimeConfig { key }, resp_tx)) = event_rx
4567                .recv()
4568                .ok()
4569                .and_then(|event| event.into_query().ok())
4570            {
4571                assert_eq!(key, "global.tick_interval_ms");
4572                let _ = resp_tx.send(QueryResponse::RuntimeConfigEntry(Some(
4573                    RuntimeConfigEntry {
4574                        key,
4575                        value: RuntimeConfigValue::Int(1000),
4576                        default: RuntimeConfigValue::Int(1000),
4577                        source: RuntimeConfigSource::Startup,
4578                        apply_mode: RuntimeConfigApplyMode::Immediate,
4579                        description: Some("tick".into()),
4580                    },
4581                )));
4582            } else {
4583                panic!("expected GetRuntimeConfig query");
4584            }
4585
4586            if let Some((QueryRequest::SetRuntimeConfig { key, value }, resp_tx)) = event_rx
4587                .recv()
4588                .ok()
4589                .and_then(|event| event.into_query().ok())
4590            {
4591                assert_eq!(key, "global.tick_interval_ms");
4592                assert_eq!(value, RuntimeConfigValue::Int(250));
4593                let _ = resp_tx.send(QueryResponse::RuntimeConfigSet(Ok(RuntimeConfigEntry {
4594                    key,
4595                    value: RuntimeConfigValue::Int(250),
4596                    default: RuntimeConfigValue::Int(1000),
4597                    source: RuntimeConfigSource::RuntimeOverride,
4598                    apply_mode: RuntimeConfigApplyMode::Immediate,
4599                    description: Some("tick".into()),
4600                })));
4601            } else {
4602                panic!("expected SetRuntimeConfig query");
4603            }
4604        });
4605
4606        let get_request = PickleValue::Dict(vec![
4607            (
4608                PickleValue::String("get".into()),
4609                PickleValue::String("runtime_config_entry".into()),
4610            ),
4611            (
4612                PickleValue::String("key".into()),
4613                PickleValue::String("global.tick_interval_ms".into()),
4614            ),
4615        ]);
4616        let get_response = handle_rpc_request(&get_request, &event_tx).unwrap();
4617        assert_eq!(
4618            get_response.get("key").and_then(|v| v.as_str()),
4619            Some("global.tick_interval_ms")
4620        );
4621
4622        let set_request = PickleValue::Dict(vec![
4623            (
4624                PickleValue::String("set".into()),
4625                PickleValue::String("runtime_config".into()),
4626            ),
4627            (
4628                PickleValue::String("key".into()),
4629                PickleValue::String("global.tick_interval_ms".into()),
4630            ),
4631            (PickleValue::String("value".into()), PickleValue::Int(250)),
4632        ]);
4633        let set_response = handle_rpc_request(&set_request, &event_tx).unwrap();
4634        assert_eq!(
4635            set_response.get("value").and_then(|v| v.as_int()),
4636            Some(250)
4637        );
4638
4639        driver.join().unwrap();
4640    }
4641
4642    // Helper: create a connected TCP pair
4643    fn tcp_pair() -> (TcpStream, TcpStream) {
4644        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4645        let port = listener.local_addr().unwrap().port();
4646        let client = TcpStream::connect(("127.0.0.1", port)).unwrap();
4647        let (server, _) = listener.accept().unwrap();
4648        client
4649            .set_read_timeout(Some(std::time::Duration::from_secs(5)))
4650            .unwrap();
4651        server
4652            .set_read_timeout(Some(std::time::Duration::from_secs(5)))
4653            .unwrap();
4654        (server, client)
4655    }
4656}