Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion modules/ble-mesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

// -------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion modules/ble-mesh/ios/BleMeshModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading