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.
|
* the Extended CONNECT request stream before giving up with HandshakeFailed.
|
||||||
*/
|
*/
|
||||||
private val connectTimeoutMillis: Long = 10_000L,
|
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 {
|
) : WebTransportFactory {
|
||||||
override suspend fun connect(
|
override suspend fun connect(
|
||||||
authority: String,
|
authority: String,
|
||||||
@@ -136,7 +148,13 @@ class QuicWebTransportFactory(
|
|||||||
|
|
||||||
// Open the Extended CONNECT request stream.
|
// Open the Extended CONNECT request stream.
|
||||||
val requestStream = conn.openBidiStream()
|
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))
|
requestStream.send.enqueue(encodeHeadersFrame(headers))
|
||||||
driver.wakeup()
|
driver.wakeup()
|
||||||
|
|
||||||
@@ -214,6 +232,21 @@ class QuicWebTransportFactory(
|
|||||||
val status: Int,
|
val status: Int,
|
||||||
) : RuntimeException()
|
) : 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> {
|
private fun splitAuthority(authority: String): Pair<String, Int> {
|
||||||
val idx = authority.lastIndexOf(':')
|
val idx = authority.lastIndexOf(':')
|
||||||
if (idx <= 0) return authority to 443
|
if (idx <= 0) return authority to 443
|
||||||
|
|||||||
+7
-1
@@ -222,8 +222,14 @@ class NostrNestsHarness private constructor(
|
|||||||
private fun startExternal(): NostrNestsHarness {
|
private fun startExternal(): NostrNestsHarness {
|
||||||
try {
|
try {
|
||||||
waitForPort("127.0.0.1", AUTH_HOST_PORT, PORT_READY_TIMEOUT_MS)
|
waitForPort("127.0.0.1", AUTH_HOST_PORT, PORT_READY_TIMEOUT_MS)
|
||||||
waitForPort("127.0.0.1", MOQ_HOST_PORT, PORT_READY_TIMEOUT_MS)
|
|
||||||
waitForHealth("http://127.0.0.1:$AUTH_HOST_PORT/health", PORT_READY_TIMEOUT_MS)
|
waitForHealth("http://127.0.0.1:$AUTH_HOST_PORT/health", PORT_READY_TIMEOUT_MS)
|
||||||
|
// moq-relay is UDP only — the Docker compose forwarder
|
||||||
|
// happens to open TCP on 4443 too, which is what the
|
||||||
|
// Docker-mode probe relies on. Bare-metal moq-relay
|
||||||
|
// doesn't, so a TCP probe fails with ConnectException
|
||||||
|
// even though the UDP listener is healthy. Skip it
|
||||||
|
// here; the actual QUIC handshake from the test will
|
||||||
|
// surface a real connection problem if there is one.
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
"external moq-auth / moq-relay not reachable on 127.0.0.1:" +
|
"external moq-auth / moq-relay not reachable on 127.0.0.1:" +
|
||||||
|
|||||||
Reference in New Issue
Block a user