mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user