Remove spin bit, slim down MMP reports with extensibility header

Delete SpinBitState, FLAG_SP (FMP bit 2), and FspInnerFlags.spin_bit.
Spin bit superseded by MMP receiver report timestamp echo for RTT.
Reclaims FMP flags bit 2 and FSP inner flags bit 0.

SenderReport: 48 -> 20 bytes. Three fields: interval_packets_sent,
interval_bytes_sent, cumulative_packets_sent.

ReceiverReport: 68 -> 54 bytes. Ten decision-driving and diagnostic
fields retained; removed max_burst_loss, mean_burst_loss,
interval_packets_recv, interval_bytes_recv.

Both report types use extensibility header: repurposed 3 reserved
bytes as [format_version:1][total_length:2 LE]. Decoders skip
unknown trailing bytes for forward compatibility. Session-layer
reports updated to match.
This commit is contained in:
Johnathan Corgan
2026-04-11 08:16:01 +00:00
parent f4278f1dd7
commit 4aded9a238
14 changed files with 296 additions and 507 deletions
+9 -14
View File
@@ -1,8 +1,8 @@
//! Encrypted frame handling (hot path).
use crate::node::Node;
use crate::node::wire::{EncryptedHeader, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, strip_inner_header};
use crate::noise::NoiseError;
use crate::node::Node;
use crate::node::wire::{EncryptedHeader, strip_inner_header, FLAG_CE, FLAG_KEY_EPOCH};
use crate::transport::ReceivedPacket;
use std::time::Instant;
use tracing::{debug, info, trace, warn};
@@ -53,8 +53,8 @@ impl Node {
// Check and perform cutover in a scoped borrow.
{
let peer = self.peers.get(&node_addr).unwrap();
let k_bit_flipped =
received_k_bit != peer.current_k_bit() && peer.pending_new_session().is_some();
let k_bit_flipped = received_k_bit != peer.current_k_bit()
&& peer.pending_new_session().is_some();
if k_bit_flipped {
let display_name = self.peer_display_name(&node_addr);
@@ -70,10 +70,9 @@ impl Node {
debug_assert!(
peer.transport_id().is_some()
&& peer.our_index().is_some()
&& self.peers_by_index.contains_key(&(
peer.transport_id().unwrap(),
peer.our_index().unwrap().as_u32()
)),
&& self.peers_by_index.contains_key(
&(peer.transport_id().unwrap(), peer.our_index().unwrap().as_u32())
),
"peers_by_index should contain pre-registered new index after K-bit flip"
);
}
@@ -149,7 +148,6 @@ impl Node {
// MMP per-frame processing and statistics
let now = Instant::now();
let ce_flag = header.flags & FLAG_CE != 0;
let sp_flag = header.flags & FLAG_SP != 0;
if let Some(peer) = self.peers.get_mut(&node_addr) {
if let Some(mmp) = peer.mmp_mut() {
@@ -160,17 +158,14 @@ impl Node {
ce_flag,
now,
);
let _spin_rtt = mmp.spin_bit.rx_observe(sp_flag, header.counter, now);
}
peer.set_current_addr(packet.transport_id, packet.remote_addr.clone());
peer.link_stats_mut()
.record_recv(packet.data.len(), packet.timestamp_ms);
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, link_message, ce_flag)
.await;
self.dispatch_link_message(&node_addr, link_message, ce_flag).await;
}
/// Log a decryption failure with replay suppression.
+1 -2
View File
@@ -59,6 +59,7 @@ impl Node {
trace!(
from = %self.peer_display_name(from),
cum_pkts = sr.cumulative_packets_sent,
interval_pkts = sr.interval_packets_sent,
interval_bytes = sr.interval_bytes_sent,
"Received SenderReport"
);
@@ -290,7 +291,6 @@ impl Node {
etx = format_args!("{:.2}", m.etx),
goodput = %format_throughput(m.goodput_bps()),
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(),
"MMP link teardown"
@@ -489,7 +489,6 @@ impl Node {
send_mtu = mmp.path_mtu.current_mtu(),
observed_mtu = mmp.path_mtu.last_observed_mtu(),
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(),
"MMP session teardown"
+5 -14
View File
@@ -251,13 +251,7 @@ impl Node {
mmp.receiver.record_recv(
header.counter, timestamp, plaintext.len(), ce_flag, now,
);
// Spin bit: advance state machine for correct TX reflection.
// RTT samples not fed into SRTT — timestamp-echo provides
// accurate RTT; spin bit includes variable inter-frame delays.
let inner_flags = FspInnerFlags::from_byte(inner_flags_byte);
let _spin_rtt = mmp.spin_bit.rx_observe(
inner_flags.spin_bit, header.counter, now,
);
let _inner_flags = FspInnerFlags::from_byte(inner_flags_byte);
}
// Feed path_mtu from datagram envelope to MMP path MTU tracking.
@@ -915,6 +909,7 @@ impl Node {
trace!(
src = %self.peer_display_name(src_addr),
cum_pkts = sr.cumulative_packets_sent,
interval_pkts = sr.interval_packets_sent,
interval_bytes = sr.interval_bytes_sent,
"Received SessionSenderReport"
);
@@ -1273,7 +1268,6 @@ impl Node {
})?;
let wants_coords = entry.coords_warmup_remaining() > 0;
let timestamp = entry.session_timestamp(now_ms);
let spin_bit = entry.mmp().is_some_and(|m| m.spin_bit.tx_bit());
if !entry.is_established() {
return Err(NodeError::SendFailed {
node_addr: *dest_addr,
@@ -1289,7 +1283,7 @@ impl Node {
// Build inner plaintext (doesn't depend on counter)
let msg_type = SessionMessageType::DataPacket.to_byte(); // 0x10
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
let inner_flags = FspInnerFlags::new().to_byte();
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, &port_payload);
// Determine whether coords fit within transport MTU.
@@ -1422,10 +1416,8 @@ impl Node {
reason: "no session".into(),
})?;
let timestamp = entry.session_timestamp(now_ms);
let spin_bit = entry.mmp().is_some_and(|m| m.spin_bit.tx_bit());
// Build inner flags with spin bit
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
let inner_flags = FspInnerFlags::new().to_byte();
// Get mutable access for encryption
let entry = self.sessions.get_mut(dest_addr).ok_or_else(|| NodeError::SendFailed {
@@ -1506,7 +1498,6 @@ impl Node {
reason: "no session".into(),
})?;
let timestamp = entry.session_timestamp(now_ms);
let spin_bit = entry.mmp().is_some_and(|m| m.spin_bit.tx_bit());
// Get mutable access for encryption
let entry = self.sessions.get_mut(dest_addr).ok_or_else(|| NodeError::SendFailed {
@@ -1527,7 +1518,7 @@ impl Node {
// FSP inner header only, no body payload
let msg_type = SessionMessageType::CoordsWarmup.to_byte();
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
let inner_flags = FspInnerFlags::new().to_byte();
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, &[]);
// Build FSP header with CP flag
+2 -6
View File
@@ -40,7 +40,7 @@ use crate::tree::TreeState;
use crate::upper::hosts::HostMap;
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
use self::wire::{build_encrypted, build_established_header, prepend_inner_header, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP};
use self::wire::{build_encrypted, build_established_header, prepend_inner_header, FLAG_CE, FLAG_KEY_EPOCH};
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity};
use rand::Rng;
use std::collections::{HashMap, VecDeque};
@@ -1637,11 +1637,7 @@ impl Node {
// 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 mut flags = if sp_flag { FLAG_SP } else { 0 };
let mut flags = 0u8;
if ce_flag {
flags |= FLAG_CE;
}
-5
View File
@@ -89,11 +89,6 @@ pub const FSP_FLAG_K: u8 = 0x02;
/// Unencrypted — payload is plaintext (error signals).
pub const FSP_FLAG_U: u8 = 0x04;
// Inner flag bit constants (byte 5 of decrypted inner header).
/// Spin bit for end-to-end RTT measurement (inside AEAD).
#[allow(dead_code)]
pub const FSP_INNER_FLAG_SP: u8 = 0x01;
// ============================================================================
// Common Prefix
+5 -9
View File
@@ -71,9 +71,6 @@ 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
@@ -516,11 +513,11 @@ mod tests {
#[test]
fn test_common_prefix_parse() {
let data = [0x10, 0x04, 0x20, 0x00]; // ver=1, phase=0, flags=SP, payload_len=32
let data = [0x10, 0x02, 0x20, 0x00]; // ver=1, phase=0, flags=CE, payload_len=32
let prefix = CommonPrefix::parse(&data).unwrap();
assert_eq!(prefix.version, 1);
assert_eq!(prefix.phase, 0);
assert_eq!(prefix.flags, FLAG_SP);
assert_eq!(prefix.flags, FLAG_CE);
assert_eq!(prefix.payload_len, 32);
}
@@ -698,10 +695,10 @@ mod tests {
let header = build_established_header(
SessionIndex::new(1),
0,
FLAG_KEY_EPOCH | FLAG_SP,
FLAG_KEY_EPOCH | FLAG_CE,
100,
);
assert_eq!(header[1], 0x05); // bits 0 and 2 set
assert_eq!(header[1], 0x03); // bits 0 and 1 set
let parsed = EncryptedHeader::parse(&[
header[0], header[1], header[2], header[3],
@@ -712,8 +709,7 @@ mod tests {
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);
assert_eq!(parsed.flags & FLAG_CE, FLAG_CE);
}
#[test]