Make ordinary Noise reconnects atomic and race-safe (#1463)

Replaces destroy-then-rebuild Noise re-handshakes with an atomic reconnect protocol: prepared XX message-1 handoff tokens (claim-once, invalidated by crossed inbound initiations), receive-only quarantine of the established transport while an inbound replacement proves identity (promote on success, rollback+cooldown on failure/timeout), deterministic lower-peerID-wins crossed-initiator resolution, and a per-link-epoch BLE revalidation policy that re-proves cached sessions inside the same bleQueue critical section as the link rebind. On main, a fresh msg1 simply destroys an established session and rekey spans two non-atomic barriers.

Includes the review fix: timeout-restores defer outbound queue draining until the convergence retry completes (restore reason plumbed end-to-end), so DMs are never drained under keys a restarted peer already discarded — with a deterministic interleaving test. Identity-mismatch restores drain immediately. Full local suite 1768 tests green.
This commit is contained in:
jack
2026-07-26 13:04:56 +02:00
committed by GitHub
parent e9275cb3d8
commit 78a81e5b57
17 changed files with 3248 additions and 179 deletions
@@ -198,6 +198,24 @@ struct BLENoisePacketHandlerTests {
#expect(recorder.broadcastPackets.isEmpty)
}
@Test
func managedHandshakeFailureDoesNotStartASecondRecovery() {
let recorder = Recorder()
recorder.handshakeResult = .failure(
NoiseManagedHandshakeFailure(underlying: TestError())
)
recorder.hasSession = false
let handler = makeHandler(recorder: recorder)
let packet = makeHandshakePacket(
recipientID: Data(hexString: localPeerID.id)
)
#expect(!handler.handleHandshake(packet, from: remotePeerID))
#expect(recorder.hasSessionQueries.isEmpty)
#expect(recorder.initiatedHandshakes.isEmpty)
#expect(recorder.broadcastPackets.isEmpty)
}
// MARK: Encrypted
@Test