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:
Johnathan Corgan
2026-02-01 05:18:59 +00:00
parent 8ba24c38ae
commit 0076e9930c
8 changed files with 149 additions and 62 deletions
+25 -4
View File
@@ -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 |
|------|----------------|--------------------------------------------|
+18 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]
+1 -1
View File
@@ -149,7 +149,7 @@ impl UdpTransport {
self.state = TransportState::Down;
info!(
debug!(
transport_id = %self.transport_id,
"UDP transport stopped"
);
+7 -17
View File
@@ -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(())
}