Switch FMP handshake from Noise IK to XX with version negotiation

Replace the 2-message IK handshake with a 3-message XX handshake for
FMP link establishment. XX requires no prior knowledge of the peer's
static key — both identities are revealed during the handshake
(responder in msg2, initiator in msg3). This is the foundation for
the forklift upgrade that enables rolling protocol upgrades.

Changes:
- Noise XX state machine alongside IK/XK (8 unit tests)
- Protocol negotiation payload codec: format byte, packed version
  min/max, 64-bit feature bitfield, TLV extensions (11 unit tests)
- FMP wire format version 0→1, msg3 header/builder, TCP stream framing
- FMP handshake switched to XX: PeerConnection 3-message flow,
  handle_msg1 simplified (no identity), handle_msg2 sends msg3 and
  promotes initiator, new handle_msg3 promotes responder with
  restart/rekey/cross-connection detection
- Rekey handshake switched to XX with negotiation payload hash chain
  fix (decrypt-and-discard in complete_rekey_msg2/msg3)
- Negotiation payload in msg2/msg3 (FMP version [1,1], features=0)
- Debug logging for handshake promotion paths
- Integration test convergence timeouts adjusted for extra round-trip

Squashed commits:
- Add Noise XX state machine alongside IK/XK
- Add protocol negotiation payload codec
- FMP wire format prep: version 1, msg3 header support
- Switch FMP handshake from Noise IK to XX
- Increase convergence timeouts for XX 3-message handshake
- Fix negotiation hash chain desync in rekey handshake
This commit is contained in:
Johnathan Corgan
2026-04-11 08:16:01 +00:00
parent 9ccaae5044
commit 179689d6f2
20 changed files with 2486 additions and 977 deletions
+12 -9
View File
@@ -1,12 +1,10 @@
//! RX event loop and packet dispatch.
use crate::control::{commands, ControlSocket};
use crate::control::queries;
use crate::control::{ControlSocket, commands};
use crate::node::wire::{
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
};
use crate::node::{Node, NodeError};
use crate::transport::ReceivedPacket;
use crate::node::wire::{CommonPrefix, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2, PHASE_MSG3, FMP_VERSION, COMMON_PREFIX_SIZE};
use std::time::Duration;
use tracing::{debug, info, warn};
@@ -18,6 +16,7 @@ impl Node {
/// - Phase 0x0: Encrypted frame (session data)
/// - Phase 0x1: Handshake message 1 (initiator -> responder)
/// - Phase 0x2: Handshake message 2 (responder -> initiator)
/// - Phase 0x3: Handshake message 3 (initiator -> responder, XX completion)
///
/// Also processes outbound IPv6 packets from the TUN reader for session
/// encapsulation and routing through the mesh.
@@ -31,7 +30,8 @@ impl Node {
/// This method takes ownership of the packet_rx channel and runs
/// until the channel is closed (typically when stop() is called).
pub async fn run_rx_loop(&mut self) -> Result<(), NodeError> {
let mut packet_rx = self.packet_rx.take().ok_or(NodeError::NotStarted)?;
let mut packet_rx = self.packet_rx.take()
.ok_or(NodeError::NotStarted)?;
// Take the TUN outbound receiver, or create a dummy channel that never
// produces messages (when TUN is disabled). Holding the sender prevents
@@ -54,12 +54,12 @@ impl Node {
}
};
let mut tick =
tokio::time::interval(Duration::from_secs(self.config.node.tick_interval_secs));
let mut tick = tokio::time::interval(Duration::from_secs(self.config.node.tick_interval_secs));
// Set up control socket channel
let (control_tx, mut control_rx) =
tokio::sync::mpsc::channel::<crate::control::ControlMessage>(32);
let (control_tx, mut control_rx) = tokio::sync::mpsc::channel::<
crate::control::ControlMessage,
>(32);
if self.config.node.control.enabled {
let config = self.config.node.control.clone();
@@ -173,6 +173,9 @@ impl Node {
PHASE_MSG2 => {
self.handle_msg2(packet).await;
}
PHASE_MSG3 => {
self.handle_msg3(packet).await;
}
_ => {
debug!(
phase = prefix.phase,