[codex] Extract BLE service policy helpers (#1321)

* Extract BLE service policy helpers

* Stabilize image media transfer test

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2026-06-01 14:16:44 +02:00
committed by GitHub
co-authored by jack
parent ffa0d7aa4f
commit 193cfdc06a
16 changed files with 1565 additions and 486 deletions
@@ -910,7 +910,7 @@ struct ChatViewModelMediaTransferTests {
viewModel.selectedPrivateChatPeer = peerID
viewModel.sendImage(from: sourceURL)
let didSend = await TestHelpers.waitUntil({ transport.sentPrivateFiles.count == 1 }, timeout: 1.0)
let didSend = await TestHelpers.waitUntil({ transport.sentPrivateFiles.count == 1 }, timeout: 5.0)
#expect(didSend)
#expect(transport.sentPrivateFiles.first?.peerID == peerID)
#expect(transport.sentPrivateFiles.first?.packet.mimeType == "image/jpeg")
@@ -0,0 +1,148 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
@Suite("BLE ingress packet guard tests")
struct BLEIngressPacketGuardTests {
@Test("valid packets return the received and validation peer context")
func validPacketsReturnContext() throws {
let local = PeerID(str: "0011223344556677")
let bound = PeerID(str: "1122334455667788")
let sender = PeerID(str: "8899aabbccddeeff")
let packet = makePacket(sender: sender, timestamp: 1_000)
let context = try #require(success(BLEIngressPacketGuard.evaluate(
packet: packet,
claimedSenderID: sender,
boundPeerID: bound,
localPeerID: local,
directAnnounceTTL: 7,
nowMs: 1_000,
isValidSyncResponse: { _ in false }
)))
#expect(context.receivedFromPeerID == bound)
#expect(context.validationPeerID == sender)
}
@Test("self loopback and direct announce spoofing are rejected before timestamp checks")
func linkBindingRejectionsWinBeforeTimestampChecks() {
let local = PeerID(str: "0011223344556677")
let bound = PeerID(str: "1122334455667788")
let claimed = PeerID(str: "8899aabbccddeeff")
let selfPacket = makePacket(sender: local, timestamp: 0)
let spoofedAnnounce = makePacket(type: .announce, sender: claimed, timestamp: 0, ttl: 7)
let selfResult = BLEIngressPacketGuard.evaluate(
packet: selfPacket,
claimedSenderID: local,
boundPeerID: bound,
localPeerID: local,
directAnnounceTTL: 7,
nowMs: 1_000_000,
isValidSyncResponse: { _ in false }
)
let spoofResult = BLEIngressPacketGuard.evaluate(
packet: spoofedAnnounce,
claimedSenderID: claimed,
boundPeerID: bound,
localPeerID: local,
directAnnounceTTL: 7,
nowMs: 1_000_000,
isValidSyncResponse: { _ in false }
)
#expect(selfResult == .failure(.selfLoopback(packetType: MessageType.message.rawValue)))
#expect(spoofResult == .failure(.directSenderMismatch(boundPeerID: bound, claimedSenderID: claimed)))
}
@Test("timestamp skew outside the window is rejected")
func timestampSkewIsRejected() {
let local = PeerID(str: "0011223344556677")
let sender = PeerID(str: "1122334455667788")
let packet = makePacket(sender: sender, timestamp: 1_000)
let result = BLEIngressPacketGuard.evaluate(
packet: packet,
claimedSenderID: sender,
boundPeerID: nil,
localPeerID: local,
directAnnounceTTL: 7,
nowMs: 200_000,
maxTimestampSkewMs: 120_000,
isValidSyncResponse: { _ in false }
)
#expect(result == .failure(.timestampSkew(
peerID: sender,
skewMs: 199_000,
maxSkewMs: 120_000
)))
}
@Test("valid RSR packets use bound peer validation and bypass timestamp skew")
func validRSRUsesBoundPeerAndBypassesTimestamp() throws {
let local = PeerID(str: "0011223344556677")
let bound = PeerID(str: "1122334455667788")
let sender = PeerID(str: "8899aabbccddeeff")
var packet = makePacket(sender: sender, timestamp: 1)
packet.isRSR = true
let context = try #require(success(BLEIngressPacketGuard.evaluate(
packet: packet,
claimedSenderID: sender,
boundPeerID: bound,
localPeerID: local,
directAnnounceTTL: 7,
nowMs: 1_000_000,
isValidSyncResponse: { $0 == bound }
)))
#expect(context.receivedFromPeerID == bound)
#expect(context.validationPeerID == bound)
}
@Test("invalid RSR packets are rejected")
func invalidRSRIsRejected() {
let local = PeerID(str: "0011223344556677")
let bound = PeerID(str: "1122334455667788")
let sender = PeerID(str: "8899aabbccddeeff")
var packet = makePacket(sender: sender, timestamp: 1)
packet.isRSR = true
let result = BLEIngressPacketGuard.evaluate(
packet: packet,
claimedSenderID: sender,
boundPeerID: bound,
localPeerID: local,
directAnnounceTTL: 7,
nowMs: 1_000_000,
isValidSyncResponse: { _ in false }
)
#expect(result == .failure(.invalidRSR(peerID: bound)))
}
private func makePacket(
type: MessageType = .message,
sender: PeerID,
timestamp: UInt64,
ttl: UInt8 = 3
) -> BitchatPacket {
BitchatPacket(
type: type.rawValue,
senderID: Data(hexString: sender.id) ?? Data(),
recipientID: nil,
timestamp: timestamp,
payload: Data([0x01, 0x02, 0x03]),
signature: nil,
ttl: ttl
)
}
private func success(_ result: Result<BLEIngressPacketContext, BLEIngressPacketGuard.Rejection>) -> BLEIngressPacketContext? {
guard case .success(let context) = result else { return nil }
return context
}
}
@@ -0,0 +1,36 @@
import Foundation
import Testing
@testable import bitchat
@Suite("BLE log rate limiter tests")
struct BLELogRateLimiterTests {
@Test("repeated log keys are suppressed until the default interval elapses")
func suppressesRepeatedKeysUntilIntervalElapses() {
let limiter = BLELogRateLimiter(defaultMinimumInterval: 5)
let now = Date(timeIntervalSince1970: 100)
#expect(limiter.shouldLog(key: "ready:peer-a", now: now))
#expect(!limiter.shouldLog(key: "ready:peer-a", now: now.addingTimeInterval(4.9)))
#expect(limiter.shouldLog(key: "ready:peer-a", now: now.addingTimeInterval(5)))
}
@Test("different log keys are tracked independently")
func tracksKeysIndependently() {
let limiter = BLELogRateLimiter(defaultMinimumInterval: 5)
let now = Date(timeIntervalSince1970: 100)
#expect(limiter.shouldLog(key: "ready:peer-a", now: now))
#expect(limiter.shouldLog(key: "ready:peer-b", now: now))
#expect(!limiter.shouldLog(key: "ready:peer-a", now: now.addingTimeInterval(1)))
}
@Test("call sites can override the default interval")
func supportsPerCallIntervals() {
let limiter = BLELogRateLimiter(defaultMinimumInterval: 5)
let now = Date(timeIntervalSince1970: 100)
#expect(limiter.shouldLog(key: "self-loopback", now: now, minimumInterval: 30))
#expect(!limiter.shouldLog(key: "self-loopback", now: now.addingTimeInterval(29), minimumInterval: 30))
#expect(limiter.shouldLog(key: "self-loopback", now: now.addingTimeInterval(30), minimumInterval: 30))
}
}
@@ -0,0 +1,136 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
@Suite("BLE outbound fragment planner tests")
struct BLEOutboundFragmentPlannerTests {
@Test("planner splits packets and preserves reassembled payload")
func plannerSplitsAndReassemblesPacket() throws {
let packet = makePacket(payload: makePayload(count: 384))
let request = BLEOutboundFragmentTransferRequest(
packet: packet,
pad: false,
maxChunk: 128,
directedPeer: nil,
transferId: nil
)
let plan = try #require(BLEOutboundFragmentPlanner.makePlan(
for: request,
defaultChunkSize: 256,
bleMaxMTU: 512,
fragmentID: Data(repeating: 0xA1, count: 8)
))
let headers = try plan.fragmentPackets.map { try #require(BLEFragmentHeader(packet: $0)) }
let reassembled = headers.reduce(into: Data()) { data, header in
data.append(header.fragmentData)
}
let decoded = try #require(BinaryProtocol.decode(reassembled))
#expect(plan.fragmentVersion == 1)
#expect(plan.chunkSize == 128)
#expect(plan.spacingMs == TransportConfig.bleFragmentSpacingMs)
#expect(headers.map(\.index) == Array(0..<headers.count))
#expect(decoded.type == packet.type)
#expect(decoded.payload == packet.payload)
#expect(plan.fragmentPackets.allSatisfy { $0.recipientID == nil })
}
@Test("directed fragments target the directed peer and use directed pacing")
func directedFragmentsUseDirectedRecipientAndSpacing() throws {
let directedPeer = PeerID(str: "8877665544332211")
let packet = makePacket(payload: makePayload(count: 256))
let request = BLEOutboundFragmentTransferRequest(
packet: packet,
pad: false,
maxChunk: 96,
directedPeer: directedPeer,
transferId: nil
)
let plan = try #require(BLEOutboundFragmentPlanner.makePlan(
for: request,
defaultChunkSize: 256,
bleMaxMTU: 512,
fragmentID: Data(repeating: 0xB2, count: 8)
))
#expect(plan.spacingMs == TransportConfig.bleFragmentSpacingDirectedMs)
#expect(plan.fragmentPackets.allSatisfy { $0.recipientID == Data(hexString: directedPeer.id) })
}
@Test("route-aware fragments use version two and route-sized chunking")
func routeAwareFragmentsUseVersionTwoAndRouteSizedChunking() throws {
let route = [
Data([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]),
Data([0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27])
]
let packet = makePacket(payload: makePayload(count: 192), route: route, isRSR: true)
let request = BLEOutboundFragmentTransferRequest(
packet: packet,
pad: false,
maxChunk: nil,
directedPeer: nil,
transferId: nil
)
let plan = try #require(BLEOutboundFragmentPlanner.makePlan(
for: request,
defaultChunkSize: 256,
bleMaxMTU: 128,
fragmentID: Data(repeating: 0xC3, count: 8)
))
let firstHeader = try #require(BLEFragmentHeader(packet: plan.fragmentPackets[0]))
#expect(plan.fragmentVersion == 2)
#expect(plan.chunkSize == 64)
#expect(firstHeader.fragmentData.count <= 64)
#expect(plan.fragmentPackets.allSatisfy { $0.route == route && $0.isRSR })
}
@Test("invalid fragment IDs do not produce a plan")
func invalidFragmentIDReturnsNil() {
let packet = makePacket(payload: makePayload(count: 128))
let request = BLEOutboundFragmentTransferRequest(
packet: packet,
pad: false,
maxChunk: nil,
directedPeer: nil,
transferId: nil
)
#expect(BLEOutboundFragmentPlanner.makePlan(
for: request,
defaultChunkSize: 256,
bleMaxMTU: 512,
fragmentID: Data(repeating: 0x00, count: 7)
) == nil)
}
private func makePacket(
payload: Data,
route: [Data]? = nil,
isRSR: Bool = false
) -> BitchatPacket {
BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]),
recipientID: nil,
timestamp: 0x0102030405,
payload: payload,
signature: nil,
ttl: 3,
route: route,
isRSR: isRSR
)
}
private func makePayload(count: Int, seed: UInt64 = 0xABCD_1234) -> Data {
var state = seed
return Data((0..<count).map { _ in
state = state &* 6364136223846793005 &+ 1442695040888963407
return UInt8(truncatingIfNeeded: state >> 32)
})
}
}
@@ -0,0 +1,111 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
@Suite("BLE peer registry tests")
struct BLEPeerRegistryTests {
@Test("upserted announces track new, reconnect, and rename transitions")
func upsertVerifiedAnnounceTracksTransitions() {
var registry = BLEPeerRegistry()
let peerID = PeerID(str: "1122334455667788")
let firstSeen = Date(timeIntervalSince1970: 100)
let first = registry.upsertVerifiedAnnounce(
peerID: peerID,
nickname: "alice",
noisePublicKey: Data([1, 2, 3]),
signingPublicKey: Data([4, 5, 6]),
isConnected: true,
now: firstSeen
)
#expect(first.isNewPeer)
#expect(!first.wasDisconnected)
#expect(first.previousNickname == nil)
#expect(registry.connectedPeerIDs == [peerID])
#expect(registry.nickname(for: peerID, connectedOnly: true) == "alice")
registry.markDisconnected(peerID)
let reconnect = registry.upsertVerifiedAnnounce(
peerID: peerID,
nickname: "alice-renamed",
noisePublicKey: Data([1, 2, 3]),
signingPublicKey: Data([4, 5, 6]),
isConnected: true,
now: firstSeen.addingTimeInterval(1)
)
#expect(!reconnect.isNewPeer)
#expect(reconnect.wasDisconnected)
#expect(reconnect.previousNickname == "alice")
#expect(registry.info(for: peerID)?.nickname == "alice-renamed")
}
@Test("reachability keeps recent verified offline peers only when mesh is attached")
func reachabilityRequiresMeshAttachmentForOfflinePeers() {
let offlinePeer = PeerID(str: "1122334455667788")
let connectedPeer = PeerID(str: "8877665544332211")
let now = Date()
var isolatedRegistry = BLEPeerRegistry()
isolatedRegistry.upsert(BLEPeerInfo(
peerID: offlinePeer,
nickname: "offline",
isConnected: false,
noisePublicKey: nil,
signingPublicKey: nil,
isVerifiedNickname: true,
lastSeen: now
))
#expect(!isolatedRegistry.isReachable(offlinePeer, now: now))
var attachedRegistry = isolatedRegistry
attachedRegistry.upsert(BLEPeerInfo(
peerID: connectedPeer,
nickname: "connected",
isConnected: true,
noisePublicKey: nil,
signingPublicKey: nil,
isVerifiedNickname: true,
lastSeen: now
))
#expect(attachedRegistry.isReachable(offlinePeer, now: now))
}
@Test("connectivity reconciliation disconnects inactive peers and prunes expired offline peers")
func reconcileConnectivityUpdatesAndPrunesPeerState() {
var registry = BLEPeerRegistry()
let inactiveConnectedPeer = PeerID(str: "1122334455667788")
let expiredOfflinePeer = PeerID(str: "8877665544332211")
let now = Date()
registry.upsert(BLEPeerInfo(
peerID: inactiveConnectedPeer,
nickname: "inactive",
isConnected: true,
noisePublicKey: nil,
signingPublicKey: nil,
isVerifiedNickname: true,
lastSeen: now.addingTimeInterval(-TransportConfig.blePeerInactivityTimeoutSeconds - 1)
))
registry.upsert(BLEPeerInfo(
peerID: expiredOfflinePeer,
nickname: "expired",
isConnected: false,
noisePublicKey: nil,
signingPublicKey: nil,
isVerifiedNickname: true,
lastSeen: .distantPast
))
let changes = registry.reconcileConnectivity(now: now, linkStates: [:])
#expect(changes.disconnectedPeerIDs == [inactiveConnectedPeer])
#expect(changes.removedPeers.map(\.peerID) == [expiredOfflinePeer])
#expect(registry.info(for: inactiveConnectedPeer)?.isConnected == false)
#expect(registry.info(for: expiredOfflinePeer) == nil)
}
}
@@ -0,0 +1,78 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
@Suite("BLE peer sender display name tests")
struct BLEPeerSenderDisplayNameTests {
@Test("local peer resolves to local nickname")
func localPeerUsesLocalNickname() {
let local = PeerID(str: "1122334455667788")
#expect(BLEPeerSenderDisplayName.resolveKnownPeer(
peerID: local,
localPeerID: local,
localNickname: "me",
peers: [:],
allowConnectedUnverified: false
) == "me")
}
@Test("verified nickname collisions add peer suffix")
func verifiedCollisionAddsSuffix() {
let local = PeerID(str: "1122334455667788")
let peer = PeerID(str: "8877665544332211")
let peers = [
peer: makeInfo(peerID: peer, nickname: "sam", isConnected: false, isVerifiedNickname: true)
]
#expect(BLEPeerSenderDisplayName.resolveKnownPeer(
peerID: peer,
localPeerID: local,
localNickname: "sam",
peers: peers,
allowConnectedUnverified: false
) == "sam#8877")
}
@Test("connected unverified fallback is opt in")
func connectedUnverifiedFallbackIsOptIn() {
let local = PeerID(str: "1122334455667788")
let peer = PeerID(str: "8877665544332211")
let peers = [
peer: makeInfo(peerID: peer, nickname: "", isConnected: true, isVerifiedNickname: false)
]
#expect(BLEPeerSenderDisplayName.resolveKnownPeer(
peerID: peer,
localPeerID: local,
localNickname: "me",
peers: peers,
allowConnectedUnverified: false
) == nil)
#expect(BLEPeerSenderDisplayName.resolveKnownPeer(
peerID: peer,
localPeerID: local,
localNickname: "me",
peers: peers,
allowConnectedUnverified: true
) == "anon8877")
}
private func makeInfo(
peerID: PeerID,
nickname: String,
isConnected: Bool,
isVerifiedNickname: Bool
) -> BLEPeerInfo {
BLEPeerInfo(
peerID: peerID,
nickname: nickname,
isConnected: isConnected,
noisePublicKey: nil,
signingPublicKey: nil,
isVerifiedNickname: isVerifiedNickname,
lastSeen: Date(timeIntervalSince1970: 0)
)
}
}
@@ -0,0 +1,105 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
@Suite("BLE receive pipeline tests")
struct BLEReceivePipelineTests {
@Test("context includes sender, type-scoped message ID, and logging policy")
func contextBuildsTypeScopedMessageID() {
let sender = PeerID(str: "1122334455667788")
let local = PeerID(str: "8877665544332211")
let packet = makePacket(type: .message, sender: sender, timestamp: 1234)
let context = BLEReceivePipeline.context(for: packet, localPeerID: local)
#expect(context.senderID == sender)
#expect(context.messageID == "\(sender)-1234-\(MessageType.message.rawValue)")
#expect(context.messageType == .message)
#expect(context.shouldDeduplicate)
#expect(context.logsHandlingDetails)
}
@Test("fragments and self sync replays bypass global deduplication")
func contextBypassesDeduplicationForFragmentsAndSelfSyncReplay() {
let local = PeerID(str: "1122334455667788")
let remote = PeerID(str: "8877665544332211")
let fragment = BLEReceivePipeline.context(
for: makePacket(type: .fragment, sender: remote),
localPeerID: local
)
let selfReplay = BLEReceivePipeline.context(
for: makePacket(type: .message, sender: local, ttl: 0),
localPeerID: local
)
#expect(!fragment.shouldDeduplicate)
#expect(!selfReplay.shouldDeduplicate)
}
@Test("dense duplicate traffic cancels pending relays but sparse traffic does not")
func duplicateRelayCancellationUsesGraphDensity() {
#expect(!BLEReceivePipeline.shouldCancelScheduledRelayForDuplicate(connectedPeerCount: 2))
#expect(BLEReceivePipeline.shouldCancelScheduledRelayForDuplicate(connectedPeerCount: 3))
}
@Test("relay decision maps packet context and suppresses local recipient traffic")
func relayDecisionSuppressesLocalRecipientTraffic() {
let sender = PeerID(str: "1122334455667788")
let local = PeerID(str: "8877665544332211")
let packet = makePacket(
type: .noiseEncrypted,
sender: sender,
recipient: local,
ttl: 7
)
let decision = BLEReceivePipeline.relayDecision(
for: packet,
senderID: sender,
localPeerID: local,
degree: 3,
highDegreeThreshold: TransportConfig.bleHighDegreeThreshold
)
#expect(!decision.shouldRelay)
}
@Test("recent traffic tracker prunes by count and time window")
func recentTrafficTrackerPrunesByCountAndWindow() {
var tracker = BLERecentTrafficTracker()
let now = Date(timeIntervalSince1970: 1_000)
tracker.recordPacket(at: now.addingTimeInterval(-TransportConfig.bleRecentPacketWindowSeconds - 1))
#expect(!tracker.hasTraffic(within: TransportConfig.bleRecentPacketWindowSeconds, now: now))
for index in 0...TransportConfig.bleRecentPacketWindowMaxCount {
tracker.recordPacket(at: now.addingTimeInterval(Double(index) * 0.001))
}
#expect(tracker.count == TransportConfig.bleRecentPacketWindowMaxCount)
#expect(tracker.hasTraffic(within: 1.0, now: now.addingTimeInterval(0.1)))
tracker.removeAll()
#expect(tracker.count == 0)
}
private func makePacket(
type: MessageType,
sender: PeerID,
recipient: PeerID? = nil,
timestamp: UInt64 = 1,
ttl: UInt8 = 7
) -> BitchatPacket {
BitchatPacket(
type: type.rawValue,
senderID: Data(hexString: sender.id) ?? Data(),
recipientID: recipient.flatMap { Data(hexString: $0.id) },
timestamp: timestamp,
payload: Data([0x01, 0x02]),
signature: nil,
ttl: ttl
)
}
}
@@ -0,0 +1,61 @@
import Foundation
import Testing
@testable import bitchat
@Suite("BLE subscription announce limiter tests")
struct BLESubscriptionAnnounceLimiterTests {
@Test("first subscription is allowed and repeated subscriptions are rate limited")
func repeatedSubscriptionsAreRateLimited() {
var limiter = BLESubscriptionAnnounceLimiter()
let centralID = "central-a"
let now = Date(timeIntervalSince1970: 100)
#expect(limiter.decision(for: centralID, now: now) == .allowed)
#expect(limiter.trackedCentralCount == 1)
let second = limiter.decision(for: centralID, now: now.addingTimeInterval(0.1))
#expect(second == .rateLimited(
backoffSeconds: TransportConfig.bleSubscriptionRateLimitMinSeconds,
attemptCount: 1,
suppressAnnounce: false
))
}
@Test("rapid subscription attempts eventually suppress announces")
func rapidAttemptsSuppressAnnouncesAtThreshold() {
var limiter = BLESubscriptionAnnounceLimiter()
let centralID = "central-a"
let now = Date(timeIntervalSince1970: 100)
#expect(limiter.decision(for: centralID, now: now) == .allowed)
var decision = BLESubscriptionAnnounceDecision.allowed
for attempt in 2...TransportConfig.bleSubscriptionRateLimitMaxAttempts {
decision = limiter.decision(
for: centralID,
now: now.addingTimeInterval(Double(attempt) * 0.01)
)
}
if case let .rateLimited(_, _, suppressAnnounce) = decision {
#expect(suppressAnnounce)
} else {
Issue.record("Expected rate-limited decision at suppression threshold")
}
}
@Test("stale limiter entries are pruned on the next decision")
func staleEntriesArePruned() {
var limiter = BLESubscriptionAnnounceLimiter()
let staleCentralID = "central-a"
let freshCentralID = "central-b"
let now = Date(timeIntervalSince1970: 100)
#expect(limiter.decision(for: staleCentralID, now: now) == .allowed)
#expect(limiter.trackedCentralCount == 1)
let afterWindow = now.addingTimeInterval(TransportConfig.bleSubscriptionRateLimitWindowSeconds + 1)
#expect(limiter.decision(for: freshCentralID, now: afterWindow) == .allowed)
#expect(limiter.trackedCentralCount == 1)
}
}