Internet-Draft moq-cluster September 2026
Curley Expires 25 March 2027 [Page]
Workgroup:
moq
Internet-Draft:
draft-lcurley-moq-cluster-01
Published:
Intended Status:
Informational
Expires:
Author:
L. Curley

MoQ Cluster Extension

Abstract

This document defines a clustering extension for MoQ Transport [moqt], used to build a mesh of relays. Each namespace advertisement carries the list of Hop IDs it has passed through, starting with the original publisher, and the accumulated cost of that path. A receiver uses the list to detect loops and to tell which advertisements come from the same publisher, and the cost to choose between paths. Each endpoint declares its own Hop ID at setup, so a peer never advertises or serves it a path that already passed through it.

Note to Readers

This document was generated by an AI model from the implementation at github.com/moq-dev/moq and is maintained alongside it. Submit an issue or PR if this spec sucks and you want to fix anything.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 25 March 2027.

▲

Table of Contents

1. Conventions and Definitions

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Upstream and downstream are relative to the flow of an advertisement, not to the endpoints: the peer that sends an advertisement is upstream, the one that receives it is downstream. The same pair of relays can be upstream of each other for different namespaces.

2. Introduction

[moqt] is designed to deliver content through a mesh of relays but does not say how to build one, and the base protocol does not carry enough information to do so. Relays that simply forward PUBLISH_NAMESPACE to each other break down: advertisements loop forever, and a relay that hears one namespace from two peers has no basis for choosing where to send a SUBSCRIBE.

This extension adds two parameters to PUBLISH_NAMESPACE and NAMESPACE. HOP_PATH lists every endpoint an advertisement has passed through, starting with the original publisher, which breaks loops and lets paths be compared. ROUTE_COST is the accumulated price of the path: the publisher seeds it, and each hop adds the RELAY_COST its upstream declared at setup, so an unpriced mesh ranks by hop count. A relay that already carries a namespace advertises a lower cost, steering subscribers toward its warm copy.

Each endpoint also declares its own Hop ID at setup, so a peer can leave it out of every path it advertises or serves to it, even across several connections between the same two relays. An advertisement is one path, so a relay forwards only the best path it knows per namespace and serves a subscription from one source at a time (Section 8).

3. Setup Negotiation

3.1. Hop ID

The extension is negotiated during SETUP ([moqt] Section 10.3). An endpoint offers it by declaring its own Hop ID:

HOP_ID Setup Option {
  Option Key (vi64) = 0x40B54
  Hop ID (vi64)
}

Negotiation is per session; a relay MUST NOT assume that because one session negotiated the extension, another did. On a session that did, every PUBLISH_NAMESPACE and NAMESPACE MUST carry HOP_PATH, NAMESPACE takes the extended form in Section 5, and a receiver MUST close the session with a PROTOCOL_VIOLATION if either arrives without HOP_PATH.

3.2. Relay Cost

An endpoint MAY declare what it charges for sending content:

RELAY_COST Setup Option {
  Option Key (vi64) = 0x40B56
  Option Value (vi64)
}

The value prices the sender's own egress, so each endpoint declares its own and the two need not match, as OSPF prices each router's own output interfaces ([RFC2328], Section 9). A receiver adds it to the ROUTE_COST of every advertisement that peer forwards (Section 6.2). Absent means 1, so an unpriced mesh ranks by hop count. 0 is distinct from absent: it makes the link free, which is how to describe two relays in the same datacenter.

A declared cost is an assertion, not an instruction: a receiver MAY charge a locally configured value instead, so a peer cannot make itself cheap by saying so.

The cost is one dimensionless integer, as in every deployed routing metric: RIP's hop count ([RFC2453], Section 3.5), OSPF's interface cost, and IS-IS's default metric, whose delay, expense, and error metrics went unimplemented ([RFC5305], Section 3), as did OSPF's per-type-of-service metrics ([RFC2178], Appendix G.10). A deployment that weighs latency, hop count, and price folds them into the one value. Like BGP's MULTI_EXIT_DISC ([RFC4271], Section 5.1.4), the value only means something within the deployment that chose its units, so a trust boundary clamps or replaces it (Section 9).

4. Hop IDs

A Hop ID is a variable-length integer naming one endpoint in a path.

Hop IDs SHOULD be unique among the endpoints an advertisement can traverse. An endpoint MAY pick one at random, since collisions in a 64-bit space are unlikely, or use a configured identifier that survives restarts.

Loops and origins are detected by comparing Hop IDs for equality, so two endpoints sharing one are indistinguishable. Redundant publishers of interchangeable content MAY share one deliberately, so the mesh treats their paths as failover options for the same content (Section 7).

4.1. The Reserved Hop ID 0

0 means "no identity" and is reserved. It stands for an endpoint that did not negotiate this extension, and an endpoint MAY declare it to withhold its identity.

Since any number of endpoints can be 0, it identifies nothing:

  • Loop detection: 0 in a HOP_PATH is never a loop. A receiver whose own Hop ID is 0 cannot detect loops through itself and MUST NOT discard an advertisement merely because the path contains 0.

  • Origin identity: an advertisement whose first entry is 0 has an unknown publisher. A receiver MUST NOT treat two such advertisements as interchangeable (Section 7).

  • Filtering: a peer that declared 0 gave the receiver nothing to filter that session on. The receiver MAY assign an ID of its own (Section 4.2) as local selection state and MUST NOT write it into HOP_PATH.

Duplicate non-zero Hop IDs in one HOP_PATH are a loop; duplicate zeros are not. Declaring 0 trades loop detection and failover for anonymity, except against a receiver that assigns an identity of its own.

4.2. Assigned Identities

A receiver MAY assign a Hop ID to a peer that declared none, whether by declaring 0 or by not negotiating the extension. It uses that ID as local selection state: as what it filters that session on, including for advertisements that arrived carrying their own HOP_PATH.

The ID is the receiver's own, not the peer's, and MUST NOT be forwarded. An advertisement that arrives with its own HOP_PATH already names the sender there, as 0 if withheld. A receiver writes 0 for an upstream that sent no HOP_PATH (Section 6.1).

An assigned ID MUST NOT be shared between peers not known to be the same endpoint. Sharing one makes their content interchangeable (Section 7) and suppresses each one's advertisements to the other, so two unrelated publishers would be merged into one and starve each other of routes.

A peer the receiver authenticated, or dialed and therefore chose, SHOULD get one stable ID, so its reconnects and redundant sessions are recognized as the same content; a fresh ID per connection would make one peer look like several. An anonymous accepted session cannot be correlated with anything, so it SHOULD get a distinct ID per session: not an identity, but enough to keep routes learned from it from being advertised back to it, which is the loop 0 cannot prevent.

5. Namespace Advertisements

HOP_PATH and ROUTE_COST are Key-Value-Pair parameters ([moqt] Section 2.5). PUBLISH_NAMESPACE ([moqt] Section 10.15) already carries parameters. NAMESPACE ([moqt] Section 10.16) does not, and a subscriber-driven mesh propagates advertisements as NAMESPACE, so this extension appends a parameter block to it:

NAMESPACE Message (Cluster) {
  Type (vi64) = 0x8,
  Length (16),
  Track Namespace Suffix (..),
  Number of Parameters (vi64),
  Parameters (..) ...
}

The added fields are encoded exactly as in PUBLISH_NAMESPACE. Negotiating this extension enables the block on every NAMESPACE, with a parameter count of 0 when it is empty; when another extension defines the same block an endpoint appends one block holding the parameters of both, not two blocks. An endpoint MUST NOT append the block when nothing negotiated it, and MUST NOT include HOP_PATH or ROUTE_COST unless this extension is.

NAMESPACE_DONE ([moqt] Section 10.17) carries no state from this extension.

An advertisement claims capability, not inventory: namespaces beneath the advertised one can be served, not that any exists. Per-request refusals follow [I-D.lcurley-moq-pattern].

5.1. HOP_PATH Parameter

HOP_PATH is the ordered list of Hop IDs an advertisement has passed through, from the original publisher to the peer sending it:

HOP_PATH Parameter {
  Type (vi64) = 0x40B57
  Length (vi64)
  Hop ID (vi64) ...
}

The list always has at least one entry, the original publisher, 0 if unknown (Section 4.1). A receiver MUST close the session with a PROTOCOL_VIOLATION if the list is empty, if the entries do not exactly fill Length, or if a non-zero Hop ID appears twice.

5.2. ROUTE_COST Parameter

ROUTE_COST is the marginal cost of subscribing through this advertisement: the price of the transfers a new subscription would cause.

ROUTE_COST Parameter {
  Type (vi64) = 0x40B58
  Value (vi64)
}

It is OPTIONAL and absent means 0. Costs still accumulate across a mesh that sends none, because each receiver adds the RELAY_COST of the link it received over (Section 6.2).

The original publisher seeds the value with its production cost: 0 for content it already produces, higher for content it would have to start on demand, such as a standby transcoder advertising everything it could serve.

A standby seed only ranks last if no live path can accumulate past it, which is a property of the deployment, not of the number. A deployment relying on standby ordering within one specificity tier (Section 7) MUST bound the charged links on an admitted path by H and each link's cost by C, including the receiving link, and MUST enforce both when admitting paths and links. Its live publishers MUST seed 0 and its standby publishers MUST seed above H * C and below saturation; 2^32 is RECOMMENDED where H * C < 2^32. Unknown, out-of-budget, and saturated routes are outside the guarantee: a receiver MUST NOT rank them above standby capacity on the guess that they already carry content.

6. Relay Behavior

A relay forwarding an advertisement MUST append its own Hop ID to the HOP_PATH it received, so its ID is always the last entry. A received 0 is forwarded unchanged.

A relay MUST discard an advertisement whose HOP_PATH already contains its own non-zero Hop ID: forwarding it would extend a loop, and subscribing through it would route the relay back to itself. This check catches loops of any length and is the only loop defense required. A conforming sender never sends one (Section 7), so a receiver MAY close the session with a PROTOCOL_VIOLATION instead; discarding is what keeps the mesh working when one member does not conform.

6.1. Bridging

An upstream that did not negotiate the extension sends no HOP_PATH. The relay creates one with a single 0 entry for that upstream (Section 4.1), then appends its own Hop ID. The identity a receiver assigned that upstream (Section 4.2) is local selection state and MUST NOT appear in HOP_PATH.

6.2. Accumulating Cost

Before forwarding or acting on an advertisement, a relay MUST add the RELAY_COST the sender declared (Section 3.2) to the ROUTE_COST it received. The addition MUST saturate rather than wrap, so an absurd value ranks last instead of overflowing to best.

A relay actively carrying the namespace (a live subscription exists for at least one of its tracks) SHOULD advertise 0 instead: its ingress is already paid for, so another subscriber costs only the links below it. This is what lets a cluster converge on a warm copy. The discount applies only to the path it actually serves from; a standby path keeps its accumulated value, since serving from it means opening a fresh ingest. When it stops carrying the namespace it SHOULD restore the accumulated value, optionally after a grace period so brief churn does not flap routing.

Two relays that each begin carrying the same namespace would each see the other's 0 as cheaper than its own source, and if both switched at once the namespace would have no source. Before re-parenting onto a 0-cost advertisement from another actively-carrying relay (one whose HOP_PATH has two or more entries), a relay SHOULD apply a deterministic tie-break, such as comparing a hash of the namespace and each Hop ID, so exactly one side moves. Equal Hop IDs, including two relays that both declared 0, cannot be ordered, and neither side SHOULD move. Cheaper advertisements from anything else carry no such hazard and SHOULD be adopted at once.

6.3. Updating an Advertisement

An endpoint updates a PUBLISH_NAMESPACE with REQUEST_UPDATE ([moqt] Section 9.5) on its request stream, carrying the HOP_PATH or ROUTE_COST that changed. An omitted parameter keeps its value, so a relay that starts carrying a namespace sends an explicit ROUTE_COST of 0. The receiver answers REQUEST_OK, or REQUEST_ERROR and closes the stream, which withdraws the advertisement.

NAMESPACE has no REQUEST_UPDATE, so an endpoint updates one by re-sending it with new parameters on the same SUBSCRIBE_NAMESPACE response stream. A receiver MUST NOT treat the repeat as a duplicate or a protocol violation.

An advertisement lives as long as its stream, so an update on a new stream would leave two streams claiming one namespace. An endpoint MUST NOT open a second stream for an advertisement it already maintains on the session.

An update replaces the old parameters atomically, so a receiver MUST NOT tear down subscriptions or drop cached state because one arrived. If the first HOP_PATH entry is unchanged the content is continuous and subscriptions MAY resume on the new route at a group boundary, even when that entry is 0: there is one advertisement, and its stream is the continuity. If the publisher did change, the endpoint MUST withdraw the advertisement (PUBLISH_NAMESPACE_DONE or NAMESPACE_DONE) and advertise again rather than update in place.

The expected update is a ROUTE_COST change, which is how a relay signals that it started or stopped carrying the namespace.

7. Path Selection

A receiver resolving a request consults only the most specific advertisements covering it: the longest prefix.

Within that tier, a receiver SHOULD prefer a HOP_PATH that contains no 0 entry over one that does, then the lowest ROUTE_COST, breaking ties toward the shorter HOP_PATH and then toward the most recently received. This is advisory: a receiver MAY apply local policy, such as measured RTT, instead.

NO_CAPACITY and its single re-resolution are defined by [I-D.lcurley-moq-pattern]. Excluding the refusing advertiser excludes every route with its non-zero first Hop ID, or its session when that ID is 0.

Two advertisements whose HOP_PATH begins with the same non-zero Hop ID come from the same publisher and carry interchangeable content: a receiver MAY hold them as redundant paths and fail an active subscription over to the survivor at a group boundary. If the first entries differ, or either is 0, they are distinct publishers reusing a namespace (Section 8).

An endpoint MUST NOT advertise a path whose HOP_PATH contains the Hop ID the peer declared: the peer could only discard it, and acting on it would form a loop. Of the paths that remain it SHOULD advertise the best, and advertises nothing when every path contains that Hop ID. Because selection is per session, a peer that the serving path runs through still receives the best standby, which is what lets it fail over if its own copy dies.

An endpoint MUST select the source for a subscription by the same rule. If only excluded sources remain the subscription is unroutable, since serving it would hand the subscriber data that already flowed through itself. One rule for advertisement and dispatch keeps advertised paths truthful and prevents subscription cycles of any length.

8. Several Publishers of One Namespace

[moqt] lets several publishers advertise one namespace and leaves to the relay how it serves a SUBSCRIBE among them. Under this extension an advertisement is a path, so a session advertises a namespace at most once, a relay forwards only the best path it knows (Section 7), and a subscription is served from one source at a time.

A receiver MAY still hold paths to several publishers of one namespace and choose between them as it sees fit: serve from the cheapest and move to the next when it fails or refuses the request, or try each in cost order until one accepts. The advertised path and the served source stay the same publisher: a relay that moves to another MUST withdraw its advertisement and advertise the new path (Section 6.3), so the first Hop ID downstream always names the publisher whose Objects flow. Moving between distinct publishers is a discontinuity: their groups are not one sequence, so a subscriber sees an unrelated Location, and a FETCH that succeeds against one may fail against the other.

Redundant publishers of the same content avoid this by sharing a Hop ID (Section 4), which makes their paths interchangeable and lets a subscription fail over at a group boundary. Publishers that do not share one are treated as reusing a name.

9. Security Considerations

A Hop ID reveals nothing beyond what its operator encodes in it; a deployment that considers its identifiers sensitive can use random values or declare 0 (Section 4.1). Declaring 0 hides an identity from the mesh; a peer MAY assign one as local selection state (Section 4.2) and MUST NOT forward it. A HOP_PATH does reveal how many hops an advertisement crossed, which hints at the size of a deployment; a relay MAY collapse its internal hops into one entry, or strip HOP_PATH, before forwarding across a trust boundary.

Because a relay only appends to HOP_PATH, it cannot make a competing path look shorter than it is; the worst it can do is under-report its own upstream portion to win an advisory tie-break. ROUTE_COST has no such protection: it is a single value the sender chooses, so a relay can advertise 0 for content it is not carrying and attract subscriptions it then has to fetch. Both cost only a suboptimal path choice, and the latter is self-limiting, since the traffic won this way must then be served.

A receiver MUST NOT make security decisions based on Hop IDs, and a deployment spanning a trust boundary SHOULD treat a peer's ROUTE_COST as a hint to clamp or ignore rather than an accounting figure.

10. IANA Considerations

This document requests the following registrations. High, distinctive values are requested to avoid the low ranges reserved by [moqt] and to minimize collisions with provisional registrations by other extensions.

10.1. MOQT Setup Options

This document requests two registrations in the "MOQT Setup Options" registry ([moqt] Section 15.4), whose policy is Specification Required.

Table 1
Value Name Reference
0x40B54 HOP_ID This Document
0x40B56 RELAY_COST This Document

10.2. MOQT Message Parameters

This document requests two registrations in the "MOQT Message Parameters" registry ([moqt] Section 15.7). Both are carried in PUBLISH_NAMESPACE, in REQUEST_UPDATE of a PUBLISH_NAMESPACE (Section 6.3), and in the extended NAMESPACE message (Section 5).

Table 2
Value Name Carried In Reference
0x40B57 HOP_PATH PUBLISH_NAMESPACE, REQUEST_UPDATE, NAMESPACE This Document
0x40B58 ROUTE_COST PUBLISH_NAMESPACE, REQUEST_UPDATE, NAMESPACE This Document

The Key-Value-Pair parity is load-bearing: HOP_PATH is odd, so its value is a length-prefixed byte string, while HOP_ID, RELAY_COST, and ROUTE_COST are even, so their values are bare varints.

11. References

11.1. Normative References

[I-D.lcurley-moq-pattern]
Curley, L., "MoQ Pattern Extension", <https://datatracker.ietf.org/doc/draft-lcurley-moq-pattern/>.
[moqt]
Nandakumar, S., Vasiliev, V., Swett, I., and A. Frindell, "Media over QUIC Transport", Work in Progress, Internet-Draft, draft-ietf-moq-transport-21, , <https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-21>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.

11.2. Informative References

[RFC2178]
Moy, J., "OSPF Version 2", RFC 2178, DOI 10.17487/RFC2178, , <https://www.rfc-editor.org/rfc/rfc2178>.
[RFC2328]
Moy, J., "OSPF Version 2", STD 54, RFC 2328, DOI 10.17487/RFC2328, , <https://www.rfc-editor.org/rfc/rfc2328>.
[RFC2453]
Malkin, G., "RIP Version 2", STD 56, RFC 2453, DOI 10.17487/RFC2453, , <https://www.rfc-editor.org/rfc/rfc2453>.
[RFC4271]
Rekhter, Y., Ed., Li, T., Ed., and S. Hares, Ed., "A Border Gateway Protocol 4 (BGP-4)", RFC 4271, DOI 10.17487/RFC4271, , <https://www.rfc-editor.org/rfc/rfc4271>.
[RFC5305]
Li, T. and H. Smit, "IS-IS Extensions for Traffic Engineering", RFC 5305, DOI 10.17487/RFC5305, , <https://www.rfc-editor.org/rfc/rfc5305>.

Appendix A. Appendix A: Changelog

A.1. moq-cluster-01

  • Assigned identities are local selection state and MUST NOT be forwarded.

  • Bridging an upstream that sent no HOP_PATH writes 0 for that hop; a received 0 is forwarded unchanged.

  • Path selection prefers a HOP_PATH with no 0 entry before comparing ROUTE_COST.

  • Renamed the RELAY_HOPS Setup Option to HOP_ID and moved it to the even key 0x40B54, so its value is a bare varint rather than a length-prefixed one.

  • A PUBLISH_NAMESPACE is updated with REQUEST_UPDATE on its request stream instead of a repeated PUBLISH_NAMESPACE; HOP_PATH and ROUTE_COST are registered for REQUEST_UPDATE. A NAMESPACE is still re-sent on its stream.

  • A session advertises a namespace at most once and a subscription is served from one source at a time. A receiver chooses among several publishers of one namespace; moving between them is a discontinuity unless they share a Hop ID.

  • Named the routing protocols whose single per-direction metric RELAY_COST follows.

  • Path selection consults the most specific advertisement first, the longest prefix; an advertisement is always a prefix, and a request beneath it that the advertiser will not serve is refused ([I-D.lcurley-moq-pattern]). Standby seeds are bounded by deployment limits.

Author's Address

Luke Curley