feat: Implement Request Sync Manager (V2 Sync) (#965)

* feat: Implement Request Sync Manager (V2 Sync)

- Add RequestSyncManager to track and attribute sync requests
- Update BitchatPacket and BinaryProtocol to support IS_RSR flag (0x10)
- Update RequestSyncPacket with new TLV fields (sinceTimestamp, fragmentIdFilter)
- Update GossipSyncManager to use unicast sync requests and mark responses as RSR
- Update BLEService to enforce timestamp validation for normal packets and exempt valid RSRs
- Add documentation for the new sync manager mechanism

* fix: Resolve compilation errors in V2 Sync implementation

- Remove duplicate restartGossipManager in BLEService
- Add missing TransportConfig constants for sync
- Add 'sync' log category to BitLogger
- Add missing BitLogger import in GossipSyncManager

* fix: Update tests for V2 Sync changes

- Add requestSyncManager parameter to GossipSyncManager init in tests
- Implement getConnectedPeers stub in RecordingDelegate
- Remove unused variable warning in SubscriptionRateLimitTests

---------

Co-authored-by: a1denvalu3 <>
Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com>
This commit is contained in:
a1denvalu3
2026-01-17 07:43:02 -10:00
committed by GitHub
co-authored by a1denvalu3 <> jack
parent 9964710de2
commit 3fc64f6168
11 changed files with 312 additions and 28 deletions
+93 -11
View File
@@ -195,6 +195,7 @@ final class BLEService: NSObject {
// MARK: - Gossip Sync
private var gossipSyncManager: GossipSyncManager?
private let requestSyncManager = RequestSyncManager()
// MARK: - Maintenance Timer
@@ -328,6 +329,31 @@ final class BLEService: NSObject {
// Initialize gossip sync manager
restartGossipManager()
}
private func restartGossipManager() {
// Stop existing
gossipSyncManager?.stop()
let config = GossipSyncManager.Config(
seenCapacity: TransportConfig.syncSeenCapacity,
gcsMaxBytes: TransportConfig.syncGCSMaxBytes,
gcsTargetFpr: TransportConfig.syncGCSTargetFpr,
maxMessageAgeSeconds: TransportConfig.syncMaxMessageAgeSeconds,
maintenanceIntervalSeconds: TransportConfig.syncMaintenanceIntervalSeconds,
stalePeerCleanupIntervalSeconds: TransportConfig.syncStalePeerCleanupIntervalSeconds,
stalePeerTimeoutSeconds: TransportConfig.syncStalePeerTimeoutSeconds,
fragmentCapacity: TransportConfig.syncFragmentCapacity,
fileTransferCapacity: TransportConfig.syncFileTransferCapacity,
fragmentSyncIntervalSeconds: TransportConfig.syncFragmentIntervalSeconds,
fileTransferSyncIntervalSeconds: TransportConfig.syncFileTransferIntervalSeconds,
messageSyncIntervalSeconds: TransportConfig.syncMessageIntervalSeconds
)
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
manager.delegate = self
manager.start()
gossipSyncManager = manager
}
// No advertising policy to set; we never include Local Name in adverts.
@@ -794,6 +820,43 @@ final class BLEService: NSObject {
}
}
private func validatePacket(_ packet: BitchatPacket, from peerID: PeerID) -> Bool {
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000)
let messageType = MessageType(rawValue: packet.type)
// 1. Timestamp Validation (Skipped for valid RSR packets)
let isRSR = packet.isRSR
// Treat TTL=0 as legacy RSR if not REQUEST_SYNC
// (Legacy clients send responses with TTL=0 but no flag)
let isLegacyRSR = packet.ttl == 0 && messageType != .requestSync
var skipTimestampCheck = false
if isRSR || isLegacyRSR {
// We check both explicit RSR flag AND legacy TTL=0 packets
if requestSyncManager.isValidResponse(from: peerID, isRSR: true) {
SecureLogger.debug("Valid RSR packet (legacy=\(isLegacyRSR)) from \(peerID.id.prefix(8))… - skipping timestamp check", category: .security)
skipTimestampCheck = true
} else {
SecureLogger.warning("Invalid or unsolicited RSR packet from \(peerID.id.prefix(8))… - rejecting", category: .security)
return false
}
}
if !skipTimestampCheck {
// Enforce timestamp check for normal packets (2 minutes skew)
let maxSkew: UInt64 = 120_000 // 2 minutes in ms
let packetTime = packet.timestamp
let skew = (packetTime > currentTime) ? (packetTime - currentTime) : (currentTime - packetTime)
if skew > maxSkew {
SecureLogger.warning("Packet timestamp skewed by \(skew)ms (max \(maxSkew)ms) from \(peerID.id.prefix(8))", category: .security)
return false
}
}
return true
}
// MARK: - Packet Broadcasting
private func broadcastPacket(_ packet: BitchatPacket, transferId: String? = nil) {
@@ -1547,6 +1610,12 @@ extension BLEService: GossipSyncManager.Delegate {
func signPacketForBroadcast(_ packet: BitchatPacket) -> BitchatPacket {
return noiseService.signPacket(packet) ?? packet
}
func getConnectedPeers() -> [PeerID] {
return collectionsQueue.sync {
peers.values.compactMap { $0.isConnected ? $0.peerID : nil }
}
}
}
// MARK: - CBCentralManagerDelegate
@@ -2130,6 +2199,11 @@ extension BLEService: CBPeripheralDelegate {
SecureLogger.error("❌ Failed to decode assembled notification frame (len=\(frame.count), prefix=\(prefix))", category: .session)
continue
}
// Validate packet (Timestamp/RSR) before processing
let senderID = PeerID(hexData: packet.senderID)
if !validatePacket(packet, from: senderID) {
continue
}
processNotificationPacket(packet, from: peripheral, peripheralUUID: peripheralUUID)
}
}
@@ -2545,6 +2619,12 @@ extension BLEService: CBPeripheralManagerDelegate {
// Clear buffer on success
pendingWriteBuffers.removeValue(forKey: centralUUID)
let senderID = PeerID(hexData: packet.senderID)
// Validate packet (Timestamp/RSR)
if !validatePacket(packet, from: senderID) {
continue
}
if packet.type != MessageType.announce.rawValue {
SecureLogger.debug("📦 Decoded (combined) packet type: \(packet.type) from sender: \(senderID)", category: .session)
}
@@ -2793,13 +2873,7 @@ extension BLEService {
meshTopology.reset()
}
private func restartGossipManager() {
gossipSyncManager?.stop()
let sync = GossipSyncManager(myPeerID: myPeerID)
sync.delegate = self
sync.start()
gossipSyncManager = sync
}
private func sendNoisePayload(_ typedPayload: Data, to peerID: PeerID) {
guard noiseService.hasSession(with: peerID) else {
@@ -3377,7 +3451,8 @@ extension BLEService {
signature: nil,
ttl: packet.ttl,
version: fragmentVersion,
route: packet.route
route: packet.route,
isRSR: packet.isRSR
)
let workItem = DispatchWorkItem { [weak self] in
@@ -3568,9 +3643,16 @@ extension BLEService {
// Decode the original packet bytes we reassembled, so flags/compression are preserved
if var originalPacket = BinaryProtocol.decode(reassembled) {
SecureLogger.debug("✅ Reassembled packet id=\(String(format: "%016llx", fragU64)) type=\(originalPacket.type) bytes=\(reassembled.count)", category: .session)
originalPacket.ttl = 0
handleReceivedPacket(originalPacket, from: peerID)
// Reassembled packet validation
let innerSender = PeerID(hexData: originalPacket.senderID)
if !validatePacket(originalPacket, from: innerSender) {
// Cleanup below
} else {
SecureLogger.debug("✅ Reassembled packet id=\(String(format: "%016llx", fragU64)) type=\(originalPacket.type) bytes=\(reassembled.count)", category: .session)
originalPacket.ttl = 0
handleReceivedPacket(originalPacket, from: peerID)
}
} else {
SecureLogger.error("❌ Failed to decode reassembled packet (type=\(originalType), total=\(total))", category: .session)
}
+14
View File
@@ -212,4 +212,18 @@ enum TransportConfig {
static let uiShareExtensionDismissDelaySeconds: TimeInterval = 2.0
static let uiShareAcceptWindowSeconds: TimeInterval = 30.0
static let uiMigrationCutoffSeconds: TimeInterval = 24 * 60 * 60
// Gossip Sync Configuration
static let syncSeenCapacity: Int = 1000
static let syncGCSMaxBytes: Int = 400
static let syncGCSTargetFpr: Double = 0.01
static let syncMaxMessageAgeSeconds: TimeInterval = 900
static let syncMaintenanceIntervalSeconds: TimeInterval = 30.0
static let syncStalePeerCleanupIntervalSeconds: TimeInterval = 60.0
static let syncStalePeerTimeoutSeconds: TimeInterval = 60.0
static let syncFragmentCapacity: Int = 600
static let syncFileTransferCapacity: Int = 200
static let syncFragmentIntervalSeconds: TimeInterval = 30.0
static let syncFileTransferIntervalSeconds: TimeInterval = 60.0
static let syncMessageIntervalSeconds: TimeInterval = 15.0
}