From 3878ac2f70b05b77e85fc84a0d193721bc0f9615 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Thu, 30 Oct 2025 19:39:33 -0400 Subject: [PATCH] feat: add Identity characteristic for stable peer tracking across MAC rotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements BLE Protocol v2 with Transport identity GATT characteristic to solve Android MAC address rotation issues. Adds IDENTITY_CHAR_UUID (00000004-...) that serves the 16-byte RNS.Transport.identity.hash, enabling reliable bidirectional mesh connectivity with Android devices whose BLE MAC addresses rotate every ~15 minutes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/RNS/Interfaces/BLEGATTServer.py | 65 +++++++++++++++++++++++++++++ src/RNS/Interfaces/BLEInterface.py | 41 ++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/RNS/Interfaces/BLEGATTServer.py b/src/RNS/Interfaces/BLEGATTServer.py index 9991540..e8ec6fe 100644 --- a/src/RNS/Interfaces/BLEGATTServer.py +++ b/src/RNS/Interfaces/BLEGATTServer.py @@ -65,6 +65,9 @@ class BLEGATTServer: # TX Characteristic: We notify on this (centrals receive) TX_CHAR_UUID = "00000003-5824-4f48-9e1a-3b3e8f0c1234" + # Identity Characteristic: Centrals read this to get stable node identity (Protocol v2) + IDENTITY_CHAR_UUID = "00000004-5824-4f48-9e1a-3b3e8f0c1234" + def __init__(self, interface, device_name: str = "Reticulum-Node", agent_capability: str = "NoInputNoOutput"): """ Initialize BLE GATT Server @@ -88,6 +91,9 @@ class BLEGATTServer: self.tx_characteristic = None self.rx_characteristic = None + # Identity (Protocol v2) + self.identity_hash = None # 16-byte Transport identity hash + # BLE agent for automatic pairing self.ble_agent = None @@ -208,6 +214,33 @@ class BLEGATTServer: return value # bluezero expects us to return the value + def _handle_read_identity(self, options): + """ + Handle read request for Identity characteristic (bluezero callback) + + Called when a central reads the Identity characteristic. + Returns the 16-byte Transport identity hash. + + Args: + options: D-Bus options dict (may contain 'device' address) + + Returns: + list of ints: The 16-byte identity hash as a list of integers + """ + # Extract central address from options + central_address = options.get("device", "unknown") + if central_address and central_address != "unknown": + central_address = central_address.split("/")[-1].replace("_", ":") + + if self.identity_hash is None: + self._log(f">>> READ REQUEST for Identity from {central_address}: Identity not available yet", level="WARNING") + return [] # Return empty if not available + + # Convert bytes to list of ints for bluezero + identity_list = list(self.identity_hash) + self._log(f">>> READ REQUEST for Identity from {central_address}: Serving {len(identity_list)} bytes", level="INFO") + return identity_list + def _handle_central_connected(self, central_address: str, mtu: Optional[int] = None): """ Handle new central connection @@ -355,6 +388,19 @@ class BLEGATTServer: ) self._log(f"Added TX characteristic: {self.TX_CHAR_UUID} (READ, NOTIFY)", level="DEBUG") + # Add Identity characteristic (read to get stable node identity - Protocol v2) + identity_value = list(self.identity_hash) if self.identity_hash else [] + self.peripheral_obj.add_characteristic( + srv_id=1, + chr_id=3, + uuid=self.IDENTITY_CHAR_UUID, + value=identity_value, + notifying=False, + flags=['read'], + read_callback=self._handle_read_identity + ) + self._log(f"Added Identity characteristic: {self.IDENTITY_CHAR_UUID} (READ) - Protocol v2", level="DEBUG") + # Find and save TX characteristic for later notification sends # Characteristics are stored in order added: chr_id=1 (RX) is index 0, chr_id=2 (TX) is index 1 if len(self.peripheral_obj.characteristics) >= 2: @@ -438,6 +484,25 @@ class BLEGATTServer: self.running = False raise + def set_transport_identity(self, identity_hash: bytes): + """ + Set the Transport identity hash for BLE Protocol v2. + + This should be called after RNS.Transport is initialized and before + starting the GATT server (or early during startup). + + Args: + identity_hash: 16-byte Reticulum Transport identity hash + """ + if not isinstance(identity_hash, bytes): + raise TypeError(f"identity_hash must be bytes, got {type(identity_hash)}") + + if len(identity_hash) != 16: + raise ValueError(f"identity_hash must be 16 bytes, got {len(identity_hash)}") + + self.identity_hash = identity_hash + self._log(f"Transport identity set: {identity_hash.hex()}", level="INFO") + async def stop(self): """ Stop the GATT server and advertising diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index 604042c..91cc2a5 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -298,6 +298,7 @@ class BLEInterface(Interface): SERVICE_UUID = "00000001-5824-4f48-9e1a-3b3e8f0c1234" # Custom Reticulum BLE service CHARACTERISTIC_RX_UUID = "00000002-5824-4f48-9e1a-3b3e8f0c1234" # RX characteristic CHARACTERISTIC_TX_UUID = "00000003-5824-4f48-9e1a-3b3e8f0c1234" # TX characteristic + CHARACTERISTIC_IDENTITY_UUID = "00000004-5824-4f48-9e1a-3b3e8f0c1234" # Identity characteristic (Protocol v2) # Discovery and connection settings DISCOVERY_INTERVAL = 5.0 # seconds between discovery scans @@ -501,6 +502,22 @@ class BLEInterface(Interface): # TODO: Remove when upstream Transport.py is fixed (see session notes) self._clear_stale_ble_paths() + # Protocol v2: Set Transport identity on GATT server for stable peer tracking + if self.gatt_server: + try: + import RNS.Transport as Transport + if hasattr(Transport, 'identity') and Transport.identity: + identity_hash = Transport.identity.hash + if identity_hash and len(identity_hash) == 16: + self.gatt_server.set_transport_identity(identity_hash) + RNS.log(f"{self} Set Transport identity on GATT server: {identity_hash.hex()}", RNS.LOG_INFO) + else: + RNS.log(f"{self} WARNING: Invalid Transport identity hash size: {len(identity_hash) if identity_hash else 0}", RNS.LOG_WARNING) + else: + RNS.log(f"{self} WARNING: Transport.identity not available yet", RNS.LOG_WARNING) + except Exception as e: + RNS.log(f"{self} Error setting Transport identity: {e}", RNS.LOG_ERROR) + self.online = True RNS.log(f"{self} started successfully", RNS.LOG_INFO) @@ -1363,6 +1380,30 @@ class BLEInterface(Interface): except Exception as e: RNS.log(f"{self} service discovery failed: {type(e).__name__}: {e} (will retry)", RNS.LOG_WARNING) + # Read Identity characteristic (Protocol v2) if available + peer_identity_hash = None + if reticulum_service: + try: + identity_char = None + for char in reticulum_service.characteristics: + if char.uuid.lower() == BLEInterface.CHARACTERISTIC_IDENTITY_UUID.lower(): + identity_char = char + break + + if identity_char: + RNS.log(f"{self} reading Identity characteristic from {peer.name}...", RNS.LOG_DEBUG) + identity_value = await client.read_gatt_char(identity_char) + if identity_value and len(identity_value) == 16: + peer_identity_hash = bytes(identity_value).hex() + RNS.log(f"{self} received peer identity from {peer.name}: {peer_identity_hash}", RNS.LOG_INFO) + else: + RNS.log(f"{self} invalid identity size from {peer.name}: {len(identity_value) if identity_value else 0} bytes", RNS.LOG_WARNING) + else: + RNS.log(f"{self} Identity characteristic not found on {peer.name} (Protocol v1 device)", RNS.LOG_DEBUG) + except Exception as e: + RNS.log(f"{self} failed to read identity from {peer.name}: {type(e).__name__}: {e}", RNS.LOG_DEBUG) + # Continue without identity + # Get negotiated MTU try: # For BlueZ backend, acquire MTU first to avoid warning