mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 12:45:20 +00:00
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:
@@ -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
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import BitFoundation
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
@Suite("BLE Noise reconnect policy")
|
||||
struct BLENoiseReconnectPolicyTests {
|
||||
@Test("Revalidation requires a cached session and no authenticated link")
|
||||
func revalidationPreconditions() {
|
||||
var policy = BLENoiseReconnectPolicy()
|
||||
let link = BLEIngressLinkID.peripheral("peripheral-a")
|
||||
let now = Date(timeIntervalSince1970: 1_000)
|
||||
|
||||
let withoutSession = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: false,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: now
|
||||
)
|
||||
#expect(!withoutSession)
|
||||
let authenticated = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: true,
|
||||
hasAuthenticatedPeerLink: true,
|
||||
now: now
|
||||
)
|
||||
#expect(!authenticated)
|
||||
let eligible = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: now
|
||||
)
|
||||
#expect(eligible)
|
||||
}
|
||||
|
||||
@Test("Revalidation is once per link epoch or after sixty seconds")
|
||||
func revalidationIsBoundPerLinkEpoch() {
|
||||
var policy = BLENoiseReconnectPolicy()
|
||||
let link = BLEIngressLinkID.central("central-a")
|
||||
let start = Date(timeIntervalSince1970: 2_000)
|
||||
|
||||
let initial = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: start
|
||||
)
|
||||
#expect(initial)
|
||||
let duringCooldown = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: start.addingTimeInterval(59.999)
|
||||
)
|
||||
#expect(!duringCooldown)
|
||||
let afterCooldown = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: start.addingTimeInterval(60)
|
||||
)
|
||||
#expect(afterCooldown)
|
||||
|
||||
policy.endLinkEpoch(link)
|
||||
let nextEpoch = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: start.addingTimeInterval(60.001)
|
||||
)
|
||||
#expect(nextEpoch)
|
||||
}
|
||||
|
||||
@Test("An authenticated sibling suppresses redundant reconnect")
|
||||
func authenticatedSiblingSuppressesReconnect() {
|
||||
var policy = BLENoiseReconnectPolicy()
|
||||
let link = BLEIngressLinkID.peripheral("unproven-sibling")
|
||||
let start = Date(timeIntervalSince1970: 3_000)
|
||||
|
||||
let suppressed = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: true,
|
||||
now: start
|
||||
)
|
||||
#expect(!suppressed)
|
||||
let eligible = policy.shouldRevalidate(
|
||||
on: link,
|
||||
hasEstablishedSession: true,
|
||||
isNoiseAuthenticatedLink: false,
|
||||
hasAuthenticatedPeerLink: false,
|
||||
now: start
|
||||
)
|
||||
#expect(eligible)
|
||||
}
|
||||
|
||||
@Test("Reserved replacement bit is not advertised")
|
||||
func reservedReplacementBitIsNotAdvertised() {
|
||||
#expect(
|
||||
!PeerCapabilities.localSupported.contains(
|
||||
.nonDestructiveNoiseReplacement
|
||||
)
|
||||
)
|
||||
#expect(PeerCapabilities.localSupported.contains(.privateMedia))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user