[proxy] Apply the upstream HTTP version before cloning transports (#7806)

createClientEntry and NewMultiTransport clone the secure transport into
its insecure variant before newUpstreamTransport applies the configured
HTTP version. http.Transport.Clone runs the source's one-time protocol
setup, and at that point ForceAttemptHTTP2 is still false while a custom
DialContext is set, so net/http disables HTTP/2 on the source for good.
Setting ForceAttemptHTTP2 afterwards has no effect.

As a result every TLS-verified upstream has been served over HTTP/1.1
since the upstream HTTP version became configurable, whatever
NB_PROXY_UPSTREAM_HTTP_VERSION says, while skip-TLS-verify upstreams
kept HTTP/2. gRPC upstreams break outright: unary calls get a 502 and
streaming calls hang until the client gives up.

Apply the version to the base transport before cloning it, and add a
test that the direct and insecure transports both offer h2.
This commit is contained in:
Brad Ison
2026-09-30 11:05:45 +02:00
committed by GitHub
parent 8edc120370
commit 8ab34fcf8b
3 changed files with 46 additions and 0 deletions
@@ -0,0 +1,40 @@
package roundtrip
import (
"net/http"
"testing"
log "github.com/sirupsen/logrus"
)
// offersHTTP2 reports whether t will negotiate h2 with a TLS upstream.
// Clone forces t's one-time protocol setup, which registers an "h2"
// handler in t.TLSNextProto only when HTTP/2 ended up enabled.
func offersHTTP2(t *http.Transport) bool {
_ = t.Clone()
_, ok := t.TLSNextProto["h2"]
return ok
}
func TestNewMultiTransportKeepsHTTP2AcrossClone(t *testing.T) {
for _, tc := range []struct {
version string
want bool
}{
{string(upstreamHTTPAuto), true},
{string(upstreamHTTP2), true},
{string(upstreamHTTP11), false},
} {
t.Run(tc.version, func(t *testing.T) {
t.Setenv(EnvUpstreamHTTPVersion, tc.version)
m := NewMultiTransport(noEmbeddedRoundTripper{}, log.New())
if got := offersHTTP2(m.direct.primary); got != tc.want {
t.Errorf("direct transport offers h2 = %v, want %v", got, tc.want)
}
if got := offersHTTP2(m.insecure.primary); got != tc.want {
t.Errorf("insecure transport offers h2 = %v, want %v", got, tc.want)
}
})
}
}
+3
View File
@@ -64,6 +64,9 @@ func NewMultiTransport(embedded http.RoundTripper, logger *log.Logger) *MultiTra
ReadBufferSize: cfg.readBufferSize,
DisableCompression: cfg.disableCompression,
}
// Clone runs the transport's one-time protocol setup, so the HTTP
// version must be applied first or the source loses HTTP/2 for good.
applyUpstreamHTTPVersion(direct, cfg.upstreamHTTPVersion)
insecure := direct.Clone()
insecure.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // matches the embedded NetBird transport's per-target opt-in
+3
View File
@@ -425,6 +425,9 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
ReadBufferSize: n.transportCfg.readBufferSize,
DisableCompression: n.transportCfg.disableCompression,
}
// Clone runs the transport's one-time protocol setup, so the HTTP
// version must be applied first or the source loses HTTP/2 for good.
applyUpstreamHTTPVersion(transport, n.transportCfg.upstreamHTTPVersion)
insecureTransport := transport.Clone()
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec