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) + } + }) + } +}