diff --git a/docs/design/fips-transports.md b/docs/design/fips-transports.md index b75e020..3aa9aab 100644 --- a/docs/design/fips-transports.md +++ b/docs/design/fips-transports.md @@ -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: diff --git a/src/bin/fips.rs b/src/bin/fips.rs new file mode 100644 index 0000000..b63fa40 --- /dev/null +++ b/src/bin/fips.rs @@ -0,0 +1,3 @@ +fn main() { + // +} diff --git a/src/config.rs b/src/config.rs index e320c13..d2a58b7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, } +/// 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 { - 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")); diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 3885376..0000000 --- a/src/main.rs +++ /dev/null @@ -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."); -} diff --git a/src/node.rs b/src/node.rs index 566ec9d..6815556 100644 --- a/src/node.rs +++ b/src/node.rs @@ -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(); diff --git a/src/transport.rs b/src/transport.rs index d31766d..ea5c31d 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -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());