mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 20:26:47 +02:00
tsweb: add TS_DEBUG_TRUSTED_CIDRS envknob to debug (#19283)
Add a new envknob that allows connections from trusted CIDR ranges to access debug endpoints without Tailscale authentication. This is useful for in-cluster scrapers like Prometheus that are not on a tailnet, do not have static IP addresses and cannot use debug keys. Fixes #19282 Signed-off-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com>
This commit is contained in:
parent
647deed2d9
commit
d948b78b23
@ -8,7 +8,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -206,3 +208,82 @@ func ExampleDebugHandler_Section() {
|
||||
fmt.Fprintf(w, "<code>%#v</code>", r)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseTrustedCIDRs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
want []netip.Prefix
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
raw: "",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single_v4",
|
||||
raw: "10.0.0.0/8",
|
||||
want: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
raw: "10.0.0.0/8,172.16.0.0/12",
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
netip.MustParsePrefix("172.16.0.0/12"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "spaces_trimmed",
|
||||
raw: " 10.0.0.0/8 , 192.168.0.0/16 ",
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ipv6",
|
||||
raw: "fd00::/8",
|
||||
want: []netip.Prefix{netip.MustParsePrefix("fd00::/8")},
|
||||
},
|
||||
{
|
||||
name: "trailing_comma",
|
||||
raw: "10.0.0.0/8,",
|
||||
want: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseTrustedCIDRs(tt.raw)
|
||||
if !slices.Equal(got, tt.want) {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowDebugAccessTrustedCIDRContains(t *testing.T) {
|
||||
// Verify that parsed CIDRs correctly match/reject IPs.
|
||||
cidrs := parseTrustedCIDRs("10.0.0.0/8,192.168.1.0/24,fd00::/8")
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"10.1.2.3", true},
|
||||
{"10.255.255.255", true},
|
||||
{"192.168.1.50", true},
|
||||
{"192.168.2.1", false},
|
||||
{"172.16.0.1", false},
|
||||
{"8.8.8.8", false},
|
||||
{"fd00::1", true},
|
||||
{"fe80::1", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ip := netip.MustParseAddr(tt.ip)
|
||||
if got := cidrsContain(cidrs, ip); got != tt.want {
|
||||
t.Errorf("CIDRs contain %s = %v, want %v", tt.ip, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -54,6 +55,50 @@ func IsProd443(addr string) bool {
|
||||
return port == "443" || port == "https"
|
||||
}
|
||||
|
||||
// debugTrustedCIDRs is the envknob for TS_DEBUG_TRUSTED_CIDRS, a
|
||||
// comma-separated list of CIDR ranges (e.g. "10.0.0.0/8,172.16.0.0/12")
|
||||
// whose source IPs are allowed to access debug endpoints without Tailscale
|
||||
// authentication. This will supersede both IsTailscaleIP() and
|
||||
// TS_ALLOW_DEBUG_IP.
|
||||
var debugTrustedCIDRs = envknob.RegisterString("TS_DEBUG_TRUSTED_CIDRS")
|
||||
|
||||
// trustedCIDRs returns the parsed CIDR prefixes from TS_DEBUG_TRUSTED_CIDRS.
|
||||
var trustedCIDRs = sync.OnceValue(func() []netip.Prefix {
|
||||
return parseTrustedCIDRs(debugTrustedCIDRs())
|
||||
})
|
||||
|
||||
// parseTrustedCIDRs parses a comma-separated list of CIDR prefixes.
|
||||
// It fatals on invalid entries, consistent with other envknob parsing.
|
||||
func parseTrustedCIDRs(raw string) []netip.Prefix {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var prefixes []netip.Prefix
|
||||
for _, s := range strings.Split(raw, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
pfx, err := netip.ParsePrefix(s)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid CIDR in TS_DEBUG_TRUSTED_CIDRS: %q: %v", s, err)
|
||||
}
|
||||
prefixes = append(prefixes, pfx)
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
// cidrsContain checks if the source IP is associated with one of the
|
||||
// provided cidrs.
|
||||
func cidrsContain(cidrs []netip.Prefix, ip netip.Addr) bool {
|
||||
for _, pfx := range cidrs {
|
||||
if pfx.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AllowDebugAccess reports whether r should be permitted to access
|
||||
// various debug endpoints.
|
||||
func AllowDebugAccess(r *http.Request) bool {
|
||||
@ -75,6 +120,9 @@ func AllowDebugAccess(r *http.Request) bool {
|
||||
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == envknob.String("TS_ALLOW_DEBUG_IP") {
|
||||
return true
|
||||
}
|
||||
if cidrsContain(trustedCIDRs(), ip) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user