Session-layer handshake message retry with exponential backoff

Add resend logic for SessionSetup/SessionAck messages routed through
the mesh. Stores the encoded payload on SessionEntry for resend in a
fresh SessionDatagram (so routing can adapt to topology changes).
Uses the same config parameters as link-layer retry.

Also fixes a latent bug: Initiating/Responding sessions previously
had no timeout — a stuck handshake would live forever. Now cleaned up
after handshake_timeout_secs (default 30s).

Responder idempotency: duplicate SessionSetup triggers resend of
stored SessionAck instead of being silently dropped. Initiator-side
duplicate SessionAck already handled safely (entry.take_state() sees
Established, puts it back and returns).

Handshake payload cleared on Established transition at both initiator
(handle_session_ack) and responder (handle_encrypted_session_msg).
This commit is contained in:
Johnathan Corgan
2026-02-19 16:20:38 +00:00
parent 6a10e9228b
commit 5d1783edd5
5 changed files with 340 additions and 9 deletions
+53
View File
@@ -70,6 +70,15 @@ pub(crate) struct SessionEntry {
is_initiator: bool,
/// Session-layer MMP state. Initialized on Established transition.
mmp: Option<MmpSessionState>,
// === Handshake Resend ===
/// Encoded session-layer payload for resend (SessionSetup or SessionAck).
/// Cleared on Established transition.
handshake_payload: Option<Vec<u8>>,
/// Number of resends performed.
resend_count: u32,
/// When the next resend should fire (Unix ms). 0 = no resend scheduled.
next_resend_at_ms: u64,
}
impl SessionEntry {
@@ -91,6 +100,9 @@ impl SessionEntry {
coords_warmup_remaining: 0,
is_initiator,
mmp: None,
handshake_payload: None,
resend_count: 0,
next_resend_at_ms: 0,
}
}
@@ -193,4 +205,45 @@ impl SessionEntry {
pub(crate) fn init_mmp(&mut self, config: &SessionMmpConfig) {
self.mmp = Some(MmpSessionState::new(config, self.is_initiator));
}
// === Handshake Resend ===
/// Store the encoded session-layer payload for potential resend.
///
/// For initiators, this is the SessionSetup payload bytes.
/// For responders, this is the SessionAck payload bytes.
/// The payload is re-wrapped in a fresh SessionDatagram on each resend
/// so routing can adapt to topology changes.
pub(crate) fn set_handshake_payload(&mut self, payload: Vec<u8>, next_resend_at_ms: u64) {
self.handshake_payload = Some(payload);
self.resend_count = 0;
self.next_resend_at_ms = next_resend_at_ms;
}
/// Get the stored handshake payload for resend.
pub(crate) fn handshake_payload(&self) -> Option<&[u8]> {
self.handshake_payload.as_deref()
}
/// Clear the stored handshake payload (called on Established transition).
pub(crate) fn clear_handshake_payload(&mut self) {
self.handshake_payload = None;
self.next_resend_at_ms = 0;
}
/// Number of resends performed so far.
pub(crate) fn resend_count(&self) -> u32 {
self.resend_count
}
/// When the next resend should fire (Unix ms). 0 = no resend scheduled.
pub(crate) fn next_resend_at_ms(&self) -> u64 {
self.next_resend_at_ms
}
/// Record a resend and schedule the next one.
pub(crate) fn record_resend(&mut self, next_resend_at_ms: u64) {
self.resend_count += 1;
self.next_resend_at_ms = next_resend_at_ms;
}
}