mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Harden the insecure-link outbox bound; document the second-replay presence gap and the courier metadata tradeoff
Review follow-ups on the canDeliverSecurely gate: - flushOutbox no longer counts connected-but-insecure flushes toward the maxSendAttempts drop: the message was actually transmitted over a live link, so a peer whose Noise handshake stalls across reconnect flapping must not burn through the cap and lose the store-and-forward copy the gate exists to preserve. Retention stays bounded by the 24h outbox TTL and the per-peer FIFO cap; acks still clear it. Attempt-counting stays for reachable-only (heuristic) sends. Regression test: >8 connected- insecure flushes keep the retained copy, drop callback never fires. - Document the known second-replay presence gap: linkBoundToOtherPeer reads the binding before rebindLinkAfterVerifiedDirectAnnounce steals the link, so once a first replay has rebound a link to an absent victim's ID, a second replay marks the victim connected. Presence display only — DMs stay on the retain+courier path via the router gate. Not closed at the announce layer because the post-rebind state is indistinguishable from a legitimate rotation/reconnect heal (a supported, field-verified flow). Covered by a two-announce test. - Note the accepted courier-spray metadata tradeoff at the connected- insecure send: nearby verified peers receive a sealed copy (they learn a DM exists, never its content), cleared on ack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -360,6 +360,38 @@ struct MessageRouterTests {
|
||||
#expect(transport.sentPrivateMessages.count == 3)
|
||||
}
|
||||
|
||||
/// Flushes over a connected-but-insecure link never count toward the
|
||||
/// attempt-cap drop: the message was actually transmitted over a live
|
||||
/// link, so a peer whose Noise handshake stalls across reconnect flapping
|
||||
/// must not burn through the cap and lose the store-and-forward copy the
|
||||
/// secure-session gate exists to preserve. Retention stays bounded by
|
||||
/// the 24h outbox TTL and the per-peer FIFO cap; an ack clears it.
|
||||
@Test @MainActor
|
||||
func flushOutbox_connectedInsecureFlushesNeverDropTheRetainedCopy() async {
|
||||
let peerID = PeerID(str: "00000000000000ac")
|
||||
let transport = MockTransport()
|
||||
transport.connectedPeers.insert(peerID)
|
||||
transport.securePeers = [] // handshake never completes
|
||||
|
||||
let router = MessageRouter(transports: [transport])
|
||||
var dropped: [String] = []
|
||||
router.onMessageDropped = { messageID, _ in dropped.append(messageID) }
|
||||
|
||||
router.sendPrivate("Hello", to: peerID, recipientNickname: "Peer", messageID: "ci1")
|
||||
|
||||
// Well past maxSendAttempts (8): every flush resends, none drops.
|
||||
for _ in 0..<10 {
|
||||
router.flushOutbox(for: peerID)
|
||||
}
|
||||
#expect(dropped.isEmpty)
|
||||
#expect(transport.sentPrivateMessages.count == 11)
|
||||
|
||||
// The copy is still retained and an ack still clears it.
|
||||
router.markDelivered("ci1")
|
||||
router.flushOutbox(for: peerID)
|
||||
#expect(transport.sentPrivateMessages.count == 11)
|
||||
}
|
||||
|
||||
/// With an established secure session the connected fast-path stays
|
||||
/// exactly as before: trusted outright, no retained copy, no courier.
|
||||
@Test @MainActor
|
||||
|
||||
Reference in New Issue
Block a user