|
| 1 | +--- |
| 2 | +title: "QMux over WebSocket" |
| 3 | +abbrev: "qmux-ws" |
| 4 | +category: info |
| 5 | + |
| 6 | +docname: draft-lcurley-qmux-websocket-latest |
| 7 | +submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF" |
| 8 | +number: |
| 9 | +date: |
| 10 | +v: 3 |
| 11 | +area: wit |
| 12 | +workgroup: moq |
| 13 | + |
| 14 | +author: |
| 15 | + - |
| 16 | + fullname: Luke Curley |
| 17 | + email: kixelated@gmail.com |
| 18 | + |
| 19 | +normative: |
| 20 | + qmux: I-D.ietf-quic-qmux |
| 21 | + RFC6455: |
| 22 | + RFC9000: |
| 23 | + |
| 24 | +informative: |
| 25 | + RFC8446: |
| 26 | + RFC9220: |
| 27 | + moqt: I-D.ietf-moq-transport |
| 28 | + |
| 29 | +--- abstract |
| 30 | + |
| 31 | +QMux [qmux] is a polyfill that runs QUIC applications over an ordered, reliable byte-stream transport such as TCP with TLS. |
| 32 | +This document defines a binding for QMux over WebSocket [RFC6455]. |
| 33 | +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. |
| 34 | + |
| 35 | +--- middle |
| 36 | + |
| 37 | +# Conventions and Definitions |
| 38 | +{::boilerplate bcp14-tagged} |
| 39 | + |
| 40 | +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]. |
| 41 | + |
| 42 | + |
| 43 | +# Introduction |
| 44 | +QMux [qmux] lets an application written against the QUIC stream and datagram API run over an ordered, reliable byte-stream transport. |
| 45 | +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. |
| 46 | + |
| 47 | +A WebSocket binding is nevertheless useful. |
| 48 | +WebSocket [RFC6455] is available in essentially every deployment environment, including: |
| 49 | + |
| 50 | +- Networks where UDP (and therefore QUIC) is blocked by a firewall or middlebox. |
| 51 | +- Web browsers, which expose a WebSocket API but do not universally expose a WebTransport or raw-socket API. |
| 52 | +- HTTP load balancers and proxies that can route and terminate WebSocket but not raw TCP or QUIC. |
| 53 | + |
| 54 | +This document specifies how to carry QMux over WebSocket. |
| 55 | +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. |
| 56 | +All other QMux semantics — in-order STREAM frame delivery, stream identifiers, flow control, transport parameters, and connection close — apply unchanged from [qmux]. |
| 57 | + |
| 58 | +This binding is application agnostic: any QUIC application that can run over QMux can run over QMux over WebSocket. |
| 59 | +Media over QUIC Transport [moqt] is one such application and is the motivating use case, but nothing in this document is specific to it. |
| 60 | + |
| 61 | + |
| 62 | +# WebSocket Binding Overview |
| 63 | +A QMux-over-WebSocket connection is an ordinary WebSocket connection [RFC6455] whose binary messages carry QMux frames. |
| 64 | + |
| 65 | +Both the QMux Record layer and the WebSocket message layer provide self-delimiting messages over a reliable, ordered byte stream. |
| 66 | +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. |
| 67 | + |
| 68 | +The WebSocket connection takes the place of the underlying byte-stream transport in [qmux]. |
| 69 | +Once the WebSocket handshake completes, each endpoint sends and receives QMux frames inside WebSocket binary messages as defined below. |
| 70 | + |
| 71 | + |
| 72 | +# Establishing a Connection |
| 73 | +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). |
| 74 | + |
| 75 | +The `ws` URI scheme is used over an unencrypted transport and the `wss` URI scheme is used over a TLS-encrypted transport. |
| 76 | +Deployments SHOULD use `wss`; an application that expects a TLS transport when running natively over QUIC SHOULD require `wss` here. |
| 77 | + |
| 78 | +How the underlying connection is authenticated and authorized is out of scope for this document, as it is for [qmux]. |
| 79 | + |
| 80 | + |
| 81 | +# Subprotocol Negotiation |
| 82 | +QMux over TCP/TLS uses TLS ALPN [RFC8446] to agree on the application protocol. |
| 83 | +The QMux wire-format version is *not* negotiated separately: it is determined by the negotiated application protocol, as described in {{versions}}. |
| 84 | +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. |
| 85 | + |
| 86 | +## Subprotocol Identifier |
| 87 | +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`. |
| 88 | +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}}). |
| 89 | + |
| 90 | +## Client Behavior |
| 91 | +A client offers one or more application protocol identifiers in the `Sec-WebSocket-Protocol` request header, in decreasing order of preference. |
| 92 | +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`). |
| 93 | + |
| 94 | +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]. |
| 95 | + |
| 96 | +## Server Behavior |
| 97 | +A server selects at most one of the client's offered identifiers and echoes it in the `Sec-WebSocket-Protocol` response header. |
| 98 | +A server MUST NOT select an identifier the client did not offer. |
| 99 | + |
| 100 | +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. |
| 101 | +If the server supports none of the offered identifiers, it MUST fail the handshake. |
| 102 | + |
| 103 | +The selected identifier determines both the application protocol and, via {{versions}}, the QMux wire-format version for the connection. |
| 104 | + |
| 105 | + |
| 106 | +# Record Framing {#framing} |
| 107 | +Each WebSocket binary message carries exactly one QMux Record's `Frames` field: one or more QMux frames concatenated, as defined in [qmux]. |
| 108 | +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. |
| 109 | + |
| 110 | +A QMux-over-WebSocket record is therefore: |
| 111 | + |
| 112 | +~~~ |
| 113 | +WebSocket Binary Message { |
| 114 | + Frames (..), |
| 115 | +} |
| 116 | +~~~ |
| 117 | + |
| 118 | +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. |
| 119 | + |
| 120 | +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. |
| 121 | +An empty binary message (zero frames) is permitted and carries no frames; a receiver MUST accept it and treat it as a no-op. |
| 122 | + |
| 123 | +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. |
| 124 | +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. |
| 125 | + |
| 126 | + |
| 127 | +# WebSocket Message Types |
| 128 | +This binding uses WebSocket message and control frames as follows: |
| 129 | + |
| 130 | +- *Binary messages* carry QMux frames as defined in {{framing}}. |
| 131 | +- *Text messages* MUST NOT be sent. A receiver MUST treat a text message as a connection error and close the WebSocket connection. |
| 132 | +- *Close frames* terminate the connection as defined in {{close}}. |
| 133 | +- *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. |
| 134 | + |
| 135 | +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. |
| 136 | + |
| 137 | + |
| 138 | +# QMux Version {#versions} |
| 139 | +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}}). |
| 140 | + |
| 141 | +The QMux version is not signaled on the wire and is not carried in the subprotocol identifier. |
| 142 | +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. |
| 143 | +For example, Media over QUIC Transport [moqt] identifier `moq-transport-18` uses [qmux]. |
| 144 | + |
| 145 | +An application protocol used with this binding MUST select a QMux version that provides the Record layer, i.e. [qmux] or later. |
| 146 | + |
| 147 | + |
| 148 | +# Keep-Alive and Idle Timeout {#keepalive} |
| 149 | +QUIC and QMux detect a dead peer with an idle timeout. |
| 150 | +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. |
| 151 | + |
| 152 | +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. |
| 153 | +The timeout SHOULD be a small multiple of the ping interval to tolerate transient delays. |
| 154 | +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. |
| 155 | + |
| 156 | +Receipt of any WebSocket frame from the peer — binary, Ping, or Pong — resets the idle timer. |
| 157 | +An endpoint replies to a Ping with a Pong per [RFC6455]; this is handled by the WebSocket layer and is independent of QMux frames. |
| 158 | + |
| 159 | +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. |
| 160 | + |
| 161 | + |
| 162 | +# Datagrams {#datagrams} |
| 163 | +QMux datagrams are supported. |
| 164 | +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}}). |
| 165 | + |
| 166 | + |
| 167 | +# Connection Close {#close} |
| 168 | +An endpoint terminates a QMux-over-WebSocket connection by sending a WebSocket Close frame [RFC6455] and then closing the underlying transport. |
| 169 | + |
| 170 | +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. |
| 171 | +Because the WebSocket layer provides its own connection close, there is no draining period: an endpoint MAY close immediately after sending its Close frame. |
| 172 | + |
| 173 | +Receipt of a WebSocket Close frame, or loss of the underlying transport, terminates the QMux connection and all of its streams. |
| 174 | + |
| 175 | + |
| 176 | +# Security Considerations |
| 177 | +This binding inherits the security considerations of QMux [qmux], WebSocket [RFC6455], and, when `wss` is used, TLS [RFC8446]. |
| 178 | + |
| 179 | +Carrying QMux over WebSocket does not add or remove any QMux-level security property. |
| 180 | +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). |
| 181 | + |
| 182 | +The keep-alive mechanism in {{keepalive}} causes an endpoint to send periodic Ping frames. |
| 183 | +An endpoint SHOULD bound the rate at which it sends and responds to Ping/Pong frames to avoid amplification or resource-exhaustion concerns. |
| 184 | + |
| 185 | +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]. |
| 186 | + |
| 187 | + |
| 188 | +# IANA Considerations |
| 189 | +This document has no IANA actions. |
| 190 | + |
| 191 | +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]). |
| 192 | +Whether those identifiers are registered in the WebSocket Subprotocol Name Registry [RFC6455] is therefore up to each application protocol, not this document. |
| 193 | + |
| 194 | + |
| 195 | +--- back |
| 196 | + |
| 197 | +# Acknowledgments |
| 198 | +{:numbered="false"} |
| 199 | + |
| 200 | +QMux is the work of the QUIC working group; this document only defines a WebSocket binding for it. |
| 201 | +Thanks to the Media over QUIC working group for motivating a transport that works where UDP does not. |
0 commit comments