diff --git a/docs/design/fips-design.md b/docs/design/fips-design.md index 4126fe8..92bacae 100644 --- a/docs/design/fips-design.md +++ b/docs/design/fips-design.md @@ -581,11 +581,13 @@ A single node may have multiple transports of different types: ## 6. Protocol Messages -FIPS uses two independent message type spaces corresponding to the two protocol -layers. Messages are exchanged over Noise-encrypted links after peer authentication. +FIPS uses a unified TLV (Type-Length-Value) wire format for all messages, +including handshake and post-handshake communication. ### Wire Format +All FIPS link messages use this framing: + ```text ┌────────┬────────┬────────────────────────────────────┐ │ Type │ Length │ Payload │ @@ -593,10 +595,29 @@ layers. Messages are exchanged over Noise-encrypted links after peer authenticat └────────┴────────┴────────────────────────────────────┘ ``` -### Link Layer Messages (Peer-to-Peer) +- **Type**: Message type identifier (determines encryption state) +- **Length**: Big-endian payload length in bytes +- **Payload**: Message-specific data + +### Handshake Messages (0x00-0x0F) + +Exchanged during Noise IK handshake before link encryption is established. +Payloads are not encrypted (except Noise-internal encryption of static key). + +| Type | Name | Payload | Description | +|------|-------------|---------|------------------------------------------| +| 0x01 | NoiseIKMsg1 | 82 bytes| Initiator: ephemeral + encrypted static | +| 0x02 | NoiseIKMsg2 | 33 bytes| Responder: ephemeral pubkey | + +Receiver logic: + +- Type < 0x10 → handshake message, process as raw Noise +- Type ≥ 0x10 → post-handshake, decrypt payload with session keys + +### Link Layer Messages (0x10-0x4F) Exchanged between directly connected peers over Noise-encrypted links. -Peer authentication uses Noise IK handshake (see §1) before any messages. +All payloads are encrypted with session keys from the Noise IK handshake. | Type | Name | Description | |------|----------------|--------------------------------------------| diff --git a/docs/design/fips-protocol-flow.md b/docs/design/fips-protocol-flow.md index 809e0c4..7baf34a 100644 --- a/docs/design/fips-protocol-flow.md +++ b/docs/design/fips-protocol-flow.md @@ -686,6 +686,15 @@ for the startup sequence. ### 7.1 Connection Flow Summary (Noise IK) +All messages use TLV framing (see fips-design.md §6 Wire Format): + +```text +┌────────┬────────┬────────────────────────────────────┐ +│ Type │ Length │ Payload │ +│ 1 byte │ 2 bytes│ Variable │ +└────────┴────────┴────────────────────────────────────┘ +``` + **Outbound (to static peer):** ```text @@ -695,10 +704,12 @@ Config: npub + transport hint (e.g., "udp:192.168.1.1:4000") Create link via transport │ ▼ -Noise IK msg1 (82 bytes): ephemeral + encrypted static key +Send: [0x01][0x00 0x52][82-byte Noise msg1] + Type=NoiseIKMsg1, Length=82 │ ▼ -Receive msg2 (33 bytes): peer's ephemeral key +Recv: [0x02][0x00 0x21][33-byte Noise msg2] + Type=NoiseIKMsg2, Length=33 │ ▼ Noise session established → link encrypted → begins tree gossip @@ -707,13 +718,16 @@ Noise session established → link encrypted → begins tree gossip **Inbound (peer connects to us):** ```text -Transport receives Noise msg1 from unknown address +Transport receives packet from unknown address + │ + ▼ +Parse TLV: Type=0x01 (NoiseIKMsg1) │ ▼ Process msg1 → learn peer's identity from encrypted static key │ ▼ -Send msg2 (33 bytes): our ephemeral key +Send: [0x02][0x00 0x21][33-byte Noise msg2] │ ▼ Noise session established → link encrypted → begins tree gossip diff --git a/src/bin/fips.rs b/src/bin/fips.rs index e4443fd..8396341 100644 --- a/src/bin/fips.rs +++ b/src/bin/fips.rs @@ -81,21 +81,14 @@ async fn main() { }; // Log node information - info!( - state = %node.state(), - leaf_only = node.is_leaf_only(), - "Node created" - ); - info!(" npub: {}", node.npub()); - info!(" node_id: {}", hex::encode(node.node_id().as_bytes())); - info!(" address: {}", node.identity().address()); + info!("Node created:"); + info!(" npub: {}", node.npub()); + info!(" node_id: {}", hex::encode(node.node_id().as_bytes())); + info!(" address: {}", node.identity().address()); + info!(" state: {}", node.state()); + info!(" leaf_only: {}", node.is_leaf_only()); // Start the node (initializes TUN, spawns I/O threads) - info!( - tun_state = %node.tun_state(), - "Starting node" - ); - if let Err(e) = node.start().await { error!("Failed to start node: {}", e); std::process::exit(1); diff --git a/src/lib.rs b/src/lib.rs index 1472848..00a1493 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,9 +41,9 @@ pub use transport::udp::UdpTransport; // Re-export protocol types pub use protocol::{ - CoordsRequired, DataFlags, DataPacket, FilterAnnounce, LinkMessageType, LookupRequest, - LookupResponse, PathBroken, ProtocolError, SessionAck, SessionDatagram, SessionFlags, - SessionMessageType, SessionSetup, TreeAnnounce, + CoordsRequired, DataFlags, DataPacket, FilterAnnounce, HandshakeMessageType, LinkMessageType, + LookupRequest, LookupResponse, PathBroken, ProtocolError, SessionAck, SessionDatagram, + SessionFlags, SessionMessageType, SessionSetup, TreeAnnounce, }; // Re-export cache types diff --git a/src/node.rs b/src/node.rs index 02097cd..d47bb2f 100644 --- a/src/node.rs +++ b/src/node.rs @@ -244,12 +244,6 @@ impl Node { .sign_declaration(&identity) .expect("signing own declaration should never fail"); - info!( - node_id = %node_id, - address = %identity.address(), - "Node initialized as root" - ); - Ok(Self { identity, config, @@ -293,12 +287,6 @@ impl Node { .sign_declaration(&identity) .expect("signing own declaration should never fail"); - info!( - node_id = %node_id, - address = %identity.address(), - "Node initialized as root" - ); - Self { identity, config, @@ -1032,12 +1020,10 @@ impl Node { } self.state = NodeState::Running; - info!( - state = %self.state, - transports = self.transports.len(), - connections = self.connections.len(), - "Node started" - ); + info!("Node started:"); + info!(" state: {}", self.state); + info!(" transports: {}", self.transports.len()); + info!(" connections: {}", self.connections.len()); Ok(()) } diff --git a/src/protocol.rs b/src/protocol.rs index 17381b7..6a78517 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -37,11 +37,63 @@ pub const DATA_HEADER_SIZE: usize = 36; // Link Layer Message Types (peer-to-peer, hop-by-hop) // ============================================================================ +/// Handshake message type identifiers. +/// +/// These messages are exchanged during Noise IK handshake before link +/// encryption is established. They use the same TLV framing as link +/// messages but payloads are not encrypted (except Noise-internal encryption). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum HandshakeMessageType { + /// Noise IK message 1: initiator sends ephemeral + encrypted static. + /// Payload: 82 bytes (33 ephemeral + 33 static + 16 tag). + NoiseIKMsg1 = 0x01, + + /// Noise IK message 2: responder sends ephemeral. + /// Payload: 33 bytes (ephemeral pubkey only). + NoiseIKMsg2 = 0x02, +} + +impl HandshakeMessageType { + /// Try to convert from a byte. + pub fn from_byte(b: u8) -> Option { + match b { + 0x01 => Some(HandshakeMessageType::NoiseIKMsg1), + 0x02 => Some(HandshakeMessageType::NoiseIKMsg2), + _ => None, + } + } + + /// Convert to a byte. + pub fn to_byte(self) -> u8 { + self as u8 + } + + /// Check if a byte represents a handshake message type. + pub fn is_handshake(b: u8) -> bool { + matches!(b, 0x01 | 0x02) + } +} + +impl fmt::Display for HandshakeMessageType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + HandshakeMessageType::NoiseIKMsg1 => "NoiseIKMsg1", + HandshakeMessageType::NoiseIKMsg2 => "NoiseIKMsg2", + }; + write!(f, "{}", name) + } +} + +// ============================================================================ +// Link-Layer Message Types +// ============================================================================ + /// Link-layer message type identifiers. /// /// These messages are exchanged between directly connected peers over -/// Noise-encrypted links. Peer authentication happens via Noise IK -/// handshake before any of these messages are sent. +/// Noise-encrypted links. All payloads are encrypted with session keys +/// established during the Noise IK handshake. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum LinkMessageType { @@ -729,6 +781,37 @@ mod tests { TreeCoordinate::new(ids.iter().map(|&v| make_node_id(v)).collect()).unwrap() } + // ===== HandshakeMessageType Tests ===== + + #[test] + fn test_handshake_message_type_roundtrip() { + let types = [ + HandshakeMessageType::NoiseIKMsg1, + HandshakeMessageType::NoiseIKMsg2, + ]; + + for ty in types { + let byte = ty.to_byte(); + let restored = HandshakeMessageType::from_byte(byte); + assert_eq!(restored, Some(ty)); + } + } + + #[test] + fn test_handshake_message_type_invalid() { + assert!(HandshakeMessageType::from_byte(0x00).is_none()); + assert!(HandshakeMessageType::from_byte(0x03).is_none()); + assert!(HandshakeMessageType::from_byte(0x10).is_none()); + } + + #[test] + fn test_handshake_message_type_is_handshake() { + assert!(HandshakeMessageType::is_handshake(0x01)); + assert!(HandshakeMessageType::is_handshake(0x02)); + assert!(!HandshakeMessageType::is_handshake(0x00)); + assert!(!HandshakeMessageType::is_handshake(0x10)); + } + // ===== LinkMessageType Tests ===== #[test] diff --git a/src/transport/udp.rs b/src/transport/udp.rs index fc03a84..4f0db94 100644 --- a/src/transport/udp.rs +++ b/src/transport/udp.rs @@ -149,7 +149,7 @@ impl UdpTransport { self.state = TransportState::Down; - info!( + debug!( transport_id = %self.transport_id, "UDP transport stopped" ); diff --git a/src/tun.rs b/src/tun.rs index e577155..89d9b9b 100644 --- a/src/tun.rs +++ b/src/tun.rs @@ -204,14 +204,13 @@ impl TunWriter { /// Blocks forever, reading packets from the channel and writing them /// to the TUN device. Returns when the channel is closed (all senders dropped). pub fn run(mut self) { - info!(name = %self.name, "TUN writer starting"); + debug!(name = %self.name, "TUN writer starting"); for packet in self.rx { if let Err(e) = self.file.write_all(&packet) { // "Bad address" is expected during shutdown when interface is deleted let err_str = e.to_string(); if err_str.contains("Bad address") { - info!(name = %self.name, "TUN interface deleted, writer stopping"); break; } error!(name = %self.name, error = %e, "TUN write error"); @@ -219,8 +218,6 @@ impl TunWriter { debug!(name = %self.name, len = packet.len(), "TUN packet written"); } } - - info!(name = %self.name, "TUN writer stopped"); } } @@ -243,7 +240,7 @@ pub fn run_tun_reader( let name = device.name().to_string(); let mut buf = vec![0u8; mtu as usize + 100]; // Extra space for headers - info!(name = %name, "TUN reader starting"); + debug!(name = %name, "TUN reader starting"); loop { match device.read_packet(&mut buf) { @@ -265,7 +262,6 @@ pub fn run_tun_reader( "Sending ICMPv6 Destination Unreachable" ); if tun_tx.send(response).is_err() { - info!(name = %name, "TUN writer channel closed, reader stopping"); break; } } @@ -277,17 +273,13 @@ pub fn run_tun_reader( Err(e) => { // "Bad address" (EFAULT) is expected during shutdown when interface is deleted let err_str = format!("{}", e); - if err_str.contains("Bad address") { - info!(name = %name, "TUN interface deleted, reader stopping"); - } else { + if !err_str.contains("Bad address") { error!(name = %name, error = %e, "TUN read error"); } break; } } } - - info!(name = %name, "TUN reader stopped"); } /// Log basic information about an IPv6 packet at DEBUG level. @@ -330,8 +322,10 @@ pub fn log_ipv6_packet(packet: &[u8]) { /// to return an error. Use this for graceful shutdown when the TUN device /// has been moved to another thread. pub async fn shutdown_tun_interface(name: &str) -> Result<(), TunError> { - info!(name, "shutdown_tun_interface called"); - delete_interface(name).await + info!("Shutting down TUN interface {}", name); + delete_interface(name).await?; + info!("TUN interface {} stopped", name); + Ok(()) } impl std::fmt::Debug for TunDevice { @@ -356,16 +350,12 @@ async fn interface_exists(name: &str) -> bool { /// Delete a network interface by name. async fn delete_interface(name: &str) -> Result<(), TunError> { - info!(name, "delete_interface: starting"); let (connection, handle, _) = new_connection() .map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?; tokio::spawn(connection); let index = get_interface_index(&handle, name).await?; - info!(name, index, "delete_interface: got index, deleting"); handle.link().del(index).execute().await?; - - info!(name, "delete_interface: done"); Ok(()) }