From 044abcc9d26c6edcf2a28a67897b3a0e9772bf19 Mon Sep 17 00:00:00 2001 From: maxiscoding28 <43095669+maxiscoding28@users.noreply.github.com> Date: Wed, 16 Sep 2026 03:01:27 -0700 Subject: [PATCH] ipn/ipnlocal: make Serve idle connection limit configurable (#21179) * ipn/ipnlocal: make Serve idle connection limit configurable Allow Kubernetes operator proxy pods to override the per-host idle connection limit through an environment knob.\n\nUpdates tailscale/tailscale#20875 Signed-off-by: maxiscoding28 * ipn/ipnlocal: document Serve idle connection default Signed-off-by: maxiscoding28 --------- Signed-off-by: maxiscoding28 --- ipn/ipnlocal/serve.go | 4 ++++ ipn/ipnlocal/serve_test.go | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 3730123a7..4f0d5303e 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -35,6 +35,7 @@ "github.com/pires/go-proxyproto" "go4.org/mem" + "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/net/netmon" "tailscale.com/net/netutil" @@ -1001,6 +1002,8 @@ func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // to the backend. The Transport gets created lazily, at most once. func (rp *reverseProxy) getTransport() *http.Transport { return rp.httpTransport.Get(func() *http.Transport { + // Zero preserves http.Transport's default MaxIdleConnsPerHost value. + maxIdleConnsPerHost, _ := envknob.LookupInt("TS_DEBUG_SERVE_MAX_IDLE_CONNS_PER_HOST") dial := rp.lb.dialer.SystemDial if rp.socketPath != "" { dial = func(ctx context.Context, _, _ string) (net.Conn, error) { @@ -1017,6 +1020,7 @@ func (rp *reverseProxy) getTransport() *http.Transport { // Values for the following parameters have been copied from http.DefaultTransport. ForceAttemptHTTP2: true, MaxIdleConns: 100, + MaxIdleConnsPerHost: maxIdleConnsPerHost, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index 84c0b293f..6737c1a1f 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -29,9 +29,12 @@ "time" "tailscale.com/control/controlclient" + "tailscale.com/envknob" "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/ipn/store/mem" + "tailscale.com/net/netmon" + "tailscale.com/net/tsdial" "tailscale.com/tailcfg" "tailscale.com/tailcfg/nodecap" "tailscale.com/tailcfg/peercap" @@ -1090,6 +1093,25 @@ type test struct { }) } +func TestServeMaxIdleConnsPerHost(t *testing.T) { + for _, tt := range []struct { + name string + env string + want int + }{ + {name: "default", want: 0}, + {name: "configured", env: "100", want: 100}, + } { + t.Run(tt.name, func(t *testing.T) { + envknob.SetenvForTest(t, "TS_DEBUG_SERVE_MAX_IDLE_CONNS_PER_HOST", tt.env) + rp := &reverseProxy{lb: &LocalBackend{dialer: tsdial.NewDialer(netmon.NewStatic())}} + if got := rp.getTransport().MaxIdleConnsPerHost; got != tt.want { + t.Errorf("MaxIdleConnsPerHost = %d, want %d", got, tt.want) + } + }) + } +} + func mustCreateURL(t *testing.T, u string) url.URL { t.Helper() uParsed, err := url.Parse(u)