From 53cb2a77fa255ee90fa167b01a156fb5ab7a09b0 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 18:31:08 -0700 Subject: [PATCH 1/7] Add moq-transport extension drafts for relay hops, timestamps, compression moq-lite has grown features with no equivalent in draft-ietf-moq-transport-18. Extract them as standalone moqt extension drafts (modeled on moq-probe) so they can be adopted upstream independently: - relay-hops: Hop ID path list (loop detection + shortest-path tiebreak) and Exclude Hop, via PUBLISH_NAMESPACE / SUBSCRIBE_NAMESPACE parameters. - timestamp: track Timescale + per-object Timestamp/Duration as Key-Value-Pair properties, enabling consistent age-based dropping across relays. - compression: per-object DEFLATE negotiated via Setup Option + track property, with a new algorithm registry and relay transcoding rules. Negotiation uses moqt Setup Options; data rides on the native Key-Value-Pair mechanism. Codepoints are distinctive provisional values to be finalized. Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-moq-compression.md | 169 ++++++++++++++++++++++++++++ draft-lcurley-moq-relay-hops.md | 184 +++++++++++++++++++++++++++++++ draft-lcurley-moq-timestamp.md | 169 ++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 draft-lcurley-moq-compression.md create mode 100644 draft-lcurley-moq-relay-hops.md create mode 100644 draft-lcurley-moq-timestamp.md diff --git a/draft-lcurley-moq-compression.md b/draft-lcurley-moq-compression.md new file mode 100644 index 0000000..788339c --- /dev/null +++ b/draft-lcurley-moq-compression.md @@ -0,0 +1,169 @@ +--- +title: "MoQ Payload Compression Extension" +abbrev: "moq-compression" +category: info + +docname: draft-lcurley-moq-compression-latest +submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF" +number: +date: +v: 3 +area: wit +workgroup: moq + +author: + - + fullname: Luke Curley + email: kixelated@gmail.com + +normative: + moqt: I-D.ietf-moq-transport + RFC1951: + +informative: + +--- abstract + +This document defines a payload compression extension for MoQ Transport {{moqt}}. +A track-level Compression property declares that every object payload on the track is compressed with a negotiated algorithm, applied independently per object so objects remain individually decodable. +Compression is negotiated per hop during SETUP, allowing a relay to transcode (including bridging endpoints that do not support the extension) while preserving the decompressed bytes exactly. + +--- middle + +# Conventions and Definitions +{::boilerplate bcp14-tagged} + + +# Introduction +{{moqt}} makes the original publisher "solely responsible for the content of the object payload ... including the underlying encoding, compression, any end-to-end encryption, or authentication" ({{moqt}} Section 2.1). +For media this is the right layering: already-compressed codecs (H.264, Opus, AV1) gain nothing from a second compression pass. + +But MoQ also carries non-media tracks — JSON, text, telemetry, captions, uncompressed binary structures — where the payloads are highly compressible and where end-to-end encryption is often not in use. +For these tracks there is no standard, transport-visible way to compress payloads, so each application reinvents it, and relays cannot help. + +This extension defines a per-track, per-object compression scheme negotiated at the transport layer: + +- **Per object, independently**: each object payload is an independent compressed stream with no shared dictionary or state between objects. This keeps every object individually decodable and avoids head-of-line decoding within a group. +- **Per hop, negotiated**: compression is negotiated during SETUP for each session, so a relay can decompress, recompress with a different algorithm, or bridge to a peer that does not support the extension — as long as the decompressed bytes are identical end to end. + + +# Setup Negotiation +The Payload Compression extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 10.3. +Unlike a purely additive property, compression MUST be negotiated: a receiver that does not understand the algorithm would otherwise pass the compressed bytes to the application as if they were plaintext. + +An endpoint advertises the algorithms it can decompress (when acting as a subscriber) and produce (when acting as a publisher) by including the following Setup Option: + +~~~ +COMPRESSION Setup Option { + Option Key (vi64) = 0xC03DE + Option Value Length (vi64) + Algorithm (vi64) ... +} +~~~ + +**Algorithm**: +One or more Algorithm identifiers (see [Compression Algorithms](#compression-algorithms)) that the sender supports, each a varint, filling the Option Value. +The identifier `none` (0) MUST NOT be listed (it requires no negotiation). + +A publisher MUST NOT compress a track with an algorithm the peer did not advertise in its SETUP. +If a receiver encounters a COMPRESSION track property naming an algorithm it did not advertise — for example because an object arrived before SETUP, or a peer misbehaved — it MUST reset the affected subscription/fetch with a PROTOCOL_VIOLATION rather than deliver the payload uninterpreted. + + +# COMPRESSION Track Property +The COMPRESSION property declares the algorithm applied to every object payload on a track. +It is a track-level Key-Value-Pair carried with the track's properties (see {{moqt}} Section 2.5 and Section 12). +Because the value is a single integer, COMPRESSION uses an even Type so the value is a bare varint: + +~~~ +COMPRESSION Track Property { + Type (vi64) = 0xC03D0 + Value (vi64) ; Algorithm identifier +} +~~~ + +**Value**: +The Algorithm identifier applied to every object payload on the track. +The absence of the property, or a value of `none` (0), means payloads are transmitted verbatim. + +The compression algorithm is fixed for the lifetime of the track and MUST NOT change. +Compression applies to the object payload only; object properties and message framing are never compressed. +An empty payload (size 0) MUST NOT be compressed and remains empty on the wire. + +A publisher SHOULD enable compression only for payload types that benefit from it. +Already-compressed media SHOULD use `none`. + + +# Compression Algorithms {#compression-algorithms} +This document defines the following algorithms. +Further algorithms MAY be registered (see [IANA Considerations](#iana-considerations)). + +| ID | Name | Description | +|---:|:--------|:--------------------------------------------------------| +| 0 | none | Payloads are transmitted verbatim. The default. | +| 1 | deflate | Raw DEFLATE {{RFC1951}}, with no zlib or gzip framing. | + +For `deflate`, each object payload is an independent raw DEFLATE stream. +There is no shared dictionary or state between objects, so each object decompresses on its own. + + +# Relay Behavior +A relay MAY forward compressed payloads unchanged, decompress them, or recompress them with a different algorithm, provided the decompressed bytes delivered to the original-format consumer are identical to what the publisher produced. + +A relay MAY transcode between algorithms — including bridging a peer that supports this extension to one that does not (by decompressing to `none`), or bridging two peers that support different algorithm sets. +When it does, it rewrites the COMPRESSION track property accordingly on the downstream session. + +A relay SHOULD NOT compress an originally-uncompressed payload unless there is a strong content signal that compression is beneficial (for example, a track name ending in `.json`), because it cannot otherwise predict whether compression will help or hurt. + +A relay or generic library MUST NOT inspect or modify the decompressed contents unless otherwise negotiated; only recompression that preserves the decompressed bytes exactly is permitted. + + +# Security Considerations +Compressing data that mixes attacker-controlled and secret content in the same object can leak the secret through compressed size, as in the CRIME and BREACH attacks. +A publisher MUST NOT enable compression on a track whose object payloads combine secret material with attacker-influenced material. +Because compression here is per-object with no cross-object dictionary, the exposure is bounded to within a single object, but it is not eliminated. + +A malicious publisher could send a small compressed payload that decompresses to a very large buffer (a "decompression bomb"). +A receiver MUST bound the size of a decompressed object payload and MUST reset the stream with a PROTOCOL_VIOLATION (or an application error) if the bound is exceeded, rather than allocating unbounded memory. + +Compression is orthogonal to {{moqt}} end-to-end encryption: an encrypted payload is effectively incompressible, so a publisher using end-to-end encryption SHOULD use `none`. + + +# 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). +The property Type is even so that its value is a bare varint with no length prefix (see {{moqt}} Section 2.5). + +## MoQ Setup Options + +This document requests a registration in the "MoQ Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. + +| Value | Name | Reference | +|:--------|:------------|:--------------| +| 0xC03DE | COMPRESSION | This Document | + +## MoQ Key-Value-Pair Types + +This document requests a registration in the "MoQ Key-Value-Pair Types" registry ({{moqt}} Section 15), used for message parameters and object/track properties. + +| Value | Name | Scope | Reference | +|:--------|:------------|:------|:--------------| +| 0xC03D0 | COMPRESSION | Track | This Document | + +## MoQ Compression Algorithms + +This document requests a new "MoQ Compression Algorithms" registry, with a registration policy of Specification Required. +The initial contents are: + +| ID | Name | Reference | +|---:|:--------|:--------------| +| 0 | none | This Document | +| 1 | deflate | This Document | + + +--- back + +# Acknowledgments +{:numbered="false"} + +This document was drafted with the assistance of Claude, an AI assistant by Anthropic. diff --git a/draft-lcurley-moq-relay-hops.md b/draft-lcurley-moq-relay-hops.md new file mode 100644 index 0000000..e4740e2 --- /dev/null +++ b/draft-lcurley-moq-relay-hops.md @@ -0,0 +1,184 @@ +--- +title: "MoQ Relay Hops Extension" +abbrev: "moq-relay-hops" +category: info + +docname: draft-lcurley-moq-relay-hops-latest +submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF" +number: +date: +v: 3 +area: wit +workgroup: moq + +author: + - + fullname: Luke Curley + email: kixelated@gmail.com + +normative: + moqt: I-D.ietf-moq-transport + +informative: + +--- abstract + +This document defines a Relay Hops extension for MoQ Transport {{moqt}}. +Each namespace advertisement carries an ordered list of Hop IDs identifying the relays it has traversed, and a namespace subscription MAY carry a Hop ID to exclude. +Together these allow a relay cluster to detect and avoid routing loops, and allow a downstream subscriber to break ties between multiple paths to the same namespace by preferring the shortest one. + +--- middle + +# Conventions and Definitions +{::boilerplate bcp14-tagged} + + +# Introduction +{{moqt}} is designed to deliver content end-to-end through a mesh of relays. +A namespace advertisement (PUBLISH_NAMESPACE, {{moqt}} Section 10.15) originates at a publisher and propagates downstream through one or more relays toward interested subscribers, which express interest with SUBSCRIBE_NAMESPACE ({{moqt}} Section 10.18). + +In a redundant deployment, relays are interconnected so that the same namespace can reach a given relay over more than one path. +This redundancy is desirable for failover, but it introduces two problems that {{moqt}} does not address: + +- **Routing loops**: relay A advertises a namespace to relay B, which advertises it back to A (directly or through a cycle). Without a way to recognize an advertisement it has already seen, a relay will re-advertise it indefinitely. +- **Path selection**: when the same namespace arrives over multiple paths, a relay or subscriber has no information with which to prefer one path over another (e.g. the shorter, and usually lower-latency, one). + +This extension solves both with a single mechanism: an ordered list of **Hop IDs** that records the relays an advertisement has traversed. + +## Why per-hop, not end-to-end +The Hop ID list is rewritten at every relay: a relay appends its own Hop ID before forwarding an advertisement downstream. +A relay therefore detects a loop by finding its own Hop ID already present in an incoming advertisement, and a subscriber compares path lengths using the list length. +Because the list is meaningful only within a single relay deployment, Hop IDs are scoped to that deployment and are not interpreted by endpoints outside it. + + +# Setup Negotiation +The Relay Hops extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 10.3. +An endpoint indicates support by including the following Setup Option: + +~~~ +RELAY_HOPS Setup Option { + Option Key (vi64) = 0x40B55 + Option Value Length (vi64) = 0 +} +~~~ + +The extension applies to a single hop (one MOQT session) and is negotiated independently for each session; a relay MUST NOT assume that because one of its sessions negotiated Relay Hops, another did. + +A relay that negotiated this extension on a downstream session SHOULD include the HOP_PATH parameter on every PUBLISH_NAMESPACE it sends on that session, and SHOULD honor an EXCLUDE_HOP parameter it receives in SUBSCRIBE_NAMESPACE. +An endpoint that did not negotiate the extension simply omits these parameters; per {{moqt}} an unknown Key-Value-Pair Type is ignored, so an advertisement forwarded into a non-supporting session loses its hop information gracefully. + + +# Hop IDs +A **Hop ID** is a variable-length integer that uniquely identifies a relay within a relay deployment (cluster). + +- How Hop IDs are assigned is deployment-specific and out of scope for this document. A deployment MUST ensure each participating relay has a Hop ID that is unique within that deployment for as long as it is in use. +- The value `0` is reserved and means "unknown hop". It is used when an advertisement was bridged from a peer that does not support this extension (or an older protocol version), so that the hop count still reflects the true path length even when an intermediate identity is unavailable. A relay MUST NOT assign `0` to itself. + +A relay MUST NOT reveal Hop IDs from one deployment to an unrelated deployment; see [Security Considerations](#security-considerations). + + +# HOP_PATH Parameter +The HOP_PATH parameter carries the ordered list of Hop IDs that an advertisement has traversed, from the origin toward the receiver. +It is a Key-Value-Pair (see {{moqt}} Section 2.5) carried in the Parameters of a PUBLISH_NAMESPACE message ({{moqt}} Section 10.15). + +Because the value is a variable-length byte string, HOP_PATH uses an odd Type so that it is length-prefixed: + +~~~ +HOP_PATH Parameter { + Type (vi64) = 0x40B57 + Length (vi64) + Hop ID (vi64) ... +} +~~~ + +**Hop ID**: +Zero or more Hop IDs, ordered from the origin-most relay to the relay immediately upstream of the receiver. +The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`. +An empty value (Length 0) means the advertisement has not yet traversed any relay that assigns Hop IDs — typically because the sender is the origin publisher. + +## Relay Behavior +When a relay forwards a namespace advertisement downstream on a session that negotiated this extension, it MUST append its own Hop ID to the HOP_PATH it received (creating an empty HOP_PATH first if the upstream advertisement carried none). +The relay's own Hop ID is therefore always the last entry of the list it sends. + +When a relay receives a namespace advertisement on a session that negotiated this extension, it MUST inspect the HOP_PATH: + +- If its own Hop ID already appears in the list, the advertisement has looped. The relay MUST NOT forward it and SHOULD drop it. +- Otherwise the relay MAY forward it downstream, appending its own Hop ID as described above. + +When bridging from a peer that did not negotiate this extension, a relay MAY synthesize a single leading `0` ("unknown hop") entry to preserve an approximate path length, or MAY omit it; this choice is deployment-specific. + +## Path Selection +A relay or subscriber that receives advertisements for the same namespace over multiple sessions MAY use the length of the HOP_PATH list as a tiebreaker, preferring the advertisement with the fewest hops (usually the lowest-latency path). +This is advisory: the receiver MAY apply additional local policy (e.g. measured RTT or administrative preference) and is not required to prefer the shortest path. + +A publisher (or relay acting as one) SHOULD advertise only the single best path it currently knows for each namespace. +If the best path changes — for example after a relay failover — the publisher MAY re-advertise the namespace; the new advertisement, carrying an updated HOP_PATH, replaces the prior one per the namespace-advertisement semantics of {{moqt}}. + + +# EXCLUDE_HOP Parameter +The EXCLUDE_HOP parameter lets a downstream subscriber tell an upstream relay to suppress advertisements that have already passed through a given relay. +A relay in a cluster uses it to prevent the upstream from sending back an advertisement that the downstream originated, the most common source of a two-hop loop. + +It is a Key-Value-Pair carried in the Parameters of a SUBSCRIBE_NAMESPACE message ({{moqt}} Section 10.18). +Because the value is a list, EXCLUDE_HOP uses an odd Type so that it is length-prefixed: + +~~~ +EXCLUDE_HOP Parameter { + Type (vi64) = 0x40B59 + Length (vi64) + Hop ID (vi64) ... +} +~~~ + +**Hop ID**: +One or more Hop IDs to exclude, in any order. +The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`, or if `Length` is 0. + +A relay that receives a SUBSCRIBE_NAMESPACE carrying EXCLUDE_HOP SHOULD NOT send, on that session, any PUBLISH_NAMESPACE whose HOP_PATH contains any of the listed Hop IDs (including the implicit final entry the relay would itself append). +The exclusion is scoped to the namespace subscription it accompanies. + +A relay that receives EXCLUDE_HOP without having negotiated the Relay Hops extension ignores it as an unknown parameter, which is the safe default (it simply does not perform the exclusion). + + +# Security Considerations +Hop IDs describe the internal topology of a relay deployment. +A relay MUST treat Hop IDs as private to the deployment: it MUST NOT forward HOP_PATH or EXCLUDE_HOP across a trust boundary (for example, to a subscriber outside the operator's own relay cluster), and SHOULD strip the HOP_PATH parameter before forwarding an advertisement to such a peer. +Leaking Hop IDs could reveal cluster size, topology, or failover state to an untrusted party. + +A malicious upstream could forge a HOP_PATH to influence a downstream's path selection (e.g. claiming a short path it cannot actually deliver). +Path selection using HOP_PATH is therefore advisory only; a receiver SHOULD corroborate it with locally measured signals (e.g. RTT) before relying on it, and MUST NOT make security decisions based on Hop IDs. + +A malicious subscriber could supply a large EXCLUDE_HOP list to consume relay resources. +Implementations SHOULD bound the number of excluded Hop IDs they will accept and MAY reject a SUBSCRIBE_NAMESPACE whose EXCLUDE_HOP list exceeds that bound. + + +# 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). +The two parameter Types are odd so that each is length-prefixed (see {{moqt}} Section 2.5). + +## MoQ Setup Options + +This document requests a registration in the "MoQ Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. + +| Value | Name | Reference | +|:--------|:-----------|:--------------| +| 0x40B55 | RELAY_HOPS | This Document | + +## MoQ Key-Value-Pair Types + +This document requests registrations in the "MoQ Key-Value-Pair Types" registry ({{moqt}} Section 15), used for message parameters and object/track properties. + +| Value | Name | Carried In | Reference | +|:--------|:------------|:-------------------|:--------------| +| 0x40B57 | HOP_PATH | PUBLISH_NAMESPACE | This Document | +| 0x40B59 | EXCLUDE_HOP | SUBSCRIBE_NAMESPACE| This Document | + + +--- back + +# Acknowledgments +{:numbered="false"} + +This document was drafted with the assistance of Claude, an AI assistant by Anthropic. diff --git a/draft-lcurley-moq-timestamp.md b/draft-lcurley-moq-timestamp.md new file mode 100644 index 0000000..769a309 --- /dev/null +++ b/draft-lcurley-moq-timestamp.md @@ -0,0 +1,169 @@ +--- +title: "MoQ Object Timestamp Extension" +abbrev: "moq-timestamp" +category: info + +docname: draft-lcurley-moq-timestamp-latest +submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF" +number: +date: +v: 3 +area: wit +workgroup: moq + +author: + - + fullname: Luke Curley + email: kixelated@gmail.com + +normative: + moqt: I-D.ietf-moq-transport + +informative: + +--- abstract + +This document defines an extension for MoQ Transport {{moqt}} that attaches a media presentation timestamp and duration to each object. +A track-level Timescale property establishes the units, an object-level Timestamp property carries the presentation time of each object, and an optional Duration property carries its presentation duration. +Exposing media time to the transport lets relays make consistent age-based decisions (e.g. dropping stale objects) without parsing the media container, and it remains consistent across hops regardless of buffering or jitter. + +--- middle + +# Conventions and Definitions +{::boilerplate bcp14-tagged} + + +# Introduction +{{moqt}} treats object payloads as opaque: "the amount of time elapsed between publishing an Object in Group ID N and in a Group ID > N ... is not defined by this specification" ({{moqt}} Section 2.3.1), and timing is left to the application's container format. + +This works for endpoints that parse the media, but not for relays. +A relay frequently needs a notion of *when* an object is meant to be presented: + +- **Age-based dropping**: a relay serving a live, latency-sensitive subscription wants to drop objects that are too old to be useful, keeping the freshest content flowing under congestion. Without a timestamp it can only approximate age from wall-clock arrival time, which drifts across hops and is corrupted by buffering and jitter. +- **Consistent expiration across hops**: every relay on a path should make the same drop decision for the same object. A timestamp embedded in the object is identical at every hop; a wall-clock arrival time is not. +- **Synchronization hints**: a subscriber can align objects from multiple tracks (e.g. audio and video) using a shared media timeline without first decoding each container. + +This extension exposes media time to the transport with three Key-Value-Pairs ({{moqt}} Section 2.5): a track-level **Timescale**, an object-level **Timestamp**, and an optional object-level **Duration**. +The transport does not interpret the *meaning* of the timeline (it is still the application's clock); it only uses the timestamp for relative age comparisons. + + +# Setup Negotiation +The Object Timestamp extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 10.3. +An endpoint indicates support by including the following Setup Option: + +~~~ +TIMESTAMP Setup Option { + Option Key (vi64) = 0x915C1 + Option Value Length (vi64) = 0 +} +~~~ + +The properties defined below are ordinary Key-Value-Pairs and a receiver that does not understand them ignores them per {{moqt}}. +Negotiation is therefore not required for correctness, but a publisher SHOULD send the Setup Option so that a relay knows it can rely on object timestamps for age-based decisions rather than falling back to wall-clock arrival time. +A relay MAY perform timestamp-based dropping for a track only if the upstream publisher advertised this option (or the track carries a non-zero Timescale). + + +# TIMESCALE Track Property +The TIMESCALE property establishes the units for every Timestamp and Duration on a track. +It is a track-level Key-Value-Pair, carried with the track's properties (see {{moqt}} Section 2.5 and Section 12). +Because the value is a single integer, TIMESCALE uses an even Type so the value is a bare varint with no length prefix: + +~~~ +TIMESCALE Track Property { + Type (vi64) = 0x915C0 + Value (vi64) ; units per second +} +~~~ + +**Value**: +The number of timestamp units per second. +Common values include `1000` (milliseconds), `1000000` (microseconds), `48000` (a typical audio sample rate), and `90000` (the RTP video clock). +A value of `0`, or the absence of the property, means the track has no media timeline: Timestamp and Duration properties, if present, MUST be ignored, and a relay MUST fall back to wall-clock arrival time for any age-based decision. + +The Timescale is fixed for the lifetime of the track and MUST NOT change. + + +# TIMESTAMP Object Property +The TIMESTAMP property carries the presentation time of an object, in the track's Timescale. +It is an object-level Key-Value-Pair carried in the object's properties ({{moqt}} Section 2.5, 11.2.1.2). +It uses an even Type so the value is a bare varint: + +~~~ +TIMESTAMP Object Property { + Type (vi64) = 0x915C2 + Value (vi64) ; absolute presentation time, in Timescale units +} +~~~ + +**Value**: +The absolute presentation timestamp of the object, expressed in the track's Timescale. +Any value (including 0) is valid. + +A publisher SHOULD attach TIMESTAMP to every object on a track whose Timescale is non-zero. +An object with no TIMESTAMP on such a track has no media time; for age comparisons a receiver MUST treat its effective time as the wall-clock arrival time of the object, which avoids stalling expiration on objects that intentionally carry no timestamp (e.g. keep-alives or gap markers). + +## Age-Based Dropping +Given two objects on the same track, both with TIMESTAMP and a non-zero Timescale, a relay computes their relative age as the difference of their timestamps divided by the Timescale. +A relay serving a live subscription MAY drop an object whose age relative to the most recent object on the track exceeds a locally configured or application-supplied threshold, resetting the corresponding stream per {{moqt}}. +This decision is identical at every hop because it depends only on values embedded in the objects, not on arrival time. + +A relay MUST NOT use timestamps to reorder delivery beyond what {{moqt}} already permits; this property informs *dropping*, not transmission order. + + +# DURATION Object Property +The DURATION property carries the presentation duration of an object, in the track's Timescale. +It is optional and is an object-level Key-Value-Pair with an even Type: + +~~~ +DURATION Object Property { + Type (vi64) = 0x915C4 + Value (vi64) ; presentation duration, in Timescale units +} +~~~ + +**Value**: +The presentation duration of the object, expressed in the track's Timescale. +A value of `0`, or the absence of the property, means the duration is unknown; the object is presented until the next object begins. + +Duration is an application-level hint and is not used by the transport for delivery or dropping decisions. + + +# Security Considerations +Timestamps expose the media timeline to relays, which is the point of the extension, but a relay still treats payloads as opaque and gains no access to media content. + +A malicious publisher could supply misleading timestamps (e.g. always claiming an object is fresh) to defeat age-based dropping, or wildly out-of-range timestamps to cause a receiver to mis-estimate age. +A receiver SHOULD bound the age it computes and SHOULD NOT make security decisions based on timestamps. +Because age-based dropping only affects which objects a live subscription receives, the worst case is degraded delivery for that subscription, not a cross-subscription effect. + + +# 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). +The three property Types are even so that each value is a bare varint with no length prefix (see {{moqt}} Section 2.5). + +## MoQ Setup Options + +This document requests a registration in the "MoQ Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. + +| Value | Name | Reference | +|:--------|:----------|:--------------| +| 0x915C1 | TIMESTAMP | This Document | + +## MoQ Key-Value-Pair Types + +This document requests registrations in the "MoQ Key-Value-Pair Types" registry ({{moqt}} Section 15), used for message parameters and object/track properties. + +| Value | Name | Scope | Reference | +|:--------|:----------|:-------|:--------------| +| 0x915C0 | TIMESCALE | Track | This Document | +| 0x915C2 | TIMESTAMP | Object | This Document | +| 0x915C4 | DURATION | Object | This Document | + + +--- back + +# Acknowledgments +{:numbered="false"} + +This document was drafted with the assistance of Claude, an AI assistant by Anthropic. From 0cb20b895bade3289fd3cdd2c9a8a7326ec6020a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 18:41:00 -0700 Subject: [PATCH 2/7] CLAUDE.md: document that `make` self-bootstraps its toolchain `make` clones the i-d-template submodule and installs xml2rfc (pip venv) and kramdown-rfc (bundler) on first run, so the drafts build without manually installing the toolchain. Note that there is no separate format step. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 11b5d95..9cdc566 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,10 +12,24 @@ This is an IETF Internet-Draft repository for Media over QUIC (MOQ) protocol spe ## Build Commands +Run `make` to build/validate drafts. **`make` self-bootstraps its toolchain** — +on first run it clones the i-d-template submodule into `lib/`, creates a Python +venv (`lib/.venv`, for xml2rfc), and bundler-installs kramdown-rfc into +`lib/.gems`. No manual install of kramdown-rfc/xml2rfc is needed; you only need +system `python3` (with venv+pip), `ruby` (with `bundle`), a C compiler, and +network access on the first build. Do not claim the drafts can't be built — run +`make`. (A successful kramdown-rfc step also validates a draft's syntax.) + +There is no separate format step: prettier is disabled (`.prettierignore` is +`**`), so editing the markdown is the whole workflow. + ```bash # Build all drafts (generates HTML and text versions) make +# Build/validate a single draft (fast feedback while editing) +make draft-lcurley-moq-lite.txt + # Clean build artifacts make clean From 3edea9bf6c4ab268aa7ecec1c02516826a884cad Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 18:44:12 -0700 Subject: [PATCH 3/7] Address review: hops bridging, empty EXCLUDE_HOP, IANA registry names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - relay-hops: bridging from a non-supporting peer now SHOULD synthesize a leading 0 hop (was MAY/omit) so path lengths stay comparable. - relay-hops: an empty EXCLUDE_HOP (Length 0) is now a permitted no-op instead of a PROTOCOL_VIOLATION, matching the empty-HOP_PATH behavior. - IANA: register in the correct moqt-18 registries — message parameters under "MOQT Message Parameters" (Section 15.7) for HOP_PATH/EXCLUDE_HOP, and object/ track properties under "MOQT Properties" (Section 15.8) for the timestamp and compression drafts; align Setup Options naming to "MOQT Setup Options". Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-moq-compression.md | 12 ++++++------ draft-lcurley-moq-relay-hops.md | 23 +++++++++++++---------- draft-lcurley-moq-timestamp.md | 8 ++++---- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/draft-lcurley-moq-compression.md b/draft-lcurley-moq-compression.md index 788339c..d988def 100644 --- a/draft-lcurley-moq-compression.md +++ b/draft-lcurley-moq-compression.md @@ -134,25 +134,25 @@ 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). The property Type is even so that its value is a bare varint with no length prefix (see {{moqt}} Section 2.5). -## MoQ Setup Options +## MOQT Setup Options -This document requests a registration in the "MoQ Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. +This document requests a registration in the "MOQT Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. | Value | Name | Reference | |:--------|:------------|:--------------| | 0xC03DE | COMPRESSION | This Document | -## MoQ Key-Value-Pair Types +## MOQT Properties -This document requests a registration in the "MoQ Key-Value-Pair Types" registry ({{moqt}} Section 15), used for message parameters and object/track properties. +This document requests a registration in the "MOQT Properties" registry ({{moqt}} Section 15.8), used for object and track properties. | Value | Name | Scope | Reference | |:--------|:------------|:------|:--------------| | 0xC03D0 | COMPRESSION | Track | This Document | -## MoQ Compression Algorithms +## MOQT Compression Algorithms -This document requests a new "MoQ Compression Algorithms" registry, with a registration policy of Specification Required. +This document requests a new "MOQT Compression Algorithms" registry, with a registration policy of Specification Required. The initial contents are: | ID | Name | Reference | diff --git a/draft-lcurley-moq-relay-hops.md b/draft-lcurley-moq-relay-hops.md index e4740e2..ddef565 100644 --- a/draft-lcurley-moq-relay-hops.md +++ b/draft-lcurley-moq-relay-hops.md @@ -105,7 +105,8 @@ When a relay receives a namespace advertisement on a session that negotiated thi - If its own Hop ID already appears in the list, the advertisement has looped. The relay MUST NOT forward it and SHOULD drop it. - Otherwise the relay MAY forward it downstream, appending its own Hop ID as described above. -When bridging from a peer that did not negotiate this extension, a relay MAY synthesize a single leading `0` ("unknown hop") entry to preserve an approximate path length, or MAY omit it; this choice is deployment-specific. +When bridging from a peer that did not negotiate this extension, a relay SHOULD synthesize a single leading `0` ("unknown hop") entry rather than omitting it. +Counting the bridged hop keeps path lengths comparable, so that the advisory path selection in the following section behaves consistently across relays that bridge the same non-supporting upstream. ## Path Selection A relay or subscriber that receives advertisements for the same namespace over multiple sessions MAY use the length of the HOP_PATH list as a tiebreaker, preferring the advertisement with the fewest hops (usually the lowest-latency path). @@ -132,7 +133,8 @@ EXCLUDE_HOP Parameter { **Hop ID**: One or more Hop IDs to exclude, in any order. -The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`, or if `Length` is 0. +The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`. +An empty list (`Length` 0) is permitted and means "exclude nothing"; like an empty HOP_PATH it is a harmless no-op and a receiver MUST NOT treat it as an error. A relay that receives a SUBSCRIBE_NAMESPACE carrying EXCLUDE_HOP SHOULD NOT send, on that session, any PUBLISH_NAMESPACE whose HOP_PATH contains any of the listed Hop IDs (including the implicit final entry the relay would itself append). The exclusion is scoped to the namespace subscription it accompanies. @@ -158,22 +160,23 @@ 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). The two parameter Types are odd so that each is length-prefixed (see {{moqt}} Section 2.5). -## MoQ Setup Options +## MOQT Setup Options -This document requests a registration in the "MoQ Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. +This document requests a registration in the "MOQT Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. | Value | Name | Reference | |:--------|:-----------|:--------------| | 0x40B55 | RELAY_HOPS | This Document | -## MoQ Key-Value-Pair Types +## MOQT Message Parameters -This document requests registrations in the "MoQ Key-Value-Pair Types" registry ({{moqt}} Section 15), used for message parameters and object/track properties. +This document requests registrations in the "MOQT Message Parameters" registry ({{moqt}} Section 15.7). +HOP_PATH and EXCLUDE_HOP are message parameters carried in PUBLISH_NAMESPACE and SUBSCRIBE_NAMESPACE respectively. -| Value | Name | Carried In | Reference | -|:--------|:------------|:-------------------|:--------------| -| 0x40B57 | HOP_PATH | PUBLISH_NAMESPACE | This Document | -| 0x40B59 | EXCLUDE_HOP | SUBSCRIBE_NAMESPACE| This Document | +| Value | Name | Carried In | Reference | +|:--------|:------------|:--------------------|:--------------| +| 0x40B57 | HOP_PATH | PUBLISH_NAMESPACE | This Document | +| 0x40B59 | EXCLUDE_HOP | SUBSCRIBE_NAMESPACE | This Document | --- back diff --git a/draft-lcurley-moq-timestamp.md b/draft-lcurley-moq-timestamp.md index 769a309..2e39db0 100644 --- a/draft-lcurley-moq-timestamp.md +++ b/draft-lcurley-moq-timestamp.md @@ -142,17 +142,17 @@ 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). The three property Types are even so that each value is a bare varint with no length prefix (see {{moqt}} Section 2.5). -## MoQ Setup Options +## MOQT Setup Options -This document requests a registration in the "MoQ Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. +This document requests a registration in the "MOQT Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. | Value | Name | Reference | |:--------|:----------|:--------------| | 0x915C1 | TIMESTAMP | This Document | -## MoQ Key-Value-Pair Types +## MOQT Properties -This document requests registrations in the "MoQ Key-Value-Pair Types" registry ({{moqt}} Section 15), used for message parameters and object/track properties. +This document requests registrations in the "MOQT Properties" registry ({{moqt}} Section 15.8), used for object and track properties. | Value | Name | Scope | Reference | |:--------|:----------|:-------|:--------------| From e8eb259d084162b080f4f9ff83a9160a2a2d3b1c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 19:36:00 -0700 Subject: [PATCH 4/7] Address review feedback on hops, timestamp, compression relay-hops: - Hop IDs are now random integers; drop the reserved 0 "unknown hop" (a moq-lite-04 legacy). Bridging a non-supporting upstream synthesizes a random stand-in ID so loop detection still works. - HOP_PATH always has >=1 entry; the first is the origin publisher's own ID, enabling broadcast-identity (same first ID = same broadcast). - Relay MUST include HOP_PATH and MUST honor EXCLUDE_HOP (was SHOULD). - EXCLUDE_HOP is now a single Hop ID (even Type, bare varint), matching moq-lite; removes the empty-list and large-list edge cases. - Reorder intro (path selection first), add broadcast-identity use. - Security: topology hiding is now advisory (MAY coalesce/strip, like BGP confederations) not MUST; clarify a relay can only append hops, not shorten a path, so forgery impact is limited to advisory tie-breaks. Drop EXCLUDE_HOP DoS note (single value now). timestamp: - Note the demuxed nature of MoQ makes timestamps near-universal per track. - Note Timescale is required to interpret units, so timing can't be resolved until track properties arrive (SUBSCRIBE_OK / TRACK_STATUS). - Duration MAY now refine age-based dropping (object end = timestamp+duration). compression: - Reframe as hop-by-hop wire optimization (HTTP Transfer-Encoding analogy), not an end-to-end publisher track property; the origin need not opt in. - COMPRESSION moves from a track property to a per-subscription property in SUBSCRIBE_OK / FETCH_OK; register under MOQT Message Parameters (15.7). Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-moq-compression.md | 59 ++++++++++++----------- draft-lcurley-moq-relay-hops.md | 80 ++++++++++++++++---------------- draft-lcurley-moq-timestamp.md | 9 +++- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/draft-lcurley-moq-compression.md b/draft-lcurley-moq-compression.md index d988def..c5aa9b3 100644 --- a/draft-lcurley-moq-compression.md +++ b/draft-lcurley-moq-compression.md @@ -25,8 +25,8 @@ informative: --- abstract This document defines a payload compression extension for MoQ Transport {{moqt}}. -A track-level Compression property declares that every object payload on the track is compressed with a negotiated algorithm, applied independently per object so objects remain individually decodable. -Compression is negotiated per hop during SETUP, allowing a relay to transcode (including bridging endpoints that do not support the extension) while preserving the decompressed bytes exactly. +Compression is a hop-by-hop wire optimization, analogous to HTTP Transfer-Encoding: the endpoint serving a subscription MAY compress object payloads with an algorithm the receiver advertised, signaling the algorithm per subscription. Each object is compressed independently so objects remain individually decodable. +Because it is hop-by-hop, the origin publisher need not participate: any relay can compress toward a downstream that supports it, or decompress toward one that does not, and the decompressed bytes — the actual object — are unchanged end to end. --- middle @@ -41,17 +41,18 @@ For media this is the right layering: already-compressed codecs (H.264, Opus, AV But MoQ also carries non-media tracks — JSON, text, telemetry, captions, uncompressed binary structures — where the payloads are highly compressible and where end-to-end encryption is often not in use. For these tracks there is no standard, transport-visible way to compress payloads, so each application reinvents it, and relays cannot help. -This extension defines a per-track, per-object compression scheme negotiated at the transport layer: +This extension defines a hop-by-hop, per-object compression scheme negotiated at the transport layer. +It is a wire optimization, analogous to HTTP Transfer-Encoding (a hop-by-hop encoding) rather than HTTP Content-Encoding (an end-to-end one): it does not conceptually change the object payload — the decompressed bytes *are* the object — it only changes how those bytes are carried over a single hop. +- **Hop-by-hop, not end-to-end**: compression is negotiated during SETUP for each session and applied independently on each hop by whichever endpoint is serving the subscription. The origin publisher need not opt in; a relay can compress toward a downstream that supports it and decompress toward one that does not. - **Per object, independently**: each object payload is an independent compressed stream with no shared dictionary or state between objects. This keeps every object individually decodable and avoids head-of-line decoding within a group. -- **Per hop, negotiated**: compression is negotiated during SETUP for each session, so a relay can decompress, recompress with a different algorithm, or bridge to a peer that does not support the extension — as long as the decompressed bytes are identical end to end. # Setup Negotiation The Payload Compression extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 10.3. Unlike a purely additive property, compression MUST be negotiated: a receiver that does not understand the algorithm would otherwise pass the compressed bytes to the application as if they were plaintext. -An endpoint advertises the algorithms it can decompress (when acting as a subscriber) and produce (when acting as a publisher) by including the following Setup Option: +Each endpoint advertises the algorithms it can decompress by including the following Setup Option: ~~~ COMPRESSION Setup Option { @@ -62,34 +63,35 @@ COMPRESSION Setup Option { ~~~ **Algorithm**: -One or more Algorithm identifiers (see [Compression Algorithms](#compression-algorithms)) that the sender supports, each a varint, filling the Option Value. +One or more Algorithm identifiers (see [Compression Algorithms](#compression-algorithms)) that the sender can decompress, each a varint, filling the Option Value. The identifier `none` (0) MUST NOT be listed (it requires no negotiation). -A publisher MUST NOT compress a track with an algorithm the peer did not advertise in its SETUP. -If a receiver encounters a COMPRESSION track property naming an algorithm it did not advertise — for example because an object arrived before SETUP, or a peer misbehaved — it MUST reset the affected subscription/fetch with a PROTOCOL_VIOLATION rather than deliver the payload uninterpreted. +A sender MUST NOT compress with an algorithm the receiver did not advertise in its SETUP. +If a receiver is told — via the COMPRESSION subscription property below — that objects use an algorithm it did not advertise (for example because data arrived before SETUP, or a peer misbehaved), it MUST reset the affected subscription/fetch with a PROTOCOL_VIOLATION rather than deliver the payload uninterpreted. -# COMPRESSION Track Property -The COMPRESSION property declares the algorithm applied to every object payload on a track. -It is a track-level Key-Value-Pair carried with the track's properties (see {{moqt}} Section 2.5 and Section 12). +# COMPRESSION Subscription Property +Compression is signaled per subscription by the endpoint serving it. +The COMPRESSION property declares the algorithm applied to the object payloads delivered for a given subscription (or fetch). +It is a Key-Value-Pair (see {{moqt}} Section 2.5) carried as a parameter of SUBSCRIBE_OK (and the corresponding FETCH_OK), sent by the publisher or relay that serves the request to the receiver. Because the value is a single integer, COMPRESSION uses an even Type so the value is a bare varint: ~~~ -COMPRESSION Track Property { +COMPRESSION Subscription Property { Type (vi64) = 0xC03D0 Value (vi64) ; Algorithm identifier } ~~~ **Value**: -The Algorithm identifier applied to every object payload on the track. +The Algorithm identifier applied to every object payload delivered for this subscription. The absence of the property, or a value of `none` (0), means payloads are transmitted verbatim. -The compression algorithm is fixed for the lifetime of the track and MUST NOT change. +The algorithm is fixed for the lifetime of the subscription; to change it, the receiver re-subscribes. Compression applies to the object payload only; object properties and message framing are never compressed. An empty payload (size 0) MUST NOT be compressed and remains empty on the wire. -A publisher SHOULD enable compression only for payload types that benefit from it. +A sender SHOULD enable compression only for payload types that benefit from it. Already-compressed media SHOULD use `none`. @@ -107,10 +109,12 @@ There is no shared dictionary or state between objects, so each object decompres # Relay Behavior -A relay MAY forward compressed payloads unchanged, decompress them, or recompress them with a different algorithm, provided the decompressed bytes delivered to the original-format consumer are identical to what the publisher produced. +Because compression is hop-by-hop, a relay handles each hop independently. +On its upstream subscription it receives objects compressed with whatever algorithm that hop signaled (or none) and decompresses them as needed. +On each downstream subscription it serves, it independently chooses whether to compress — using any algorithm that downstream advertised — and signals its choice with the COMPRESSION subscription property on that SUBSCRIBE_OK / FETCH_OK. -A relay MAY transcode between algorithms — including bridging a peer that supports this extension to one that does not (by decompressing to `none`), or bridging two peers that support different algorithm sets. -When it does, it rewrites the COMPRESSION track property accordingly on the downstream session. +A relay MAY therefore bridge a compressing hop to a non-supporting one (by decompressing), compress an originally-uncompressed upstream toward a downstream that supports it, or recompress with a different algorithm. +In every case the decompressed bytes delivered to the application MUST be identical to what the origin published. A relay SHOULD NOT compress an originally-uncompressed payload unless there is a strong content signal that compression is beneficial (for example, a track name ending in `.json`), because it cannot otherwise predict whether compression will help or hurt. @@ -119,20 +123,20 @@ A relay or generic library MUST NOT inspect or modify the decompressed contents # Security Considerations Compressing data that mixes attacker-controlled and secret content in the same object can leak the secret through compressed size, as in the CRIME and BREACH attacks. -A publisher MUST NOT enable compression on a track whose object payloads combine secret material with attacker-influenced material. +A sender MUST NOT enable compression for payloads that combine secret material with attacker-influenced material. Because compression here is per-object with no cross-object dictionary, the exposure is bounded to within a single object, but it is not eliminated. -A malicious publisher could send a small compressed payload that decompresses to a very large buffer (a "decompression bomb"). +A malicious sender could emit a small compressed payload that decompresses to a very large buffer (a "decompression bomb"). A receiver MUST bound the size of a decompressed object payload and MUST reset the stream with a PROTOCOL_VIOLATION (or an application error) if the bound is exceeded, rather than allocating unbounded memory. -Compression is orthogonal to {{moqt}} end-to-end encryption: an encrypted payload is effectively incompressible, so a publisher using end-to-end encryption SHOULD use `none`. +Compression is orthogonal to {{moqt}} end-to-end encryption: an encrypted payload is effectively incompressible, so a sender SHOULD NOT compress an end-to-end-encrypted payload. # 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). -The property Type is even so that its value is a bare varint with no length prefix (see {{moqt}} Section 2.5). +The parameter Type is even so that its value is a bare varint with no length prefix (see {{moqt}} Section 2.5). ## MOQT Setup Options @@ -142,13 +146,14 @@ This document requests a registration in the "MOQT Setup Options" registry ({{mo |:--------|:------------|:--------------| | 0xC03DE | COMPRESSION | This Document | -## MOQT Properties +## MOQT Message Parameters -This document requests a registration in the "MOQT Properties" registry ({{moqt}} Section 15.8), used for object and track properties. +This document requests a registration in the "MOQT Message Parameters" registry ({{moqt}} Section 15.7). +COMPRESSION is a subscription property carried in SUBSCRIBE_OK and FETCH_OK. -| Value | Name | Scope | Reference | -|:--------|:------------|:------|:--------------| -| 0xC03D0 | COMPRESSION | Track | This Document | +| Value | Name | Carried In | Reference | +|:--------|:------------|:----------------------|:--------------| +| 0xC03D0 | COMPRESSION | SUBSCRIBE_OK, FETCH_OK | This Document | ## MOQT Compression Algorithms diff --git a/draft-lcurley-moq-relay-hops.md b/draft-lcurley-moq-relay-hops.md index ddef565..fc5b754 100644 --- a/draft-lcurley-moq-relay-hops.md +++ b/draft-lcurley-moq-relay-hops.md @@ -24,8 +24,9 @@ informative: --- abstract This document defines a Relay Hops extension for MoQ Transport {{moqt}}. -Each namespace advertisement carries an ordered list of Hop IDs identifying the relays it has traversed, and a namespace subscription MAY carry a Hop ID to exclude. -Together these allow a relay cluster to detect and avoid routing loops, and allow a downstream subscriber to break ties between multiple paths to the same namespace by preferring the shortest one. +Each namespace advertisement carries an ordered list of Hop IDs identifying the relays it has traversed, starting with the origin publisher. +This lets a subscriber prefer the shortest of several paths to the same namespace, identify which advertisements refer to the same broadcast (same origin), and lets a relay cluster detect and avoid routing loops. +A namespace subscription MAY carry a single Hop ID to exclude, which a relay uses to suppress advertisements that have already passed through that hop. --- middle @@ -38,17 +39,19 @@ Together these allow a relay cluster to detect and avoid routing loops, and allo A namespace advertisement (PUBLISH_NAMESPACE, {{moqt}} Section 10.15) originates at a publisher and propagates downstream through one or more relays toward interested subscribers, which express interest with SUBSCRIBE_NAMESPACE ({{moqt}} Section 10.18). In a redundant deployment, relays are interconnected so that the same namespace can reach a given relay over more than one path. -This redundancy is desirable for failover, but it introduces two problems that {{moqt}} does not address: +This redundancy is desirable for failover, but it leaves a receiver with no information that {{moqt}} does not address: -- **Routing loops**: relay A advertises a namespace to relay B, which advertises it back to A (directly or through a cycle). Without a way to recognize an advertisement it has already seen, a relay will re-advertise it indefinitely. - **Path selection**: when the same namespace arrives over multiple paths, a relay or subscriber has no information with which to prefer one path over another (e.g. the shorter, and usually lower-latency, one). +- **Broadcast identity**: two advertisements for the same namespace may refer to the same broadcast or to two distinct origins reusing a namespace. With no origin identity a receiver cannot tell them apart, nor deduplicate redundant paths to one broadcast. +- **Routing loops**: relay A advertises a namespace to relay B, which advertises it back to A (directly or through a cycle). Without a way to recognize an advertisement it has already seen, a relay will re-advertise it indefinitely. -This extension solves both with a single mechanism: an ordered list of **Hop IDs** that records the relays an advertisement has traversed. +This extension solves all three with a single mechanism: an ordered list of **Hop IDs** that records the path an advertisement has taken, starting with the origin publisher and with one entry appended per relay. +The first entry identifies the origin (broadcast identity); the list length gives the path length (path selection); a relay finding its own Hop ID already in the list detects a loop. ## Why per-hop, not end-to-end The Hop ID list is rewritten at every relay: a relay appends its own Hop ID before forwarding an advertisement downstream. A relay therefore detects a loop by finding its own Hop ID already present in an incoming advertisement, and a subscriber compares path lengths using the list length. -Because the list is meaningful only within a single relay deployment, Hop IDs are scoped to that deployment and are not interpreted by endpoints outside it. +Hop IDs are chosen randomly (see [Hop IDs](#hop-ids)) so they are unique with overwhelming probability without any central coordination, even across independently operated relays. # Setup Negotiation @@ -64,24 +67,27 @@ RELAY_HOPS Setup Option { The extension applies to a single hop (one MOQT session) and is negotiated independently for each session; a relay MUST NOT assume that because one of its sessions negotiated Relay Hops, another did. -A relay that negotiated this extension on a downstream session SHOULD include the HOP_PATH parameter on every PUBLISH_NAMESPACE it sends on that session, and SHOULD honor an EXCLUDE_HOP parameter it receives in SUBSCRIBE_NAMESPACE. +A relay that negotiated this extension on a downstream session MUST include the HOP_PATH parameter on every PUBLISH_NAMESPACE it sends on that session, and MUST honor an EXCLUDE_HOP parameter it receives in SUBSCRIBE_NAMESPACE. An endpoint that did not negotiate the extension simply omits these parameters; per {{moqt}} an unknown Key-Value-Pair Type is ignored, so an advertisement forwarded into a non-supporting session loses its hop information gracefully. # Hop IDs -A **Hop ID** is a variable-length integer that uniquely identifies a relay within a relay deployment (cluster). +A **Hop ID** is a variable-length integer that identifies a single relay (or the origin publisher) within the path of an advertisement. -- How Hop IDs are assigned is deployment-specific and out of scope for this document. A deployment MUST ensure each participating relay has a Hop ID that is unique within that deployment for as long as it is in use. -- The value `0` is reserved and means "unknown hop". It is used when an advertisement was bridged from a peer that does not support this extension (or an older protocol version), so that the hop count still reflects the true path length even when an intermediate identity is unavailable. A relay MUST NOT assign `0` to itself. +Each relay and each origin publisher chooses its Hop ID **randomly**. +An endpoint SHOULD draw a full-width random value (up to the 62-bit varint maximum) so that the probability of two endpoints choosing the same Hop ID is negligible. +Random assignment means there is no registry, no coordination, and no reserved values: a Hop ID is simply an opaque identifier that is, with overwhelming probability, unique. -A relay MUST NOT reveal Hop IDs from one deployment to an unrelated deployment; see [Security Considerations](#security-considerations). +An endpoint SHOULD keep its Hop ID stable for the lifetime of a session (and MAY reuse it across sessions) so that loop detection and path comparison are consistent. + +When a relay bridges an advertisement from an upstream peer that did **not** negotiate this extension, the upstream carries no HOP_PATH. The relay MUST synthesize one (see [Relay Behavior](#relay-behavior)) by assigning a random Hop ID to stand in for the unknown upstream, so that loop detection and path length still work within the cooperating region of the mesh. # HOP_PATH Parameter -The HOP_PATH parameter carries the ordered list of Hop IDs that an advertisement has traversed, from the origin toward the receiver. +The HOP_PATH parameter carries the ordered list of Hop IDs that an advertisement has traversed, from the origin publisher toward the receiver. It is a Key-Value-Pair (see {{moqt}} Section 2.5) carried in the Parameters of a PUBLISH_NAMESPACE message ({{moqt}} Section 10.15). -Because the value is a variable-length byte string, HOP_PATH uses an odd Type so that it is length-prefixed: +Because the value is a variable-length list, HOP_PATH uses an odd Type so that it is length-prefixed: ~~~ HOP_PATH Parameter { @@ -92,73 +98,69 @@ HOP_PATH Parameter { ~~~ **Hop ID**: -Zero or more Hop IDs, ordered from the origin-most relay to the relay immediately upstream of the receiver. -The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`. -An empty value (Length 0) means the advertisement has not yet traversed any relay that assigns Hop IDs — typically because the sender is the origin publisher. +One or more Hop IDs, ordered from the origin publisher (first entry) to the relay immediately upstream of the receiver (last entry). +The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`, or if the list is empty (`Length` 0). +HOP_PATH always contains at least one entry: the first entry is the Hop ID of the origin publisher, even before the advertisement has traversed any relay. ## Relay Behavior -When a relay forwards a namespace advertisement downstream on a session that negotiated this extension, it MUST append its own Hop ID to the HOP_PATH it received (creating an empty HOP_PATH first if the upstream advertisement carried none). +When a relay forwards a namespace advertisement downstream on a session that negotiated this extension, it MUST append its own Hop ID to the HOP_PATH it received. The relay's own Hop ID is therefore always the last entry of the list it sends. +If the advertisement arrived from an upstream that did not negotiate this extension (and so carried no HOP_PATH), the relay MUST first create a HOP_PATH whose single initial entry is a random Hop ID it assigns to stand in for that unknown upstream, then append its own Hop ID. When a relay receives a namespace advertisement on a session that negotiated this extension, it MUST inspect the HOP_PATH: - If its own Hop ID already appears in the list, the advertisement has looped. The relay MUST NOT forward it and SHOULD drop it. - Otherwise the relay MAY forward it downstream, appending its own Hop ID as described above. -When bridging from a peer that did not negotiate this extension, a relay SHOULD synthesize a single leading `0` ("unknown hop") entry rather than omitting it. -Counting the bridged hop keeps path lengths comparable, so that the advisory path selection in the following section behaves consistently across relays that bridge the same non-supporting upstream. - ## Path Selection A relay or subscriber that receives advertisements for the same namespace over multiple sessions MAY use the length of the HOP_PATH list as a tiebreaker, preferring the advertisement with the fewest hops (usually the lowest-latency path). This is advisory: the receiver MAY apply additional local policy (e.g. measured RTT or administrative preference) and is not required to prefer the shortest path. +Two advertisements for the same namespace whose HOP_PATH begins with the same Hop ID share an origin and therefore refer to the same broadcast; a receiver MAY treat them as redundant paths and keep only the best one. +If the first Hop IDs differ, the advertisements come from distinct origins that happen to reuse a namespace, and a receiver MUST NOT treat them as interchangeable. + A publisher (or relay acting as one) SHOULD advertise only the single best path it currently knows for each namespace. If the best path changes — for example after a relay failover — the publisher MAY re-advertise the namespace; the new advertisement, carrying an updated HOP_PATH, replaces the prior one per the namespace-advertisement semantics of {{moqt}}. # EXCLUDE_HOP Parameter -The EXCLUDE_HOP parameter lets a downstream subscriber tell an upstream relay to suppress advertisements that have already passed through a given relay. +The EXCLUDE_HOP parameter lets a downstream subscriber tell an upstream relay to suppress advertisements that have already passed through a given hop. A relay in a cluster uses it to prevent the upstream from sending back an advertisement that the downstream originated, the most common source of a two-hop loop. It is a Key-Value-Pair carried in the Parameters of a SUBSCRIBE_NAMESPACE message ({{moqt}} Section 10.18). -Because the value is a list, EXCLUDE_HOP uses an odd Type so that it is length-prefixed: +A single Hop ID is excluded, so EXCLUDE_HOP uses an even Type and its value is a bare varint with no length prefix: ~~~ EXCLUDE_HOP Parameter { - Type (vi64) = 0x40B59 - Length (vi64) - Hop ID (vi64) ... + Type (vi64) = 0x40B58 + Hop ID (vi64) } ~~~ **Hop ID**: -One or more Hop IDs to exclude, in any order. -The number of entries is determined by consuming Hop IDs until `Length` bytes have been read; a receiver MUST close the session with a PROTOCOL_VIOLATION if the entries do not exactly fill `Length`. -An empty list (`Length` 0) is permitted and means "exclude nothing"; like an empty HOP_PATH it is a harmless no-op and a receiver MUST NOT treat it as an error. +The single Hop ID to exclude. +To exclude nothing, a subscriber simply omits the parameter; there is no reserved "exclude nothing" value. -A relay that receives a SUBSCRIBE_NAMESPACE carrying EXCLUDE_HOP SHOULD NOT send, on that session, any PUBLISH_NAMESPACE whose HOP_PATH contains any of the listed Hop IDs (including the implicit final entry the relay would itself append). +A relay that receives a SUBSCRIBE_NAMESPACE carrying EXCLUDE_HOP MUST NOT send, on that session, any PUBLISH_NAMESPACE whose HOP_PATH contains the excluded Hop ID (including the entry the relay would itself append). The exclusion is scoped to the namespace subscription it accompanies. A relay that receives EXCLUDE_HOP without having negotiated the Relay Hops extension ignores it as an unknown parameter, which is the safe default (it simply does not perform the exclusion). # Security Considerations -Hop IDs describe the internal topology of a relay deployment. -A relay MUST treat Hop IDs as private to the deployment: it MUST NOT forward HOP_PATH or EXCLUDE_HOP across a trust boundary (for example, to a subscriber outside the operator's own relay cluster), and SHOULD strip the HOP_PATH parameter before forwarding an advertisement to such a peer. -Leaking Hop IDs could reveal cluster size, topology, or failover state to an untrusted party. - -A malicious upstream could forge a HOP_PATH to influence a downstream's path selection (e.g. claiming a short path it cannot actually deliver). -Path selection using HOP_PATH is therefore advisory only; a receiver SHOULD corroborate it with locally measured signals (e.g. RTT) before relying on it, and MUST NOT make security decisions based on Hop IDs. +Hop IDs are opaque random integers, so an individual value reveals nothing about a relay's identity or location. +A HOP_PATH list does, however, expose the number of hops an advertisement traversed, which can hint at the size and shape of a relay deployment. +A relay that wishes to hide its internal topology MAY coalesce the hops within its own administrative domain into a single Hop ID, or strip HOP_PATH entirely, before forwarding across a trust boundary (for example, to a subscriber outside the operator's own relay cluster). +This is analogous to how BGP confederations hide internal AS topology while preserving loop detection; it is a deployment choice, not a requirement. -A malicious subscriber could supply a large EXCLUDE_HOP list to consume relay resources. -Implementations SHOULD bound the number of excluded Hop IDs they will accept and MAY reject a SUBSCRIBE_NAMESPACE whose EXCLUDE_HOP list exceeds that bound. +Because a relay only ever appends to HOP_PATH, it cannot make a competing path appear shorter than it is; the worst a misbehaving relay can do is under-report the upstream portion of its own path to win an advisory tie-break. Since path selection is advisory, the impact is limited to a suboptimal path choice. A receiver MUST NOT make security decisions based on Hop IDs, and SHOULD corroborate path selection with locally measured signals (e.g. RTT) when it matters. # 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; they also avoid the greasing pattern (`0x7f * N + 0x9D`). -The two parameter Types are odd so that each is length-prefixed (see {{moqt}} Section 2.5). +HOP_PATH carries a list, so its Type is odd (length-prefixed); EXCLUDE_HOP carries a single Hop ID, so its Type is even (a bare varint). See {{moqt}} Section 2.5. ## MOQT Setup Options @@ -176,7 +178,7 @@ HOP_PATH and EXCLUDE_HOP are message parameters carried in PUBLISH_NAMESPACE and | Value | Name | Carried In | Reference | |:--------|:------------|:--------------------|:--------------| | 0x40B57 | HOP_PATH | PUBLISH_NAMESPACE | This Document | -| 0x40B59 | EXCLUDE_HOP | SUBSCRIBE_NAMESPACE | This Document | +| 0x40B58 | EXCLUDE_HOP | SUBSCRIBE_NAMESPACE | This Document | --- back diff --git a/draft-lcurley-moq-timestamp.md b/draft-lcurley-moq-timestamp.md index 2e39db0..8d0834e 100644 --- a/draft-lcurley-moq-timestamp.md +++ b/draft-lcurley-moq-timestamp.md @@ -43,6 +43,9 @@ A relay frequently needs a notion of *when* an object is meant to be presented: - **Consistent expiration across hops**: every relay on a path should make the same drop decision for the same object. A timestamp embedded in the object is identical at every hop; a wall-clock arrival time is not. - **Synchronization hints**: a subscriber can align objects from multiple tracks (e.g. audio and video) using a shared media timeline without first decoding each container. +MoQ also demultiplexes media into many independent tracks — audio, video, captions, metadata, and more — so a timestamp is needed on nearly every track. +Re-implementing per-object timestamping inside each application's container format, for every track, is repetitive and error-prone; standardizing it at the transport lets one implementation serve every track and lets relays use it directly. + This extension exposes media time to the transport with three Key-Value-Pairs ({{moqt}} Section 2.5): a track-level **Timescale**, an object-level **Timestamp**, and an optional object-level **Duration**. The transport does not interpret the *meaning* of the timeline (it is still the application's clock); it only uses the timestamp for relative age comparisons. @@ -82,6 +85,10 @@ A value of `0`, or the absence of the property, means the track has no media tim The Timescale is fixed for the lifetime of the track and MUST NOT change. +The Timescale is required to interpret the units of every Timestamp and Duration, so a receiver cannot resolve an object's timing until it has the track's properties. +Those properties are delivered in SUBSCRIBE_OK or TRACK_STATUS ({{moqt}} Section 12), so a receiver that begins receiving objects before it has them MUST buffer the timing (or treat it as unknown) until the Timescale arrives. +A relay that has not yet learned the Timescale MUST fall back to wall-clock arrival time for any age-based decision. + # TIMESTAMP Object Property The TIMESTAMP property carries the presentation time of an object, in the track's Timescale. @@ -125,7 +132,7 @@ DURATION Object Property { The presentation duration of the object, expressed in the track's Timescale. A value of `0`, or the absence of the property, means the duration is unknown; the object is presented until the next object begins. -Duration is an application-level hint and is not used by the transport for delivery or dropping decisions. +Duration is primarily an application-level presentation hint, but a relay MAY also use it to refine age-based dropping: an object's Timestamp plus its Duration marks the end of its presentation interval, which is a more precise "this object is now in the past" signal than the Timestamp alone (for example, the last object of a group has no following object to bound it). A relay MUST NOT rely on Duration being present; when it is absent, the relay falls back to comparing Timestamps as in [Age-Based Dropping](#age-based-dropping). # Security Considerations From 6f59a40a99989b2ef4d76bfc33924c26b181c2a4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 19:43:39 -0700 Subject: [PATCH 5/7] compression: revert COMPRESSION to a publisher track property Per review: COMPRESSION is the original publisher's end-to-end signal that a track's payloads are worth compressing (and which algorithm), not a per-hop choice. Actual on-wire compression stays hop-by-hop and is gated by negotiation: a payload is compressed only on a hop that negotiated the extension and whose receiver advertised the algorithm; otherwise it travels verbatim. The property is forwarded unchanged by relays, making the on-wire state unambiguous without per-object signaling. Re-register under MOQT Properties (Section 15.8, Track scope). Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-moq-compression.md | 64 +++++++++++++++++--------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/draft-lcurley-moq-compression.md b/draft-lcurley-moq-compression.md index c5aa9b3..d7beea5 100644 --- a/draft-lcurley-moq-compression.md +++ b/draft-lcurley-moq-compression.md @@ -25,8 +25,9 @@ informative: --- abstract This document defines a payload compression extension for MoQ Transport {{moqt}}. -Compression is a hop-by-hop wire optimization, analogous to HTTP Transfer-Encoding: the endpoint serving a subscription MAY compress object payloads with an algorithm the receiver advertised, signaling the algorithm per subscription. Each object is compressed independently so objects remain individually decodable. -Because it is hop-by-hop, the origin publisher need not participate: any relay can compress toward a downstream that supports it, or decompress toward one that does not, and the decompressed bytes — the actual object — are unchanged end to end. +A track-level Compression property lets the original publisher signal that a track's object payloads are worth compressing, and with which algorithm. +Compression is then applied independently on each hop: a payload is compressed only on a hop that has negotiated the extension and whose receiver supports the algorithm, and is sent verbatim otherwise. +Each object is compressed independently so objects remain individually decodable, and the decompressed bytes — the actual object — are unchanged end to end. --- middle @@ -41,10 +42,10 @@ For media this is the right layering: already-compressed codecs (H.264, Opus, AV But MoQ also carries non-media tracks — JSON, text, telemetry, captions, uncompressed binary structures — where the payloads are highly compressible and where end-to-end encryption is often not in use. For these tracks there is no standard, transport-visible way to compress payloads, so each application reinvents it, and relays cannot help. -This extension defines a hop-by-hop, per-object compression scheme negotiated at the transport layer. -It is a wire optimization, analogous to HTTP Transfer-Encoding (a hop-by-hop encoding) rather than HTTP Content-Encoding (an end-to-end one): it does not conceptually change the object payload — the decompressed bytes *are* the object — it only changes how those bytes are carried over a single hop. +Like HTTP Transfer-Encoding, the on-wire compression is a hop-by-hop optimization: it does not conceptually change the object payload — the decompressed bytes *are* the object — it only changes how those bytes are carried over a single hop. +What this extension adds on top is an end-to-end *signal*: a track property by which the original publisher marks the content as worth compressing and names the algorithm. The signal travels end-to-end; the compression happens per hop. -- **Hop-by-hop, not end-to-end**: compression is negotiated during SETUP for each session and applied independently on each hop by whichever endpoint is serving the subscription. The origin publisher need not opt in; a relay can compress toward a downstream that supports it and decompress toward one that does not. +- **Publisher signals, hops apply**: the COMPRESSION track property is set by the original publisher and carried end-to-end, but a payload is only compressed on a hop that negotiated the extension and whose receiver supports the algorithm. Where the extension is not negotiated, the same payload travels verbatim. - **Per object, independently**: each object payload is an independent compressed stream with no shared dictionary or state between objects. This keeps every object individually decodable and avoids head-of-line decoding within a group. @@ -67,32 +68,37 @@ One or more Algorithm identifiers (see [Compression Algorithms](#compression-alg The identifier `none` (0) MUST NOT be listed (it requires no negotiation). A sender MUST NOT compress with an algorithm the receiver did not advertise in its SETUP. -If a receiver is told — via the COMPRESSION subscription property below — that objects use an algorithm it did not advertise (for example because data arrived before SETUP, or a peer misbehaved), it MUST reset the affected subscription/fetch with a PROTOCOL_VIOLATION rather than deliver the payload uninterpreted. +This makes the on-wire state unambiguous on every hop without any per-object signaling: a receiver decompresses a track's payloads **if and only if** the COMPRESSION track property is present and the receiver advertised that algorithm in its own SETUP. In every other case — the property absent, the extension not negotiated, or the algorithm not advertised by the receiver — the sender was not permitted to compress, so the receiver treats the payloads as verbatim. -# COMPRESSION Subscription Property -Compression is signaled per subscription by the endpoint serving it. -The COMPRESSION property declares the algorithm applied to the object payloads delivered for a given subscription (or fetch). -It is a Key-Value-Pair (see {{moqt}} Section 2.5) carried as a parameter of SUBSCRIBE_OK (and the corresponding FETCH_OK), sent by the publisher or relay that serves the request to the receiver. +# COMPRESSION Track Property +The COMPRESSION property is the original publisher's signal that a track's object payloads are worth compressing, and which algorithm to use. +It is a track-level Key-Value-Pair carried with the track's properties (see {{moqt}} Section 2.5 and Section 12), set by the original publisher and forwarded unchanged by relays. Because the value is a single integer, COMPRESSION uses an even Type so the value is a bare varint: ~~~ -COMPRESSION Subscription Property { +COMPRESSION Track Property { Type (vi64) = 0xC03D0 Value (vi64) ; Algorithm identifier } ~~~ **Value**: -The Algorithm identifier applied to every object payload delivered for this subscription. -The absence of the property, or a value of `none` (0), means payloads are transmitted verbatim. +The Algorithm identifier the publisher recommends for this track's payloads. +The absence of the property, or a value of `none` (0), means the track is not marked for compression and its payloads are always transmitted verbatim. + +The property is fixed for the lifetime of the track and MUST NOT change; a relay forwards it unchanged. + +Whether payloads are actually compressed is decided per hop: + +- On a hop where the extension is negotiated and the receiver advertised the property's algorithm, every non-empty object payload MUST be compressed with that algorithm, and the receiver decompresses it. +- On any other hop — the extension not negotiated, or the receiver did not advertise that algorithm — payloads are sent verbatim. The receiver either never sees the property (an ignored unknown Key-Value-Pair) or sees it but knows the sender was not permitted to compress for it, so it treats the payloads as verbatim either way. -The algorithm is fixed for the lifetime of the subscription; to change it, the receiver re-subscribes. Compression applies to the object payload only; object properties and message framing are never compressed. An empty payload (size 0) MUST NOT be compressed and remains empty on the wire. -A sender SHOULD enable compression only for payload types that benefit from it. -Already-compressed media SHOULD use `none`. +A publisher SHOULD set COMPRESSION only for payload types that benefit from it. +Already-compressed media SHOULD omit it (or use `none`). # Compression Algorithms {#compression-algorithms} @@ -109,27 +115,26 @@ There is no shared dictionary or state between objects, so each object decompres # Relay Behavior -Because compression is hop-by-hop, a relay handles each hop independently. -On its upstream subscription it receives objects compressed with whatever algorithm that hop signaled (or none) and decompresses them as needed. -On each downstream subscription it serves, it independently chooses whether to compress — using any algorithm that downstream advertised — and signals its choice with the COMPRESSION subscription property on that SUBSCRIBE_OK / FETCH_OK. +A relay forwards the COMPRESSION track property unchanged — it is the publisher's end-to-end signal — and applies compression independently on each hop. -A relay MAY therefore bridge a compressing hop to a non-supporting one (by decompressing), compress an originally-uncompressed upstream toward a downstream that supports it, or recompress with a different algorithm. -In every case the decompressed bytes delivered to the application MUST be identical to what the origin published. +On its upstream subscription, the relay receives payloads compressed if and only if that hop compressed them (the extension negotiated and the relay advertised the algorithm); it decompresses them as needed. +On each downstream subscription it serves, it compresses payloads with the track's algorithm when that downstream negotiated the extension and advertised the algorithm, and sends them verbatim otherwise. -A relay SHOULD NOT compress an originally-uncompressed payload unless there is a strong content signal that compression is beneficial (for example, a track name ending in `.json`), because it cannot otherwise predict whether compression will help or hurt. +Compression is thus driven by the publisher's track property, not by the relay: a relay does not compress a track the publisher did not mark. +In every case the decompressed bytes delivered to the application MUST be identical to what the origin published. A relay or generic library MUST NOT inspect or modify the decompressed contents unless otherwise negotiated; only recompression that preserves the decompressed bytes exactly is permitted. # Security Considerations Compressing data that mixes attacker-controlled and secret content in the same object can leak the secret through compressed size, as in the CRIME and BREACH attacks. -A sender MUST NOT enable compression for payloads that combine secret material with attacker-influenced material. +A publisher MUST NOT set COMPRESSION on a track whose object payloads combine secret material with attacker-influenced material. Because compression here is per-object with no cross-object dictionary, the exposure is bounded to within a single object, but it is not eliminated. A malicious sender could emit a small compressed payload that decompresses to a very large buffer (a "decompression bomb"). A receiver MUST bound the size of a decompressed object payload and MUST reset the stream with a PROTOCOL_VIOLATION (or an application error) if the bound is exceeded, rather than allocating unbounded memory. -Compression is orthogonal to {{moqt}} end-to-end encryption: an encrypted payload is effectively incompressible, so a sender SHOULD NOT compress an end-to-end-encrypted payload. +Compression is orthogonal to {{moqt}} end-to-end encryption: an encrypted payload is effectively incompressible, so a publisher using end-to-end encryption SHOULD omit COMPRESSION (or use `none`). # IANA Considerations @@ -146,14 +151,13 @@ This document requests a registration in the "MOQT Setup Options" registry ({{mo |:--------|:------------|:--------------| | 0xC03DE | COMPRESSION | This Document | -## MOQT Message Parameters +## MOQT Properties -This document requests a registration in the "MOQT Message Parameters" registry ({{moqt}} Section 15.7). -COMPRESSION is a subscription property carried in SUBSCRIBE_OK and FETCH_OK. +This document requests a registration in the "MOQT Properties" registry ({{moqt}} Section 15.8), used for object and track properties. -| Value | Name | Carried In | Reference | -|:--------|:------------|:----------------------|:--------------| -| 0xC03D0 | COMPRESSION | SUBSCRIBE_OK, FETCH_OK | This Document | +| Value | Name | Scope | Reference | +|:--------|:------------|:------|:--------------| +| 0xC03D0 | COMPRESSION | Track | This Document | ## MOQT Compression Algorithms From 0fe1ec65ca1c7453e2ba645768205599576c28e9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 19:46:42 -0700 Subject: [PATCH 6/7] compression: clarify decompression-bomb reset scope; tighten pronoun Per review: the decompression-bomb guard now explicitly resets only the affected Subscribe/Fetch stream (and MAY close the session with PROTOCOL_VIOLATION only if the peer is abusive), so one bad object doesn't tear down unrelated subscriptions. Also clarify the relay-behavior pronoun. Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-moq-compression.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/draft-lcurley-moq-compression.md b/draft-lcurley-moq-compression.md index d7beea5..4bf500a 100644 --- a/draft-lcurley-moq-compression.md +++ b/draft-lcurley-moq-compression.md @@ -118,7 +118,7 @@ There is no shared dictionary or state between objects, so each object decompres A relay forwards the COMPRESSION track property unchanged — it is the publisher's end-to-end signal — and applies compression independently on each hop. On its upstream subscription, the relay receives payloads compressed if and only if that hop compressed them (the extension negotiated and the relay advertised the algorithm); it decompresses them as needed. -On each downstream subscription it serves, it compresses payloads with the track's algorithm when that downstream negotiated the extension and advertised the algorithm, and sends them verbatim otherwise. +On each downstream subscription the relay serves, it compresses payloads with the track's algorithm when that downstream negotiated the extension and advertised the algorithm, and sends them verbatim otherwise. Compression is thus driven by the publisher's track property, not by the relay: a relay does not compress a track the publisher did not mark. In every case the decompressed bytes delivered to the application MUST be identical to what the origin published. @@ -132,7 +132,7 @@ A publisher MUST NOT set COMPRESSION on a track whose object payloads combine se Because compression here is per-object with no cross-object dictionary, the exposure is bounded to within a single object, but it is not eliminated. A malicious sender could emit a small compressed payload that decompresses to a very large buffer (a "decompression bomb"). -A receiver MUST bound the size of a decompressed object payload and MUST reset the stream with a PROTOCOL_VIOLATION (or an application error) if the bound is exceeded, rather than allocating unbounded memory. +A receiver MUST bound the size of a decompressed object payload. If the bound is exceeded it MUST reset the affected Subscribe/Fetch stream (rather than allocate unbounded memory) and MAY close the session with a PROTOCOL_VIOLATION if it considers the peer abusive; the reset is stream-scoped so a single bad object does not tear down unrelated subscriptions. Compression is orthogonal to {{moqt}} end-to-end encryption: an encrypted payload is effectively incompressible, so a publisher using end-to-end encryption SHOULD omit COMPRESSION (or use `none`). From a070b5a94313338899cd8801c2f6cd77c15fd0fb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 19:51:39 -0700 Subject: [PATCH 7/7] compression: clarify the track property is the sole enabler - A relay MUST forward the COMPRESSION track property unchanged even on hops that did not negotiate the extension, so a further-downstream hop that does negotiate it can still act on the publisher's signal. - State explicitly that compression is enabled only by the track property plus a negotiated hop: a publisher MUST NOT compress without the property, and there is no per-object enabling. (The .json relay heuristic was already removed in 6f59a40.) Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-moq-compression.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/draft-lcurley-moq-compression.md b/draft-lcurley-moq-compression.md index 4bf500a..13f7302 100644 --- a/draft-lcurley-moq-compression.md +++ b/draft-lcurley-moq-compression.md @@ -87,7 +87,11 @@ COMPRESSION Track Property { The Algorithm identifier the publisher recommends for this track's payloads. The absence of the property, or a value of `none` (0), means the track is not marked for compression and its payloads are always transmitted verbatim. -The property is fixed for the lifetime of the track and MUST NOT change; a relay forwards it unchanged. +The property is fixed for the lifetime of the track and MUST NOT change. +A relay MUST forward it unchanged on every hop, including a hop that has not negotiated the extension: there it is simply an ignored unknown Key-Value-Pair, but forwarding it lets a further-downstream hop that does negotiate the extension still act on the publisher's signal. + +Compression is enabled only by the combination of this track property and the extension being negotiated on a hop. +A publisher MUST NOT compress object payloads on a track that does not carry the COMPRESSION property, and there is no way to enable compression on a per-object basis: the property governs the whole track, and on a compressing hop every non-empty payload is compressed. Whether payloads are actually compressed is decided per hop: