From 9106b237eb72b750b531e67479942496ff2899a4 Mon Sep 17 00:00:00 2001 From: Saleh <_@lr0.org> Date: Wed, 8 Jul 2026 12:39:54 +0300 Subject: [PATCH] cmd/tailscale/cli: fix nil dereference in configure kubeconfig (#20324) PeerStatus.AllowedIPs is only populated when a peer has allowed IPs, so it is nil for peers whose backing nodes are offline or not yet approved, such as a kube-apiserver ProxyGroup with no healthy nodes. When the argument to "tailscale configure kubeconfig" resolved to a Tailscale Service ExtraRecord, nodeOrServiceDNSNameFromArg iterated AllowedIPs of every peer without a nil check and panicked with SIGSEGV. Skip peers with no AllowedIPs so the command reports the existing "is in MagicDNS, but is not currently reachable on any known peer" error instead of crashing. Fixes #20255 Signed-off-by: Salih Muhammed --- cmd/tailscale/cli/configure-kube.go | 5 ++ cmd/tailscale/cli/configure-kube_test.go | 70 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/cmd/tailscale/cli/configure-kube.go b/cmd/tailscale/cli/configure-kube.go index 3b274f062..e2ef6becf 100644 --- a/cmd/tailscale/cli/configure-kube.go +++ b/cmd/tailscale/cli/configure-kube.go @@ -306,6 +306,11 @@ func nodeOrServiceDNSNameFromArg(st *ipnstate.Status, dns *tailcfg.DNSConfig, ar } ipPrefix := netip.PrefixFrom(ip, ip.BitLen()) for _, ps := range st.Peer { + if ps.AllowedIPs == nil { + // Peer with no addresses visible in the tailnet, e.g. a ProxyGroup + // whose backing nodes are offline or not yet approved (#20255). + continue + } for _, allowedIP := range ps.AllowedIPs.All() { if allowedIP == ipPrefix { return rec.Name, nil diff --git a/cmd/tailscale/cli/configure-kube_test.go b/cmd/tailscale/cli/configure-kube_test.go index d5425531b..45ab7f542 100644 --- a/cmd/tailscale/cli/configure-kube_test.go +++ b/cmd/tailscale/cli/configure-kube_test.go @@ -7,6 +7,7 @@ package cli import ( "bytes" "fmt" + "net/netip" "os" "path/filepath" "runtime" @@ -14,6 +15,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" + "tailscale.com/types/key" + "tailscale.com/types/views" ) func TestKubeconfig(t *testing.T) { @@ -339,3 +344,68 @@ func TestGetInputs(t *testing.T) { } } } + +func TestNodeOrServiceDNSNameFromArg(t *testing.T) { + svcIP := netip.MustParseAddr("100.100.100.100") + dnsCfg := &tailcfg.DNSConfig{ + ExtraRecords: []tailcfg.DNSRecord{ + {Name: "svc.example.ts.net", Value: svcIP.String()}, + }, + } + + peerWithService := &ipnstate.PeerStatus{DNSName: "node-a.example.ts.net."} + allowed := views.SliceOf([]netip.Prefix{netip.PrefixFrom(svcIP, svcIP.BitLen())}) + peerWithService.AllowedIPs = &allowed + + // A peer with no AllowedIPs, as reported for a ProxyGroup whose backing + // nodes are offline or not yet approved (issue #20255). + peerNoAddrs := &ipnstate.PeerStatus{DNSName: "node-b.example.ts.net."} + + tests := []struct { + name string + peers []*ipnstate.PeerStatus + arg string + want string + wantErr string + }{ + { + name: "service_with_no_reachable_peer", + peers: []*ipnstate.PeerStatus{peerNoAddrs}, + arg: "svc", + wantErr: "not currently reachable", + }, + { + name: "service_advertised_by_peer", + peers: []*ipnstate.PeerStatus{peerNoAddrs, peerWithService}, + arg: "svc", + want: "svc.example.ts.net", + }, + { + name: "node_dns_name", + peers: []*ipnstate.PeerStatus{peerNoAddrs}, + arg: "node-b", + want: "node-b.example.ts.net.", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := &ipnstate.Status{Peer: map[key.NodePublic]*ipnstate.PeerStatus{}} + for _, ps := range tt.peers { + st.Peer[key.NewNode().Public()] = ps + } + got, err := nodeOrServiceDNSNameFromArg(st, dnsCfg, tt.arg) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("err = %v, want error containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +}