mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
Session 48: Wire format design and logging improvements
Wire format: - Add HandshakeMessageType enum (NoiseIKMsg1=0x01, NoiseIKMsg2=0x02) - Define unified TLV framing for handshake and post-handshake messages - Update fips-design.md and fips-protocol-flow.md with wire format details Initialization: - Defer TUN setup until after peer connections initiated - Order: packet channel → transports → peers → TUN Logging cleanup: - Consolidate node/TUN info logs into aligned multi-line format - TUN shutdown: single start/stop message, remove intermediate noise - Remove ip link show callout and redundant Starting node message - Change UDP transport stopped to debug level
This commit is contained in:
@@ -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 |
|
||||
|------|----------------|--------------------------------------------|
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-13
@@ -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);
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+4
-18
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
+85
-2
@@ -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<Self> {
|
||||
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]
|
||||
|
||||
@@ -149,7 +149,7 @@ impl UdpTransport {
|
||||
|
||||
self.state = TransportState::Down;
|
||||
|
||||
info!(
|
||||
debug!(
|
||||
transport_id = %self.transport_id,
|
||||
"UDP transport stopped"
|
||||
);
|
||||
|
||||
+7
-17
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user