From e7e9189983ce0dc2bd38b2e9ac1b677c4b1d3d06 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Fri, 31 Oct 2025 19:57:25 -0400 Subject: [PATCH] feat: Add identity exchange in connection handshake for true unified interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhances BLE Protocol v2 handshake to include the central's identity (16 bytes) instead of empty bytes. This enables the peripheral side to create identity-based unified interfaces even without discovering the central via scanning. **Problem Solved:** - Peripheral couldn't create identity-based interface without scanning the central - Resulted in separate "legacy" and identity-based interfaces for same peer - Prevented true interface unification in asymmetric discovery scenarios **Solution:** 1. Central sends its own identity (16 bytes) in handshake write 2. Peripheral detects identity handshake (16 bytes, first write) 3. Peripheral extracts identity and migrates interface from legacy to identity-based 4. Both sides now have identity-based interfaces that can unify! **Changes:** **_connect_to_peer() (line 1487):** ```python # OLD: await client.write_gatt_char(RX_UUID, b'', response=True) # NEW: Send our own identity in handshake our_identity = self.gatt_server.identity_value if self.gatt_server else b'\x00' * 16 await client.write_gatt_char(RX_UUID, our_identity, response=True) ``` **handle_peripheral_data() (line 1792):** ```python # Detect identity handshake (16 bytes, first write) if len(data) == 16 and sender_address not in self.address_to_identity: central_identity = bytes(data) central_identity_hash = RNS.Identity.full_hash(central_identity)[:16].hex()[:16] # Store identity mapping self.address_to_identity[sender_address] = central_identity self.identity_to_address[central_identity_hash] = sender_address # Migrate interface from legacy to identity-based tracking legacy_conn_id = f"{sender_address}-peripheral" if legacy_conn_id in self.spawned_interfaces: legacy_if = self.spawned_interfaces[legacy_conn_id] del self.spawned_interfaces[legacy_conn_id] legacy_if.peer_identity = central_identity self.spawned_interfaces[central_identity_hash] = legacy_if return # Don't process handshake as fragment data ``` **Flow:** 1. Pi1 connects to Pi2 as central 2. Pi1 reads Pi2's identity → creates identity-based interface 3. Pi1 sends handshake WITH Pi1's identity 4. Pi2 receives handshake, extracts Pi1's identity 5. Pi2 migrates interface to identity-based tracking 6. When Pi2 later discovers Pi1, adds central connection to SAME interface 7. Result: Both Pis have unified "central+peripheral" interfaces! **Benefits:** - ✅ Works with asymmetric discovery (only one side scans) - ✅ Enables true unified interfaces in all scenarios - ✅ Solves Android backgrounding (peripheral gets central's identity immediately) - ✅ Faster interface unification (don't wait for bidirectional discovery) **Backward Compatibility:** - Protocol v1 devices send/receive empty handshake, work as before - Handshake size detection (0 vs 16 bytes) determines protocol version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/RNS/Interfaces/BLEInterface.py | 66 ++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index b19715b..007844b 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -1484,13 +1484,15 @@ class BLEInterface(Interface): RNS.log(f"{self} failed to read identity from {peer.name}: {type(e).__name__}: {e}", RNS.LOG_DEBUG) # Continue without identity - # Send connection handshake to trigger peripheral callback - # Write empty bytes to RX characteristic to ensure remote's on_central_connected fires - # This guarantees bidirectional peer interface spawning even when only one side discovers - # TODO: Consider sending handshake packet with protocol version/capabilities/flags + # Send connection handshake WITH our identity to trigger peripheral callback + # This enables the peripheral to create a unified interface with our identity + # without needing to discover us via scanning (solves asymmetric discovery issue) try: - await client.write_gatt_char(self.CHARACTERISTIC_RX_UUID, b'', response=True) - RNS.log(f"{self} sent connection handshake to {peer.name}", RNS.LOG_DEBUG) + # Get our own identity to send in handshake + our_identity = self.gatt_server.identity_hash if (self.gatt_server and self.gatt_server.identity_hash) else b'\x00' * 16 + await client.write_gatt_char(self.CHARACTERISTIC_RX_UUID, our_identity, response=True) + identity_preview = our_identity[:8].hex() if len(our_identity) >= 8 else "null" + RNS.log(f"{self} sent connection handshake WITH identity to {peer.name} ({len(our_identity)} bytes, {identity_preview}...)", RNS.LOG_DEBUG) except Exception as e: RNS.log(f"{self} handshake write failed (non-critical): {e}", RNS.LOG_WARNING) @@ -1788,8 +1790,56 @@ class BLEInterface(Interface): """ RNS.log(f"{self} received {len(data)} bytes from central {sender_address}", RNS.LOG_EXTREME) - # NOTE: Interface creation is handled by handle_central_connected() callback - # which is called when the central first connects (via handshake write) + # Detect identity handshake (16 bytes, likely the first write from this peer) + # The central sends its own identity in the handshake to enable unified interface creation + # even when the peripheral hasn't discovered the central via scanning + if len(data) == 16: + central_identity = bytes(data) + central_identity_hash = RNS.Identity.full_hash(central_identity)[:16].hex()[:16] + + # Store or verify identity mapping + if sender_address not in self.address_to_identity: + # First time seeing this identity for this address + self.address_to_identity[sender_address] = central_identity + self.identity_to_address[central_identity_hash] = sender_address + RNS.log(f"{self} received identity handshake from {sender_address}: {central_identity_hash}", RNS.LOG_INFO) + else: + # Already know identity - verify it matches + existing_identity = self.address_to_identity[sender_address] + if existing_identity == central_identity: + RNS.log(f"{self} received identity handshake confirmation from {sender_address}: {central_identity_hash}", RNS.LOG_DEBUG) + else: + RNS.log(f"{self} WARNING: identity mismatch for {sender_address}! Existing vs received", RNS.LOG_WARNING) + + # Check if we need to merge interfaces + legacy_conn_id = f"{sender_address}-peripheral" + if legacy_conn_id in self.spawned_interfaces: + # Legacy peripheral interface exists - need to migrate or merge + if central_identity_hash in self.spawned_interfaces: + # We already have an identity-based interface (from central connection) + # Add peripheral connection to it and remove legacy interface + identity_if = self.spawned_interfaces[central_identity_hash] + legacy_if = self.spawned_interfaces[legacy_conn_id] + + # Add peripheral connection to unified interface + identity_if.add_peripheral_connection() + + # Clean up legacy interface + legacy_if.detach() + del self.spawned_interfaces[legacy_conn_id] + + RNS.log(f"{self} merged legacy peripheral into identity-based interface {central_identity_hash} (now {identity_if._get_connection_state_str()})", RNS.LOG_INFO) + else: + # No identity-based interface yet - migrate the legacy one + legacy_if = self.spawned_interfaces[legacy_conn_id] + del self.spawned_interfaces[legacy_conn_id] + + legacy_if.peer_identity = central_identity + self.spawned_interfaces[central_identity_hash] = legacy_if + + RNS.log(f"{self} migrated interface from legacy ({legacy_conn_id}) to identity-based ({central_identity_hash})", RNS.LOG_INFO) + + return # Don't process handshake as data # Update fragmenter MTU if GATT server has learned a new MTU # (MTU is provided by BlueZ in write callback options)