mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
Implement the coordinate discovery protocol with flood-based lookup and reverse-path response routing. Wire format: LookupRequest (0x30) encode/decode with TTL, visited bloom filter, origin coords. LookupResponse (0x31) encode/decode with target coords and Schnorr proof signature. Handler logic: request dedup by request_id, visited filter loop prevention, TTL enforcement, lazy purge of expired entries (10s). Response routing: originator caches route in route_cache, transit nodes reverse-path forward via recent_requests. Node state: RecentRequest struct, route_cache (RouteCache), and recent_requests map for dedup + reverse-path forwarding. 13 handler tests (9 unit + 4 integration) plus 4 protocol tests. 392 tests pass, clean build.
1113 lines
37 KiB
Rust
1113 lines
37 KiB
Rust
//! Session-layer message types: setup, ack, data, and error messages.
|
||
|
||
use super::ProtocolError;
|
||
use crate::tree::TreeCoordinate;
|
||
use crate::NodeAddr;
|
||
use std::fmt;
|
||
|
||
// ============================================================================
|
||
// Session Layer Message Types
|
||
// ============================================================================
|
||
|
||
/// SessionDatagram payload message type identifiers.
|
||
///
|
||
/// These messages are carried as payloads inside `SessionDatagram` (link
|
||
/// message type 0x40). Session-layer messages (SessionSetup, SessionAck,
|
||
/// DataPacket) are end-to-end encrypted with session keys. Error signals
|
||
/// (CoordsRequired, PathBroken) are plaintext link-layer messages generated
|
||
/// by transit routers that cannot establish e2e sessions with the source.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
#[repr(u8)]
|
||
pub enum SessionMessageType {
|
||
// Session establishment (0x00-0x0F)
|
||
/// Session setup with coordinates (warms router caches).
|
||
SessionSetup = 0x00,
|
||
/// Session acknowledgement.
|
||
SessionAck = 0x01,
|
||
|
||
// Data (0x10-0x1F)
|
||
/// Encrypted IPv6 datagram payload.
|
||
DataPacket = 0x10,
|
||
|
||
// Link-layer error signals (0x20-0x2F) — plaintext, from transit routers
|
||
/// Router cache miss — needs coordinates (link-layer error signal).
|
||
CoordsRequired = 0x20,
|
||
/// Routing failure — local minimum or unreachable (link-layer error signal).
|
||
PathBroken = 0x21,
|
||
}
|
||
|
||
impl SessionMessageType {
|
||
/// Try to convert from a byte.
|
||
pub fn from_byte(b: u8) -> Option<Self> {
|
||
match b {
|
||
0x00 => Some(SessionMessageType::SessionSetup),
|
||
0x01 => Some(SessionMessageType::SessionAck),
|
||
0x10 => Some(SessionMessageType::DataPacket),
|
||
0x20 => Some(SessionMessageType::CoordsRequired),
|
||
0x21 => Some(SessionMessageType::PathBroken),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Convert to a byte.
|
||
pub fn to_byte(self) -> u8 {
|
||
self as u8
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for SessionMessageType {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
let name = match self {
|
||
SessionMessageType::SessionSetup => "SessionSetup",
|
||
SessionMessageType::SessionAck => "SessionAck",
|
||
SessionMessageType::DataPacket => "DataPacket",
|
||
SessionMessageType::CoordsRequired => "CoordsRequired",
|
||
SessionMessageType::PathBroken => "PathBroken",
|
||
};
|
||
write!(f, "{}", name)
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Coordinate Wire Format Helpers
|
||
// ============================================================================
|
||
|
||
/// Encode a TreeCoordinate as address-only wire format: count(u16 LE) + addrs(16 × n).
|
||
///
|
||
/// Session-layer messages serialize coordinates as NodeAddr arrays (16 bytes each),
|
||
/// without the sequence/timestamp metadata used by the tree gossip protocol.
|
||
pub(crate) fn encode_coords(coords: &TreeCoordinate, buf: &mut Vec<u8>) {
|
||
let addrs: Vec<&NodeAddr> = coords.node_addrs().collect();
|
||
let count = addrs.len() as u16;
|
||
buf.extend_from_slice(&count.to_le_bytes());
|
||
for addr in addrs {
|
||
buf.extend_from_slice(addr.as_bytes());
|
||
}
|
||
}
|
||
|
||
/// Decode a TreeCoordinate from address-only wire format.
|
||
///
|
||
/// Returns the decoded coordinate and the number of bytes consumed.
|
||
pub(crate) fn decode_coords(data: &[u8]) -> Result<(TreeCoordinate, usize), ProtocolError> {
|
||
if data.len() < 2 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 2,
|
||
got: data.len(),
|
||
});
|
||
}
|
||
let count = u16::from_le_bytes([data[0], data[1]]) as usize;
|
||
let needed = 2 + count * 16;
|
||
if data.len() < needed {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: needed,
|
||
got: data.len(),
|
||
});
|
||
}
|
||
if count == 0 {
|
||
return Err(ProtocolError::Malformed(
|
||
"coordinate with zero entries".into(),
|
||
));
|
||
}
|
||
let mut addrs = Vec::with_capacity(count);
|
||
for i in 0..count {
|
||
let offset = 2 + i * 16;
|
||
let mut bytes = [0u8; 16];
|
||
bytes.copy_from_slice(&data[offset..offset + 16]);
|
||
addrs.push(NodeAddr::from_bytes(bytes));
|
||
}
|
||
let coord = TreeCoordinate::from_addrs(addrs)
|
||
.map_err(|e| ProtocolError::Malformed(e.to_string()))?;
|
||
Ok((coord, needed))
|
||
}
|
||
|
||
/// Decode an optional coordinate field (count may be 0).
|
||
///
|
||
/// Returns None if count is 0, Some(coord) otherwise, plus bytes consumed.
|
||
fn decode_optional_coords(data: &[u8]) -> Result<(Option<TreeCoordinate>, usize), ProtocolError> {
|
||
if data.len() < 2 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 2,
|
||
got: data.len(),
|
||
});
|
||
}
|
||
let count = u16::from_le_bytes([data[0], data[1]]) as usize;
|
||
let needed = 2 + count * 16;
|
||
if data.len() < needed {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: needed,
|
||
got: data.len(),
|
||
});
|
||
}
|
||
if count == 0 {
|
||
return Ok((None, 2));
|
||
}
|
||
let mut addrs = Vec::with_capacity(count);
|
||
for i in 0..count {
|
||
let offset = 2 + i * 16;
|
||
let mut bytes = [0u8; 16];
|
||
bytes.copy_from_slice(&data[offset..offset + 16]);
|
||
addrs.push(NodeAddr::from_bytes(bytes));
|
||
}
|
||
let coord = TreeCoordinate::from_addrs(addrs)
|
||
.map_err(|e| ProtocolError::Malformed(e.to_string()))?;
|
||
Ok((Some(coord), needed))
|
||
}
|
||
|
||
/// Encode a count of zero (for empty/absent coordinate fields).
|
||
fn encode_empty_coords(buf: &mut Vec<u8>) {
|
||
buf.extend_from_slice(&0u16.to_le_bytes());
|
||
}
|
||
|
||
// ============================================================================
|
||
// Session Flags
|
||
// ============================================================================
|
||
|
||
/// Session flags for setup options.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub struct SessionFlags {
|
||
/// Request acknowledgement from destination.
|
||
pub request_ack: bool,
|
||
/// Set up bidirectional session.
|
||
pub bidirectional: bool,
|
||
}
|
||
|
||
impl SessionFlags {
|
||
/// Create default flags.
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Set request_ack flag.
|
||
pub fn with_ack(mut self) -> Self {
|
||
self.request_ack = true;
|
||
self
|
||
}
|
||
|
||
/// Set bidirectional flag.
|
||
pub fn bidirectional(mut self) -> Self {
|
||
self.bidirectional = true;
|
||
self
|
||
}
|
||
|
||
/// Convert to a byte.
|
||
pub fn to_byte(&self) -> u8 {
|
||
let mut flags = 0u8;
|
||
if self.request_ack {
|
||
flags |= 0x01;
|
||
}
|
||
if self.bidirectional {
|
||
flags |= 0x02;
|
||
}
|
||
flags
|
||
}
|
||
|
||
/// Convert from a byte.
|
||
pub fn from_byte(byte: u8) -> Self {
|
||
Self {
|
||
request_ack: byte & 0x01 != 0,
|
||
bidirectional: byte & 0x02 != 0,
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Session Setup
|
||
// ============================================================================
|
||
|
||
/// Session setup to establish cached coordinate state.
|
||
///
|
||
/// Carried inside a SessionDatagram envelope which provides src_addr and
|
||
/// dest_addr. The SessionSetup payload contains coordinates, session flags,
|
||
/// and the Noise IK handshake message for session establishment.
|
||
///
|
||
/// ## Wire Format
|
||
///
|
||
/// | Offset | Field | Size | Description |
|
||
/// |--------|------------------|---------|-------------------------------------|
|
||
/// | 0 | msg_type | 1 byte | 0x00 |
|
||
/// | 1 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIR |
|
||
/// | 2 | src_coords_count | 2 bytes | u16 LE, number of src coord entries |
|
||
/// | 4 | src_coords | 16 × n | NodeAddr array (self → root) |
|
||
/// | ... | dest_coords_count| 2 bytes | u16 LE, number of dest coord entries|
|
||
/// | ... | dest_coords | 16 × m | NodeAddr array (dest → root) |
|
||
/// | ... | handshake_len | 2 bytes | u16 LE, Noise payload length |
|
||
/// | ... | handshake_payload| variable| Noise IK msg1 (82 bytes typical) |
|
||
#[derive(Clone, Debug)]
|
||
pub struct SessionSetup {
|
||
/// Source coordinates (for return path caching).
|
||
pub src_coords: TreeCoordinate,
|
||
/// Destination coordinates (for forward routing).
|
||
pub dest_coords: TreeCoordinate,
|
||
/// Session options.
|
||
pub flags: SessionFlags,
|
||
/// Noise IK handshake message 1.
|
||
pub handshake_payload: Vec<u8>,
|
||
}
|
||
|
||
impl SessionSetup {
|
||
/// Create a new session setup message.
|
||
pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
|
||
Self {
|
||
src_coords,
|
||
dest_coords,
|
||
flags: SessionFlags::new(),
|
||
handshake_payload: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Set session flags.
|
||
pub fn with_flags(mut self, flags: SessionFlags) -> Self {
|
||
self.flags = flags;
|
||
self
|
||
}
|
||
|
||
/// Set the Noise handshake payload.
|
||
pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
|
||
self.handshake_payload = payload;
|
||
self
|
||
}
|
||
|
||
/// Encode as wire format (msg_type + flags + coords + handshake).
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut buf = Vec::new();
|
||
buf.push(SessionMessageType::SessionSetup.to_byte());
|
||
buf.push(self.flags.to_byte());
|
||
encode_coords(&self.src_coords, &mut buf);
|
||
encode_coords(&self.dest_coords, &mut buf);
|
||
let hs_len = self.handshake_payload.len() as u16;
|
||
buf.extend_from_slice(&hs_len.to_le_bytes());
|
||
buf.extend_from_slice(&self.handshake_payload);
|
||
buf
|
||
}
|
||
|
||
/// Decode from wire format (after msg_type byte has been consumed).
|
||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||
if payload.is_empty() {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 1,
|
||
got: 0,
|
||
});
|
||
}
|
||
let flags = SessionFlags::from_byte(payload[0]);
|
||
let mut offset = 1;
|
||
|
||
let (src_coords, consumed) = decode_coords(&payload[offset..])?;
|
||
offset += consumed;
|
||
|
||
let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
|
||
offset += consumed;
|
||
|
||
if payload.len() < offset + 2 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: offset + 2,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
|
||
offset += 2;
|
||
|
||
if payload.len() < offset + hs_len {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: offset + hs_len,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let handshake_payload = payload[offset..offset + hs_len].to_vec();
|
||
|
||
Ok(Self {
|
||
src_coords,
|
||
dest_coords,
|
||
flags,
|
||
handshake_payload,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Session Ack
|
||
// ============================================================================
|
||
|
||
/// Session acknowledgement.
|
||
///
|
||
/// Carried inside a SessionDatagram envelope which provides src_addr and
|
||
/// dest_addr. The SessionAck payload contains the acknowledger's coordinates
|
||
/// for route cache warming and the Noise IK handshake response.
|
||
///
|
||
/// ## Wire Format
|
||
///
|
||
/// | Offset | Field | Size | Description |
|
||
/// |--------|------------------|---------|-------------------------------------|
|
||
/// | 0 | msg_type | 1 byte | 0x01 |
|
||
/// | 1 | flags | 1 byte | Reserved |
|
||
/// | 2 | src_coords_count | 2 bytes | u16 LE |
|
||
/// | 4 | src_coords | 16 × n | Acknowledger's coords (for caching) |
|
||
/// | ... | handshake_len | 2 bytes | u16 LE, Noise payload length |
|
||
/// | ... | handshake_payload| variable| Noise IK msg2 (33 bytes typical) |
|
||
#[derive(Clone, Debug)]
|
||
pub struct SessionAck {
|
||
/// Acknowledger's coordinates.
|
||
pub src_coords: TreeCoordinate,
|
||
/// Reserved flags byte (for forward compatibility).
|
||
pub flags: u8,
|
||
/// Noise IK handshake message 2.
|
||
pub handshake_payload: Vec<u8>,
|
||
}
|
||
|
||
impl SessionAck {
|
||
/// Create a new session acknowledgement.
|
||
pub fn new(src_coords: TreeCoordinate) -> Self {
|
||
Self {
|
||
src_coords,
|
||
flags: 0,
|
||
handshake_payload: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Set the Noise handshake payload.
|
||
pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
|
||
self.handshake_payload = payload;
|
||
self
|
||
}
|
||
|
||
/// Encode as wire format (msg_type + flags + coords + handshake).
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut buf = Vec::new();
|
||
buf.push(SessionMessageType::SessionAck.to_byte());
|
||
buf.push(self.flags);
|
||
encode_coords(&self.src_coords, &mut buf);
|
||
let hs_len = self.handshake_payload.len() as u16;
|
||
buf.extend_from_slice(&hs_len.to_le_bytes());
|
||
buf.extend_from_slice(&self.handshake_payload);
|
||
buf
|
||
}
|
||
|
||
/// Decode from wire format (after msg_type byte has been consumed).
|
||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||
if payload.is_empty() {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 1,
|
||
got: 0,
|
||
});
|
||
}
|
||
let flags = payload[0];
|
||
let mut offset = 1;
|
||
|
||
let (src_coords, consumed) = decode_coords(&payload[offset..])?;
|
||
offset += consumed;
|
||
|
||
if payload.len() < offset + 2 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: offset + 2,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
|
||
offset += 2;
|
||
|
||
if payload.len() < offset + hs_len {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: offset + hs_len,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let handshake_payload = payload[offset..offset + hs_len].to_vec();
|
||
|
||
Ok(Self {
|
||
src_coords,
|
||
flags,
|
||
handshake_payload,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Data Messages
|
||
// ============================================================================
|
||
|
||
/// Data packet flags.
|
||
///
|
||
/// ## Flag Bits
|
||
///
|
||
/// | Bit | Name | Description |
|
||
/// |-----|----------------|------------------------------------------|
|
||
/// | 0 | COORDS_PRESENT | Coordinates follow the fixed header |
|
||
/// | 1-7 | reserved | Reserved for future use |
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub struct DataFlags {
|
||
/// When set, source and destination coordinates follow the header.
|
||
/// Used to warm router caches after receiving CoordsRequired.
|
||
pub coords_present: bool,
|
||
/// Reserved bits (preserved for forward compatibility).
|
||
reserved: u8,
|
||
}
|
||
|
||
/// Bit 0: coordinates follow the header.
|
||
pub const DATA_FLAG_COORDS_PRESENT: u8 = 0x01;
|
||
|
||
impl DataFlags {
|
||
/// Create default flags (no coordinates).
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Create flags with COORDS_PRESENT set.
|
||
pub fn with_coords() -> Self {
|
||
Self {
|
||
coords_present: true,
|
||
reserved: 0,
|
||
}
|
||
}
|
||
|
||
/// Set the coords_present flag.
|
||
pub fn set_coords_present(&mut self, value: bool) {
|
||
self.coords_present = value;
|
||
}
|
||
|
||
/// Convert to a byte.
|
||
pub fn to_byte(&self) -> u8 {
|
||
let mut flags = self.reserved & !DATA_FLAG_COORDS_PRESENT;
|
||
if self.coords_present {
|
||
flags |= DATA_FLAG_COORDS_PRESENT;
|
||
}
|
||
flags
|
||
}
|
||
|
||
/// Convert from a byte.
|
||
pub fn from_byte(byte: u8) -> Self {
|
||
Self {
|
||
coords_present: byte & DATA_FLAG_COORDS_PRESENT != 0,
|
||
reserved: byte & !DATA_FLAG_COORDS_PRESENT,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// DataPacket header size in bytes (excluding payload).
|
||
/// msg_type(1) + flags(1) + payload_length(2) = 4
|
||
/// (Addressing and hop_limit are in the SessionDatagram envelope.)
|
||
pub const DATA_HEADER_SIZE: usize = 4;
|
||
|
||
/// Encrypted application data carried inside a SessionDatagram.
|
||
///
|
||
/// ## Wire Format (minimal, no coordinates)
|
||
///
|
||
/// | Offset | Field | Size | Description |
|
||
/// |--------|----------------|---------|----------------------------|
|
||
/// | 0 | msg_type | 1 byte | 0x10 |
|
||
/// | 1 | flags | 1 byte | Bit 0: COORDS_PRESENT |
|
||
/// | 2 | payload_length | 2 bytes | u16 LE |
|
||
/// | 4 | payload | variable| Encrypted application data |
|
||
///
|
||
/// ## Wire Format (with coordinates, when COORDS_PRESENT is set)
|
||
///
|
||
/// | Offset | Field | Size | Description |
|
||
/// |--------|------------------|---------|----------------------------|
|
||
/// | 0 | msg_type | 1 byte | 0x10 |
|
||
/// | 1 | flags | 1 byte | 0x01 (COORDS_PRESENT) |
|
||
/// | 2 | payload_length | 2 bytes | u16 LE |
|
||
/// | 4 | src_coords_count | 2 bytes | u16 LE |
|
||
/// | 6 | src_coords | 16 × n | Source coordinates |
|
||
/// | ... | dest_coords_count| 2 bytes | u16 LE |
|
||
/// | ... | dest_coords | 16 × m | Destination coordinates |
|
||
/// | ... | payload | variable| Encrypted application data |
|
||
#[derive(Clone, Debug)]
|
||
pub struct DataPacket {
|
||
/// Packet flags.
|
||
pub flags: DataFlags,
|
||
/// Payload data (end-to-end encrypted application data).
|
||
pub payload: Vec<u8>,
|
||
/// Source coordinates (present when COORDS_PRESENT flag is set).
|
||
pub src_coords: Option<TreeCoordinate>,
|
||
/// Destination coordinates (present when COORDS_PRESENT flag is set).
|
||
pub dest_coords: Option<TreeCoordinate>,
|
||
}
|
||
|
||
impl DataPacket {
|
||
/// Create a new data packet.
|
||
pub fn new(payload: Vec<u8>) -> Self {
|
||
Self {
|
||
flags: DataFlags::new(),
|
||
payload,
|
||
src_coords: None,
|
||
dest_coords: None,
|
||
}
|
||
}
|
||
|
||
/// Set the flags.
|
||
pub fn with_flags(mut self, flags: DataFlags) -> Self {
|
||
self.flags = flags;
|
||
self
|
||
}
|
||
|
||
/// Set coordinates for route cache warming.
|
||
pub fn with_coords(mut self, src: TreeCoordinate, dest: TreeCoordinate) -> Self {
|
||
self.src_coords = Some(src);
|
||
self.dest_coords = Some(dest);
|
||
self.flags.coords_present = true;
|
||
self
|
||
}
|
||
|
||
/// Get the payload length.
|
||
pub fn payload_len(&self) -> usize {
|
||
self.payload.len()
|
||
}
|
||
|
||
/// Total packet size (header + optional coords + payload).
|
||
pub fn total_size(&self) -> usize {
|
||
DATA_HEADER_SIZE + self.coords_wire_size() + self.payload.len()
|
||
}
|
||
|
||
/// Header size in bytes.
|
||
pub fn header_size(&self) -> usize {
|
||
DATA_HEADER_SIZE
|
||
}
|
||
|
||
/// Wire size of the optional coordinate fields.
|
||
fn coords_wire_size(&self) -> usize {
|
||
if !self.flags.coords_present {
|
||
return 0;
|
||
}
|
||
let src_count = self.src_coords.as_ref().map_or(0, |c| c.depth() + 1);
|
||
let dest_count = self.dest_coords.as_ref().map_or(0, |c| c.depth() + 1);
|
||
2 + src_count * 16 + 2 + dest_count * 16
|
||
}
|
||
|
||
/// Encode as wire format.
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut buf = Vec::new();
|
||
buf.push(SessionMessageType::DataPacket.to_byte());
|
||
buf.push(self.flags.to_byte());
|
||
let payload_len = self.payload.len() as u16;
|
||
buf.extend_from_slice(&payload_len.to_le_bytes());
|
||
if self.flags.coords_present {
|
||
if let Some(ref src) = self.src_coords {
|
||
encode_coords(src, &mut buf);
|
||
} else {
|
||
encode_empty_coords(&mut buf);
|
||
}
|
||
if let Some(ref dest) = self.dest_coords {
|
||
encode_coords(dest, &mut buf);
|
||
} else {
|
||
encode_empty_coords(&mut buf);
|
||
}
|
||
}
|
||
buf.extend_from_slice(&self.payload);
|
||
buf
|
||
}
|
||
|
||
/// Decode from wire format (after msg_type byte has been consumed).
|
||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||
if payload.len() < 3 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 3,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let flags = DataFlags::from_byte(payload[0]);
|
||
let payload_len = u16::from_le_bytes([payload[1], payload[2]]) as usize;
|
||
let mut offset = 3;
|
||
|
||
let (src_coords, dest_coords) = if flags.coords_present {
|
||
let (src, consumed) = decode_optional_coords(&payload[offset..])?;
|
||
offset += consumed;
|
||
let (dest, consumed) = decode_optional_coords(&payload[offset..])?;
|
||
offset += consumed;
|
||
(src, dest)
|
||
} else {
|
||
(None, None)
|
||
};
|
||
|
||
if payload.len() < offset + payload_len {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: offset + payload_len,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let data = payload[offset..offset + payload_len].to_vec();
|
||
|
||
Ok(Self {
|
||
flags,
|
||
payload: data,
|
||
src_coords,
|
||
dest_coords,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Error Messages
|
||
// ============================================================================
|
||
|
||
/// Link-layer error signal indicating router cache miss.
|
||
///
|
||
/// Generated by a transit router when it cannot forward a SessionDatagram
|
||
/// due to missing cached coordinates for the destination. Carried inside
|
||
/// a new SessionDatagram addressed back to the original source
|
||
/// (src_addr=reporter, dest_addr=original_source). Plaintext — not
|
||
/// end-to-end encrypted, since the transit router has no session with
|
||
/// the source.
|
||
///
|
||
/// ## Wire Format
|
||
///
|
||
/// | Offset | Field | Size | Description |
|
||
/// |--------|----------|---------|------------------------------------|
|
||
/// | 0 | msg_type | 1 byte | 0x20 |
|
||
/// | 1 | flags | 1 byte | Reserved |
|
||
/// | 2 | dest_addr| 16 bytes| The node_addr we couldn't route to |
|
||
/// | 18 | reporter | 16 bytes| NodeAddr of reporting router |
|
||
///
|
||
/// Payload: 34 bytes
|
||
#[derive(Clone, Debug)]
|
||
pub struct CoordsRequired {
|
||
/// Destination that couldn't be routed.
|
||
pub dest_addr: NodeAddr,
|
||
/// Router reporting the miss.
|
||
pub reporter: NodeAddr,
|
||
}
|
||
|
||
/// Wire size of CoordsRequired payload: msg_type(1) + flags(1) + dest_addr(16) + reporter(16).
|
||
pub const COORDS_REQUIRED_SIZE: usize = 34;
|
||
|
||
impl CoordsRequired {
|
||
/// Create a new CoordsRequired error.
|
||
pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
|
||
Self { dest_addr, reporter }
|
||
}
|
||
|
||
/// Encode as wire format.
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut buf = Vec::with_capacity(COORDS_REQUIRED_SIZE);
|
||
buf.push(SessionMessageType::CoordsRequired.to_byte());
|
||
buf.push(0x00); // reserved flags
|
||
buf.extend_from_slice(self.dest_addr.as_bytes());
|
||
buf.extend_from_slice(self.reporter.as_bytes());
|
||
buf
|
||
}
|
||
|
||
/// Decode from wire format (after msg_type byte has been consumed).
|
||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||
// flags(1) + dest_addr(16) + reporter(16) = 33
|
||
if payload.len() < 33 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 33,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
// payload[0] is flags (reserved, ignored)
|
||
let mut dest_bytes = [0u8; 16];
|
||
dest_bytes.copy_from_slice(&payload[1..17]);
|
||
let mut reporter_bytes = [0u8; 16];
|
||
reporter_bytes.copy_from_slice(&payload[17..33]);
|
||
|
||
Ok(Self {
|
||
dest_addr: NodeAddr::from_bytes(dest_bytes),
|
||
reporter: NodeAddr::from_bytes(reporter_bytes),
|
||
})
|
||
}
|
||
}
|
||
|
||
/// Error indicating routing failure (local minimum or unreachable).
|
||
///
|
||
/// Carried inside a SessionDatagram addressed back to the original source.
|
||
/// The reporting router creates a new SessionDatagram with src_addr=reporter
|
||
/// and dest_addr=original_source, so the `original_src` field from the old
|
||
/// design is no longer needed — it's the SessionDatagram's dest_addr.
|
||
///
|
||
/// ## Wire Format
|
||
///
|
||
/// | Offset | Field | Size | Description |
|
||
/// |--------|-------------------|----------|-------------------------------|
|
||
/// | 0 | msg_type | 1 byte | 0x21 |
|
||
/// | 1 | flags | 1 byte | Reserved |
|
||
/// | 2 | dest_addr | 16 bytes | The unreachable node_addr |
|
||
/// | 18 | reporter | 16 bytes | NodeAddr of reporting router |
|
||
/// | 34 | last_coords_count | 2 bytes | u16 LE |
|
||
/// | 36 | last_known_coords | 16 × n | Stale coords that failed |
|
||
#[derive(Clone, Debug)]
|
||
pub struct PathBroken {
|
||
/// Destination that couldn't be reached.
|
||
pub dest_addr: NodeAddr,
|
||
/// Node that detected the failure.
|
||
pub reporter: NodeAddr,
|
||
/// Optional: last known coordinates of destination.
|
||
pub last_known_coords: Option<TreeCoordinate>,
|
||
}
|
||
|
||
impl PathBroken {
|
||
/// Create a new PathBroken error.
|
||
pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
|
||
Self {
|
||
dest_addr,
|
||
reporter,
|
||
last_known_coords: None,
|
||
}
|
||
}
|
||
|
||
/// Add last known coordinates.
|
||
pub fn with_last_coords(mut self, coords: TreeCoordinate) -> Self {
|
||
self.last_known_coords = Some(coords);
|
||
self
|
||
}
|
||
|
||
/// Encode as wire format.
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut buf = Vec::new();
|
||
buf.push(SessionMessageType::PathBroken.to_byte());
|
||
buf.push(0x00); // reserved flags
|
||
buf.extend_from_slice(self.dest_addr.as_bytes());
|
||
buf.extend_from_slice(self.reporter.as_bytes());
|
||
if let Some(ref coords) = self.last_known_coords {
|
||
encode_coords(coords, &mut buf);
|
||
} else {
|
||
encode_empty_coords(&mut buf);
|
||
}
|
||
buf
|
||
}
|
||
|
||
/// Decode from wire format (after msg_type byte has been consumed).
|
||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||
// flags(1) + dest_addr(16) + reporter(16) + coords_count(2) = 35 minimum
|
||
if payload.len() < 35 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 35,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
// payload[0] is flags (reserved, ignored)
|
||
let mut dest_bytes = [0u8; 16];
|
||
dest_bytes.copy_from_slice(&payload[1..17]);
|
||
let mut reporter_bytes = [0u8; 16];
|
||
reporter_bytes.copy_from_slice(&payload[17..33]);
|
||
|
||
let (last_known_coords, _consumed) = decode_optional_coords(&payload[33..])?;
|
||
|
||
Ok(Self {
|
||
dest_addr: NodeAddr::from_bytes(dest_bytes),
|
||
reporter: NodeAddr::from_bytes(reporter_bytes),
|
||
last_known_coords,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn make_node_addr(val: u8) -> NodeAddr {
|
||
let mut bytes = [0u8; 16];
|
||
bytes[0] = val;
|
||
NodeAddr::from_bytes(bytes)
|
||
}
|
||
|
||
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
||
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
||
}
|
||
|
||
// ===== SessionMessageType Tests =====
|
||
|
||
#[test]
|
||
fn test_session_message_type_roundtrip() {
|
||
let types = [
|
||
SessionMessageType::SessionSetup,
|
||
SessionMessageType::SessionAck,
|
||
SessionMessageType::DataPacket,
|
||
SessionMessageType::CoordsRequired,
|
||
SessionMessageType::PathBroken,
|
||
];
|
||
|
||
for ty in types {
|
||
let byte = ty.to_byte();
|
||
let restored = SessionMessageType::from_byte(byte);
|
||
assert_eq!(restored, Some(ty));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_message_type_invalid() {
|
||
assert!(SessionMessageType::from_byte(0xFF).is_none());
|
||
assert!(SessionMessageType::from_byte(0x99).is_none());
|
||
}
|
||
|
||
// ===== SessionFlags Tests =====
|
||
|
||
#[test]
|
||
fn test_session_flags() {
|
||
let flags = SessionFlags::new().with_ack().bidirectional();
|
||
|
||
assert!(flags.request_ack);
|
||
assert!(flags.bidirectional);
|
||
|
||
let byte = flags.to_byte();
|
||
let restored = SessionFlags::from_byte(byte);
|
||
|
||
assert_eq!(flags, restored);
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_flags_default() {
|
||
let flags = SessionFlags::new();
|
||
assert!(!flags.request_ack);
|
||
assert!(!flags.bidirectional);
|
||
assert_eq!(flags.to_byte(), 0);
|
||
}
|
||
|
||
// ===== DataPacket Tests =====
|
||
|
||
#[test]
|
||
fn test_data_packet_size() {
|
||
let packet = DataPacket::new(vec![0u8; 100]);
|
||
|
||
// 4 byte header + 100 byte payload
|
||
assert_eq!(packet.total_size(), 104);
|
||
assert_eq!(packet.header_size(), 4);
|
||
assert_eq!(packet.payload_len(), 100);
|
||
}
|
||
|
||
#[test]
|
||
fn test_data_packet_builder() {
|
||
let packet = DataPacket::new(vec![1, 2, 3])
|
||
.with_flags(DataFlags::from_byte(0x80));
|
||
|
||
assert_eq!(packet.flags.to_byte(), 0x80);
|
||
}
|
||
|
||
#[test]
|
||
fn test_data_flags_coords_present() {
|
||
// Default: no coords
|
||
let flags = DataFlags::new();
|
||
assert!(!flags.coords_present);
|
||
assert_eq!(flags.to_byte(), 0x00);
|
||
|
||
// With coords
|
||
let flags = DataFlags::with_coords();
|
||
assert!(flags.coords_present);
|
||
assert_eq!(flags.to_byte(), 0x01);
|
||
|
||
// Round-trip preserves flag
|
||
let flags = DataFlags::from_byte(0x01);
|
||
assert!(flags.coords_present);
|
||
assert_eq!(flags.to_byte(), 0x01);
|
||
|
||
// Reserved bits preserved
|
||
let flags = DataFlags::from_byte(0x81); // coords + reserved bit 7
|
||
assert!(flags.coords_present);
|
||
assert_eq!(flags.to_byte(), 0x81);
|
||
|
||
// Coords bit toggles independently
|
||
let flags = DataFlags::from_byte(0x80); // only reserved bit 7
|
||
assert!(!flags.coords_present);
|
||
assert_eq!(flags.to_byte(), 0x80);
|
||
}
|
||
|
||
// ===== SessionSetup Tests =====
|
||
|
||
#[test]
|
||
fn test_session_setup() {
|
||
let setup = SessionSetup::new(make_coords(&[1, 0]), make_coords(&[2, 0]))
|
||
.with_flags(SessionFlags::new().with_ack());
|
||
|
||
assert!(setup.flags.request_ack);
|
||
assert!(!setup.flags.bidirectional);
|
||
}
|
||
|
||
// ===== CoordsRequired Tests =====
|
||
|
||
#[test]
|
||
fn test_coords_required() {
|
||
let err = CoordsRequired::new(make_node_addr(1), make_node_addr(2));
|
||
|
||
assert_eq!(err.dest_addr, make_node_addr(1));
|
||
assert_eq!(err.reporter, make_node_addr(2));
|
||
}
|
||
|
||
// ===== PathBroken Tests =====
|
||
|
||
#[test]
|
||
fn test_path_broken() {
|
||
let err = PathBroken::new(make_node_addr(2), make_node_addr(3))
|
||
.with_last_coords(make_coords(&[2, 0]));
|
||
|
||
assert_eq!(err.dest_addr, make_node_addr(2));
|
||
assert_eq!(err.reporter, make_node_addr(3));
|
||
assert!(err.last_known_coords.is_some());
|
||
}
|
||
|
||
// ===== Encode/Decode Roundtrip Tests =====
|
||
|
||
#[test]
|
||
fn test_session_setup_encode_decode() {
|
||
let handshake = vec![0xAA; 82]; // typical Noise IK msg1
|
||
let setup = SessionSetup::new(make_coords(&[1, 2, 0]), make_coords(&[3, 4, 0]))
|
||
.with_flags(SessionFlags::new().with_ack().bidirectional())
|
||
.with_handshake(handshake.clone());
|
||
|
||
let encoded = setup.encode();
|
||
|
||
// Verify msg_type byte
|
||
assert_eq!(encoded[0], 0x00);
|
||
|
||
// Decode (skip msg_type byte)
|
||
let decoded = SessionSetup::decode(&encoded[1..]).unwrap();
|
||
|
||
assert_eq!(decoded.flags, setup.flags);
|
||
assert_eq!(decoded.src_coords, setup.src_coords);
|
||
assert_eq!(decoded.dest_coords, setup.dest_coords);
|
||
assert_eq!(decoded.handshake_payload, handshake);
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_setup_no_handshake() {
|
||
let setup = SessionSetup::new(make_coords(&[5, 0]), make_coords(&[6, 0]));
|
||
|
||
let encoded = setup.encode();
|
||
let decoded = SessionSetup::decode(&encoded[1..]).unwrap();
|
||
|
||
assert!(decoded.handshake_payload.is_empty());
|
||
assert_eq!(decoded.src_coords, setup.src_coords);
|
||
assert_eq!(decoded.dest_coords, setup.dest_coords);
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_ack_encode_decode() {
|
||
let handshake = vec![0xBB; 33]; // typical Noise IK msg2
|
||
let ack = SessionAck::new(make_coords(&[7, 8, 0]))
|
||
.with_handshake(handshake.clone());
|
||
|
||
let encoded = ack.encode();
|
||
assert_eq!(encoded[0], 0x01);
|
||
|
||
let decoded = SessionAck::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.src_coords, ack.src_coords);
|
||
assert_eq!(decoded.handshake_payload, handshake);
|
||
}
|
||
|
||
#[test]
|
||
fn test_data_packet_encode_decode_minimal() {
|
||
let data = vec![1, 2, 3, 4, 5];
|
||
let packet = DataPacket::new(data.clone());
|
||
|
||
let encoded = packet.encode();
|
||
assert_eq!(encoded[0], 0x10); // msg_type
|
||
assert_eq!(encoded[1], 0x00); // flags (no coords)
|
||
|
||
let decoded = DataPacket::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.payload, data);
|
||
assert!(!decoded.flags.coords_present);
|
||
assert!(decoded.src_coords.is_none());
|
||
assert!(decoded.dest_coords.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_data_packet_encode_decode_with_coords() {
|
||
let data = vec![0xFF; 100];
|
||
let src = make_coords(&[1, 2, 0]);
|
||
let dest = make_coords(&[3, 4, 0]);
|
||
let packet = DataPacket::new(data.clone())
|
||
.with_coords(src.clone(), dest.clone());
|
||
|
||
let encoded = packet.encode();
|
||
assert_eq!(encoded[0], 0x10);
|
||
assert_eq!(encoded[1], 0x01); // COORDS_PRESENT
|
||
|
||
let decoded = DataPacket::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.payload, data);
|
||
assert!(decoded.flags.coords_present);
|
||
assert_eq!(decoded.src_coords.unwrap(), src);
|
||
assert_eq!(decoded.dest_coords.unwrap(), dest);
|
||
}
|
||
|
||
#[test]
|
||
fn test_coords_required_encode_decode() {
|
||
let err = CoordsRequired::new(make_node_addr(0xAA), make_node_addr(0xBB));
|
||
|
||
let encoded = err.encode();
|
||
assert_eq!(encoded.len(), COORDS_REQUIRED_SIZE);
|
||
assert_eq!(encoded[0], 0x20);
|
||
|
||
let decoded = CoordsRequired::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.dest_addr, err.dest_addr);
|
||
assert_eq!(decoded.reporter, err.reporter);
|
||
}
|
||
|
||
#[test]
|
||
fn test_path_broken_encode_decode_no_coords() {
|
||
let err = PathBroken::new(make_node_addr(0xCC), make_node_addr(0xDD));
|
||
|
||
let encoded = err.encode();
|
||
assert_eq!(encoded[0], 0x21);
|
||
|
||
let decoded = PathBroken::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.dest_addr, err.dest_addr);
|
||
assert_eq!(decoded.reporter, err.reporter);
|
||
assert!(decoded.last_known_coords.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_path_broken_encode_decode_with_coords() {
|
||
let coords = make_coords(&[0xCC, 0xDD, 0xEE]);
|
||
let err = PathBroken::new(make_node_addr(0x11), make_node_addr(0x22))
|
||
.with_last_coords(coords.clone());
|
||
|
||
let encoded = err.encode();
|
||
let decoded = PathBroken::decode(&encoded[1..]).unwrap();
|
||
|
||
assert_eq!(decoded.dest_addr, err.dest_addr);
|
||
assert_eq!(decoded.reporter, err.reporter);
|
||
assert_eq!(decoded.last_known_coords.unwrap(), coords);
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_setup_decode_too_short() {
|
||
assert!(SessionSetup::decode(&[]).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_ack_decode_too_short() {
|
||
assert!(SessionAck::decode(&[]).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_data_packet_decode_too_short() {
|
||
assert!(DataPacket::decode(&[]).is_err());
|
||
assert!(DataPacket::decode(&[0x00]).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_coords_required_decode_too_short() {
|
||
assert!(CoordsRequired::decode(&[]).is_err());
|
||
assert!(CoordsRequired::decode(&[0x00; 10]).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_path_broken_decode_too_short() {
|
||
assert!(PathBroken::decode(&[]).is_err());
|
||
assert!(PathBroken::decode(&[0x00; 20]).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_data_packet_large_payload() {
|
||
let data = vec![0x42; 65000];
|
||
let packet = DataPacket::new(data.clone());
|
||
|
||
let encoded = packet.encode();
|
||
let decoded = DataPacket::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.payload.len(), 65000);
|
||
assert_eq!(decoded.payload, data);
|
||
}
|
||
|
||
#[test]
|
||
fn test_session_setup_deep_coords() {
|
||
// Depth-10 coordinate (11 entries: self + 10 ancestors)
|
||
let addrs: Vec<u8> = (0..11).collect();
|
||
let src = make_coords(&addrs);
|
||
let dest = make_coords(&[20, 21, 22, 23, 24]);
|
||
let setup = SessionSetup::new(src.clone(), dest.clone())
|
||
.with_handshake(vec![0x55; 82]);
|
||
|
||
let encoded = setup.encode();
|
||
let decoded = SessionSetup::decode(&encoded[1..]).unwrap();
|
||
|
||
assert_eq!(decoded.src_coords, src);
|
||
assert_eq!(decoded.dest_coords, dest);
|
||
}
|
||
}
|