mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
ForceAttemptHTTP2 only puts h2 in the ALPN offer — the upstream still picks — so "auto" already meant "whatever the upstream chose". What ALPN cannot express is an upstream that selects h2 and then fails to speak it, which is the case the setting was added for: today that leaves the operator pinning every upstream to 1.1 to work around one broken backend. Auto now completes itself. The first h2-level failure for a host pins that host to an HTTP/1.1-only clone of its transport for 10 minutes and retries the request there when the body can be replayed, so a broken backend costs one failed h2 attempt instead of a configuration change. The pin is per upstream host, so one broken backend does not drop the others, and it expires so a fixed backend returns to h2 on its own. Only h2 framing errors trigger it: a dial, TLS or context error says nothing about the protocol and retrying it over HTTP/1.1 would fix nothing. The explicit values stay absolute — "2" never downgrades. Pinning HTTP/1.1 now also strips h2 from the ALPN offer. Configuring h2 makes net/http append it to the transport's TLSClientConfig, so a clone taken from a transport that already served a request would otherwise advertise a protocol the clone refuses to speak, and the reply would come back as h2 frames parsed as an HTTP/1.1 message.
112 lines
4.3 KiB
Go
112 lines
4.3 KiB
Go
package roundtrip
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// MultiTransport dispatches each request to either the embedded NetBird
|
|
// http.RoundTripper or a stdlib http.Transport based on a per-request
|
|
// context flag set by the reverse-proxy rewrite step. When the flag is
|
|
// absent (the default for every existing target), requests follow the
|
|
// embedded NetBird path — current behaviour, preserved.
|
|
//
|
|
// The stdlib branch is used when a target was configured with
|
|
// direct_upstream=true. It dials via the host's network stack, which is
|
|
// what private (`netbird proxy`) deployments and centralised proxies
|
|
// fronting host-reachable upstreams (public APIs, LAN services,
|
|
// localhost sidecars) want.
|
|
//
|
|
// An embedded roundtripper is required. To run direct-only (no WG
|
|
// branch at all), construct the MultiTransport via NewDirectOnly.
|
|
type MultiTransport struct {
|
|
embedded http.RoundTripper
|
|
direct *upstreamTransport
|
|
insecure *upstreamTransport
|
|
}
|
|
|
|
// errNoEmbeddedTransport is returned when a request reaches the
|
|
// embedded branch on a MultiTransport that wasn't given one. Surfaces
|
|
// the misconfiguration to the caller instead of silently routing to
|
|
// the direct branch, which would bypass the WG tunnel.
|
|
var errNoEmbeddedTransport = errors.New("multitransport: embedded roundtripper not configured")
|
|
|
|
// NewMultiTransport wires both branches. embedded is the existing NetBird
|
|
// roundtripper and must not be nil — pass to NewDirectOnly for a
|
|
// MultiTransport that only ever uses the direct branch. The direct
|
|
// branches honour the same NB_PROXY_* tuning env vars as the embedded
|
|
// transport (see loadTransportConfig) plus a dial-timeout wrapper that
|
|
// respects types.WithDialTimeout.
|
|
func NewMultiTransport(embedded http.RoundTripper, logger *log.Logger) *MultiTransport {
|
|
if logger == nil {
|
|
logger = log.StandardLogger()
|
|
}
|
|
cfg := loadTransportConfig(logger)
|
|
dialer := &net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}
|
|
direct := &http.Transport{
|
|
DialContext: dialWithTimeout(dialer.DialContext),
|
|
MaxIdleConns: cfg.maxIdleConns,
|
|
MaxIdleConnsPerHost: cfg.maxIdleConnsPerHost,
|
|
MaxConnsPerHost: cfg.maxConnsPerHost,
|
|
IdleConnTimeout: cfg.idleConnTimeout,
|
|
TLSHandshakeTimeout: cfg.tlsHandshakeTimeout,
|
|
ExpectContinueTimeout: cfg.expectContinueTimeout,
|
|
ResponseHeaderTimeout: cfg.responseHeaderTimeout,
|
|
WriteBufferSize: cfg.writeBufferSize,
|
|
ReadBufferSize: cfg.readBufferSize,
|
|
DisableCompression: cfg.disableCompression,
|
|
}
|
|
insecure := direct.Clone()
|
|
insecure.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // matches the embedded NetBird transport's per-target opt-in
|
|
|
|
return &MultiTransport{
|
|
embedded: embedded,
|
|
direct: newUpstreamTransport(direct, cfg.upstreamHTTPVersion, logger),
|
|
insecure: newUpstreamTransport(insecure, cfg.upstreamHTTPVersion, logger),
|
|
}
|
|
}
|
|
|
|
// NewDirectOnly returns a MultiTransport with no embedded branch.
|
|
// Every request goes through the direct branch regardless of the
|
|
// per-request flag, so the embedded path can never be reached
|
|
// silently — wiring code that needs WG must use NewMultiTransport.
|
|
func NewDirectOnly(logger *log.Logger) *MultiTransport {
|
|
return NewMultiTransport(noEmbeddedRoundTripper{}, logger)
|
|
}
|
|
|
|
// noEmbeddedRoundTripper is the sentinel embedded transport for
|
|
// direct-only MultiTransports. RoundTrip is never called in practice
|
|
// because the direct branch matches every request, but if anything
|
|
// ever did reach this path it would fail loudly instead of falling
|
|
// back to direct.
|
|
type noEmbeddedRoundTripper struct{}
|
|
|
|
func (noEmbeddedRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
|
|
return nil, errNoEmbeddedTransport
|
|
}
|
|
|
|
// RoundTrip dispatches by reading the direct-upstream flag from the request
|
|
// context. When set, the request is forwarded via the stdlib transport,
|
|
// honouring the existing per-request skip-TLS-verify flag. Otherwise it
|
|
// goes through the embedded NetBird roundtripper.
|
|
func (m *MultiTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if DirectUpstreamFromContext(req.Context()) {
|
|
if skipTLSVerifyFromContext(req.Context()) {
|
|
return m.insecure.RoundTrip(req)
|
|
}
|
|
return m.direct.RoundTrip(req)
|
|
}
|
|
if m.embedded == nil {
|
|
return nil, errNoEmbeddedTransport
|
|
}
|
|
return m.embedded.RoundTrip(req)
|
|
}
|