Repository navigation
feat(bindings): let browsers on one network find each other in a nearby room - #635
Merged
Merged
Conversation
…by room
A page cannot scan for Bluetooth, browse DNS-SD or join Wi-Fi Direct, so two
browsers on one network had to carry WebRTC codes or a room code between
them. What they share is the address the internet sees them from. A
NearbyRoom is a Nostr topic and key derived from the application id and that
address (IPv4, or the IPv6 /64, since each IPv6 device has its own), learned
from a server-reflexive candidate of the application's STUN server. Each
device announces a presence every 5 s, lists the others it heard within
15 s, and links over WebRTC to one its person picks; the link runs on the
local network.
The room key is public by construction, so it seals only presences, which
hold a key used for this room and the application's label. Every connection
code is sealed with NIP-44 to one presence key, under the room key as a
second layer with the role bound in, so a holder of the room key reads no
code and no local address. admit({ peer, label }) is required and decides
every offer; a refused session is ignored while its offer stays fresh. The
relays and STUN servers are the application's, and a room without them
refuses to open.
The transport crate gains seal_to_key and open_from_key over its NIP-44
code. RoomChannel now takes its subscription and opener as a source, so the
nearby room reuses the signaller's relay handling unchanged. The test
runner starts a STUN responder that reports a fixed address, and Firefox
runs with loopback ICE gathering so it can reach it. The lobby example
gains a nearby section that asks before it admits. Documented as W13,
R30 and ADR 0034.
bahdotsh
force-pushed
the
feat/web-nearby-room
branch
from
October 25, 2026 12:21
59ae409 to
83246d4
Compare
The nearby room borrowed its limits from the signalling room, where only a holder of an eighty-bit code can offer. Here the room key is derived from an IP address, so *anyone on the internet* can offer. Same caps, very different room. It turns out that a refused session stayed among the sixteen the host decides on, and every repeat kept it alive. Sixteen refused sessions republished by a stranger, and the host never admitted anyone again, with no event and no log. The device list had the same disease: full at sixty-four, it refused newcomers, so whoever filled it first owned it. Presences were sealed under the public key alone, so filling it, relabelling a real device or keeping a departed one listed took nothing but a guess at the address. And the label on an offer sat outside the NIP-44 seal, so anyone could re-wrap a real joiner's offer under a name of their choosing before the host was asked. This is not great. Refused sessions now live apart, bounded, only so their repeats are not asked about again; an offer dropped while sixteen wait on the application is reported as a `full` event, and `admit` gets an AbortSignal so a question nobody answered comes down. A presence is signed by the key it announces, the label rides inside the seal, and a full list forgets the device heard from longest ago. The signature is now checked after the cheap AEAD, but still checked: it binds created_at, and without it a relay could re-stamp an old offer. While at it: open() closes what it opened when the first announce fails, a closed room refuses to open or connect again, and publicAddress prefers IPv4, because on a dual-stack network the family that answers first varies and two devices that picked differently landed in different rooms. The docs stop citing sixteen sessions as a protection and say what the SDK does not bound.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #634. This branch contains #634's three commits. Review only the last commit,
59ae4095. Merge #634 first, then merge main into this branch.What this does
A page cannot scan for Bluetooth, browse DNS-SD or join Wi-Fi Direct, so two browsers on one network had no way to notice each other.
NearbyRoommeets the browsers behind one public address in a Nostr room derived from that address and the application id. It lists them by the label each application chose and links over WebRTC to the one a person picks. The relays carry only the setup, and the link runs on the local network.admit({ peer, label })is required and decides every offer. A refused session is ignored while its offer stays fresh, so republished offers do not ask again.Documented as W13 in
docs/bridges/web.md, residual risk R30, and ADR 0034. ADR 0034 records that same-network discovery for browsers needs the internet and is shared with everyone behind the address.Supporting changes
seal_to_keyandopen_from_keyover its existing NIP-44 code, with a round-trip and wrong-key test.RoomChannelinnostr-signal.jsnow takes its subscription and opener as a source. The nearby room reuses the signaller's relay handling, with redial, probe and keep-alive unchanged.test/stun-server.mjsis a STUN Binding responder in Node's standard library. It reports a fixed documentation address, so a page on one machine gets a server-reflexive candidate.test-browser.shstarts it. Firefox runs withmedia.peerconnection.ice.loopbackon so it gathers toward loopback, the same kind of test-host setting as the existing obfuscation pref.Testing
cargo test -p offline-protocol-web -p offline-protocol-transport --lib: 58 and 315 pass. Seven new nearby tests check:bash bindings/web/scripts/test-browser.sh: 21 of 21 pass, seven pages in Chromium, Firefox and WebKit. Onnearby.html:-D warnings.Not done
docs/bridges/web-devices.md, which also covers the phone-on-cellular case where the two must stop listing each other.