feat(httpconnect): add NewH2ProxyTransport for pure H2 CONNECT tunneling #589
feat(httpconnect): add NewH2ProxyTransport for pure H2 CONNECT tunneling #589
Conversation
Uses golang.org/x/net/http2.Transport directly, enabling: - h2c (cleartext H2 via prior knowledge) with WithPlainHTTP() — no TLS required - H2-TLS path with direct stdTLS.Client() handshake (required for http2.Transport type assertion to *tls.Conn for NegotiatedProtocol) - Multiple concurrent CONNECT tunnels multiplexed over one TCP connection Adds test case for h2c using http2.Server.ServeConn on a raw TCP listener. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plexing assertion Verifies the TLS path of NewH2ProxyTransport by opening 3 concurrent CONNECT tunnels and asserting that only 1 TCP connection was established to the proxy. Uses transport.FuncStreamDialer to count connections at the dial level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace table-driven test with standalone test functions per transport variant
- Consistent naming: Test_ConnectClient_{protocol}_{modifier}
- Extract verifyTunnel() and newH2ConnectHandler() helpers to eliminate duplication
- Replace generateRootCA() with tlsCertPool() backed by httptest's built-in cert
- Add doc comments to all test functions, helpers, and types
- Use t.Cleanup instead of defer throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds NewH2ProxyTransport, a new transport factory that uses golang.org/x/net/http2.Transport directly (instead of wrapping net/http.Transport with http2.ConfigureTransport). This enables two new capabilities not available in the existing NewHTTPProxyTransport: true H2 multiplexing (multiple concurrent CONNECT tunnels over a single TCP connection) and h2c (cleartext HTTP/2 via prior knowledge) via WithPlainHTTP(). The PR also refactors the existing test suite from a single table-driven test into standalone, well-documented test functions.
Changes:
NewH2ProxyTransportfactory function supporting both h2c (plaintext) and TLS modes, with correct*tls.Connreturn inDialTLSContextto satisfyhttp2.Transport's internal type assertion- Two new tests:
Test_ConnectClient_H2C(h2c via raw TCP +http2.Server.ServeConn) andTest_ConnectClient_H2_TLS_Multiplexed(asserts 3 concurrent streams share 1 TCP connection using a counting dialer) - Test refactor: table-driven test split into standalone functions, shared helpers extracted (
verifyTunnel,newH2ConnectHandler,tlsCertPool), custom RSA CA generation removed in favor ofhttptest's built-in certificate
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
x/httpconnect/transport.go |
Adds NewH2ProxyTransport with h2c and TLS paths using golang.org/x/net/http2.Transport |
x/httpconnect/connect_client_test.go |
Refactors tests to standalone functions; adds Test_ConnectClient_H2C and Test_ConnectClient_H2_TLS_Multiplexed |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
- Detect http.Hijacker to branch between H1 (hijack+bufio) and H2/H3 (ResponseWriter with explicit flushing) relay strategies - Share relay logic after the branch: client→target goroutine with ReadFrom-preferred copy, target→client via clientWriter.ReadFrom - Add flushingWriter for H2/H3: Write flushes after every call; ReadFrom shadows http.ResponseWriter's own ReadFrom (which doesn't flush) and prefers r.WriteTo to avoid an intermediate buffer - Remove newH2ConnectHandler from httpconnect tests now that httpproxy.NewConnectHandler supports H2/H3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…me NewHTTP3ProxyTransport to NewH3ProxyTransport Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Register HTTP CONNECT proxy transports in the configurl package: - httpconnect://host:port — H1.1 or H2 via ALPN (multiplexed when H2 is negotiated) - h2connect://host:port — pure H2, always multiplexed; supports h2c via ?plain=true - h3connect://host:port — H3/QUIC, always multiplexed All three support ?sni= and ?certname= query parameters for TLS configuration. Includes an end-to-end test exercising h2connect over h2c. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
x/httpconnect/connect_client_test.go:454
- The
flusherWritertype (lines 427–454) is defined but no longer used by any test in the refactored test suite. It was only used in the old inline H2 test-server code that has been replaced byhttpproxy.NewConnectHandler. This dead code should be removed to keep the file clean.
// flusherWriter wraps an http.ResponseWriter so that every Write is followed by a Flush.
// This is required when relaying data over an H2 response stream: without explicit flushing,
// written bytes sit in the buffer and the remote end never receives them.
type flusherWriter struct {
http.Flusher
io.Writer
}
func (fw flusherWriter) ReadFrom(r io.Reader) (int64, error) {
var (
buf = make([]byte, 32*1024)
total int64
)
for {
nr, er := r.Read(buf)
if nr > 0 {
nw, ew := fw.Writer.Write(buf[:nr])
total += int64(nw)
if ew != nil {
return total, ew
}
fw.Flush()
}
if er != nil {
if er == io.EOF {
return total, nil
}
return total, er
}
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
…Parser via option NewConnectHandler no longer imports configurl. The Transport header feature is now opt-in via WithStreamDialerParser, resolving the import cycle httpconnect → httpproxy → configurl → httpconnect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- configurl/doc.go: replace broken [WithPlainHTTP] godoc link with plain text - httpconnect/connect_client_test.go: fix Test_ConnectClient_H2_TLS comment (it uses NewH2ProxyTransport, not NewHTTPProxyTransport) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… builder list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Adds HTTP CONNECT proxy tunnel support to the
httpconnectpackage and integrates it intoconfigurl.x/httpconnect— new HTTP CONNECT tunnel client over H1, H2, and H3:NewHTTPProxyTransport— H1.1 or H2 via TLS ALPN (multiplexed when H2 is negotiated)NewH2ProxyTransport— pure HTTP/2; always multiplexed; supports h2c (cleartext) viaWithPlainHTTP()NewH3ProxyTransport— HTTP/3 over QUIC; always multiplexed (renamed fromNewHTTP3ProxyTransport)NewConnectClient—transport.StreamDialerthat sends CONNECT requests through any of the abovehttpproxy.NewConnectHandlerto support H2/H3 streams (no longer requireshttp.Hijacker)x/configurl— three new transport types:httpconnect://host:port[?sni=SNI][&certname=CERTNAME]h2connect://host:port[?sni=SNI][&certname=CERTNAME][&plain=true]h3connect://host:port[?sni=SNI][&certname=CERTNAME]This improves #319
TLS path correctness
When TLS is used,
DialTLSContextperforms the handshake usingstdTLS.Client()and returns*stdTLS.Conndirectly. This is required becausehttp2.Transportinternally type-asserts to*tls.Connto readNegotiatedProtocol— returning the SDK'stls.WrapConnwrapper wouldsilently break protocol negotiation.
Tests
New tests covering all new paths, plus a refactor of the existing test suite:
Test_ConnectClient_H2CNewH2ProxyTransport+WithPlainHTTP(), served byhttp2.Server.ServeConnon a raw TCP listenerTest_ConnectClient_H2_TLS_MultiplexedNewH2ProxyTransport; asserts 3 concurrent CONNECT streams share exactly 1 TCP connection using a counting dialerThe existing test file was also refactored for readability:
(
Test_ConnectClient_{protocol}_{modifier})verifyTunnel()andnewH2ConnectHandler()helpers to eliminate duplicationhttptest's built-in certificate viaproxySrv.Certificate()