rekey: apply symmetric jitter to desynchronize dual-initiation

Add a per-session signed jitter offset (uniform [-15, +15] seconds)
to the rekey timer triggers in check_rekey (FMP) and check_session_rekey
(FSP). The configured `node.rekey.after_secs` becomes the nominal
interval rather than a floor; mean is preserved. Desynchronizes
both endpoints in symmetric-start meshes so the dual-initiation
race stops occurring rather than being resolved after the fact by
the smaller-NodeAddr tie-breaker.

Per-session storage means each rekey cutover reconstructs the
session and redraws the jitter naturally — successive cycles get
independent offsets, preventing drift back into sync.
This commit is contained in:
Johnathan Corgan
2026-05-14 16:32:07 +00:00
parent 7bd8d3b7a0
commit 4f3d2f8471
6 changed files with 178 additions and 2 deletions
+62
View File
@@ -67,6 +67,68 @@ fn test_session_entry_new_initiating() {
assert_eq!(entry.last_activity(), 1000);
}
#[test]
fn test_session_entry_rekey_jitter_in_range() {
use crate::node::REKEY_JITTER_SECS;
use crate::noise::HandshakeState;
// Every newly constructed SessionEntry's jitter must lie in the
// symmetric range [-REKEY_JITTER_SECS, +REKEY_JITTER_SECS].
for _ in 0..100 {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake =
HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
identity_b.pubkey_full(),
EndToEndState::Initiating(handshake),
1000,
true,
);
let j = entry.rekey_jitter_secs();
assert!(
(-REKEY_JITTER_SECS..=REKEY_JITTER_SECS).contains(&j),
"jitter {} outside [-{}, +{}]",
j,
REKEY_JITTER_SECS,
REKEY_JITTER_SECS
);
}
}
#[test]
fn test_session_entry_rekey_jitter_mean_near_zero() {
use crate::noise::HandshakeState;
// Sanity check that the distribution is roughly symmetric and not
// stuck at one extreme. With N=200 draws from a uniform ~30-second
// range, the empirical mean should be well under 5 in absolute value.
let mut sum: i64 = 0;
let n: i64 = 200;
for _ in 0..n {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake =
HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
identity_b.pubkey_full(),
EndToEndState::Initiating(handshake),
1000,
true,
);
sum += entry.rekey_jitter_secs();
}
let mean = sum / n;
assert!(
mean.abs() < 5,
"empirical mean {} not within 5 of 0 over {} samples",
mean,
n
);
}
#[test]
fn test_session_entry_touch() {
use crate::noise::HandshakeState;