feat(websocket): share port with HTTP handler#3509
Conversation
Add WithFallbackHTTPHandler so /ws and /tls/ws listeners can serve a caller-supplied http.Handler for every request that is not a WebSocket upgrade. Lets a libp2p node share its TCP port with a regular HTTP/HTTPS site (censorship resistance, single-port deployments, HTTP-native retrieval endpoints next to libp2p WebSockets). - option + listener wiring with handshake-timer disarm so HTTP/2 streams and HTTP/1.1 keep-alive survive past the 15s default - HTTP/2 over TLS via http.Server.ServeTLS ALPN; HTTP/2 cleartext via h2c.NewHandler wrap when the listener is plaintext - tcpreuse IsHTTP gains PRI so the h2c connection preface routes to the HTTP demux branch - transport stays interop-maximal: any HTTP version on any listener; application-level version policy is the wrapped handler's call - 9 fallback handler tests, modern-browser wss:// flow with raw h2 framer SETTINGS check, h2 ext-CONNECT regression, h2 keep-alive disarm
Two test handlers intentionally ignore one of (w, r); revive flagged them in PR #3509's go-check job.
negotiatingConn.Unwrap read and wrote stopClose without synchronization and treated context.AfterFunc's stop returning false as a timeout. Under HTTP/2 multiplexing, several streams on one TCP connection share a single *negotiatingConn, so concurrent Unwrap calls raced on stopClose and the losers silently dropped their requests. Wrap the disarm in sync.Once and record the AfterFunc outcome so all callers observe the same result. Add a unit test on the disarm contract and an integration test that drives concurrent h2 streams through the fallback handler, both run under -race.
|
@MarcoPolo @sukunrt kind ping to put this on the top of review queue. The foundations of It also adds opt-in |
Pulls in Boxo v0.40.0, go-libp2p-kad-dht v0.40.0, cheggaaa/pb v3, and the dag --local-only / migration fetcher work from master. Dep conflicts resolved by keeping our HTTPProvider-required pins on top of master's baseline: - certmagic v0.25.3 (pulled in by p2p-forge v0.9.0) - p2p-forge v0.9.0 - go-libp2p v0.48.1-0.20260515215300-a72c0588b088 (pin for libp2p/go-libp2p#3509, still open; needed for websocket.WithFallbackHTTPHandler) Other transitive bumps follow from `make mod_tidy`. AutoTLS canary + HTTPProvider CLI tests pass on the merged tree.
| hostPort := startWSSListener(t, handler) | ||
|
|
||
| // Force ALPN to h2 only, so the connection MUST be HTTP/2 or fail. | ||
| resp, err := httpsClient([]string{"h2"}).Get("https://" + hostPort + "/") |
There was a problem hiding this comment.
I think you need an extended connect request here to make it fail if gorilla/ws implements h2 websocket support.
There was a problem hiding this comment.
Good catch @sukunrt, you're right. A plain GET never trips IsWebSocketUpgrade. Improved and it now sends a real extended CONNECT.
One wrinkle worth noting: the bundled h2 server resets the stream before the handler ever sees an extended CONNECT unless it advertises ENABLE_CONNECT_PROTOCOL (golang/go#71128), which is gated by GODEBUG=http2xconnect=1 and read once at startup. So the test re-execs itself in a child with that set, sends the extended CONNECT over raw frames, and asserts it reaches the fallback handler. Confirmed it goes red if dispatch ever routes extended CONNECT to the WS upgrade path.
See 9dce1ca. Not a fan, but unsure if this could be done in a better way, given golang limitations.
Strengthen the HTTP/2-never-reaches-WS test so it actually proves the invariant it documents. A plain h2 GET can never be a WebSocket upgrade, so the old test would not have caught a gorilla/websocket bump that teaches IsWebSocketUpgrade about RFC 8441 extended CONNECT. - send a real extended CONNECT (:method=CONNECT, :protocol=websocket) over raw h2 frames and assert it reaches the fallback handler - re-exec the test in a child with GODEBUG=http2xconnect=1, since the bundled h2 server otherwise rejects extended CONNECT before any handler runs and the setting is read once at startup - bound the child with a context timeout, merge into any inherited GODEBUG, and require the child to report a pass so a zero-match -test.run cannot pass silently
Adds
websocket.WithFallbackHTTPHandler(http.Handler)so a/wsor/tls/wslistener answers non-WebSocket requests with the supplied handler instead of 404, on the same TCP port.👉 Opt-in, backward-compatible, default behaviour is unchanged, but go-libp2p gains important regression tests.
Two uses
/tls/wsport. HTTP retrieval clients (boxo/bitswap/network/httpnet) fetch blocks directly over HTTPS using the AutoTLS cert. Improved interop of IPFS stack.Routing
WebSocket upgrades go to libp2p as before; everything else goes to the handler. The handler runs over HTTP/1.1 and HTTP/2 on TLS listeners (ALPN inside
http.Server.ServeTLS) and over HTTP/1.1 and HTTP/2 cleartext on plaintext listeners (h2c.NewHandler). Also intcpreuse:IsHTTPmatches thePRIh2c preface so it routes to the HTTP demux branch.Important
End-to-end example in ipfs/kubo#11333, which shares kubo's AutoTLS-secured
/tls/wsand a/tls/httpendpoint on one Let's Encrypt cert:core/node/libp2p/transport.goL46-L58/tls/ws(h2-only over TLS):cmd/ipfs/kubo/daemon.goL1256-L1262test/autotls/canary_test.goRegression tests
👉 tests added here protect ecosystem from silently breaking
/ws.gorilla/websocket has no HTTP/2 / RFC 8441 server support today. WebSocket-over-h2 needs extended CONNECT (
:method=CONNECT, :protocol=websocket); gorilla'sUpgrader.Upgradestill reads HTTP/1.1 hijack-based headers, and Go's h2 stack does not advertiseSETTINGS_ENABLE_CONNECT_PROTOCOL(gated on process-globalGODEBUG=http2xconnect=1; per-server API tracked ingolang/go#71128). Two tripwires added by this PR catch any future upstream shift so go-libp2p maintainers can deliberately embrace WS-over-h2 or pin the dispatch:TestFallbackHTTPHandler_HTTP2NeverReachesWSPath: every HTTP/2 request reaches the fallback handler, never the WebSocket hijack path. Fails if a future gorilla release extendsIsWebSocketUpgradeto recognise ext-CONNECT.TestModernBrowserWSSFlow: the server's initial h2 SETTINGS frame omitsSETTINGS_ENABLE_CONNECT_PROTOCOL. Browsers rely on its absence to fall back to HTTP/1.1 forwss://; the test fails if Go ever flips this flag on by default.Plus a concurrency pin for the new fallback path:
TestFallbackHTTPHandler_KeepAlive+TestNegotiatingConnUnwrapConcurrent: the handshake-timer is disarmed once a fallback request arrives, safely under concurrent HTTP/2 streams sharing one*negotiatingConn.