Files
fips/src/control/protocol.rs
T
Johnathan Corgan 5053cf673d Add connect/disconnect control commands and maelstrom chaos scenario
Add runtime peer management to the FIPS daemon via control socket
commands, and a new chaos simulation scenario that exercises dynamic
topology mutation with ephemeral node identities.

Daemon (connect/disconnect commands):
- Extend control socket Request with optional params field
- Add commands.rs module for mutating command dispatch, separate from
  read-only queries
- Add api_connect() on Node: builds ephemeral PeerConfig (no auto-
  reconnect), pre-seeds identity cache, reuses initiate_peer_connection
- Add api_disconnect() on Node: calls remove_active_peer(), clears
  retry_pending to suppress reconnection
- Route non-show_* commands to async command dispatch in rx_loop

fipsctl CLI:
- Add Connect and Disconnect subcommands accepting npub or hostname
- Resolve hostnames from /etc/fips/hosts before sending to daemon
- Refactor socket I/O into reusable send_request helper

Chaos simulator (maelstrom scenario):
- Add PeerChurnManager: periodically disconnects a random active link
  and connects a random unconnected node pair via control socket
- Add send_command() to control.py using base64-encoded JSON payloads
  to avoid shell quoting issues in docker exec
- Add PeerChurnConfig to scenario with interval and ephemeral_fraction
- Ephemeral identity support: nodes configured without nsec generate
  fresh keypairs on restart; simulator queries show_status for new
  npub and updates its cache via on_node_restart callback
- Add maelstrom.yaml: all chaos dimensions (netem, link flaps, node
  churn, peer topology churn, traffic) with 50% ephemeral identity
2026-03-20 05:44:31 +00:00

112 lines
3.4 KiB
Rust

//! Control socket protocol types.
//!
//! Line-delimited JSON protocol for the Unix domain socket.
//! Each request is one JSON line, each response is one JSON line.
use serde::{Deserialize, Serialize};
/// A control request from a client.
#[derive(Debug, Deserialize)]
pub struct Request {
/// The command to execute (e.g., "show_status", "connect").
pub command: String,
/// Optional parameters for mutating commands.
#[serde(default)]
pub params: Option<serde_json::Value>,
}
/// A control response to a client.
#[derive(Debug, Serialize)]
pub struct Response {
/// "ok" or "error".
pub status: String,
/// Response data (present on success).
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
/// Error message (present on failure).
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl Response {
/// Create a success response with data.
pub fn ok(data: serde_json::Value) -> Self {
Self {
status: "ok".to_string(),
data: Some(data),
message: None,
}
}
/// Create an error response with a message.
pub fn error(message: impl Into<String>) -> Self {
Self {
status: "error".to_string(),
data: None,
message: Some(message.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_request() {
let json = r#"{"command": "show_status"}"#;
let req: Request = serde_json::from_str(json).unwrap();
assert_eq!(req.command, "show_status");
}
#[test]
fn test_serialize_ok_response() {
let resp = Response::ok(serde_json::json!({"state": "running"}));
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"status\":\"ok\""));
assert!(json.contains("\"state\":\"running\""));
assert!(!json.contains("\"message\""));
}
#[test]
fn test_serialize_error_response() {
let resp = Response::error("unknown command: foo");
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"status\":\"error\""));
assert!(json.contains("unknown command: foo"));
assert!(!json.contains("\"data\""));
}
#[test]
fn test_deserialize_unknown_fields_ignored() {
let json = r#"{"command": "show_peers", "extra": true}"#;
let req: Request = serde_json::from_str(json).unwrap();
assert_eq!(req.command, "show_peers");
}
#[test]
fn test_deserialize_request_with_params() {
let json = r#"{"command": "connect", "params": {"npub": "npub1abc", "address": "1.2.3.4:2121", "transport": "udp"}}"#;
let req: Request = serde_json::from_str(json).unwrap();
assert_eq!(req.command, "connect");
let params = req.params.unwrap();
assert_eq!(params["npub"], "npub1abc");
assert_eq!(params["transport"], "udp");
}
#[test]
fn test_deserialize_request_without_params() {
let json = r#"{"command": "show_status"}"#;
let req: Request = serde_json::from_str(json).unwrap();
assert_eq!(req.command, "show_status");
assert!(req.params.is_none());
}
#[test]
fn test_deserialize_malformed_request() {
let json = r#"{"not_command": "foo"}"#;
let result: Result<Request, _> = serde_json::from_str(json);
assert!(result.is_err());
}
}