Summary
A native visor's WebTransport carrier dial to a dmsg server is rejected by the server with CRYPTO_ERROR 0x178 (remote): tls: no application protocol, while an identical dial from a clean standalone process to the same server at the same moment succeeds. So the WT carrier never wins convergence on those servers — the visor falls back to the quic carrier.
This is distinct from servers that are genuinely UDP-unreachable (a container mapping only TCP): those fail WT and quic for everyone.
Evidence
Same server (0371ab4b… @ 45.79.213.251:30087), seconds apart, using the new per-carrier probe (PR #3835):
Clean standalone client — skywire cli svc health --dmsg-server 0371ab4b… --carriers wt,quic,tcp:
CARRIER STATUS LATENCY ADDR
wt OK 116ms https://45.79.213.251:30087/dmsg
quic FAIL - 45.79.213.251:30087 (fresh-socket quic timed out)
tcp OK 71ms 45.79.213.251:30087
In-visor — skywire cli dmsg converge --carriers wt,quic,ws,tcp Note:
0371ab4b tcp 45.79.213.251:30087 (prefers wt)
↳ re-dial to wt failed: quic: dial 45.79.213.251:30087:
CRYPTO_ERROR 0x178 (remote): tls: no application protocol
Ruled out: the ALPN constant (skyquic.WebTransportNextProto == http3.NextProtoH3 == "h3"; webtransport-go only sets NextProtos when empty, so it stays ["h3"]), the pinned cert hash (the error is ALPN, not a cert mismatch), reachability, and backoff. dialSessionWT is identical code in both processes — the divergence is process-global.
Leading hypothesis
For these servers AddressWT (https://IP:30087/dmsg) and AddressUDP (IP:30087) are the same UDP host:port — the dmsg server serves the quic carrier and WebTransport on one UDP socket, ALPN-demuxed. The visor already holds a quic-carrier connection to that endpoint (ALPN skywire-quic-1); its later WT dial to the same UDP endpoint appears to negotiate the wrong ALPN (quic-go connection coalescing / shared packet-routing by server address), so the server's WT side sees no h3 and rejects. A clean process, with no pre-existing quic-carrier connection there, negotiates h3 fine. The inverse also holds: in-visor quic-OK/wt-FAIL vs clean wt-OK/quic-FAIL on the same port.
Reframing
Where AddressWT and AddressUDP are the same UDP endpoint, WebTransport-over-quic and the quic carrier are the same underlying transport to the same socket — preferring wt over quic gains a native visor nothing. WT preference is meaningful for browsers (which can't speak the raw quic carrier) and for servers that serve WT on a distinct port.
Options
- Serve WT on a distinct port from the quic carrier — no shared UDP socket, no ALPN collision. Cleanest, but the reporter would prefer to avoid it if the same-port case can be made to work.
- Make convergence co-location-aware: skip the wt-over-quic "upgrade" when
AddressWT and AddressUDP resolve to the same UDP endpoint (kills the failing churn; keeps WT preference meaningful for browsers / distinct-port servers).
- Investigate the quic-go coalescing mechanism to force the in-visor WT dial onto a fresh handshake so same-port WT+quic both work.
Repro
skywire cli svc health --dmsg-server <pk> --carriers wt,quic,ws,tcp (clean per-carrier) vs skywire cli dmsg converge --carriers wt,quic,ws,tcp (in-visor, Note carries the real per-carrier error). Both from PR #3835.
Note: the wasm/browser visor is a separate question — it makes no quic-carrier connections at all (browser JS WebTransport only), so this collision cannot explain a wasm WS→WT failure; that remains independently open.
Summary
A native visor's WebTransport carrier dial to a dmsg server is rejected by the server with
CRYPTO_ERROR 0x178 (remote): tls: no application protocol, while an identical dial from a clean standalone process to the same server at the same moment succeeds. So the WT carrier never wins convergence on those servers — the visor falls back to the quic carrier.This is distinct from servers that are genuinely UDP-unreachable (a container mapping only TCP): those fail WT and quic for everyone.
Evidence
Same server (
0371ab4b… @ 45.79.213.251:30087), seconds apart, using the new per-carrier probe (PR #3835):Clean standalone client —
skywire cli svc health --dmsg-server 0371ab4b… --carriers wt,quic,tcp:In-visor —
skywire cli dmsg converge --carriers wt,quic,ws,tcpNote:Ruled out: the ALPN constant (
skyquic.WebTransportNextProto == http3.NextProtoH3 == "h3"; webtransport-go only setsNextProtoswhen empty, so it stays["h3"]), the pinned cert hash (the error is ALPN, not a cert mismatch), reachability, and backoff.dialSessionWTis identical code in both processes — the divergence is process-global.Leading hypothesis
For these servers
AddressWT(https://IP:30087/dmsg) andAddressUDP(IP:30087) are the same UDP host:port — the dmsg server serves the quic carrier and WebTransport on one UDP socket, ALPN-demuxed. The visor already holds a quic-carrier connection to that endpoint (ALPNskywire-quic-1); its later WT dial to the same UDP endpoint appears to negotiate the wrong ALPN (quic-go connection coalescing / shared packet-routing by server address), so the server's WT side sees noh3and rejects. A clean process, with no pre-existing quic-carrier connection there, negotiatesh3fine. The inverse also holds: in-visor quic-OK/wt-FAIL vs clean wt-OK/quic-FAIL on the same port.Reframing
Where
AddressWTandAddressUDPare the same UDP endpoint, WebTransport-over-quic and the quic carrier are the same underlying transport to the same socket — preferringwtoverquicgains a native visor nothing. WT preference is meaningful for browsers (which can't speak the raw quic carrier) and for servers that serve WT on a distinct port.Options
AddressWTandAddressUDPresolve to the same UDP endpoint (kills the failing churn; keeps WT preference meaningful for browsers / distinct-port servers).Repro
skywire cli svc health --dmsg-server <pk> --carriers wt,quic,ws,tcp(clean per-carrier) vsskywire cli dmsg converge --carriers wt,quic,ws,tcp(in-visor, Note carries the real per-carrier error). Both from PR #3835.Note: the wasm/browser visor is a separate question — it makes no quic-carrier connections at all (browser JS WebTransport only), so this collision cannot explain a wasm WS→WT failure; that remains independently open.