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 <root@lr0.org>
This commit is contained in:
Saleh
2026-07-08 10:39:54 +01:00
committed by GitHub
parent 887005d255
commit 9106b237eb
2 changed files with 75 additions and 0 deletions
+5
View File
@@ -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
+70
View File
@@ -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)
}
})
}
}