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};
13use std::sync::mpsc;
14use std::sync::{
15    atomic::{AtomicBool, Ordering},
16    Arc,
17};
18use std::thread;
19use std::time::Duration;
20
21use rns_core::msgpack::{self, Value as MsgpackValue};
22use rns_crypto::hmac::hmac_sha256;
23use rns_crypto::sha256::sha256;
24
25use crate::event::{
26    BackboneInterfaceEntry, BackbonePeerStateEntry, BlackholeInfo, DrainStatus, Event, EventSender,
27    HookInfo, InterfaceStatsResponse, KnownDestinationEntry, LifecycleState, PathTableEntry,
28    ProviderBridgeStats, QueryRequest, QueryResponse, RateTableEntry, RuntimeConfigApplyMode,
29    RuntimeConfigEntry, RuntimeConfigError, RuntimeConfigErrorCode, RuntimeConfigSource,
30    RuntimeConfigValue, SingleInterfaceStat,
31};
32use crate::md5::hmac_md5;
33use crate::pickle::{self, PickleValue};
34
35const CHALLENGE_PREFIX: &[u8] = b"#CHALLENGE#";
36const WELCOME: &[u8] = b"#WELCOME#";
37const FAILURE: &[u8] = b"#FAILURE#";
38const CHALLENGE_LEN: usize = 40;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41enum RpcCodec {
42    Msgpack,
43    Pickle,
44}
45
46/// RPC address types.
47#[derive(Debug, Clone)]
48pub enum RpcAddr {
49    Tcp(String, u16),
50}
51
52/// RPC server that listens for incoming connections and handles queries.
53pub struct RpcServer {
54    shutdown: Arc<AtomicBool>,
55    thread: Option<thread::JoinHandle<()>>,
56}
57
58impl RpcServer {
59    /// Start the RPC server on the given address.
60    pub fn start(addr: &RpcAddr, auth_key: [u8; 32], event_tx: EventSender) -> io::Result<Self> {
61        let shutdown = Arc::new(AtomicBool::new(false));
62        let shutdown2 = shutdown.clone();
63
64        let listener = match addr {
65            RpcAddr::Tcp(host, port) => {
66                let l = TcpListener::bind((host.as_str(), *port))?;
67                // Non-blocking so we can check shutdown flag
68                l.set_nonblocking(true)?;
69                l
70            }
71        };
72
73        let thread = thread::Builder::new()
74            .name("rpc-server".into())
75            .spawn(move || {
76                rpc_server_loop(listener, auth_key, event_tx, shutdown2);
77            })
78            .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
79
80        Ok(RpcServer {
81            shutdown,
82            thread: Some(thread),
83        })
84    }
85
86    /// Stop the RPC server.
87    pub fn stop(&mut self) {
88        self.shutdown.store(true, Ordering::Relaxed);
89        if let Some(handle) = self.thread.take() {
90            let _ = handle.join();
91        }
92    }
93}
94
95impl Drop for RpcServer {
96    fn drop(&mut self) {
97        self.stop();
98    }
99}
100
101fn rpc_server_loop(
102    listener: TcpListener,
103    auth_key: [u8; 32],
104    event_tx: EventSender,
105    shutdown: Arc<AtomicBool>,
106) {
107    loop {
108        if shutdown.load(Ordering::Relaxed) {
109            break;
110        }
111
112        match listener.accept() {
113            Ok((stream, _addr)) => {
114                // Set blocking for this connection
115                let _ = stream.set_nonblocking(false);
116                let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(10)));
117                let _ = stream.set_write_timeout(Some(std::time::Duration::from_secs(10)));
118
119                if let Err(e) = handle_connection(stream, &auth_key, &event_tx) {
120                    log::debug!("RPC connection error: {}", e);
121                }
122            }
123            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
124                // No pending connection, sleep briefly and retry
125                thread::sleep(std::time::Duration::from_millis(100));
126            }
127            Err(e) => {
128                log::error!("RPC accept error: {}", e);
129                thread::sleep(std::time::Duration::from_millis(100));
130            }
131        }
132    }
133}
134
135fn handle_connection(
136    mut stream: TcpStream,
137    auth_key: &[u8; 32],
138    event_tx: &EventSender,
139) -> io::Result<()> {
140    // Authentication: send challenge, verify response
141    server_auth(&mut stream, auth_key)?;
142
143    // Read request.
144    let request_bytes = recv_bytes(&mut stream)?;
145    let (request, codec) = decode_rpc_payload(&request_bytes)?;
146
147    // Translate request dict to query, send to driver, get response.
148    let response = handle_rpc_request(&request, event_tx)?;
149
150    // Encode response and send
151    let response_bytes = encode_rpc_payload(&response, codec);
152    send_bytes(&mut stream, &response_bytes)?;
153
154    Ok(())
155}
156
157fn decode_rpc_payload(data: &[u8]) -> io::Result<(PickleValue, RpcCodec)> {
158    if let Ok(value) = msgpack::unpack_exact(data) {
159        return Ok((msgpack_to_pickle(&value), RpcCodec::Msgpack));
160    }
161    pickle::decode(data)
162        .map(|value| (value, RpcCodec::Pickle))
163        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
164}
165
166fn encode_rpc_payload(value: &PickleValue, codec: RpcCodec) -> Vec<u8> {
167    match codec {
168        RpcCodec::Msgpack => msgpack::pack(&pickle_to_msgpack(value)),
169        RpcCodec::Pickle => pickle::encode(value),
170    }
171}
172
173fn pickle_to_msgpack(value: &PickleValue) -> MsgpackValue {
174    match value {
175        PickleValue::None => MsgpackValue::Nil,
176        PickleValue::Bool(v) => MsgpackValue::Bool(*v),
177        PickleValue::Int(v) if *v >= 0 => MsgpackValue::UInt(*v as u64),
178        PickleValue::Int(v) => MsgpackValue::Int(*v),
179        PickleValue::Float(v) => MsgpackValue::Float(*v),
180        PickleValue::String(v) => MsgpackValue::Str(v.clone()),
181        PickleValue::Bytes(v) => MsgpackValue::Bin(v.clone()),
182        PickleValue::List(values) => {
183            MsgpackValue::Array(values.iter().map(pickle_to_msgpack).collect())
184        }
185        PickleValue::Dict(values) => MsgpackValue::Map(
186            values
187                .iter()
188                .map(|(key, value)| (pickle_to_msgpack(key), pickle_to_msgpack(value)))
189                .collect(),
190        ),
191    }
192}
193
194fn msgpack_to_pickle(value: &MsgpackValue) -> PickleValue {
195    match value {
196        MsgpackValue::Nil => PickleValue::None,
197        MsgpackValue::Bool(v) => PickleValue::Bool(*v),
198        MsgpackValue::UInt(v) if *v <= i64::MAX as u64 => PickleValue::Int(*v as i64),
199        MsgpackValue::UInt(v) => PickleValue::Float(*v as f64),
200        MsgpackValue::Int(v) => PickleValue::Int(*v),
201        MsgpackValue::Float(v) => PickleValue::Float(*v),
202        MsgpackValue::Bin(v) => PickleValue::Bytes(v.clone()),
203        MsgpackValue::Str(v) => PickleValue::String(v.clone()),
204        MsgpackValue::Array(values) => {
205            PickleValue::List(values.iter().map(msgpack_to_pickle).collect())
206        }
207        MsgpackValue::Map(values) => PickleValue::Dict(
208            values
209                .iter()
210                .map(|(key, value)| (msgpack_to_pickle(key), msgpack_to_pickle(value)))
211                .collect(),
212        ),
213    }
214}
215
216/// Server-side authentication: challenge-response.
217fn server_auth(stream: &mut TcpStream, auth_key: &[u8; 32]) -> io::Result<()> {
218    // Generate challenge: #CHALLENGE#{sha256}<40 random bytes>
219    let mut random_bytes = [0u8; CHALLENGE_LEN];
220    // Use /dev/urandom for randomness
221    {
222        let mut f = std::fs::File::open("/dev/urandom")?;
223        f.read_exact(&mut random_bytes)?;
224    }
225
226    let mut challenge_message = Vec::with_capacity(CHALLENGE_PREFIX.len() + 8 + CHALLENGE_LEN);
227    challenge_message.extend_from_slice(CHALLENGE_PREFIX);
228    challenge_message.extend_from_slice(b"{sha256}");
229    challenge_message.extend_from_slice(&random_bytes);
230
231    send_bytes(stream, &challenge_message)?;
232
233    // Read response (max 256 bytes)
234    let response = recv_bytes(stream)?;
235
236    // Verify response
237    // The message to HMAC is everything after #CHALLENGE# (i.e. {sha256}<random>)
238    let message = &challenge_message[CHALLENGE_PREFIX.len()..];
239
240    if verify_response(auth_key, message, &response) {
241        send_bytes(stream, WELCOME)?;
242        Ok(())
243    } else {
244        send_bytes(stream, FAILURE)?;
245        Err(io::Error::new(
246            io::ErrorKind::PermissionDenied,
247            "auth failed",
248        ))
249    }
250}
251
252/// Verify a client's HMAC response.
253fn verify_response(auth_key: &[u8; 32], message: &[u8], response: &[u8]) -> bool {
254    // Modern protocol: response = {sha256}<hmac-sha256 digest>
255    if response.starts_with(b"{sha256}") {
256        let digest = &response[8..];
257        let expected = hmac_sha256(auth_key, message);
258        constant_time_eq(digest, &expected)
259    }
260    // Legacy protocol: response = raw 16-byte HMAC-MD5 digest
261    else if response.len() == 16 {
262        let expected = hmac_md5(auth_key, message);
263        constant_time_eq(response, &expected)
264    }
265    // Legacy with {md5} prefix
266    else if response.starts_with(b"{md5}") {
267        let digest = &response[5..];
268        let expected = hmac_md5(auth_key, message);
269        constant_time_eq(digest, &expected)
270    } else {
271        false
272    }
273}
274
275/// Constant-time byte comparison.
276fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
277    if a.len() != b.len() {
278        return false;
279    }
280    let mut diff = 0u8;
281    for (x, y) in a.iter().zip(b.iter()) {
282        diff |= x ^ y;
283    }
284    diff == 0
285}
286
287/// Send bytes with 4-byte big-endian length prefix.
288fn send_bytes(stream: &mut TcpStream, data: &[u8]) -> io::Result<()> {
289    let len = data.len() as i32;
290    stream.write_all(&len.to_be_bytes())?;
291    stream.write_all(data)?;
292    stream.flush()
293}
294
295/// Receive bytes with 4-byte big-endian length prefix.
296fn recv_bytes(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
297    let mut len_buf = [0u8; 4];
298    stream.read_exact(&mut len_buf)?;
299    let len = i32::from_be_bytes(len_buf);
300
301    if len < 0 {
302        // Extended format: 8-byte length
303        let mut len8_buf = [0u8; 8];
304        stream.read_exact(&mut len8_buf)?;
305        let len = u64::from_be_bytes(len8_buf) as usize;
306        if len > 64 * 1024 * 1024 {
307            return Err(io::Error::new(
308                io::ErrorKind::InvalidData,
309                "message too large",
310            ));
311        }
312        let mut buf = vec![0u8; len];
313        stream.read_exact(&mut buf)?;
314        Ok(buf)
315    } else {
316        let len = len as usize;
317        if len > 64 * 1024 * 1024 {
318            return Err(io::Error::new(
319                io::ErrorKind::InvalidData,
320                "message too large",
321            ));
322        }
323        let mut buf = vec![0u8; len];
324        stream.read_exact(&mut buf)?;
325        Ok(buf)
326    }
327}
328
329/// Translate an RPC request dict to a query event and get response.
330fn handle_rpc_request(request: &PickleValue, event_tx: &EventSender) -> io::Result<PickleValue> {
331    // Handle "get" requests
332    if let Some(get_val) = request.get("get") {
333        if let Some(path) = get_val.as_str() {
334            return match path {
335                "interface_stats" => {
336                    let resp = send_query(event_tx, QueryRequest::InterfaceStats)?;
337                    if let QueryResponse::InterfaceStats(stats) = resp {
338                        Ok(interface_stats_to_pickle(&stats))
339                    } else {
340                        Ok(PickleValue::None)
341                    }
342                }
343                "path_table" => {
344                    let max_hops = request
345                        .get("max_hops")
346                        .and_then(|v| v.as_int().map(|n| n as u8));
347                    let resp = send_query(event_tx, QueryRequest::PathTable { max_hops })?;
348                    if let QueryResponse::PathTable(entries) = resp {
349                        Ok(path_table_to_pickle(&entries))
350                    } else {
351                        Ok(PickleValue::None)
352                    }
353                }
354                "rate_table" => {
355                    let resp = send_query(event_tx, QueryRequest::RateTable)?;
356                    if let QueryResponse::RateTable(entries) = resp {
357                        Ok(rate_table_to_pickle(&entries))
358                    } else {
359                        Ok(PickleValue::None)
360                    }
361                }
362                "next_hop" => {
363                    let hash = extract_dest_hash(request, "destination_hash")?;
364                    let resp = send_query(event_tx, QueryRequest::NextHop { dest_hash: hash })?;
365                    if let QueryResponse::NextHop(Some(nh)) = resp {
366                        Ok(PickleValue::Bytes(nh.next_hop.to_vec()))
367                    } else {
368                        Ok(PickleValue::None)
369                    }
370                }
371                "next_hop_if_name" => {
372                    let hash = extract_dest_hash(request, "destination_hash")?;
373                    let resp =
374                        send_query(event_tx, QueryRequest::NextHopIfName { dest_hash: hash })?;
375                    if let QueryResponse::NextHopIfName(Some(name)) = resp {
376                        Ok(PickleValue::String(name))
377                    } else {
378                        Ok(PickleValue::None)
379                    }
380                }
381                "link_count" => {
382                    let resp = send_query(event_tx, QueryRequest::LinkCount)?;
383                    if let QueryResponse::LinkCount(n) = resp {
384                        Ok(PickleValue::Int(n as i64))
385                    } else {
386                        Ok(PickleValue::None)
387                    }
388                }
389                "transport_identity" => {
390                    let resp = send_query(event_tx, QueryRequest::TransportIdentity)?;
391                    if let QueryResponse::TransportIdentity(Some(hash)) = resp {
392                        Ok(PickleValue::Bytes(hash.to_vec()))
393                    } else {
394                        Ok(PickleValue::None)
395                    }
396                }
397                "blackholed" => {
398                    let resp = send_query(event_tx, QueryRequest::GetBlackholed)?;
399                    if let QueryResponse::Blackholed(entries) = resp {
400                        Ok(blackholed_to_pickle(&entries))
401                    } else {
402                        Ok(PickleValue::None)
403                    }
404                }
405                "discovered_interfaces" => {
406                    let only_available = request
407                        .get("only_available")
408                        .and_then(|v| v.as_bool())
409                        .unwrap_or(false);
410                    let only_transport = request
411                        .get("only_transport")
412                        .and_then(|v| v.as_bool())
413                        .unwrap_or(false);
414                    let resp = send_query(
415                        event_tx,
416                        QueryRequest::DiscoveredInterfaces {
417                            only_available,
418                            only_transport,
419                        },
420                    )?;
421                    if let QueryResponse::DiscoveredInterfaces(interfaces) = resp {
422                        Ok(discovered_interfaces_to_pickle(&interfaces))
423                    } else {
424                        Ok(PickleValue::None)
425                    }
426                }
427                "hooks" => {
428                    let (response_tx, response_rx) = mpsc::channel();
429                    event_tx
430                        .send(Event::ListHooks { response_tx })
431                        .map_err(|_| {
432                            io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down")
433                        })?;
434                    let hooks = response_rx
435                        .recv_timeout(std::time::Duration::from_secs(5))
436                        .map_err(|_| {
437                            io::Error::new(io::ErrorKind::TimedOut, "list hooks timed out")
438                        })?;
439                    Ok(hooks_to_pickle(&hooks))
440                }
441                "runtime_config" => {
442                    let resp = send_query(event_tx, QueryRequest::ListRuntimeConfig)?;
443                    if let QueryResponse::RuntimeConfigList(entries) = resp {
444                        Ok(runtime_config_list_to_pickle(&entries))
445                    } else {
446                        Ok(PickleValue::None)
447                    }
448                }
449                "known_destinations" => {
450                    let resp = send_query(event_tx, QueryRequest::KnownDestinations)?;
451                    if let QueryResponse::KnownDestinations(entries) = resp {
452                        Ok(known_destinations_to_pickle(&entries))
453                    } else {
454                        Ok(PickleValue::None)
455                    }
456                }
457                "runtime_config_entry" => {
458                    let key = request
459                        .get("key")
460                        .and_then(|v| v.as_str())
461                        .unwrap_or_default()
462                        .to_string();
463                    let resp = send_query(event_tx, QueryRequest::GetRuntimeConfig { key })?;
464                    if let QueryResponse::RuntimeConfigEntry(entry) = resp {
465                        Ok(entry
466                            .as_ref()
467                            .map(runtime_config_entry_to_pickle)
468                            .unwrap_or(PickleValue::None))
469                    } else {
470                        Ok(PickleValue::None)
471                    }
472                }
473                "backbone_peer_state" => {
474                    let interface_name = request
475                        .get("interface")
476                        .and_then(|v| v.as_str())
477                        .map(|s| s.to_string());
478                    let resp =
479                        send_query(event_tx, QueryRequest::BackbonePeerState { interface_name })?;
480                    if let QueryResponse::BackbonePeerState(entries) = resp {
481                        Ok(backbone_peer_state_to_pickle(&entries))
482                    } else {
483                        Ok(PickleValue::None)
484                    }
485                }
486                "backbone_interfaces" => {
487                    let resp = send_query(event_tx, QueryRequest::BackboneInterfaces)?;
488                    if let QueryResponse::BackboneInterfaces(entries) = resp {
489                        Ok(backbone_interfaces_to_pickle(&entries))
490                    } else {
491                        Ok(PickleValue::None)
492                    }
493                }
494                "provider_bridge_stats" => {
495                    let resp = send_query(event_tx, QueryRequest::ProviderBridgeStats)?;
496                    if let QueryResponse::ProviderBridgeStats(stats) = resp {
497                        Ok(stats
498                            .as_ref()
499                            .map(provider_bridge_stats_to_pickle)
500                            .unwrap_or(PickleValue::None))
501                    } else {
502                        Ok(PickleValue::None)
503                    }
504                }
505                "drain_status" => {
506                    let resp = send_query(event_tx, QueryRequest::DrainStatus)?;
507                    if let QueryResponse::DrainStatus(status) = resp {
508                        Ok(drain_status_to_pickle(&status))
509                    } else {
510                        Ok(PickleValue::None)
511                    }
512                }
513                _ => Ok(PickleValue::None),
514            };
515        }
516    }
517
518    if let Some(begin_val) = request.get("begin_drain") {
519        let timeout_secs = begin_val
520            .as_float()
521            .or_else(|| begin_val.as_int().map(|value| value as f64))
522            .unwrap_or(0.0)
523            .max(0.0);
524        let timeout = Duration::from_secs_f64(timeout_secs);
525        let _ = event_tx.send(Event::BeginDrain { timeout });
526        return Ok(PickleValue::Bool(true));
527    }
528
529    if let Some(set_val) = request.get("set").and_then(|v| v.as_str()) {
530        if set_val == "known_destination_retained" {
531            let dest_hash = extract_dest_hash(request, "dest_hash")?;
532            let resp = send_query(event_tx, QueryRequest::RetainKnownDestination { dest_hash })?;
533            return if let QueryResponse::RetainKnownDestination(ok) = resp {
534                Ok(PickleValue::Bool(ok))
535            } else {
536                Ok(PickleValue::None)
537            };
538        }
539        if set_val == "identity_retained" {
540            let identity_hash = extract_dest_hash(request, "identity_hash")?;
541            let resp = send_query(event_tx, QueryRequest::RetainIdentity { identity_hash })?;
542            return if let QueryResponse::RetainIdentity(ok) = resp {
543                Ok(PickleValue::Bool(ok))
544            } else {
545                Ok(PickleValue::None)
546            };
547        }
548        if set_val == "known_destination_used" {
549            let dest_hash = extract_dest_hash(request, "dest_hash")?;
550            let resp = send_query(
551                event_tx,
552                QueryRequest::MarkKnownDestinationUsed { dest_hash },
553            )?;
554            return if let QueryResponse::MarkKnownDestinationUsed(ok) = resp {
555                Ok(PickleValue::Bool(ok))
556            } else {
557                Ok(PickleValue::None)
558            };
559        }
560        if set_val == "runtime_config" {
561            let key = request
562                .get("key")
563                .and_then(|v| v.as_str())
564                .unwrap_or_default()
565                .to_string();
566            let Some(value) = request
567                .get("value")
568                .and_then(runtime_config_value_from_pickle)
569            else {
570                return Ok(runtime_config_error_to_pickle(&RuntimeConfigError {
571                    code: RuntimeConfigErrorCode::InvalidType,
572                    message: "runtime-config set requires a scalar value".into(),
573                }));
574            };
575            let resp = send_query(event_tx, QueryRequest::SetRuntimeConfig { key, value })?;
576            return if let QueryResponse::RuntimeConfigSet(result) = resp {
577                Ok(runtime_config_result_to_pickle(result))
578            } else {
579                Ok(PickleValue::None)
580            };
581        }
582    }
583
584    if let Some(reset_val) = request.get("reset").and_then(|v| v.as_str()) {
585        if reset_val == "runtime_config" {
586            let key = request
587                .get("key")
588                .and_then(|v| v.as_str())
589                .unwrap_or_default()
590                .to_string();
591            let resp = send_query(event_tx, QueryRequest::ResetRuntimeConfig { key })?;
592            return if let QueryResponse::RuntimeConfigReset(result) = resp {
593                Ok(runtime_config_result_to_pickle(result))
594            } else {
595                Ok(PickleValue::None)
596            };
597        }
598    }
599
600    if let Some(clear_val) = request.get("clear").and_then(|v| v.as_str()) {
601        if clear_val == "known_destination_retained" {
602            let dest_hash = extract_dest_hash(request, "dest_hash")?;
603            let resp = send_query(
604                event_tx,
605                QueryRequest::UnretainKnownDestination { dest_hash },
606            )?;
607            return if let QueryResponse::UnretainKnownDestination(ok) = resp {
608                Ok(PickleValue::Bool(ok))
609            } else {
610                Ok(PickleValue::None)
611            };
612        }
613        if clear_val == "backbone_peer_state" {
614            let interface_name = required_string(request, "interface")?;
615            let peer_ip = required_string(request, "ip")?;
616            let peer_ip = peer_ip
617                .parse()
618                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid peer IP"))?;
619            let resp = send_query(
620                event_tx,
621                QueryRequest::ClearBackbonePeerState {
622                    interface_name,
623                    peer_ip,
624                },
625            )?;
626            return if let QueryResponse::ClearBackbonePeerState(ok) = resp {
627                Ok(PickleValue::Bool(ok))
628            } else {
629                Ok(PickleValue::None)
630            };
631        }
632    }
633
634    if let Some(set_val) = request.get("set").and_then(|v| v.as_str()) {
635        if set_val == "backbone_peer_blacklist" {
636            let interface_name = required_string(request, "interface")?;
637            let peer_ip = required_string(request, "ip")?;
638            let peer_ip: IpAddr = peer_ip
639                .parse()
640                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid peer IP"))?;
641            let duration_secs = request
642                .get("duration_secs")
643                .and_then(|v| v.as_int())
644                .ok_or_else(|| {
645                    io::Error::new(io::ErrorKind::InvalidInput, "missing duration_secs")
646                })?;
647            let reason = request
648                .get("reason")
649                .and_then(|v| v.as_str())
650                .unwrap_or("sentinel blacklist")
651                .to_string();
652            let penalty_level = request
653                .get("penalty_level")
654                .and_then(|v| v.as_int())
655                .unwrap_or(0)
656                .clamp(0, u8::MAX as i64) as u8;
657            let resp = send_query(
658                event_tx,
659                QueryRequest::BlacklistBackbonePeer {
660                    interface_name,
661                    peer_ip,
662                    duration: Duration::from_secs(duration_secs as u64),
663                    reason,
664                    penalty_level,
665                },
666            )?;
667            return if let QueryResponse::BlacklistBackbonePeer(ok) = resp {
668                Ok(PickleValue::Bool(ok))
669            } else {
670                Ok(PickleValue::None)
671            };
672        }
673    }
674
675    // Handle "request_path" -- trigger a path request to the network
676    if let Some(hash_val) = request.get("request_path") {
677        if let Some(hash_bytes) = hash_val.as_bytes() {
678            if hash_bytes.len() >= 16 {
679                let mut dest_hash = [0u8; 16];
680                dest_hash.copy_from_slice(&hash_bytes[..16]);
681                let _ = event_tx.send(crate::event::Event::RequestPath { dest_hash });
682                return Ok(PickleValue::Bool(true));
683            }
684        }
685    }
686
687    // Handle "send_probe" requests
688    if let Some(hash_val) = request.get("send_probe") {
689        if let Some(hash_bytes) = hash_val.as_bytes() {
690            if hash_bytes.len() >= 16 {
691                let mut dest_hash = [0u8; 16];
692                dest_hash.copy_from_slice(&hash_bytes[..16]);
693                let payload_size = request
694                    .get("size")
695                    .and_then(|v| v.as_int())
696                    .and_then(|n| {
697                        if n > 0 && n <= 400 {
698                            Some(n as usize)
699                        } else {
700                            None
701                        }
702                    })
703                    .unwrap_or(16);
704                let resp = send_query(
705                    event_tx,
706                    QueryRequest::SendProbe {
707                        dest_hash,
708                        payload_size,
709                    },
710                )?;
711                if let QueryResponse::SendProbe(Some((packet_hash, hops))) = resp {
712                    return Ok(PickleValue::Dict(vec![
713                        (
714                            PickleValue::String("packet_hash".into()),
715                            PickleValue::Bytes(packet_hash.to_vec()),
716                        ),
717                        (
718                            PickleValue::String("hops".into()),
719                            PickleValue::Int(hops as i64),
720                        ),
721                    ]));
722                } else {
723                    return Ok(PickleValue::None);
724                }
725            }
726        }
727    }
728
729    // Handle "check_proof" requests
730    if let Some(hash_val) = request.get("check_proof") {
731        if let Some(hash_bytes) = hash_val.as_bytes() {
732            if hash_bytes.len() >= 32 {
733                let mut packet_hash = [0u8; 32];
734                packet_hash.copy_from_slice(&hash_bytes[..32]);
735                let resp = send_query(event_tx, QueryRequest::CheckProof { packet_hash })?;
736                if let QueryResponse::CheckProof(Some(rtt)) = resp {
737                    return Ok(PickleValue::Float(rtt));
738                } else {
739                    return Ok(PickleValue::None);
740                }
741            }
742        }
743    }
744
745    // Handle "blackhole" requests
746    if let Some(hash_val) = request.get("blackhole") {
747        if let Some(hash_bytes) = hash_val.as_bytes() {
748            if hash_bytes.len() >= 16 {
749                let mut identity_hash = [0u8; 16];
750                identity_hash.copy_from_slice(&hash_bytes[..16]);
751                let duration_hours = request.get("duration").and_then(|v| v.as_float());
752                let reason = request
753                    .get("reason")
754                    .and_then(|v| v.as_str())
755                    .map(|s| s.to_string());
756                let resp = send_query(
757                    event_tx,
758                    QueryRequest::BlackholeIdentity {
759                        identity_hash,
760                        duration_hours,
761                        reason,
762                    },
763                )?;
764                return Ok(PickleValue::Bool(matches!(
765                    resp,
766                    QueryResponse::BlackholeResult(true)
767                )));
768            }
769        }
770    }
771
772    // Handle "unblackhole" requests
773    if let Some(hash_val) = request.get("unblackhole") {
774        if let Some(hash_bytes) = hash_val.as_bytes() {
775            if hash_bytes.len() >= 16 {
776                let mut identity_hash = [0u8; 16];
777                identity_hash.copy_from_slice(&hash_bytes[..16]);
778                let resp = send_query(
779                    event_tx,
780                    QueryRequest::UnblackholeIdentity { identity_hash },
781                )?;
782                return Ok(PickleValue::Bool(matches!(
783                    resp,
784                    QueryResponse::UnblackholeResult(true)
785                )));
786            }
787        }
788    }
789
790    // Handle "drop" requests
791    if let Some(drop_val) = request.get("drop") {
792        if let Some(path) = drop_val.as_str() {
793            return match path {
794                "path" => {
795                    let hash = extract_dest_hash(request, "destination_hash")?;
796                    let resp = send_query(event_tx, QueryRequest::DropPath { dest_hash: hash })?;
797                    if let QueryResponse::DropPath(ok) = resp {
798                        Ok(PickleValue::Bool(ok))
799                    } else {
800                        Ok(PickleValue::None)
801                    }
802                }
803                "all_via" => {
804                    let hash = extract_dest_hash(request, "destination_hash")?;
805                    let resp = send_query(
806                        event_tx,
807                        QueryRequest::DropAllVia {
808                            transport_hash: hash,
809                        },
810                    )?;
811                    if let QueryResponse::DropAllVia(n) = resp {
812                        Ok(PickleValue::Int(n as i64))
813                    } else {
814                        Ok(PickleValue::None)
815                    }
816                }
817                "announce_queues" => {
818                    let resp = send_query(event_tx, QueryRequest::DropAnnounceQueues)?;
819                    if let QueryResponse::DropAnnounceQueues = resp {
820                        Ok(PickleValue::Bool(true))
821                    } else {
822                        Ok(PickleValue::None)
823                    }
824                }
825                _ => Ok(PickleValue::None),
826            };
827        }
828    }
829
830    if let Some(hook_val) = request.get("hook").and_then(|v| v.as_str()) {
831        return handle_hook_rpc_request(hook_val, request, event_tx);
832    }
833
834    Ok(PickleValue::None)
835}
836
837fn handle_hook_rpc_request(
838    op: &str,
839    request: &PickleValue,
840    event_tx: &EventSender,
841) -> io::Result<PickleValue> {
842    match op {
843        "load" => {
844            let name = required_string(request, "name")?;
845            let attach_point = required_string(request, "attach_point")?;
846            let priority = request
847                .get("priority")
848                .and_then(|v| v.as_int())
849                .unwrap_or(0) as i32;
850            let wasm = request
851                .get("wasm")
852                .and_then(|v| v.as_bytes())
853                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing wasm"))?
854                .to_vec();
855            let (response_tx, response_rx) = mpsc::channel();
856            event_tx
857                .send(Event::LoadHook {
858                    name,
859                    wasm_bytes: wasm,
860                    attach_point,
861                    priority,
862                    response_tx,
863                })
864                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down"))?;
865            let response = response_rx
866                .recv_timeout(std::time::Duration::from_secs(5))
867                .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "hook load timed out"))?;
868            Ok(hook_result_to_pickle(response))
869        }
870        "load_builtin" => {
871            let name = required_string(request, "name")?;
872            let attach_point = required_string(request, "attach_point")?;
873            let builtin_id = required_string(request, "builtin_id")?;
874            let priority = request
875                .get("priority")
876                .and_then(|v| v.as_int())
877                .unwrap_or(0) as i32;
878            let (response_tx, response_rx) = mpsc::channel();
879            event_tx
880                .send(Event::LoadBuiltinHook {
881                    name,
882                    builtin_id,
883                    attach_point,
884                    priority,
885                    response_tx,
886                })
887                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down"))?;
888            let response = response_rx
889                .recv_timeout(std::time::Duration::from_secs(5))
890                .map_err(|_| {
891                    io::Error::new(io::ErrorKind::TimedOut, "built-in hook load timed out")
892                })?;
893            Ok(hook_result_to_pickle(response))
894        }
895        "unload" => {
896            let name = required_string(request, "name")?;
897            let attach_point = required_string(request, "attach_point")?;
898            let (response_tx, response_rx) = mpsc::channel();
899            event_tx
900                .send(Event::UnloadHook {
901                    name,
902                    attach_point,
903                    response_tx,
904                })
905                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down"))?;
906            let response = response_rx
907                .recv_timeout(std::time::Duration::from_secs(5))
908                .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "hook unload timed out"))?;
909            Ok(hook_result_to_pickle(response))
910        }
911        "enable" | "disable" => {
912            let name = required_string(request, "name")?;
913            let attach_point = required_string(request, "attach_point")?;
914            let enabled = op == "enable";
915            let (response_tx, response_rx) = mpsc::channel();
916            event_tx
917                .send(Event::SetHookEnabled {
918                    name,
919                    attach_point,
920                    enabled,
921                    response_tx,
922                })
923                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down"))?;
924            let response = response_rx
925                .recv_timeout(std::time::Duration::from_secs(5))
926                .map_err(|_| {
927                    io::Error::new(io::ErrorKind::TimedOut, "hook enable/disable timed out")
928                })?;
929            Ok(hook_result_to_pickle(response))
930        }
931        "set_priority" => {
932            let name = required_string(request, "name")?;
933            let attach_point = required_string(request, "attach_point")?;
934            let priority = request
935                .get("priority")
936                .and_then(|v| v.as_int())
937                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing priority"))?
938                as i32;
939            let (response_tx, response_rx) = mpsc::channel();
940            event_tx
941                .send(Event::SetHookPriority {
942                    name,
943                    attach_point,
944                    priority,
945                    response_tx,
946                })
947                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down"))?;
948            let response = response_rx
949                .recv_timeout(std::time::Duration::from_secs(5))
950                .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "hook priority timed out"))?;
951            Ok(hook_result_to_pickle(response))
952        }
953        _ => Ok(PickleValue::None),
954    }
955}
956
957/// Send a query to the driver and wait for the response.
958fn send_query(event_tx: &EventSender, request: QueryRequest) -> io::Result<QueryResponse> {
959    let (resp_tx, resp_rx) = mpsc::channel();
960    event_tx
961        .send(Event::Query(request, resp_tx))
962        .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "driver shut down"))?;
963    resp_rx
964        .recv_timeout(std::time::Duration::from_secs(5))
965        .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "query timed out"))
966}
967
968/// Extract a 16-byte destination hash from an RPC dict field.
969fn extract_dest_hash(request: &PickleValue, key: &str) -> io::Result<[u8; 16]> {
970    let bytes = request
971        .get(key)
972        .and_then(|v| v.as_bytes())
973        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing destination_hash"))?;
974    if bytes.len() < 16 {
975        return Err(io::Error::new(io::ErrorKind::InvalidData, "hash too short"));
976    }
977    let mut hash = [0u8; 16];
978    hash.copy_from_slice(&bytes[..16]);
979    Ok(hash)
980}
981
982fn required_string(request: &PickleValue, key: &str) -> io::Result<String> {
983    request
984        .get(key)
985        .and_then(|v| v.as_str())
986        .map(|s| s.to_string())
987        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("missing {}", key)))
988}
989
990fn hook_result_to_pickle(result: Result<(), String>) -> PickleValue {
991    match result {
992        Ok(()) => PickleValue::Dict(vec![(
993            PickleValue::String("ok".into()),
994            PickleValue::Bool(true),
995        )]),
996        Err(error) => PickleValue::Dict(vec![
997            (PickleValue::String("ok".into()), PickleValue::Bool(false)),
998            (
999                PickleValue::String("error".into()),
1000                PickleValue::String(error),
1001            ),
1002        ]),
1003    }
1004}
1005
1006// --- Pickle response builders ---
1007
1008fn interface_stats_to_pickle(stats: &InterfaceStatsResponse) -> PickleValue {
1009    let mut ifaces = Vec::new();
1010    for iface in &stats.interfaces {
1011        ifaces.push(single_iface_to_pickle(iface));
1012    }
1013
1014    let mut dict = vec![
1015        (
1016            PickleValue::String("interfaces".into()),
1017            PickleValue::List(ifaces),
1018        ),
1019        (
1020            PickleValue::String("transport_enabled".into()),
1021            PickleValue::Bool(stats.transport_enabled),
1022        ),
1023        (
1024            PickleValue::String("transport_uptime".into()),
1025            PickleValue::Float(stats.transport_uptime),
1026        ),
1027        (
1028            PickleValue::String("rxb".into()),
1029            PickleValue::Int(stats.total_rxb as i64),
1030        ),
1031        (
1032            PickleValue::String("txb".into()),
1033            PickleValue::Int(stats.total_txb as i64),
1034        ),
1035    ];
1036
1037    if let Some(tid) = stats.transport_id {
1038        dict.push((
1039            PickleValue::String("transport_id".into()),
1040            PickleValue::Bytes(tid.to_vec()),
1041        ));
1042    } else {
1043        dict.push((
1044            PickleValue::String("transport_id".into()),
1045            PickleValue::None,
1046        ));
1047    }
1048
1049    if let Some(pr) = stats.probe_responder {
1050        dict.push((
1051            PickleValue::String("probe_responder".into()),
1052            PickleValue::Bytes(pr.to_vec()),
1053        ));
1054    } else {
1055        dict.push((
1056            PickleValue::String("probe_responder".into()),
1057            PickleValue::None,
1058        ));
1059    }
1060
1061    if let Some(pool) = &stats.backbone_peer_pool {
1062        let members = pool
1063            .members
1064            .iter()
1065            .map(|member| {
1066                let mut member_dict = vec![
1067                    (
1068                        PickleValue::String("name".into()),
1069                        PickleValue::String(member.name.clone()),
1070                    ),
1071                    (
1072                        PickleValue::String("remote".into()),
1073                        PickleValue::String(member.remote.clone()),
1074                    ),
1075                    (
1076                        PickleValue::String("source".into()),
1077                        PickleValue::String(member.source.clone()),
1078                    ),
1079                    (
1080                        PickleValue::String("priority".into()),
1081                        PickleValue::Int(member.priority as i64),
1082                    ),
1083                    (
1084                        PickleValue::String("state".into()),
1085                        PickleValue::String(member.state.clone()),
1086                    ),
1087                    (
1088                        PickleValue::String("failure_count".into()),
1089                        PickleValue::Int(member.failure_count as i64),
1090                    ),
1091                ];
1092                member_dict.push((
1093                    PickleValue::String("interface_id".into()),
1094                    member
1095                        .interface_id
1096                        .map(|id| PickleValue::Int(id as i64))
1097                        .unwrap_or(PickleValue::None),
1098                ));
1099                member_dict.push((
1100                    PickleValue::String("last_error".into()),
1101                    member
1102                        .last_error
1103                        .as_ref()
1104                        .map(|err| PickleValue::String(err.clone()))
1105                        .unwrap_or(PickleValue::None),
1106                ));
1107                member_dict.push((
1108                    PickleValue::String("cooldown_remaining_seconds".into()),
1109                    member
1110                        .cooldown_remaining_seconds
1111                        .map(PickleValue::Float)
1112                        .unwrap_or(PickleValue::None),
1113                ));
1114                PickleValue::Dict(member_dict)
1115            })
1116            .collect();
1117        dict.push((
1118            PickleValue::String("backbone_peer_pool".into()),
1119            PickleValue::Dict(vec![
1120                (
1121                    PickleValue::String("max_connected".into()),
1122                    PickleValue::Int(pool.max_connected as i64),
1123                ),
1124                (
1125                    PickleValue::String("active_count".into()),
1126                    PickleValue::Int(pool.active_count as i64),
1127                ),
1128                (
1129                    PickleValue::String("standby_count".into()),
1130                    PickleValue::Int(pool.standby_count as i64),
1131                ),
1132                (
1133                    PickleValue::String("cooldown_count".into()),
1134                    PickleValue::Int(pool.cooldown_count as i64),
1135                ),
1136                (
1137                    PickleValue::String("members".into()),
1138                    PickleValue::List(members),
1139                ),
1140            ]),
1141        ));
1142    } else {
1143        dict.push((
1144            PickleValue::String("backbone_peer_pool".into()),
1145            PickleValue::None,
1146        ));
1147    }
1148
1149    PickleValue::Dict(dict)
1150}
1151
1152fn single_iface_to_pickle(s: &SingleInterfaceStat) -> PickleValue {
1153    let mut dict = vec![
1154        (
1155            PickleValue::String("id".into()),
1156            PickleValue::Int(s.id as i64),
1157        ),
1158        (
1159            PickleValue::String("name".into()),
1160            PickleValue::String(s.name.clone()),
1161        ),
1162        (
1163            PickleValue::String("status".into()),
1164            PickleValue::Bool(s.status),
1165        ),
1166        (
1167            PickleValue::String("mode".into()),
1168            PickleValue::Int(s.mode as i64),
1169        ),
1170        (
1171            PickleValue::String("gravity".into()),
1172            PickleValue::Int(s.gravity),
1173        ),
1174        (
1175            PickleValue::String("announces_to_internal".into()),
1176            s.announces_to_internal
1177                .map(PickleValue::Bool)
1178                .unwrap_or(PickleValue::None),
1179        ),
1180        (
1181            PickleValue::String("rxb".into()),
1182            PickleValue::Int(s.rxb as i64),
1183        ),
1184        (
1185            PickleValue::String("txb".into()),
1186            PickleValue::Int(s.txb as i64),
1187        ),
1188        (
1189            PickleValue::String("rx_packets".into()),
1190            PickleValue::Int(s.rx_packets as i64),
1191        ),
1192        (
1193            PickleValue::String("tx_packets".into()),
1194            PickleValue::Int(s.tx_packets as i64),
1195        ),
1196        (
1197            PickleValue::String("cpu_load".into()),
1198            s.cpu_load
1199                .map(PickleValue::Float)
1200                .unwrap_or(PickleValue::None),
1201        ),
1202        (
1203            PickleValue::String("mem_load".into()),
1204            s.mem_load
1205                .map(PickleValue::Float)
1206                .unwrap_or(PickleValue::None),
1207        ),
1208        (
1209            PickleValue::String("switch_id".into()),
1210            s.switch_id
1211                .map(|v| PickleValue::Bytes(v.to_vec()))
1212                .unwrap_or(PickleValue::None),
1213        ),
1214        (
1215            PickleValue::String("endpoint_id".into()),
1216            s.endpoint_id
1217                .map(|v| PickleValue::Bytes(v.to_vec()))
1218                .unwrap_or(PickleValue::None),
1219        ),
1220        (
1221            PickleValue::String("via_switch_id".into()),
1222            s.via_switch_id
1223                .map(|v| PickleValue::Bytes(v.to_vec()))
1224                .unwrap_or(PickleValue::None),
1225        ),
1226        (
1227            PickleValue::String("peers".into()),
1228            s.peers
1229                .map(|v| PickleValue::Int(v as i64))
1230                .unwrap_or(PickleValue::None),
1231        ),
1232        (
1233            PickleValue::String("started".into()),
1234            PickleValue::Float(s.started),
1235        ),
1236        (
1237            PickleValue::String("ia_freq".into()),
1238            PickleValue::Float(s.ia_freq),
1239        ),
1240        (
1241            PickleValue::String("oa_freq".into()),
1242            PickleValue::Float(s.oa_freq),
1243        ),
1244        (
1245            PickleValue::String("ip_freq".into()),
1246            PickleValue::Float(s.ip_freq),
1247        ),
1248        (
1249            PickleValue::String("op_freq".into()),
1250            PickleValue::Float(s.op_freq),
1251        ),
1252        (
1253            PickleValue::String("burst_active".into()),
1254            PickleValue::Bool(s.burst_active),
1255        ),
1256        (
1257            PickleValue::String("burst_activated".into()),
1258            PickleValue::Float(s.burst_activated),
1259        ),
1260        (
1261            PickleValue::String("pr_burst_active".into()),
1262            PickleValue::Bool(s.pr_burst_active),
1263        ),
1264        (
1265            PickleValue::String("pr_burst_activated".into()),
1266            PickleValue::Float(s.pr_burst_activated),
1267        ),
1268        (
1269            PickleValue::String("clients".into()),
1270            s.clients
1271                .map(|clients| PickleValue::Int(clients as i64))
1272                .unwrap_or(PickleValue::None),
1273        ),
1274        (
1275            PickleValue::String("blocked_ips".into()),
1276            s.blocked_ips
1277                .map(|blocked| PickleValue::Int(blocked as i64))
1278                .unwrap_or(PickleValue::None),
1279        ),
1280        (
1281            PickleValue::String("blocked_ip_list".into()),
1282            s.blocked_ip_list
1283                .as_ref()
1284                .map(|blocked| {
1285                    PickleValue::List(blocked.iter().cloned().map(PickleValue::String).collect())
1286                })
1287                .unwrap_or(PickleValue::None),
1288        ),
1289        (
1290            PickleValue::String("announce_rate_grace".into()),
1291            PickleValue::Int(s.announce_rate_grace as i64),
1292        ),
1293        (
1294            PickleValue::String("announce_rate_penalty".into()),
1295            PickleValue::Float(s.announce_rate_penalty),
1296        ),
1297    ];
1298
1299    match s.announce_rate_target {
1300        Some(target) => dict.push((
1301            PickleValue::String("announce_rate_target".into()),
1302            PickleValue::Float(target),
1303        )),
1304        None => dict.push((
1305            PickleValue::String("announce_rate_target".into()),
1306            PickleValue::None,
1307        )),
1308    }
1309
1310    match s.bitrate {
1311        Some(br) => dict.push((
1312            PickleValue::String("bitrate".into()),
1313            PickleValue::Int(br as i64),
1314        )),
1315        None => dict.push((PickleValue::String("bitrate".into()), PickleValue::None)),
1316    }
1317
1318    match s.ifac_size {
1319        Some(sz) => dict.push((
1320            PickleValue::String("ifac_size".into()),
1321            PickleValue::Int(sz as i64),
1322        )),
1323        None => dict.push((PickleValue::String("ifac_size".into()), PickleValue::None)),
1324    }
1325
1326    PickleValue::Dict(dict)
1327}
1328
1329fn path_table_to_pickle(entries: &[PathTableEntry]) -> PickleValue {
1330    let list: Vec<PickleValue> = entries
1331        .iter()
1332        .map(|e| {
1333            PickleValue::Dict(vec![
1334                (
1335                    PickleValue::String("hash".into()),
1336                    PickleValue::Bytes(e.hash.to_vec()),
1337                ),
1338                (
1339                    PickleValue::String("timestamp".into()),
1340                    PickleValue::Float(e.timestamp),
1341                ),
1342                (
1343                    PickleValue::String("via".into()),
1344                    PickleValue::Bytes(e.via.to_vec()),
1345                ),
1346                (
1347                    PickleValue::String("hops".into()),
1348                    PickleValue::Int(e.hops as i64),
1349                ),
1350                (
1351                    PickleValue::String("expires".into()),
1352                    PickleValue::Float(e.expires),
1353                ),
1354                (
1355                    PickleValue::String("interface".into()),
1356                    PickleValue::String(e.interface_name.clone()),
1357                ),
1358            ])
1359        })
1360        .collect();
1361    PickleValue::List(list)
1362}
1363
1364fn rate_table_to_pickle(entries: &[RateTableEntry]) -> PickleValue {
1365    let list: Vec<PickleValue> = entries
1366        .iter()
1367        .map(|e| {
1368            PickleValue::Dict(vec![
1369                (
1370                    PickleValue::String("hash".into()),
1371                    PickleValue::Bytes(e.hash.to_vec()),
1372                ),
1373                (
1374                    PickleValue::String("last".into()),
1375                    PickleValue::Float(e.last),
1376                ),
1377                (
1378                    PickleValue::String("rate_violations".into()),
1379                    PickleValue::Int(e.rate_violations as i64),
1380                ),
1381                (
1382                    PickleValue::String("blocked_until".into()),
1383                    PickleValue::Float(e.blocked_until),
1384                ),
1385                (
1386                    PickleValue::String("timestamps".into()),
1387                    PickleValue::List(
1388                        e.timestamps
1389                            .iter()
1390                            .map(|&t| PickleValue::Float(t))
1391                            .collect(),
1392                    ),
1393                ),
1394            ])
1395        })
1396        .collect();
1397    PickleValue::List(list)
1398}
1399
1400fn blackholed_to_pickle(entries: &[BlackholeInfo]) -> PickleValue {
1401    let list: Vec<PickleValue> = entries
1402        .iter()
1403        .map(|e| {
1404            let mut dict = vec![
1405                (
1406                    PickleValue::String("identity_hash".into()),
1407                    PickleValue::Bytes(e.identity_hash.to_vec()),
1408                ),
1409                (
1410                    PickleValue::String("created".into()),
1411                    PickleValue::Float(e.created),
1412                ),
1413                (
1414                    PickleValue::String("expires".into()),
1415                    PickleValue::Float(e.expires),
1416                ),
1417            ];
1418            if let Some(ref reason) = e.reason {
1419                dict.push((
1420                    PickleValue::String("reason".into()),
1421                    PickleValue::String(reason.clone()),
1422                ));
1423            } else {
1424                dict.push((PickleValue::String("reason".into()), PickleValue::None));
1425            }
1426            PickleValue::Dict(dict)
1427        })
1428        .collect();
1429    PickleValue::List(list)
1430}
1431
1432fn discovered_interfaces_to_pickle(
1433    interfaces: &[crate::discovery::DiscoveredInterface],
1434) -> PickleValue {
1435    let list: Vec<PickleValue> = interfaces
1436        .iter()
1437        .map(|iface| {
1438            let mut dict = vec![
1439                (
1440                    PickleValue::String("type".into()),
1441                    PickleValue::String(iface.interface_type.clone()),
1442                ),
1443                (
1444                    PickleValue::String("transport".into()),
1445                    PickleValue::Bool(iface.transport),
1446                ),
1447                (
1448                    PickleValue::String("name".into()),
1449                    PickleValue::String(iface.name.clone()),
1450                ),
1451                (
1452                    PickleValue::String("discovered".into()),
1453                    PickleValue::Float(iface.discovered),
1454                ),
1455                (
1456                    PickleValue::String("last_heard".into()),
1457                    PickleValue::Float(iface.last_heard),
1458                ),
1459                (
1460                    PickleValue::String("heard_count".into()),
1461                    PickleValue::Int(iface.heard_count as i64),
1462                ),
1463                (
1464                    PickleValue::String("status".into()),
1465                    PickleValue::String(iface.status.as_str().into()),
1466                ),
1467                (
1468                    PickleValue::String("stamp".into()),
1469                    PickleValue::Bytes(iface.stamp.clone()),
1470                ),
1471                (
1472                    PickleValue::String("value".into()),
1473                    PickleValue::Int(iface.stamp_value as i64),
1474                ),
1475                (
1476                    PickleValue::String("transport_id".into()),
1477                    PickleValue::Bytes(iface.transport_id.to_vec()),
1478                ),
1479                (
1480                    PickleValue::String("network_id".into()),
1481                    PickleValue::Bytes(iface.network_id.to_vec()),
1482                ),
1483                (
1484                    PickleValue::String("hops".into()),
1485                    PickleValue::Int(iface.hops as i64),
1486                ),
1487            ];
1488
1489            // Optional location fields
1490            if let Some(v) = iface.latitude {
1491                dict.push((
1492                    PickleValue::String("latitude".into()),
1493                    PickleValue::Float(v),
1494                ));
1495            } else {
1496                dict.push((PickleValue::String("latitude".into()), PickleValue::None));
1497            }
1498            if let Some(v) = iface.longitude {
1499                dict.push((
1500                    PickleValue::String("longitude".into()),
1501                    PickleValue::Float(v),
1502                ));
1503            } else {
1504                dict.push((PickleValue::String("longitude".into()), PickleValue::None));
1505            }
1506            if let Some(v) = iface.height {
1507                dict.push((PickleValue::String("height".into()), PickleValue::Float(v)));
1508            } else {
1509                dict.push((PickleValue::String("height".into()), PickleValue::None));
1510            }
1511
1512            // Connection info
1513            if let Some(ref v) = iface.reachable_on {
1514                dict.push((
1515                    PickleValue::String("reachable_on".into()),
1516                    PickleValue::String(v.clone()),
1517                ));
1518            } else {
1519                dict.push((
1520                    PickleValue::String("reachable_on".into()),
1521                    PickleValue::None,
1522                ));
1523            }
1524            if let Some(v) = iface.port {
1525                dict.push((
1526                    PickleValue::String("port".into()),
1527                    PickleValue::Int(v as i64),
1528                ));
1529            } else {
1530                dict.push((PickleValue::String("port".into()), PickleValue::None));
1531            }
1532
1533            // RNode/RF specific
1534            if let Some(v) = iface.frequency {
1535                dict.push((
1536                    PickleValue::String("frequency".into()),
1537                    PickleValue::Int(v as i64),
1538                ));
1539            } else {
1540                dict.push((PickleValue::String("frequency".into()), PickleValue::None));
1541            }
1542            if let Some(v) = iface.bandwidth {
1543                dict.push((
1544                    PickleValue::String("bandwidth".into()),
1545                    PickleValue::Int(v as i64),
1546                ));
1547            } else {
1548                dict.push((PickleValue::String("bandwidth".into()), PickleValue::None));
1549            }
1550            if let Some(v) = iface.spreading_factor {
1551                dict.push((PickleValue::String("sf".into()), PickleValue::Int(v as i64)));
1552            } else {
1553                dict.push((PickleValue::String("sf".into()), PickleValue::None));
1554            }
1555            if let Some(v) = iface.coding_rate {
1556                dict.push((PickleValue::String("cr".into()), PickleValue::Int(v as i64)));
1557            } else {
1558                dict.push((PickleValue::String("cr".into()), PickleValue::None));
1559            }
1560            if let Some(ref v) = iface.modulation {
1561                dict.push((
1562                    PickleValue::String("modulation".into()),
1563                    PickleValue::String(v.clone()),
1564                ));
1565            } else {
1566                dict.push((PickleValue::String("modulation".into()), PickleValue::None));
1567            }
1568            if let Some(v) = iface.channel {
1569                dict.push((
1570                    PickleValue::String("channel".into()),
1571                    PickleValue::Int(v as i64),
1572                ));
1573            } else {
1574                dict.push((PickleValue::String("channel".into()), PickleValue::None));
1575            }
1576
1577            // IFAC info
1578            if let Some(ref v) = iface.ifac_netname {
1579                dict.push((
1580                    PickleValue::String("ifac_netname".into()),
1581                    PickleValue::String(v.clone()),
1582                ));
1583            } else {
1584                dict.push((
1585                    PickleValue::String("ifac_netname".into()),
1586                    PickleValue::None,
1587                ));
1588            }
1589            if let Some(ref v) = iface.ifac_netkey {
1590                dict.push((
1591                    PickleValue::String("ifac_netkey".into()),
1592                    PickleValue::String(v.clone()),
1593                ));
1594            } else {
1595                dict.push((PickleValue::String("ifac_netkey".into()), PickleValue::None));
1596            }
1597
1598            // Config entry
1599            if let Some(ref v) = iface.config_entry {
1600                dict.push((
1601                    PickleValue::String("config_entry".into()),
1602                    PickleValue::String(v.clone()),
1603                ));
1604            } else {
1605                dict.push((
1606                    PickleValue::String("config_entry".into()),
1607                    PickleValue::None,
1608                ));
1609            }
1610
1611            dict.push((
1612                PickleValue::String("discovery_hash".into()),
1613                PickleValue::Bytes(iface.discovery_hash.to_vec()),
1614            ));
1615
1616            PickleValue::Dict(dict)
1617        })
1618        .collect();
1619    PickleValue::List(list)
1620}
1621
1622fn hooks_to_pickle(hooks: &[HookInfo]) -> PickleValue {
1623    PickleValue::List(
1624        hooks
1625            .iter()
1626            .map(|hook| {
1627                PickleValue::Dict(vec![
1628                    (
1629                        PickleValue::String("name".into()),
1630                        PickleValue::String(hook.name.clone()),
1631                    ),
1632                    (
1633                        PickleValue::String("type".into()),
1634                        PickleValue::String(hook.hook_type.clone()),
1635                    ),
1636                    (
1637                        PickleValue::String("attach_point".into()),
1638                        PickleValue::String(hook.attach_point.clone()),
1639                    ),
1640                    (
1641                        PickleValue::String("priority".into()),
1642                        PickleValue::Int(hook.priority as i64),
1643                    ),
1644                    (
1645                        PickleValue::String("enabled".into()),
1646                        PickleValue::Bool(hook.enabled),
1647                    ),
1648                    (
1649                        PickleValue::String("consecutive_traps".into()),
1650                        PickleValue::Int(hook.consecutive_traps as i64),
1651                    ),
1652                ])
1653            })
1654            .collect(),
1655    )
1656}
1657
1658fn backbone_peer_state_to_pickle(entries: &[BackbonePeerStateEntry]) -> PickleValue {
1659    PickleValue::List(
1660        entries
1661            .iter()
1662            .map(|entry| {
1663                PickleValue::Dict(vec![
1664                    (
1665                        PickleValue::String("interface".into()),
1666                        PickleValue::String(entry.interface_name.clone()),
1667                    ),
1668                    (
1669                        PickleValue::String("ip".into()),
1670                        PickleValue::String(entry.peer_ip.to_string()),
1671                    ),
1672                    (
1673                        PickleValue::String("connected_count".into()),
1674                        PickleValue::Int(entry.connected_count as i64),
1675                    ),
1676                    (
1677                        PickleValue::String("blacklisted_remaining_secs".into()),
1678                        entry
1679                            .blacklisted_remaining_secs
1680                            .map(PickleValue::Float)
1681                            .unwrap_or(PickleValue::None),
1682                    ),
1683                    (
1684                        PickleValue::String("blacklist_reason".into()),
1685                        entry
1686                            .blacklist_reason
1687                            .as_ref()
1688                            .map(|v: &String| PickleValue::String(v.clone()))
1689                            .unwrap_or(PickleValue::None),
1690                    ),
1691                    (
1692                        PickleValue::String("reject_count".into()),
1693                        PickleValue::Int(entry.reject_count as i64),
1694                    ),
1695                ])
1696            })
1697            .collect(),
1698    )
1699}
1700
1701fn backbone_interfaces_to_pickle(entries: &[BackboneInterfaceEntry]) -> PickleValue {
1702    PickleValue::List(
1703        entries
1704            .iter()
1705            .map(|entry| {
1706                PickleValue::Dict(vec![
1707                    (
1708                        PickleValue::String("id".into()),
1709                        PickleValue::Int(entry.interface_id.0 as i64),
1710                    ),
1711                    (
1712                        PickleValue::String("name".into()),
1713                        PickleValue::String(entry.interface_name.clone()),
1714                    ),
1715                ])
1716            })
1717            .collect(),
1718    )
1719}
1720
1721fn provider_bridge_stats_to_pickle(stats: &ProviderBridgeStats) -> PickleValue {
1722    PickleValue::Dict(vec![
1723        (
1724            PickleValue::String("connected".into()),
1725            PickleValue::Bool(stats.connected),
1726        ),
1727        (
1728            PickleValue::String("consumer_count".into()),
1729            PickleValue::Int(stats.consumer_count as i64),
1730        ),
1731        (
1732            PickleValue::String("queue_max_events".into()),
1733            PickleValue::Int(stats.queue_max_events as i64),
1734        ),
1735        (
1736            PickleValue::String("queue_max_bytes".into()),
1737            PickleValue::Int(stats.queue_max_bytes as i64),
1738        ),
1739        (
1740            PickleValue::String("backlog_len".into()),
1741            PickleValue::Int(stats.backlog_len as i64),
1742        ),
1743        (
1744            PickleValue::String("backlog_bytes".into()),
1745            PickleValue::Int(stats.backlog_bytes as i64),
1746        ),
1747        (
1748            PickleValue::String("backlog_dropped_pending".into()),
1749            PickleValue::Int(stats.backlog_dropped_pending as i64),
1750        ),
1751        (
1752            PickleValue::String("backlog_dropped_total".into()),
1753            PickleValue::Int(stats.backlog_dropped_total as i64),
1754        ),
1755        (
1756            PickleValue::String("total_disconnect_count".into()),
1757            PickleValue::Int(stats.total_disconnect_count as i64),
1758        ),
1759        (
1760            PickleValue::String("consumers".into()),
1761            PickleValue::List(
1762                stats
1763                    .consumers
1764                    .iter()
1765                    .map(|consumer| {
1766                        PickleValue::Dict(vec![
1767                            (
1768                                PickleValue::String("id".into()),
1769                                PickleValue::Int(consumer.id as i64),
1770                            ),
1771                            (
1772                                PickleValue::String("connected".into()),
1773                                PickleValue::Bool(consumer.connected),
1774                            ),
1775                            (
1776                                PickleValue::String("queue_len".into()),
1777                                PickleValue::Int(consumer.queue_len as i64),
1778                            ),
1779                            (
1780                                PickleValue::String("queued_bytes".into()),
1781                                PickleValue::Int(consumer.queued_bytes as i64),
1782                            ),
1783                            (
1784                                PickleValue::String("dropped_pending".into()),
1785                                PickleValue::Int(consumer.dropped_pending as i64),
1786                            ),
1787                            (
1788                                PickleValue::String("dropped_total".into()),
1789                                PickleValue::Int(consumer.dropped_total as i64),
1790                            ),
1791                            (
1792                                PickleValue::String("queue_max_events".into()),
1793                                PickleValue::Int(consumer.queue_max_events as i64),
1794                            ),
1795                            (
1796                                PickleValue::String("queue_max_bytes".into()),
1797                                PickleValue::Int(consumer.queue_max_bytes as i64),
1798                            ),
1799                        ])
1800                    })
1801                    .collect(),
1802            ),
1803        ),
1804    ])
1805}
1806
1807fn lifecycle_state_name(state: LifecycleState) -> &'static str {
1808    match state {
1809        LifecycleState::Active => "active",
1810        LifecycleState::Draining => "draining",
1811        LifecycleState::Stopping => "stopping",
1812        LifecycleState::Stopped => "stopped",
1813    }
1814}
1815
1816fn drain_status_to_pickle(status: &DrainStatus) -> PickleValue {
1817    PickleValue::Dict(vec![
1818        (
1819            PickleValue::String("state".into()),
1820            PickleValue::String(lifecycle_state_name(status.state).into()),
1821        ),
1822        (
1823            PickleValue::String("drain_age_seconds".into()),
1824            status
1825                .drain_age_seconds
1826                .map(PickleValue::Float)
1827                .unwrap_or(PickleValue::None),
1828        ),
1829        (
1830            PickleValue::String("deadline_remaining_seconds".into()),
1831            status
1832                .deadline_remaining_seconds
1833                .map(PickleValue::Float)
1834                .unwrap_or(PickleValue::None),
1835        ),
1836        (
1837            PickleValue::String("drain_complete".into()),
1838            PickleValue::Bool(status.drain_complete),
1839        ),
1840        (
1841            PickleValue::String("interface_writer_queued_frames".into()),
1842            PickleValue::Int(status.interface_writer_queued_frames as i64),
1843        ),
1844        (
1845            PickleValue::String("provider_backlog_events".into()),
1846            PickleValue::Int(status.provider_backlog_events as i64),
1847        ),
1848        (
1849            PickleValue::String("provider_consumer_queued_events".into()),
1850            PickleValue::Int(status.provider_consumer_queued_events as i64),
1851        ),
1852        (
1853            PickleValue::String("detail".into()),
1854            status
1855                .detail
1856                .as_ref()
1857                .map(|detail| PickleValue::String(detail.clone()))
1858                .unwrap_or(PickleValue::None),
1859        ),
1860    ])
1861}
1862
1863fn runtime_config_value_to_pickle(value: &RuntimeConfigValue) -> PickleValue {
1864    match value {
1865        RuntimeConfigValue::Int(v) => PickleValue::Int(*v),
1866        RuntimeConfigValue::Float(v) => PickleValue::Float(*v),
1867        RuntimeConfigValue::Bool(v) => PickleValue::Bool(*v),
1868        RuntimeConfigValue::String(v) => PickleValue::String(v.clone()),
1869        RuntimeConfigValue::Null => PickleValue::None,
1870    }
1871}
1872
1873fn runtime_config_value_from_pickle(value: &PickleValue) -> Option<RuntimeConfigValue> {
1874    match value {
1875        PickleValue::Int(v) => Some(RuntimeConfigValue::Int(*v)),
1876        PickleValue::Float(v) => Some(RuntimeConfigValue::Float(*v)),
1877        PickleValue::Bool(v) => Some(RuntimeConfigValue::Bool(*v)),
1878        PickleValue::String(v) => Some(RuntimeConfigValue::String(v.clone())),
1879        PickleValue::None => Some(RuntimeConfigValue::Null),
1880        _ => None,
1881    }
1882}
1883
1884fn runtime_config_entry_to_pickle(entry: &RuntimeConfigEntry) -> PickleValue {
1885    PickleValue::Dict(vec![
1886        (
1887            PickleValue::String("key".into()),
1888            PickleValue::String(entry.key.clone()),
1889        ),
1890        (
1891            PickleValue::String("value".into()),
1892            runtime_config_value_to_pickle(&entry.value),
1893        ),
1894        (
1895            PickleValue::String("default".into()),
1896            runtime_config_value_to_pickle(&entry.default),
1897        ),
1898        (
1899            PickleValue::String("source".into()),
1900            PickleValue::String(match entry.source {
1901                RuntimeConfigSource::Startup => "startup".into(),
1902                RuntimeConfigSource::RuntimeOverride => "runtime_override".into(),
1903            }),
1904        ),
1905        (
1906            PickleValue::String("apply_mode".into()),
1907            PickleValue::String(match entry.apply_mode {
1908                RuntimeConfigApplyMode::Immediate => "immediate".into(),
1909                RuntimeConfigApplyMode::NewConnectionsOnly => "new_connections_only".into(),
1910                RuntimeConfigApplyMode::NextReconnect => "next_reconnect".into(),
1911                RuntimeConfigApplyMode::RestartRequired => "restart_required".into(),
1912            }),
1913        ),
1914        (
1915            PickleValue::String("description".into()),
1916            entry
1917                .description
1918                .as_ref()
1919                .map(|v| PickleValue::String(v.clone()))
1920                .unwrap_or(PickleValue::None),
1921        ),
1922    ])
1923}
1924
1925fn runtime_config_list_to_pickle(entries: &[RuntimeConfigEntry]) -> PickleValue {
1926    PickleValue::List(entries.iter().map(runtime_config_entry_to_pickle).collect())
1927}
1928
1929fn runtime_config_error_to_pickle(error: &RuntimeConfigError) -> PickleValue {
1930    PickleValue::Dict(vec![
1931        (
1932            PickleValue::String("error".into()),
1933            PickleValue::String(match error.code {
1934                RuntimeConfigErrorCode::UnknownKey => "unknown_key".into(),
1935                RuntimeConfigErrorCode::InvalidType => "invalid_type".into(),
1936                RuntimeConfigErrorCode::InvalidValue => "invalid_value".into(),
1937                RuntimeConfigErrorCode::Unsupported => "unsupported".into(),
1938                RuntimeConfigErrorCode::NotFound => "not_found".into(),
1939                RuntimeConfigErrorCode::ApplyFailed => "apply_failed".into(),
1940            }),
1941        ),
1942        (
1943            PickleValue::String("message".into()),
1944            PickleValue::String(error.message.clone()),
1945        ),
1946    ])
1947}
1948
1949fn runtime_config_result_to_pickle(
1950    result: Result<RuntimeConfigEntry, RuntimeConfigError>,
1951) -> PickleValue {
1952    match result {
1953        Ok(entry) => runtime_config_entry_to_pickle(&entry),
1954        Err(error) => runtime_config_error_to_pickle(&error),
1955    }
1956}
1957
1958fn known_destination_entry_to_pickle(entry: &KnownDestinationEntry) -> PickleValue {
1959    PickleValue::Dict(vec![
1960        (
1961            PickleValue::String("dest_hash".into()),
1962            PickleValue::Bytes(entry.dest_hash.to_vec()),
1963        ),
1964        (
1965            PickleValue::String("identity_hash".into()),
1966            PickleValue::Bytes(entry.identity_hash.to_vec()),
1967        ),
1968        (
1969            PickleValue::String("public_key".into()),
1970            PickleValue::Bytes(entry.public_key.to_vec()),
1971        ),
1972        (
1973            PickleValue::String("app_data".into()),
1974            entry
1975                .app_data
1976                .as_ref()
1977                .map(|data: &Vec<u8>| PickleValue::Bytes(data.clone()))
1978                .unwrap_or(PickleValue::None),
1979        ),
1980        (
1981            PickleValue::String("hops".into()),
1982            PickleValue::Int(entry.hops as i64),
1983        ),
1984        (
1985            PickleValue::String("received_at".into()),
1986            PickleValue::Float(entry.received_at),
1987        ),
1988        (
1989            PickleValue::String("receiving_interface".into()),
1990            PickleValue::Int(entry.receiving_interface.0 as i64),
1991        ),
1992        (
1993            PickleValue::String("was_used".into()),
1994            PickleValue::Bool(entry.was_used),
1995        ),
1996        (
1997            PickleValue::String("last_used_at".into()),
1998            entry
1999                .last_used_at
2000                .map(PickleValue::Float)
2001                .unwrap_or(PickleValue::None),
2002        ),
2003        (
2004            PickleValue::String("retained".into()),
2005            PickleValue::Bool(entry.retained),
2006        ),
2007    ])
2008}
2009
2010fn known_destinations_to_pickle(entries: &[KnownDestinationEntry]) -> PickleValue {
2011    PickleValue::List(
2012        entries
2013            .iter()
2014            .map(known_destination_entry_to_pickle)
2015            .collect(),
2016    )
2017}
2018
2019fn parse_known_destination_entry(value: &PickleValue) -> io::Result<KnownDestinationEntry> {
2020    let get_bytes = |key: &str, len: usize| -> io::Result<Vec<u8>> {
2021        let value = value.get(key).ok_or_else(|| {
2022            io::Error::new(io::ErrorKind::InvalidData, format!("missing {}", key))
2023        })?;
2024        let bytes = value.as_bytes().ok_or_else(|| {
2025            io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key))
2026        })?;
2027        if bytes.len() != len {
2028            return Err(io::Error::new(
2029                io::ErrorKind::InvalidData,
2030                format!("invalid {} length", key),
2031            ));
2032        }
2033        Ok(bytes.to_vec())
2034    };
2035    let get_int = |key: &str| -> io::Result<i64> {
2036        value
2037            .get(key)
2038            .and_then(|v| v.as_int())
2039            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key)))
2040    };
2041    let get_float = |key: &str| -> io::Result<f64> {
2042        value
2043            .get(key)
2044            .and_then(|v| v.as_float())
2045            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key)))
2046    };
2047    let get_bool = |key: &str| -> io::Result<bool> {
2048        value
2049            .get(key)
2050            .and_then(|v| v.as_bool())
2051            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid {}", key)))
2052    };
2053
2054    let mut dest_hash = [0u8; 16];
2055    dest_hash.copy_from_slice(&get_bytes("dest_hash", 16)?);
2056    let mut identity_hash = [0u8; 16];
2057    identity_hash.copy_from_slice(&get_bytes("identity_hash", 16)?);
2058    let mut public_key = [0u8; 64];
2059    public_key.copy_from_slice(&get_bytes("public_key", 64)?);
2060    let app_data = value
2061        .get("app_data")
2062        .and_then(|v| v.as_bytes())
2063        .map(|bytes| bytes.to_vec());
2064    let last_used_at = value.get("last_used_at").and_then(|v| v.as_float());
2065
2066    Ok(KnownDestinationEntry {
2067        dest_hash,
2068        identity_hash,
2069        public_key,
2070        app_data,
2071        hops: get_int("hops")? as u8,
2072        received_at: get_float("received_at")?,
2073        receiving_interface: rns_core::transport::types::InterfaceId(
2074            get_int("receiving_interface")? as u64,
2075        ),
2076        was_used: get_bool("was_used")?,
2077        last_used_at,
2078        retained: get_bool("retained")?,
2079    })
2080}
2081
2082fn parse_known_destination_list(value: &PickleValue) -> io::Result<Vec<KnownDestinationEntry>> {
2083    let list = value
2084        .as_list()
2085        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "expected list"))?;
2086    list.iter().map(parse_known_destination_entry).collect()
2087}
2088
2089// --- RPC Client ---
2090
2091/// RPC client for connecting to a running daemon.
2092pub struct RpcClient {
2093    stream: TcpStream,
2094}
2095
2096impl RpcClient {
2097    /// Connect to an RPC server and authenticate.
2098    pub fn connect(addr: &RpcAddr, auth_key: &[u8; 32]) -> io::Result<Self> {
2099        let mut stream = match addr {
2100            RpcAddr::Tcp(host, port) => TcpStream::connect((host.as_str(), *port))?,
2101        };
2102
2103        stream.set_read_timeout(Some(std::time::Duration::from_secs(10)))?;
2104        stream.set_write_timeout(Some(std::time::Duration::from_secs(10)))?;
2105
2106        // Client-side authentication
2107        client_auth(&mut stream, auth_key)?;
2108
2109        Ok(RpcClient { stream })
2110    }
2111
2112    /// Send an RPC request and receive a response.
2113    pub fn call(&mut self, request: &PickleValue) -> io::Result<PickleValue> {
2114        let request_bytes = encode_rpc_payload(request, RpcCodec::Msgpack);
2115        send_bytes(&mut self.stream, &request_bytes)?;
2116
2117        let response_bytes = recv_bytes(&mut self.stream)?;
2118        decode_rpc_payload(&response_bytes).map(|(value, _)| value)
2119    }
2120
2121    pub fn list_hooks(&mut self) -> io::Result<Vec<HookInfo>> {
2122        let response = self.call(&PickleValue::Dict(vec![(
2123            PickleValue::String("get".into()),
2124            PickleValue::String("hooks".into()),
2125        )]))?;
2126        parse_hook_list(&response)
2127    }
2128
2129    pub fn begin_drain(&mut self, timeout: Duration) -> io::Result<bool> {
2130        let response = self.call(&PickleValue::Dict(vec![(
2131            PickleValue::String("begin_drain".into()),
2132            PickleValue::Float(timeout.as_secs_f64()),
2133        )]))?;
2134        Ok(response.as_bool().unwrap_or(false))
2135    }
2136
2137    pub fn drain_status(&mut self) -> io::Result<Option<DrainStatus>> {
2138        let response = self.call(&PickleValue::Dict(vec![(
2139            PickleValue::String("get".into()),
2140            PickleValue::String("drain_status".into()),
2141        )]))?;
2142        parse_drain_status(&response)
2143    }
2144
2145    pub fn provider_bridge_stats(&mut self) -> io::Result<PickleValue> {
2146        self.call(&PickleValue::Dict(vec![(
2147            PickleValue::String("get".into()),
2148            PickleValue::String("provider_bridge_stats".into()),
2149        )]))
2150    }
2151
2152    pub fn load_hook(
2153        &mut self,
2154        name: &str,
2155        attach_point: &str,
2156        priority: i32,
2157        wasm: &[u8],
2158    ) -> io::Result<Result<(), String>> {
2159        let response = self.call(&PickleValue::Dict(vec![
2160            (
2161                PickleValue::String("hook".into()),
2162                PickleValue::String("load".into()),
2163            ),
2164            (
2165                PickleValue::String("name".into()),
2166                PickleValue::String(name.to_string()),
2167            ),
2168            (
2169                PickleValue::String("attach_point".into()),
2170                PickleValue::String(attach_point.to_string()),
2171            ),
2172            (
2173                PickleValue::String("priority".into()),
2174                PickleValue::Int(priority as i64),
2175            ),
2176            (
2177                PickleValue::String("wasm".into()),
2178                PickleValue::Bytes(wasm.to_vec()),
2179            ),
2180        ]))?;
2181        parse_hook_result(&response)
2182    }
2183
2184    pub fn load_builtin_hook(
2185        &mut self,
2186        name: &str,
2187        attach_point: &str,
2188        priority: i32,
2189        builtin_id: &str,
2190    ) -> io::Result<Result<(), String>> {
2191        let response = self.call(&PickleValue::Dict(vec![
2192            (
2193                PickleValue::String("hook".into()),
2194                PickleValue::String("load_builtin".into()),
2195            ),
2196            (
2197                PickleValue::String("name".into()),
2198                PickleValue::String(name.to_string()),
2199            ),
2200            (
2201                PickleValue::String("attach_point".into()),
2202                PickleValue::String(attach_point.to_string()),
2203            ),
2204            (
2205                PickleValue::String("priority".into()),
2206                PickleValue::Int(priority as i64),
2207            ),
2208            (
2209                PickleValue::String("builtin_id".into()),
2210                PickleValue::String(builtin_id.to_string()),
2211            ),
2212        ]))?;
2213        parse_hook_result(&response)
2214    }
2215
2216    pub fn unload_hook(
2217        &mut self,
2218        name: &str,
2219        attach_point: &str,
2220    ) -> io::Result<Result<(), String>> {
2221        let response = self.call(&PickleValue::Dict(vec![
2222            (
2223                PickleValue::String("hook".into()),
2224                PickleValue::String("unload".into()),
2225            ),
2226            (
2227                PickleValue::String("name".into()),
2228                PickleValue::String(name.to_string()),
2229            ),
2230            (
2231                PickleValue::String("attach_point".into()),
2232                PickleValue::String(attach_point.to_string()),
2233            ),
2234        ]))?;
2235        parse_hook_result(&response)
2236    }
2237
2238    pub fn set_hook_enabled(
2239        &mut self,
2240        name: &str,
2241        attach_point: &str,
2242        enabled: bool,
2243    ) -> io::Result<Result<(), String>> {
2244        let op = if enabled { "enable" } else { "disable" };
2245        let response = self.call(&PickleValue::Dict(vec![
2246            (
2247                PickleValue::String("hook".into()),
2248                PickleValue::String(op.into()),
2249            ),
2250            (
2251                PickleValue::String("name".into()),
2252                PickleValue::String(name.to_string()),
2253            ),
2254            (
2255                PickleValue::String("attach_point".into()),
2256                PickleValue::String(attach_point.to_string()),
2257            ),
2258        ]))?;
2259        parse_hook_result(&response)
2260    }
2261
2262    pub fn set_hook_priority(
2263        &mut self,
2264        name: &str,
2265        attach_point: &str,
2266        priority: i32,
2267    ) -> io::Result<Result<(), String>> {
2268        let response = self.call(&PickleValue::Dict(vec![
2269            (
2270                PickleValue::String("hook".into()),
2271                PickleValue::String("set_priority".into()),
2272            ),
2273            (
2274                PickleValue::String("name".into()),
2275                PickleValue::String(name.to_string()),
2276            ),
2277            (
2278                PickleValue::String("attach_point".into()),
2279                PickleValue::String(attach_point.to_string()),
2280            ),
2281            (
2282                PickleValue::String("priority".into()),
2283                PickleValue::Int(priority as i64),
2284            ),
2285        ]))?;
2286        parse_hook_result(&response)
2287    }
2288
2289    pub fn blacklist_backbone_peer(
2290        &mut self,
2291        interface: &str,
2292        ip: &str,
2293        duration_secs: u64,
2294        reason: Option<&str>,
2295        penalty_level: Option<u8>,
2296    ) -> io::Result<bool> {
2297        let mut request = vec![
2298            (
2299                PickleValue::String("set".into()),
2300                PickleValue::String("backbone_peer_blacklist".into()),
2301            ),
2302            (
2303                PickleValue::String("interface".into()),
2304                PickleValue::String(interface.to_string()),
2305            ),
2306            (
2307                PickleValue::String("ip".into()),
2308                PickleValue::String(ip.to_string()),
2309            ),
2310            (
2311                PickleValue::String("duration_secs".into()),
2312                PickleValue::Int(duration_secs as i64),
2313            ),
2314        ];
2315        if let Some(reason) = reason {
2316            request.push((
2317                PickleValue::String("reason".into()),
2318                PickleValue::String(reason.to_string()),
2319            ));
2320        }
2321        if let Some(level) = penalty_level {
2322            request.push((
2323                PickleValue::String("penalty_level".into()),
2324                PickleValue::Int(level as i64),
2325            ));
2326        }
2327        let response = self.call(&PickleValue::Dict(request))?;
2328        Ok(response.as_bool().unwrap_or(false))
2329    }
2330
2331    pub fn known_destinations(&mut self) -> io::Result<Vec<KnownDestinationEntry>> {
2332        let response = self.call(&PickleValue::Dict(vec![(
2333            PickleValue::String("get".into()),
2334            PickleValue::String("known_destinations".into()),
2335        )]))?;
2336        parse_known_destination_list(&response)
2337    }
2338
2339    pub fn retain_known_destination(&mut self, dest_hash: [u8; 16]) -> io::Result<bool> {
2340        let response = self.call(&PickleValue::Dict(vec![
2341            (
2342                PickleValue::String("set".into()),
2343                PickleValue::String("known_destination_retained".into()),
2344            ),
2345            (
2346                PickleValue::String("dest_hash".into()),
2347                PickleValue::Bytes(dest_hash.to_vec()),
2348            ),
2349        ]))?;
2350        Ok(response.as_bool().unwrap_or(false))
2351    }
2352
2353    pub fn retain_identity(&mut self, identity_hash: [u8; 16]) -> io::Result<bool> {
2354        let response = self.call(&PickleValue::Dict(vec![
2355            (
2356                PickleValue::String("set".into()),
2357                PickleValue::String("identity_retained".into()),
2358            ),
2359            (
2360                PickleValue::String("identity_hash".into()),
2361                PickleValue::Bytes(identity_hash.to_vec()),
2362            ),
2363        ]))?;
2364        Ok(response.as_bool().unwrap_or(false))
2365    }
2366
2367    pub fn unretain_known_destination(&mut self, dest_hash: [u8; 16]) -> io::Result<bool> {
2368        let response = self.call(&PickleValue::Dict(vec![
2369            (
2370                PickleValue::String("clear".into()),
2371                PickleValue::String("known_destination_retained".into()),
2372            ),
2373            (
2374                PickleValue::String("dest_hash".into()),
2375                PickleValue::Bytes(dest_hash.to_vec()),
2376            ),
2377        ]))?;
2378        Ok(response.as_bool().unwrap_or(false))
2379    }
2380
2381    pub fn mark_known_destination_used(&mut self, dest_hash: [u8; 16]) -> io::Result<bool> {
2382        let response = self.call(&PickleValue::Dict(vec![
2383            (
2384                PickleValue::String("set".into()),
2385                PickleValue::String("known_destination_used".into()),
2386            ),
2387            (
2388                PickleValue::String("dest_hash".into()),
2389                PickleValue::Bytes(dest_hash.to_vec()),
2390            ),
2391        ]))?;
2392        Ok(response.as_bool().unwrap_or(false))
2393    }
2394}
2395
2396fn parse_lifecycle_state(value: &str) -> Option<LifecycleState> {
2397    match value {
2398        "active" => Some(LifecycleState::Active),
2399        "draining" => Some(LifecycleState::Draining),
2400        "stopping" => Some(LifecycleState::Stopping),
2401        "stopped" => Some(LifecycleState::Stopped),
2402        _ => None,
2403    }
2404}
2405
2406fn parse_drain_status(value: &PickleValue) -> io::Result<Option<DrainStatus>> {
2407    if !matches!(value, PickleValue::Dict(_)) {
2408        return Ok(None);
2409    }
2410    let state = value
2411        .get("state")
2412        .and_then(|entry| entry.as_str())
2413        .and_then(parse_lifecycle_state)
2414        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing drain state"))?;
2415    let drain_age_seconds = value.get("drain_age_seconds").and_then(|entry| {
2416        entry
2417            .as_float()
2418            .or_else(|| entry.as_int().map(|v| v as f64))
2419    });
2420    let deadline_remaining_seconds = value.get("deadline_remaining_seconds").and_then(|entry| {
2421        entry
2422            .as_float()
2423            .or_else(|| entry.as_int().map(|v| v as f64))
2424    });
2425    let drain_complete = value
2426        .get("drain_complete")
2427        .and_then(|entry| entry.as_bool())
2428        .unwrap_or(false);
2429    let interface_writer_queued_frames = value
2430        .get("interface_writer_queued_frames")
2431        .and_then(|entry| entry.as_int())
2432        .unwrap_or(0)
2433        .max(0) as usize;
2434    let provider_backlog_events = value
2435        .get("provider_backlog_events")
2436        .and_then(|entry| entry.as_int())
2437        .unwrap_or(0)
2438        .max(0) as usize;
2439    let provider_consumer_queued_events = value
2440        .get("provider_consumer_queued_events")
2441        .and_then(|entry| entry.as_int())
2442        .unwrap_or(0)
2443        .max(0) as usize;
2444    let detail = value
2445        .get("detail")
2446        .and_then(|entry| entry.as_str().map(|v| v.to_string()));
2447    Ok(Some(DrainStatus {
2448        state,
2449        drain_age_seconds,
2450        deadline_remaining_seconds,
2451        drain_complete,
2452        interface_writer_queued_frames,
2453        provider_backlog_events,
2454        provider_consumer_queued_events,
2455        detail,
2456    }))
2457}
2458
2459fn parse_hook_result(response: &PickleValue) -> io::Result<Result<(), String>> {
2460    let ok = response
2461        .get("ok")
2462        .and_then(|v| v.as_bool())
2463        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid hook response"))?;
2464    if ok {
2465        Ok(Ok(()))
2466    } else {
2467        Ok(Err(response
2468            .get("error")
2469            .and_then(|v| v.as_str())
2470            .unwrap_or("unknown hook error")
2471            .to_string()))
2472    }
2473}
2474
2475fn parse_hook_list(response: &PickleValue) -> io::Result<Vec<HookInfo>> {
2476    let list = response
2477        .as_list()
2478        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid hooks response"))?;
2479    let mut hooks = Vec::with_capacity(list.len());
2480    for item in list {
2481        hooks.push(HookInfo {
2482            name: item
2483                .get("name")
2484                .and_then(|v| v.as_str())
2485                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing hook name"))?
2486                .to_string(),
2487            hook_type: item
2488                .get("type")
2489                .and_then(|v| v.as_str())
2490                .unwrap_or(default_hook_type())
2491                .to_string(),
2492            attach_point: item
2493                .get("attach_point")
2494                .and_then(|v| v.as_str())
2495                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing attach_point"))?
2496                .to_string(),
2497            priority: item
2498                .get("priority")
2499                .and_then(|v| v.as_int())
2500                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing priority"))?
2501                as i32,
2502            enabled: item
2503                .get("enabled")
2504                .and_then(|v| v.as_bool())
2505                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing enabled"))?,
2506            consecutive_traps: item
2507                .get("consecutive_traps")
2508                .and_then(|v| v.as_int())
2509                .ok_or_else(|| {
2510                    io::Error::new(io::ErrorKind::InvalidData, "missing consecutive_traps")
2511                })? as u32,
2512        });
2513    }
2514    Ok(hooks)
2515}
2516
2517fn default_hook_type() -> &'static str {
2518    #[cfg(feature = "rns-hooks-native")]
2519    {
2520        return "native";
2521    }
2522    #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
2523    {
2524        return "wasm";
2525    }
2526    #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
2527    {
2528        "wasm"
2529    }
2530}
2531
2532/// Client-side authentication: answer the server's challenge.
2533fn client_auth(stream: &mut TcpStream, auth_key: &[u8; 32]) -> io::Result<()> {
2534    // Read challenge
2535    let challenge = recv_bytes(stream)?;
2536
2537    if !challenge.starts_with(CHALLENGE_PREFIX) {
2538        return Err(io::Error::new(
2539            io::ErrorKind::InvalidData,
2540            "expected challenge",
2541        ));
2542    }
2543
2544    let message = &challenge[CHALLENGE_PREFIX.len()..];
2545
2546    // Create HMAC response
2547    let response = create_response(auth_key, message);
2548    send_bytes(stream, &response)?;
2549
2550    // Read welcome/failure
2551    let result = recv_bytes(stream)?;
2552    if result == WELCOME {
2553        Ok(())
2554    } else {
2555        Err(io::Error::new(
2556            io::ErrorKind::PermissionDenied,
2557            "authentication failed",
2558        ))
2559    }
2560}
2561
2562/// Create an HMAC response to a challenge message.
2563fn create_response(auth_key: &[u8; 32], message: &[u8]) -> Vec<u8> {
2564    // Check if message has {sha256} prefix (modern protocol)
2565    if message.starts_with(b"{sha256}") || message.len() > 20 {
2566        // Modern protocol: use HMAC-SHA256 with {sha256} prefix
2567        let digest = hmac_sha256(auth_key, message);
2568        let mut response = Vec::with_capacity(8 + 32);
2569        response.extend_from_slice(b"{sha256}");
2570        response.extend_from_slice(&digest);
2571        response
2572    } else {
2573        // Legacy protocol: raw HMAC-MD5
2574        let digest = hmac_md5(auth_key, message);
2575        digest.to_vec()
2576    }
2577}
2578
2579/// Derive the RPC auth key from transport identity private key.
2580pub fn derive_auth_key(private_key: &[u8]) -> [u8; 32] {
2581    sha256(private_key)
2582}
2583
2584#[cfg(test)]
2585mod tests {
2586    use super::*;
2587
2588    #[test]
2589    fn send_recv_bytes_roundtrip() {
2590        let (mut c1, mut c2) = tcp_pair();
2591        let data = b"hello world";
2592        send_bytes(&mut c1, data).unwrap();
2593        let received = recv_bytes(&mut c2).unwrap();
2594        assert_eq!(&received, data);
2595    }
2596
2597    #[test]
2598    fn send_recv_empty() {
2599        let (mut c1, mut c2) = tcp_pair();
2600        send_bytes(&mut c1, b"").unwrap();
2601        let received = recv_bytes(&mut c2).unwrap();
2602        assert!(received.is_empty());
2603    }
2604
2605    #[test]
2606    fn auth_success() {
2607        let key = derive_auth_key(b"test-private-key");
2608        let (mut server, mut client) = tcp_pair();
2609
2610        let key2 = key;
2611        let t = thread::spawn(move || {
2612            client_auth(&mut client, &key2).unwrap();
2613        });
2614
2615        server_auth(&mut server, &key).unwrap();
2616        t.join().unwrap();
2617    }
2618
2619    #[test]
2620    fn auth_failure_wrong_key() {
2621        let server_key = derive_auth_key(b"server-key");
2622        let client_key = derive_auth_key(b"wrong-key");
2623        let (mut server, mut client) = tcp_pair();
2624
2625        let t = thread::spawn(move || {
2626            let result = client_auth(&mut client, &client_key);
2627            assert!(result.is_err());
2628        });
2629
2630        let result = server_auth(&mut server, &server_key);
2631        assert!(result.is_err());
2632        t.join().unwrap();
2633    }
2634
2635    #[test]
2636    fn verify_sha256_response() {
2637        let key = derive_auth_key(b"mykey");
2638        let message = b"{sha256}abcdefghijklmnopqrstuvwxyz0123456789ABCD";
2639        let response = create_response(&key, message);
2640        assert!(response.starts_with(b"{sha256}"));
2641        assert!(verify_response(&key, message, &response));
2642    }
2643
2644    #[test]
2645    fn verify_legacy_md5_response() {
2646        let key = derive_auth_key(b"mykey");
2647        // Legacy message: 20 bytes, no prefix
2648        let message = b"01234567890123456789";
2649        // Create legacy response (raw HMAC-MD5)
2650        let digest = hmac_md5(&key, message);
2651        assert!(verify_response(&key, message, &digest));
2652    }
2653
2654    #[test]
2655    fn constant_time_eq_works() {
2656        assert!(constant_time_eq(b"hello", b"hello"));
2657        assert!(!constant_time_eq(b"hello", b"world"));
2658        assert!(!constant_time_eq(b"hello", b"hell"));
2659    }
2660
2661    #[test]
2662    fn rpc_payload_accepts_msgpack_and_legacy_pickle() {
2663        let request = PickleValue::Dict(vec![
2664            (
2665                PickleValue::String("get".into()),
2666                PickleValue::String("link_count".into()),
2667            ),
2668            (PickleValue::String("max_hops".into()), PickleValue::Int(3)),
2669        ]);
2670
2671        let msgpack_bytes = encode_rpc_payload(&request, RpcCodec::Msgpack);
2672        let (decoded_msgpack, msgpack_codec) = decode_rpc_payload(&msgpack_bytes).unwrap();
2673        assert_eq!(decoded_msgpack, request);
2674        assert_eq!(msgpack_codec, RpcCodec::Msgpack);
2675
2676        let pickle_bytes = encode_rpc_payload(&request, RpcCodec::Pickle);
2677        let (decoded_pickle, pickle_codec) = decode_rpc_payload(&pickle_bytes).unwrap();
2678        assert_eq!(decoded_pickle, request);
2679        assert_eq!(pickle_codec, RpcCodec::Pickle);
2680    }
2681
2682    #[test]
2683    fn rpc_roundtrip() {
2684        let key = derive_auth_key(b"test-key");
2685        let (event_tx, event_rx) = crate::event::channel();
2686
2687        // Start server
2688        // Bind manually to get the actual port
2689        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2690        let port = listener.local_addr().unwrap().port();
2691        listener.set_nonblocking(true).unwrap();
2692
2693        let shutdown = Arc::new(AtomicBool::new(false));
2694        let shutdown2 = shutdown.clone();
2695
2696        // Driver thread that handles queries
2697        let driver_thread = thread::spawn(move || loop {
2698            match event_rx.recv_timeout(std::time::Duration::from_secs(5)) {
2699                Ok(Event::Query(QueryRequest::LinkCount, resp_tx)) => {
2700                    let _ = resp_tx.send(QueryResponse::LinkCount(42));
2701                }
2702                Ok(Event::Query(QueryRequest::InterfaceStats, resp_tx)) => {
2703                    let _ = resp_tx.send(QueryResponse::InterfaceStats(InterfaceStatsResponse {
2704                        interfaces: vec![SingleInterfaceStat {
2705                            id: 7,
2706                            name: "TestInterface".into(),
2707                            status: true,
2708                            mode: 1,
2709                            gravity: -2,
2710                            announces_to_internal: Some(true),
2711                            rxb: 1000,
2712                            txb: 2000,
2713                            rx_packets: 10,
2714                            tx_packets: 20,
2715                            cpu_load: None,
2716                            mem_load: None,
2717                            switch_id: None,
2718                            endpoint_id: None,
2719                            via_switch_id: None,
2720                            peers: None,
2721                            bitrate: Some(10_000_000),
2722                            ifac_size: None,
2723                            started: 1000.0,
2724                            ia_freq: 0.0,
2725                            ip_freq: 0.0,
2726                            op_freq: 0.0,
2727                            op_samples: 0,
2728                            burst_active: false,
2729                            burst_activated: 0.0,
2730                            pr_burst_active: false,
2731                            pr_burst_activated: 0.0,
2732                            oa_freq: 0.0,
2733                            clients: Some(2),
2734                            blocked_ips: None,
2735                            blocked_ip_list: None,
2736                            announce_rate_target: Some(3600.0),
2737                            announce_rate_grace: 5,
2738                            announce_rate_penalty: 0.0,
2739                            interface_type: "TestInterface".into(),
2740                        }],
2741                        transport_id: None,
2742                        transport_enabled: true,
2743                        transport_uptime: 3600.0,
2744                        total_rxb: 1000,
2745                        total_txb: 2000,
2746                        probe_responder: None,
2747                        backbone_peer_pool: None,
2748                    }));
2749                }
2750                _ => break,
2751            }
2752        });
2753
2754        let key2 = key;
2755        let shutdown3 = shutdown2.clone();
2756        let server_thread = thread::spawn(move || {
2757            rpc_server_loop(listener, key2, event_tx, shutdown3);
2758        });
2759
2760        // Give server time to start
2761        thread::sleep(std::time::Duration::from_millis(50));
2762
2763        // Client: connect and query link count
2764        let server_addr = RpcAddr::Tcp("127.0.0.1".into(), port);
2765        let mut client = RpcClient::connect(&server_addr, &key).unwrap();
2766        let response = client
2767            .call(&PickleValue::Dict(vec![(
2768                PickleValue::String("get".into()),
2769                PickleValue::String("link_count".into()),
2770            )]))
2771            .unwrap();
2772        assert_eq!(response.as_int().unwrap(), 42);
2773        drop(client);
2774
2775        // Client: query interface stats
2776        let mut client2 = RpcClient::connect(&server_addr, &key).unwrap();
2777        let response2 = client2
2778            .call(&PickleValue::Dict(vec![(
2779                PickleValue::String("get".into()),
2780                PickleValue::String("interface_stats".into()),
2781            )]))
2782            .unwrap();
2783        let ifaces = response2.get("interfaces").unwrap().as_list().unwrap();
2784        assert_eq!(ifaces.len(), 1);
2785        let iface = &ifaces[0];
2786        assert_eq!(
2787            iface.get("name").unwrap().as_str().unwrap(),
2788            "TestInterface"
2789        );
2790        assert_eq!(iface.get("rxb").unwrap().as_int().unwrap(), 1000);
2791        drop(client2);
2792
2793        // Shutdown
2794        shutdown2.store(true, Ordering::Relaxed);
2795        server_thread.join().unwrap();
2796        driver_thread.join().unwrap();
2797    }
2798
2799    #[test]
2800    fn derive_auth_key_deterministic() {
2801        let key1 = derive_auth_key(b"test");
2802        let key2 = derive_auth_key(b"test");
2803        assert_eq!(key1, key2);
2804        // Different input → different key
2805        let key3 = derive_auth_key(b"other");
2806        assert_ne!(key1, key3);
2807    }
2808
2809    #[test]
2810    fn pickle_request_handling() {
2811        // Test the request → query translation without networking
2812        let (event_tx, event_rx) = crate::event::channel();
2813
2814        let driver = thread::spawn(move || {
2815            if let Ok(Event::Query(QueryRequest::DropPath { dest_hash }, resp_tx)) = event_rx.recv()
2816            {
2817                assert_eq!(dest_hash, [1u8; 16]);
2818                let _ = resp_tx.send(QueryResponse::DropPath(true));
2819            }
2820        });
2821
2822        let request = PickleValue::Dict(vec![
2823            (
2824                PickleValue::String("drop".into()),
2825                PickleValue::String("path".into()),
2826            ),
2827            (
2828                PickleValue::String("destination_hash".into()),
2829                PickleValue::Bytes(vec![1u8; 16]),
2830            ),
2831        ]);
2832
2833        let response = handle_rpc_request(&request, &event_tx).unwrap();
2834        assert_eq!(response, PickleValue::Bool(true));
2835        driver.join().unwrap();
2836    }
2837
2838    #[test]
2839    fn hook_list_request_handling() {
2840        let (event_tx, event_rx) = crate::event::channel();
2841
2842        let driver = thread::spawn(move || {
2843            if let Ok(Event::ListHooks { response_tx }) = event_rx.recv() {
2844                let _ = response_tx.send(vec![HookInfo {
2845                    name: "stats".into(),
2846                    hook_type: "wasm".into(),
2847                    attach_point: "PreIngress".into(),
2848                    priority: 7,
2849                    enabled: true,
2850                    consecutive_traps: 0,
2851                }]);
2852            }
2853        });
2854
2855        let request = PickleValue::Dict(vec![(
2856            PickleValue::String("get".into()),
2857            PickleValue::String("hooks".into()),
2858        )]);
2859        let response = handle_rpc_request(&request, &event_tx).unwrap();
2860        let hooks = parse_hook_list(&response).unwrap();
2861        assert_eq!(hooks.len(), 1);
2862        assert_eq!(hooks[0].name, "stats");
2863        driver.join().unwrap();
2864    }
2865
2866    #[test]
2867    fn hook_load_request_handling() {
2868        let (event_tx, event_rx) = crate::event::channel();
2869
2870        let driver = thread::spawn(move || {
2871            if let Ok(Event::LoadHook {
2872                name,
2873                wasm_bytes,
2874                attach_point,
2875                priority,
2876                response_tx,
2877            }) = event_rx.recv()
2878            {
2879                assert_eq!(name, "stats");
2880                assert_eq!(attach_point, "PreIngress");
2881                assert_eq!(priority, 11);
2882                assert_eq!(wasm_bytes, vec![1, 2, 3]);
2883                let _ = response_tx.send(Ok(()));
2884            }
2885        });
2886
2887        let request = PickleValue::Dict(vec![
2888            (
2889                PickleValue::String("hook".into()),
2890                PickleValue::String("load".into()),
2891            ),
2892            (
2893                PickleValue::String("name".into()),
2894                PickleValue::String("stats".into()),
2895            ),
2896            (
2897                PickleValue::String("attach_point".into()),
2898                PickleValue::String("PreIngress".into()),
2899            ),
2900            (PickleValue::String("priority".into()), PickleValue::Int(11)),
2901            (
2902                PickleValue::String("wasm".into()),
2903                PickleValue::Bytes(vec![1, 2, 3]),
2904            ),
2905        ]);
2906        let response = handle_rpc_request(&request, &event_tx).unwrap();
2907        assert_eq!(parse_hook_result(&response).unwrap(), Ok(()));
2908        driver.join().unwrap();
2909    }
2910
2911    #[test]
2912    fn interface_stats_pickle_format() {
2913        let stats = InterfaceStatsResponse {
2914            interfaces: vec![SingleInterfaceStat {
2915                id: 1,
2916                name: "TCP".into(),
2917                status: true,
2918                mode: 1,
2919                gravity: -2,
2920                announces_to_internal: Some(true),
2921                rxb: 100,
2922                txb: 200,
2923                rx_packets: 5,
2924                tx_packets: 10,
2925                cpu_load: Some(12.0),
2926                mem_load: Some(34.5),
2927                switch_id: Some([1, 2, 3, 4]),
2928                endpoint_id: Some([5; 8]),
2929                via_switch_id: Some([6; 4]),
2930                peers: Some(2),
2931                bitrate: Some(1000000),
2932                ifac_size: Some(16),
2933                started: 1000.0,
2934                ia_freq: 1.0,
2935                ip_freq: 2.0,
2936                op_freq: 3.0,
2937                op_samples: 0,
2938                burst_active: true,
2939                burst_activated: 1200.0,
2940                pr_burst_active: true,
2941                pr_burst_activated: 1300.0,
2942                oa_freq: 4.0,
2943                clients: Some(3),
2944                blocked_ips: Some(2),
2945                blocked_ip_list: Some(vec!["192.0.2.1".into(), "203.0.113.2".into()]),
2946                announce_rate_target: Some(3600.0),
2947                announce_rate_grace: 5,
2948                announce_rate_penalty: 0.0,
2949                interface_type: "TCPClientInterface".into(),
2950            }],
2951            transport_id: Some([0xAB; 16]),
2952            transport_enabled: true,
2953            transport_uptime: 3600.0,
2954            total_rxb: 100,
2955            total_txb: 200,
2956            probe_responder: None,
2957            backbone_peer_pool: None,
2958        };
2959
2960        let pickle = interface_stats_to_pickle(&stats);
2961
2962        // Verify it round-trips through encode/decode
2963        let encoded = pickle::encode(&pickle);
2964        let decoded = pickle::decode(&encoded).unwrap();
2965        assert_eq!(
2966            decoded.get("transport_enabled").unwrap().as_bool().unwrap(),
2967            true
2968        );
2969        let ifaces = decoded.get("interfaces").unwrap().as_list().unwrap();
2970        assert_eq!(ifaces[0].get("id").unwrap().as_int().unwrap(), 1);
2971        assert_eq!(ifaces[0].get("name").unwrap().as_str().unwrap(), "TCP");
2972        assert_eq!(ifaces[0].get("gravity").unwrap().as_int().unwrap(), -2);
2973        assert!(ifaces[0]
2974            .get("announces_to_internal")
2975            .unwrap()
2976            .as_bool()
2977            .unwrap());
2978        assert_eq!(ifaces[0].get("ia_freq").unwrap().as_float().unwrap(), 1.0);
2979        assert_eq!(ifaces[0].get("ip_freq").unwrap().as_float().unwrap(), 2.0);
2980        assert_eq!(ifaces[0].get("op_freq").unwrap().as_float().unwrap(), 3.0);
2981        assert_eq!(ifaces[0].get("oa_freq").unwrap().as_float().unwrap(), 4.0);
2982        assert_eq!(
2983            ifaces[0].get("burst_active").unwrap().as_bool().unwrap(),
2984            true
2985        );
2986        assert_eq!(
2987            ifaces[0]
2988                .get("burst_activated")
2989                .unwrap()
2990                .as_float()
2991                .unwrap(),
2992            1200.0
2993        );
2994        assert_eq!(
2995            ifaces[0].get("pr_burst_active").unwrap().as_bool().unwrap(),
2996            true
2997        );
2998        assert_eq!(
2999            ifaces[0]
3000                .get("pr_burst_activated")
3001                .unwrap()
3002                .as_float()
3003                .unwrap(),
3004            1300.0
3005        );
3006        assert_eq!(
3007            ifaces[0]
3008                .get("announce_rate_target")
3009                .unwrap()
3010                .as_float()
3011                .unwrap(),
3012            3600.0
3013        );
3014        assert_eq!(
3015            ifaces[0]
3016                .get("announce_rate_grace")
3017                .unwrap()
3018                .as_int()
3019                .unwrap(),
3020            5
3021        );
3022        assert_eq!(
3023            ifaces[0]
3024                .get("announce_rate_penalty")
3025                .unwrap()
3026                .as_float()
3027                .unwrap(),
3028            0.0
3029        );
3030        assert_eq!(ifaces[0].get("clients").unwrap().as_int().unwrap(), 3);
3031        assert_eq!(ifaces[0].get("blocked_ips").unwrap().as_int().unwrap(), 2);
3032        assert_eq!(
3033            ifaces[0]
3034                .get("blocked_ip_list")
3035                .unwrap()
3036                .as_list()
3037                .unwrap()
3038                .iter()
3039                .map(|value| value.as_str().unwrap())
3040                .collect::<Vec<_>>(),
3041            vec!["192.0.2.1", "203.0.113.2"]
3042        );
3043    }
3044
3045    #[test]
3046    fn interface_stats_pickle_includes_backbone_peer_pool_priority() {
3047        let stats = InterfaceStatsResponse {
3048            interfaces: vec![],
3049            transport_id: None,
3050            transport_enabled: true,
3051            transport_uptime: 1.0,
3052            total_rxb: 0,
3053            total_txb: 0,
3054            probe_responder: None,
3055            backbone_peer_pool: Some(crate::event::BackbonePeerPoolStatus {
3056                max_connected: 1,
3057                active_count: 1,
3058                standby_count: 0,
3059                cooldown_count: 0,
3060                members: vec![crate::event::BackbonePeerPoolMemberStatus {
3061                    name: "peer".into(),
3062                    remote: "127.0.0.1:4242".into(),
3063                    source: "configured".into(),
3064                    priority: 77,
3065                    state: "active".into(),
3066                    interface_id: Some(42),
3067                    failure_count: 0,
3068                    last_error: None,
3069                    cooldown_remaining_seconds: None,
3070                }],
3071            }),
3072        };
3073
3074        let pickle = interface_stats_to_pickle(&stats);
3075        let encoded = pickle::encode(&pickle);
3076        let decoded = pickle::decode(&encoded).unwrap();
3077        let pool = decoded.get("backbone_peer_pool").unwrap();
3078        let members = pool.get("members").unwrap().as_list().unwrap();
3079        assert_eq!(members[0].get("priority").unwrap().as_int().unwrap(), 77);
3080    }
3081
3082    #[test]
3083    fn send_probe_rpc_unknown_dest() {
3084        let (event_tx, event_rx) = crate::event::channel();
3085
3086        let driver = thread::spawn(move || {
3087            if let Ok(Event::Query(
3088                QueryRequest::SendProbe {
3089                    dest_hash,
3090                    payload_size,
3091                },
3092                resp_tx,
3093            )) = event_rx.recv()
3094            {
3095                assert_eq!(dest_hash, [0xAA; 16]);
3096                assert_eq!(payload_size, 16); // default
3097                let _ = resp_tx.send(QueryResponse::SendProbe(None));
3098            }
3099        });
3100
3101        let request = PickleValue::Dict(vec![(
3102            PickleValue::String("send_probe".into()),
3103            PickleValue::Bytes(vec![0xAA; 16]),
3104        )]);
3105
3106        let response = handle_rpc_request(&request, &event_tx).unwrap();
3107        assert_eq!(response, PickleValue::None);
3108        driver.join().unwrap();
3109    }
3110
3111    #[test]
3112    fn send_probe_rpc_with_result() {
3113        let (event_tx, event_rx) = crate::event::channel();
3114
3115        let packet_hash = [0xBB; 32];
3116        let driver = thread::spawn(move || {
3117            if let Ok(Event::Query(
3118                QueryRequest::SendProbe {
3119                    dest_hash,
3120                    payload_size,
3121                },
3122                resp_tx,
3123            )) = event_rx.recv()
3124            {
3125                assert_eq!(dest_hash, [0xCC; 16]);
3126                assert_eq!(payload_size, 32);
3127                let _ = resp_tx.send(QueryResponse::SendProbe(Some((packet_hash, 3))));
3128            }
3129        });
3130
3131        let request = PickleValue::Dict(vec![
3132            (
3133                PickleValue::String("send_probe".into()),
3134                PickleValue::Bytes(vec![0xCC; 16]),
3135            ),
3136            (PickleValue::String("size".into()), PickleValue::Int(32)),
3137        ]);
3138
3139        let response = handle_rpc_request(&request, &event_tx).unwrap();
3140        let ph = response.get("packet_hash").unwrap().as_bytes().unwrap();
3141        assert_eq!(ph, &[0xBB; 32]);
3142        assert_eq!(response.get("hops").unwrap().as_int().unwrap(), 3);
3143        driver.join().unwrap();
3144    }
3145
3146    #[test]
3147    fn retain_identity_rpc_sends_identity_hash_query() {
3148        let (event_tx, event_rx) = crate::event::channel();
3149        let identity_hash = [0x44; 16];
3150
3151        let driver = thread::spawn(move || {
3152            if let Ok(Event::Query(
3153                QueryRequest::RetainIdentity {
3154                    identity_hash: hash,
3155                },
3156                resp_tx,
3157            )) = event_rx.recv()
3158            {
3159                assert_eq!(hash, identity_hash);
3160                let _ = resp_tx.send(QueryResponse::RetainIdentity(true));
3161            }
3162        });
3163
3164        let request = PickleValue::Dict(vec![
3165            (
3166                PickleValue::String("set".into()),
3167                PickleValue::String("identity_retained".into()),
3168            ),
3169            (
3170                PickleValue::String("identity_hash".into()),
3171                PickleValue::Bytes(identity_hash.to_vec()),
3172            ),
3173        ]);
3174
3175        let response = handle_rpc_request(&request, &event_tx).unwrap();
3176        assert_eq!(response, PickleValue::Bool(true));
3177        driver.join().unwrap();
3178    }
3179
3180    #[test]
3181    fn send_probe_rpc_size_validation() {
3182        let (event_tx, event_rx) = crate::event::channel();
3183
3184        // Negative size should be clamped to default (16)
3185        let driver = thread::spawn(move || {
3186            if let Ok(Event::Query(QueryRequest::SendProbe { payload_size, .. }, resp_tx)) =
3187                event_rx.recv()
3188            {
3189                assert_eq!(payload_size, 16); // default, not negative
3190                let _ = resp_tx.send(QueryResponse::SendProbe(None));
3191            }
3192        });
3193
3194        let request = PickleValue::Dict(vec![
3195            (
3196                PickleValue::String("send_probe".into()),
3197                PickleValue::Bytes(vec![0xDD; 16]),
3198            ),
3199            (PickleValue::String("size".into()), PickleValue::Int(-1)),
3200        ]);
3201
3202        let response = handle_rpc_request(&request, &event_tx).unwrap();
3203        assert_eq!(response, PickleValue::None);
3204        driver.join().unwrap();
3205    }
3206
3207    #[test]
3208    fn send_probe_rpc_size_too_large() {
3209        let (event_tx, event_rx) = crate::event::channel();
3210
3211        // Size > 400 should be clamped to default (16)
3212        let driver = thread::spawn(move || {
3213            if let Ok(Event::Query(QueryRequest::SendProbe { payload_size, .. }, resp_tx)) =
3214                event_rx.recv()
3215            {
3216                assert_eq!(payload_size, 16); // default, not 999
3217                let _ = resp_tx.send(QueryResponse::SendProbe(None));
3218            }
3219        });
3220
3221        let request = PickleValue::Dict(vec![
3222            (
3223                PickleValue::String("send_probe".into()),
3224                PickleValue::Bytes(vec![0xDD; 16]),
3225            ),
3226            (PickleValue::String("size".into()), PickleValue::Int(999)),
3227        ]);
3228
3229        let response = handle_rpc_request(&request, &event_tx).unwrap();
3230        assert_eq!(response, PickleValue::None);
3231        driver.join().unwrap();
3232    }
3233
3234    #[test]
3235    fn check_proof_rpc_not_found() {
3236        let (event_tx, event_rx) = crate::event::channel();
3237
3238        let driver = thread::spawn(move || {
3239            if let Ok(Event::Query(QueryRequest::CheckProof { packet_hash }, resp_tx)) =
3240                event_rx.recv()
3241            {
3242                assert_eq!(packet_hash, [0xEE; 32]);
3243                let _ = resp_tx.send(QueryResponse::CheckProof(None));
3244            }
3245        });
3246
3247        let request = PickleValue::Dict(vec![(
3248            PickleValue::String("check_proof".into()),
3249            PickleValue::Bytes(vec![0xEE; 32]),
3250        )]);
3251
3252        let response = handle_rpc_request(&request, &event_tx).unwrap();
3253        assert_eq!(response, PickleValue::None);
3254        driver.join().unwrap();
3255    }
3256
3257    #[test]
3258    fn check_proof_rpc_found() {
3259        let (event_tx, event_rx) = crate::event::channel();
3260
3261        let driver = thread::spawn(move || {
3262            if let Ok(Event::Query(QueryRequest::CheckProof { packet_hash }, resp_tx)) =
3263                event_rx.recv()
3264            {
3265                assert_eq!(packet_hash, [0xFF; 32]);
3266                let _ = resp_tx.send(QueryResponse::CheckProof(Some(0.352)));
3267            }
3268        });
3269
3270        let request = PickleValue::Dict(vec![(
3271            PickleValue::String("check_proof".into()),
3272            PickleValue::Bytes(vec![0xFF; 32]),
3273        )]);
3274
3275        let response = handle_rpc_request(&request, &event_tx).unwrap();
3276        if let PickleValue::Float(rtt) = response {
3277            assert!((rtt - 0.352).abs() < 0.001);
3278        } else {
3279            panic!("Expected Float, got {:?}", response);
3280        }
3281        driver.join().unwrap();
3282    }
3283
3284    #[test]
3285    fn request_path_rpc() {
3286        let (event_tx, event_rx) = crate::event::channel();
3287
3288        let driver =
3289            thread::spawn(
3290                move || match event_rx.recv_timeout(std::time::Duration::from_secs(5)) {
3291                    Ok(Event::RequestPath { dest_hash }) => {
3292                        assert_eq!(dest_hash, [0x11; 16]);
3293                    }
3294                    other => panic!("Expected RequestPath event, got {:?}", other),
3295                },
3296            );
3297
3298        let request = PickleValue::Dict(vec![(
3299            PickleValue::String("request_path".into()),
3300            PickleValue::Bytes(vec![0x11; 16]),
3301        )]);
3302
3303        let response = handle_rpc_request(&request, &event_tx).unwrap();
3304        assert_eq!(response, PickleValue::Bool(true));
3305        driver.join().unwrap();
3306    }
3307
3308    #[test]
3309    fn begin_drain_rpc_emits_event() {
3310        let (event_tx, event_rx) = crate::event::channel();
3311
3312        let driver = thread::spawn(
3313            move || match event_rx.recv_timeout(Duration::from_secs(5)) {
3314                Ok(Event::BeginDrain { timeout }) => {
3315                    assert!((timeout.as_secs_f64() - 1.5).abs() < 0.001);
3316                }
3317                other => panic!("Expected BeginDrain event, got {:?}", other),
3318            },
3319        );
3320
3321        let request = PickleValue::Dict(vec![(
3322            PickleValue::String("begin_drain".into()),
3323            PickleValue::Float(1.5),
3324        )]);
3325
3326        let response = handle_rpc_request(&request, &event_tx).unwrap();
3327        assert_eq!(response, PickleValue::Bool(true));
3328        driver.join().unwrap();
3329    }
3330
3331    #[test]
3332    fn drain_status_rpc_roundtrips_fields() {
3333        let (event_tx, event_rx) = crate::event::channel();
3334
3335        let driver = thread::spawn(move || {
3336            if let Ok(Event::Query(QueryRequest::DrainStatus, resp_tx)) = event_rx.recv() {
3337                let _ = resp_tx.send(QueryResponse::DrainStatus(DrainStatus {
3338                    state: LifecycleState::Draining,
3339                    drain_age_seconds: Some(0.75),
3340                    deadline_remaining_seconds: Some(2.25),
3341                    drain_complete: false,
3342                    interface_writer_queued_frames: 3,
3343                    provider_backlog_events: 4,
3344                    provider_consumer_queued_events: 5,
3345                    detail: Some("node is draining existing work".into()),
3346                }));
3347            }
3348        });
3349
3350        let request = PickleValue::Dict(vec![(
3351            PickleValue::String("get".into()),
3352            PickleValue::String("drain_status".into()),
3353        )]);
3354
3355        let response = handle_rpc_request(&request, &event_tx).unwrap();
3356        assert_eq!(response.get("state").unwrap().as_str(), Some("draining"));
3357        assert_eq!(
3358            response.get("drain_complete").unwrap().as_bool(),
3359            Some(false)
3360        );
3361        assert_eq!(
3362            response
3363                .get("deadline_remaining_seconds")
3364                .unwrap()
3365                .as_float(),
3366            Some(2.25)
3367        );
3368        assert_eq!(
3369            response
3370                .get("interface_writer_queued_frames")
3371                .unwrap()
3372                .as_int(),
3373            Some(3)
3374        );
3375        assert_eq!(
3376            response.get("provider_backlog_events").unwrap().as_int(),
3377            Some(4)
3378        );
3379        assert_eq!(
3380            response
3381                .get("provider_consumer_queued_events")
3382                .unwrap()
3383                .as_int(),
3384            Some(5)
3385        );
3386        assert_eq!(
3387            response.get("detail").unwrap().as_str(),
3388            Some("node is draining existing work")
3389        );
3390        driver.join().unwrap();
3391    }
3392
3393    #[test]
3394    fn interface_stats_with_probe_responder() {
3395        let probe_hash = [0x42; 16];
3396        let stats = InterfaceStatsResponse {
3397            interfaces: vec![],
3398            transport_id: None,
3399            transport_enabled: true,
3400            transport_uptime: 100.0,
3401            total_rxb: 0,
3402            total_txb: 0,
3403            probe_responder: Some(probe_hash),
3404            backbone_peer_pool: None,
3405        };
3406
3407        let pickle = interface_stats_to_pickle(&stats);
3408        let encoded = pickle::encode(&pickle);
3409        let decoded = pickle::decode(&encoded).unwrap();
3410
3411        let pr = decoded.get("probe_responder").unwrap().as_bytes().unwrap();
3412        assert_eq!(pr, &probe_hash);
3413    }
3414
3415    #[test]
3416    fn runtime_config_get_and_set_rpc() {
3417        let (event_tx, event_rx) = crate::event::channel();
3418
3419        let driver = thread::spawn(move || {
3420            if let Ok(Event::Query(QueryRequest::GetRuntimeConfig { key }, resp_tx)) =
3421                event_rx.recv()
3422            {
3423                assert_eq!(key, "global.tick_interval_ms");
3424                let _ = resp_tx.send(QueryResponse::RuntimeConfigEntry(Some(
3425                    RuntimeConfigEntry {
3426                        key,
3427                        value: RuntimeConfigValue::Int(1000),
3428                        default: RuntimeConfigValue::Int(1000),
3429                        source: RuntimeConfigSource::Startup,
3430                        apply_mode: RuntimeConfigApplyMode::Immediate,
3431                        description: Some("tick".into()),
3432                    },
3433                )));
3434            } else {
3435                panic!("expected GetRuntimeConfig query");
3436            }
3437
3438            if let Ok(Event::Query(QueryRequest::SetRuntimeConfig { key, value }, resp_tx)) =
3439                event_rx.recv()
3440            {
3441                assert_eq!(key, "global.tick_interval_ms");
3442                assert_eq!(value, RuntimeConfigValue::Int(250));
3443                let _ = resp_tx.send(QueryResponse::RuntimeConfigSet(Ok(RuntimeConfigEntry {
3444                    key,
3445                    value: RuntimeConfigValue::Int(250),
3446                    default: RuntimeConfigValue::Int(1000),
3447                    source: RuntimeConfigSource::RuntimeOverride,
3448                    apply_mode: RuntimeConfigApplyMode::Immediate,
3449                    description: Some("tick".into()),
3450                })));
3451            } else {
3452                panic!("expected SetRuntimeConfig query");
3453            }
3454        });
3455
3456        let get_request = PickleValue::Dict(vec![
3457            (
3458                PickleValue::String("get".into()),
3459                PickleValue::String("runtime_config_entry".into()),
3460            ),
3461            (
3462                PickleValue::String("key".into()),
3463                PickleValue::String("global.tick_interval_ms".into()),
3464            ),
3465        ]);
3466        let get_response = handle_rpc_request(&get_request, &event_tx).unwrap();
3467        assert_eq!(
3468            get_response.get("key").and_then(|v| v.as_str()),
3469            Some("global.tick_interval_ms")
3470        );
3471
3472        let set_request = PickleValue::Dict(vec![
3473            (
3474                PickleValue::String("set".into()),
3475                PickleValue::String("runtime_config".into()),
3476            ),
3477            (
3478                PickleValue::String("key".into()),
3479                PickleValue::String("global.tick_interval_ms".into()),
3480            ),
3481            (PickleValue::String("value".into()), PickleValue::Int(250)),
3482        ]);
3483        let set_response = handle_rpc_request(&set_request, &event_tx).unwrap();
3484        assert_eq!(
3485            set_response.get("value").and_then(|v| v.as_int()),
3486            Some(250)
3487        );
3488
3489        driver.join().unwrap();
3490    }
3491
3492    // Helper: create a connected TCP pair
3493    fn tcp_pair() -> (TcpStream, TcpStream) {
3494        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3495        let port = listener.local_addr().unwrap().port();
3496        let client = TcpStream::connect(("127.0.0.1", port)).unwrap();
3497        let (server, _) = listener.accept().unwrap();
3498        client
3499            .set_read_timeout(Some(std::time::Duration::from_secs(5)))
3500            .unwrap();
3501        server
3502            .set_read_timeout(Some(std::time::Duration::from_secs(5)))
3503            .unwrap();
3504        (server, client)
3505    }
3506}