fix(audio-rooms): advertise moq-lite-03 in WT CONNECT sub-protocols
Without `wt-available-protocols`, moq-relay (`web-transport-quinn`) falls back to the legacy in-band SETUP exchange (moq-lite-02) instead of selecting the moq-lite-03 sub-protocol from the ALPN-style negotiation header. Then the relay tries to decode our first post-CONNECT bytes as a SETUP_CLIENT message, hits an unknown control type, and closes the QUIC connection with `connection closed err=invalid value` — surfaced client-side as a stuck SUBSCRIBE that ends with `subscribe stream FIN before reply for id=0` (the bidi gets FIN'd because the whole connection is being torn down). Pass `wt-available-protocols: "moq-lite-03"` on the Extended CONNECT request, encoded as an RFC 8941 Structured Field List of strings (the header format mandated by draft-ietf-webtrans-http3-14 §3.3). With this, moq-relay logs `negotiated version=moq-lite-03 transport="quic"` and the SUBSCRIBE makes it into the relay's actual moq-lite session pump. Mechanism: web-transport-proto's `ConnectRequest::encode` reads `self.protocols` and writes them as a comma-separated list of bare strings under `wt-available-protocols`. The server side (web-transport- quinn) reads the same header into `request.protocols`, and moq-native's `QuinnRequest::ok()` picks the first match against its supported ALPN list (`moq-lite-04`, `moq-lite-03`, `moq-00`, `moqt-15`, etc.). On match, version selection happens via the WT sub-protocol response and the in-band SETUP is skipped — which is what moq-lite-03 expects. Default the factory list to `["moq-lite-03"]`. Callers that want a different version (or to disable sub-protocol negotiation entirely to talk to a SETUP-based server) override the constructor parameter. Bare-metal harness: NostrNestsHarness.startExternal() now skips the TCP probe of the moq-relay port. moq-relay binds UDP only; the Docker forwarder happens to also open TCP, but a directly-launched binary doesn't, so the previous `Socket(host, 4443)` probe failed with ConnectException. The QUIC handshake from the test surfaces a real transport problem if any.
This commit is contained in:
+34
-1
@@ -79,6 +79,18 @@ class QuicWebTransportFactory(
|
||||
* the Extended CONNECT request stream before giving up with HandshakeFailed.
|
||||
*/
|
||||
private val connectTimeoutMillis: Long = 10_000L,
|
||||
/**
|
||||
* WebTransport sub-protocols to advertise on the Extended CONNECT request,
|
||||
* via the `wt-available-protocols` header (RFC 8941 Structured Field List
|
||||
* of strings — see draft-ietf-webtrans-http3-14 §3.3).
|
||||
*
|
||||
* For nests, this MUST contain `moq-lite-03`; without it, moq-relay falls
|
||||
* back to the legacy in-band SETUP exchange (moq-lite-02) and our first
|
||||
* post-CONNECT message is decoded as SETUP_CLIENT, producing
|
||||
* `connection closed err=invalid value` on the relay side and a stalled
|
||||
* subscribe / `subscribe stream FIN before reply` on the client side.
|
||||
*/
|
||||
private val webTransportSubProtocols: List<String> = listOf("moq-lite-03"),
|
||||
) : WebTransportFactory {
|
||||
override suspend fun connect(
|
||||
authority: String,
|
||||
@@ -136,7 +148,13 @@ class QuicWebTransportFactory(
|
||||
|
||||
// Open the Extended CONNECT request stream.
|
||||
val requestStream = conn.openBidiStream()
|
||||
val headers = buildExtendedConnectHeaders(authority, path, bearerToken)
|
||||
val extraHeaders =
|
||||
if (webTransportSubProtocols.isNotEmpty()) {
|
||||
listOf("wt-available-protocols" to encodeSfStringList(webTransportSubProtocols))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val headers = buildExtendedConnectHeaders(authority, path, bearerToken, extraHeaders)
|
||||
requestStream.send.enqueue(encodeHeadersFrame(headers))
|
||||
driver.wakeup()
|
||||
|
||||
@@ -214,6 +232,21 @@ class QuicWebTransportFactory(
|
||||
val status: Int,
|
||||
) : RuntimeException()
|
||||
|
||||
/**
|
||||
* Encode [items] as an RFC 8941 Structured Field List of bare strings
|
||||
* (the format `wt-available-protocols` requires per draft-ietf-webtrans-http3
|
||||
* §3.3). Each entry becomes `"value"`; the items are comma-separated.
|
||||
*
|
||||
* The values we emit (e.g. `moq-lite-03`) are bare ASCII so we don't
|
||||
* need to handle escaping — assert it instead of silently mis-emitting.
|
||||
*/
|
||||
private fun encodeSfStringList(items: List<String>): String {
|
||||
require(items.all { p -> p.all { it in 0x20.toChar()..0x7e.toChar() && it != '"' && it != '\\' } }) {
|
||||
"wt-available-protocols entry contains characters that need RFC 8941 escaping: $items"
|
||||
}
|
||||
return items.joinToString(", ") { "\"$it\"" }
|
||||
}
|
||||
|
||||
private fun splitAuthority(authority: String): Pair<String, Int> {
|
||||
val idx = authority.lastIndexOf(':')
|
||||
if (idx <= 0) return authority to 443
|
||||
|
||||
Reference in New Issue
Block a user