Add Unix domain control socket for runtime observability

Add a Unix domain socket interface for querying node state at runtime.
A spawned tokio task accepts connections and communicates with the main
event loop via mpsc/oneshot channels, keeping all Node access
single-threaded.

Includes:
- src/control/ module with socket lifecycle, JSON protocol, and 11
  query handlers (status, peers, links, tree, sessions, bloom, mmp,
  cache, connections, transports, routing)
- Separate fipsctl binary for CLI queries (fipsctl show <command>)
- ControlConfig in node configuration (enabled, socket_path)
- Integration into the main select! event loop
This commit is contained in:
Johnathan Corgan
2026-02-22 20:53:00 +00:00
parent 92d5df8037
commit 0a6d433d32
12 changed files with 980 additions and 3 deletions
+29 -1
View File
@@ -1,10 +1,12 @@
//! RX event loop and packet dispatch.
use crate::control::ControlSocket;
use crate::control::queries;
use crate::node::{Node, NodeError};
use crate::transport::ReceivedPacket;
use crate::node::wire::{CommonPrefix, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2, FMP_VERSION, COMMON_PREFIX_SIZE};
use std::time::Duration;
use tracing::{debug, info};
use tracing::{debug, info, warn};
impl Node {
/// Run the receive event loop.
@@ -53,6 +55,28 @@ impl Node {
let mut tick = tokio::time::interval(Duration::from_secs(self.config.node.tick_interval_secs));
// Set up control socket channel
let (control_tx, mut control_rx) = tokio::sync::mpsc::channel::<
crate::control::ControlMessage,
>(32);
if self.config.node.control.enabled {
let config = self.config.node.control.clone();
let tx = control_tx.clone();
tokio::spawn(async move {
match ControlSocket::bind(&config) {
Ok(socket) => {
socket.accept_loop(tx).await;
}
Err(e) => {
warn!(error = %e, "Failed to bind control socket");
}
}
});
}
// Drop unused sender to avoid keeping channel open if control is disabled
drop(control_tx);
info!("RX event loop started");
loop {
@@ -73,6 +97,10 @@ impl Node {
);
self.register_identity(identity.node_addr, identity.pubkey);
}
Some((request, response_tx)) = control_rx.recv() => {
let response = queries::dispatch(self, &request.command);
let _ = response_tx.send(response);
}
_ = tick.tick() => {
self.check_timeouts();
let now_ms = std::time::SystemTime::now()
+15
View File
@@ -919,6 +919,11 @@ impl Node {
self.sessions.len()
}
/// Iterate over all session entries (for control queries).
pub(crate) fn session_entries(&self) -> impl Iterator<Item = (&NodeAddr, &SessionEntry)> {
self.sessions.iter()
}
// === Identity Cache ===
/// Register a node in the identity cache for FipsAddress → NodeAddr lookup.
@@ -952,6 +957,16 @@ impl Node {
self.identity_cache.len()
}
/// Number of pending discovery lookups.
pub fn pending_lookup_count(&self) -> usize {
self.pending_lookups.len()
}
/// Number of recent discovery requests tracked.
pub fn recent_request_count(&self) -> usize {
self.recent_requests.len()
}
// === Routing ===
/// Find next hop for a destination node address.