fix(ble): stabilize dual connections by detecting legitimate dual-connection scenarios#390
fix(ble): stabilize dual connections by detecting legitimate dual-connection scenarios#390torlando-tech wants to merge 1 commit intomainfrom
Conversation
…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>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile OverviewGreptile SummaryFixes 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
Test CoverageManual 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
Important Files Changed
Sequence DiagramsequenceDiagram
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
|
| // 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). |
There was a problem hiding this comment.
@claude doesn't this kind of logic normally exist in the python layer? is this an abstraction violation?
Summary
Changes
Test plan
🤖 Generated with Claude Code