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