relay

August 5, 2026 · View on GitHub

A single-instance MOQT relay (draft-ietf-moq-transport-19).

One UDP port serves both transport mappings: raw QUIC for clients dialing moqt://host:port, and WebTransport (HTTP/3) at -webtransport-path for browsers and anything dialing the https://host:port/path form of the same URI (§3.1.4). Both are QUIC over UDP — the https scheme names an HTTP origin, not a TCP transport — and the listener picks per connection from the negotiated ALPN, so there is no transport to select.

Accepts publisher and subscriber sessions, routes objects between them via the track registry, and caches recent objects per track for late-joining subscribers. No authentication — suitable for local development and testing.

Usage

relay [-addr host:port] [-cert file] [-key file] [-webtransport-path /moq]
FlagDefaultDescription
-addr0.0.0.0:4433UDP address to listen on
-certPEM certificate file. If omitted, an ephemeral self-signed cert is generated.
-keyPEM private key file. Required when -cert is set.
-webtransport-path/moqHTTP/3 path browsers use for the WebTransport CONNECT. Raw QUIC ignores it.
-catalog-track-namecatalogTrack name whose object cache uses -catalog-ttl instead of the default; empty disables the override.
-catalog-ttl0Per-object TTL for tracks matching -catalog-track-name; 0 means infinite retention (FIFO size cap still applies).
-max-subscriptions0Per-session cap on concurrent subscriptions (§13.1); 0 = unlimited.
-max-namespace-requests0Per-session cap on concurrent namespace requests (§13.7.1); 0 = unlimited.

Quick start

# Start the relay (ephemeral self-signed cert, port 4433)
go run ./cmd/relay

# Publish the current time every second to moq-example/clock
go run ./cmd/clock publish

# Subscribe and print each tick
go run ./cmd/clock subscribe

Clients connect with InsecureSkipVerify: true so no extra cert setup is needed for the ephemeral cert. For a persistent cert (e.g. from Let's Encrypt or mkcert):

go run ./cmd/relay -cert server.crt -key server.key

Shutdown

The relay handles SIGINT and SIGTERM. On receipt it sends GOAWAY (§10.4) to all active sessions with a 5-second grace period, then force-closes any that have not drained — and it does not exit until that drain finishes, so peers actually receive the GOAWAY rather than losing the connection under them.

Interop testing

This directory ships a Dockerfile and entrypoint-relay.sh that package the relay for the moq-interop-runner framework. The entrypoint maps the runner's MOQT_* environment variables onto the relay's flags:

EnvDefaultMaps to
MOQT_PORT4443-addr 0.0.0.0:$MOQT_PORT
MOQT_CERT / MOQT_KEY/certs/cert.pem, /certs/priv.key-cert / -key
MOQT_TRANSPORTwebtransportAccepted and logged only — both mappings are always served, so it selects nothing. An unknown value is still rejected.
MOQT_WEBTRANSPORT_PATH/-webtransport-path

Run the suite locally

From the repository root, against a third-party test client (defaults to moq-dev-rs, draft-18), over both transports:

make interop-loopback            # our client -> our relay, both transports (CI gate)
make interop                     # raw QUIC + WebTransport vs a third-party client
make interop-quic                # raw QUIC only
make interop-webtransport        # WebTransport only
make interop CLIENT_IMAGE=ghcr.io/englishm/moq-interop-runner-moq-test-client-draft-18:latest
make interop-matrix              # several draft-18 clients × transports, pass/skip/fail table

These build the relay image from source, generate a self-signed cert pair under interop/certs/, and run interop/docker-compose.yml (relay + client on a Compose bridge network, so it works on both Linux and Docker Desktop for macOS). interop-matrix is driven by interop/matrix.sh; edit its CLIENTS list (or set the CLIENTS env var) to add or mark clients.

make interop is not run by CI: every published third-party client still speaks draft-18, while this relay advertises only the moqt-19 ALPN, so the handshake cannot complete and every case fails with "failed to connect to server". The CI job is kept but gated to manual workflow_dispatch; drop the if: in ci.yml once those images move to draft-19. make interop-loopback is what gates PRs meanwhile — same six cases against our own relay image over both moqt:// and https://, so the container's WebTransport path stays covered.

Client direction

The inverse — our own client (cmd/interop-client) against a relay — is make interop-client (loopback by default; override CLIENT_RELAY_IMAGE / CLIENT_RELAY_URL for a third-party relay). See that command's README for details.

Run via the runner itself

The relay is registered as moq-go in the runner's implementations.json (both a relay and a client role). After building the images (make interop-build, make interop-client-build):

cd ../moq-interop-runner
make interop-relay RELAY=moq-go       # our relay ← every registered client
make interop-client CLIENT=moq-go     # our client → every registered relay