mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Add rustfmt.toml with stable defaults and apply cargo fmt to all source files. This establishes a consistent formatting baseline for CI enforcement.
1865 lines
75 KiB
Rust
1865 lines
75 KiB
Rust
//! End-to-end session message handlers.
|
|
//!
|
|
//! Handles locally-delivered session payloads from SessionDatagram envelopes.
|
|
//! Dispatches based on FSP common prefix phase to specific handlers for
|
|
//! SessionSetup (Noise XK msg1), SessionAck (msg2), SessionMsg3 (msg3),
|
|
//! encrypted data, and error signals (CoordsRequired, PathBroken).
|
|
|
|
use crate::NodeAddr;
|
|
use crate::mmp::report::ReceiverReport;
|
|
use crate::mmp::{MAX_SESSION_REPORT_INTERVAL_MS, MIN_SESSION_REPORT_INTERVAL_MS};
|
|
use crate::node::session::{EndToEndState, SessionEntry};
|
|
use crate::node::session_wire::{
|
|
FSP_COMMON_PREFIX_SIZE, FSP_FLAG_CP, FSP_FLAG_K, FSP_HEADER_SIZE, FSP_PHASE_ESTABLISHED,
|
|
FSP_PHASE_MSG1, FSP_PHASE_MSG2, FSP_PHASE_MSG3, FSP_PORT_HEADER_SIZE, FSP_PORT_IPV6_SHIM,
|
|
FspCommonPrefix, FspEncryptedHeader, build_fsp_header, fsp_prepend_inner_header,
|
|
fsp_strip_inner_header, parse_encrypted_coords,
|
|
};
|
|
use crate::node::{Node, NodeError};
|
|
use crate::noise::{
|
|
HandshakeState, XK_HANDSHAKE_MSG1_SIZE, XK_HANDSHAKE_MSG2_SIZE, XK_HANDSHAKE_MSG3_SIZE,
|
|
};
|
|
use crate::protocol::{
|
|
CoordsRequired, FspInnerFlags, MtuExceeded, PathBroken, PathMtuNotification, SessionAck,
|
|
SessionDatagram, SessionMessageType, SessionMsg3, SessionReceiverReport, SessionSenderReport,
|
|
SessionSetup,
|
|
};
|
|
use crate::protocol::{coords_wire_size, encode_coords};
|
|
use crate::upper::icmp::FIPS_OVERHEAD;
|
|
use secp256k1::PublicKey;
|
|
use tracing::{debug, info, trace};
|
|
|
|
impl Node {
|
|
/// Handle a locally-delivered session datagram payload.
|
|
///
|
|
/// Called from `handle_session_datagram()` when `dest_addr == self.node_addr()`.
|
|
/// Dispatches based on the 4-byte FSP common prefix:
|
|
///
|
|
/// - Phase 0x1 → SessionSetup (handshake msg1)
|
|
/// - Phase 0x2 → SessionAck (handshake msg2)
|
|
/// - Phase 0x3 → SessionMsg3 (XK handshake msg3)
|
|
/// - Phase 0x0 + U flag → plaintext error signal (CoordsRequired/PathBroken)
|
|
/// - Phase 0x0 + !U → encrypted session message (data, reports, etc.)
|
|
pub(in crate::node) async fn handle_session_payload(
|
|
&mut self,
|
|
src_addr: &NodeAddr,
|
|
payload: &[u8],
|
|
path_mtu: u16,
|
|
ce_flag: bool,
|
|
) {
|
|
let prefix = match FspCommonPrefix::parse(payload) {
|
|
Some(p) => p,
|
|
None => {
|
|
debug!(
|
|
len = payload.len(),
|
|
"Session payload too short for FSP prefix"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let inner = &payload[FSP_COMMON_PREFIX_SIZE..];
|
|
|
|
match prefix.phase {
|
|
FSP_PHASE_MSG1 => {
|
|
self.handle_session_setup(src_addr, inner).await;
|
|
}
|
|
FSP_PHASE_MSG2 => {
|
|
self.handle_session_ack(src_addr, inner).await;
|
|
}
|
|
FSP_PHASE_MSG3 => {
|
|
self.handle_session_msg3(src_addr, inner).await;
|
|
}
|
|
FSP_PHASE_ESTABLISHED if prefix.is_unencrypted() => {
|
|
// Plaintext error signals: read msg_type from first byte after prefix
|
|
if inner.is_empty() {
|
|
debug!("Empty plaintext error signal");
|
|
return;
|
|
}
|
|
let error_type = inner[0];
|
|
let error_body = &inner[1..];
|
|
match SessionMessageType::from_byte(error_type) {
|
|
Some(SessionMessageType::CoordsRequired) => {
|
|
self.handle_coords_required(error_body).await;
|
|
}
|
|
Some(SessionMessageType::PathBroken) => {
|
|
self.handle_path_broken(error_body).await;
|
|
}
|
|
Some(SessionMessageType::MtuExceeded) => {
|
|
self.handle_mtu_exceeded(error_body).await;
|
|
}
|
|
_ => {
|
|
debug!(error_type, "Unknown plaintext error signal type");
|
|
}
|
|
}
|
|
}
|
|
FSP_PHASE_ESTABLISHED => {
|
|
self.handle_encrypted_session_msg(src_addr, payload, path_mtu, ce_flag)
|
|
.await;
|
|
}
|
|
_ => {
|
|
debug!(phase = prefix.phase, "Unknown FSP phase");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handle an encrypted session message (phase 0x0, U flag clear).
|
|
///
|
|
/// Full FSP receive pipeline:
|
|
/// 1. Parse FspEncryptedHeader (12 bytes) → counter, flags, header_bytes
|
|
/// 2. If CP flag: parse cleartext coords, cache them
|
|
/// 3. Session lookup (must be Established)
|
|
/// 4. AEAD decrypt with AAD = header_bytes
|
|
/// 5. Strip FSP inner header → timestamp, msg_type, inner_flags
|
|
/// 6. Dispatch by msg_type
|
|
async fn handle_encrypted_session_msg(
|
|
&mut self,
|
|
src_addr: &NodeAddr,
|
|
payload: &[u8],
|
|
path_mtu: u16,
|
|
ce_flag: bool,
|
|
) {
|
|
// Parse the 12-byte encrypted header (includes the 4-byte prefix)
|
|
let header = match FspEncryptedHeader::parse(payload) {
|
|
Some(h) => h,
|
|
None => {
|
|
debug!(
|
|
len = payload.len(),
|
|
"Encrypted session message too short for FSP header"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Determine where ciphertext starts (after header, optionally after coords)
|
|
let mut ciphertext_offset = FSP_HEADER_SIZE;
|
|
|
|
// If CP flag set, parse cleartext coords between header and ciphertext
|
|
if header.has_coords() {
|
|
let coord_data = &payload[FSP_HEADER_SIZE..];
|
|
match parse_encrypted_coords(coord_data) {
|
|
Ok((src_coords, dest_coords, bytes_consumed)) => {
|
|
let now_ms = Self::now_ms();
|
|
if let Some(coords) = src_coords {
|
|
self.coord_cache.insert(*src_addr, coords, now_ms);
|
|
}
|
|
if let Some(coords) = dest_coords {
|
|
self.coord_cache.insert(*self.node_addr(), coords, now_ms);
|
|
}
|
|
ciphertext_offset += bytes_consumed;
|
|
}
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to parse coords from encrypted session message");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let ciphertext = &payload[ciphertext_offset..];
|
|
|
|
// Look up session entry — must be Established to decrypt
|
|
{
|
|
let entry = match self.sessions.get(src_addr) {
|
|
Some(e) => e,
|
|
None => {
|
|
debug!(src = %self.peer_display_name(src_addr), "Encrypted session message for unknown session");
|
|
return;
|
|
}
|
|
};
|
|
// Drop encrypted data if session is not yet established.
|
|
// With XK, the responder must wait for msg3 before it can decrypt.
|
|
if !entry.is_established() {
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"Encrypted message but session not established (awaiting handshake completion)"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// K-bit flip detection: peer has cut over to the new session.
|
|
let received_k_bit = header.flags & FSP_FLAG_K != 0;
|
|
{
|
|
let entry = self.sessions.get(src_addr).unwrap();
|
|
let k_bit_flipped =
|
|
received_k_bit != entry.current_k_bit() && entry.pending_new_session().is_some();
|
|
|
|
if k_bit_flipped {
|
|
let display_name = self.peer_display_name(src_addr);
|
|
info!(
|
|
peer = %display_name,
|
|
"Peer FSP K-bit flip detected, promoting new session"
|
|
);
|
|
let now_ms = Self::now_ms();
|
|
let entry = self.sessions.get_mut(src_addr).unwrap();
|
|
entry.handle_peer_kbit_flip(now_ms);
|
|
}
|
|
}
|
|
|
|
let mut entry = match self.sessions.remove(src_addr) {
|
|
Some(e) => e,
|
|
None => return,
|
|
};
|
|
|
|
// Decrypt with AAD = the 12-byte header
|
|
let session = match entry.state_mut() {
|
|
EndToEndState::Established(s) => s,
|
|
_ => {
|
|
debug!(src = %self.peer_display_name(src_addr), "Encrypted message but session not established");
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let plaintext = match session.decrypt_with_replay_check_and_aad(
|
|
ciphertext,
|
|
header.counter,
|
|
&header.header_bytes,
|
|
) {
|
|
Ok(pt) => pt,
|
|
Err(e) => {
|
|
// Current session failed — try previous session (drain window)
|
|
if let Some(prev_session) = entry.previous_noise_session_mut() {
|
|
match prev_session.decrypt_with_replay_check_and_aad(
|
|
ciphertext,
|
|
header.counter,
|
|
&header.header_bytes,
|
|
) {
|
|
Ok(pt) => pt,
|
|
Err(_) => {
|
|
debug!(
|
|
error = %e, src = %self.peer_display_name(src_addr), counter = header.counter,
|
|
"Session AEAD decryption failed (current and previous)"
|
|
);
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
debug!(
|
|
error = %e, src = %self.peer_display_name(src_addr), counter = header.counter,
|
|
"Session AEAD decryption failed"
|
|
);
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
self.sessions.insert(*src_addr, entry);
|
|
|
|
// Strip FSP inner header (6 bytes)
|
|
let (timestamp, msg_type, inner_flags_byte, rest) = match fsp_strip_inner_header(&plaintext)
|
|
{
|
|
Some(parts) => parts,
|
|
None => {
|
|
debug!(src = %self.peer_display_name(src_addr), "Decrypted payload too short for FSP inner header");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// MMP per-message recording on RX path
|
|
if let Some(entry) = self.sessions.get_mut(src_addr)
|
|
&& let Some(mmp) = entry.mmp_mut()
|
|
{
|
|
let now = std::time::Instant::now();
|
|
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);
|
|
}
|
|
|
|
// Feed path_mtu from datagram envelope to MMP path MTU tracking.
|
|
// Done for ALL session messages, not just DataPackets, so the
|
|
// destination learns the path MTU even when only reports flow.
|
|
if let Some(entry) = self.sessions.get_mut(src_addr)
|
|
&& let Some(mmp) = entry.mmp_mut()
|
|
{
|
|
mmp.path_mtu.observe_incoming_mtu(path_mtu);
|
|
}
|
|
|
|
// Dispatch by msg_type
|
|
match SessionMessageType::from_byte(msg_type) {
|
|
Some(SessionMessageType::DataPacket) => {
|
|
// msg_type 0x10: port-multiplexed service dispatch
|
|
if rest.len() < FSP_PORT_HEADER_SIZE {
|
|
debug!(len = rest.len(), "DataPacket too short for port header");
|
|
return;
|
|
}
|
|
let dst_port = u16::from_le_bytes([rest[2], rest[3]]);
|
|
let service_payload = &rest[FSP_PORT_HEADER_SIZE..];
|
|
|
|
match dst_port {
|
|
FSP_PORT_IPV6_SHIM => {
|
|
use crate::FipsAddress;
|
|
let src_ipv6 = FipsAddress::from_node_addr(src_addr).to_ipv6().octets();
|
|
let dst_ipv6 = FipsAddress::from_node_addr(self.node_addr())
|
|
.to_ipv6()
|
|
.octets();
|
|
|
|
match crate::upper::ipv6_shim::decompress_ipv6(
|
|
service_payload,
|
|
src_ipv6,
|
|
dst_ipv6,
|
|
) {
|
|
Some(mut packet) => {
|
|
if ce_flag {
|
|
mark_ipv6_ecn_ce(&mut packet);
|
|
self.stats_mut().congestion.record_ce_received();
|
|
}
|
|
if let Some(tun_tx) = &self.tun_tx {
|
|
if let Err(e) = tun_tx.send(packet) {
|
|
debug!(error = %e, "Failed to deliver decompressed IPv6 packet to TUN");
|
|
}
|
|
} else {
|
|
trace!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"IPv6 shim packet decompressed (no TUN interface)"
|
|
);
|
|
}
|
|
}
|
|
None => {
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
len = service_payload.len(),
|
|
"IPv6 shim decompression failed"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
dst_port,
|
|
"Unknown FSP service port, dropping DataPacket"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Some(SessionMessageType::SenderReport) => {
|
|
self.handle_session_sender_report(src_addr, rest);
|
|
}
|
|
Some(SessionMessageType::ReceiverReport) => {
|
|
self.handle_session_receiver_report(src_addr, rest);
|
|
}
|
|
Some(SessionMessageType::PathMtuNotification) => {
|
|
self.handle_session_path_mtu_notification(src_addr, rest);
|
|
}
|
|
Some(SessionMessageType::CoordsWarmup) => {
|
|
// Standalone coordinate warming — coords already extracted
|
|
// from CP flag by transit nodes. No action needed at endpoint.
|
|
trace!(src = %self.peer_display_name(src_addr), "CoordsWarmup received");
|
|
}
|
|
_ => {
|
|
debug!(src = %self.peer_display_name(src_addr), msg_type, "Unknown session message type, dropping");
|
|
}
|
|
}
|
|
|
|
// Only application data resets the idle timer and traffic counters —
|
|
// MMP reports (SenderReport, ReceiverReport, PathMtuNotification) do not.
|
|
if msg_type == SessionMessageType::DataPacket.to_byte()
|
|
&& let Some(entry) = self.sessions.get_mut(src_addr)
|
|
{
|
|
entry.record_recv(rest.len());
|
|
entry.touch(Self::now_ms());
|
|
}
|
|
|
|
// Flush any pending outbound packets (e.g., simultaneous initiation
|
|
// where responder also had queued outbound packets)
|
|
self.flush_pending_packets(src_addr).await;
|
|
}
|
|
|
|
/// Handle an incoming SessionSetup (Noise XK msg1).
|
|
///
|
|
/// The remote node wants to establish an end-to-end session with us.
|
|
/// We create an XK responder handshake, process msg1, send SessionAck with msg2,
|
|
/// and transition to AwaitingMsg3.
|
|
async fn handle_session_setup(&mut self, src_addr: &NodeAddr, inner: &[u8]) {
|
|
let setup = match SessionSetup::decode(inner) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!(error = %e, "Malformed SessionSetup");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if setup.handshake_payload.len() != XK_HANDSHAKE_MSG1_SIZE {
|
|
debug!(
|
|
len = setup.handshake_payload.len(),
|
|
expected = XK_HANDSHAKE_MSG1_SIZE,
|
|
"Invalid handshake payload size in SessionSetup"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check for existing session with this remote
|
|
if let Some(existing) = self.sessions.get(src_addr) {
|
|
if existing.is_initiating() {
|
|
// Simultaneous initiation: smaller NodeAddr wins as initiator
|
|
if self.identity.node_addr() < src_addr {
|
|
// We win — drop their setup, they'll process ours
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"Simultaneous session initiation: we win (smaller addr), dropping their setup"
|
|
);
|
|
return;
|
|
}
|
|
// We lose — discard our pending handshake, become responder below
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"Simultaneous session initiation: we lose, becoming responder"
|
|
);
|
|
} else if existing.is_awaiting_msg3() {
|
|
// Duplicate setup while we already sent msg2 — resend stored ack
|
|
if let Some(payload) = existing.handshake_payload() {
|
|
debug!(src = %self.peer_display_name(src_addr), "Duplicate SessionSetup, resending SessionAck");
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *src_addr, payload.to_vec())
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
if let Err(e) = self.send_session_datagram(&mut datagram).await {
|
|
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to resend SessionAck");
|
|
}
|
|
} else {
|
|
debug!(src = %self.peer_display_name(src_addr), "Duplicate SessionSetup, no stored ack to resend");
|
|
}
|
|
return;
|
|
} else if existing.is_established() {
|
|
// Rekey: if rekey enabled, treat as rekey for key rotation.
|
|
// The existing established session remains active for traffic.
|
|
if self.config.node.rekey.enabled {
|
|
let rekey_in_progress = existing.has_rekey_in_progress();
|
|
let has_pending = existing.pending_new_session().is_some();
|
|
|
|
// Dual-initiation detection: both sides sent SessionSetup
|
|
// simultaneously. Apply tie-breaker — smaller NodeAddr
|
|
// wins as initiator (same as initial session setup).
|
|
if rekey_in_progress {
|
|
if self.identity.node_addr() < src_addr {
|
|
// We win as initiator — drop their msg1.
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"Dual FSP rekey initiation: we win (smaller addr), dropping their msg1"
|
|
);
|
|
return;
|
|
}
|
|
// We lose — abandon our rekey, become responder below.
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"Dual FSP rekey initiation: we lose (larger addr), abandoning ours"
|
|
);
|
|
let entry = self.sessions.get_mut(src_addr).unwrap();
|
|
entry.abandon_rekey();
|
|
} else if has_pending {
|
|
// Guard: already have a pending session waiting for K-bit cutover
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"FSP rekey msg1 received but already have pending session, dropping"
|
|
);
|
|
return;
|
|
}
|
|
let our_keypair = self.identity.keypair();
|
|
let mut handshake = HandshakeState::new_xk_responder(our_keypair);
|
|
handshake.set_local_epoch(self.startup_epoch);
|
|
|
|
if let Err(e) = handshake.read_xk_message_1(&setup.handshake_payload) {
|
|
debug!(error = %e, "Failed to process rekey XK msg1");
|
|
return;
|
|
}
|
|
|
|
// Generate msg2
|
|
let msg2 = match handshake.write_xk_message_2() {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to generate rekey XK msg2");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Build and send SessionAck
|
|
let our_coords = self.tree_state.my_coords().clone();
|
|
let ack = SessionAck::new(our_coords, setup.src_coords).with_handshake(msg2);
|
|
let ack_payload = ack.encode();
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *src_addr, ack_payload)
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
if let Err(e) = self.send_session_datagram(&mut datagram).await {
|
|
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send rekey SessionAck");
|
|
return;
|
|
}
|
|
|
|
// Store rekey state on the existing entry
|
|
let now_ms = Self::now_ms();
|
|
let entry = self.sessions.get_mut(src_addr).unwrap();
|
|
entry.set_rekey_state(handshake, false);
|
|
entry.record_peer_rekey(now_ms);
|
|
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"FSP rekey: processed peer's msg1, sent msg2, awaiting msg3"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Re-establishment: replace existing session below
|
|
debug!(src = %self.peer_display_name(src_addr), "Session re-establishment from peer");
|
|
}
|
|
}
|
|
|
|
// Create XK responder handshake and process msg1
|
|
let our_keypair = self.identity.keypair();
|
|
let mut handshake = HandshakeState::new_xk_responder(our_keypair);
|
|
handshake.set_local_epoch(self.startup_epoch);
|
|
|
|
if let Err(e) = handshake.read_xk_message_1(&setup.handshake_payload) {
|
|
debug!(error = %e, "Failed to process Noise XK msg1 in SessionSetup");
|
|
return;
|
|
}
|
|
|
|
// XK: responder does NOT learn initiator's identity until msg3
|
|
// Use a placeholder pubkey from src_addr for the session entry.
|
|
// The real pubkey will be registered when msg3 arrives.
|
|
|
|
// Generate msg2
|
|
let msg2 = match handshake.write_xk_message_2() {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to generate Noise XK msg2 for SessionAck");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Build and send SessionAck (include initiator's coords for return-path warming)
|
|
let our_coords = self.tree_state.my_coords().clone();
|
|
let ack = SessionAck::new(our_coords, setup.src_coords).with_handshake(msg2);
|
|
let ack_payload = ack.encode();
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *src_addr, ack_payload.clone())
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
// Route the ack back to the initiator
|
|
if let Err(e) = self.send_session_datagram(&mut datagram).await {
|
|
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send SessionAck");
|
|
return;
|
|
}
|
|
|
|
// Store session entry in AwaitingMsg3 state with ack payload for potential resend.
|
|
// Use a dummy pubkey since we don't know the initiator's identity yet.
|
|
// We use our own pubkey as placeholder; it will be replaced in handle_session_msg3.
|
|
let placeholder_pubkey = self.identity.keypair().public_key();
|
|
let now_ms = Self::now_ms();
|
|
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
|
|
let mut entry = SessionEntry::new(
|
|
*src_addr,
|
|
placeholder_pubkey,
|
|
EndToEndState::AwaitingMsg3(handshake),
|
|
now_ms,
|
|
false,
|
|
);
|
|
entry.set_handshake_payload(ack_payload, now_ms + resend_interval);
|
|
self.sessions.insert(*src_addr, entry);
|
|
|
|
debug!(src = %self.peer_display_name(src_addr), "SessionSetup processed (XK), SessionAck sent, awaiting msg3");
|
|
}
|
|
|
|
/// Handle an incoming SessionAck (Noise XK msg2).
|
|
///
|
|
/// Processes msg2, generates and sends msg3, then transitions to Established.
|
|
async fn handle_session_ack(&mut self, src_addr: &NodeAddr, inner: &[u8]) {
|
|
let ack = match SessionAck::decode(inner) {
|
|
Ok(a) => a,
|
|
Err(e) => {
|
|
debug!(error = %e, "Malformed SessionAck");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if ack.handshake_payload.len() != XK_HANDSHAKE_MSG2_SIZE {
|
|
debug!(
|
|
len = ack.handshake_payload.len(),
|
|
expected = XK_HANDSHAKE_MSG2_SIZE,
|
|
"Invalid handshake payload size in SessionAck"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Remove the entry to take ownership of the handshake state
|
|
let mut entry = match self.sessions.remove(src_addr) {
|
|
Some(e) => e,
|
|
None => {
|
|
debug!(src = %self.peer_display_name(src_addr), "SessionAck for unknown session");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Rekey path: entry is Established with rekey_state
|
|
if entry.is_established() && entry.has_rekey_in_progress() && entry.is_rekey_initiator() {
|
|
let mut handshake = match entry.take_rekey_state() {
|
|
Some(hs) => hs,
|
|
None => {
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Process XK msg2
|
|
if let Err(e) = handshake.read_xk_message_2(&ack.handshake_payload) {
|
|
debug!(error = %e, "Failed to process rekey XK msg2");
|
|
entry.abandon_rekey();
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
|
|
// Generate XK msg3
|
|
let msg3 = match handshake.write_xk_message_3() {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to generate rekey XK msg3");
|
|
entry.abandon_rekey();
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Send SessionMsg3
|
|
let msg3_wire = SessionMsg3::new(msg3);
|
|
let msg3_payload = msg3_wire.encode();
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *src_addr, msg3_payload)
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
if let Err(e) = self.send_session_datagram(&mut datagram).await {
|
|
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send rekey SessionMsg3");
|
|
entry.abandon_rekey();
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
|
|
// Complete handshake → store as pending new session
|
|
let session = match handshake.into_session() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to create session from rekey XK");
|
|
entry.abandon_rekey();
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
};
|
|
|
|
entry.set_pending_session(session);
|
|
entry.set_rekey_completed_ms(Self::now_ms());
|
|
self.sessions.insert(*src_addr, entry);
|
|
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"FSP rekey: completed XK as initiator, pending cutover"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Must be in Initiating state — check before take to avoid poisoning
|
|
if !entry.is_initiating() {
|
|
debug!(src = %self.peer_display_name(src_addr), "SessionAck but session not in Initiating state");
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
let mut handshake = match entry.take_state() {
|
|
Some(EndToEndState::Initiating(hs)) => hs,
|
|
_ => unreachable!("checked is_initiating above"),
|
|
};
|
|
|
|
// Process XK msg2: read_xk_message_2 (extracts responder's epoch)
|
|
if let Err(e) = handshake.read_xk_message_2(&ack.handshake_payload) {
|
|
debug!(error = %e, "Failed to process Noise XK msg2 in SessionAck");
|
|
return; // Entry was already removed, don't put back a broken session
|
|
}
|
|
|
|
// Generate XK msg3: write_xk_message_3 (sends encrypted static + epoch)
|
|
let msg3 = match handshake.write_xk_message_3() {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to generate Noise XK msg3");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Send SessionMsg3 (phase 0x3)
|
|
let msg3_wire = SessionMsg3::new(msg3);
|
|
let msg3_payload = msg3_wire.encode();
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *src_addr, msg3_payload)
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
if let Err(e) = self.send_session_datagram(&mut datagram).await {
|
|
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send SessionMsg3");
|
|
return;
|
|
}
|
|
|
|
// Complete the handshake: into_session()
|
|
let session = match handshake.into_session() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to create session after XK msg3");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let now_ms = Self::now_ms();
|
|
entry.set_state(EndToEndState::Established(session));
|
|
entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
|
|
entry.mark_established(now_ms);
|
|
entry.init_mmp(&self.config.node.session_mmp);
|
|
entry.clear_handshake_payload();
|
|
entry.touch(now_ms);
|
|
self.sessions.insert(*src_addr, entry);
|
|
self.coord_cache.insert(*src_addr, ack.src_coords, now_ms);
|
|
|
|
// Flush any queued outbound packets for this destination
|
|
self.flush_pending_packets(src_addr).await;
|
|
|
|
info!(src = %self.peer_display_name(src_addr), "Session established (initiator, XK)");
|
|
}
|
|
|
|
/// Handle an incoming SessionMsg3 (Noise XK msg3).
|
|
///
|
|
/// The initiator reveals their encrypted static key. The responder
|
|
/// processes msg3, learns the initiator's identity, and transitions
|
|
/// to Established.
|
|
async fn handle_session_msg3(&mut self, src_addr: &NodeAddr, inner: &[u8]) {
|
|
let msg3 = match SessionMsg3::decode(inner) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Malformed SessionMsg3");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if msg3.handshake_payload.len() != XK_HANDSHAKE_MSG3_SIZE {
|
|
debug!(
|
|
len = msg3.handshake_payload.len(),
|
|
expected = XK_HANDSHAKE_MSG3_SIZE,
|
|
"Invalid handshake payload size in SessionMsg3"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Remove the entry to take ownership of the handshake state
|
|
let mut entry = match self.sessions.remove(src_addr) {
|
|
Some(e) => e,
|
|
None => {
|
|
debug!(src = %self.peer_display_name(src_addr), "SessionMsg3 for unknown session");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Rekey path: entry is Established with rekey_state (responder side)
|
|
if entry.is_established() && entry.has_rekey_in_progress() && !entry.is_rekey_initiator() {
|
|
let mut handshake = match entry.take_rekey_state() {
|
|
Some(hs) => hs,
|
|
None => {
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Process XK msg3
|
|
if let Err(e) = handshake.read_xk_message_3(&msg3.handshake_payload) {
|
|
debug!(error = %e, "Failed to process rekey XK msg3");
|
|
entry.abandon_rekey();
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
|
|
// Complete the handshake → store as pending new session
|
|
let session = match handshake.into_session() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to create session from rekey XK msg3");
|
|
entry.abandon_rekey();
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
};
|
|
|
|
entry.set_pending_session(session);
|
|
self.sessions.insert(*src_addr, entry);
|
|
|
|
debug!(
|
|
src = %self.peer_display_name(src_addr),
|
|
"FSP rekey: completed XK as responder, pending cutover"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Must be in AwaitingMsg3 state
|
|
if !entry.is_awaiting_msg3() {
|
|
debug!(src = %self.peer_display_name(src_addr), "SessionMsg3 but session not in AwaitingMsg3 state");
|
|
self.sessions.insert(*src_addr, entry);
|
|
return;
|
|
}
|
|
let mut handshake = match entry.take_state() {
|
|
Some(EndToEndState::AwaitingMsg3(hs)) => hs,
|
|
_ => unreachable!("checked is_awaiting_msg3 above"),
|
|
};
|
|
|
|
// Process XK msg3: read_xk_message_3 (extracts initiator's static key and epoch)
|
|
if let Err(e) = handshake.read_xk_message_3(&msg3.handshake_payload) {
|
|
debug!(error = %e, "Failed to process Noise XK msg3");
|
|
return; // Entry was already removed
|
|
}
|
|
|
|
// Extract the initiator's static public key (now available after msg3)
|
|
let remote_pubkey = match handshake.remote_static() {
|
|
Some(pk) => *pk,
|
|
None => {
|
|
debug!("No remote static key after processing XK msg3");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Register the initiator's identity for future TUN → session routing
|
|
self.register_identity(*src_addr, remote_pubkey);
|
|
|
|
// Complete the handshake
|
|
let session = match handshake.into_session() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!(error = %e, "Failed to create session from XK handshake");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let now_ms = Self::now_ms();
|
|
// Replace the placeholder pubkey with the real one
|
|
let mut new_entry = SessionEntry::new(
|
|
*src_addr,
|
|
remote_pubkey,
|
|
EndToEndState::Established(session),
|
|
now_ms,
|
|
false,
|
|
);
|
|
new_entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
|
|
new_entry.mark_established(now_ms);
|
|
new_entry.init_mmp(&self.config.node.session_mmp);
|
|
new_entry.touch(now_ms);
|
|
self.sessions.insert(*src_addr, new_entry);
|
|
|
|
// Flush any pending packets
|
|
self.flush_pending_packets(src_addr).await;
|
|
|
|
info!(src = %self.peer_display_name(src_addr), "Session established (responder, XK)");
|
|
}
|
|
|
|
// === Session-layer MMP report handlers ===
|
|
|
|
/// Handle an incoming session-layer SenderReport (msg_type 0x11).
|
|
///
|
|
/// Informational only — the peer is telling us about what they sent.
|
|
/// Logged but not used for metrics (same pattern as link-layer).
|
|
fn handle_session_sender_report(&mut self, src_addr: &NodeAddr, body: &[u8]) {
|
|
let sr = match SessionSenderReport::decode(body) {
|
|
Ok(sr) => sr,
|
|
Err(e) => {
|
|
debug!(src = %self.peer_display_name(src_addr), error = %e, "Malformed SessionSenderReport");
|
|
return;
|
|
}
|
|
};
|
|
|
|
trace!(
|
|
src = %self.peer_display_name(src_addr),
|
|
cum_pkts = sr.cumulative_packets_sent,
|
|
interval_bytes = sr.interval_bytes_sent,
|
|
"Received SessionSenderReport"
|
|
);
|
|
}
|
|
|
|
/// Handle an incoming session-layer ReceiverReport (msg_type 0x12).
|
|
///
|
|
/// 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.
|
|
fn handle_session_receiver_report(&mut self, src_addr: &NodeAddr, body: &[u8]) {
|
|
let session_rr = match SessionReceiverReport::decode(body) {
|
|
Ok(rr) => rr,
|
|
Err(e) => {
|
|
debug!(src = %self.peer_display_name(src_addr), error = %e, "Malformed SessionReceiverReport");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Convert to link-layer ReceiverReport for MmpMetrics processing
|
|
let rr: ReceiverReport = ReceiverReport::from(&session_rr);
|
|
|
|
let now_ms = Self::now_ms();
|
|
let peer_name = self.peer_display_name(src_addr);
|
|
let entry = match self.sessions.get_mut(src_addr) {
|
|
Some(e) => e,
|
|
None => {
|
|
debug!(src = %peer_name, "SessionReceiverReport for unknown session");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let our_timestamp_ms = entry.session_timestamp(now_ms);
|
|
|
|
let Some(mmp) = entry.mmp_mut() else {
|
|
return;
|
|
};
|
|
|
|
let now = std::time::Instant::now();
|
|
mmp.metrics
|
|
.process_receiver_report(&rr, our_timestamp_ms, now);
|
|
|
|
// Feed SRTT back to sender/receiver report interval tuning (session-layer bounds)
|
|
if let Some(srtt_ms) = mmp.metrics.srtt_ms() {
|
|
let srtt_us = (srtt_ms * 1000.0) as i64;
|
|
mmp.sender.update_report_interval_with_bounds(
|
|
srtt_us,
|
|
MIN_SESSION_REPORT_INTERVAL_MS,
|
|
MAX_SESSION_REPORT_INTERVAL_MS,
|
|
);
|
|
mmp.receiver.update_report_interval_with_bounds(
|
|
srtt_us,
|
|
MIN_SESSION_REPORT_INTERVAL_MS,
|
|
MAX_SESSION_REPORT_INTERVAL_MS,
|
|
);
|
|
// Also update PathMtu notification interval from SRTT
|
|
mmp.path_mtu.update_interval_from_srtt(srtt_ms);
|
|
}
|
|
|
|
// Update reverse delivery ratio from our own receiver state, using per-interval deltas.
|
|
let our_recv_packets = mmp.receiver.cumulative_packets_recv();
|
|
let peer_highest = mmp.receiver.highest_counter();
|
|
mmp.metrics
|
|
.update_reverse_delivery(our_recv_packets, peer_highest);
|
|
|
|
trace!(
|
|
src = %peer_name,
|
|
rtt_ms = ?mmp.metrics.srtt_ms(),
|
|
loss = format_args!("{:.1}%", mmp.metrics.loss_rate() * 100.0),
|
|
"Processed SessionReceiverReport"
|
|
);
|
|
}
|
|
|
|
/// Handle an incoming PathMtuNotification (msg_type 0x13).
|
|
///
|
|
/// The destination is telling us the path MTU has changed.
|
|
/// Apply source-side rules (decrease immediate, increase validated).
|
|
fn handle_session_path_mtu_notification(&mut self, src_addr: &NodeAddr, body: &[u8]) {
|
|
let notif = match PathMtuNotification::decode(body) {
|
|
Ok(n) => n,
|
|
Err(e) => {
|
|
debug!(src = %self.peer_display_name(src_addr), error = %e, "Malformed PathMtuNotification");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let peer_name = self.peer_display_name(src_addr);
|
|
let entry = match self.sessions.get_mut(src_addr) {
|
|
Some(e) => e,
|
|
None => {
|
|
debug!(src = %peer_name, "PathMtuNotification for unknown session");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let Some(mmp) = entry.mmp_mut() else {
|
|
return;
|
|
};
|
|
|
|
let old_mtu = mmp.path_mtu.current_mtu();
|
|
let now = std::time::Instant::now();
|
|
mmp.path_mtu.apply_notification(notif.path_mtu, now);
|
|
let new_mtu = mmp.path_mtu.current_mtu();
|
|
|
|
if new_mtu != old_mtu {
|
|
debug!(
|
|
src = %peer_name,
|
|
old_mtu,
|
|
new_mtu,
|
|
"Path MTU changed via notification"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handle a CoordsRequired error signal from a transit router.
|
|
///
|
|
/// The router couldn't route our packet because it lacks cached
|
|
/// coordinates for the destination. Send a standalone CoordsWarmup
|
|
/// immediately (rate-limited), trigger discovery, and reset the
|
|
/// warmup counter for subsequent data packets.
|
|
async fn handle_coords_required(&mut self, inner: &[u8]) {
|
|
self.stats_mut().errors.coords_required += 1;
|
|
|
|
let msg = match CoordsRequired::decode(inner) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Malformed CoordsRequired");
|
|
return;
|
|
}
|
|
};
|
|
|
|
debug!(
|
|
dest = %msg.dest_addr,
|
|
reporter = %msg.reporter,
|
|
"CoordsRequired: transit router needs coordinates"
|
|
);
|
|
|
|
// Send standalone CoordsWarmup immediately (rate-limited)
|
|
if self
|
|
.coords_response_rate_limiter
|
|
.should_send(&msg.dest_addr)
|
|
{
|
|
if let Some(entry) = self.sessions.get(&msg.dest_addr)
|
|
&& entry.is_established()
|
|
&& let Err(e) = self.send_coords_warmup(&msg.dest_addr).await
|
|
{
|
|
debug!(dest = %msg.dest_addr, error = %e,
|
|
"Failed to send CoordsWarmup in response to CoordsRequired");
|
|
}
|
|
} else {
|
|
trace!(dest = %msg.dest_addr,
|
|
"CoordsRequired response rate-limited, skipping standalone CoordsWarmup");
|
|
}
|
|
|
|
// Only trigger discovery if we have the target's identity cached —
|
|
// otherwise we can't verify the LookupResponse proof.
|
|
if self.has_cached_identity(&msg.dest_addr) {
|
|
self.maybe_initiate_lookup(&msg.dest_addr).await;
|
|
} else {
|
|
debug!(dest = %msg.dest_addr,
|
|
"Skipping discovery after CoordsRequired: no cached identity for target");
|
|
}
|
|
|
|
// Reset coords warmup counter so the next N packets also include
|
|
// COORDS_PRESENT, re-warming transit caches along the path.
|
|
if let Some(entry) = self.sessions.get_mut(&msg.dest_addr) {
|
|
let n = self.config.node.session.coords_warmup_packets;
|
|
entry.set_coords_warmup_remaining(n);
|
|
debug!(
|
|
dest = %msg.dest_addr,
|
|
warmup_packets = n,
|
|
"Reset coords warmup counter after CoordsRequired"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handle a PathBroken error signal from a transit router.
|
|
///
|
|
/// The router has coordinates but still can't route to the destination.
|
|
/// Send a standalone CoordsWarmup immediately (rate-limited), invalidate
|
|
/// cached coordinates, trigger re-discovery, and reset the warmup counter.
|
|
async fn handle_path_broken(&mut self, inner: &[u8]) {
|
|
self.stats_mut().errors.path_broken += 1;
|
|
|
|
let msg = match PathBroken::decode(inner) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Malformed PathBroken");
|
|
return;
|
|
}
|
|
};
|
|
|
|
debug!(
|
|
dest = %msg.dest_addr,
|
|
reporter = %msg.reporter,
|
|
"PathBroken: transit router reports routing failure"
|
|
);
|
|
|
|
// Send standalone CoordsWarmup immediately (rate-limited)
|
|
if self
|
|
.coords_response_rate_limiter
|
|
.should_send(&msg.dest_addr)
|
|
{
|
|
if let Some(entry) = self.sessions.get(&msg.dest_addr)
|
|
&& entry.is_established()
|
|
&& let Err(e) = self.send_coords_warmup(&msg.dest_addr).await
|
|
{
|
|
debug!(dest = %msg.dest_addr, error = %e,
|
|
"Failed to send CoordsWarmup in response to PathBroken");
|
|
}
|
|
} else {
|
|
trace!(dest = %msg.dest_addr,
|
|
"PathBroken response rate-limited, skipping standalone CoordsWarmup");
|
|
}
|
|
|
|
// Invalidate stale cached coordinates
|
|
self.coord_cache.remove(&msg.dest_addr);
|
|
|
|
// Trigger re-discovery to get fresh coordinates, but only if we have
|
|
// the target's identity cached — otherwise we can't verify the
|
|
// LookupResponse proof. This avoids a race when the XK responder
|
|
// receives PathBroken before msg3 completes (identity unknown).
|
|
if self.has_cached_identity(&msg.dest_addr) {
|
|
self.maybe_initiate_lookup(&msg.dest_addr).await;
|
|
} else {
|
|
debug!(dest = %msg.dest_addr,
|
|
"Skipping discovery after PathBroken: no cached identity for target");
|
|
}
|
|
|
|
// Reset coords warmup counter so the next N packets include
|
|
// COORDS_PRESENT, re-warming transit caches along the new path.
|
|
if let Some(entry) = self.sessions.get_mut(&msg.dest_addr) {
|
|
let n = self.config.node.session.coords_warmup_packets;
|
|
entry.set_coords_warmup_remaining(n);
|
|
debug!(
|
|
dest = %msg.dest_addr,
|
|
warmup_packets = n,
|
|
"Reset coords warmup counter after PathBroken"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handle an MtuExceeded error signal from a transit router.
|
|
///
|
|
/// A transit router couldn't forward our packet because it exceeded the
|
|
/// next-hop transport MTU. Apply the reported bottleneck MTU to our
|
|
/// PathMtuState for the affected session, causing an immediate decrease.
|
|
async fn handle_mtu_exceeded(&mut self, inner: &[u8]) {
|
|
self.stats_mut().errors.mtu_exceeded += 1;
|
|
|
|
let msg = match MtuExceeded::decode(inner) {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
debug!(error = %e, "Malformed MtuExceeded");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let peer_name = self.peer_display_name(&msg.dest_addr);
|
|
debug!(
|
|
dest = %peer_name,
|
|
reporter = %msg.reporter,
|
|
bottleneck_mtu = msg.mtu,
|
|
"MtuExceeded: transit router reports oversized packet"
|
|
);
|
|
|
|
// Apply to PathMtuState: immediate decrease via apply_notification()
|
|
if let Some(entry) = self.sessions.get_mut(&msg.dest_addr)
|
|
&& let Some(mmp) = entry.mmp_mut()
|
|
{
|
|
let old_mtu = mmp.path_mtu.current_mtu();
|
|
let now = std::time::Instant::now();
|
|
if mmp.path_mtu.apply_notification(msg.mtu, now) {
|
|
let new_mtu = mmp.path_mtu.current_mtu();
|
|
info!(
|
|
dest = %peer_name,
|
|
old_mtu,
|
|
new_mtu,
|
|
reporter = %msg.reporter,
|
|
"Path MTU decreased via reactive MtuExceeded signal"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Session Initiation (Send Path) ===
|
|
|
|
/// Initiate an end-to-end session with a remote node.
|
|
///
|
|
/// Creates a Noise XK handshake as initiator, wraps msg1 in a
|
|
/// SessionSetup, encapsulates in a SessionDatagram, and routes
|
|
/// toward the destination.
|
|
pub(in crate::node) async fn initiate_session(
|
|
&mut self,
|
|
dest_addr: NodeAddr,
|
|
dest_pubkey: PublicKey,
|
|
) -> Result<(), NodeError> {
|
|
// Check for existing session
|
|
if let Some(existing) = self.sessions.get(&dest_addr)
|
|
&& (existing.is_established() || existing.is_initiating())
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
// Create Noise XK initiator handshake
|
|
let our_keypair = self.identity.keypair();
|
|
let mut handshake = HandshakeState::new_xk_initiator(our_keypair, dest_pubkey);
|
|
handshake.set_local_epoch(self.startup_epoch);
|
|
let msg1 = handshake
|
|
.write_xk_message_1()
|
|
.map_err(|e| NodeError::SendFailed {
|
|
node_addr: dest_addr,
|
|
reason: format!("Noise XK msg1 generation failed: {}", e),
|
|
})?;
|
|
|
|
// Build SessionSetup with coordinates
|
|
let our_coords = self.tree_state.my_coords().clone();
|
|
let dest_coords = self.get_dest_coords(&dest_addr);
|
|
let setup = SessionSetup::new(our_coords, dest_coords).with_handshake(msg1);
|
|
let setup_payload = setup.encode();
|
|
|
|
// Wrap in SessionDatagram
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, dest_addr, setup_payload.clone())
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
// Route toward destination
|
|
self.send_session_datagram(&mut datagram).await?;
|
|
|
|
// Register destination identity for TUN → session routing
|
|
self.register_identity(dest_addr, dest_pubkey);
|
|
|
|
// Store session entry with handshake payload for potential resend
|
|
let now_ms = Self::now_ms();
|
|
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
|
|
let mut entry = SessionEntry::new(
|
|
dest_addr,
|
|
dest_pubkey,
|
|
EndToEndState::Initiating(handshake),
|
|
now_ms,
|
|
true,
|
|
);
|
|
entry.set_handshake_payload(setup_payload, now_ms + resend_interval);
|
|
self.sessions.insert(dest_addr, entry);
|
|
|
|
info!(dest = %self.peer_display_name(&dest_addr), "Session initiation started");
|
|
Ok(())
|
|
}
|
|
|
|
/// Send application data over an established session.
|
|
///
|
|
/// Uses the FSP pipeline: builds a 12-byte cleartext header (used as AAD),
|
|
/// prepends the 6-byte inner header to the plaintext, encrypts with AAD,
|
|
/// optionally inserts cleartext coords, and wraps in a SessionDatagram.
|
|
///
|
|
/// The `src_port` and `dst_port` identify the service. A 4-byte port header
|
|
/// `[src_port:2 LE][dst_port:2 LE]` is prepended to `payload` inside the
|
|
/// AEAD envelope. The receiver dispatches by `dst_port`.
|
|
pub(in crate::node) async fn send_session_data(
|
|
&mut self,
|
|
dest_addr: &NodeAddr,
|
|
src_port: u16,
|
|
dst_port: u16,
|
|
payload: &[u8],
|
|
) -> Result<(), NodeError> {
|
|
let now_ms = Self::now_ms();
|
|
|
|
// First borrow: read session metadata (NLL releases before coord decision)
|
|
let entry = self
|
|
.sessions
|
|
.get(dest_addr)
|
|
.ok_or_else(|| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "no session".into(),
|
|
})?;
|
|
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,
|
|
reason: "session not established".into(),
|
|
});
|
|
}
|
|
|
|
// Build port-prefixed plaintext: [src_port:2 LE][dst_port:2 LE][payload...]
|
|
let mut port_payload = Vec::with_capacity(FSP_PORT_HEADER_SIZE + payload.len());
|
|
port_payload.extend_from_slice(&src_port.to_le_bytes());
|
|
port_payload.extend_from_slice(&dst_port.to_le_bytes());
|
|
port_payload.extend_from_slice(payload);
|
|
|
|
// 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_plaintext =
|
|
fsp_prepend_inner_header(timestamp, msg_type, inner_flags, &port_payload);
|
|
|
|
// Determine whether coords fit within transport MTU.
|
|
// If not, send standalone CoordsWarmup before the data packet.
|
|
let (include_coords, my_coords, dest_coords) = if wants_coords {
|
|
let src = self.tree_state.my_coords().clone();
|
|
let dst = self.get_dest_coords(dest_addr);
|
|
let coords_size = coords_wire_size(&src) + coords_wire_size(&dst);
|
|
let total_wire =
|
|
FIPS_OVERHEAD as usize + FSP_PORT_HEADER_SIZE + coords_size + payload.len();
|
|
if total_wire <= self.transport_mtu() as usize {
|
|
(true, Some(src), Some(dst))
|
|
} else {
|
|
// Coords don't fit piggybacked — send standalone CoordsWarmup first
|
|
if let Err(e) = self.send_coords_warmup(dest_addr).await {
|
|
debug!(dest = %self.peer_display_name(dest_addr), error = %e,
|
|
"Failed to send standalone CoordsWarmup before data packet");
|
|
}
|
|
(false, None, None)
|
|
}
|
|
} else {
|
|
(false, None, None)
|
|
};
|
|
|
|
// Decrement warmup counter if we sent coords (piggybacked or standalone)
|
|
if wants_coords && let Some(entry) = self.sessions.get_mut(dest_addr) {
|
|
entry.set_coords_warmup_remaining(entry.coords_warmup_remaining() - 1);
|
|
}
|
|
|
|
// Build FSP flags (CP flag if coords, K-bit for key epoch)
|
|
let mut flags = if include_coords { FSP_FLAG_CP } else { 0 };
|
|
if let Some(entry) = self.sessions.get(dest_addr)
|
|
&& entry.current_k_bit()
|
|
{
|
|
flags |= FSP_FLAG_K;
|
|
}
|
|
|
|
// Borrow session for counter + encryption (after potential standalone send)
|
|
let entry = self
|
|
.sessions
|
|
.get_mut(dest_addr)
|
|
.ok_or_else(|| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "no session".into(),
|
|
})?;
|
|
let session = match entry.state_mut() {
|
|
EndToEndState::Established(s) => s,
|
|
_ => {
|
|
return Err(NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "session not established".into(),
|
|
});
|
|
}
|
|
};
|
|
let counter = session.current_send_counter();
|
|
|
|
// Build 12-byte FSP header (used as AAD for AEAD)
|
|
let payload_len = inner_plaintext.len() as u16;
|
|
let header = build_fsp_header(counter, flags, payload_len);
|
|
|
|
// Encrypt with AAD binding to the FSP header
|
|
let ciphertext = session
|
|
.encrypt_with_aad(&inner_plaintext, &header)
|
|
.map_err(|e| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: format!("session encrypt failed: {}", e),
|
|
})?;
|
|
|
|
// Assemble: header(12) + [coords] + ciphertext
|
|
let mut fsp_payload = Vec::with_capacity(FSP_HEADER_SIZE + ciphertext.len() + 200);
|
|
fsp_payload.extend_from_slice(&header);
|
|
if let (Some(src), Some(dst)) = (&my_coords, &dest_coords) {
|
|
encode_coords(src, &mut fsp_payload);
|
|
encode_coords(dst, &mut fsp_payload);
|
|
}
|
|
fsp_payload.extend_from_slice(&ciphertext);
|
|
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
self.send_session_datagram(&mut datagram).await?;
|
|
|
|
// Re-borrow after send (which borrowed &mut self)
|
|
if let Some(entry) = self.sessions.get_mut(dest_addr) {
|
|
entry.record_sent(payload.len());
|
|
if let Some(mmp) = entry.mmp_mut() {
|
|
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
|
|
}
|
|
entry.touch(now_ms);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Send an IPv6 packet through the IPv6 shim (port 256) with header compression.
|
|
///
|
|
/// Compresses the IPv6 header (format 0x00), then sends via `send_session_data`
|
|
/// with `src_port=256, dst_port=256`.
|
|
pub(in crate::node) async fn send_ipv6_packet(
|
|
&mut self,
|
|
dest_addr: &NodeAddr,
|
|
ipv6_packet: &[u8],
|
|
) -> Result<(), NodeError> {
|
|
let compressed = crate::upper::ipv6_shim::compress_ipv6(ipv6_packet).ok_or_else(|| {
|
|
NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "IPv6 header compression failed".into(),
|
|
}
|
|
})?;
|
|
self.send_session_data(
|
|
dest_addr,
|
|
FSP_PORT_IPV6_SHIM,
|
|
FSP_PORT_IPV6_SHIM,
|
|
&compressed,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Send a non-data session message (reports, notifications) over an established session.
|
|
///
|
|
/// Similar to `send_session_data()` but:
|
|
/// - Takes an explicit `msg_type` byte (0x11, 0x12, 0x13, etc.)
|
|
/// - Never includes COORDS_PRESENT (reports are lightweight)
|
|
/// - Reads spin bit from MMP state for the inner header
|
|
/// - Records the send in MMP sender state
|
|
pub(in crate::node) async fn send_session_msg(
|
|
&mut self,
|
|
dest_addr: &NodeAddr,
|
|
msg_type: u8,
|
|
payload: &[u8],
|
|
) -> Result<(), NodeError> {
|
|
let now_ms = Self::now_ms();
|
|
|
|
// Read spin bit and session timestamp from entry
|
|
let entry = self
|
|
.sessions
|
|
.get(dest_addr)
|
|
.ok_or_else(|| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
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();
|
|
|
|
// Get mutable access for encryption
|
|
let entry = self
|
|
.sessions
|
|
.get_mut(dest_addr)
|
|
.ok_or_else(|| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "no session".into(),
|
|
})?;
|
|
|
|
// Read K-bit before mutable borrow of session state
|
|
let k_flags = if entry.current_k_bit() { FSP_FLAG_K } else { 0 };
|
|
|
|
let session = match entry.state_mut() {
|
|
EndToEndState::Established(s) => s,
|
|
_ => {
|
|
return Err(NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "session not established".into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
let counter = session.current_send_counter();
|
|
|
|
// FSP inner header + plaintext
|
|
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, payload);
|
|
|
|
// Build 12-byte FSP header (K-bit for key epoch, no CP for reports)
|
|
let payload_len = inner_plaintext.len() as u16;
|
|
let header = build_fsp_header(counter, k_flags, payload_len);
|
|
|
|
// Encrypt with AAD
|
|
let ciphertext = session
|
|
.encrypt_with_aad(&inner_plaintext, &header)
|
|
.map_err(|e| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: format!("session encrypt failed: {}", e),
|
|
})?;
|
|
|
|
// Assemble: header(12) + ciphertext (no coords)
|
|
let mut fsp_payload = Vec::with_capacity(FSP_HEADER_SIZE + ciphertext.len());
|
|
fsp_payload.extend_from_slice(&header);
|
|
fsp_payload.extend_from_slice(&ciphertext);
|
|
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
self.send_session_datagram(&mut datagram).await?;
|
|
|
|
// Record in MMP sender state (no touch — MMP reports don't reset idle timer)
|
|
if let Some(entry) = self.sessions.get_mut(dest_addr)
|
|
&& let Some(mmp) = entry.mmp_mut()
|
|
{
|
|
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Send a standalone CoordsWarmup message to warm transit node caches.
|
|
///
|
|
/// Constructs an encrypted FSP message with CP flag set and
|
|
/// msg_type=CoordsWarmup. Transit nodes extract the cleartext
|
|
/// coordinates via `try_warm_coord_cache()` (same as CP-flagged data
|
|
/// packets). The encrypted inner payload is the 6-byte inner header
|
|
/// with no application data.
|
|
async fn send_coords_warmup(&mut self, dest_addr: &NodeAddr) -> Result<(), NodeError> {
|
|
let now_ms = Self::now_ms();
|
|
|
|
let my_coords = self.tree_state.my_coords().clone();
|
|
let dest_coords = self.get_dest_coords(dest_addr);
|
|
|
|
// Read session metadata
|
|
let entry = self
|
|
.sessions
|
|
.get(dest_addr)
|
|
.ok_or_else(|| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
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 {
|
|
node_addr: *dest_addr,
|
|
reason: "no session".into(),
|
|
})?;
|
|
let session = match entry.state_mut() {
|
|
EndToEndState::Established(s) => s,
|
|
_ => {
|
|
return Err(NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: "session not established".into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
let counter = session.current_send_counter();
|
|
|
|
// FSP inner header only, no body payload
|
|
let msg_type = SessionMessageType::CoordsWarmup.to_byte();
|
|
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
|
|
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, &[]);
|
|
|
|
// Build FSP header with CP flag
|
|
let payload_len = inner_plaintext.len() as u16;
|
|
let header = build_fsp_header(counter, FSP_FLAG_CP, payload_len);
|
|
|
|
// Encrypt with AAD
|
|
let ciphertext = session
|
|
.encrypt_with_aad(&inner_plaintext, &header)
|
|
.map_err(|e| NodeError::SendFailed {
|
|
node_addr: *dest_addr,
|
|
reason: format!("session encrypt failed: {}", e),
|
|
})?;
|
|
|
|
// Assemble: header(12) + coords + ciphertext
|
|
let coords_size = coords_wire_size(&my_coords) + coords_wire_size(&dest_coords);
|
|
let mut fsp_payload = Vec::with_capacity(FSP_HEADER_SIZE + coords_size + ciphertext.len());
|
|
fsp_payload.extend_from_slice(&header);
|
|
encode_coords(&my_coords, &mut fsp_payload);
|
|
encode_coords(&dest_coords, &mut fsp_payload);
|
|
fsp_payload.extend_from_slice(&ciphertext);
|
|
|
|
let my_addr = *self.node_addr();
|
|
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
|
|
.with_ttl(self.config.node.session.default_ttl);
|
|
|
|
self.send_session_datagram(&mut datagram).await?;
|
|
|
|
// Record in MMP (infrastructure traffic — no idle timer touch)
|
|
if let Some(entry) = self.sessions.get_mut(dest_addr)
|
|
&& let Some(mmp) = entry.mmp_mut()
|
|
{
|
|
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
|
|
}
|
|
|
|
debug!(dest = %self.peer_display_name(dest_addr), "Sent standalone CoordsWarmup");
|
|
Ok(())
|
|
}
|
|
|
|
/// Route and send a SessionDatagram through the mesh.
|
|
///
|
|
/// Finds the next hop for the destination, seeds path_mtu from the
|
|
/// first-hop transport MTU, and sends as an encrypted link message.
|
|
pub(in crate::node) async fn send_session_datagram(
|
|
&mut self,
|
|
datagram: &mut SessionDatagram,
|
|
) -> Result<(), NodeError> {
|
|
let next_hop_addr = match self.find_next_hop(&datagram.dest_addr) {
|
|
Some(peer) => *peer.node_addr(),
|
|
None => {
|
|
return Err(NodeError::SendFailed {
|
|
node_addr: datagram.dest_addr,
|
|
reason: "no route to destination".into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Seed path_mtu from the first-hop transport MTU (same as forwarding path)
|
|
if let Some(peer) = self.peers.get(&next_hop_addr)
|
|
&& let Some(tid) = peer.transport_id()
|
|
&& let Some(transport) = self.transports.get(&tid)
|
|
{
|
|
if let Some(addr) = peer.current_addr() {
|
|
datagram.path_mtu = datagram.path_mtu.min(transport.link_mtu(addr));
|
|
} else {
|
|
datagram.path_mtu = datagram.path_mtu.min(transport.mtu());
|
|
}
|
|
}
|
|
|
|
// Source-side: seed our PathMtuState.current_mtu from the outbound
|
|
// transport MTU so it doesn't stay at u16::MAX until the destination
|
|
// sends a PathMtuNotification back.
|
|
if let Some(entry) = self.sessions.get_mut(&datagram.dest_addr)
|
|
&& let Some(mmp) = entry.mmp_mut()
|
|
{
|
|
mmp.path_mtu.seed_source_mtu(datagram.path_mtu);
|
|
}
|
|
|
|
let encoded = datagram.encode();
|
|
self.send_encrypted_link_message(&next_hop_addr, &encoded)
|
|
.await?;
|
|
self.stats_mut().forwarding.record_originated(encoded.len());
|
|
Ok(())
|
|
}
|
|
|
|
/// Look up destination coordinates from available caches.
|
|
///
|
|
/// Returns our own coordinates as a fallback (the SessionSetup will
|
|
/// carry src_coords for return path routing; empty dest_coords
|
|
/// would fail wire encoding since TreeCoordinate requires ≥1 entry).
|
|
pub(in crate::node) fn get_dest_coords(&self, dest: &NodeAddr) -> crate::tree::TreeCoordinate {
|
|
let now_ms = Self::now_ms();
|
|
if let Some(coords) = self.coord_cache.get(dest, now_ms) {
|
|
return coords.clone();
|
|
}
|
|
// Fallback: use our own coordinates. The SessionSetup dest_coords
|
|
// field cannot be empty (wire format requires ≥1 entry). Using our
|
|
// own coords is safe — transit routers will still cache them, and
|
|
// the destination will return its actual coords in the SessionAck.
|
|
self.tree_state.my_coords().clone()
|
|
}
|
|
|
|
/// Current Unix time in milliseconds.
|
|
pub(in crate::node) fn now_ms() -> u64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
// === TUN Outbound (Data Plane) ===
|
|
|
|
/// Handle an outbound IPv6 packet from the TUN reader.
|
|
///
|
|
/// Extracts the destination FipsAddress, looks up the NodeAddr and PublicKey
|
|
/// from the identity cache, and either sends through an established session
|
|
/// or initiates a new one (queuing the packet until established).
|
|
///
|
|
/// Also performs MTU checking: if the packet (plus FIPS overhead) exceeds
|
|
/// the transport MTU, an ICMP Packet Too Big message is sent back to the
|
|
/// source and the packet is dropped.
|
|
pub(in crate::node) async fn handle_tun_outbound(&mut self, ipv6_packet: Vec<u8>) {
|
|
// Validate IPv6 header
|
|
if ipv6_packet.len() < 40 || ipv6_packet[0] >> 4 != 6 {
|
|
return;
|
|
}
|
|
|
|
// Check if packet will fit after FIPS encapsulation
|
|
let effective_mtu = self.effective_ipv6_mtu() as usize;
|
|
if ipv6_packet.len() > effective_mtu {
|
|
self.send_icmpv6_packet_too_big(&ipv6_packet, effective_mtu as u32);
|
|
return;
|
|
}
|
|
|
|
// Extract destination FipsAddress prefix (IPv6 dest bytes 1-15)
|
|
// IPv6 header: bytes 24-39 are dest addr, so prefix = bytes 25-39
|
|
let mut prefix = [0u8; 15];
|
|
prefix.copy_from_slice(&ipv6_packet[25..40]);
|
|
|
|
// Look up in identity cache
|
|
let (dest_addr, dest_pubkey) = match self.lookup_by_fips_prefix(&prefix) {
|
|
Some((addr, pk)) => (addr, pk),
|
|
None => {
|
|
self.send_icmpv6_dest_unreachable(&ipv6_packet);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Check for established session
|
|
if let Some(entry) = self.sessions.get(&dest_addr) {
|
|
if entry.is_established() {
|
|
// Check per-destination path MTU learned from MtuExceeded signals.
|
|
// The first oversized packet is forwarded normally and triggers
|
|
// the MtuExceeded signal; subsequent packets are caught here and
|
|
// generate ICMPv6 Packet Too Big back to the application.
|
|
if let Some(mmp) = entry.mmp() {
|
|
let path_mtu = mmp.path_mtu.current_mtu();
|
|
let path_ipv6_mtu = crate::upper::icmp::effective_ipv6_mtu(path_mtu) as usize;
|
|
if path_ipv6_mtu < effective_mtu && ipv6_packet.len() > path_ipv6_mtu {
|
|
self.send_icmpv6_packet_too_big(&ipv6_packet, path_ipv6_mtu as u32);
|
|
return;
|
|
}
|
|
}
|
|
if let Err(e) = self.send_ipv6_packet(&dest_addr, &ipv6_packet).await {
|
|
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to send TUN packet via session");
|
|
}
|
|
return;
|
|
}
|
|
// Session exists but not yet established — queue the packet
|
|
self.queue_pending_packet(dest_addr, ipv6_packet);
|
|
return;
|
|
}
|
|
|
|
// No session: initiate one and queue the packet.
|
|
// If session initiation fails (no route), trigger discovery and
|
|
// queue the packet for retry when discovery completes.
|
|
if let Err(e) = self.initiate_session(dest_addr, dest_pubkey).await {
|
|
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to initiate session, trying discovery");
|
|
self.maybe_initiate_lookup(&dest_addr).await;
|
|
self.queue_pending_packet(dest_addr, ipv6_packet);
|
|
return;
|
|
}
|
|
self.queue_pending_packet(dest_addr, ipv6_packet);
|
|
}
|
|
|
|
/// Send ICMPv6 Destination Unreachable back through TUN.
|
|
pub(in crate::node) fn send_icmpv6_dest_unreachable(&self, original_packet: &[u8]) {
|
|
use crate::FipsAddress;
|
|
use crate::upper::icmp::{
|
|
DestUnreachableCode, build_dest_unreachable, should_send_icmp_error,
|
|
};
|
|
|
|
if !should_send_icmp_error(original_packet) {
|
|
return;
|
|
}
|
|
|
|
let our_ipv6 = FipsAddress::from_node_addr(self.node_addr()).to_ipv6();
|
|
if let Some(response) =
|
|
build_dest_unreachable(original_packet, DestUnreachableCode::NoRoute, our_ipv6)
|
|
&& let Some(tun_tx) = &self.tun_tx
|
|
{
|
|
let _ = tun_tx.send(response);
|
|
}
|
|
}
|
|
|
|
/// Send ICMPv6 Packet Too Big back through TUN.
|
|
///
|
|
/// Rate-limited per source address to prevent ICMP floods from
|
|
/// misconfigured applications sending repeated oversized packets.
|
|
pub(in crate::node) fn send_icmpv6_packet_too_big(&mut self, original_packet: &[u8], mtu: u32) {
|
|
use crate::upper::icmp::build_packet_too_big;
|
|
use std::net::Ipv6Addr;
|
|
|
|
// Extract source address for rate limiting
|
|
if original_packet.len() < 40 {
|
|
return;
|
|
}
|
|
let src_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[8..24]).unwrap());
|
|
|
|
// Rate limit ICMP PTB messages per source
|
|
if !self.icmp_rate_limiter.should_send(src_addr) {
|
|
debug!(
|
|
src = %src_addr,
|
|
"Rate limiting ICMP Packet Too Big"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Use the original packet's *destination* as the ICMP source so the
|
|
// kernel sees the PTB coming from a remote router, not from itself.
|
|
// Linux ignores PTBs whose source matches a local address, which
|
|
// causes a PMTUD blackhole when both src and ICMP-src are local.
|
|
let dest_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[24..40]).unwrap());
|
|
if let Some(response) = build_packet_too_big(original_packet, mtu, dest_addr)
|
|
&& let Some(tun_tx) = &self.tun_tx
|
|
{
|
|
debug!(
|
|
original_src = %src_addr,
|
|
original_dst = %dest_addr,
|
|
packet_size = original_packet.len(),
|
|
reported_mtu = mtu,
|
|
"Sending ICMP Packet Too Big"
|
|
);
|
|
let _ = tun_tx.send(response);
|
|
}
|
|
}
|
|
|
|
/// Queue a packet while waiting for session establishment.
|
|
fn queue_pending_packet(&mut self, dest_addr: NodeAddr, packet: Vec<u8>) {
|
|
// Reject if we already have too many pending destinations
|
|
let max_dests = self.config.node.session.pending_max_destinations;
|
|
if !self.pending_tun_packets.contains_key(&dest_addr)
|
|
&& self.pending_tun_packets.len() >= max_dests
|
|
{
|
|
return;
|
|
}
|
|
|
|
let queue = self.pending_tun_packets.entry(dest_addr).or_default();
|
|
if queue.len() >= self.config.node.session.pending_packets_per_dest {
|
|
queue.pop_front(); // Drop oldest
|
|
}
|
|
queue.push_back(packet);
|
|
}
|
|
|
|
/// Flush pending packets for a destination whose session just reached Established.
|
|
async fn flush_pending_packets(&mut self, dest_addr: &NodeAddr) {
|
|
let packets = match self.pending_tun_packets.remove(dest_addr) {
|
|
Some(q) => q,
|
|
None => return,
|
|
};
|
|
for packet in packets {
|
|
if let Err(e) = self.send_ipv6_packet(dest_addr, &packet).await {
|
|
debug!(dest = %self.peer_display_name(dest_addr), error = %e, "Failed to send queued TUN packet");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Retry session initiation after discovery provided coordinates.
|
|
///
|
|
/// Called when a LookupResponse arrives and we have pending TUN packets
|
|
/// for the discovered target. The coord_cache now has coords, so
|
|
/// `find_next_hop()` should succeed and the SessionSetup can be sent.
|
|
pub(in crate::node) async fn retry_session_after_discovery(&mut self, dest_addr: NodeAddr) {
|
|
// Look up the destination's public key from the identity cache
|
|
let mut prefix = [0u8; 15];
|
|
prefix.copy_from_slice(&dest_addr.as_bytes()[0..15]);
|
|
let dest_pubkey = match self.lookup_by_fips_prefix(&prefix) {
|
|
Some((_, pk)) => pk,
|
|
None => {
|
|
debug!(dest = %self.peer_display_name(&dest_addr), "Discovery complete but no identity for session retry");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Skip if a session already exists
|
|
if let Some(existing) = self.sessions.get(&dest_addr)
|
|
&& (existing.is_established() || existing.is_initiating())
|
|
{
|
|
return;
|
|
}
|
|
|
|
match self.initiate_session(dest_addr, dest_pubkey).await {
|
|
Ok(()) => {
|
|
debug!(dest = %self.peer_display_name(&dest_addr), "Session initiated after discovery");
|
|
}
|
|
Err(e) => {
|
|
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Session retry after discovery failed");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Mark ECN-CE in an IPv6 packet's Traffic Class field.
|
|
///
|
|
/// IPv6 Traffic Class occupies bits across bytes 0 and 1:
|
|
/// byte[0] bits[3:0] = TC[7:4]
|
|
/// byte[1] bits[7:4] = TC[3:0]
|
|
/// ECN is TC[1:0]. Only marks CE (0b11) if the packet is ECN-capable
|
|
/// (ECT(0) or ECT(1)). Packets with ECN=0b00 (Not-ECT) are never marked
|
|
/// per RFC 3168.
|
|
///
|
|
/// No checksum update needed: IPv6 has no header checksum, and the Traffic
|
|
/// Class field is not part of the TCP/UDP pseudo-header.
|
|
pub(in crate::node) fn mark_ipv6_ecn_ce(packet: &mut [u8]) {
|
|
if packet.len() < 2 {
|
|
return;
|
|
}
|
|
// Extract 8-bit Traffic Class from IPv6 header bytes 0-1
|
|
let tc = ((packet[0] & 0x0F) << 4) | (packet[1] >> 4);
|
|
let ecn = tc & 0x03;
|
|
// Only mark CE on ECN-capable packets (ECT(0)=0b10 or ECT(1)=0b01)
|
|
if ecn == 0 {
|
|
return;
|
|
}
|
|
// Set both ECN bits to 1 (CE = 0b11)
|
|
let new_tc = tc | 0x03;
|
|
packet[0] = (packet[0] & 0xF0) | (new_tc >> 4);
|
|
packet[1] = (new_tc << 4) | (packet[1] & 0x0F);
|
|
}
|