From c50e4db0b65887feaaf60bc5bd939e0ccc535366 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 15:38:15 -0700 Subject: [PATCH 1/3] Add QMux over WebSocket draft QMux (draft-ietf-quic-qmux-01) defines bindings over TCP and TLS but not over WebSocket, since that is out of the QUIC WG charter scope. moq-lite already relies on a WebSocket binding (and moq-dev/web-transport's rs/qmux crate implements one), so this adds a standalone, application-agnostic draft specifying it. Key points: - One WebSocket binary message carries one QMux Record's frames; the Record Size field is omitted because the WS message boundary delimits it. This relies on the Record layer introduced in qmux-01. - Sec-WebSocket-Protocol carries the application ALPN alone (e.g. moq-transport-18); the QMux version is implied by the application protocol, matching the QMux-over-TLS convention. - Text messages are forbidden; keep-alive uses WebSocket Ping/Pong since WebSocket has no built-in idle timeout; datagrams are not available on a reliable, ordered transport. Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-qmux-websocket.md | 205 ++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 draft-lcurley-qmux-websocket.md diff --git a/draft-lcurley-qmux-websocket.md b/draft-lcurley-qmux-websocket.md new file mode 100644 index 0000000..fb777c5 --- /dev/null +++ b/draft-lcurley-qmux-websocket.md @@ -0,0 +1,205 @@ +--- +title: "QMux over WebSocket" +abbrev: "qmux-ws" +category: info + +docname: draft-lcurley-qmux-websocket-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: + qmux: I-D.ietf-quic-qmux + RFC6455: + RFC9000: + +informative: + RFC8446: + RFC9220: + moqt: I-D.ietf-moq-transport + +--- abstract + +QMux [qmux] is a polyfill that runs QUIC applications over an ordered, reliable byte-stream transport such as TCP with TLS. +This document defines a binding for QMux over WebSocket [RFC6455]. +The mapping is straightforward because both layers provide length-delimited messages over a reliable byte stream: a single WebSocket binary message carries exactly one QMux Record's frames, so the Record `Size` field is omitted. +A WebSocket binding lets QUIC applications reach environments where UDP is blocked and where only an HTTP/WebSocket stack is available, including web browsers that lack WebTransport. + +--- middle + +# Conventions and Definitions +{::boilerplate bcp14-tagged} + +This document uses the terms QMux Record, QMux Frame, and transport parameter as defined in [qmux], and the terms WebSocket connection, message, frame, and subprotocol as defined in [RFC6455]. + + +# Introduction +QMux [qmux] lets an application written against the QUIC stream and datagram API run over an ordered, reliable byte-stream transport. +It defines a binding over TCP and over TLS, but it does not define a binding over WebSocket; the WebSocket binding is out of scope for the QUIC working group charter. + +A WebSocket binding is nevertheless useful. +WebSocket [RFC6455] is available in essentially every deployment environment, including: + +- Networks where UDP (and therefore QUIC) is blocked by a firewall or middlebox. +- Web browsers, which expose a WebSocket API but do not universally expose a WebTransport or raw-socket API. +- HTTP load balancers and proxies that can route and terminate WebSocket but not raw TCP or QUIC. + +This document specifies how to carry QMux over WebSocket. +It defines the message framing, the subprotocol negotiation used in place of TLS ALPN, how the QMux version is selected, keep-alive behavior, and the handling of datagrams. +All other QMux semantics — in-order STREAM frame delivery, stream identifiers, flow control, transport parameters, and connection close — apply unchanged from [qmux]. + +This binding is application agnostic: any QUIC application that can run over QMux can run over QMux over WebSocket. +Media over QUIC Transport [moqt] is one such application and is the motivating use case, but nothing in this document is specific to it. + + +# WebSocket Binding Overview +A QMux-over-WebSocket connection is an ordinary WebSocket connection [RFC6455] whose binary messages carry QMux frames. + +Both the QMux Record layer and the WebSocket message layer provide self-delimiting messages over a reliable, ordered byte stream. +The two layers are therefore collapsed: instead of prefixing each Record with its `Size`, the binding relies on the WebSocket message boundary to delimit it. + +The WebSocket connection takes the place of the underlying byte-stream transport in [qmux]. +Once the WebSocket handshake completes, each endpoint sends and receives QMux frames inside WebSocket binary messages as defined below. + + +# Establishing a Connection +A client establishes a QMux-over-WebSocket connection by opening a WebSocket connection per [RFC6455] (the opening handshake over HTTP/1.1) or per [RFC9220] (the bootstrapping mechanism over HTTP/2 or HTTP/3). + +The `ws` URI scheme is used over an unencrypted transport and the `wss` URI scheme is used over a TLS-encrypted transport. +Deployments SHOULD use `wss`; an application that expects a TLS transport when running natively over QUIC SHOULD require `wss` here. + +How the underlying connection is authenticated and authorized is out of scope for this document, as it is for [qmux]. + + +# Subprotocol Negotiation +QMux over TCP/TLS uses TLS ALPN [RFC8446] to agree on the application protocol. +The QMux wire-format version is *not* negotiated separately: it is determined by the negotiated application protocol, as described in {{versions}}. +WebSocket has no ALPN exchange, so this binding uses the WebSocket subprotocol negotiation of [RFC6455] Section 1.9 — the `Sec-WebSocket-Protocol` header — in its place, carrying the same application protocol identifier. + +## Subprotocol Identifier +The subprotocol identifier is exactly the application protocol identifier that the application would use as its ALPN over native QUIC; for example `moq-transport-18`. +No QMux version token appears on the wire. +The application protocol identifier implies the QMux wire-format version per {{versions}}, so a single identifier selects both the application protocol and the wire format unambiguously. + +## Client Behavior +A client offers one or more application protocol identifiers in the `Sec-WebSocket-Protocol` request header, in decreasing order of preference. +A client that supports multiple application protocols, or multiple versions of one, offers one identifier per protocol version it is willing to use (for example `moq-transport-18` and `moq-transport-17`). + +A client MUST treat the absence of a `Sec-WebSocket-Protocol` response header, or a response value it did not offer, as a failed handshake per [RFC6455]. + +## Server Behavior +A server selects at most one of the client's offered identifiers and echoes it in the `Sec-WebSocket-Protocol` response header. +A server MUST NOT select an identifier the client did not offer. + +A server SHOULD select identifiers in its own order of preference (for example, preferring a newer application protocol version), independent of the client's ordering. +If the server supports none of the offered identifiers, it MUST fail the handshake. + +The selected identifier determines both the application protocol and, via {{versions}}, the QMux wire-format version for the connection. + + +# Record Framing {#framing} +Each WebSocket binary message carries exactly one QMux Record's `Frames` field: one or more QMux frames concatenated, as defined in [qmux]. +Because the WebSocket framing layer already delimits each message, the QMux Record `Size` field is redundant: it MUST NOT be transmitted and MUST NOT be expected by the receiver. + +A QMux-over-WebSocket record is therefore: + +~~~ +WebSocket Binary Message { + Frames (..), +} +~~~ + +The frames inside a message are encoded exactly as in [qmux], including the in-order STREAM frame requirement: for each QUIC stream, a sender MUST send that stream's payload in order, so a receiver can deliver payload to the application as it arrives without reassembly. + +An endpoint MAY place multiple frames in a single binary message and MAY split a logical sequence of frames across multiple messages, subject to the constraint that each STREAM frame's payload bytes are delivered in order. +An empty binary message (zero frames) is permitted and carries no frames; a receiver MUST accept it and treat it as a no-op. + +The maximum size of a binary message is bounded by the `max_record_size` transport parameter defined in [qmux] and by any WebSocket implementation limits. +An endpoint MUST NOT send a binary message whose payload exceeds the peer's advertised `max_record_size`, and MAY treat receipt of an oversized message as a connection error. + + +# WebSocket Message Types +This binding uses WebSocket message and control frames as follows: + +- *Binary messages* carry QMux frames as defined in {{framing}}. +- *Text messages* MUST NOT be sent. A receiver MUST treat a text message as a connection error and close the WebSocket connection. +- *Close frames* terminate the connection as defined in {{close}}. +- *Ping and Pong frames* are used for keep-alive as defined in {{keepalive}} and are otherwise handled by the WebSocket layer; they carry no QMux frames. + +The first QMux frame sent by each endpoint MUST be the `QX_TRANSPORT_PARAMETERS` frame, exactly as required by [qmux]; this binding does not change that requirement. + + +# QMux Version {#versions} +This binding builds on QMux as defined in [qmux] (draft-ietf-quic-qmux-01), which introduced the QMux Record layer that this binding relies on (see {{framing}}). + +The QMux version is not signaled on the wire and is not carried in the subprotocol identifier. +As with QMux over TLS, it is implied by the negotiated application protocol: each application protocol that runs over QMux specifies which QMux version each of its ALPN identifiers uses. +For example, Media over QUIC Transport [moqt] identifier `moq-transport-18` uses [qmux]. + +An application protocol used with this binding MUST select a QMux version that provides the Record layer, i.e. [qmux] or later. + + +# Keep-Alive and Idle Timeout {#keepalive} +QUIC and QMux detect a dead peer with an idle timeout. +A WebSocket connection has no built-in idle timeout: if the peer's host crashes or its network drops without a TCP FIN, the local socket can remain "open" until OS-level TCP keep-alive eventually probes, which may take hours. + +To detect a dead peer in a timely manner, an endpoint SHOULD send WebSocket Ping frames [RFC6455] periodically and SHOULD close the connection if no WebSocket frame of any kind is received from the peer within a timeout. +The timeout SHOULD be a small multiple of the ping interval to tolerate transient delays. +Reasonable defaults are a 5-second ping interval and a 30-second timeout, matching common QUIC idle-timeout configurations, but the values are a local policy decision. + +Receipt of any WebSocket frame from the peer — binary, Ping, or Pong — resets the idle timer. +An endpoint replies to a Ping with a Pong per [RFC6455]; this is handled by the WebSocket layer and is independent of QMux frames. + +This keep-alive operates at the WebSocket layer and is separate from the QMux `max_idle_timeout` transport parameter and the `QX_PING` frame defined in [qmux], either of which an endpoint MAY also use. + + +# Datagrams {#datagrams} +A WebSocket connection is reliable and ordered, so it cannot provide the unreliable, unordered delivery of the QUIC datagram extension. + +An endpoint MUST NOT negotiate the QMux datagram transport parameter over this binding, and MUST NOT send QMux DATAGRAM frames. +An application that uses datagrams over native QUIC MUST provide an alternative over this binding (for example, by carrying the same payloads on a QUIC stream). + + +# Connection Close {#close} +An endpoint terminates a QMux-over-WebSocket connection by sending a WebSocket Close frame [RFC6455] and then closing the underlying transport. + +A QMux `CONNECTION_CLOSE` frame, if sent, conveys the QMux-level error code and reason and SHOULD be sent in a final binary message before the WebSocket Close frame. +Because the WebSocket layer provides its own connection close, there is no draining period: an endpoint MAY close immediately after sending its Close frame. + +Receipt of a WebSocket Close frame, or loss of the underlying transport, terminates the QMux connection and all of its streams. + + +# Security Considerations +This binding inherits the security considerations of QMux [qmux], WebSocket [RFC6455], and, when `wss` is used, TLS [RFC8446]. + +Carrying QMux over WebSocket does not add or remove any QMux-level security property. +In particular, this binding provides no transport-layer confidentiality or integrity of its own; deployments that require those properties MUST use `wss` (WebSocket over TLS). + +The keep-alive mechanism in {{keepalive}} causes an endpoint to send periodic Ping frames. +An endpoint SHOULD bound the rate at which it sends and responds to Ping/Pong frames to avoid amplification or resource-exhaustion concerns. + +Because a server selects the subprotocol from a client-supplied list, a server MUST validate the selected identifier against its own supported set and MUST NOT echo an arbitrary client-supplied value, as required by [RFC6455]. + + +# IANA Considerations +This document has no IANA actions. + +This binding defines no subprotocol identifiers of its own: the `Sec-WebSocket-Protocol` value is the application protocol identifier, which is owned by the application protocol's specification (for example, [moqt]). +Whether those identifiers are registered in the WebSocket Subprotocol Name Registry [RFC6455] is therefore up to each application protocol, not this document. + + +--- back + +# Acknowledgments +{:numbered="false"} + +QMux is the work of the QUIC working group; this document only defines a WebSocket binding for it. +Thanks to the Media over QUIC working group for motivating a transport that works where UDP does not. From 838bbf44e9fd51693619bba20a243ef75c0a7933 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 19:39:17 -0700 Subject: [PATCH 2/3] Address review: trim abstract, clarify version selection, support datagrams - Abstract: drop the in-the-weeds Record/Size mechanism detail. - Subprotocol Identifier: state that the app protocol id selects the QMux version (moq-transport-18 -> qmux-01) with no separate version negotiation. - Datagrams: support all QMux features instead of prohibiting datagrams; they are negotiated and encoded as in qmux and carried in binary messages, delivered reliably/in-order as is inherent to a reliable byte-stream binding. Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-qmux-websocket.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/draft-lcurley-qmux-websocket.md b/draft-lcurley-qmux-websocket.md index fb777c5..a8ce480 100644 --- a/draft-lcurley-qmux-websocket.md +++ b/draft-lcurley-qmux-websocket.md @@ -30,7 +30,6 @@ informative: QMux [qmux] is a polyfill that runs QUIC applications over an ordered, reliable byte-stream transport such as TCP with TLS. This document defines a binding for QMux over WebSocket [RFC6455]. -The mapping is straightforward because both layers provide length-delimited messages over a reliable byte stream: a single WebSocket binary message carries exactly one QMux Record's frames, so the Record `Size` field is omitted. A WebSocket binding lets QUIC applications reach environments where UDP is blocked and where only an HTTP/WebSocket stack is available, including web browsers that lack WebTransport. --- middle @@ -86,8 +85,7 @@ WebSocket has no ALPN exchange, so this binding uses the WebSocket subprotocol n ## Subprotocol Identifier The subprotocol identifier is exactly the application protocol identifier that the application would use as its ALPN over native QUIC; for example `moq-transport-18`. -No QMux version token appears on the wire. -The application protocol identifier implies the QMux wire-format version per {{versions}}, so a single identifier selects both the application protocol and the wire format unambiguously. +The application protocol identifier also determines the QMux wire-format version — for example `moq-transport-18` indicates that `qmux-01` is to be used — so there is no separate QMux version negotiation (see {{versions}}). ## Client Behavior A client offers one or more application protocol identifiers in the `Sec-WebSocket-Protocol` request header, in decreasing order of preference. @@ -162,10 +160,11 @@ This keep-alive operates at the WebSocket layer and is separate from the QMux `m # Datagrams {#datagrams} -A WebSocket connection is reliable and ordered, so it cannot provide the unreliable, unordered delivery of the QUIC datagram extension. +QMux datagrams are supported. +They are negotiated and encoded exactly as in [qmux]: an endpoint advertises the datagram transport parameter and carries QMux DATAGRAM frames inside binary messages, like any other frame ({{framing}}). -An endpoint MUST NOT negotiate the QMux datagram transport parameter over this binding, and MUST NOT send QMux DATAGRAM frames. -An application that uses datagrams over native QUIC MUST provide an alternative over this binding (for example, by carrying the same payloads on a QUIC stream). +A WebSocket connection is reliable and ordered, so DATAGRAM frames carried over this binding are delivered reliably and in order rather than with the best-effort, drop-on-congestion semantics of the QUIC datagram extension. +This difference is inherent to running over a reliable byte-stream transport and applies equally to QMux over TCP or TLS; an application that depends on datagrams being droppable over native QUIC needs to account for it. # Connection Close {#close} From e04a0edc65388be72ed45a21f85d315e4c3090c3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 9 Jun 2026 19:51:34 -0700 Subject: [PATCH 3/3] Datagrams: drop redundant note on reliable delivery The loss of QUIC's unordered/unreliable semantics is inherent to qmux and already documented there; no need to restate it in this binding. Co-Authored-By: Claude Opus 4.8 (1M context) --- draft-lcurley-qmux-websocket.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/draft-lcurley-qmux-websocket.md b/draft-lcurley-qmux-websocket.md index a8ce480..b75cf73 100644 --- a/draft-lcurley-qmux-websocket.md +++ b/draft-lcurley-qmux-websocket.md @@ -163,9 +163,6 @@ This keep-alive operates at the WebSocket layer and is separate from the QMux `m QMux datagrams are supported. They are negotiated and encoded exactly as in [qmux]: an endpoint advertises the datagram transport parameter and carries QMux DATAGRAM frames inside binary messages, like any other frame ({{framing}}). -A WebSocket connection is reliable and ordered, so DATAGRAM frames carried over this binding are delivered reliably and in order rather than with the best-effort, drop-on-congestion semantics of the QUIC datagram extension. -This difference is inherent to running over a reliable byte-stream transport and applies equally to QMux over TCP or TLS; an application that depends on datagrams being droppable over native QUIC needs to account for it. - # Connection Close {#close} An endpoint terminates a QMux-over-WebSocket connection by sending a WebSocket Close frame [RFC6455] and then closing the underlying transport.