mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 01:45:18 +00:00
Fix courier handoff verification and directed sends
This commit is contained in:
@@ -62,6 +62,13 @@ struct BLEAnnounceHandlerEnvironment {
|
|||||||
/// Orchestrates inbound announce packets: preflight validation, signature
|
/// Orchestrates inbound announce packets: preflight validation, signature
|
||||||
/// trust, registry/topology updates, identity persistence, UI notification,
|
/// trust, registry/topology updates, identity persistence, UI notification,
|
||||||
/// gossip tracking, and the reciprocal announce response.
|
/// gossip tracking, and the reciprocal announce response.
|
||||||
|
struct BLEAnnounceHandlingResult {
|
||||||
|
let peerID: PeerID
|
||||||
|
let announcement: AnnouncementPacket
|
||||||
|
let isDirectAnnounce: Bool
|
||||||
|
let isVerified: Bool
|
||||||
|
}
|
||||||
|
|
||||||
final class BLEAnnounceHandler {
|
final class BLEAnnounceHandler {
|
||||||
private let environment: BLEAnnounceHandlerEnvironment
|
private let environment: BLEAnnounceHandlerEnvironment
|
||||||
|
|
||||||
@@ -69,7 +76,8 @@ final class BLEAnnounceHandler {
|
|||||||
self.environment = environment
|
self.environment = environment
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(_ packet: BitchatPacket, from peerID: PeerID) {
|
@discardableResult
|
||||||
|
func handle(_ packet: BitchatPacket, from peerID: PeerID) -> BLEAnnounceHandlingResult? {
|
||||||
let env = environment
|
let env = environment
|
||||||
let now = env.now()
|
let now = env.now()
|
||||||
let preflight = BLEAnnouncePreflightPolicy.evaluate(
|
let preflight = BLEAnnouncePreflightPolicy.evaluate(
|
||||||
@@ -85,15 +93,15 @@ final class BLEAnnounceHandler {
|
|||||||
announcement = acceptance.announcement
|
announcement = acceptance.announcement
|
||||||
case .reject(.malformed):
|
case .reject(.malformed):
|
||||||
SecureLogger.error("❌ Failed to decode announce packet from \(peerID.id.prefix(8))…", category: .session)
|
SecureLogger.error("❌ Failed to decode announce packet from \(peerID.id.prefix(8))…", category: .session)
|
||||||
return
|
return nil
|
||||||
case .reject(.senderMismatch(let derivedFromKey)):
|
case .reject(.senderMismatch(let derivedFromKey)):
|
||||||
SecureLogger.warning("⚠️ Announce sender mismatch: derived \(derivedFromKey.id.prefix(8))… vs packet \(peerID.id.prefix(8))…", category: .security)
|
SecureLogger.warning("⚠️ Announce sender mismatch: derived \(derivedFromKey.id.prefix(8))… vs packet \(peerID.id.prefix(8))…", category: .security)
|
||||||
return
|
return nil
|
||||||
case .reject(.selfAnnounce):
|
case .reject(.selfAnnounce):
|
||||||
return
|
return nil
|
||||||
case .reject(.stale(let ageSeconds)):
|
case .reject(.stale(let ageSeconds)):
|
||||||
SecureLogger.debug("⏰ Ignoring stale announce from \(peerID.id.prefix(8))… (age: \(ageSeconds)s)", category: .session)
|
SecureLogger.debug("⏰ Ignoring stale announce from \(peerID.id.prefix(8))… (age: \(ageSeconds)s)", category: .session)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress announce logs to reduce noise
|
// Suppress announce logs to reduce noise
|
||||||
@@ -210,5 +218,12 @@ final class BLEAnnounceHandler {
|
|||||||
let delay = Double.random(in: 0.3...0.6)
|
let delay = Double.random(in: 0.3...0.6)
|
||||||
env.scheduleAfterglow(delay)
|
env.scheduleAfterglow(delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return BLEAnnounceHandlingResult(
|
||||||
|
peerID: peerID,
|
||||||
|
announcement: announcement,
|
||||||
|
isDirectAnnounce: isDirectAnnounce,
|
||||||
|
isVerified: verifiedAnnounce
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,35 @@ enum BLEFanoutSelector {
|
|||||||
packetType: UInt8,
|
packetType: UInt8,
|
||||||
messageID: String
|
messageID: String
|
||||||
) -> BLEFanoutSelection {
|
) -> BLEFanoutSelection {
|
||||||
|
let rawAllowed = allowedLinks(
|
||||||
|
peripheralIDs: peripheralIDs,
|
||||||
|
centralIDs: centralIDs,
|
||||||
|
ingressLink: ingressLink,
|
||||||
|
excludedLinks: excludedLinks
|
||||||
|
)
|
||||||
|
|
||||||
|
if let directedPeerHint,
|
||||||
|
let directedSelection = directLinks(
|
||||||
|
to: directedPeerHint,
|
||||||
|
links: rawAllowed,
|
||||||
|
peripheralPeerBindings: peripheralPeerBindings,
|
||||||
|
centralPeerBindings: centralPeerBindings
|
||||||
|
) {
|
||||||
|
return directedSelection
|
||||||
|
}
|
||||||
|
if let directedPeerHint,
|
||||||
|
hasBoundLink(
|
||||||
|
to: directedPeerHint,
|
||||||
|
peripheralIDs: peripheralIDs,
|
||||||
|
centralIDs: centralIDs,
|
||||||
|
peripheralPeerBindings: peripheralPeerBindings,
|
||||||
|
centralPeerBindings: centralPeerBindings
|
||||||
|
) {
|
||||||
|
return BLEFanoutSelection(peripheralIDs: [], centralIDs: [])
|
||||||
|
}
|
||||||
|
|
||||||
let allowed = collapseDuplicateLinksPerPeer(
|
let allowed = collapseDuplicateLinksPerPeer(
|
||||||
allowedLinks(
|
rawAllowed,
|
||||||
peripheralIDs: peripheralIDs,
|
|
||||||
centralIDs: centralIDs,
|
|
||||||
ingressLink: ingressLink,
|
|
||||||
excludedLinks: excludedLinks
|
|
||||||
),
|
|
||||||
peripheralPeerBindings: peripheralPeerBindings,
|
peripheralPeerBindings: peripheralPeerBindings,
|
||||||
centralPeerBindings: centralPeerBindings
|
centralPeerBindings: centralPeerBindings
|
||||||
)
|
)
|
||||||
@@ -71,6 +93,42 @@ enum BLEFanoutSelector {
|
|||||||
return (allowedPeripheralIDs, allowedCentralIDs)
|
return (allowedPeripheralIDs, allowedCentralIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func directLinks(
|
||||||
|
to peerID: PeerID,
|
||||||
|
links: (peripheralIDs: [String], centralIDs: [String]),
|
||||||
|
peripheralPeerBindings: [String: PeerID],
|
||||||
|
centralPeerBindings: [String: PeerID]
|
||||||
|
) -> BLEFanoutSelection? {
|
||||||
|
let directLinks = collapseDuplicateLinksPerPeer(
|
||||||
|
(
|
||||||
|
peripheralIDs: links.peripheralIDs.filter { peripheralPeerBindings[$0] == peerID },
|
||||||
|
centralIDs: links.centralIDs.filter { centralPeerBindings[$0] == peerID }
|
||||||
|
),
|
||||||
|
peripheralPeerBindings: peripheralPeerBindings,
|
||||||
|
centralPeerBindings: centralPeerBindings
|
||||||
|
)
|
||||||
|
|
||||||
|
guard !directLinks.peripheralIDs.isEmpty || !directLinks.centralIDs.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return BLEFanoutSelection(
|
||||||
|
peripheralIDs: Set(directLinks.peripheralIDs),
|
||||||
|
centralIDs: Set(directLinks.centralIDs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func hasBoundLink(
|
||||||
|
to peerID: PeerID,
|
||||||
|
peripheralIDs: [String],
|
||||||
|
centralIDs: [String],
|
||||||
|
peripheralPeerBindings: [String: PeerID],
|
||||||
|
centralPeerBindings: [String: PeerID]
|
||||||
|
) -> Bool {
|
||||||
|
peripheralIDs.contains { peripheralPeerBindings[$0] == peerID }
|
||||||
|
|| centralIDs.contains { centralPeerBindings[$0] == peerID }
|
||||||
|
}
|
||||||
|
|
||||||
// Dual-role pairs hold two live links (we-as-central writing to their
|
// Dual-role pairs hold two live links (we-as-central writing to their
|
||||||
// peripheral, and they-as-central subscribed to ours). Sending the same
|
// peripheral, and they-as-central subscribed to ours). Sending the same
|
||||||
// packet down both doubles airtime for nothing — the receiver's assembler
|
// packet down both doubles airtime for nothing — the receiver's assembler
|
||||||
|
|||||||
@@ -1065,6 +1065,9 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// Directed send helper (unicast to a specific peerID) without altering packet contents
|
// Directed send helper (unicast to a specific peerID) without altering packet contents
|
||||||
private func sendPacketDirected(_ packet: BitchatPacket, to peerID: PeerID) {
|
private func sendPacketDirected(_ packet: BitchatPacket, to peerID: PeerID) {
|
||||||
|
#if DEBUG
|
||||||
|
_test_onOutboundPacket?(packet)
|
||||||
|
#endif
|
||||||
guard let data = packet.toBinaryData(padding: false) else { return }
|
guard let data = packet.toBinaryData(padding: false) else { return }
|
||||||
sendOnAllLinks(packet: packet, data: data, pad: false, directedOnlyPeer: peerID)
|
sendOnAllLinks(packet: packet, data: data, pad: false, directedOnlyPeer: peerID)
|
||||||
}
|
}
|
||||||
@@ -2491,33 +2494,39 @@ extension BLEService {
|
|||||||
|
|
||||||
/// Seal `content` to the recipient's static key (one-way Noise X) and hand
|
/// Seal `content` to the recipient's static key (one-way Noise X) and hand
|
||||||
/// the envelope to the given couriers for physical delivery. Returns false
|
/// the envelope to the given couriers for physical delivery. Returns false
|
||||||
/// when no courier is connected; sealing and sending happen asynchronously.
|
/// when no courier is connected, the payload cannot be built, or sealing
|
||||||
|
/// fails; link writes are queued asynchronously after the envelope is ready.
|
||||||
func sendCourierMessage(_ content: String, messageID: String, recipientNoiseKey: Data, via couriers: [PeerID]) -> Bool {
|
func sendCourierMessage(_ content: String, messageID: String, recipientNoiseKey: Data, via couriers: [PeerID]) -> Bool {
|
||||||
let connected = couriers.filter { isPeerConnected($0) }
|
let connected = couriers.filter { isPeerConnected($0) }
|
||||||
guard !connected.isEmpty,
|
guard !connected.isEmpty,
|
||||||
let typedPayload = BLENoisePayloadFactory.privateMessage(content: content, messageID: messageID) else {
|
let typedPayload = BLENoisePayloadFactory.privateMessage(content: content, messageID: messageID) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let payload: Data
|
||||||
|
do {
|
||||||
|
let now = Date()
|
||||||
|
let sealed = try noiseService.sealCourierPayload(typedPayload, recipientStaticKey: recipientNoiseKey)
|
||||||
|
let envelope = CourierEnvelope(
|
||||||
|
recipientTag: CourierEnvelope.recipientTag(
|
||||||
|
noiseStaticKey: recipientNoiseKey,
|
||||||
|
epochDay: CourierEnvelope.epochDay(for: now)
|
||||||
|
),
|
||||||
|
expiry: UInt64((now.timeIntervalSince1970 + CourierEnvelope.maxLifetimeSeconds) * 1000),
|
||||||
|
ciphertext: sealed
|
||||||
|
)
|
||||||
|
guard let encoded = envelope.encode() else { return false }
|
||||||
|
payload = encoded
|
||||||
|
} catch {
|
||||||
|
SecureLogger.error("Failed to seal courier envelope: \(error)", category: .encryption)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
do {
|
for courier in connected {
|
||||||
let now = Date()
|
SecureLogger.debug("📦 Depositing courier envelope with \(courier.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
let sealed = try self.noiseService.sealCourierPayload(typedPayload, recipientStaticKey: recipientNoiseKey)
|
self.sendPacketDirected(self.makeCourierPacket(payload, to: courier), to: courier)
|
||||||
let envelope = CourierEnvelope(
|
|
||||||
recipientTag: CourierEnvelope.recipientTag(
|
|
||||||
noiseStaticKey: recipientNoiseKey,
|
|
||||||
epochDay: CourierEnvelope.epochDay(for: now)
|
|
||||||
),
|
|
||||||
expiry: UInt64((now.timeIntervalSince1970 + CourierEnvelope.maxLifetimeSeconds) * 1000),
|
|
||||||
ciphertext: sealed
|
|
||||||
)
|
|
||||||
guard let payload = envelope.encode() else { return }
|
|
||||||
for courier in connected {
|
|
||||||
SecureLogger.debug("📦 Depositing courier envelope with \(courier.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
|
||||||
self.broadcastPacket(self.makeCourierPacket(payload, to: courier))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
SecureLogger.error("Failed to seal courier envelope: \(error)", category: .encryption)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -2605,7 +2614,7 @@ extension BLEService {
|
|||||||
SecureLogger.debug("📦 Handing over \(envelopes.count) courier envelope(s) to \(peerID.id.prefix(8))…", category: .session)
|
SecureLogger.debug("📦 Handing over \(envelopes.count) courier envelope(s) to \(peerID.id.prefix(8))…", category: .session)
|
||||||
for envelope in envelopes {
|
for envelope in envelopes {
|
||||||
guard let payload = envelope.encode() else { continue }
|
guard let payload = envelope.encode() else { continue }
|
||||||
broadcastPacket(makeCourierPacket(payload, to: peerID))
|
sendPacketDirected(makeCourierPacket(payload, to: peerID), to: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3159,21 +3168,14 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleAnnounce(_ packet: BitchatPacket, from peerID: PeerID) {
|
private func handleAnnounce(_ packet: BitchatPacket, from peerID: PeerID) {
|
||||||
announceHandler.handle(packet, from: peerID)
|
let result = announceHandler.handle(packet, from: peerID)
|
||||||
|
|
||||||
// Courier handover: an announce is the moment we learn a peer's Noise
|
// Courier handover: an announce is the moment we learn a peer's Noise
|
||||||
// static key, so check whether we're carrying mail addressed to them.
|
// static key, so check whether we're carrying mail addressed to them.
|
||||||
// Re-run the preflight so handover only follows announces that pass
|
guard !courierStore.isEmpty,
|
||||||
// the key<->peerID binding check (mirrors the handler's own gate).
|
let result,
|
||||||
guard !courierStore.isEmpty else { return }
|
result.isVerified else { return }
|
||||||
if case .accept(let acceptance) = BLEAnnouncePreflightPolicy.evaluate(
|
deliverCourierMail(to: result.peerID, noiseKey: result.announcement.noisePublicKey)
|
||||||
packet: packet,
|
|
||||||
from: peerID,
|
|
||||||
localPeerID: myPeerID,
|
|
||||||
now: Date()
|
|
||||||
) {
|
|
||||||
deliverCourierMail(to: peerID, noiseKey: acceptance.announcement.noisePublicKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the announce handler environment. All queue hops stay here so
|
/// Builds the announce handler environment. All queue hops stay here so
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ struct CourierEndToEndTests {
|
|||||||
lock.lock(); defer { lock.unlock() }
|
lock.lock(); defer { lock.unlock() }
|
||||||
return packets.first { $0.type == type.rawValue }
|
return packets.first { $0.type == type.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func count(ofType type: MessageType) -> Int {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
return packets.filter { $0.type == type.rawValue }.count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class NoiseCaptureDelegate: BitchatDelegate {
|
private final class NoiseCaptureDelegate: BitchatDelegate {
|
||||||
@@ -139,7 +144,7 @@ struct CourierEndToEndTests {
|
|||||||
)
|
)
|
||||||
#expect(announced)
|
#expect(announced)
|
||||||
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
||||||
carol._test_handlePacket(announcePacket, fromPeerID: bob.myPeerID)
|
carol._test_handlePacket(announcePacket, fromPeerID: bob.myPeerID, preseedPeer: false)
|
||||||
|
|
||||||
let handedOver = await TestHelpers.waitUntil(
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
||||||
@@ -167,6 +172,90 @@ struct CourierEndToEndTests {
|
|||||||
#expect(message.content == "the camp moved north")
|
#expect(message.content == "the camp moved north")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func unverifiedAnnounceDoesNotTriggerCourierHandover() async throws {
|
||||||
|
let alice = makeService()
|
||||||
|
let carol = makeService()
|
||||||
|
let bob = makeService()
|
||||||
|
carol.courierDepositPolicy = { _ in true }
|
||||||
|
|
||||||
|
let aliceOut = PacketTap()
|
||||||
|
alice._test_onOutboundPacket = aliceOut.record
|
||||||
|
let carolOut = PacketTap()
|
||||||
|
carol._test_onOutboundPacket = carolOut.record
|
||||||
|
let bobOut = PacketTap()
|
||||||
|
bob._test_onOutboundPacket = bobOut.record
|
||||||
|
|
||||||
|
preseedConnectedPeer(carol, in: alice)
|
||||||
|
|
||||||
|
#expect(alice.sendCourierMessage(
|
||||||
|
"hold until verified",
|
||||||
|
messageID: "courier-msg-unverified-announce",
|
||||||
|
recipientNoiseKey: bob.noiseStaticPublicKeyData(),
|
||||||
|
via: [carol.myPeerID]
|
||||||
|
))
|
||||||
|
let deposited = await TestHelpers.waitUntil(
|
||||||
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(deposited)
|
||||||
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
|
|
||||||
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID)
|
||||||
|
let carried = await TestHelpers.waitUntil(
|
||||||
|
{ !carol.courierStore.isEmpty },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(carried)
|
||||||
|
|
||||||
|
let forgedAnnounce = try makeUnsignedAnnounce(from: bob)
|
||||||
|
carol._test_handlePacket(forgedAnnounce, fromPeerID: bob.myPeerID, preseedPeer: false)
|
||||||
|
|
||||||
|
let leakedOnUnverifiedAnnounce = await TestHelpers.waitUntil(
|
||||||
|
{ carolOut.count(ofType: .courierEnvelope) > 0 },
|
||||||
|
timeout: TestConstants.shortTimeout
|
||||||
|
)
|
||||||
|
#expect(!leakedOnUnverifiedAnnounce)
|
||||||
|
#expect(!carol.courierStore.isEmpty)
|
||||||
|
|
||||||
|
bob.sendBroadcastAnnounce()
|
||||||
|
let announced = await TestHelpers.waitUntil(
|
||||||
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(announced)
|
||||||
|
let verifiedAnnounce = try #require(bobOut.first(ofType: .announce))
|
||||||
|
carol._test_handlePacket(verifiedAnnounce, fromPeerID: bob.myPeerID, preseedPeer: false)
|
||||||
|
|
||||||
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
|
{ carolOut.count(ofType: .courierEnvelope) == 1 },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(handedOver)
|
||||||
|
#expect(carol.courierStore.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sendCourierMessageRejectsInvalidRecipientKeyBeforeQueueing() async throws {
|
||||||
|
let alice = makeService()
|
||||||
|
let carol = makeService()
|
||||||
|
preseedConnectedPeer(carol, in: alice)
|
||||||
|
|
||||||
|
let aliceOut = PacketTap()
|
||||||
|
alice._test_onOutboundPacket = aliceOut.record
|
||||||
|
|
||||||
|
#expect(!alice.sendCourierMessage(
|
||||||
|
"this cannot be sealed",
|
||||||
|
messageID: "courier-msg-invalid-key",
|
||||||
|
recipientNoiseKey: Data(repeating: 0x01, count: 8),
|
||||||
|
via: [carol.myPeerID]
|
||||||
|
))
|
||||||
|
|
||||||
|
let queuedPacket = await TestHelpers.waitUntil(
|
||||||
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
|
timeout: TestConstants.shortTimeout
|
||||||
|
)
|
||||||
|
#expect(!queuedPacket)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func depositFromUntrustedPeerIsRejected() async throws {
|
@Test func depositFromUntrustedPeerIsRejected() async throws {
|
||||||
let carol = makeService()
|
let carol = makeService()
|
||||||
carol.courierDepositPolicy = { _ in false } // depositor is not a mutual favorite
|
carol.courierDepositPolicy = { _ in false } // depositor is not a mutual favorite
|
||||||
@@ -202,6 +291,26 @@ struct CourierEndToEndTests {
|
|||||||
)
|
)
|
||||||
#expect(!stored)
|
#expect(!stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeUnsignedAnnounce(from service: BLEService) throws -> BitchatPacket {
|
||||||
|
let announcement = AnnouncementPacket(
|
||||||
|
nickname: "Unsigned",
|
||||||
|
noisePublicKey: service.noiseStaticPublicKeyData(),
|
||||||
|
signingPublicKey: service.noiseSigningPublicKeyData(),
|
||||||
|
directNeighbors: nil
|
||||||
|
)
|
||||||
|
let payload = try #require(announcement.encode())
|
||||||
|
|
||||||
|
return BitchatPacket(
|
||||||
|
type: MessageType.announce.rawValue,
|
||||||
|
senderID: Data(hexString: service.myPeerID.id) ?? Data(),
|
||||||
|
recipientID: nil,
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: payload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: TransportConfig.messageTTLDefault
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Router courier selection
|
// MARK: - Router courier selection
|
||||||
|
|||||||
@@ -98,8 +98,12 @@ struct BLEAnnounceHandlerTests {
|
|||||||
recorder.upsertResult = BLEPeerAnnounceUpdate(isNewPeer: true, wasDisconnected: false, previousNickname: nil)
|
recorder.upsertResult = BLEPeerAnnounceUpdate(isNewPeer: true, wasDisconnected: false, previousNickname: nil)
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result?.peerID == peerID)
|
||||||
|
#expect(result?.announcement.noisePublicKey == noiseKey)
|
||||||
|
#expect(result?.isDirectAnnounce == true)
|
||||||
|
#expect(result?.isVerified == true)
|
||||||
#expect(recorder.verifySignatureCalls.count == 1)
|
#expect(recorder.verifySignatureCalls.count == 1)
|
||||||
#expect(recorder.verifySignatureCalls.first?.signingPublicKey == Data(repeating: 0x99, count: 32))
|
#expect(recorder.verifySignatureCalls.first?.signingPublicKey == Data(repeating: 0x99, count: 32))
|
||||||
#expect(recorder.barrierCount == 1)
|
#expect(recorder.barrierCount == 1)
|
||||||
@@ -161,8 +165,11 @@ struct BLEAnnounceHandlerTests {
|
|||||||
let recorder = Recorder()
|
let recorder = Recorder()
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result?.peerID == peerID)
|
||||||
|
#expect(result?.announcement.noisePublicKey == noiseKey)
|
||||||
|
#expect(result?.isVerified == false)
|
||||||
#expect(recorder.verifySignatureCalls.isEmpty)
|
#expect(recorder.verifySignatureCalls.isEmpty)
|
||||||
#expect(recorder.barrierCount == 1)
|
#expect(recorder.barrierCount == 1)
|
||||||
#expect(recorder.upsertCalls.isEmpty)
|
#expect(recorder.upsertCalls.isEmpty)
|
||||||
@@ -197,8 +204,9 @@ struct BLEAnnounceHandlerTests {
|
|||||||
recorder.signatureValid = false
|
recorder.signatureValid = false
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result?.isVerified == false)
|
||||||
#expect(recorder.verifySignatureCalls.count == 1)
|
#expect(recorder.verifySignatureCalls.count == 1)
|
||||||
#expect(recorder.upsertCalls.isEmpty)
|
#expect(recorder.upsertCalls.isEmpty)
|
||||||
#expect(recorder.uiEventDeliveries.count == 1)
|
#expect(recorder.uiEventDeliveries.count == 1)
|
||||||
@@ -222,8 +230,9 @@ struct BLEAnnounceHandlerTests {
|
|||||||
let recorder = Recorder()
|
let recorder = Recorder()
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result == nil)
|
||||||
expectNoSideEffects(recorder)
|
expectNoSideEffects(recorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,8 +251,9 @@ struct BLEAnnounceHandlerTests {
|
|||||||
let recorder = Recorder()
|
let recorder = Recorder()
|
||||||
let handler = makeHandler(recorder: recorder, localPeerID: peerID, now: now)
|
let handler = makeHandler(recorder: recorder, localPeerID: peerID, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result == nil)
|
||||||
expectNoSideEffects(recorder)
|
expectNoSideEffects(recorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,8 +273,9 @@ struct BLEAnnounceHandlerTests {
|
|||||||
let recorder = Recorder()
|
let recorder = Recorder()
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result == nil)
|
||||||
expectNoSideEffects(recorder)
|
expectNoSideEffects(recorder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,8 +322,10 @@ struct BLEAnnounceHandlerTests {
|
|||||||
recorder.upsertResult = BLEPeerAnnounceUpdate(isNewPeer: true, wasDisconnected: false, previousNickname: nil)
|
recorder.upsertResult = BLEPeerAnnounceUpdate(isNewPeer: true, wasDisconnected: false, previousNickname: nil)
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result?.isDirectAnnounce == false)
|
||||||
|
#expect(result?.isVerified == true)
|
||||||
#expect(recorder.upsertCalls.count == 1)
|
#expect(recorder.upsertCalls.count == 1)
|
||||||
#expect(recorder.upsertCalls.first?.isConnected == false)
|
#expect(recorder.upsertCalls.first?.isConnected == false)
|
||||||
#expect(recorder.uiEventDeliveries.count == 1)
|
#expect(recorder.uiEventDeliveries.count == 1)
|
||||||
@@ -384,8 +397,9 @@ struct BLEAnnounceHandlerTests {
|
|||||||
recorder.existingNoisePublicKey = Data(repeating: 0xAA, count: 32)
|
recorder.existingNoisePublicKey = Data(repeating: 0xAA, count: 32)
|
||||||
let handler = makeHandler(recorder: recorder, now: now)
|
let handler = makeHandler(recorder: recorder, now: now)
|
||||||
|
|
||||||
handler.handle(packet, from: peerID)
|
let result = handler.handle(packet, from: peerID)
|
||||||
|
|
||||||
|
#expect(result?.isVerified == false)
|
||||||
#expect(recorder.upsertCalls.isEmpty)
|
#expect(recorder.upsertCalls.isEmpty)
|
||||||
#expect(recorder.uiEventDeliveries.count == 1)
|
#expect(recorder.uiEventDeliveries.count == 1)
|
||||||
#expect(recorder.uiEventDeliveries.first?.notifyPeerConnected == false)
|
#expect(recorder.uiEventDeliveries.first?.notifyPeerConnected == false)
|
||||||
|
|||||||
@@ -19,6 +19,79 @@ struct BLEFanoutSelectorTests {
|
|||||||
#expect(selection.centralIDs == Set(["c2"]))
|
#expect(selection.centralIDs == Set(["c2"]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func directedSendUsesOnlyBoundPeripheralLinkWhenAvailable() {
|
||||||
|
let target = PeerID(str: "1122334455667788")
|
||||||
|
let bystander = PeerID(str: "8877665544332211")
|
||||||
|
let selection = BLEFanoutSelector.selectLinks(
|
||||||
|
peripheralIDs: ["target-p", "bystander-p"],
|
||||||
|
centralIDs: ["target-c", "bystander-c"],
|
||||||
|
ingressLink: nil,
|
||||||
|
peripheralPeerBindings: [
|
||||||
|
"target-p": target,
|
||||||
|
"bystander-p": bystander
|
||||||
|
],
|
||||||
|
centralPeerBindings: [
|
||||||
|
"target-c": target,
|
||||||
|
"bystander-c": bystander
|
||||||
|
],
|
||||||
|
directedPeerHint: target,
|
||||||
|
packetType: MessageType.courierEnvelope.rawValue,
|
||||||
|
messageID: "message-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(selection.peripheralIDs == Set(["target-p"]))
|
||||||
|
#expect(selection.centralIDs.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func directedSendUsesBoundCentralLinkWhenNoPeripheralLinkExists() {
|
||||||
|
let target = PeerID(str: "1122334455667788")
|
||||||
|
let bystander = PeerID(str: "8877665544332211")
|
||||||
|
let selection = BLEFanoutSelector.selectLinks(
|
||||||
|
peripheralIDs: ["bystander-p"],
|
||||||
|
centralIDs: ["target-c", "bystander-c"],
|
||||||
|
ingressLink: nil,
|
||||||
|
peripheralPeerBindings: [
|
||||||
|
"bystander-p": bystander
|
||||||
|
],
|
||||||
|
centralPeerBindings: [
|
||||||
|
"target-c": target,
|
||||||
|
"bystander-c": bystander
|
||||||
|
],
|
||||||
|
directedPeerHint: target,
|
||||||
|
packetType: MessageType.courierEnvelope.rawValue,
|
||||||
|
messageID: "message-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(selection.peripheralIDs.isEmpty)
|
||||||
|
#expect(selection.centralIDs == Set(["target-c"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func directedSendToKnownPeerDoesNotFallBackWhenOnlyDirectLinkIsExcluded() {
|
||||||
|
let target = PeerID(str: "1122334455667788")
|
||||||
|
let bystander = PeerID(str: "8877665544332211")
|
||||||
|
let selection = BLEFanoutSelector.selectLinks(
|
||||||
|
peripheralIDs: ["bystander-p"],
|
||||||
|
centralIDs: ["target-c", "bystander-c"],
|
||||||
|
ingressLink: .central("target-c"),
|
||||||
|
peripheralPeerBindings: [
|
||||||
|
"bystander-p": bystander
|
||||||
|
],
|
||||||
|
centralPeerBindings: [
|
||||||
|
"target-c": target,
|
||||||
|
"bystander-c": bystander
|
||||||
|
],
|
||||||
|
directedPeerHint: target,
|
||||||
|
packetType: MessageType.courierEnvelope.rawValue,
|
||||||
|
messageID: "message-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(selection.peripheralIDs.isEmpty)
|
||||||
|
#expect(selection.centralIDs.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func directedSendExcludesAllLinksToIngressPeer() {
|
func directedSendExcludesAllLinksToIngressPeer() {
|
||||||
let selection = BLEFanoutSelector.selectLinks(
|
let selection = BLEFanoutSelector.selectLinks(
|
||||||
|
|||||||
Reference in New Issue
Block a user