Saturday, October 31, 2026

WebRTC-Direct examples in every language: no signaling, plus TURN and STUN modes

The latest grpc-over-datachannels release ships WebRTC-Direct examples for every supported language — generated, like the runtimes themselves. A server listens on a UDP socket and prints a certhash multiaddr (/ip4/…/udp/…/webrtc-direct/certhash/f…); a client dials that address and gets a DTLS/SCTP data channel carrying plain gRPC frames. No signaling server, no SDP exchange.

Three ways to connect

Direct — the client dials the multiaddr straight over UDP. This is the LAN and public-host path, and the server prints everything a client needs on one MULTIADDR line.

Via TURN — a client behind a symmetric NAT allocates a relay on a TURN server and dials the same listener through it. TURN is purely a client-side tool here: the server never changes and never learns TURN exists. Set DIRECT_TURN=host:3478 (plus user/password) or pass --turn to the sidecar.

Full ICE with STUN — a Direct listener gathers no ICE candidates, so STUN belongs to the peer mode: the Go and Rust examples include peer-serve/peer-dial modes where GRPC_DC_STUN=stun.l.google.com:19302 enables server-reflexive candidate gathering, with SDP exchanged through files as a stand-in for any signaling channel. Browsers join through this same full-ICE path — a browser has no UDP sockets, so it can never dial a multiaddr directly.

Every language, one shape

JavaScript, Go, and Rust embed the WebRTC stack and run the examples natively. The other thirteen languages — Python, Ruby, PHP, Lua, C, C++, Dart, Java, Kotlin, Scala, Swift, C#, and Zig — reach WebRTC-Direct through their generated SidecarTransport plus the small Rust direct_sidecar bridge, so the language code is identical whether the path is direct or TURN. Each example has serve and dial modes, and the compose file in examples/direct wires servers and clients together with a shared /signal volume as the whole client→server handoff, coturn fixture included.

Try it

Everything lands in examples/direct in the upcoming v0.36.0 release (December 1): start the Node server, copy its multiaddr, and dial it from any language. The per-language catalog lives at browser-grpc/examples. Two honest limits, documented in the README: a NATted listener still needs a port forward, a public host, or the TURN dial — STUN alone cannot make an inbound UDP listener reachable — and browser + tunnel + TURN is not supported, because the quick tunnels are TCP-only.

Friday, October 30, 2026

v0.35.12: the event bus gets its own repository

Release v0.35.12 of grpc-over-datachannels is out, and its headline is organisational: the generated event bus now has a dedicated home at browser-grpc/eventbus, the same treatment the Kafka proxy got with browser-grpc/kafka-proxy.

What is in the repo

The schema (proto/events.proto, grpcdc.events.v1.EventBus), the complete dependency-free Python and JavaScript trees — broker, clients, demos, and tests that run as-is — and the generated EventBus stubs, brokers, demos, and tests for the other fourteen languages under gen/<lang>/, ready to drop into the main repo's runtime trees. An EVENTBUS.md guide covers the service surface: typed routing tags, containment filters, broker-assigned sequence numbers, and the git-backed log variant.

Run it

The demo ships as a container: docker run --rm ghcr.io/browser-grpc/eventbus-demo:latest starts a broker, subscribes with a topic filter, and publishes three events so you can watch the filter admit two of them. The repo's CI builds the test image and runs the Python and JavaScript suites plus both demos on every push — it is green on the release commit.

Everything else moved too

As usual the release is published across GitHub, npm (grpc-dc), PyPI, NuGet (GrpcDc), Maven Central (io.github.browser-grpc:grpc-dc), and GHCR, all at 0.35.12. Background on the design is in A generated event bus; the main project remains browser-grpc/grpc-over-datachannels.

A generated event bus: publish/subscribe as a gRPC service in sixteen languages

Most systems eventually grow an event bus, and most event buses arrive as a heavyweight broker plus a client library per language. grpc-over-datachannels takes the generator's route instead: define the bus once as a gRPC service — grpcdc.events.v1.EventBus — and generate the broker and clients in all sixteen languages from that one schema.

The whole API is two calls

Publish takes routing tags, an opaque payload (typically your own serialized event proto), and a publisher id, and returns a broker-assigned sequence number. Subscribe takes a containment filter and returns a stream of events. An event carries its tags, payload, the broker-global sequence (1-based, in publish order), publisher id, and the broker's wall-clock timestamp.

Tag-value filtering

Tags are typed values — string, int64, double, bool, or any enum carried by number so it survives peers that do not know the enum. A subscription filter is a list of (tag, value) pairs, and an event is delivered when its tag list contains every pair in the filter; an empty filter receives everything, and the same tag may appear several times when an event must carry several values. There is no query language to learn and no stringly-typed topic hierarchy to govern: matching is set containment, evaluated the same way in every generated implementation.

Why generated matters here

Because the bus is just a .proto, publishers and subscribers can be browser tabs, native services, or mixed pairs over a DataChannel, and the broker itself has a generated implementation per language — including a git-backed event log variant for durable, inspectable history. Sequence numbers make ordering claims testable, and the same generated BDD scenarios that grade the runtimes grade the bus. If your architecture needs events, you already have the client: it was generated with your other stubs.

Update (v0.35.12): the event bus now has a dedicated repository — browser-grpc/eventbus — with the schema, the full Python and JavaScript trees, generated sources for the other fourteen languages, and CI. Run the demo with docker run --rm ghcr.io/browser-grpc/eventbus-demo:latest.

One open port for every peer: PeerMux and single-socket WebRTC serving

Running a WebRTC server usually means socket sprawl: a listener per peer, or a framework that owns your sockets for you. grpc-over-datachannels takes the opposite approach with PeerMux: many signaled peers hosted on one bound UDP socket — the single-port topology popularised by large conferencing systems — implemented in the Rust, Go, and JavaScript (Node) stacks.

Routing packets without addresses

A mux cannot route by source address: peers sit behind NATs, addresses get reused, and two muxes may peer with each other. So PeerMux routes by protocol contents instead. STUN packets route by ICE ufrag. The first DTLS flight routes by a pair-hint extension. Every later DTLS record routes by its RFC 9146 connection id. Once the DataChannel is up, the gRPC frames never see the socket at all — the mux is purely a packet demultiplexer under the stack.

What falls out of the design

A server needs exactly one open port for all its peer connections, which simplifies firewalls, containers, and home hosting. TURN still works on muxed peers (the allocation's UDP leg rides a dedicated per-peer socket). And because routing never depends on who sent a packet from where, two muxes can peer with each other directly.

Verified cross-stack

One generated scenario tests all three stacks, and the cross-stack matrix pairs mux against mux across implementations — Rust serving Go, Go serving JavaScript, and so on — so the routing rules are graded against independent code, not just against the implementation that proposed them. If you are embedding WebRTC serving into a native process, one port is all you have to give it.

WebRTC with no operating system: the embedded profile

WebRTC is usually something that happens on phones, browsers, and servers — places with an operating system, a network stack, and memory to spare. But a gRPC-speaking sensor or controller should not need Linux just to make an encrypted call. The WebRTC stack under grpc-over-datachannels is built no_std for exactly that reason.

Traits instead of an OS

The Rust stack cross-builds for Cortex-M. Everything an OS would normally provide is injected through platform traits: a clock, an entropy source, and the datagram transport. On top of those traits sits an in-crate network stack — Ethernet framing, ARP, IPv4/IPv6, UDP, and DHCP — so the device does its own networking from the wire up: plug in Ethernet, get an address, run ICE/DTLS/SCTP, open a DataChannel, speak gRPC.

Proving it without hardware on your desk

The bring-up is demonstrated by a two-device example over a virtual cable (cargo run --example iot_cable in webrtc/rust): power-on to DataChannel echo between two stack instances with nothing between them but the simulated wire. That same frame protocol then carries generated gRPC stubs — the small C99 and Lua runtimes target constrained hosts too, and every generated runtime is dependency-free, which matters when there is no package manager to lean on.

Why bother

Because the alternative for embedded RPC is usually a bespoke protocol you now own forever. A generated stub, a standard wire format, and interoperability graded against browsers and stock gRPC implementations give an embedded target the same contract as every other peer in the system — including calling, and being called by, a browser tab.

One data channel, many calls: the grpc-dc frame protocol

gRPC normally rides on HTTP/2: one TCP connection, many streams, HPACK headers, flow-control windows. WebRTC DataChannels already give us a reliable, ordered, encrypted message pipe — so grpc-over-datachannels replaces the HTTP/2 layer with a small frame protocol designed for that pipe. This post is a tour of the wire format, specified in SPEC.md in the repository.

One message, one frame

DataChannels preserve message boundaries, so every binary message is exactly one frame: a type byte, a 32-bit big-endian stream id, and a payload. The frame types are deliberately few. HEADERS opens a call and carries a small JSON document — the method path and metadata — instead of an HTTP header block. DATA carries exactly one serialized protobuf message, untouched. HALF_CLOSE says the client will send no more, mirroring gRPC semantics. TRAILERS ends the call with status, and RST aborts from either side.

Stream lifecycle

The initiating peer allocates odd stream ids (1, 3, 5, …), the other side even ones, so both peers can open calls without coordination. A full-duplex call is: HEADERS from the client, HEADERS back with initial metadata, any number of DATA frames, HALF_CLOSE, more DATA, then TRAILERS. A unary call is just the degenerate case of one DATA frame each way — there is no separate unary fast path to keep in sync across sixteen runtimes.

The details that matter

Deadlines travel as the familiar grpc-timeout metadata key, recomputed at each forwarding hop. Unknown frame types are ignored, which gives the protocol forward compatibility without a version negotiation. Malformed frames, unknown stream ids, and calls to unregistered methods fail closed with an error status. And because a data channel is a stream of messages rather than bytes, backpressure is handled per stream at the frame level rather than by parsing a byte stream.

The full specification, including tracing propagation and the deliberate divergences from gRPC-over-HTTP/2, lives in the grpc-over-datachannels repository. Try the frames live in the in-browser generator.

Rebuilding WebRTC from scratch: ICE, DTLS, SCTP with zero dependencies

Every language runtime in grpc-over-datachannels is generated and dependency-free — but a gRPC client in Go or Rust still needs something underneath that speaks WebRTC to a browser. Native WebRTC libraries are large, stateful, and different in every language. So the project reimplements the WebRTC stack itself: ICE for connectivity, DTLS for encryption, SCTP for the data-channel transport, and DCEP for channel setup. This post explains the profile we implemented and the rules that kept it tractable.

A profile, not the whole RFC stack

WebRTC is a family of specifications with many optional corners. The reimplementation targets a profile: the subset real browsers actually negotiate, verified continuously against Chromium and Firefox (driven headlessly through Playwright) rather than against our reading of the RFCs alone. Where browsers diverge from the documents, the browsers win, and the divergence is written down in the spec.

What is in the stack

STUN/TURN for NAT traversal (with per-language tests against public STUN servers in CI), ICE with host/server-reflexive/relay candidates, DTLS 1.2 handshakes, SCTP with ordered-reliable and lossy/unordered channel modes, and a single-socket multiplexer (PeerMux) that hosts many peers on one bound UDP port. WebRTC-Direct gives native peers a libp2p-style direct dial without SDP file shuffling. The Rust stack is no_std: platform traits inject clock, entropy, and datagrams, and an in-crate network stack provides Ethernet framing, ARP, IP, UDP, and DHCP, so the same code runs on embedded targets with no operating system.

Why zero dependencies is the point

A generated client you paste into a project should not drag in a dependency tree you have to audit. The stack builds from each language's standard library only, which is also what makes the same wire behaviour reproducible across the Go, JavaScript, and Rust stacks: they are checked against each other and against real browsers, not against a shared library's quirks. The design rules and verification strategy are in SPEC-WEBRTC.md in the repository.