diff --git a/modules/ble-mesh/README.md b/modules/ble-mesh/README.md index 1f1624c..4483d0d 100644 --- a/modules/ble-mesh/README.md +++ b/modules/ble-mesh/README.md @@ -211,7 +211,14 @@ hold, or they discard opposite links and end up with none: > the device with the lexicographically **smaller** tag keeps the link it > **dialled out** on. -On the other phone that same wire is the inbound one, so both keep it. +On the other phone that same wire is the inbound one, so both keep it. Both +platforms run this resolver (`resolveDuplicateLinks` on iOS, +`resolveDuplicateLink` on Android). + +`HELLO` is accepted **once per link**: a second one on an already-announced link +is ignored, never re-labelled. The wire is unauthenticated, so otherwise a +stranger could claim a peer's tag to spoof payload attribution, or collide tags +to make the resolver tear a victim's real link down. A link that has not sent `HELLO` within 10 s is dropped. Android will only give you a handful of concurrent GATT connections — typically around seven, after which diff --git a/modules/ble-mesh/android/src/main/java/expo/modules/blemesh/BleMeshModule.kt b/modules/ble-mesh/android/src/main/java/expo/modules/blemesh/BleMeshModule.kt index bed9c46..8cbc186 100644 --- a/modules/ble-mesh/android/src/main/java/expo/modules/blemesh/BleMeshModule.kt +++ b/modules/ble-mesh/android/src/main/java/expo/modules/blemesh/BleMeshModule.kt @@ -1058,13 +1058,23 @@ class BleMeshModule : Module() { * advertised tag at all still resolves normally here. */ private fun handleHello(link: Link, tag: ByteArray) { + // HELLO is write-once. The tag was assigned above this guard before, so a + // re-HELLO on the unauthenticated wire re-attributed a live link (#29). + if (link.announced) return if (tag.size != TAG_BYTES) { dropLink(link.peerId, announce = false) return } link.remoteTag = hex(tag) - if (link.announced) return + // Keep one link when both directions came up, same tie-break as iOS (#52). + val loser = resolveDuplicateLink(link) + if (loser == link.peerId) { + // Peer keeps the other direction; retire this one before it announced. + dropLink(link.peerId, announce = false) + return + } + link.announced = true // A link that reached HELLO is proof the peer is dialable, so the backoff // ladder for it starts from scratch next time. @@ -1081,6 +1091,30 @@ class BleMeshModule : Module() { "isIncoming" to link.isIncoming ) ) + + // Drop the duplicate after announcing, so dropLink's same-tag guard sees a + // live link and stays quiet — JS keeps one peer, no drop/re-add. + if (loser != null) dropLink(loser, announce = true) + } + + /** + * The peerId of the link to discard when [link] duplicates one to the same + * peer, else null. Same tie-break as the iOS resolver, computed identically + * on both phones: smaller tag keeps the link it dialled out on. + */ + private fun resolveDuplicateLink(link: Link): String? { + val tag = link.remoteTag ?: return null + val other = links.values.firstOrNull { + it.peerId != link.peerId && it.remoteTag == tag + } ?: return null + + val weAreSmaller = hex(localTag) < tag + val keeper = if (weAreSmaller) { + if (link.isIncoming) other else link + } else { + if (link.isIncoming) link else other + } + return if (keeper.peerId == link.peerId) other.peerId else link.peerId } // ------------------------------------------------------------------------- diff --git a/modules/ble-mesh/ios/BleMeshModule.swift b/modules/ble-mesh/ios/BleMeshModule.swift index 87ab744..2eb44fe 100644 --- a/modules/ble-mesh/ios/BleMeshModule.swift +++ b/modules/ble-mesh/ios/BleMeshModule.swift @@ -791,6 +791,9 @@ private final class BleMeshRadio: NSObject { /// is in-band, full width, and symmetric, so a link that connects with no /// advertised tag at all still resolves normally here. private func handleHello(link: Link, tag: Data) { + // HELLO is write-once. The wire is unauthenticated, so a HELLO on an + // announced link is a stranger trying to re-label or starve it (#29). + guard !link.announced else { return } guard tag.count == Wire.tagBytes else { dropLink(link.peerId, announce: false) return @@ -802,7 +805,6 @@ private final class BleMeshRadio: NSObject { if loser == link.peerId { return } } - guard !link.announced else { return } link.announced = true emit("onConnected", [ "id": link.peerId,