mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
FLP wire format revision and MMP link-layer measurement protocol
## FLP Wire Format Revision Replace the 1-byte discriminator with a structured wire format: - 4-byte common prefix (ver+phase, flags, payload_len) and 16-byte established frame header with AEAD AAD binding - 5-byte encrypted inner header (4-byte session-relative timestamp + 1-byte message type) on all link messages - Phase-based packet dispatch replacing discriminator-based dispatch - SessionDatagram reassigned from type 0x40 to 0x00; add SenderReport (0x01) and ReceiverReport (0x02) message types for MMP - SessionDatagram: rename hop_limit to ttl, add path_mtu field (u16 LE) with min(datagram.path_mtu, transport.mtu()) at forwarding - Updated handshake packets (msg1: 87->90 bytes, msg2: 42->45 bytes) - FIPS_OVERHEAD updated from 135 to 144 bytes ## MMP Link-Layer Measurement Protocol Add the Metrics Measurement Protocol for link quality measurement between FIPS peers. Measures RTT, loss, jitter, throughput, OWD trend, and ETX from periodic sender/receiver reports exchanged over established links. Module layout: - mmp/algorithms.rs: JitterEstimator, SrttEstimator, DualEwma, OwdTrend, SpinBit, ETX computation - mmp/report.rs: SenderReport (48B) and ReceiverReport (68B) wire format - mmp/sender.rs: per-peer TX counters and interval tracking - mmp/receiver.rs: per-peer RX counters, jitter, loss, gap tracking - mmp/metrics.rs: derived metrics from report processing (SRTT, goodput_bps) - mmp/mod.rs: MmpMode (Full/Lightweight/Minimal), MmpConfig, MmpPeerState - node/handlers/mmp.rs: report dispatch, timer-driven generation, operator logging (periodic + teardown) Integration: per-frame TX/RX hooks in encrypted message handling, report dispatch from link message router, timer-driven generation from tick handler, and periodic operator logging with throughput formatting. Three operating modes: Full (sender + receiver reports, spin bit, CE echo), Lightweight (receiver reports only), Minimal (spin bit + CE echo only). ## Design Documentation Updated FLP sections across all design documents to match the implemented wire format, including revised overhead calculations and numeric values. 568 tests pass, clippy clean.
This commit is contained in:
@@ -17,6 +17,18 @@ impl Node {
|
||||
let payload = &plaintext[1..];
|
||||
|
||||
match msg_type {
|
||||
0x00 => {
|
||||
// SessionDatagram
|
||||
self.handle_session_datagram(from, payload).await;
|
||||
}
|
||||
0x01 => {
|
||||
// SenderReport
|
||||
self.handle_sender_report(from, payload);
|
||||
}
|
||||
0x02 => {
|
||||
// ReceiverReport
|
||||
self.handle_receiver_report(from, payload);
|
||||
}
|
||||
0x10 => {
|
||||
// TreeAnnounce
|
||||
self.handle_tree_announce(from, payload).await;
|
||||
@@ -33,10 +45,6 @@ impl Node {
|
||||
// LookupResponse
|
||||
self.handle_lookup_response(from, payload).await;
|
||||
}
|
||||
0x40 => {
|
||||
// SessionDatagram
|
||||
self.handle_session_datagram(from, payload).await;
|
||||
}
|
||||
0x50 => {
|
||||
// Disconnect
|
||||
self.handle_disconnect(from, payload);
|
||||
@@ -86,6 +94,11 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// MMP teardown log (before we drop the peer)
|
||||
if let Some(mmp) = peer.mmp() {
|
||||
Self::log_mmp_teardown(node_addr, mmp);
|
||||
}
|
||||
|
||||
let link_id = peer.link_id();
|
||||
|
||||
// Free session index
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Encrypted frame handling (hot path).
|
||||
|
||||
use crate::node::Node;
|
||||
use crate::node::wire::{EncryptedHeader, strip_inner_header, FLAG_CE, FLAG_SP};
|
||||
use crate::transport::ReceivedPacket;
|
||||
use crate::node::wire::EncryptedHeader;
|
||||
use std::time::Instant;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
impl Node {
|
||||
/// Handle an encrypted frame (discriminator 0x00).
|
||||
/// Handle an encrypted frame (phase 0x0).
|
||||
///
|
||||
/// This is the hot path for established sessions. We use O(1)
|
||||
/// index-based lookup to find the session, then decrypt.
|
||||
@@ -53,9 +54,13 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Decrypt with replay check (this is the expensive part)
|
||||
let ciphertext = &packet.data[header.ciphertext_offset..];
|
||||
let plaintext = match session.decrypt_with_replay_check(ciphertext, header.counter) {
|
||||
// Decrypt with replay check and AAD (this is the expensive part)
|
||||
let ciphertext = &packet.data[header.ciphertext_offset()..];
|
||||
let plaintext = match session.decrypt_with_replay_check_and_aad(
|
||||
ciphertext,
|
||||
header.counter,
|
||||
&header.header_bytes,
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
@@ -70,6 +75,37 @@ impl Node {
|
||||
|
||||
// === PACKET IS AUTHENTIC ===
|
||||
|
||||
// Strip inner header (4-byte timestamp + msg_type)
|
||||
let (timestamp, link_message) = match strip_inner_header(&plaintext) {
|
||||
Some(parts) => parts,
|
||||
None => {
|
||||
debug!(
|
||||
node_addr = %node_addr,
|
||||
len = plaintext.len(),
|
||||
"Decrypted payload too short for inner header"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// MMP per-frame processing: feed counter, timestamp, flags to receiver state
|
||||
let now = Instant::now();
|
||||
let ce_flag = header.flags & FLAG_CE != 0;
|
||||
let sp_flag = header.flags & FLAG_SP != 0;
|
||||
if let Some(mmp) = peer.mmp_mut() {
|
||||
mmp.receiver.record_recv(
|
||||
header.counter,
|
||||
timestamp,
|
||||
packet.data.len(),
|
||||
ce_flag,
|
||||
now,
|
||||
);
|
||||
// Spin bit: feed to spin state, get optional RTT sample
|
||||
if let Some(rtt) = mmp.spin_bit.rx_observe(sp_flag, header.counter, now) {
|
||||
mmp.metrics.srtt.update(rtt.as_micros() as i64);
|
||||
}
|
||||
}
|
||||
|
||||
// Update address for roaming support
|
||||
peer.set_current_addr(packet.transport_id, packet.remote_addr.clone());
|
||||
|
||||
@@ -77,7 +113,7 @@ impl Node {
|
||||
peer.link_stats_mut().record_recv(packet.data.len(), packet.timestamp_ms);
|
||||
peer.touch(packet.timestamp_ms);
|
||||
|
||||
// Dispatch to link message handler
|
||||
self.dispatch_link_message(&node_addr, &plaintext).await;
|
||||
// Dispatch to link message handler (msg_type + payload, inner header stripped)
|
||||
self.dispatch_link_message(&node_addr, link_message).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! SessionDatagram forwarding handler.
|
||||
//!
|
||||
//! Handles incoming SessionDatagram (0x40) link messages: decodes the
|
||||
//! Handles incoming SessionDatagram (0x00) link messages: decodes the
|
||||
//! envelope, enforces hop limits, performs coordinate cache warming from
|
||||
//! plaintext session-layer headers, routes to the next hop or delivers
|
||||
//! locally, and generates error signals on routing failure.
|
||||
@@ -16,7 +16,7 @@ use tracing::debug;
|
||||
impl Node {
|
||||
/// Handle an incoming SessionDatagram from a peer.
|
||||
///
|
||||
/// Called by `dispatch_link_message` for msg_type 0x40. The payload
|
||||
/// Called by `dispatch_link_message` for msg_type 0x00. The payload
|
||||
/// has already had its msg_type byte stripped by dispatch.
|
||||
pub(in crate::node) async fn handle_session_datagram(&mut self, _from: &NodeAddr, payload: &[u8]) {
|
||||
let mut datagram = match SessionDatagram::decode(payload) {
|
||||
@@ -27,12 +27,12 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Hop limit enforcement: decrement and drop if exhausted
|
||||
if !datagram.decrement_hop_limit() {
|
||||
// TTL enforcement: decrement and drop if exhausted
|
||||
if !datagram.decrement_ttl() {
|
||||
debug!(
|
||||
src = %datagram.src_addr,
|
||||
dest = %datagram.dest_addr,
|
||||
"SessionDatagram hop limit exhausted, dropping"
|
||||
"SessionDatagram TTL exhausted, dropping"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +56,15 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Forward: re-encode (includes 0x40 type byte) and send
|
||||
// Apply path_mtu min() from the outgoing link's transport MTU
|
||||
if let Some(peer) = self.peers.get(&next_hop_addr)
|
||||
&& let Some(tid) = peer.transport_id()
|
||||
&& let Some(transport) = self.transports.get(&tid)
|
||||
{
|
||||
datagram.path_mtu = datagram.path_mtu.min(transport.mtu());
|
||||
}
|
||||
|
||||
// Forward: re-encode (includes 0x00 type byte) and send
|
||||
let encoded = datagram.encode();
|
||||
if let Err(e) = self
|
||||
.send_encrypted_link_message(&next_hop_addr, &encoded)
|
||||
@@ -204,7 +212,7 @@ impl Node {
|
||||
};
|
||||
|
||||
let error_dg = SessionDatagram::new(my_addr, original.src_addr, error_payload)
|
||||
.with_hop_limit(self.config.node.session.default_hop_limit);
|
||||
.with_ttl(self.config.node.session.default_ttl);
|
||||
|
||||
let next_hop_addr = match self.find_next_hop(&original.src_addr) {
|
||||
Some(peer) => *peer.node_addr(),
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
impl Node {
|
||||
/// Handle handshake message 1 (discriminator 0x01).
|
||||
/// Handle handshake message 1 (phase 0x1).
|
||||
///
|
||||
/// This creates a new inbound connection. Rate limiting is applied
|
||||
/// before any expensive crypto operations.
|
||||
@@ -222,7 +222,7 @@ impl Node {
|
||||
self.msg1_rate_limiter.complete_handshake();
|
||||
}
|
||||
|
||||
/// Handle handshake message 2 (discriminator 0x02).
|
||||
/// Handle handshake message 2 (phase 0x2).
|
||||
///
|
||||
/// This completes an outbound handshake we initiated.
|
||||
pub(in crate::node) async fn handle_msg2(&mut self, packet: ReceivedPacket) {
|
||||
@@ -549,6 +549,8 @@ impl Node {
|
||||
transport_id,
|
||||
current_addr,
|
||||
link_stats,
|
||||
is_outbound,
|
||||
&self.config.node.mmp,
|
||||
);
|
||||
new_peer.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
|
||||
|
||||
@@ -629,6 +631,8 @@ impl Node {
|
||||
transport_id,
|
||||
current_addr,
|
||||
link_stats,
|
||||
is_outbound,
|
||||
&self.config.node.mmp,
|
||||
);
|
||||
new_peer.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//! MMP report dispatch, periodic report generation, and operator logging.
|
||||
//!
|
||||
//! Handles incoming SenderReport / ReceiverReport messages, drives
|
||||
//! periodic report generation on the tick timer, and emits periodic
|
||||
//! and teardown metric logs.
|
||||
|
||||
use crate::mmp::MmpMode;
|
||||
use crate::mmp::report::{ReceiverReport, SenderReport};
|
||||
use crate::node::Node;
|
||||
use crate::NodeAddr;
|
||||
use std::time::Instant;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Format bytes/sec as human-readable throughput.
|
||||
fn format_throughput(bps: f64) -> String {
|
||||
if bps == 0.0 {
|
||||
"n/a".to_string()
|
||||
} else if bps >= 1_000_000.0 {
|
||||
format!("{:.1}MB/s", bps / 1_000_000.0)
|
||||
} else if bps >= 1_000.0 {
|
||||
format!("{:.1}KB/s", bps / 1_000.0)
|
||||
} else {
|
||||
format!("{:.0}B/s", bps)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Handle an incoming SenderReport from a peer.
|
||||
///
|
||||
/// The peer is telling us about what they sent. We feed this to our
|
||||
/// receiver state for cross-reference (not currently used for metrics,
|
||||
/// but stored for future use).
|
||||
pub(in crate::node) fn handle_sender_report(&mut self, from: &NodeAddr, payload: &[u8]) {
|
||||
let sr = match SenderReport::decode(payload) {
|
||||
Ok(sr) => sr,
|
||||
Err(e) => {
|
||||
debug!(from = %from, error = %e, "Malformed SenderReport");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let peer = match self.peers.get_mut(from) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!(from = %from, "SenderReport from unknown peer");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if peer.mmp().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(
|
||||
from = %from,
|
||||
cum_pkts = sr.cumulative_packets_sent,
|
||||
interval_bytes = sr.interval_bytes_sent,
|
||||
"Received SenderReport"
|
||||
);
|
||||
|
||||
// Store sender's report in receiver state for cross-reference.
|
||||
// Currently informational; the receiver already tracks its own
|
||||
// counters and echoes timestamps from data frames.
|
||||
}
|
||||
|
||||
/// Handle an incoming ReceiverReport from a peer.
|
||||
///
|
||||
/// The peer is telling us about what they received from us. We feed
|
||||
/// this to our metrics to compute RTT, loss rate, and trend indicators.
|
||||
pub(in crate::node) fn handle_receiver_report(&mut self, from: &NodeAddr, payload: &[u8]) {
|
||||
let rr = match ReceiverReport::decode(payload) {
|
||||
Ok(rr) => rr,
|
||||
Err(e) => {
|
||||
debug!(from = %from, error = %e, "Malformed ReceiverReport");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let peer = match self.peers.get_mut(from) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!(from = %from, "ReceiverReport from unknown peer");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Get session timestamp before taking mutable borrow on MMP
|
||||
let our_timestamp_ms = peer.session_elapsed_ms();
|
||||
|
||||
let Some(mmp) = peer.mmp_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Process the report: computes RTT from timestamp echo, updates
|
||||
// loss rate, goodput rate, jitter trend, and ETX.
|
||||
let now = Instant::now();
|
||||
mmp.metrics.process_receiver_report(&rr, our_timestamp_ms, now);
|
||||
|
||||
// Feed SRTT back to sender/receiver report interval tuning
|
||||
if let Some(srtt_ms) = mmp.metrics.srtt_ms() {
|
||||
let srtt_us = (srtt_ms * 1000.0) as i64;
|
||||
mmp.sender.update_report_interval_from_srtt(srtt_us);
|
||||
mmp.receiver.update_report_interval_from_srtt(srtt_us);
|
||||
}
|
||||
|
||||
// Update reverse delivery ratio from our own receiver state
|
||||
// (what fraction of peer's frames we received).
|
||||
let our_recv_packets = mmp.receiver.cumulative_packets_recv();
|
||||
let peer_highest = mmp.receiver.highest_counter();
|
||||
if peer_highest > 0 {
|
||||
let reverse_ratio = (our_recv_packets as f64) / (peer_highest as f64);
|
||||
mmp.metrics.set_delivery_ratio_reverse(reverse_ratio);
|
||||
}
|
||||
|
||||
debug!(
|
||||
from = %from,
|
||||
rtt_ms = ?mmp.metrics.srtt_ms(),
|
||||
loss = format_args!("{:.1}%", mmp.metrics.loss_rate() * 100.0),
|
||||
etx = format_args!("{:.2}", mmp.metrics.etx),
|
||||
"Processed ReceiverReport"
|
||||
);
|
||||
}
|
||||
|
||||
/// Check all peers for pending MMP reports and send them.
|
||||
///
|
||||
/// Called from the tick handler. Also emits periodic operator logs.
|
||||
pub(in crate::node) async fn check_mmp_reports(&mut self) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Collect peers that need reports (can't borrow self mutably while iterating)
|
||||
let mut sender_reports: Vec<(NodeAddr, Vec<u8>)> = Vec::new();
|
||||
let mut receiver_reports: Vec<(NodeAddr, Vec<u8>)> = Vec::new();
|
||||
|
||||
for (node_addr, peer) in self.peers.iter_mut() {
|
||||
let Some(mmp) = peer.mmp_mut() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mode = mmp.mode();
|
||||
|
||||
// Sender reports: Full mode only
|
||||
if mode == MmpMode::Full && mmp.sender.should_send_report(now) {
|
||||
if let Some(sr) = mmp.sender.build_report(now) {
|
||||
sender_reports.push((*node_addr, sr.encode()));
|
||||
}
|
||||
}
|
||||
|
||||
// Receiver reports: Full and Lightweight modes
|
||||
if mode != MmpMode::Minimal && mmp.receiver.should_send_report(now) {
|
||||
if let Some(rr) = mmp.receiver.build_report(now) {
|
||||
receiver_reports.push((*node_addr, rr.encode()));
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic operator logging
|
||||
if mmp.should_log(now) {
|
||||
Self::log_mmp_metrics(node_addr, mmp);
|
||||
mmp.mark_logged(now);
|
||||
}
|
||||
}
|
||||
|
||||
// Send collected reports
|
||||
for (node_addr, encoded) in sender_reports {
|
||||
if let Err(e) = self.send_encrypted_link_message(&node_addr, &encoded).await {
|
||||
warn!(peer = %node_addr, error = %e, "Failed to send SenderReport");
|
||||
}
|
||||
}
|
||||
|
||||
for (node_addr, encoded) in receiver_reports {
|
||||
if let Err(e) = self.send_encrypted_link_message(&node_addr, &encoded).await {
|
||||
warn!(peer = %node_addr, error = %e, "Failed to send ReceiverReport");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit periodic MMP metrics for a peer at info and debug levels.
|
||||
fn log_mmp_metrics(node_addr: &NodeAddr, mmp: &crate::mmp::MmpPeerState) {
|
||||
let m = &mmp.metrics;
|
||||
|
||||
let rtt_str = match m.srtt_ms() {
|
||||
Some(rtt) => format!("{:.1}ms", rtt),
|
||||
None => "n/a".to_string(),
|
||||
};
|
||||
let loss_pct = m.loss_rate() * 100.0;
|
||||
let tx_pkts = mmp.sender.cumulative_packets_sent();
|
||||
let rx_pkts = mmp.receiver.cumulative_packets_recv();
|
||||
|
||||
let goodput_bps = m.goodput_bps();
|
||||
let goodput_str = format_throughput(goodput_bps);
|
||||
|
||||
// Info-level: concise summary
|
||||
info!(
|
||||
peer = %node_addr,
|
||||
rtt = %rtt_str,
|
||||
loss = format_args!("{:.1}%", loss_pct),
|
||||
goodput = %goodput_str,
|
||||
tx_pkts = tx_pkts,
|
||||
rx_pkts = rx_pkts,
|
||||
"MMP link metrics"
|
||||
);
|
||||
|
||||
// Debug-level: extended details
|
||||
debug!(
|
||||
peer = %node_addr,
|
||||
jitter_us = mmp.receiver.jitter_us(),
|
||||
reorder = mmp.receiver.cumulative_packets_recv(),
|
||||
rtt_trend = format_args!("{}", if m.rtt_trend.initialized() {
|
||||
format!("short={:.1} long={:.1}", m.rtt_trend.short(), m.rtt_trend.long())
|
||||
} else {
|
||||
"n/a".to_string()
|
||||
}),
|
||||
loss_trend = format_args!("{}", if m.loss_trend.initialized() {
|
||||
format!("short={:.4} long={:.4}", m.loss_trend.short(), m.loss_trend.long())
|
||||
} else {
|
||||
"n/a".to_string()
|
||||
}),
|
||||
delivery_fwd = format_args!("{:.3}", m.delivery_ratio_forward),
|
||||
delivery_rev = format_args!("{:.3}", m.delivery_ratio_reverse),
|
||||
mode = %mmp.mode(),
|
||||
"MMP link metrics (detail)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a teardown log summarizing lifetime MMP metrics for a removed peer.
|
||||
pub(in crate::node) fn log_mmp_teardown(node_addr: &NodeAddr, mmp: &crate::mmp::MmpPeerState) {
|
||||
let m = &mmp.metrics;
|
||||
|
||||
let rtt_str = match m.srtt_ms() {
|
||||
Some(rtt) => format!("{:.1}ms", rtt),
|
||||
None => "n/a".to_string(),
|
||||
};
|
||||
|
||||
info!(
|
||||
peer = %node_addr,
|
||||
rtt = %rtt_str,
|
||||
loss = format_args!("{:.1}%", m.loss_rate() * 100.0),
|
||||
etx = format_args!("{:.2}", m.etx),
|
||||
tx_pkts = mmp.sender.cumulative_packets_sent(),
|
||||
tx_bytes = mmp.sender.cumulative_bytes_sent(),
|
||||
rx_pkts = mmp.receiver.cumulative_packets_recv(),
|
||||
rx_bytes = mmp.receiver.cumulative_bytes_recv(),
|
||||
jitter_us = mmp.receiver.jitter_us(),
|
||||
"MMP link teardown"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod dispatch;
|
||||
mod encrypted;
|
||||
mod forwarding;
|
||||
mod handshake;
|
||||
mod mmp;
|
||||
mod rx_loop;
|
||||
mod session;
|
||||
mod timeout;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::transport::ReceivedPacket;
|
||||
use crate::node::wire::{DISCRIMINATOR_ENCRYPTED, DISCRIMINATOR_MSG1, DISCRIMINATOR_MSG2};
|
||||
use crate::node::wire::{CommonPrefix, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2, FLP_VERSION, COMMON_PREFIX_SIZE};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info};
|
||||
|
||||
@@ -10,10 +10,10 @@ impl Node {
|
||||
/// Run the receive event loop.
|
||||
///
|
||||
/// Processes packets from all transports, dispatching based on
|
||||
/// the discriminator byte in the wire protocol:
|
||||
/// - 0x00: Encrypted frame (session data)
|
||||
/// - 0x01: Handshake message 1 (initiator -> responder)
|
||||
/// - 0x02: Handshake message 2 (responder -> initiator)
|
||||
/// the phase field in the 4-byte common prefix:
|
||||
/// - Phase 0x0: Encrypted frame (session data)
|
||||
/// - Phase 0x1: Handshake message 1 (initiator -> responder)
|
||||
/// - Phase 0x2: Handshake message 2 (responder -> initiator)
|
||||
///
|
||||
/// Also processes outbound IPv6 packets from the TUN reader for session
|
||||
/// encapsulation and routing through the mesh.
|
||||
@@ -83,6 +83,7 @@ impl Node {
|
||||
self.process_pending_retries(now_ms).await;
|
||||
self.check_tree_state().await;
|
||||
self.check_bloom_state().await;
|
||||
self.check_mmp_reports().await;
|
||||
self.purge_stale_lookups(now_ms);
|
||||
}
|
||||
}
|
||||
@@ -94,29 +95,41 @@ impl Node {
|
||||
|
||||
/// Process a single received packet.
|
||||
///
|
||||
/// Dispatches based on the discriminator byte.
|
||||
/// Dispatches based on the phase field in the 4-byte common prefix.
|
||||
async fn process_packet(&mut self, packet: ReceivedPacket) {
|
||||
if packet.data.is_empty() {
|
||||
return; // Drop empty packets
|
||||
if packet.data.len() < COMMON_PREFIX_SIZE {
|
||||
return; // Drop packets too short for common prefix
|
||||
}
|
||||
|
||||
let discriminator = packet.data[0];
|
||||
match discriminator {
|
||||
DISCRIMINATOR_ENCRYPTED => {
|
||||
let prefix = match CommonPrefix::parse(&packet.data) {
|
||||
Some(p) => p,
|
||||
None => return, // Malformed prefix
|
||||
};
|
||||
|
||||
if prefix.version != FLP_VERSION {
|
||||
debug!(
|
||||
version = prefix.version,
|
||||
transport_id = %packet.transport_id,
|
||||
"Unknown FLP version, dropping"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
match prefix.phase {
|
||||
PHASE_ESTABLISHED => {
|
||||
self.handle_encrypted_frame(packet).await;
|
||||
}
|
||||
DISCRIMINATOR_MSG1 => {
|
||||
PHASE_MSG1 => {
|
||||
self.handle_msg1(packet).await;
|
||||
}
|
||||
DISCRIMINATOR_MSG2 => {
|
||||
PHASE_MSG2 => {
|
||||
self.handle_msg2(packet).await;
|
||||
}
|
||||
_ => {
|
||||
// Unknown discriminator, drop silently
|
||||
debug!(
|
||||
discriminator = discriminator,
|
||||
phase = prefix.phase,
|
||||
transport_id = %packet.transport_id,
|
||||
"Unknown packet discriminator, dropping"
|
||||
"Unknown FLP phase, dropping"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ impl Node {
|
||||
let ack = SessionAck::new(our_coords).with_handshake(msg2);
|
||||
let my_addr = *self.node_addr();
|
||||
let datagram = SessionDatagram::new(my_addr, *src_addr, ack.encode())
|
||||
.with_hop_limit(self.config.node.session.default_hop_limit);
|
||||
.with_ttl(self.config.node.session.default_ttl);
|
||||
|
||||
// Route the ack back to the initiator
|
||||
if let Err(e) = self.send_session_datagram(&datagram).await {
|
||||
@@ -432,7 +432,7 @@ impl Node {
|
||||
// Wrap in SessionDatagram
|
||||
let my_addr = *self.node_addr();
|
||||
let datagram = SessionDatagram::new(my_addr, dest_addr, setup.encode())
|
||||
.with_hop_limit(self.config.node.session.default_hop_limit);
|
||||
.with_ttl(self.config.node.session.default_ttl);
|
||||
|
||||
// Route toward destination
|
||||
self.send_session_datagram(&datagram).await?;
|
||||
@@ -498,7 +498,7 @@ impl Node {
|
||||
|
||||
let my_addr = *self.node_addr();
|
||||
let datagram = SessionDatagram::new(my_addr, *dest_addr, data_packet.encode())
|
||||
.with_hop_limit(self.config.node.session.default_hop_limit);
|
||||
.with_ttl(self.config.node.session.default_ttl);
|
||||
|
||||
self.send_session_datagram(&datagram).await?;
|
||||
|
||||
|
||||
+28
-4
@@ -30,7 +30,7 @@ use crate::transport::udp::UdpTransport;
|
||||
use crate::tree::TreeState;
|
||||
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
|
||||
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use self::wire::build_encrypted;
|
||||
use self::wire::{build_encrypted, build_established_header, prepend_inner_header, FLAG_SP};
|
||||
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
@@ -1026,6 +1026,10 @@ impl Node {
|
||||
/// The plaintext should include the message type byte followed by the
|
||||
/// message-specific payload (e.g., `[0x50, reason]` for Disconnect).
|
||||
///
|
||||
/// The send path prepends a 4-byte session-relative timestamp (inner
|
||||
/// header) before encryption. The full 16-byte outer header is used
|
||||
/// as AAD for the AEAD construction.
|
||||
///
|
||||
/// This is the standard path for sending any link-layer control message
|
||||
/// to a peer over their encrypted Noise session.
|
||||
pub(super) async fn send_encrypted_link_message(
|
||||
@@ -1049,19 +1053,35 @@ impl Node {
|
||||
reason: "no current_addr".into(),
|
||||
})?;
|
||||
|
||||
// Prepend 4-byte session-relative timestamp (inner header)
|
||||
let timestamp_ms = peer.session_elapsed_ms();
|
||||
|
||||
// MMP: read spin bit value before entering session borrow
|
||||
let sp_flag = peer.mmp()
|
||||
.map(|mmp| mmp.spin_bit.tx_bit())
|
||||
.unwrap_or(false);
|
||||
let flags = if sp_flag { FLAG_SP } else { 0 };
|
||||
|
||||
let session = peer.noise_session_mut().ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no noise session".into(),
|
||||
})?;
|
||||
|
||||
// Get counter before encrypt (encrypt increments it)
|
||||
// Inner plaintext: [timestamp:4 LE][msg_type][payload...]
|
||||
let inner_plaintext = prepend_inner_header(timestamp_ms, plaintext);
|
||||
|
||||
// Build 16-byte outer header (used as AAD for AEAD)
|
||||
let counter = session.current_send_counter();
|
||||
let ciphertext = session.encrypt(plaintext).map_err(|e| NodeError::SendFailed {
|
||||
let payload_len = inner_plaintext.len() as u16;
|
||||
let header = build_established_header(their_index, counter, flags, payload_len);
|
||||
|
||||
// Encrypt with AAD binding to the outer header
|
||||
let ciphertext = session.encrypt_with_aad(&inner_plaintext, &header).map_err(|e| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: format!("encryption failed: {}", e),
|
||||
})?;
|
||||
|
||||
let wire_packet = build_encrypted(their_index, counter, &ciphertext);
|
||||
let wire_packet = build_encrypted(&header, &ciphertext);
|
||||
|
||||
// Re-borrow peer for stats update after sending
|
||||
let transport = self.transports.get(&transport_id)
|
||||
@@ -1076,6 +1096,10 @@ impl Node {
|
||||
// Update send statistics
|
||||
if let Some(peer) = self.peers.get_mut(node_addr) {
|
||||
peer.link_stats_mut().record_sent(bytes_sent);
|
||||
// MMP: record sent frame for sender report generation
|
||||
if let Some(mmp) = peer.mmp_mut() {
|
||||
mmp.sender.record_sent(counter, timestamp_ms, bytes_sent);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! SessionDatagram forwarding tests.
|
||||
//!
|
||||
//! Tests for the handle_session_datagram handler including decode errors,
|
||||
//! hop limit enforcement, local delivery, coordinate cache warming, and
|
||||
//! TTL enforcement, local delivery, coordinate cache warming, and
|
||||
//! multi-hop forwarding through live node topologies.
|
||||
|
||||
use super::*;
|
||||
@@ -26,7 +26,7 @@ async fn test_forwarding_decode_error() {
|
||||
node.handle_session_datagram(&from, &[0x00; 5]).await;
|
||||
}
|
||||
|
||||
// --- Hop limit ---
|
||||
// --- TTL ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forwarding_hop_limit_exhausted() {
|
||||
@@ -35,7 +35,7 @@ async fn test_forwarding_hop_limit_exhausted() {
|
||||
let src = make_node_addr(0x01);
|
||||
let dest = make_node_addr(0x02);
|
||||
let dg = SessionDatagram::new(src, dest, vec![0x10, 0x00, 0x00, 0x00])
|
||||
.with_hop_limit(0);
|
||||
.with_ttl(0);
|
||||
let encoded = dg.encode();
|
||||
// Dispatch with payload after msg_type byte
|
||||
node.handle_session_datagram(&from, &encoded[1..]).await;
|
||||
@@ -44,17 +44,17 @@ async fn test_forwarding_hop_limit_exhausted() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forwarding_hop_limit_one_drops_at_transit() {
|
||||
// hop_limit=1 means after decrement it becomes 0 — the datagram can
|
||||
// ttl=1 means after decrement it becomes 0 — the datagram can
|
||||
// still be delivered this hop but would be dropped at the next.
|
||||
// decrement_hop_limit returns true (1 > 0), so the handler proceeds.
|
||||
// decrement_ttl returns true (1 > 0), so the handler proceeds.
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let my_addr = *node.node_addr();
|
||||
let src = make_node_addr(0x01);
|
||||
let dg = SessionDatagram::new(src, my_addr, vec![0x10, 0x00, 0x00, 0x00])
|
||||
.with_hop_limit(1);
|
||||
.with_ttl(1);
|
||||
let encoded = dg.encode();
|
||||
// Should succeed — hop_limit=1 decrements to 0 but packet is still processed
|
||||
// Should succeed — ttl=1 decrements to 0 but packet is still processed
|
||||
node.handle_session_datagram(&from, &encoded[1..]).await;
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ async fn test_forwarding_multi_hop() {
|
||||
let node1_addr = *nodes[1].node.node_addr();
|
||||
let node4_addr = *nodes[4].node.node_addr();
|
||||
|
||||
// Build a SessionDatagram with enough hop_limit for 4 hops
|
||||
// Build a SessionDatagram with enough TTL for 4 hops
|
||||
let dg = SessionDatagram::new(
|
||||
node0_addr,
|
||||
node4_addr,
|
||||
@@ -372,9 +372,9 @@ async fn test_forwarding_multi_hop() {
|
||||
#[tokio::test]
|
||||
async fn test_forwarding_hop_limit_prevents_infinite_loops() {
|
||||
// 3-node chain: 0 -- 1 -- 2
|
||||
// Send a datagram with hop_limit=1. It should be forwarded by node 1
|
||||
// Send a datagram with ttl=1. It should be forwarded by node 1
|
||||
// (decrement to 0) and delivered at node 2 (local delivery). If node 2
|
||||
// tried to forward further, the 0 hop_limit would prevent it.
|
||||
// tried to forward further, the 0 ttl would prevent it.
|
||||
let edges = vec![(0, 1), (1, 2)];
|
||||
let mut nodes = run_tree_test(3, &edges, false).await;
|
||||
verify_tree_convergence(&nodes);
|
||||
@@ -389,7 +389,7 @@ async fn test_forwarding_hop_limit_prevents_infinite_loops() {
|
||||
node2_addr,
|
||||
vec![0x10, 0x00, 0x04, 0x00, 1, 2, 3, 4],
|
||||
)
|
||||
.with_hop_limit(2); // Enough for 0→1 (decrement to 1) and 1→2 (decrement to 0, local delivery)
|
||||
.with_ttl(2); // Enough for 0->1 (decrement to 1) and 1->2 (decrement to 0, local delivery)
|
||||
|
||||
let encoded = dg.encode();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::*;
|
||||
async fn test_two_node_handshake_udp() {
|
||||
use crate::config::UdpConfig;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::node::wire::{build_encrypted, build_msg1};
|
||||
use crate::node::wire::{build_encrypted, build_established_header, build_msg1, prepend_inner_header};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
// === Setup: Two nodes with UDP transports on localhost ===
|
||||
@@ -156,13 +156,17 @@ async fn test_two_node_handshake_udp() {
|
||||
// === Phase 4: Encrypted frame A → B ===
|
||||
|
||||
// A encrypts a test message and sends to B
|
||||
let plaintext_a = b"hello from A";
|
||||
// Prepend inner header (timestamp + msg_type) as the real send path does
|
||||
let msg_a = b"\x10test from A"; // msg_type 0x10 (TreeAnnounce) + dummy payload
|
||||
let inner_a = prepend_inner_header(0, msg_a);
|
||||
let peer_b = node_a.get_peer_mut(&peer_b_node_addr).unwrap();
|
||||
let their_index_b = peer_b.their_index().expect("A should know B's index");
|
||||
let session_a = peer_b.noise_session_mut().unwrap();
|
||||
let ciphertext_a = session_a.encrypt(plaintext_a).unwrap();
|
||||
let counter_a = session_a.current_send_counter();
|
||||
let header_a = build_established_header(their_index_b, counter_a, 0, inner_a.len() as u16);
|
||||
let ciphertext_a = session_a.encrypt_with_aad(&inner_a, &header_a).unwrap();
|
||||
|
||||
let wire_encrypted = build_encrypted(their_index_b, 0, &ciphertext_a);
|
||||
let wire_encrypted = build_encrypted(&header_a, &ciphertext_a);
|
||||
let transport = node_a.transports.get(&transport_id_a).unwrap();
|
||||
transport
|
||||
.send(&remote_addr_b, &wire_encrypted)
|
||||
@@ -186,13 +190,17 @@ async fn test_two_node_handshake_udp() {
|
||||
|
||||
// === Phase 5: Encrypted frame B → A ===
|
||||
|
||||
let plaintext_b = b"hello from B";
|
||||
// Prepend inner header (timestamp + msg_type) as the real send path does
|
||||
let msg_b = b"\x10test from B"; // msg_type 0x10 (TreeAnnounce) + dummy payload
|
||||
let inner_b = prepend_inner_header(0, msg_b);
|
||||
let peer_a = node_b.get_peer_mut(&peer_a_node_addr).unwrap();
|
||||
let their_index_a = peer_a.their_index().expect("B should know A's index");
|
||||
let session_b = peer_a.noise_session_mut().unwrap();
|
||||
let ciphertext_b = session_b.encrypt(plaintext_b).unwrap();
|
||||
let counter_b = session_b.current_send_counter();
|
||||
let header_b = build_established_header(their_index_a, counter_b, 0, inner_b.len() as u16);
|
||||
let ciphertext_b = session_b.encrypt_with_aad(&inner_b, &header_b).unwrap();
|
||||
|
||||
let wire_encrypted_b = build_encrypted(their_index_a, 0, &ciphertext_b);
|
||||
let wire_encrypted_b = build_encrypted(&header_b, &ciphertext_b);
|
||||
let transport = node_b.transports.get(&transport_id_b).unwrap();
|
||||
transport
|
||||
.send(&remote_addr_a, &wire_encrypted_b)
|
||||
@@ -328,7 +336,7 @@ async fn test_run_rx_loop_handshake() {
|
||||
//
|
||||
// This is the key difference from test_two_node_handshake_udp:
|
||||
// instead of calling handle_msg1() directly, we run the full rx loop
|
||||
// which dispatches based on the discriminator byte.
|
||||
// which dispatches based on the common prefix phase field.
|
||||
|
||||
tokio::select! {
|
||||
result = node_b.run_rx_loop() => {
|
||||
|
||||
@@ -203,23 +203,28 @@ pub(super) fn print_tree_snapshot(label: &str, nodes: &[TestNode]) {
|
||||
///
|
||||
/// Returns the number of packets processed.
|
||||
pub(super) async fn process_available_packets(nodes: &mut [TestNode]) -> usize {
|
||||
use crate::node::wire::{DISCRIMINATOR_ENCRYPTED, DISCRIMINATOR_MSG1, DISCRIMINATOR_MSG2};
|
||||
use crate::node::wire::{CommonPrefix, FLP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2, COMMON_PREFIX_SIZE};
|
||||
|
||||
let mut count = 0;
|
||||
for node in nodes.iter_mut() {
|
||||
while let Ok(packet) = node.packet_rx.try_recv() {
|
||||
if packet.data.is_empty() {
|
||||
if packet.data.len() < COMMON_PREFIX_SIZE {
|
||||
continue;
|
||||
}
|
||||
match packet.data[0] {
|
||||
DISCRIMINATOR_MSG1 => node.node.handle_msg1(packet).await,
|
||||
DISCRIMINATOR_MSG2 => node.node.handle_msg2(packet).await,
|
||||
DISCRIMINATOR_ENCRYPTED => {
|
||||
node.node.handle_encrypted_frame(packet).await
|
||||
if let Some(prefix) = CommonPrefix::parse(&packet.data) {
|
||||
if prefix.version != FLP_VERSION {
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
match prefix.phase {
|
||||
PHASE_MSG1 => node.node.handle_msg1(packet).await,
|
||||
PHASE_MSG2 => node.node.handle_msg2(packet).await,
|
||||
PHASE_ESTABLISHED => {
|
||||
node.node.handle_encrypted_frame(packet).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
|
||||
+361
-93
@@ -1,15 +1,21 @@
|
||||
//! Wire Format Parsing and Serialization
|
||||
//!
|
||||
//! Defines the FIPS link-layer wire format for packet dispatch.
|
||||
//! All packets begin with a discriminator byte followed by type-specific payload.
|
||||
//! Defines the FIPS link-layer wire format (FLP) for packet dispatch.
|
||||
//! All packets begin with a 4-byte common prefix followed by phase-specific fields.
|
||||
//!
|
||||
//! ## Common Prefix (4 bytes)
|
||||
//!
|
||||
//! ```text
|
||||
//! [ver+phase:1][flags:1][payload_len:2 LE]
|
||||
//! ```
|
||||
//!
|
||||
//! ## Packet Types
|
||||
//!
|
||||
//! | Byte | Type | Size | Description |
|
||||
//! |------|-----------------|-----------|--------------------------------|
|
||||
//! | 0x00 | Encrypted frame | 29+ bytes | Post-handshake encrypted data |
|
||||
//! | 0x01 | Noise IK msg1 | 87 bytes | Handshake initiation |
|
||||
//! | 0x02 | Noise IK msg2 | 42 bytes | Handshake response |
|
||||
//! | Phase | Type | Size | Description |
|
||||
//! |-------|-----------------|-----------|--------------------------------|
|
||||
//! | 0x0 | Encrypted frame | 32+ bytes | Post-handshake encrypted data |
|
||||
//! | 0x1 | Noise IK msg1 | 90 bytes | Handshake initiation |
|
||||
//! | 0x2 | Noise IK msg2 | 45 bytes | Handshake response |
|
||||
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
||||
@@ -18,73 +24,171 @@ use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
/// Discriminator for encrypted frames (post-handshake data).
|
||||
pub const DISCRIMINATOR_ENCRYPTED: u8 = 0x00;
|
||||
/// FLP protocol version (4 high bits of byte 0).
|
||||
pub const FLP_VERSION: u8 = 0;
|
||||
|
||||
/// Discriminator for Noise IK message 1 (handshake initiation).
|
||||
pub const DISCRIMINATOR_MSG1: u8 = 0x01;
|
||||
/// Phase value for established (encrypted) frames.
|
||||
pub const PHASE_ESTABLISHED: u8 = 0x0;
|
||||
|
||||
/// Discriminator for Noise IK message 2 (handshake response).
|
||||
pub const DISCRIMINATOR_MSG2: u8 = 0x02;
|
||||
/// Phase value for Noise IK message 1 (handshake initiation).
|
||||
pub const PHASE_MSG1: u8 = 0x1;
|
||||
|
||||
/// Size of Noise IK message 1 wire packet: discriminator + sender_idx + noise_msg1.
|
||||
pub const MSG1_WIRE_SIZE: usize = 1 + 4 + HANDSHAKE_MSG1_SIZE; // 87 bytes
|
||||
/// Phase value for Noise IK message 2 (handshake response).
|
||||
pub const PHASE_MSG2: u8 = 0x2;
|
||||
|
||||
/// Size of Noise IK message 2 wire packet: discriminator + sender_idx + receiver_idx + noise_msg2.
|
||||
pub const MSG2_WIRE_SIZE: usize = 1 + 4 + 4 + HANDSHAKE_MSG2_SIZE; // 42 bytes
|
||||
/// Size of the common packet prefix (all packet types).
|
||||
pub const COMMON_PREFIX_SIZE: usize = 4;
|
||||
|
||||
/// Minimum size for encrypted frame: discriminator + receiver_idx + counter + tag.
|
||||
pub const ENCRYPTED_MIN_SIZE: usize = 1 + 4 + 8 + TAG_SIZE; // 29 bytes
|
||||
/// Size of the full established frame header (prefix + receiver_idx + counter).
|
||||
pub const ESTABLISHED_HEADER_SIZE: usize = 16;
|
||||
|
||||
/// Size of Noise IK message 1 wire packet: prefix + sender_idx + noise_msg1.
|
||||
pub const MSG1_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + HANDSHAKE_MSG1_SIZE; // 90 bytes
|
||||
|
||||
/// Size of Noise IK message 2 wire packet: prefix + sender_idx + receiver_idx + noise_msg2.
|
||||
pub const MSG2_WIRE_SIZE: usize = COMMON_PREFIX_SIZE + 4 + 4 + HANDSHAKE_MSG2_SIZE; // 45 bytes
|
||||
|
||||
/// Minimum size for encrypted frame: header + tag (no plaintext).
|
||||
pub const ENCRYPTED_MIN_SIZE: usize = ESTABLISHED_HEADER_SIZE + TAG_SIZE; // 32 bytes
|
||||
|
||||
/// Size of the encrypted inner header (timestamp + message type).
|
||||
pub const INNER_HEADER_SIZE: usize = 5;
|
||||
|
||||
// Flag bit constants (byte 1 of common prefix, meaningful only for phase 0x0).
|
||||
// Reserved for upcoming rekeying, congestion signaling, and RTT measurement.
|
||||
#[allow(dead_code)]
|
||||
/// Key epoch flag — selects active key during rekeying.
|
||||
pub const FLAG_KEY_EPOCH: u8 = 0x01;
|
||||
#[allow(dead_code)]
|
||||
/// Congestion Experienced echo flag.
|
||||
pub const FLAG_CE: u8 = 0x02;
|
||||
#[allow(dead_code)]
|
||||
/// Spin bit for RTT measurement.
|
||||
pub const FLAG_SP: u8 = 0x04;
|
||||
|
||||
// ============================================================================
|
||||
// Common Prefix
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed common packet prefix (first 4 bytes of every FLP packet).
|
||||
///
|
||||
/// Wire format:
|
||||
/// ```text
|
||||
/// [ver(4bits)+phase(4bits)][flags:1][payload_len:2 LE]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommonPrefix {
|
||||
/// Protocol version (high nibble of byte 0).
|
||||
pub version: u8,
|
||||
/// Session lifecycle phase (low nibble of byte 0).
|
||||
pub phase: u8,
|
||||
/// Per-packet signal flags (meaningful only for phase 0x0).
|
||||
#[allow(dead_code)]
|
||||
pub flags: u8,
|
||||
/// Length of payload following the phase-specific header (excludes AEAD tag).
|
||||
#[allow(dead_code)]
|
||||
pub payload_len: u16,
|
||||
}
|
||||
|
||||
impl CommonPrefix {
|
||||
/// Parse a common prefix from the first 4 bytes of packet data.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < COMMON_PREFIX_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
let flags = data[1];
|
||||
let payload_len = u16::from_le_bytes([data[2], data[3]]);
|
||||
|
||||
Some(Self {
|
||||
version,
|
||||
phase,
|
||||
flags,
|
||||
payload_len,
|
||||
})
|
||||
}
|
||||
|
||||
/// Encode the ver+phase byte.
|
||||
fn ver_phase_byte(version: u8, phase: u8) -> u8 {
|
||||
(version << 4) | (phase & 0x0F)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Encrypted Frame Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed encrypted frame header.
|
||||
/// Parsed established frame header (phase 0x0).
|
||||
///
|
||||
/// Wire format:
|
||||
/// Wire format (16 bytes):
|
||||
/// ```text
|
||||
/// [0x00][receiver_idx:4 LE][counter:8 LE][ciphertext+tag]
|
||||
/// [ver+phase:1][flags:1][payload_len:2 LE][receiver_idx:4 LE][counter:8 LE]
|
||||
/// ```
|
||||
///
|
||||
/// The full 16-byte header is used as AAD for the AEAD construction.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EncryptedHeader {
|
||||
/// Per-packet flags (K, CE, SP).
|
||||
#[allow(dead_code)]
|
||||
pub flags: u8,
|
||||
/// Length of encrypted payload (excluding AEAD tag).
|
||||
#[allow(dead_code)]
|
||||
pub payload_len: u16,
|
||||
/// Session index chosen by the receiver (for O(1) lookup).
|
||||
pub receiver_idx: SessionIndex,
|
||||
/// Monotonic counter used as AEAD nonce.
|
||||
pub counter: u64,
|
||||
/// Offset where ciphertext begins in the original packet.
|
||||
pub ciphertext_offset: usize,
|
||||
/// Raw 16-byte header for use as AEAD AAD.
|
||||
pub header_bytes: [u8; ESTABLISHED_HEADER_SIZE],
|
||||
}
|
||||
|
||||
impl EncryptedHeader {
|
||||
/// Parse an encrypted frame header from packet data.
|
||||
/// Parse an established frame header from packet data.
|
||||
///
|
||||
/// Returns None if the packet is too short or has wrong discriminator.
|
||||
/// Returns None if the packet is too short or has wrong version/phase.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < ENCRYPTED_MIN_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != DISCRIMINATOR_ENCRYPTED {
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FLP_VERSION || phase != PHASE_ESTABLISHED {
|
||||
return None;
|
||||
}
|
||||
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
let flags = data[1];
|
||||
let payload_len = u16::from_le_bytes([data[2], data[3]]);
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
let counter = u64::from_le_bytes([
|
||||
data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
|
||||
data[8], data[9], data[10], data[11],
|
||||
data[12], data[13], data[14], data[15],
|
||||
]);
|
||||
|
||||
let mut header_bytes = [0u8; ESTABLISHED_HEADER_SIZE];
|
||||
header_bytes.copy_from_slice(&data[..ESTABLISHED_HEADER_SIZE]);
|
||||
|
||||
Some(Self {
|
||||
flags,
|
||||
payload_len,
|
||||
receiver_idx,
|
||||
counter,
|
||||
ciphertext_offset: 13,
|
||||
header_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Offset where ciphertext begins in the original packet.
|
||||
pub fn ciphertext_offset(&self) -> usize {
|
||||
ESTABLISHED_HEADER_SIZE
|
||||
}
|
||||
|
||||
/// Get the ciphertext slice from the original packet.
|
||||
#[cfg(test)]
|
||||
pub fn ciphertext<'a>(&self, data: &'a [u8]) -> &'a [u8] {
|
||||
&data[self.ciphertext_offset..]
|
||||
&data[ESTABLISHED_HEADER_SIZE..]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +196,11 @@ impl EncryptedHeader {
|
||||
// Msg1 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed Noise IK message 1 header.
|
||||
/// Parsed Noise IK message 1 header (phase 0x1).
|
||||
///
|
||||
/// Wire format:
|
||||
/// Wire format (90 bytes):
|
||||
/// ```text
|
||||
/// [0x01][sender_idx:4 LE][noise_msg1:82]
|
||||
/// [0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:82]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg1Header {
|
||||
@@ -109,21 +213,29 @@ pub struct Msg1Header {
|
||||
impl Msg1Header {
|
||||
/// Parse a msg1 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet has wrong size or discriminator.
|
||||
/// Returns None if the packet has wrong size or version/phase.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != MSG1_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != DISCRIMINATOR_MSG1 {
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FLP_VERSION || phase != PHASE_MSG1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
// flags must be zero during handshake
|
||||
if data[1] != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
|
||||
Some(Self {
|
||||
sender_idx,
|
||||
noise_msg1_offset: 5,
|
||||
noise_msg1_offset: COMMON_PREFIX_SIZE + 4, // 8
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,11 +250,11 @@ impl Msg1Header {
|
||||
// Msg2 Header
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed Noise IK message 2 header.
|
||||
/// Parsed Noise IK message 2 header (phase 0x2).
|
||||
///
|
||||
/// Wire format:
|
||||
/// Wire format (45 bytes):
|
||||
/// ```text
|
||||
/// [0x02][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]
|
||||
/// [0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Msg2Header {
|
||||
@@ -157,23 +269,31 @@ pub struct Msg2Header {
|
||||
impl Msg2Header {
|
||||
/// Parse a msg2 header from packet data.
|
||||
///
|
||||
/// Returns None if the packet has wrong size or discriminator.
|
||||
/// Returns None if the packet has wrong size or version/phase.
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != MSG2_WIRE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != DISCRIMINATOR_MSG2 {
|
||||
let version = data[0] >> 4;
|
||||
let phase = data[0] & 0x0F;
|
||||
|
||||
if version != FLP_VERSION || phase != PHASE_MSG2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[5], data[6], data[7], data[8]]);
|
||||
// flags must be zero during handshake
|
||||
if data[1] != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sender_idx = SessionIndex::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
let receiver_idx = SessionIndex::from_le_bytes([data[8], data[9], data[10], data[11]]);
|
||||
|
||||
Some(Self {
|
||||
sender_idx,
|
||||
receiver_idx,
|
||||
noise_msg2_offset: 9,
|
||||
noise_msg2_offset: COMMON_PREFIX_SIZE + 4 + 4, // 12
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,12 +310,16 @@ impl Msg2Header {
|
||||
|
||||
/// Build a wire-format msg1 packet.
|
||||
///
|
||||
/// Format: `[0x01][sender_idx:4 LE][noise_msg1:82]`
|
||||
/// Format: `[0x01][0x00][payload_len:2 LE][sender_idx:4 LE][noise_msg1:82]`
|
||||
pub fn build_msg1(sender_idx: SessionIndex, noise_msg1: &[u8]) -> Vec<u8> {
|
||||
debug_assert_eq!(noise_msg1.len(), HANDSHAKE_MSG1_SIZE);
|
||||
|
||||
let payload_len = (4 + noise_msg1.len()) as u16; // sender_idx + noise_msg1
|
||||
|
||||
let mut packet = Vec::with_capacity(MSG1_WIRE_SIZE);
|
||||
packet.push(DISCRIMINATOR_MSG1);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FLP_VERSION, PHASE_MSG1));
|
||||
packet.push(0x00); // flags must be zero
|
||||
packet.extend_from_slice(&payload_len.to_le_bytes());
|
||||
packet.extend_from_slice(&sender_idx.to_le_bytes());
|
||||
packet.extend_from_slice(noise_msg1);
|
||||
packet
|
||||
@@ -203,30 +327,80 @@ pub fn build_msg1(sender_idx: SessionIndex, noise_msg1: &[u8]) -> Vec<u8> {
|
||||
|
||||
/// Build a wire-format msg2 packet.
|
||||
///
|
||||
/// Format: `[0x02][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]`
|
||||
/// Format: `[0x02][0x00][payload_len:2 LE][sender_idx:4 LE][receiver_idx:4 LE][noise_msg2:33]`
|
||||
pub fn build_msg2(sender_idx: SessionIndex, receiver_idx: SessionIndex, noise_msg2: &[u8]) -> Vec<u8> {
|
||||
debug_assert_eq!(noise_msg2.len(), HANDSHAKE_MSG2_SIZE);
|
||||
|
||||
let payload_len = (4 + 4 + noise_msg2.len()) as u16; // sender + receiver + noise
|
||||
|
||||
let mut packet = Vec::with_capacity(MSG2_WIRE_SIZE);
|
||||
packet.push(DISCRIMINATOR_MSG2);
|
||||
packet.push(CommonPrefix::ver_phase_byte(FLP_VERSION, PHASE_MSG2));
|
||||
packet.push(0x00); // flags must be zero
|
||||
packet.extend_from_slice(&payload_len.to_le_bytes());
|
||||
packet.extend_from_slice(&sender_idx.to_le_bytes());
|
||||
packet.extend_from_slice(&receiver_idx.to_le_bytes());
|
||||
packet.extend_from_slice(noise_msg2);
|
||||
packet
|
||||
}
|
||||
|
||||
/// Build the 16-byte outer header for an established frame.
|
||||
///
|
||||
/// Returns the header bytes (for use as AAD) separately from the construction.
|
||||
pub fn build_established_header(
|
||||
receiver_idx: SessionIndex,
|
||||
counter: u64,
|
||||
flags: u8,
|
||||
payload_len: u16,
|
||||
) -> [u8; ESTABLISHED_HEADER_SIZE] {
|
||||
let mut header = [0u8; ESTABLISHED_HEADER_SIZE];
|
||||
header[0] = CommonPrefix::ver_phase_byte(FLP_VERSION, PHASE_ESTABLISHED);
|
||||
header[1] = flags;
|
||||
header[2..4].copy_from_slice(&payload_len.to_le_bytes());
|
||||
header[4..8].copy_from_slice(&receiver_idx.to_le_bytes());
|
||||
header[8..16].copy_from_slice(&counter.to_le_bytes());
|
||||
header
|
||||
}
|
||||
|
||||
/// Build a wire-format encrypted frame.
|
||||
///
|
||||
/// Format: `[0x00][receiver_idx:4 LE][counter:8 LE][ciphertext+tag]`
|
||||
pub fn build_encrypted(receiver_idx: SessionIndex, counter: u64, ciphertext: &[u8]) -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(13 + ciphertext.len());
|
||||
packet.push(DISCRIMINATOR_ENCRYPTED);
|
||||
packet.extend_from_slice(&receiver_idx.to_le_bytes());
|
||||
packet.extend_from_slice(&counter.to_le_bytes());
|
||||
/// Format: `[header:16][ciphertext+tag]`
|
||||
///
|
||||
/// The header is constructed from the parameters and used as AAD during
|
||||
/// encryption. The caller should use `build_established_header` to construct
|
||||
/// the header, encrypt with it as AAD, then call this to assemble the packet.
|
||||
pub fn build_encrypted(header: &[u8; ESTABLISHED_HEADER_SIZE], ciphertext: &[u8]) -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(ESTABLISHED_HEADER_SIZE + ciphertext.len());
|
||||
packet.extend_from_slice(header);
|
||||
packet.extend_from_slice(ciphertext);
|
||||
packet
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Inner Header Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Prepend the 5-byte inner header (timestamp + msg_type) to a link message.
|
||||
///
|
||||
/// The caller provides the original plaintext starting with `[msg_type][payload...]`.
|
||||
/// This prepends `[timestamp:4 LE]` before the msg_type byte.
|
||||
pub fn prepend_inner_header(timestamp_ms: u32, plaintext: &[u8]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(4 + plaintext.len());
|
||||
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||
buf.extend_from_slice(plaintext);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Strip the 4-byte timestamp from a decrypted inner payload.
|
||||
///
|
||||
/// Returns `(timestamp, &payload_starting_at_msg_type)` or None if too short.
|
||||
pub fn strip_inner_header(plaintext: &[u8]) -> Option<(u32, &[u8])> {
|
||||
if plaintext.len() < INNER_HEADER_SIZE {
|
||||
return None;
|
||||
}
|
||||
let timestamp = u32::from_le_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
|
||||
Some((timestamp, &plaintext[4..]))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
@@ -235,36 +409,61 @@ pub fn build_encrypted(receiver_idx: SessionIndex, counter: u64, ciphertext: &[u
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_parse() {
|
||||
let data = [0x00, 0x04, 0x20, 0x00]; // ver=0, phase=0, flags=SP, payload_len=32
|
||||
let prefix = CommonPrefix::parse(&data).unwrap();
|
||||
assert_eq!(prefix.version, 0);
|
||||
assert_eq!(prefix.phase, 0);
|
||||
assert_eq!(prefix.flags, FLAG_SP);
|
||||
assert_eq!(prefix.payload_len, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_too_short() {
|
||||
assert!(CommonPrefix::parse(&[0, 0, 0]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_parse() {
|
||||
// Build a valid encrypted frame
|
||||
let receiver_idx = SessionIndex::new(0x12345678);
|
||||
let counter = 42u64;
|
||||
let ciphertext = vec![0xaa; 32]; // 16 plaintext + 16 tag
|
||||
let flags = 0u8;
|
||||
let payload_len = 32u16; // 16 plaintext + 16 tag
|
||||
let ciphertext = vec![0xaa; 48]; // payload_len + TAG_SIZE
|
||||
|
||||
let packet = build_encrypted(receiver_idx, counter, &ciphertext);
|
||||
let header = build_established_header(receiver_idx, counter, flags, payload_len);
|
||||
let packet = build_encrypted(&header, &ciphertext);
|
||||
|
||||
assert_eq!(packet.len(), 13 + 32);
|
||||
assert_eq!(packet[0], DISCRIMINATOR_ENCRYPTED);
|
||||
assert_eq!(packet.len(), ESTABLISHED_HEADER_SIZE + 48);
|
||||
assert_eq!(packet[0], 0x00); // ver=0, phase=0
|
||||
|
||||
// Parse it back
|
||||
let header = EncryptedHeader::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.receiver_idx, receiver_idx);
|
||||
assert_eq!(header.counter, 42);
|
||||
assert_eq!(header.ciphertext_offset, 13);
|
||||
assert_eq!(header.ciphertext(&packet), &ciphertext[..]);
|
||||
let parsed = EncryptedHeader::parse(&packet).expect("should parse");
|
||||
assert_eq!(parsed.receiver_idx, receiver_idx);
|
||||
assert_eq!(parsed.counter, 42);
|
||||
assert_eq!(parsed.flags, 0);
|
||||
assert_eq!(parsed.payload_len, 32);
|
||||
assert_eq!(parsed.header_bytes, header);
|
||||
assert_eq!(parsed.ciphertext(&packet), &ciphertext[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_too_short() {
|
||||
let packet = vec![0x00; 28]; // One byte too short
|
||||
let packet = vec![0x00; ENCRYPTED_MIN_SIZE - 1];
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_wrong_discriminator() {
|
||||
let mut packet = vec![0x00; 30];
|
||||
packet[0] = 0x01; // Wrong discriminator
|
||||
fn test_encrypted_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; ENCRYPTED_MIN_SIZE];
|
||||
packet[0] = 0x01; // phase 1 (msg1), not established
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_header_wrong_version() {
|
||||
let mut packet = vec![0x00; ENCRYPTED_MIN_SIZE];
|
||||
packet[0] = 0x10; // version 1, phase 0
|
||||
assert!(EncryptedHeader::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
@@ -276,27 +475,34 @@ mod tests {
|
||||
let packet = build_msg1(sender_idx, &noise_msg1);
|
||||
|
||||
assert_eq!(packet.len(), MSG1_WIRE_SIZE);
|
||||
assert_eq!(packet[0], DISCRIMINATOR_MSG1);
|
||||
assert_eq!(packet[0], 0x01); // ver=0, phase=1
|
||||
|
||||
let header = Msg1Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
assert_eq!(header.noise_msg1_offset, 5);
|
||||
assert_eq!(header.noise_msg1_offset, 8);
|
||||
assert_eq!(header.noise_msg1(&packet), &noise_msg1[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_wrong_size() {
|
||||
let packet = vec![0x01; 86]; // One byte too short
|
||||
let packet = vec![0x01; MSG1_WIRE_SIZE - 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
|
||||
let packet = vec![0x01; 88]; // One byte too long
|
||||
let packet = vec![0x01; MSG1_WIRE_SIZE + 1];
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_wrong_discriminator() {
|
||||
fn test_msg1_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG1_WIRE_SIZE];
|
||||
packet[0] = 0x02; // Wrong discriminator
|
||||
packet[0] = 0x02; // phase 2, not phase 1
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg1_header_nonzero_flags() {
|
||||
let mut packet = build_msg1(SessionIndex::new(1), &[0u8; HANDSHAKE_MSG1_SIZE]);
|
||||
packet[1] = 0x01; // flags must be zero during handshake
|
||||
assert!(Msg1Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
@@ -309,52 +515,114 @@ mod tests {
|
||||
let packet = build_msg2(sender_idx, receiver_idx, &noise_msg2);
|
||||
|
||||
assert_eq!(packet.len(), MSG2_WIRE_SIZE);
|
||||
assert_eq!(packet[0], DISCRIMINATOR_MSG2);
|
||||
assert_eq!(packet[0], 0x02); // ver=0, phase=2
|
||||
|
||||
let header = Msg2Header::parse(&packet).expect("should parse");
|
||||
assert_eq!(header.sender_idx, sender_idx);
|
||||
assert_eq!(header.receiver_idx, receiver_idx);
|
||||
assert_eq!(header.noise_msg2_offset, 9);
|
||||
assert_eq!(header.noise_msg2_offset, 12);
|
||||
assert_eq!(header.noise_msg2(&packet), &noise_msg2[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_size() {
|
||||
let packet = vec![0x02; 41]; // One byte too short
|
||||
let packet = vec![0x02; MSG2_WIRE_SIZE - 1];
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
|
||||
let packet = vec![0x02; 43]; // One byte too long
|
||||
let packet = vec![0x02; MSG2_WIRE_SIZE + 1];
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_msg2_header_wrong_discriminator() {
|
||||
fn test_msg2_header_wrong_phase() {
|
||||
let mut packet = vec![0x00; MSG2_WIRE_SIZE];
|
||||
packet[0] = 0x00; // Wrong discriminator
|
||||
packet[0] = 0x00; // phase 0, not phase 2
|
||||
assert!(Msg2Header::parse(&packet).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wire_sizes() {
|
||||
// Verify constants match spec
|
||||
assert_eq!(MSG1_WIRE_SIZE, 87); // 1 + 4 + 82
|
||||
assert_eq!(MSG2_WIRE_SIZE, 42); // 1 + 4 + 4 + 33
|
||||
assert_eq!(ENCRYPTED_MIN_SIZE, 29); // 1 + 4 + 8 + 16
|
||||
assert_eq!(MSG1_WIRE_SIZE, 90); // 4 + 4 + 82
|
||||
assert_eq!(MSG2_WIRE_SIZE, 45); // 4 + 4 + 4 + 33
|
||||
assert_eq!(ENCRYPTED_MIN_SIZE, 32); // 16 + 16
|
||||
assert_eq!(COMMON_PREFIX_SIZE, 4);
|
||||
assert_eq!(ESTABLISHED_HEADER_SIZE, 16);
|
||||
assert_eq!(INNER_HEADER_SIZE, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_indices() {
|
||||
// Test that indices survive the roundtrip correctly (endianness)
|
||||
let idx = SessionIndex::new(0xDEADBEEF);
|
||||
|
||||
let msg1 = build_msg1(idx, &[0u8; HANDSHAKE_MSG1_SIZE]);
|
||||
let parsed = Msg1Header::parse(&msg1).unwrap();
|
||||
assert_eq!(parsed.sender_idx.as_u32(), 0xDEADBEEF);
|
||||
|
||||
// Verify little-endian encoding
|
||||
assert_eq!(msg1[1], 0xEF);
|
||||
assert_eq!(msg1[2], 0xBE);
|
||||
assert_eq!(msg1[3], 0xAD);
|
||||
assert_eq!(msg1[4], 0xDE);
|
||||
// Verify little-endian encoding (sender_idx starts at offset 4)
|
||||
assert_eq!(msg1[4], 0xEF);
|
||||
assert_eq!(msg1[5], 0xBE);
|
||||
assert_eq!(msg1[6], 0xAD);
|
||||
assert_eq!(msg1[7], 0xDE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_header_prepend_strip() {
|
||||
let timestamp: u32 = 12345;
|
||||
let original = vec![0x10, 0xAA, 0xBB]; // msg_type + payload
|
||||
|
||||
let with_header = prepend_inner_header(timestamp, &original);
|
||||
assert_eq!(with_header.len(), 4 + 3); // timestamp + original
|
||||
|
||||
let (ts, rest) = strip_inner_header(&with_header).unwrap();
|
||||
assert_eq!(ts, 12345);
|
||||
assert_eq!(rest, &original[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_header_too_short() {
|
||||
assert!(strip_inner_header(&[0, 0, 0, 0]).is_none()); // needs 5 bytes minimum
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flags_byte() {
|
||||
let header = build_established_header(
|
||||
SessionIndex::new(1),
|
||||
0,
|
||||
FLAG_KEY_EPOCH | FLAG_SP,
|
||||
100,
|
||||
);
|
||||
assert_eq!(header[1], 0x05); // bits 0 and 2 set
|
||||
|
||||
let parsed = EncryptedHeader::parse(&[
|
||||
header[0], header[1], header[2], header[3],
|
||||
header[4], header[5], header[6], header[7],
|
||||
header[8], header[9], header[10], header[11],
|
||||
header[12], header[13], header[14], header[15],
|
||||
// minimum: TAG_SIZE bytes of ciphertext
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
]).unwrap();
|
||||
assert_eq!(parsed.flags & FLAG_KEY_EPOCH, FLAG_KEY_EPOCH);
|
||||
assert_eq!(parsed.flags & FLAG_CE, 0);
|
||||
assert_eq!(parsed.flags & FLAG_SP, FLAG_SP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_len_in_msg1() {
|
||||
let packet = build_msg1(SessionIndex::new(1), &[0u8; HANDSHAKE_MSG1_SIZE]);
|
||||
let prefix = CommonPrefix::parse(&packet).unwrap();
|
||||
// payload_len = sender_idx(4) + noise_msg1(82) = 86
|
||||
assert_eq!(prefix.payload_len, 86);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_len_in_msg2() {
|
||||
let packet = build_msg2(
|
||||
SessionIndex::new(1),
|
||||
SessionIndex::new(2),
|
||||
&[0u8; HANDSHAKE_MSG2_SIZE],
|
||||
);
|
||||
let prefix = CommonPrefix::parse(&packet).unwrap();
|
||||
// payload_len = sender_idx(4) + receiver_idx(4) + noise_msg2(33) = 41
|
||||
assert_eq!(prefix.payload_len, 41);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user