Add explicit counter to DataPacket for out-of-order decryption

Session-layer encryption used an implicit nonce counter with no counter
on the wire, requiring packets to arrive in exact order. Under bulk
transfer load (e.g., SCP), UDP packet loss or reordering permanently
desynchronized sender/receiver counters, causing all subsequent
decryption to fail with no recovery.

Add an 8-byte counter field to the DataPacket wire format and switch
from implicit-counter decrypt() to decrypt_with_replay_check() which
uses the explicit wire counter plus a 2048-packet sliding replay
window — the same pattern already used at the link layer.

Wire format: msg_type(1) + flags(1) + counter(8) + payload_len(2) = 12
bytes (was 4). FIPS_OVERHEAD updated 127 → 135 bytes accordingly.
This commit is contained in:
Origami74
2026-02-17 00:33:01 +00:00
committed by Johnathan Corgan
parent 3ca2f9500a
commit d3477a7c0f
4 changed files with 51 additions and 33 deletions
+7 -4
View File
@@ -293,10 +293,10 @@ impl Node {
} }
}; };
let plaintext = match session.decrypt(&packet.payload) { let plaintext = match session.decrypt_with_replay_check(&packet.payload, packet.counter) {
Ok(pt) => pt, Ok(pt) => pt,
Err(e) => { Err(e) => {
debug!(error = %e, src = %src_addr, "Session decryption failed"); debug!(error = %e, src = %src_addr, counter = packet.counter, "Session decryption failed");
self.sessions.insert(*src_addr, entry); self.sessions.insert(*src_addr, entry);
return; return;
} }
@@ -473,6 +473,9 @@ impl Node {
} }
}; };
// Get counter before encrypting (encrypt will increment it)
let counter = session.current_send_counter();
// Encrypt with session key // Encrypt with session key
let ciphertext = session.encrypt(plaintext).map_err(|e| NodeError::SendFailed { let ciphertext = session.encrypt(plaintext).map_err(|e| NodeError::SendFailed {
node_addr: *dest_addr, node_addr: *dest_addr,
@@ -485,8 +488,8 @@ impl Node {
entry.set_coords_warmup_remaining(entry.coords_warmup_remaining() - 1); entry.set_coords_warmup_remaining(entry.coords_warmup_remaining() - 1);
} }
// Build DataPacket, conditionally with coordinates // Build DataPacket with explicit counter for replay protection
let mut data_packet = DataPacket::new(ciphertext); let mut data_packet = DataPacket::new(counter, ciphertext);
if include_coords { if include_coords {
let my_coords = self.tree_state.my_coords().clone(); let my_coords = self.tree_state.my_coords().clone();
let dest_coords = self.get_dest_coords(dest_addr); let dest_coords = self.get_dest_coords(dest_addr);
+2 -2
View File
@@ -194,7 +194,7 @@ async fn test_coord_cache_warming_data_packet_with_coords() {
let src_coords = TreeCoordinate::from_addrs(vec![src_addr, root_addr]).unwrap(); let src_coords = TreeCoordinate::from_addrs(vec![src_addr, root_addr]).unwrap();
let dest_coords = TreeCoordinate::from_addrs(vec![dest_addr, root_addr]).unwrap(); let dest_coords = TreeCoordinate::from_addrs(vec![dest_addr, root_addr]).unwrap();
let data = DataPacket::new(vec![1, 2, 3, 4]) let data = DataPacket::new(0, vec![1, 2, 3, 4])
.with_coords(src_coords.clone(), dest_coords.clone()); .with_coords(src_coords.clone(), dest_coords.clone());
let data_payload = data.encode(); let data_payload = data.encode();
@@ -229,7 +229,7 @@ async fn test_coord_cache_warming_opaque_data_packet() {
let dest_addr = make_node_addr(0x02); let dest_addr = make_node_addr(0x02);
// DataPacket without COORDS_PRESENT — no coords to cache // DataPacket without COORDS_PRESENT — no coords to cache
let data = DataPacket::new(vec![1, 2, 3, 4]); let data = DataPacket::new(0, vec![1, 2, 3, 4]);
let data_payload = data.encode(); let data_payload = data.encode();
let dg = SessionDatagram::new(src_addr, dest_addr, data_payload); let dg = SessionDatagram::new(src_addr, dest_addr, data_payload);
+40 -25
View File
@@ -482,20 +482,21 @@ impl DataFlags {
} }
/// DataPacket header size in bytes (excluding payload). /// DataPacket header size in bytes (excluding payload).
/// msg_type(1) + flags(1) + payload_length(2) = 4 /// msg_type(1) + flags(1) + counter(8) + payload_length(2) = 12
/// (Addressing and hop_limit are in the SessionDatagram envelope.) /// (Addressing and hop_limit are in the SessionDatagram envelope.)
pub const DATA_HEADER_SIZE: usize = 4; pub const DATA_HEADER_SIZE: usize = 12;
/// Encrypted application data carried inside a SessionDatagram. /// Encrypted application data carried inside a SessionDatagram.
/// ///
/// ## Wire Format (minimal, no coordinates) /// ## Wire Format (minimal, no coordinates)
/// ///
/// | Offset | Field | Size | Description | /// | Offset | Field | Size | Description |
/// |--------|----------------|---------|----------------------------| /// |--------|----------------|---------|-------------------------------|
/// | 0 | msg_type | 1 byte | 0x10 | /// | 0 | msg_type | 1 byte | 0x10 |
/// | 1 | flags | 1 byte | Bit 0: COORDS_PRESENT | /// | 1 | flags | 1 byte | Bit 0: COORDS_PRESENT |
/// | 2 | payload_length | 2 bytes | u16 LE | /// | 2 | counter | 8 bytes | u64 LE, encryption nonce |
/// | 4 | payload | variable| Encrypted application data | /// | 10 | payload_length | 2 bytes | u16 LE |
/// | 12 | payload | variable| Encrypted application data |
/// ///
/// ## Wire Format (with coordinates, when COORDS_PRESENT is set) /// ## Wire Format (with coordinates, when COORDS_PRESENT is set)
/// ///
@@ -503,9 +504,10 @@ pub const DATA_HEADER_SIZE: usize = 4;
/// |--------|------------------|---------|----------------------------| /// |--------|------------------|---------|----------------------------|
/// | 0 | msg_type | 1 byte | 0x10 | /// | 0 | msg_type | 1 byte | 0x10 |
/// | 1 | flags | 1 byte | 0x01 (COORDS_PRESENT) | /// | 1 | flags | 1 byte | 0x01 (COORDS_PRESENT) |
/// | 2 | payload_length | 2 bytes | u16 LE | /// | 2 | counter | 8 bytes | u64 LE, encryption nonce |
/// | 4 | src_coords_count | 2 bytes | u16 LE | /// | 10 | payload_length | 2 bytes | u16 LE |
/// | 6 | src_coords | 16 × n | Source coordinates | /// | 12 | src_coords_count | 2 bytes | u16 LE |
/// | 14 | src_coords | 16 × n | Source coordinates |
/// | ... | dest_coords_count| 2 bytes | u16 LE | /// | ... | dest_coords_count| 2 bytes | u16 LE |
/// | ... | dest_coords | 16 × m | Destination coordinates | /// | ... | dest_coords | 16 × m | Destination coordinates |
/// | ... | payload | variable| Encrypted application data | /// | ... | payload | variable| Encrypted application data |
@@ -513,6 +515,9 @@ pub const DATA_HEADER_SIZE: usize = 4;
pub struct DataPacket { pub struct DataPacket {
/// Packet flags. /// Packet flags.
pub flags: DataFlags, pub flags: DataFlags,
/// Encryption counter (used as nonce for ChaCha20Poly1305).
/// Transmitted on the wire so the receiver can decrypt out-of-order packets.
pub counter: u64,
/// Payload data (end-to-end encrypted application data). /// Payload data (end-to-end encrypted application data).
pub payload: Vec<u8>, pub payload: Vec<u8>,
/// Source coordinates (present when COORDS_PRESENT flag is set). /// Source coordinates (present when COORDS_PRESENT flag is set).
@@ -522,10 +527,11 @@ pub struct DataPacket {
} }
impl DataPacket { impl DataPacket {
/// Create a new data packet. /// Create a new data packet with the given counter and payload.
pub fn new(payload: Vec<u8>) -> Self { pub fn new(counter: u64, payload: Vec<u8>) -> Self {
Self { Self {
flags: DataFlags::new(), flags: DataFlags::new(),
counter,
payload, payload,
src_coords: None, src_coords: None,
dest_coords: None, dest_coords: None,
@@ -576,6 +582,7 @@ impl DataPacket {
let mut buf = Vec::new(); let mut buf = Vec::new();
buf.push(SessionMessageType::DataPacket.to_byte()); buf.push(SessionMessageType::DataPacket.to_byte());
buf.push(self.flags.to_byte()); buf.push(self.flags.to_byte());
buf.extend_from_slice(&self.counter.to_le_bytes());
let payload_len = self.payload.len() as u16; let payload_len = self.payload.len() as u16;
buf.extend_from_slice(&payload_len.to_le_bytes()); buf.extend_from_slice(&payload_len.to_le_bytes());
if self.flags.coords_present { if self.flags.coords_present {
@@ -596,15 +603,20 @@ impl DataPacket {
/// Decode from wire format (after msg_type byte has been consumed). /// Decode from wire format (after msg_type byte has been consumed).
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> { pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
if payload.len() < 3 { // flags(1) + counter(8) + payload_len(2) = 11
if payload.len() < 11 {
return Err(ProtocolError::MessageTooShort { return Err(ProtocolError::MessageTooShort {
expected: 3, expected: 11,
got: payload.len(), got: payload.len(),
}); });
} }
let flags = DataFlags::from_byte(payload[0]); let flags = DataFlags::from_byte(payload[0]);
let payload_len = u16::from_le_bytes([payload[1], payload[2]]) as usize; let counter = u64::from_le_bytes([
let mut offset = 3; payload[1], payload[2], payload[3], payload[4],
payload[5], payload[6], payload[7], payload[8],
]);
let payload_len = u16::from_le_bytes([payload[9], payload[10]]) as usize;
let mut offset = 11;
let (src_coords, dest_coords) = if flags.coords_present { let (src_coords, dest_coords) = if flags.coords_present {
let (src, consumed) = decode_optional_coords(&payload[offset..])?; let (src, consumed) = decode_optional_coords(&payload[offset..])?;
@@ -626,6 +638,7 @@ impl DataPacket {
Ok(Self { Ok(Self {
flags, flags,
counter,
payload: data, payload: data,
src_coords, src_coords,
dest_coords, dest_coords,
@@ -854,17 +867,17 @@ mod tests {
#[test] #[test]
fn test_data_packet_size() { fn test_data_packet_size() {
let packet = DataPacket::new(vec![0u8; 100]); let packet = DataPacket::new(0, vec![0u8; 100]);
// 4 byte header + 100 byte payload // 12 byte header + 100 byte payload
assert_eq!(packet.total_size(), 104); assert_eq!(packet.total_size(), 112);
assert_eq!(packet.header_size(), 4); assert_eq!(packet.header_size(), 12);
assert_eq!(packet.payload_len(), 100); assert_eq!(packet.payload_len(), 100);
} }
#[test] #[test]
fn test_data_packet_builder() { fn test_data_packet_builder() {
let packet = DataPacket::new(vec![1, 2, 3]) let packet = DataPacket::new(0, vec![1, 2, 3])
.with_flags(DataFlags::from_byte(0x80)); .with_flags(DataFlags::from_byte(0x80));
assert_eq!(packet.flags.to_byte(), 0x80); assert_eq!(packet.flags.to_byte(), 0x80);
@@ -983,7 +996,7 @@ mod tests {
#[test] #[test]
fn test_data_packet_encode_decode_minimal() { fn test_data_packet_encode_decode_minimal() {
let data = vec![1, 2, 3, 4, 5]; let data = vec![1, 2, 3, 4, 5];
let packet = DataPacket::new(data.clone()); let packet = DataPacket::new(42, data.clone());
let encoded = packet.encode(); let encoded = packet.encode();
assert_eq!(encoded[0], 0x10); // msg_type assert_eq!(encoded[0], 0x10); // msg_type
@@ -991,6 +1004,7 @@ mod tests {
let decoded = DataPacket::decode(&encoded[1..]).unwrap(); let decoded = DataPacket::decode(&encoded[1..]).unwrap();
assert_eq!(decoded.payload, data); assert_eq!(decoded.payload, data);
assert_eq!(decoded.counter, 42);
assert!(!decoded.flags.coords_present); assert!(!decoded.flags.coords_present);
assert!(decoded.src_coords.is_none()); assert!(decoded.src_coords.is_none());
assert!(decoded.dest_coords.is_none()); assert!(decoded.dest_coords.is_none());
@@ -1001,7 +1015,7 @@ mod tests {
let data = vec![0xFF; 100]; let data = vec![0xFF; 100];
let src = make_coords(&[1, 2, 0]); let src = make_coords(&[1, 2, 0]);
let dest = make_coords(&[3, 4, 0]); let dest = make_coords(&[3, 4, 0]);
let packet = DataPacket::new(data.clone()) let packet = DataPacket::new(1000, data.clone())
.with_coords(src.clone(), dest.clone()); .with_coords(src.clone(), dest.clone());
let encoded = packet.encode(); let encoded = packet.encode();
@@ -1010,6 +1024,7 @@ mod tests {
let decoded = DataPacket::decode(&encoded[1..]).unwrap(); let decoded = DataPacket::decode(&encoded[1..]).unwrap();
assert_eq!(decoded.payload, data); assert_eq!(decoded.payload, data);
assert_eq!(decoded.counter, 1000);
assert!(decoded.flags.coords_present); assert!(decoded.flags.coords_present);
assert_eq!(decoded.src_coords.unwrap(), src); assert_eq!(decoded.src_coords.unwrap(), src);
assert_eq!(decoded.dest_coords.unwrap(), dest); assert_eq!(decoded.dest_coords.unwrap(), dest);
@@ -1086,7 +1101,7 @@ mod tests {
#[test] #[test]
fn test_data_packet_large_payload() { fn test_data_packet_large_payload() {
let data = vec![0x42; 65000]; let data = vec![0x42; 65000];
let packet = DataPacket::new(data.clone()); let packet = DataPacket::new(0, data.clone());
let encoded = packet.encode(); let encoded = packet.encode();
let decoded = DataPacket::decode(&encoded[1..]).unwrap(); let decoded = DataPacket::decode(&encoded[1..]).unwrap();
+2 -2
View File
@@ -65,9 +65,9 @@ const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADE
/// - Noise encryption tag: 16 bytes /// - Noise encryption tag: 16 bytes
/// - Encrypted frame header: 13 bytes /// - Encrypted frame header: 13 bytes
/// - SessionDatagram header: 34 bytes /// - SessionDatagram header: 34 bytes
/// - DataPacket header: 4 bytes /// - DataPacket header: 12 bytes (msg_type + flags + counter + payload_len)
/// - Coordinates (worst case): ~60 bytes (2 coords with depth 3 each) /// - Coordinates (worst case): ~60 bytes (2 coords with depth 3 each)
pub const FIPS_OVERHEAD: u16 = 16 + 13 + 34 + 4 + 60; // 127 bytes pub const FIPS_OVERHEAD: u16 = 16 + 13 + 34 + 12 + 60; // 135 bytes
/// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic. /// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic.
/// ///