mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 22:25:19 +00:00
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:
co-authored by
a1denvalu3 <>
jack
parent
9964710de2
commit
3fc64f6168
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user