From a4ce8cc979187a2c9f75edbb1dd20b69c16876a7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 30 Oct 2025 00:39:24 +0100 Subject: [PATCH] better tracking of first announce seen (#502) --- .../mesh/BluetoothConnectionManager.kt | 4 ++++ .../mesh/BluetoothConnectionTracker.kt | 20 ++++++++++++++++ .../android/mesh/BluetoothMeshService.kt | 24 +++++++------------ .../bitchat/android/mesh/SecurityManager.kt | 19 +++++++++------ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index f446888e..3ac87764 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -84,6 +84,10 @@ class BluetoothConnectionManager( // Public property for address-peer mapping val addressPeerMap get() = connectionTracker.addressPeerMap + + // Expose first-announce helpers to higher layers + fun noteAnnounceReceived(address: String) { connectionTracker.noteAnnounceReceived(address) } + fun hasSeenFirstAnnounce(address: String): Boolean = connectionTracker.hasSeenFirstAnnounce(address) init { powerManager.delegate = this diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt index 0f0bdd86..745a1a39 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -30,6 +30,8 @@ class BluetoothConnectionTracker( private val connectedDevices = ConcurrentHashMap() private val subscribedDevices = CopyOnWriteArrayList() val addressPeerMap = ConcurrentHashMap() + // Track whether we have seen the first ANNOUNCE on a given device connection + private val firstAnnounceSeen = ConcurrentHashMap() // RSSI tracking from scan results (for devices we discover but may connect as servers) private val scanRSSI = ConcurrentHashMap() @@ -91,6 +93,8 @@ class BluetoothConnectionTracker( Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}") connectedDevices[deviceAddress] = deviceConn pendingConnections.remove(deviceAddress) + // Mark as awaiting first ANNOUNCE on this connection + firstAnnounceSeen[deviceAddress] = false } /** @@ -280,6 +284,7 @@ class BluetoothConnectionTracker( addressPeerMap.remove(deviceAddress) } pendingConnections.remove(deviceAddress) + firstAnnounceSeen.remove(deviceAddress) Log.d(TAG, "Cleaned up device connection for $deviceAddress") } @@ -313,6 +318,21 @@ class BluetoothConnectionTracker( addressPeerMap.clear() pendingConnections.clear() scanRSSI.clear() + firstAnnounceSeen.clear() + } + + /** + * Mark that we have received the first ANNOUNCE over this device connection. + */ + fun noteAnnounceReceived(deviceAddress: String) { + firstAnnounceSeen[deviceAddress] = true + } + + /** + * Check whether the first ANNOUNCE has been seen for a device connection. + */ + fun hasSeenFirstAnnounce(deviceAddress: String): Boolean { + return firstAnnounceSeen[deviceAddress] == true } /** diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 56c640df..ee5f0f16 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -408,25 +408,17 @@ class BluetoothMeshService(private val context: Context) { val deviceAddress = routed.relayAddress val pid = routed.peerID if (deviceAddress != null && pid != null) { - // Only set mapping if not already mapped - if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) { + // First ANNOUNCE over a device connection defines a direct neighbor. + if (!connectionManager.hasSeenFirstAnnounce(deviceAddress)) { + // Bind or rebind this device address to the announcing peer connectionManager.addressPeerMap[deviceAddress] = pid - Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE") + connectionManager.noteAnnounceReceived(deviceAddress) + Log.d(TAG, "Mapped device $deviceAddress to peer $pid on FIRST-ANNOUNCE for this connection") - // Mark this peer as directly connected for UI - try { - peerManager.getPeerInfo(pid)?.let { - // Set direct connection flag - // (This will also trigger a peer list update) - peerManager.setDirectConnection(pid, true) - // Also push reactive directness state to UI (best-effort) - try { - // Note: UI observes via didUpdatePeerList, but we can also update ChatState on a timer - } catch (_: Exception) { } - } - } catch (_: Exception) { } + // Mark as directly connected (upgrades from routed if needed) + try { peerManager.setDirectConnection(pid, true) } catch (_: Exception) { } - // Schedule initial sync for this new directly connected peer only + // Initial sync for this newly direct peer try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } } } diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 476ead73..717d302c 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -67,16 +67,21 @@ class SecurityManager(private val encryptionService: EncryptionService, private // } // Duplicate detection + val messageType = MessageType.fromValue(packet.type) val messageID = generateMessageID(packet, peerID) - if (processedMessages.contains(messageID)) { - Log.d(TAG, "Dropping duplicate packet: $messageID") - return false + if (messageType != MessageType.ANNOUNCE) { + if (processedMessages.contains(messageID)) { + Log.d(TAG, "Dropping duplicate packet: $messageID") + return false + } + // Add to processed messages + processedMessages.add(messageID) + messageTimestamps[messageID] = currentTime + } else { + // Do not deduplicate ANNOUNCE at the security layer. + // They are signed/idempotent and we need to ensure first-announce per-connection can bind. } - // Add to processed messages - processedMessages.add(messageID) - messageTimestamps[messageID] = currentTime - // NEW: Signature verification logging (not rejecting yet) verifyPacketSignatureWithLogging(packet, peerID)