mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 08:37:02 +00:00
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
112 lines
3.4 KiB
Rust
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());
|
|
}
|
|
}
|