Source-Specific Multicast got three LGTMs — and the spec disagrees with Chrome

Chrome’s Source-Specific Multicast feature for Direct Sockets picked up its
third LGTM on 2026-10-21, eight days after it was already shipping enabled
by default in Chrome 158 — an unusual ordering that says something about how
the Chromium feature pipeline handles a feature that gets implemented before
its intent process finishes. Meanwhile an open pull request against the WICG
Direct Sockets specification documents a dozen concrete places where the
shipped Chromium implementation and the spec text disagree, most of them
about which DOMException a caller actually gets back.

This post covers both: the intent thread’s approval path, and the spec-vs-
implementation divergence that is still open and unmerged as of this writing.
It also has a note on what this means for a browser-based ADB client trying to
use multicast for device discovery, which is the reason webadb.online pays
attention to this API at all.

The intent thread and its three LGTMs

The intent to ship for Source-Specific Multicast (SSM) was posted to blink-dev
on 2026-10-14 at 19:43 PDT, generated by Chrome Platform Status on behalf of
the feature owner. The thread is
Intent to Ship: Source Specific Multicast for Direct Sockets API,
and the feature’s chromestatus entry is
6208452397498368.

The reply sequence is short and worth reading in full, because it is an
example of the Chromium review process being asked a question it is not
usually asked, and answering it.

2026-10-19, 13:57 PDT — Reilly Grant (Chrome, IWA owner):

IWA OWNER LGTM

This is an extension of an extension of an existing IWA-only API. The
previous justification for why multicast would create a privacy issues for
the web platform (see also threads on net-dev@) still applies.

2026-10-19, 15:48 PDT — Alex Russell (Chrome, API owner for the broader
web platform):

What’s the developer benefit for SSM? Is anyone asking for this?

That question is the interesting one. Blink intents require three LGTMs from
API owners before implementation can proceed, and a feature that is already
in the tree and already enabled by default does not obviously need to survive
that process — the implementation is a fact by then, and the intent is
formality. Asking “is anyone asking for this” puts the burden back on the
requester, which is the reviewer’s job.

2026-10-20, 06:29 PDT — Omar Ramadan (feature owner, Blockcast):

The benefit is that SSM makes the multicast API usable beyond a single LAN.
Multicast that crosses network boundaries is SSM in practice:

  • RFC 8815 deprecates inter-domain ASM.
  • ISP and enterprise deployments carry channels in the SSM ranges (232/8,
    FF3x::/32), where routers only forward source-specific joins (RFC 4607).
  • AMT relays (RFC 7450) serve (S,G) channels.

That is the TreeDN model (RFC 9706): native multicast where the network
supports it and AMT relays everywhere else.

Who’s asking:

  1. My company, Blockcast. We built that workaround, and this intent lets us
    drop it on native networks.

2026-10-21, 08:37 PDT — Alex Russell: Thanks Omar, LGTM1
2026-10-21, 09:38 PDT — Chris Harrelson: LGTM2
2026-10-21, 08:24 PDT — Mike Taylor: LGTM3

Three LGTMs inside a single morning, one business day after the requester’s
answer. The intent is approved.

The answer itself is technically substantial, and it is worth reading for the
specific claim it makes about ASM’s limits. The argument is not “multicast is
useful” — that was already settled when base multicast shipped. It is that
inter-domain multicast is SSM in practice, because that is what the
routing infrastructure carries:

  • RFC 8815 deprecates Any-Source Multicast for inter-domain use.
  • RFC 4607 defines SSM, and ISP and enterprise networks carry their
    channels in the SSM address ranges — 232.0.0.0/8 for IPv4, ff3x::/32 for
    IPv6 — where routers only forward source-specific joins.
  • RFC 7450’s AMT (AMT Relay) serves (S,G) channels.
  • RFC 9706 defines TreeDN, the combination.

The practical consequence for a browser application is the punchline of the
whole thread. An ASM-only client cannot receive those channels off its own
LAN. Its only workaround is to write an AMT gateway in JavaScript over
unicast UDP, which requires a relay and throws away native delivery
everywhere the network would have supported it. The Blockcast Multicast
Gateway (BEACON) Chrome extension is exactly that workaround, so the feature
owner had an existing implementation to point at.

Harald Alvestrand had made the same argument back in November 2025 in
WICG/direct-sockets#80,
which closed the same month the spec PR landed:

Basically Any-Source Multicast with a scope larger than your local network
is a high-powered DDOS vector, not something that it’s safe to build
services on, so I heartily recommend adding support for Single-Source
Multicast.

The intent also notes the precedent: Node.js’s dgram module has had
addSourceSpecificMembership / dropSourceSpecificMembership for years.

The order is strange on purpose

Here is the part that reads like a bug and is not. The chromestatus entry
records the feature as:

1
2
3
browsers.chrome.status:  "Enabled by default"
browsers.chrome.desktop: 158
browsers.chrome.announced: true

Chrome 158 branched on 2026-10-21 — the same day the third LGTM landed. The
feature was therefore in the tree and enabled by default before its
intent process concluded. That is not how the pipeline is supposed to work;
normally the intent comes first and the implementation follows.

What happened is a replay of the sequence this blog recorded on 2026-10-12:
commit
3076e5f5
landed the SSM implementation in the network-service and Blink layers, then
seven months later was reverted —

Reason for revert: Suspicious about causing test failures of
ChromeDirectSocketsUdpIsolatedWebAppMulticastTest.multicastJoinGroupSSMSameGroupDifferentSources
at ci.chromium.org/ui/p/chromium/builders/ci/mac15-x64-rel-tests/15107.

— because of a flaky macOS test, not because of a design objection. The
implementation came back, this time under the two-week release cadence that
Chrome adopted in mid-2026, and shipped in 158. The intent process then had
to catch up to a shipped reality and confirm that shipping it was still the
right call.

That is the honest framing: the LGTMs are not pre-approval of a design, they
are a ratification of a shipped one. The “is anyone asking for this”
question from Alex Russell is what makes it a ratification rather than a
rubber stamp — the answer had to still be good in October 2026, not just in
the November 2025 prototype.

The spec and the implementation do not agree

The second piece of news is quieter and, for anyone writing code against the
API today, more immediately useful.

WICG/direct-sockets#86 —
“Multicast: disallow ASM/SSM mixing per group, validate leaveGroup(),
handle close” — was opened 2026-10-14 by kkroo and is still open. Its
description is a list of five concrete divergences between the spec text and
what Chromium actually does:

  1. joinGroup() throws an InvalidStateError when the socket already has,
    or is joining, a membership of the other kind for the same
    groupAddress. That means an SSM join while an ASM membership exists,
    or an ASM join while SSM memberships exist. The explainer already states
    this rule (“Mixing ASM and SSM memberships for the same group address is
    not allowed”).
  2. leaveGroup() validates groupAddress (multicast) and
    options.sourceAddress (unicast, not loopback or unspecified, same IP
    version) the same way joinGroup() does. It throws a TypeError before
    the InvalidStateError membership check.
  3. Both methods check for a closed socket first, before validating their
    arguments.
  4. leaveGroup() throws an InvalidStateError while the join is still
    pending, because [[joinedGroups]] only holds completed joins.
  5. Closing the socket rejects pending joinGroup() and leaveGroup()
    promises with an AbortError, and joinedGroups is empty afterwards
    (Chromium’s MulticastController::OnCloseOrAbort()).

Point 5 is the one with real architectural weight, and it is worth spelling
out. The spec previously had no defined way to cancel a pending
joinGroup()
. A joinGroup() call issues a setsockopt(MCAST_JOIN_GROUP)
against the network service; if the socket is closed while that is in flight,
the promise had no defined settlement path. Chromium’s implementation
answers it with AbortError, and the spec PR writes that down.

This is not a theoretical concern. The whole reason SSM exists is that
MCAST_JOIN_SOURCE_GROUP is a privileged operation that a browser’s
network-service sandbox has to allow explicitly. The original implementation
landed without those setsockopt codes in the BPF allowlist, and every SSM
join attempt hit the sandbox’s CrashSIGSYSSockopt() and killed the process:

Root cause: MCAST_JOIN_SOURCE_GROUP / MCAST_LEAVE_SOURCE_GROUP were
not in the BPF setsockopt allowlist for the network service sandbox. Any SSM
setsockopt call hit CrashSIGSYSSockopt(), killing the process.

Fix: crrev.com/c/7641869 — MCAST_JOIN_SOURCE_GROUP and
MCAST_LEAVE_SOURCE_GROUP added to both the IPv4 and IPv6 setsockopt case
lists in RestrictSetSockoptForNetworkService().

So a join can fail slowly — after a network round trip — and the failure mode
has to be specified, because “the socket closed while your join was in flight”
is a completely ordinary thing for an application to do during teardown.

The shape of the API

For reference, the API surface is small. MulticastController is obtained
from the multicastController member of the dictionary returned by a bound
UDPSocket‘s opened promise, and it exposes exactly three things:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// The socket must be in bound mode.
const socket = new UDPSocket({ remoteAddress: "0.0.0.0", remotePort: 0 });
const { readable, writable, multicastController } = await socket.opened;

// Any-Source Multicast — the classic form.
await multicastController.joinGroup("239.0.0.1");

// Source-Specific Multicast — only packets from this source are delivered.
await multicastController.joinGroup("239.0.0.1", {
sourceAddress: "192.168.1.50",
});

// Introspection: a DOMString for ASM, a MulticastMembership for SSM.
const groups = multicastController.joinedGroups;

await multicastController.leaveGroup("239.0.0.1", {
sourceAddress: "192.168.1.50",
});

The joinedGroups return type is a deliberate feature-detection mechanism,
and it is the reason the intent could claim the feature is low-risk:

Low. sourceAddress is an optional dictionary member: code that doesn’t
pass it behaves exactly as before, and joinedGroups still returns strings
for ASM memberships. Where SSM is unavailable (older Chrome, or the feature
turned off), sourceAddress is ignored and joinGroup() makes an ASM join;
apps can detect this because joinedGroups then lists a string, not a
MulticastMembership.

That is a well-constructed backwards-compatibility story. An application
that wants SSM can call joinGroup(group, { sourceAddress }) unconditionally
and then inspect joinedGroups to find out whether it actually got a
source-filtered membership or silently degraded to ASM. No feature detection
branch at the call site, no version sniffing.

The mismatches that are being deferred

The PR is honest about what it does not fix. These gaps predate SSM and
apply to ASM as well:

  • The spec compares groupAddress and sourceAddress as strings.
    Chromium compares the parsed IP addresses, and joinedGroups returns their
    canonical form (FF02::1 is listed as ff02::1).
  • The spec orders joinedGroups by join time. Chromium returns hash-map
    order.
  • The spec rejects every OS failure with NetworkError. Chromium maps some
    errors to DataError, NotSupportedError, InvalidAccessError or
    InvalidStateError.

That third bullet is the one that will bite application code. A caller that
writes catch (e) { if (e.name === "NetworkError") { /* retry */ } } is
wrong against the shipped implementation for at least four distinct error
classes. A robust caller has to treat the DOMException name as an
open set.

The PR also notes that the spec already requires rejecting a loopback
sourceAddress, and that Chromium aligns with that in CL
8582708
— a loopback source would defeat the entire security model of SSM, since
RFC 4607’s model assumes a routable source.

What this means for webadb.online

Direct impact on webadb.online today: none, and that is by design.

webadb.online reaches devices over WebUSB, not over Direct Sockets.
The Direct Sockets API is gated behind Isolated Web Apps, and IWAs are
ChromeOS-only for now — an IWA cannot be installed on the Android phone that
is plugged into the user’s laptop, and an IWA cannot ask for a WebUSB device
the way a normal page can. The two mechanisms are complementary rather than
substitutable, and webadb.online uses the one that works in a regular tab on
any platform.

But the reason to keep tracking this API is specific: Direct Sockets
multicast is the only browser-reachable mechanism for mDNS-style device
discovery
, and mDNS discovery is what a Wi-Fi-ADB workflow needs. Today,
webadb.online’s Wi-Fi ADB path requires the user to type the device’s IP
address and port by hand. If an IWA can join a multicast group and listen
for mDNS responses, discovery becomes automatic.

The shape of that future is now much clearer, and it comes with the
caveats this post has been documenting:

  1. SSM is the model that actually works off-LAN. The intent thread’s
    central argument — that inter-domain multicast is SSM in practice
    (RFC 8815, RFC 4607, RFC 7450, RFC 9706) — is exactly the argument for
    an ADB-over-network feature that has to work on a corporate Wi-Fi network
    rather than only on a home LAN. An ASM-only client on an enterprise
    network would receive nothing at all, because enterprise switches
    commonly filter or drop ASM joins outside the local scope.
  2. The API degrades gracefully, but the detection is non-obvious. Because
    sourceAddress is silently ignored where SSM is unavailable, the
    joinedGroups return-type difference is the only reliable signal. Any
    webadb code that adopted this must check the shape of the returned
    value, not just its presence.
  3. The error semantics are still in flux. PR #86 is open. The AbortError
    on socket close, the TypeError-before-InvalidStateError ordering in
    leaveGroup(), and the InvalidStateError for ASM/SSM mixing are all
    documented-but-unmerged. Writing against the spec today means writing
    against a spec that does not yet match Chrome 158.

The third point is the general lesson, and it applies well beyond this API.
The Chromium feature pipeline can ship a feature enabled-by-default and
then run its intent process, and the specification can lag the
implementation by months. When you are writing against a browser API that
just shipped, the implementation is the ground truth and the spec is a
roadmap.

One closing note on the upstream ADB side, for completeness: ya-webadb
itself was completely quiet this week.
The repository’s main branch still
sits at 3a7d781c (2026-10-27, the scrcpy inject-key shorthands), there
were no new releases since v3.0.0-beta.4 on 2026-10-09, and no issue or
pull request in the repository was updated in the last 24 hours. scrcpy
upstream is likewise unchanged since 2026-10-08. Two posts ago this blog
covered the withdrawal of ya-webadb PR #875; that remains the most recent
movement in the library, and there is nothing new to report on it.

Timeline

For readers who want the whole arc in one place:

Date Event
2025-09-23 Explainer for multicast in Direct Sockets merged (WICG#78)
2025-10-09 Multicast added to the spec (WICG#79)
2025-11-19 SSM chromestatus entry created (6208452397498368)
2025-11-26 SSM added to the spec (WICG#81)
2025-12-01 Intent to Prototype posted to blink-dev
2026-02-21 SSM added to the explainer (WICG#83)
2026-02-21 SSM lands in Chromium (main@{1597329})
2026-07-10 Sending to a multicast address requires the permission policy (WICG#85)
2026-09-12 Chromium commit 3076e5f5 reverts the SSM implementation over a flaky macOS test
2026-10-08 SSM relands and is cherry-picked to the 158 branch
2026-10-14 Intent to Ship posted; WICG#86 opened
2026-10-19 Reilly Grant: IWA OWNER LGTM. Alex Russell asks “is anyone asking for this?”
2026-10-20 Omar Ramadan answers; SSM implementation is in the tree for 158
2026-10-21 Alex Russell LGTM1, Chris Harrelson LGTM2, Mike Taylor LGTM3
2026-10-21 Chrome 158 branches
2026-10-27 chromestatus entry 6208452397498368 last updated
2026-10-30 This post

References