Merge pull request #25 from torlando-tech/release/v0.2.2
Some checks failed
Release / Validate Release (push) Has been cancelled
Release / Run Tests (push) Has been cancelled
Release / Run Tests-1 (push) Has been cancelled
Release / Run Tests-2 (push) Has been cancelled
Release / Run Tests-3 (push) Has been cancelled
Release / Build Release Artifacts (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Some checks failed
Release / Validate Release (push) Has been cancelled
Release / Run Tests (push) Has been cancelled
Release / Run Tests-1 (push) Has been cancelled
Release / Run Tests-2 (push) Has been cancelled
Release / Run Tests-3 (push) Has been cancelled
Release / Build Release Artifacts (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Release v0.2.2
This commit is contained in:
commit
54ee4048e0
3 changed files with 352 additions and 32 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.2] - 2025-11-15
|
||||
|
||||
### Added
|
||||
- pipx installation support with automated D-Bus dependency handling
|
||||
- BlueZ LE-only mode configuration in installer (prevents BR/EDR fallback)
|
||||
- Scanner watchdog to detect and recover from Bluetooth stack corruption
|
||||
- Service UUID filtering for more efficient peer discovery
|
||||
- Pre-built wheel support for Pi Zero W Python 3.13 (saves 20+ min install time)
|
||||
|
||||
### Fixed
|
||||
- **Connection race condition causing "Operation already in progress" errors**
|
||||
- Added `_connecting_peers` state tracking in `linux_bluetooth_driver.py` to prevent concurrent connection attempts to the same peer
|
||||
|
|
@ -24,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Prevents BlueZ from maintaining stale connection state after abandoned connection attempts
|
||||
- Enables successful reconnection after blacklist period expires
|
||||
- Fixes issue where devices could not reconnect after multiple failed attempts due to corrupted BlueZ state
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py` (lines 786-830, 980-1069), `src/RNS/Interfaces/BLEInterface.py` (lines 1475-1490)
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py`, `src/RNS/Interfaces/BLEInterface.py`
|
||||
|
||||
- **Scanner interference causing "Operation already in progress" errors during connection attempts**
|
||||
- Added `_should_pause_scanning()` method to check for active connections before starting scanner
|
||||
|
|
@ -34,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Prevents BlueZ "InProgress" errors from scanner.start() conflicting with connection operations
|
||||
- Improves connection reliability by eliminating scan-induced connection failures
|
||||
- Reduces BlueZ error log spam from scan loop
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py` (lines 539-551, 586-588)
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py`
|
||||
- Tests: `tests/test_scanner_connection_coordination.py`
|
||||
|
||||
- **BR/EDR fallback - clarify ConnectDevice() object path return as success**
|
||||
|
|
@ -44,7 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Some BlueZ versions report BR/EDR profile unavailable while LE connection succeeds - this is expected
|
||||
- Improved logging shows object path for debugging visibility
|
||||
- Clarifies that object path return means success, not error
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py` (lines 1121-1132)
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py`
|
||||
- Tests: `tests/test_breddr_fallback_prevention.py`
|
||||
|
||||
- **GATT server initialization race causing "Reticulum service not found" errors**
|
||||
|
|
@ -55,9 +64,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Eliminates "service not found" errors during server startup window (typically 50-200ms)
|
||||
- Graceful degradation: warns if verification times out but doesn't fail startup
|
||||
- Typical verification time: 100-300ms, no runtime performance impact
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py` (lines 1493-1559, 1527-1538)
|
||||
- Files: `src/RNS/Interfaces/linux_bluetooth_driver.py`
|
||||
- Tests: `tests/test_gatt_server_readiness.py`
|
||||
|
||||
- D-Bus disconnect monitoring switched to ObjectManager with polling fallback
|
||||
- Peripheral disconnect cleanup preventing new connections after hitting peer limit
|
||||
- Identity mapping cleanup on disconnect (prevents stale peer tracking)
|
||||
- RSSI sentinel value filtering (-127 from BlueZ)
|
||||
- Columba Android compatibility (filter 1-byte keepalive packets)
|
||||
|
||||
### Changed
|
||||
- Refactored to driver-based architecture (future Windows/macOS/Android support)
|
||||
|
||||
## [0.1.1] - 2025-11-10
|
||||
|
||||
### Fixed
|
||||
|
|
@ -145,7 +163,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Known Issues
|
||||
- MAC address randomization can cause connection issues (fixed in v2.2.0)
|
||||
- Race condition from concurrent connection attempts (fixed in unreleased)
|
||||
- Race condition from concurrent connection attempts (fixed in v0.2.2)
|
||||
- BR/EDR fallback on dual-mode devices (fixed in v2.2.0)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -284,6 +284,26 @@ class BLEInterface(Interface):
|
|||
|
||||
super().__init__()
|
||||
|
||||
# CRITICAL: Set HW_MTU as instance attribute after super().__init__()
|
||||
#
|
||||
# Bug explanation:
|
||||
# - Base Interface.__init__() sets self.HW_MTU = None
|
||||
# - BLEInterface.HW_MTU = 500 is a CLASS attribute, not instance
|
||||
# - After super().__init__(), self.HW_MTU is None (instance shadows class)
|
||||
# - BLEPeerInterface copies: self.HW_MTU = parent.HW_MTU (gets None)
|
||||
#
|
||||
# Impact when HW_MTU is None:
|
||||
# - Transport.py line ~1855 checks: if packet.receiving_interface.HW_MTU == None
|
||||
# - If true, it TRUNCATES packet.data by 3 bytes (LINK_MTU_SIZE) before
|
||||
# passing to Link.validate_request()
|
||||
# - Link.link_id_from_lr_packet() uses len(packet.data) to compute truncation
|
||||
# - Since packet.data was pre-truncated, it computes WRONG link_id
|
||||
# - Link proof's destination_hash won't match pending link's link_id
|
||||
# - Result: Links time out despite proof arriving correctly
|
||||
#
|
||||
# This bug ONLY affects BLE because other interfaces set HW_MTU in __init__
|
||||
self.HW_MTU = BLEInterface.HW_MTU
|
||||
|
||||
# Parse configuration
|
||||
c = Interface.get_config_obj(configuration)
|
||||
|
||||
|
|
@ -364,6 +384,7 @@ class BLEInterface(Interface):
|
|||
self.fragmenters = {} # address -> BLEFragmenter (per MTU)
|
||||
self.reassemblers = {} # address -> BLEReassembler
|
||||
self.frag_lock = threading.Lock()
|
||||
self.pending_mtu = {} # address -> mtu (for MTU/identity race condition)
|
||||
|
||||
# Discovery state with prioritization
|
||||
|
||||
|
|
@ -751,7 +772,7 @@ class BLEInterface(Interface):
|
|||
role = self.driver.get_peer_role(address)
|
||||
|
||||
if peer_identity is not None:
|
||||
# Central mode: identity provided by driver
|
||||
# Identity provided by driver (central mode direct, peripheral mode via late callback)
|
||||
if len(peer_identity) == 16:
|
||||
identity_hash = self._compute_identity_hash(peer_identity)
|
||||
|
||||
|
|
@ -759,8 +780,15 @@ class BLEInterface(Interface):
|
|||
self.address_to_identity[address] = peer_identity
|
||||
self.identity_to_address[identity_hash] = address
|
||||
|
||||
RNS.log(f"{self} connected to {address} as CENTRAL, received identity: {identity_hash}", RNS.LOG_INFO)
|
||||
role_str = role.upper() if role else "UNKNOWN"
|
||||
RNS.log(f"{self} connected to {address} as {role_str}, received identity: {identity_hash}", RNS.LOG_INFO)
|
||||
self._record_connection_success(address)
|
||||
|
||||
# Check for pending MTU (race condition: MTU negotiated before identity)
|
||||
if address in self.pending_mtu:
|
||||
pending_mtu = self.pending_mtu.pop(address)
|
||||
RNS.log(f"{self} creating deferred fragmenter for {address} (MTU={pending_mtu})", RNS.LOG_DEBUG)
|
||||
self._mtu_negotiated_callback(address, pending_mtu)
|
||||
else:
|
||||
RNS.log(f"{self} invalid identity from {address} (wrong length), disconnecting", RNS.LOG_WARNING)
|
||||
self.driver.disconnect(address)
|
||||
|
|
@ -819,7 +847,10 @@ class BLEInterface(Interface):
|
|||
# Get peer identity
|
||||
peer_identity = self.address_to_identity.get(address)
|
||||
if not peer_identity:
|
||||
RNS.log(f"{self} no identity for {address}, cannot create fragmenter", RNS.LOG_WARNING)
|
||||
# Race condition: MTU negotiated before identity received
|
||||
# Store pending MTU and create fragmenter when identity arrives
|
||||
RNS.log(f"{self} no identity for {address}, storing pending MTU {mtu}", RNS.LOG_DEBUG)
|
||||
self.pending_mtu[address] = mtu
|
||||
return
|
||||
|
||||
# Create or update fragmenter
|
||||
|
|
@ -980,6 +1011,50 @@ class BLEInterface(Interface):
|
|||
if frag_key in self.reassemblers:
|
||||
del self.reassemblers[frag_key]
|
||||
|
||||
# Clean up pending MTU (from MTU/identity race condition)
|
||||
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.
|
||||
|
|
@ -1186,15 +1261,37 @@ 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)
|
||||
# Bypass MAC sorting - we must reconnect after MAC rotation
|
||||
# regardless of which device has the higher MAC address
|
||||
score = self._score_peer(peer)
|
||||
scored_peers.append((score, peer))
|
||||
continue # Skip remaining checks, peer already added
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -59,11 +59,9 @@ if not hasattr(RNS, 'Identity'):
|
|||
RNS.Identity = MagicMock()
|
||||
RNS.Identity.full_hash = lambda x: (x * 2)[:16]
|
||||
|
||||
# Mock RNS.Interfaces.Interface (required by BLEInterface.py)
|
||||
if 'RNS.Interfaces' not in _sys.modules:
|
||||
rns_interfaces_mock = MagicMock()
|
||||
_sys.modules['RNS.Interfaces'] = rns_interfaces_mock
|
||||
|
||||
# Mock RNS.Interfaces.Interface module (the base class module, not the whole namespace)
|
||||
# We only mock the Interface.py module, allowing BLEInterface.py to be imported from src/
|
||||
if 'RNS.Interfaces.Interface' not in _sys.modules:
|
||||
# Create mock Interface base class
|
||||
class MockInterface:
|
||||
MODE_FULL = 1
|
||||
|
|
@ -72,7 +70,40 @@ if 'RNS.Interfaces' not in _sys.modules:
|
|||
self.OUT = True
|
||||
self.online = False
|
||||
|
||||
rns_interfaces_mock.Interface = MockInterface
|
||||
@staticmethod
|
||||
def get_config_obj(configuration):
|
||||
"""Mock config object wrapper - just returns a dict-like object."""
|
||||
class ConfigObj:
|
||||
def __init__(self, config):
|
||||
self._config = config if config else {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._config.get(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._config.get(key, default)
|
||||
|
||||
def as_string(self, key, default=None):
|
||||
val = self._config.get(key, default)
|
||||
return str(val) if val is not None else default
|
||||
|
||||
def as_int(self, key, default=None):
|
||||
val = self._config.get(key, default)
|
||||
return int(val) if val is not None else default
|
||||
|
||||
def as_bool(self, key, default=False):
|
||||
val = self._config.get(key, default)
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
if isinstance(val, str):
|
||||
return val.lower() in ('true', 'yes', '1', 'on')
|
||||
return bool(val) if val is not None else default
|
||||
return ConfigObj(configuration)
|
||||
|
||||
# Create a mock module for RNS.Interfaces.Interface
|
||||
interface_module = MagicMock()
|
||||
interface_module.Interface = MockInterface
|
||||
_sys.modules['RNS.Interfaces.Interface'] = interface_module
|
||||
|
||||
from tests.mock_ble_driver import MockBLEDriver
|
||||
from RNS.Interfaces.BLEInterface import BLEInterface, DiscoveredPeer
|
||||
|
|
@ -212,10 +243,13 @@ class TestMACEdgeCases:
|
|||
peers_to_connect = interface._select_peers_to_connect()
|
||||
peer_addresses = [p.address for p in peers_to_connect]
|
||||
|
||||
# Should only connect to peer with lower MAC (00)
|
||||
assert "AA:BB:CC:DD:EE:00" in peer_addresses
|
||||
assert "AA:BB:CC:DD:EE:02" not in peer_addresses
|
||||
assert "AA:BB:CC:DD:EE:FF" not in peer_addresses
|
||||
# MAC sorting: lower MAC initiates. Our MAC is AA:BB:CC:DD:EE:01
|
||||
# - AA:BB:CC:DD:EE:00 is LOWER than us, so THEY initiate (we skip)
|
||||
# - AA:BB:CC:DD:EE:02 is HIGHER than us, so WE initiate
|
||||
# - AA:BB:CC:DD:EE:FF is HIGHER than us, so WE initiate
|
||||
assert "AA:BB:CC:DD:EE:00" not in peer_addresses # They initiate
|
||||
assert "AA:BB:CC:DD:EE:02" in peer_addresses # We initiate
|
||||
assert "AA:BB:CC:DD:EE:FF" in peer_addresses # We initiate
|
||||
|
||||
|
||||
class TestDualConnectionPrevention:
|
||||
|
|
@ -269,11 +303,12 @@ class TestDualConnectionPrevention:
|
|||
interface.local_address = driver.local_address
|
||||
|
||||
# Add peers with MACs above and below ours
|
||||
# MAC sorting: lower MAC initiates. Our MAC is 55:55:55:55:55:55
|
||||
peers_data = [
|
||||
("11:11:11:11:11:11", -60), # Below (should connect)
|
||||
("22:22:22:22:22:22", -60), # Below (should connect)
|
||||
("AA:AA:AA:AA:AA:AA", -60), # Above (should NOT connect)
|
||||
("FF:FF:FF:FF:FF:FF", -60), # Above (should NOT connect)
|
||||
("11:11:11:11:11:11", -60), # LOWER than us - THEY initiate, we skip
|
||||
("22:22:22:22:22:22", -60), # LOWER than us - THEY initiate, we skip
|
||||
("AA:AA:AA:AA:AA:AA", -60), # HIGHER than us - WE initiate
|
||||
("FF:FF:FF:FF:FF:FF", -60), # HIGHER than us - WE initiate
|
||||
]
|
||||
|
||||
for addr, rssi in peers_data:
|
||||
|
|
@ -284,11 +319,11 @@ class TestDualConnectionPrevention:
|
|||
peers_to_connect = interface._select_peers_to_connect()
|
||||
peer_addresses = [p.address for p in peers_to_connect]
|
||||
|
||||
# Should connect to lower MACs only
|
||||
assert "11:11:11:11:11:11" in peer_addresses
|
||||
assert "22:22:22:22:22:22" in peer_addresses
|
||||
assert "AA:AA:AA:AA:AA:AA" not in peer_addresses
|
||||
assert "FF:FF:FF:FF:FF:FF" not in peer_addresses
|
||||
# MAC sorting: lower MAC initiates, so we connect to HIGHER MACs
|
||||
assert "11:11:11:11:11:11" not in peer_addresses # They initiate
|
||||
assert "22:22:22:22:22:22" not in peer_addresses # They initiate
|
||||
assert "AA:AA:AA:AA:AA:AA" in peer_addresses # We initiate
|
||||
assert "FF:FF:FF:FF:FF:FF" in peer_addresses # We initiate
|
||||
|
||||
|
||||
class TestMACParsingErrors:
|
||||
|
|
@ -317,5 +352,175 @@ class TestMACParsingErrors:
|
|||
pytest.fail(f"Invalid MAC should be handled gracefully: {e}")
|
||||
|
||||
|
||||
class TestMACRotationBypassesSorting:
|
||||
"""
|
||||
Test that MAC rotation bypasses MAC sorting.
|
||||
|
||||
Bug fix: After MAC rotation cleanup, the peer must be added to the connection
|
||||
list regardless of MAC sorting. Previously, the code fell through to the MAC
|
||||
sorting check which could skip the peer if local MAC > peer MAC.
|
||||
|
||||
Fix: After _cleanup_stale_interface(), immediately add peer and continue,
|
||||
bypassing the MAC sorting check.
|
||||
"""
|
||||
|
||||
def test_mac_rotation_bypasses_sorting_when_local_mac_higher(self):
|
||||
"""
|
||||
Test that MAC rotation adds peer even when local MAC is higher.
|
||||
|
||||
This is the core bug fix test. Without the fix:
|
||||
- MAC rotation detected, stale interface cleaned up
|
||||
- Code falls through to MAC sorting check
|
||||
- Local MAC (FF:...) > Peer MAC (11:...) → peer skipped
|
||||
- Peer interface never recreated!
|
||||
|
||||
With the fix:
|
||||
- MAC rotation detected, stale interface cleaned up
|
||||
- Peer immediately added, continue (bypass MAC sorting)
|
||||
- Peer interface recreated correctly
|
||||
"""
|
||||
driver = MockBLEDriver(local_address="FF:FF:FF:FF:FF:FF") # Higher MAC
|
||||
owner = MockOwner()
|
||||
|
||||
config = {"name": "Test", "enable_central": True}
|
||||
interface = BLEInterface(owner, config)
|
||||
interface.driver = driver
|
||||
interface.local_address = driver.local_address
|
||||
|
||||
# Set up MAC rotation scenario:
|
||||
# - Identity exists at old address
|
||||
# - Peer discovered at new address (lower MAC)
|
||||
# - Old connection is stale (not in peers dict)
|
||||
old_address = "AA:AA:AA:AA:AA:AA"
|
||||
new_address = "11:22:33:44:55:66" # Lower than local MAC
|
||||
peer_identity = bytes.fromhex("ab5609dfffb33b21a102e1ff81196be5")
|
||||
identity_hash = interface._compute_identity_hash(peer_identity)
|
||||
|
||||
# Set up existing identity mapping at old address
|
||||
interface.identity_to_address[identity_hash] = old_address
|
||||
interface.address_to_identity[new_address] = peer_identity
|
||||
|
||||
# Create a mock spawned interface (stale)
|
||||
mock_peer_interface = MagicMock()
|
||||
interface.spawned_interfaces[identity_hash] = mock_peer_interface
|
||||
|
||||
# old_address NOT in interface.peers (connection is dead/stale)
|
||||
|
||||
# Discover peer at new address
|
||||
peer = DiscoveredPeer(new_address, "RNS-ab5609", -60)
|
||||
interface.discovered_peers[new_address] = peer
|
||||
|
||||
# Select peers to connect
|
||||
peers_to_connect = interface._select_peers_to_connect()
|
||||
peer_addresses = [p.address for p in peers_to_connect]
|
||||
|
||||
# Even though local MAC > peer MAC, peer should be added due to MAC rotation
|
||||
assert new_address in peer_addresses, \
|
||||
"MAC rotation should bypass MAC sorting and add peer"
|
||||
|
||||
def test_mac_rotation_cleanup_is_called(self):
|
||||
"""Test that _cleanup_stale_interface is called during MAC rotation."""
|
||||
driver = MockBLEDriver(local_address="FF:FF:FF:FF:FF:FF")
|
||||
owner = MockOwner()
|
||||
|
||||
config = {"name": "Test", "enable_central": True}
|
||||
interface = BLEInterface(owner, config)
|
||||
interface.driver = driver
|
||||
interface.local_address = driver.local_address
|
||||
|
||||
# Track cleanup calls
|
||||
cleanup_calls = []
|
||||
original_cleanup = interface._cleanup_stale_interface
|
||||
|
||||
def tracked_cleanup(identity_hash, old_address):
|
||||
cleanup_calls.append((identity_hash, old_address))
|
||||
return original_cleanup(identity_hash, old_address)
|
||||
|
||||
interface._cleanup_stale_interface = tracked_cleanup
|
||||
|
||||
# Set up MAC rotation scenario
|
||||
old_address = "AA:AA:AA:AA:AA:AA"
|
||||
new_address = "11:22:33:44:55:66"
|
||||
peer_identity = bytes.fromhex("ab5609dfffb33b21a102e1ff81196be5")
|
||||
identity_hash = interface._compute_identity_hash(peer_identity)
|
||||
|
||||
interface.identity_to_address[identity_hash] = old_address
|
||||
interface.address_to_identity[new_address] = peer_identity
|
||||
|
||||
mock_peer_interface = MagicMock()
|
||||
interface.spawned_interfaces[identity_hash] = mock_peer_interface
|
||||
|
||||
# Discover peer at new address
|
||||
peer = DiscoveredPeer(new_address, "RNS-ab5609", -60)
|
||||
interface.discovered_peers[new_address] = peer
|
||||
|
||||
# Select peers
|
||||
interface._select_peers_to_connect()
|
||||
|
||||
# Verify cleanup was called
|
||||
assert len(cleanup_calls) == 1
|
||||
assert cleanup_calls[0] == (identity_hash, old_address)
|
||||
|
||||
def test_active_connection_prevents_rotation_cleanup(self):
|
||||
"""Test that active connection prevents MAC rotation cleanup."""
|
||||
driver = MockBLEDriver(local_address="FF:FF:FF:FF:FF:FF")
|
||||
owner = MockOwner()
|
||||
|
||||
config = {"name": "Test", "enable_central": True}
|
||||
interface = BLEInterface(owner, config)
|
||||
interface.driver = driver
|
||||
interface.local_address = driver.local_address
|
||||
|
||||
# Set up scenario where old connection is ACTIVE
|
||||
old_address = "AA:AA:AA:AA:AA:AA"
|
||||
new_address = "11:22:33:44:55:66"
|
||||
peer_identity = bytes.fromhex("ab5609dfffb33b21a102e1ff81196be5")
|
||||
identity_hash = interface._compute_identity_hash(peer_identity)
|
||||
|
||||
interface.identity_to_address[identity_hash] = old_address
|
||||
interface.address_to_identity[new_address] = peer_identity
|
||||
|
||||
mock_peer_interface = MagicMock()
|
||||
interface.spawned_interfaces[identity_hash] = mock_peer_interface
|
||||
|
||||
# OLD connection is ACTIVE (in peers dict)
|
||||
interface.peers[old_address] = {"mtu": 512}
|
||||
|
||||
# Discover peer at new address
|
||||
peer = DiscoveredPeer(new_address, "RNS-ab5609", -60)
|
||||
interface.discovered_peers[new_address] = peer
|
||||
|
||||
# Select peers
|
||||
peers_to_connect = interface._select_peers_to_connect()
|
||||
peer_addresses = [p.address for p in peers_to_connect]
|
||||
|
||||
# Should NOT add peer (old connection still active)
|
||||
assert new_address not in peer_addresses, \
|
||||
"Active connection should prevent MAC rotation"
|
||||
|
||||
def test_normal_mac_sorting_still_works(self):
|
||||
"""Test that normal MAC sorting still works when no rotation."""
|
||||
driver = MockBLEDriver(local_address="FF:FF:FF:FF:FF:FF") # Higher MAC
|
||||
owner = MockOwner()
|
||||
|
||||
config = {"name": "Test", "enable_central": True}
|
||||
interface = BLEInterface(owner, config)
|
||||
interface.driver = driver
|
||||
interface.local_address = driver.local_address
|
||||
|
||||
# No existing identity mapping - this is a completely new peer
|
||||
peer_address = "11:22:33:44:55:66" # Lower MAC
|
||||
peer = DiscoveredPeer(peer_address, "NewPeer", -60)
|
||||
interface.discovered_peers[peer_address] = peer
|
||||
|
||||
# Select peers
|
||||
peers_to_connect = interface._select_peers_to_connect()
|
||||
peer_addresses = [p.address for p in peers_to_connect]
|
||||
|
||||
# Should NOT add peer (they have lower MAC, they should initiate)
|
||||
assert peer_address not in peer_addresses, \
|
||||
"Normal MAC sorting should skip peer with lower MAC"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue