Skip to content

fix(ble): stabilize dual connections by detecting legitimate dual-connection scenarios#390

Draft
torlando-tech wants to merge 1 commit intomainfrom
fix/ble-dual-connection-stability
Draft

fix(ble): stabilize dual connections by detecting legitimate dual-connection scenarios#390
torlando-tech wants to merge 1 commit intomainfrom
fix/ble-dual-connection-stability

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Fixes BLE connection flakiness where dual connections (same peer as both central and peripheral) were being incorrectly rejected
  • Connections now stable for 20+ minutes (vs 1.5-12 seconds before)
  • Re-enables Kotlin-side identity detection for peripheral connections to properly track dual connection state

Changes

  • KotlinBLEBridge.kt: Added dual connection detection before calling Python's duplicate identity check - allows legitimate dual connections while still detecting MAC rotation
  • BleGattServer.kt: Re-enabled Kotlin-side identity tracking for peripheral connections (was commented out 2026-01-17)

Test plan

  • Manual testing with two Columba phones
  • Verified connections remain stable for 20+ minutes
  • Verify MAC rotation still triggers reconnection (expected every ~15 mins)

🤖 Generated with Claude Code

…nection scenarios

Previously, BLE connections would die within 1.5-12 seconds due to two issues:

1. Deduplication closing one side of dual connections, triggering Android's
   L2CAP idle timer which killed the remaining connection
2. Duplicate identity detection rejecting legitimate dual connections as
   "MAC rotation"

This commit fixes both issues:

**Disable deduplication (DEDUPLICATION_ENABLED = false)**
- When both phones connect to each other (dual connection), keep both paths
- The protocol already handles this correctly - sends on one path only
- Avoids triggering Android's 1-second L2CAP idle timer

**Detect legitimate dual connections before rejecting duplicates**
- When connecting as central, check if we already have a peripheral-only
  connection to the same identity (and vice versa)
- If so, this is a dual connection, not MAC rotation - allow it
- Added debug logging for dual connection detection

**Re-enable Kotlin-side peripheral identity detection**
- When a central writes its 16-byte identity handshake to our GATT server,
  store it in KotlinBLEBridge.identityToAddress
- This allows the dual connection check to find the existing peripheral peer
- Python also detects identity (via data callback), which is fine - both coexist

Tested: Connections now stable for 20+ minutes (vs 1.5-12 seconds before)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Jan 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 30, 2026

Greptile Overview

Greptile Summary

Fixes BLE connection instability by properly detecting and allowing legitimate dual connections (same peer connected as both central and peripheral), while still detecting MAC rotation attacks.

Key Changes

  • Dual Connection Detection: Added logic in KotlinBLEBridge.handleIdentityReceived() to check if an incoming connection with the same identity is on the opposite role (central vs peripheral) before calling Python's duplicate identity check. This prevents legitimate dual connections from being rejected as duplicates.

  • Re-enabled Identity Tracking: Restored Kotlin-side identity detection in BleGattServer (previously disabled 2026-01-17) to populate the identityToAddress mapping needed for dual connection detection. Both Kotlin and Python now track identities, with Python handling duplicate notifications.

  • Keepalive Improvements: Reduced keepalive interval from 15s to 7s and renamed failure tracking from consecutiveKeepaliveFailures to consecutiveKeepaliveWriteFailures to clarify that write failures (not receive failures) indicate GATT degradation. Also reduced max failures from 3 to 2.

  • Deduplication Disabled: Set DEDUPLICATION_ENABLED = false to disable automatic connection deduplication, as closing one connection can trigger Android's L2CAP idle timer and kill both connections.

Test Coverage

Manual testing shows connections now stable for 20+ minutes vs 1.5-12 seconds before. Unit tests updated to reflect new constants and field names.

Confidence Score: 4/5

  • Safe to merge with proper manual testing - the logic is sound but complexity requires verification in production
  • The dual connection detection logic is well-reasoned and addresses a real stability issue. The two-phase check (identityToAddress map + peer scan) handles race conditions. However, the complexity of BLE state management and the interaction between Kotlin and Python layers means thorough manual testing is critical before declaring complete confidence.
  • Pay close attention to KotlinBLEBridge.kt:2164-2240 (dual connection detection logic) and verify the re-enabled identity tracking in BleGattServer.kt:754-779 doesn't cause issues with Python's identity handling

Important Files Changed

Filename Overview
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/bridge/KotlinBLEBridge.kt Added dual connection detection logic to distinguish legitimate dual connections from MAC rotation, disables deduplication by default, includes keepalive improvements for connection stability
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/server/BleGattServer.kt Re-enabled Kotlin-side identity detection for peripheral connections to support dual connection tracking, added immediate keepalive support and write failure tracking
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/client/BleGattClient.kt Renamed keepalive failure tracking to write-specific failures, reduced keepalive interval to 7s, added immediate keepalive method for deduplication
reticulum/src/main/java/com/lxmf/messenger/reticulum/ble/model/BleConstants.kt Reduced keepalive interval from 15s to 7s and max write failures from 3 to 2 to stay below L2CAP idle timeout threshold

Sequence Diagram

sequenceDiagram
    participant PhoneA as Phone A
    participant PhoneB as Phone B
    participant BridgeA as KotlinBLEBridge (A)
    participant BridgeB as KotlinBLEBridge (B)
    participant ServerA as BleGattServer (A)
    participant ServerB as BleGattServer (B)
    participant ClientA as BleGattClient (A)
    participant ClientB as BleGattClient (B)

    Note over PhoneA,PhoneB: Dual Connection Establishment

    PhoneA->>ClientA: Start scanning
    PhoneB->>ServerB: Start advertising
    
    ClientA->>ServerB: Connect as central
    ServerB->>BridgeB: onPeerConnected (peripheral role)
    BridgeB->>BridgeB: handlePeerConnected(isCentral=false)
    
    ClientA->>ServerB: Send identity handshake (16 bytes)
    ServerB->>ServerB: Detect identity (re-enabled logic)
    ServerB->>BridgeB: onIdentityReceived(A's identity)
    BridgeB->>BridgeB: Store in identityToAddress map
    
    Note over PhoneB: Phone B now knows A's identity

    PhoneB->>ClientB: Start scanning
    PhoneA->>ServerA: Start advertising
    
    ClientB->>ServerA: Connect as central
    ServerA->>BridgeA: onPeerConnected (peripheral role)
    BridgeA->>BridgeA: handlePeerConnected(isCentral=false)
    
    ClientB->>ServerA: Send identity handshake (16 bytes)
    ServerA->>BridgeA: onIdentityReceived(B's identity)
    BridgeA->>BridgeA: handleIdentityReceived()
    
    Note over BridgeA: Check for dual connection
    
    BridgeA->>BridgeA: Check identityToAddress map
    BridgeA->>BridgeA: Find existing peer with B's identity
    BridgeA->>BridgeA: existingPeer.isCentral=true, newConnection.isCentral=false
    BridgeA->>BridgeA: Opposite roles detected = Legitimate dual connection
    BridgeA->>BridgeA: skipDuplicateCheck = true
    
    Note over BridgeA: Skip Python duplicate check
    
    BridgeA->>PhoneA: Allow both connections (no disconnect)
    BridgeB->>PhoneB: Allow both connections (no disconnect)
    
    Note over PhoneA,PhoneB: Both connections stable for 20+ minutes
    
    ClientA->>ServerB: Keepalive every 7s
    ClientB->>ServerA: Keepalive every 7s
    ServerA->>ClientB: Notify keepalive every 7s
    ServerB->>ClientA: Notify keepalive every 7s
Loading

Repository owner deleted a comment from torlando-proton Jan 31, 2026
// Check for duplicate identity (Android MAC rotation) via Python callback
// Python's _check_duplicate_identity returns True if this identity is already connected
// at a different address, meaning this is a MAC rotation attempt that should be rejected.
// Check if this is a legitimate dual connection (same identity, opposite role).
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude doesn't this kind of logic normally exist in the python layer? is this an abstraction violation?

@torlando-tech torlando-tech marked this pull request as draft February 1, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant