From f21382acd52cd6924e97f19ef905dc80b6bdc8c5 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 31 Dec 2025 22:57:36 -0500 Subject: [PATCH] fix: add identity handshake timeout for non-Reticulum connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connections from non-Reticulum BLE devices (AirTags, BLE scanners, etc.) that connect to our GATT server but never complete the identity handshake are now automatically disconnected after 30 seconds. Changes: - Track pending identity connections with timestamps in _pending_identity_connections - Add _cleanup_pending_identity_connections() to disconnect stale connections - Remove from pending tracking when identity is provided in callback - Add debug logging for cleanup timer operations This prevents non-protocol devices from appearing indefinitely in the BLE connections list. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/ble_reticulum/BLEInterface.py | 58 +++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/ble_reticulum/BLEInterface.py b/src/ble_reticulum/BLEInterface.py index 1cdf78c..459235e 100644 --- a/src/ble_reticulum/BLEInterface.py +++ b/src/ble_reticulum/BLEInterface.py @@ -382,6 +382,11 @@ class BLEInterface(Interface): self._identity_cache = {} self._identity_cache_ttl = 60 # seconds + # Pending connections awaiting identity handshake (address -> timestamp) + # If identity not received within timeout, connection is closed + self._pending_identity_connections = {} + self._pending_identity_timeout = 30 # seconds + # Fragmentation self.fragmenters = {} # address -> BLEFragmenter (per MTU) self.reassemblers = {} # address -> BLEReassembler @@ -679,6 +684,7 @@ class BLEInterface(Interface): self.cleanup_timer = threading.Timer(30.0, self._periodic_cleanup_task) self.cleanup_timer.daemon = True self.cleanup_timer.start() + RNS.log(f"{self} cleanup timer started (30s interval)", RNS.LOG_DEBUG) def _periodic_cleanup_task(self): """ @@ -693,6 +699,8 @@ class BLEInterface(Interface): if not self.online: return # Don't reschedule if interface is offline + RNS.log(f"{self} periodic cleanup running, pending identity connections: {len(self._pending_identity_connections)}", RNS.LOG_DEBUG) + with self.frag_lock: total_cleaned = 0 for peer_address, reassembler in list(self.reassemblers.items()): @@ -709,9 +717,42 @@ class BLEInterface(Interface): # Validate spawned interfaces against actual connections self._validate_spawned_interfaces() + # Check for pending connections that never received identity (timeout) + self._cleanup_pending_identity_connections() + # Reschedule for next cleanup cycle self._start_cleanup_timer() + def _cleanup_pending_identity_connections(self): + """ + Disconnect connections that never completed identity handshake. + + This handles cases like non-Reticulum BLE devices (AirTags, scanners) + that connect to our GATT server but never send the identity handshake. + These connections are tracked when established and disconnected if + identity is not received within the timeout period. + """ + now = time.time() + timed_out = [] + + for address, connect_time in list(self._pending_identity_connections.items()): + elapsed = now - connect_time + if elapsed > self._pending_identity_timeout: + timed_out.append(address) + RNS.log( + f"{self} connection from {address} timed out waiting for identity " + f"({elapsed:.1f}s > {self._pending_identity_timeout}s), disconnecting", + RNS.LOG_WARNING + ) + + # Disconnect timed-out connections + for address in timed_out: + del self._pending_identity_connections[address] + try: + self.driver.disconnect(address) + except Exception as e: + RNS.log(f"{self} error disconnecting timed-out connection {address}: {e}", RNS.LOG_ERROR) + def _validate_spawned_interfaces(self): """ Validate that all spawned interfaces have actual underlying connections. @@ -849,6 +890,10 @@ class BLEInterface(Interface): RNS.log(f"{self} connected to {address} as {role_str}, received identity: {identity_hash}", RNS.LOG_INFO) self._record_connection_success(address) + # Remove from pending identity tracking if it was tracked + if address in self._pending_identity_connections: + del self._pending_identity_connections[address] + # Check for pending MTU (race condition: MTU negotiated before identity) if address in self.pending_mtu: pending_mtu = self.pending_mtu.pop(address) @@ -862,11 +907,15 @@ class BLEInterface(Interface): elif role == "peripheral": # Peripheral mode: identity will arrive via handshake RNS.log(f"{self} connected to {address} as PERIPHERAL, waiting for identity handshake...", RNS.LOG_INFO) + # Track pending connection - will timeout if identity never arrives + self._pending_identity_connections[address] = time.time() # The identity will be received in `_data_received_callback` else: - RNS.log(f"{self} connected to {address}, but identity not provided and role is {role}. Disconnecting.", RNS.LOG_WARNING) - self.driver.disconnect(address) + # Connection without identity from non-peripheral role + # Track as pending - could be a device that connects but never handshakes + RNS.log(f"{self} connected to {address} (role={role}) without identity, tracking for timeout", RNS.LOG_INFO) + self._pending_identity_connections[address] = time.time() def _check_duplicate_identity(self, address: str, peer_identity: bytes) -> bool: """ @@ -1016,6 +1065,11 @@ class BLEInterface(Interface): ) RNS.log(f"{self} identity handshake complete for {address}", RNS.LOG_INFO) + + # Remove from pending identity tracking (no longer waiting for handshake) + if address in self._pending_identity_connections: + del self._pending_identity_connections[address] + return True # Handshake processed successfully except Exception as e: