Skip to content

feat(relay)!: no internal client; a session is a client or a peer - #517

Merged
okdaichi merged 5 commits into
mainfrom
feat/drop-internal-client
Oct 12, 2026
Merged

okdaichi merged 5 commits into
mainfrom
feat/drop-internal-client

Conversation

@okdaichi

@okdaichi okdaichi commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #514.

Why

The relay sorted sessions into three classes. Two of them are how every service connects: a client with a credential, and a peer with a peer certificate from the relay CA. The third, the internal client, was a certificate from the same CA without the peering name: no credential, subscribe to anything, announce nothing.

Only the HLS egress used it. The funnel, the other platform service, connects with a credential it signs itself, and could not have used it, since it publishes. So there were two ways for a platform service to reach a relay, and the relay carried a grant, an admission path, a metric label and documentation for one caller.

Change

Relay

  • A certificate from the CA without the peering name grants nothing. Its session is a client's and is asked for a credential like any other. classify returns client or peer; classInternal, internalGrant and admission.internal are gone.
  • CA_FILE without a peer certificate still accepts peers. The startup lines call that inbound only where they said internal clients only.
  • Metrics and log: class is client or peer.
  • A session dropped for presenting the relay's own peer certificate is logged. It was dropped silently.

HLS egress

The egress connects to a relay with auth on in one of two ways:

RELAY_SIGNING_KEY (new) RELAY_CERT_FILE, RELAY_KEY_FILE
The relay takes it for a client a peer
It may subscribe to and fetch RELAY_TRACK_PATH, nothing else everything a relay may
Usage reported under the key none
Lasts a fresh credential on every connect until the certificate expires; re-read on every reconnect
  • RELAY_SIGNING_KEY is why the credential way works at all. A token lives an hour at most (token.MaxLifetime), and the relay checks that when a session starts. A credential fixed in RELAY_URL (?jwt=) therefore admitted the egress once and then refused its first reconnect after the hour, and every one after. The egress now signs a fresh one on each connect, as the funnel does, for its own path alone.
  • The egress refuses to start with settings the relay would not take as meant:
    • a peer certificate beside a signing key or a ?jwt=: the relay never reads a peer's credential, so it would limit nothing;
    • a RELAY_CERT_FILE without the peering name, naming the missing name;
    • a signing key whose prefix does not cover RELAY_TRACK_PATH.

What this costs

  • Breaking for an egress that holds a certificate without the peering name. It refuses to start and says why.
  • An egress as a peer has more than the internal client had. A peer may announce, and the relay would route what it announced. The docs say to issue a peer certificate only to an egress under the same control as the relays. Its sessions count as class="peer" in the session metrics, with relay-to-relay traffic.
  • An egress with a signing key needs its key in the relay's key set, and its usage records reach the usage receiver under that key.

qumo-deploy deploys no HLS egress today, and nothing in it issues a certificate without the peering name.

Review

A review of the first version found nine things, all addressed in the last commit:

  1. ?jwt= could not keep an egress connected past an hour → RELAY_SIGNING_KEY.
  2. An egress given the relay's own peer certificate was dropped silently as "self" → logged by the relay; documented that each holder needs its own certificate.
  3. A certificate beside a credential silently won → refused at startup.
  4. The docs said a credential's expiry ends the session → corrected; it does not.
  5. The refusal message pointed at RELAY_CERT_FILE when one was already set → the missing peering name is caught at startup and named.
  6. relay-config.example.env still described the internal client → rewritten.
  7. A comment in peer_trust.go still named it → fixed.
  8. The metric docs called class="peer" relay-to-relay traffic only → they now say it counts anything holding a peer certificate.
  9. A test counted every auth request, including late readiness probes → it counts requests at a path of its own.

Checked

  • go test ./internal/relay/ ./internal/hls/ and go test -tags=integration ./internal/integration/ pass; golangci-lint run ./internal/... is clean.
  • Tests cover: an egress with a peer certificate subscribes without being asked; a certificate without the peering name is asked for a credential and refused; the credential the egress signs verifies against its key and grants subscribe and fetch on its path, nothing beneath it, no other broadcast and no publishing; each connect signs a new one; the startup refusals.
  • Not run: qumo hls itself against a relay whose key set holds its signing key. The signing is unit-tested and the dial is the one it used before.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PskJnUHF5bxqi5ksWgz63G

okdaichi and others added 4 commits October 12, 2026 17:09
"relay: new session" carried the remote address and whether the session
was checked. It now names the session id, the transport, the path, the
credential's sub, the MoQ version, the ALPN, the TLS version and the SNI,
and for an upgraded session the subprotocols the client offered, its
user agent and its origin. "relay: session ended" is new: the reason,
the duration and the byte totals.

A WebTransport client that offers no subprotocol is served as before;
its line now shows an empty offer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PskJnUHF5bxqi5ksWgz63G
The transport label of the session metrics said "peer" or "internal"
for a relay peer's or an internal client's session. Those are not
transports: both run over native QUIC. The metrics and the session log
now name the class (client, internal, peer) and the transport
(webtransport, websocket, quic) separately.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PskJnUHF5bxqi5ksWgz63G
A native-QUIC session whose certificate the relay CA verified, without
the peering name, was an internal client: served without a credential,
subscribe-only. Only the HLS egress used it, beside the two ways every
other service connects, a credential or a peer certificate.

Such a certificate now grants nothing: its session is a client's and
needs a credential. The HLS egress connects with a credential, or with
a peer certificate as a relay peer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PskJnUHF5bxqi5ksWgz63G
A token lives an hour at most, so a credential fixed in RELAY_URL
stopped admitting the egress at its first reconnect after that, which
left a peer certificate as the only lasting way in once the internal
client was gone. With RELAY_SIGNING_KEY the egress signs a fresh
credential on every connect, for RELAY_TRACK_PATH alone.

The egress refuses to start with a peer certificate beside a
credential, which the relay would never read, and with a certificate
that lacks the peering name. The relay logs a session it drops for
presenting the relay's own peer certificate.

Docs: the credential's expiry does not end a live session; a peer
certificate must be the holder's own; class="peer" counts anything
holding a peer certificate; the example config and a comment still
named the internal client.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PskJnUHF5bxqi5ksWgz63G
@okdaichi
okdaichi changed the base branch from feat/session-log to main October 12, 2026 09:51
@okdaichi
okdaichi merged commit fb2b5fe into main Oct 12, 2026
13 checks passed
@okdaichi
okdaichi deleted the feat/drop-internal-client branch October 12, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant