mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 21:05:18 +00:00
Fix duplicate peer entries when device restarts
- Added cleanup timer to remove stale peers after 60 seconds of inactivity - Track last seen timestamp for all peers on packet receipt - Detect and remove stale peer IDs with duplicate nicknames in announce handler - Clean up all associated data when removing stale peers - Notify UI immediately when stale peers are removed This fixes the issue where restarting the iPhone app would show duplicate "jack" entries on macOS. Now when a device reconnects with a new peer ID but same nickname, the old entry is automatically removed.
This commit is contained in:
@@ -55,6 +55,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
private var announcedPeers = Set<String>() // Track peers who have already been announced
|
private var announcedPeers = Set<String>() // Track peers who have already been announced
|
||||||
private var processedKeyExchanges = Set<String>() // Track processed key exchanges to prevent duplicates
|
private var processedKeyExchanges = Set<String>() // Track processed key exchanges to prevent duplicates
|
||||||
private var intentionalDisconnects = Set<String>() // Track peripherals we're disconnecting intentionally
|
private var intentionalDisconnects = Set<String>() // Track peripherals we're disconnecting intentionally
|
||||||
|
private var peerLastSeenTimestamps: [String: Date] = [:] // Track when we last heard from each peer
|
||||||
|
private var cleanupTimer: Timer? // Timer to clean up stale peers
|
||||||
|
|
||||||
// Store-and-forward message cache
|
// Store-and-forward message cache
|
||||||
private struct StoredMessage {
|
private struct StoredMessage {
|
||||||
@@ -86,6 +88,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
private var peerListUpdateTimer: Timer?
|
private var peerListUpdateTimer: Timer?
|
||||||
private let peerListUpdateDebounceInterval: TimeInterval = 0.1 // 100ms debounce for more responsive updates
|
private let peerListUpdateDebounceInterval: TimeInterval = 0.1 // 100ms debounce for more responsive updates
|
||||||
|
|
||||||
|
// Stale peer cleanup
|
||||||
|
private var stalePeerCleanupTimer: Timer?
|
||||||
|
private var peerLastSeenTimestamps: [String: Date] = [:] // Track when we last saw each peer
|
||||||
|
|
||||||
// Cover traffic for privacy
|
// Cover traffic for privacy
|
||||||
private var coverTrafficTimer: Timer?
|
private var coverTrafficTimer: Timer?
|
||||||
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
|
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
|
||||||
@@ -293,6 +299,11 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start stale peer cleanup timer (every 30 seconds)
|
||||||
|
cleanupTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||||
|
self?.cleanupStalePeers()
|
||||||
|
}
|
||||||
|
|
||||||
// Register for app termination notifications
|
// Register for app termination notifications
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
@@ -318,6 +329,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
coverTrafficTimer?.invalidate()
|
coverTrafficTimer?.invalidate()
|
||||||
bloomFilterResetTimer?.invalidate()
|
bloomFilterResetTimer?.invalidate()
|
||||||
aggregationTimer?.invalidate()
|
aggregationTimer?.invalidate()
|
||||||
|
cleanupTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
@@ -359,6 +371,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Clear announcement tracking
|
// Clear announcement tracking
|
||||||
announcedToPeers.removeAll()
|
announcedToPeers.removeAll()
|
||||||
|
|
||||||
|
// Clear last seen timestamps
|
||||||
|
peerLastSeenTimestamps.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServices() {
|
func startServices() {
|
||||||
@@ -768,7 +783,17 @@ class BluetoothMeshService: NSObject {
|
|||||||
peerID != myPeerID
|
peerID != myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// print("[DEBUG] Active peers: \(peersCopy), Valid peers: \(validPeers)")
|
// Get nicknames for logging
|
||||||
|
peerNicknamesLock.lock()
|
||||||
|
let peerNicknamesCopy = peerNicknames
|
||||||
|
peerNicknamesLock.unlock()
|
||||||
|
|
||||||
|
let peerInfo = validPeers.map { peerID in
|
||||||
|
let nickname = peerNicknamesCopy[peerID] ?? "unknown"
|
||||||
|
return "\(peerID):\(nickname)"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[PEERS] Active peers: \(peerInfo.joined(separator: ", "))")
|
||||||
return Array(validPeers).sorted()
|
return Array(validPeers).sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +828,44 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up stale peers that haven't been seen in a while
|
||||||
|
private func cleanupStalePeers() {
|
||||||
|
let staleThreshold: TimeInterval = 60.0 // 60 seconds
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
activePeersLock.lock()
|
||||||
|
let peersToRemove = activePeers.filter { peerID in
|
||||||
|
if let lastSeen = peerLastSeenTimestamps[peerID] {
|
||||||
|
return now.timeIntervalSince(lastSeen) > staleThreshold
|
||||||
|
}
|
||||||
|
return false // Keep peers we haven't tracked yet
|
||||||
|
}
|
||||||
|
|
||||||
|
for peerID in peersToRemove {
|
||||||
|
activePeers.remove(peerID)
|
||||||
|
peerLastSeenTimestamps.removeValue(forKey: peerID)
|
||||||
|
|
||||||
|
// Clean up all associated data
|
||||||
|
connectedPeripherals.removeValue(forKey: peerID)
|
||||||
|
peerRSSI.removeValue(forKey: peerID)
|
||||||
|
announcedPeers.remove(peerID)
|
||||||
|
announcedToPeers.remove(peerID)
|
||||||
|
processedKeyExchanges.removeAll { $0.contains(peerID) }
|
||||||
|
|
||||||
|
peerNicknamesLock.lock()
|
||||||
|
let nickname = peerNicknames[peerID]
|
||||||
|
peerNicknames.removeValue(forKey: peerID)
|
||||||
|
peerNicknamesLock.unlock()
|
||||||
|
|
||||||
|
// print("[CLEANUP] Removed stale peer \(peerID) (\(nickname ?? "unknown"))")
|
||||||
|
}
|
||||||
|
activePeersLock.unlock()
|
||||||
|
|
||||||
|
if !peersToRemove.isEmpty {
|
||||||
|
notifyPeerListUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Store-and-Forward Methods
|
// MARK: - Store-and-Forward Methods
|
||||||
|
|
||||||
private func cacheMessage(_ packet: BitchatPacket, messageID: String) {
|
private func cacheMessage(_ packet: BitchatPacket, messageID: String) {
|
||||||
@@ -1056,6 +1119,12 @@ class BluetoothMeshService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update last seen timestamp for this peer
|
||||||
|
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8),
|
||||||
|
senderID != "unknown" && senderID != self.myPeerID {
|
||||||
|
peerLastSeenTimestamps[senderID] = Date()
|
||||||
|
}
|
||||||
|
|
||||||
// Replay attack protection: Check timestamp is within reasonable window (5 minutes)
|
// Replay attack protection: Check timestamp is within reasonable window (5 minutes)
|
||||||
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
|
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
|
||||||
let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp))
|
let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp))
|
||||||
@@ -1392,7 +1461,60 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Check if we've already announced this peer
|
// Check if we've already announced this peer
|
||||||
let isFirstAnnounce = !announcedPeers.contains(senderID)
|
let isFirstAnnounce = !announcedPeers.contains(senderID)
|
||||||
|
|
||||||
|
// Clean up stale peer IDs with the same nickname
|
||||||
peerNicknamesLock.lock()
|
peerNicknamesLock.lock()
|
||||||
|
var stalePeerIDs: [String] = []
|
||||||
|
for (existingPeerID, existingNickname) in peerNicknames {
|
||||||
|
if existingNickname == nickname && existingPeerID != senderID {
|
||||||
|
// Found a stale peer ID with the same nickname
|
||||||
|
stalePeerIDs.append(existingPeerID)
|
||||||
|
// print("[ANNOUNCE] Found stale peer ID \(existingPeerID) with same nickname '\(nickname)' as new peer \(senderID)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stale peer IDs
|
||||||
|
for stalePeerID in stalePeerIDs {
|
||||||
|
// print("[ANNOUNCE] Removing stale peer \(stalePeerID) -> '\(nickname)' (replaced by \(senderID))")
|
||||||
|
peerNicknames.removeValue(forKey: stalePeerID)
|
||||||
|
|
||||||
|
// Also remove from active peers
|
||||||
|
activePeersLock.lock()
|
||||||
|
activePeers.remove(stalePeerID)
|
||||||
|
activePeersLock.unlock()
|
||||||
|
|
||||||
|
// Remove from announced peers
|
||||||
|
announcedPeers.remove(stalePeerID)
|
||||||
|
announcedToPeers.remove(stalePeerID)
|
||||||
|
|
||||||
|
// Disconnect any peripherals associated with stale ID
|
||||||
|
if let peripheral = connectedPeripherals[stalePeerID] {
|
||||||
|
intentionalDisconnects.insert(peripheral.identifier.uuidString)
|
||||||
|
centralManager.cancelPeripheralConnection(peripheral)
|
||||||
|
connectedPeripherals.removeValue(forKey: stalePeerID)
|
||||||
|
peripheralCharacteristics.removeValue(forKey: peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove RSSI data
|
||||||
|
peerRSSI.removeValue(forKey: stalePeerID)
|
||||||
|
|
||||||
|
// Clear cached messages tracking
|
||||||
|
cachedMessagesSentToPeer.remove(stalePeerID)
|
||||||
|
|
||||||
|
// Remove from last seen timestamps
|
||||||
|
peerLastSeenTimestamps.removeValue(forKey: stalePeerID)
|
||||||
|
|
||||||
|
// Remove from processed key exchanges
|
||||||
|
processedKeyExchanges.removeAll { $0.contains(stalePeerID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had stale peers, notify the UI immediately
|
||||||
|
if !stalePeerIDs.isEmpty {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.notifyPeerListUpdate(immediate: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now add the new peer ID with the nickname
|
||||||
peerNicknames[senderID] = nickname
|
peerNicknames[senderID] = nickname
|
||||||
peerNicknamesLock.unlock()
|
peerNicknamesLock.unlock()
|
||||||
|
|
||||||
@@ -1406,7 +1528,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
let wasInserted = activePeers.insert(senderID).inserted
|
let wasInserted = activePeers.insert(senderID).inserted
|
||||||
activePeersLock.unlock()
|
activePeersLock.unlock()
|
||||||
if wasInserted {
|
if wasInserted {
|
||||||
// print("[DEBUG] Added peer \(senderID) to active peers via announce")
|
print("[ANNOUNCE] Added peer \(senderID) (\(nickname)) to active peers")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show join message only for first announce
|
// Show join message only for first announce
|
||||||
@@ -2270,4 +2392,71 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Prefix with dummy marker (will be encrypted)
|
// Prefix with dummy marker (will be encrypted)
|
||||||
return coverTrafficPrefix + (templates.randomElement() ?? "ok")
|
return coverTrafficPrefix + (templates.randomElement() ?? "ok")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Stale Peer Cleanup
|
||||||
|
|
||||||
|
private func cleanupStalePeers() {
|
||||||
|
let staleThreshold: TimeInterval = 60.0 // Consider peers stale after 60 seconds of no activity
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
var peersToRemove: [String] = []
|
||||||
|
|
||||||
|
// Check for stale peers
|
||||||
|
activePeersLock.lock()
|
||||||
|
for peerID in activePeers {
|
||||||
|
if let lastSeen = peerLastSeenTimestamps[peerID] {
|
||||||
|
if now.timeIntervalSince(lastSeen) > staleThreshold {
|
||||||
|
peersToRemove.append(peerID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No timestamp recorded, consider it stale
|
||||||
|
peersToRemove.append(peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activePeersLock.unlock()
|
||||||
|
|
||||||
|
// Remove stale peers
|
||||||
|
for peerID in peersToRemove {
|
||||||
|
print("[CLEANUP] Removing stale peer \(peerID) - last seen: \(peerLastSeenTimestamps[peerID]?.description ?? "never")")
|
||||||
|
|
||||||
|
// Remove from all tracking structures
|
||||||
|
activePeersLock.lock()
|
||||||
|
activePeers.remove(peerID)
|
||||||
|
activePeersLock.unlock()
|
||||||
|
|
||||||
|
peerNicknamesLock.lock()
|
||||||
|
let nickname = peerNicknames[peerID]
|
||||||
|
peerNicknames.removeValue(forKey: peerID)
|
||||||
|
peerNicknamesLock.unlock()
|
||||||
|
|
||||||
|
// Remove from other tracking
|
||||||
|
announcedPeers.remove(peerID)
|
||||||
|
announcedToPeers.remove(peerID)
|
||||||
|
peerRSSI.removeValue(forKey: peerID)
|
||||||
|
peerLastSeenTimestamps.removeValue(forKey: peerID)
|
||||||
|
cachedMessagesSentToPeer.remove(peerID)
|
||||||
|
|
||||||
|
// Disconnect any associated peripherals
|
||||||
|
if let peripheral = connectedPeripherals[peerID] {
|
||||||
|
intentionalDisconnects.insert(peripheral.identifier.uuidString)
|
||||||
|
centralManager.cancelPeripheralConnection(peripheral)
|
||||||
|
connectedPeripherals.removeValue(forKey: peerID)
|
||||||
|
peripheralCharacteristics.removeValue(forKey: peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the cleanup
|
||||||
|
if let nick = nickname {
|
||||||
|
print("[CLEANUP] Removed stale peer: \(nick) (\(peerID))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify UI if any peers were removed
|
||||||
|
if !peersToRemove.isEmpty {
|
||||||
|
notifyPeerListUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePeerLastSeen(_ peerID: String) {
|
||||||
|
peerLastSeenTimestamps[peerID] = Date()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user