Add epoch-based peer restart detection to Noise IK handshake

Each node generates a random 8-byte startup epoch, encrypted inside
both Noise IK handshake messages (msg1 and msg2). When a peer's msg1
arrives with a different epoch than the stored value, the node tears
down the stale session and processes the msg1 as a new connection,
enabling near-instant restart detection instead of the 30-second
dead timeout.

Wire format impact:
- msg1: 82 -> 106 bytes (added 24-byte encrypted epoch after ss DH)
- msg2: 33 -> 57 bytes (added 24-byte encrypted epoch after se DH)
- Wire msg1: 90 -> 114 bytes, wire msg2: 45 -> 69 bytes
This commit is contained in:
Johnathan Corgan
2026-02-22 20:50:50 +00:00
parent 1adfd9e90f
commit f920526ece
15 changed files with 338 additions and 87 deletions
+9 -9
View File
@@ -65,7 +65,7 @@ async fn test_two_node_handshake_udp() {
// Start handshake (generates Noise IK msg1)
let our_keypair_a = node_a.identity.keypair();
let noise_msg1 = conn_a.start_handshake(our_keypair_a, 1000).unwrap();
let noise_msg1 = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
conn_a.set_source_addr(remote_addr_b.clone());
@@ -303,7 +303,7 @@ async fn test_run_rx_loop_handshake() {
let our_index_a = node_a.index_allocator.allocate().unwrap();
let our_keypair_a = node_a.identity.keypair();
let noise_msg1 = conn_a.start_handshake(our_keypair_a, 1000).unwrap();
let noise_msg1 = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
conn_a.set_source_addr(remote_addr_b.clone());
@@ -488,7 +488,7 @@ async fn test_cross_connection_both_initiate() {
let mut conn_a = PeerConnection::outbound(link_id_a_out, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let our_keypair_a = node_a.identity.keypair();
let noise_msg1_a = conn_a.start_handshake(our_keypair_a, 1000).unwrap();
let noise_msg1_a = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
conn_a.set_source_addr(remote_addr_b.clone());
@@ -509,7 +509,7 @@ async fn test_cross_connection_both_initiate() {
let mut conn_b = PeerConnection::outbound(link_id_b_out, peer_a_identity, 1000);
let our_index_b = node_b.index_allocator.allocate().unwrap();
let our_keypair_b = node_b.identity.keypair();
let noise_msg1_b = conn_b.start_handshake(our_keypair_b, 1000).unwrap();
let noise_msg1_b = conn_b.start_handshake(our_keypair_b, node_b.startup_epoch, 1000).unwrap();
conn_b.set_our_index(our_index_b);
conn_b.set_transport_id(transport_id_b);
conn_b.set_source_addr(remote_addr_a.clone());
@@ -611,7 +611,7 @@ async fn test_stale_connection_cleanup() {
// Allocate session index and set transport info
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let _noise_msg1 = conn.start_handshake(our_keypair, past_time_ms).unwrap();
let _noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, past_time_ms).unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
@@ -665,7 +665,7 @@ async fn test_failed_connection_cleanup() {
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let _noise_msg1 = conn.start_handshake(our_keypair, now_ms).unwrap();
let _noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, now_ms).unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
@@ -710,7 +710,7 @@ async fn test_msg1_stored_for_resend() {
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let noise_msg1 = conn.start_handshake(our_keypair, now_ms).unwrap();
let noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, now_ms).unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
@@ -741,7 +741,7 @@ async fn test_resend_scheduling() {
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let noise_msg1 = conn.start_handshake(our_keypair, now_ms).unwrap();
let noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, now_ms).unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
@@ -827,7 +827,7 @@ async fn test_duplicate_msg2_dropped() {
let sender_idx = SessionIndex::new(99);
// Build a fake msg2 packet
let fake_noise_msg2 = vec![0u8; 33]; // Noise IK msg2 is 33 bytes
let fake_noise_msg2 = vec![0u8; 57]; // Noise IK msg2 is 57 bytes (33 ephem + 24 encrypted epoch)
let wire_msg2 = build_msg2(sender_idx, receiver_idx, &fake_noise_msg2);
let packet = ReceivedPacket {
+4 -2
View File
@@ -50,13 +50,15 @@ pub(super) fn make_completed_connection(
// Run initiator side of handshake
let our_keypair = node.identity.keypair();
let msg1 = conn.start_handshake(our_keypair, current_time_ms).unwrap();
let msg1 = conn.start_handshake(our_keypair, node.startup_epoch, current_time_ms).unwrap();
// Run responder side to generate msg2
let mut resp_conn = PeerConnection::inbound(LinkId::new(999), current_time_ms);
let peer_keypair = peer_identity_full.keypair();
let mut resp_epoch = [0u8; 8];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut resp_epoch);
let msg2 = resp_conn
.receive_handshake_init(peer_keypair, &msg1, current_time_ms)
.receive_handshake_init(peer_keypair, resp_epoch, &msg1, current_time_ms)
.unwrap();
// Complete initiator handshake
+8
View File
@@ -1160,6 +1160,14 @@ fn make_noise_session(
);
let mut responder = HandshakeState::new_responder(remote_identity.keypair());
// Set epochs for both sides (required for handshake message encryption)
let mut init_epoch = [0u8; 8];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut init_epoch);
initiator.set_local_epoch(init_epoch);
let mut resp_epoch = [0u8; 8];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut resp_epoch);
responder.set_local_epoch(resp_epoch);
let msg1 = initiator.write_message_1().unwrap();
responder.read_message_1(&msg1).unwrap();
let msg2 = responder.write_message_2().unwrap();
+1 -1
View File
@@ -64,7 +64,7 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz
let our_index = initiator.node.index_allocator.allocate().unwrap();
let our_keypair = initiator.node.identity().keypair();
let noise_msg1 = conn.start_handshake(our_keypair, 1000).unwrap();
let noise_msg1 = conn.start_handshake(our_keypair, initiator.node.startup_epoch, 1000).unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(responder_addr.clone());
+5 -3
View File
@@ -450,7 +450,7 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
PeerConnection::outbound(pending_link_id, peer_b_identity, pending_time_ms);
let our_keypair = node.identity.keypair();
let _msg1 = pending_conn.start_handshake(our_keypair, pending_time_ms).unwrap();
let _msg1 = pending_conn.start_handshake(our_keypair, node.startup_epoch, pending_time_ms).unwrap();
let pending_index = node.index_allocator.allocate().unwrap();
pending_conn.set_our_index(pending_index);
@@ -491,14 +491,16 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
let our_keypair = node.identity.keypair();
let msg1 = completing_conn
.start_handshake(our_keypair, completing_time_ms)
.start_handshake(our_keypair, node.startup_epoch, completing_time_ms)
.unwrap();
// B responds
let mut resp_conn = PeerConnection::inbound(LinkId::new(999), completing_time_ms);
let peer_keypair = peer_b_full.keypair();
let mut resp_epoch = [0u8; 8];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut resp_epoch);
let msg2 = resp_conn
.receive_handshake_init(peer_keypair, &msg1, completing_time_ms)
.receive_handshake_init(peer_keypair, resp_epoch, &msg1, completing_time_ms)
.unwrap();
completing_conn