mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Restructure config to match sysctl-style paths
- Add NodeConfig wrapper: node.identity.nsec, node.leaf_only - YAML structure mirrors architecture documentation exactly - Move main.rs to src/bin/fips.rs (empty binary placeholder) - Add "Connection Model" section to fips-transports.md documenting connectionless vs connection-oriented transport categories - Fix clippy warnings (dead_code, clone_on_copy, assertions_on_constants)
This commit is contained in:
@@ -94,6 +94,62 @@ the routing layer.
|
||||
**Bandwidth**: Order of magnitude. Affects flow control and congestion
|
||||
decisions, but FIPS routing itself is low-bandwidth (control plane only).
|
||||
|
||||
## Connection Model
|
||||
|
||||
Transports fall into two categories based on whether they require connection
|
||||
establishment before data can be exchanged:
|
||||
|
||||
### Connectionless Transports
|
||||
|
||||
These transports can send datagrams to a peer address without prior setup.
|
||||
Links are lightweight—just a `(transport_id, remote_addr)` tuple with implicit
|
||||
"established" state.
|
||||
|
||||
| Transport | Notes |
|
||||
|-----------|-------|
|
||||
| UDP/IP | Stateless datagrams; NAT state is implicit |
|
||||
| Ethernet | Send to MAC address directly |
|
||||
| WiFi | Same as Ethernet (802.11 frame) |
|
||||
| DOCSIS | Cable modem layer; uses IP in practice |
|
||||
| LoRa | Raw packets to device address |
|
||||
| I2P | Datagram mode (not streaming) |
|
||||
|
||||
### Connection-Oriented Transports
|
||||
|
||||
These transports require explicit connection setup before FIPS traffic can flow.
|
||||
Links track real connection state and hold I/O handles. The link must complete
|
||||
transport-layer connection before FIPS authentication can proceed.
|
||||
|
||||
| Transport | Connection Setup |
|
||||
|-----------|------------------|
|
||||
| TCP/IP | TCP handshake |
|
||||
| WebSocket | HTTP upgrade + TCP |
|
||||
| Tor | Circuit establishment (slow: 500ms-5s) |
|
||||
| Bluetooth Classic | L2CAP connection |
|
||||
| BLE | L2CAP CoC or GATT connection |
|
||||
| Zigbee | Network join + binding |
|
||||
| Serial | Physical connection (static) |
|
||||
| Dialup | PPP negotiation |
|
||||
|
||||
### Implications for FIPS
|
||||
|
||||
**Link lifecycle**: Connectionless transports use a trivial link model (no state
|
||||
machine). Connection-oriented transports require a real state machine:
|
||||
`Connecting → Connected → Disconnected`. See
|
||||
[fips-architecture.md](fips-architecture.md) for link lifecycle details.
|
||||
|
||||
**Startup latency**: Connection-oriented transports add latency before a peer
|
||||
becomes usable. Tor is particularly slow (circuit setup). This affects peer
|
||||
timeout configuration.
|
||||
|
||||
**Failure modes**: Connectionless links "fail" only when the transport itself
|
||||
is down or the peer stops responding. Connection-oriented links can fail during
|
||||
connection setup, adding more error handling paths.
|
||||
|
||||
**Framing**: Connection-oriented stream transports (TCP, WebSocket, Tor) require
|
||||
length-prefix framing to delineate FIPS packets. Datagram transports have
|
||||
natural packet boundaries.
|
||||
|
||||
## UDP/IP as Primary Internet Transport
|
||||
|
||||
For internet-connected nodes, UDP/IP is the recommended transport:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
//
|
||||
}
|
||||
+88
-51
@@ -6,6 +6,17 @@
|
||||
//! 3. `/etc/fips/fips.yaml` (system - lowest priority)
|
||||
//!
|
||||
//! Values from higher priority files override those from lower priority files.
|
||||
//!
|
||||
//! # YAML Structure
|
||||
//!
|
||||
//! The YAML structure mirrors the sysctl-style paths in the architecture docs.
|
||||
//! For example, `node.identity.nsec` in the docs corresponds to:
|
||||
//!
|
||||
//! ```yaml
|
||||
//! node:
|
||||
//! identity:
|
||||
//! nsec: "nsec1..."
|
||||
//! ```
|
||||
|
||||
use crate::{Identity, IdentityError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -34,21 +45,33 @@ pub enum ConfigError {
|
||||
Identity(#[from] IdentityError),
|
||||
}
|
||||
|
||||
/// Identity configuration section.
|
||||
/// Identity configuration (`node.identity.*`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct IdentityConfig {
|
||||
/// Node secret key in nsec (bech32) or hex format.
|
||||
/// Secret key in nsec (bech32) or hex format (`node.identity.nsec`).
|
||||
/// If not specified, a new keypair will be generated.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nsec: Option<String>,
|
||||
}
|
||||
|
||||
/// Node configuration (`node.*`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct NodeConfig {
|
||||
/// Identity configuration (`node.identity.*`).
|
||||
#[serde(default)]
|
||||
pub identity: IdentityConfig,
|
||||
|
||||
/// Leaf-only mode (`node.leaf_only`).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub leaf_only: bool,
|
||||
}
|
||||
|
||||
/// Root configuration structure.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Identity configuration.
|
||||
/// Node configuration (`node.*`).
|
||||
#[serde(default)]
|
||||
pub identity: IdentityConfig,
|
||||
pub node: NodeConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -129,9 +152,13 @@ impl Config {
|
||||
///
|
||||
/// Values from `other` override values in `self` when present.
|
||||
pub fn merge(&mut self, other: Config) {
|
||||
// Merge identity section
|
||||
if other.identity.nsec.is_some() {
|
||||
self.identity.nsec = other.identity.nsec;
|
||||
// Merge node.identity section
|
||||
if other.node.identity.nsec.is_some() {
|
||||
self.node.identity.nsec = other.node.identity.nsec;
|
||||
}
|
||||
// Merge node.leaf_only
|
||||
if other.node.leaf_only {
|
||||
self.node.leaf_only = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +167,7 @@ impl Config {
|
||||
/// If an nsec is configured, uses that to create the identity.
|
||||
/// Otherwise, generates a new random identity.
|
||||
pub fn create_identity(&self) -> Result<Identity, ConfigError> {
|
||||
match &self.identity.nsec {
|
||||
match &self.node.identity.nsec {
|
||||
Some(nsec) => Ok(Identity::from_secret_str(nsec)?),
|
||||
None => Ok(Identity::generate()),
|
||||
}
|
||||
@@ -148,7 +175,12 @@ impl Config {
|
||||
|
||||
/// Check if an identity is configured (vs. will be generated).
|
||||
pub fn has_identity(&self) -> bool {
|
||||
self.identity.nsec.is_some()
|
||||
self.node.identity.nsec.is_some()
|
||||
}
|
||||
|
||||
/// Check if leaf-only mode is configured.
|
||||
pub fn is_leaf_only(&self) -> bool {
|
||||
self.node.leaf_only
|
||||
}
|
||||
|
||||
/// Serialize this configuration to YAML.
|
||||
@@ -166,29 +198,31 @@ mod tests {
|
||||
#[test]
|
||||
fn test_empty_config() {
|
||||
let config = Config::new();
|
||||
assert!(config.identity.nsec.is_none());
|
||||
assert!(config.node.identity.nsec.is_none());
|
||||
assert!(!config.has_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_with_nsec() {
|
||||
let yaml = r#"
|
||||
identity:
|
||||
nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
|
||||
node:
|
||||
identity:
|
||||
nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_some());
|
||||
assert!(config.node.identity.nsec.is_some());
|
||||
assert!(config.has_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_with_hex() {
|
||||
let yaml = r#"
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
node:
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_some());
|
||||
assert!(config.node.identity.nsec.is_some());
|
||||
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
@@ -198,53 +232,51 @@ identity:
|
||||
fn test_parse_yaml_empty() {
|
||||
let yaml = "";
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_none());
|
||||
assert!(config.node.identity.nsec.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_partial() {
|
||||
let yaml = r#"
|
||||
identity: {}
|
||||
node:
|
||||
identity: {}
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_none());
|
||||
assert!(config.node.identity.nsec.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_configs() {
|
||||
let mut base = Config::new();
|
||||
base.identity.nsec = Some("base_nsec".to_string());
|
||||
base.node.identity.nsec = Some("base_nsec".to_string());
|
||||
|
||||
let override_config = Config {
|
||||
identity: IdentityConfig {
|
||||
nsec: Some("override_nsec".to_string()),
|
||||
},
|
||||
};
|
||||
let mut override_config = Config::new();
|
||||
override_config.node.identity.nsec = Some("override_nsec".to_string());
|
||||
|
||||
base.merge(override_config);
|
||||
assert_eq!(base.identity.nsec, Some("override_nsec".to_string()));
|
||||
assert_eq!(
|
||||
base.node.identity.nsec,
|
||||
Some("override_nsec".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_preserves_base_when_override_empty() {
|
||||
let mut base = Config::new();
|
||||
base.identity.nsec = Some("base_nsec".to_string());
|
||||
base.node.identity.nsec = Some("base_nsec".to_string());
|
||||
|
||||
let override_config = Config::new();
|
||||
|
||||
base.merge(override_config);
|
||||
assert_eq!(base.identity.nsec, Some("base_nsec".to_string()));
|
||||
assert_eq!(base.node.identity.nsec, Some("base_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_identity_from_nsec() {
|
||||
let config = Config {
|
||||
identity: IdentityConfig {
|
||||
nsec: Some(
|
||||
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
|
||||
),
|
||||
},
|
||||
};
|
||||
let mut config = Config::new();
|
||||
config.node.identity.nsec = Some(
|
||||
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
|
||||
);
|
||||
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
@@ -263,13 +295,14 @@ identity: {}
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
|
||||
let yaml = r#"
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
node:
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
"#;
|
||||
fs::write(&config_path, yaml).unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
assert!(config.identity.nsec.is_some());
|
||||
assert!(config.node.identity.nsec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -283,8 +316,9 @@ identity:
|
||||
fs::write(
|
||||
&low_priority,
|
||||
r#"
|
||||
identity:
|
||||
nsec: "low_priority_nsec"
|
||||
node:
|
||||
identity:
|
||||
nsec: "low_priority_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -292,8 +326,9 @@ identity:
|
||||
fs::write(
|
||||
&high_priority,
|
||||
r#"
|
||||
identity:
|
||||
nsec: "high_priority_nsec"
|
||||
node:
|
||||
identity:
|
||||
nsec: "high_priority_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -302,7 +337,10 @@ identity:
|
||||
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
|
||||
|
||||
assert_eq!(loaded.len(), 2);
|
||||
assert_eq!(config.identity.nsec, Some("high_priority_nsec".to_string()));
|
||||
assert_eq!(
|
||||
config.node.identity.nsec,
|
||||
Some("high_priority_nsec".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -314,8 +352,9 @@ identity:
|
||||
fs::write(
|
||||
&existing,
|
||||
r#"
|
||||
identity:
|
||||
nsec: "existing_nsec"
|
||||
node:
|
||||
identity:
|
||||
nsec: "existing_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -325,7 +364,7 @@ identity:
|
||||
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0], existing);
|
||||
assert_eq!(config.identity.nsec, Some("existing_nsec".to_string()));
|
||||
assert_eq!(config.node.identity.nsec, Some("existing_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -343,13 +382,11 @@ identity:
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml() {
|
||||
let config = Config {
|
||||
identity: IdentityConfig {
|
||||
nsec: Some("test_nsec".to_string()),
|
||||
},
|
||||
};
|
||||
let mut config = Config::new();
|
||||
config.node.identity.nsec = Some("test_nsec".to_string());
|
||||
|
||||
let yaml = config.to_yaml().unwrap();
|
||||
assert!(yaml.contains("node:"));
|
||||
assert!(yaml.contains("identity:"));
|
||||
assert!(yaml.contains("nsec:"));
|
||||
assert!(yaml.contains("test_nsec"));
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
use fips::Config;
|
||||
|
||||
fn main() {
|
||||
println!("FIPS Node Startup");
|
||||
println!("=================\n");
|
||||
|
||||
// Load configuration from standard search paths
|
||||
println!("1. Loading configuration...");
|
||||
println!(" Search paths (in priority order, lowest to highest):");
|
||||
for path in Config::search_paths() {
|
||||
let exists = path.exists();
|
||||
let status = if exists { "[found]" } else { "[not found]" };
|
||||
println!(" {} {}", status, path.display());
|
||||
}
|
||||
println!();
|
||||
|
||||
let (config, loaded_paths) = match Config::load() {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
eprintln!(" Error loading config: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if loaded_paths.is_empty() {
|
||||
println!(" No config files found, using defaults.");
|
||||
} else {
|
||||
println!(" Loaded {} config file(s):", loaded_paths.len());
|
||||
for path in &loaded_paths {
|
||||
println!(" - {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// Create identity from configuration
|
||||
println!("\n2. Initializing identity...");
|
||||
let identity = match config.create_identity() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
eprintln!(" Error creating identity: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if config.has_identity() {
|
||||
println!(" Using configured identity.");
|
||||
} else {
|
||||
println!(" No identity configured, generated new keypair.");
|
||||
}
|
||||
|
||||
println!();
|
||||
if let Some(nsec) = &config.identity.nsec {
|
||||
println!(" nsec: {}", nsec);
|
||||
} else {
|
||||
println!(" nsec: (generated)");
|
||||
}
|
||||
println!(" npub: {}", identity.npub());
|
||||
println!(" node_id: {}", identity.node_id());
|
||||
println!(" IPv6 address: {}", identity.address());
|
||||
|
||||
println!("\nReady.");
|
||||
}
|
||||
+2
-1
@@ -501,6 +501,7 @@ mod tests {
|
||||
Node::new(config).unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn make_node_id(val: u8) -> NodeId {
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes[0] = val;
|
||||
@@ -646,7 +647,7 @@ mod tests {
|
||||
|
||||
let peer_identity = Identity::generate();
|
||||
let peer_pub = crate::PeerIdentity::from_pubkey(peer_identity.pubkey());
|
||||
let peer1 = Peer::discovered(peer_pub.clone(), LinkId::new(1));
|
||||
let peer1 = Peer::discovered(peer_pub, LinkId::new(1));
|
||||
let peer2 = Peer::discovered(peer_pub, LinkId::new(2));
|
||||
|
||||
node.add_peer(peer1).unwrap();
|
||||
|
||||
@@ -695,7 +695,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::assertions_on_constants)]
|
||||
fn test_transport_type_constants() {
|
||||
// These assertions verify the constant definitions are correct
|
||||
assert!(!TransportType::UDP.connection_oriented);
|
||||
assert!(!TransportType::UDP.reliable);
|
||||
assert!(TransportType::UDP.is_connectionless());
|
||||
|
||||
Reference in New Issue
Block a user