From 30d13aa86986d10b9fbc01a6bac3d7b41f768f6d Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sun, 26 Jul 2026 17:42:21 +0000 Subject: [PATCH] proxy: allow overriding tunnel-cache TTL via env var The tunnel-peer validation cache (peer -> user/groups from ValidateTunnelPeer) memoizes positive results for a fixed 5 minutes, so an authorization change (e.g. a revoked peer) can take up to 5 minutes to take effect on the proxy. Add NB_PROXY_TUNNEL_CACHE_TTL to override the default at construction. The value is a Go duration string (e.g. "30s", "2m"); unset, unparseable, or non-positive values keep the 300s default and log a warning. Operators who need faster propagation can lower it (trading more ValidateTunnelPeer RPCs for freshness). --- proxy/internal/auth/tunnel_cache.go | 39 +++++++++++++++++++++--- proxy/internal/auth/tunnel_cache_test.go | 29 ++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/proxy/internal/auth/tunnel_cache.go b/proxy/internal/auth/tunnel_cache.go index 10b671d82..185c53c62 100644 --- a/proxy/internal/auth/tunnel_cache.go +++ b/proxy/internal/auth/tunnel_cache.go @@ -3,20 +3,30 @@ package auth import ( "context" "net/netip" + "os" + "strings" "sync" "time" + log "github.com/sirupsen/logrus" "golang.org/x/sync/singleflight" "github.com/netbirdio/netbird/proxy/internal/types" "github.com/netbirdio/netbird/shared/management/proto" ) -// tunnelCacheTTL caps how long a positive ValidateTunnelPeer result is -// reused before re-fetching from management. 5 minutes balances freshness -// against management load on busy mesh networks. +// tunnelCacheTTL is the default cap on how long a positive ValidateTunnelPeer +// result is reused before re-fetching from management. 5 minutes balances +// freshness against management load on busy mesh networks. Override it with +// envTunnelCacheTTL when an account needs authorization changes to take effect +// sooner (at the cost of more ValidateTunnelPeer RPCs). const tunnelCacheTTL = 300 * time.Second +// envTunnelCacheTTL overrides tunnelCacheTTL. The value is a Go duration string +// (e.g. "30s", "2m"); an unset, unparseable, or non-positive value keeps the +// default. +const envTunnelCacheTTL = "NB_PROXY_TUNNEL_CACHE_TTL" + // tunnelCachePerAccount caps the number of cached identities per account. // Bounded eviction avoids memory growth in pathological cases (huge peer // roster, brief request bursts) while staying generous for normal use. @@ -60,16 +70,35 @@ type accountBucket struct { order []tunnelCacheKey } -// newTunnelValidationCache constructs a cache with default TTL and bounds. +// newTunnelValidationCache constructs a cache with the configured TTL +// (envTunnelCacheTTL override or default) and default bounds. func newTunnelValidationCache() *tunnelValidationCache { return &tunnelValidationCache{ entries: make(map[types.AccountID]*accountBucket), - ttl: tunnelCacheTTL, + ttl: tunnelCacheTTLFromEnv(), maxSize: tunnelCachePerAccount, now: time.Now, } } +// tunnelCacheTTLFromEnv returns the tunnel-cache TTL, honoring the +// envTunnelCacheTTL override. The override must be a positive Go duration +// string (e.g. "30s", "2m"); anything unset, unparseable, or non-positive +// falls back to tunnelCacheTTL. +func tunnelCacheTTLFromEnv() time.Duration { + raw := strings.TrimSpace(os.Getenv(envTunnelCacheTTL)) + if raw == "" { + return tunnelCacheTTL + } + d, err := time.ParseDuration(raw) + if err != nil || d <= 0 { + log.Warnf("ignoring invalid %s=%q (want a positive Go duration like 30s or 2m); using default %s", + envTunnelCacheTTL, raw, tunnelCacheTTL) + return tunnelCacheTTL + } + return d +} + // get returns a cached response for the key, or nil when missing or // expired. Expired entries are evicted lazily on read. func (c *tunnelValidationCache) get(key tunnelCacheKey) *proto.ValidateTunnelPeerResponse { diff --git a/proxy/internal/auth/tunnel_cache_test.go b/proxy/internal/auth/tunnel_cache_test.go index 1a63dc107..d663b91e7 100644 --- a/proxy/internal/auth/tunnel_cache_test.go +++ b/proxy/internal/auth/tunnel_cache_test.go @@ -169,3 +169,32 @@ func TestTunnelCache_BoundedSizeEvictsOldest(t *testing.T) { assert.NotNil(t, cache.get(keys[1]), "second-newest must remain cached") assert.NotNil(t, cache.get(keys[2]), "newest must remain cached") } + +func TestTunnelCacheTTLFromEnv(t *testing.T) { + t.Run("unset uses default", func(t *testing.T) { + t.Setenv(envTunnelCacheTTL, "") + assert.Equal(t, tunnelCacheTTL, tunnelCacheTTLFromEnv()) + }) + t.Run("valid duration overrides", func(t *testing.T) { + t.Setenv(envTunnelCacheTTL, "45s") + assert.Equal(t, 45*time.Second, tunnelCacheTTLFromEnv()) + }) + t.Run("whitespace trimmed", func(t *testing.T) { + t.Setenv(envTunnelCacheTTL, " 2m ") + assert.Equal(t, 2*time.Minute, tunnelCacheTTLFromEnv()) + }) + t.Run("unparseable uses default", func(t *testing.T) { + t.Setenv(envTunnelCacheTTL, "nonsense") + assert.Equal(t, tunnelCacheTTL, tunnelCacheTTLFromEnv()) + }) + t.Run("non-positive uses default", func(t *testing.T) { + t.Setenv(envTunnelCacheTTL, "0s") + assert.Equal(t, tunnelCacheTTL, tunnelCacheTTLFromEnv()) + t.Setenv(envTunnelCacheTTL, "-30s") + assert.Equal(t, tunnelCacheTTL, tunnelCacheTTLFromEnv()) + }) + t.Run("constructor honors override", func(t *testing.T) { + t.Setenv(envTunnelCacheTTL, "90s") + assert.Equal(t, 90*time.Second, newTunnelValidationCache().ttl) + }) +}