From 2663764cf49d12d482fc3e2138ed3964005e89b9 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Tue, 25 Nov 2025 18:23:43 -0500 Subject: [PATCH] fix: Add MAC rotation recovery for stale interface cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a peer reconnects from a new MAC address (common with Android MAC rotation, also possible on Linux after adapter reset), the old interface state may become stale. This change adds: 1. _cleanup_stale_interface() method: - Detaches old interface - Cleans up identity mappings - Removes fragmenter/reassembler for old address - Clears pending MTU for old address 2. MAC rotation detection in _select_peers_to_connect(): - If interface exists for identity but old address is not in peers (connection dead), clean up stale state and allow reconnection - If old connection still active, skip as before (no change) This is defensive code that handles edge cases like: - Android MAC rotation (~15 min intervals) - Linux adapter reset while peer state is tracked - Connection timeout without proper disconnect callback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/RNS/Interfaces/BLEInterface.py | 68 +++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index bcc551b..8cbfa7c 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -1015,6 +1015,46 @@ class BLEInterface(Interface): if address in self.pending_mtu: del self.pending_mtu[address] + def _cleanup_stale_interface(self, identity_hash: str, old_address: str): + """ + Clean up stale interface after MAC rotation. + + Called when we detect the same identity at a new MAC address but the + old connection is no longer alive. This allows reconnection to the + peer at their new MAC address. + + Args: + identity_hash: 16-character hex hash of the peer's identity + old_address: The old MAC address that is no longer valid + """ + # Get peer identity for fragmenter cleanup + peer_identity = self.address_to_identity.get(old_address) + + # Detach and remove old interface + if identity_hash in self.spawned_interfaces: + old_interface = self.spawned_interfaces.pop(identity_hash) + old_interface.detach() + RNS.log(f"{self} detached stale interface for {identity_hash[:8]}", RNS.LOG_DEBUG) + + # Clean up address mappings + if identity_hash in self.identity_to_address: + del self.identity_to_address[identity_hash] + + # Clean up fragmenter/reassembler for old address + if peer_identity: + frag_key = self._get_fragmenter_key(peer_identity, old_address) + with self.frag_lock: + if frag_key in self.fragmenters: + del self.fragmenters[frag_key] + if frag_key in self.reassemblers: + del self.reassemblers[frag_key] + + # Clean up pending MTU for old address + if old_address in self.pending_mtu: + del self.pending_mtu[old_address] + + RNS.log(f"{self} cleaned up stale state for {old_address}", RNS.LOG_DEBUG) + def _error_callback(self, severity: str, message: str, exc: Exception = None): """ Driver callback: Handle driver errors. @@ -1221,15 +1261,33 @@ class BLEInterface(Interface): RNS.LOG_DEBUG) continue - # Protocol v2.2: Skip if interface exists for this identity (any connection type) - # This prevents dual connections (central + peripheral to same peer) + # Protocol v2.2: Skip if interface exists AND is still alive + # This prevents dual connections but allows MAC rotation recovery peer_identity = self.address_to_identity.get(address) if peer_identity: identity_hash = self._compute_identity_hash(peer_identity) if identity_hash in self.spawned_interfaces: - RNS.log(f"{self} [v2.2] skipping {peer.name} - interface exists for identity {identity_hash[:8]}", - RNS.LOG_DEBUG) - continue + # Check if existing interface is still connected + existing_address = self.identity_to_address.get(identity_hash) + if existing_address and existing_address != address: + # Same identity at different MAC = MAC rotation + # Check if old connection is still alive + if existing_address in self.peers: + # Old connection still active - skip (correct behavior) + RNS.log(f"{self} [v2.2] skipping {peer.name} - already connected via {existing_address[-8:]}", + RNS.LOG_DEBUG) + continue + else: + # Old connection dead - clean up and allow new connection + RNS.log(f"{self} [v2.2] MAC rotation: {identity_hash[:8]} moved from {existing_address[-8:]} to {address[-8:]}, cleaning up stale interface", + RNS.LOG_INFO) + self._cleanup_stale_interface(identity_hash, existing_address) + # Fall through to connect to new MAC + elif existing_address == address: + # Same address, interface exists - skip + RNS.log(f"{self} [v2.2] skipping {peer.name} - interface exists for identity {identity_hash[:8]}", + RNS.LOG_DEBUG) + continue # Protocol v2.2: MAC address sorting - deterministic connection direction # Lower MAC initiates (central), higher MAC only accepts (peripheral)