diff --git a/cmd/containerboot/egressservices.go b/cmd/containerboot/egressservices.go index fe835a69e..21d9f0bcb 100644 --- a/cmd/containerboot/egressservices.go +++ b/cmd/containerboot/egressservices.go @@ -27,7 +27,6 @@ import ( "tailscale.com/kube/egressservices" "tailscale.com/kube/kubeclient" "tailscale.com/kube/kubetypes" - "tailscale.com/tailcfg" "tailscale.com/util/httpm" "tailscale.com/util/linuxfw" "tailscale.com/util/mak" @@ -477,30 +476,26 @@ func (ep *egressProxy) tailnetTargetIPsForSvc(svc egressservices.Config, n ipn.N log.Printf("netmap is not available, unable to determine backend addresses for %s", svc.TailnetTarget.FQDN) return addrs, nil } - var ( - node tailcfg.NodeView - nodeFound bool - ) - for _, nn := range n.NetMap.Peers { - if equalFQDNs(nn.Name(), svc.TailnetTarget.FQDN) { - node = nn - nodeFound = true - break - } + egressAddrs, err := resolveTailnetFQDN(n.NetMap, svc.TailnetTarget.FQDN) + if err != nil { + return nil, fmt.Errorf("error fetching backend addresses for %q: %w", svc.TailnetTarget.FQDN, err) } - if nodeFound { - for _, addr := range node.Addresses().AsSlice() { - if addr.Addr().Is6() && !ep.nfr.HasIPV6NAT() { - log.Printf("tailnet target %v is an IPv6 address, but this host does not support IPv6 in the chosen firewall mode, skipping.", addr.Addr().String()) - continue - } - addrs = append(addrs, addr.Addr()) - } - // Egress target endpoints configured via FQDN are stored, so - // that we can determine if a netmap update should trigger a - // resync. - mak.Set(&ep.targetFQDNs, svc.TailnetTarget.FQDN, node.Addresses().AsSlice()) + if len(egressAddrs) == 0 { + log.Printf("tailnet target %q does not have any backend addresses, skipping", svc.TailnetTarget.FQDN) + return addrs, nil } + + for _, addr := range egressAddrs { + if addr.Addr().Is6() && !ep.nfr.HasIPV6NAT() { + log.Printf("tailnet target %v is an IPv6 address, but this host does not support IPv6 in the chosen firewall mode, skipping.", addr.Addr().String()) + continue + } + addrs = append(addrs, addr.Addr()) + } + // Egress target endpoints configured via FQDN are stored, so + // that we can determine if a netmap update should trigger a + // resync. + mak.Set(&ep.targetFQDNs, svc.TailnetTarget.FQDN, egressAddrs) return addrs, nil } diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go index f056d26f3..8c9d33c61 100644 --- a/cmd/containerboot/main.go +++ b/cmd/containerboot/main.go @@ -127,8 +127,10 @@ import ( "tailscale.com/kube/services" "tailscale.com/tailcfg" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/types/ptr" "tailscale.com/util/deephash" + "tailscale.com/util/dnsname" "tailscale.com/util/linuxfw" ) @@ -526,27 +528,14 @@ runLoop: } } if cfg.TailnetTargetFQDN != "" { - var ( - egressAddrs []netip.Prefix - newCurentEgressIPs deephash.Sum - egressIPsHaveChanged bool - node tailcfg.NodeView - nodeFound bool - ) - for _, n := range n.NetMap.Peers { - if strings.EqualFold(n.Name(), cfg.TailnetTargetFQDN) { - node = n - nodeFound = true - break - } - } - if !nodeFound { - log.Printf("Tailscale node %q not found; it either does not exist, or not reachable because of ACLs", cfg.TailnetTargetFQDN) + egressAddrs, err := resolveTailnetFQDN(n.NetMap, cfg.TailnetTargetFQDN) + if err != nil { + log.Print(err.Error()) break } - egressAddrs = node.Addresses().AsSlice() - newCurentEgressIPs = deephash.Hash(&egressAddrs) - egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs + + newCurentEgressIPs := deephash.Hash(&egressAddrs) + egressIPsHaveChanged := newCurentEgressIPs != currentEgressIPs // The firewall rules get (re-)installed: // - on startup // - when the tailnet IPs of the tailnet target have changed @@ -892,3 +881,65 @@ func runHTTPServer(mux *http.ServeMux, addr string) (close func() error) { return errors.Join(err, ln.Close()) } } + +// resolveTailnetFQDN resolves a tailnet FQDN to a list of IP prefixes, which +// can be either a peer device or a Tailscale Service. +func resolveTailnetFQDN(nm *netmap.NetworkMap, fqdn string) ([]netip.Prefix, error) { + dnsFQDN, err := dnsname.ToFQDN(fqdn) + if err != nil { + return nil, fmt.Errorf("error parsing %q as FQDN: %w", fqdn, err) + } + + // Check all peer devices first. + for _, p := range nm.Peers { + if strings.EqualFold(p.Name(), dnsFQDN.WithTrailingDot()) { + return p.Addresses().AsSlice(), nil + } + } + + // If not found yet, check for a matching Tailscale Service. + if svcIPs := serviceIPsFromNetMap(nm, dnsFQDN); len(svcIPs) != 0 { + return svcIPs, nil + } + + return nil, fmt.Errorf("could not find Tailscale node or service %q; it either does not exist, or not reachable because of ACLs", fqdn) +} + +// serviceIPsFromNetMap returns all IPs of a Tailscale Service if its FQDN is +// found in the netmap. Note that Tailscale Services are not a first-class +// object in the netmap, so we guess based on DNS ExtraRecords and AllowedIPs. +func serviceIPsFromNetMap(nm *netmap.NetworkMap, fqdn dnsname.FQDN) []netip.Prefix { + var extraRecords []tailcfg.DNSRecord + for _, rec := range nm.DNS.ExtraRecords { + recFQDN, err := dnsname.ToFQDN(rec.Name) + if err != nil { + continue + } + if strings.EqualFold(fqdn.WithTrailingDot(), recFQDN.WithTrailingDot()) { + extraRecords = append(extraRecords, rec) + } + } + + if len(extraRecords) == 0 { + return nil + } + + // Validate we can see a peer advertising the Tailscale Service. + var prefixes []netip.Prefix + for _, extraRecord := range extraRecords { + ip, err := netip.ParseAddr(extraRecord.Value) + if err != nil { + continue + } + ipPrefix := netip.PrefixFrom(ip, ip.BitLen()) + for _, ps := range nm.Peers { + for _, allowedIP := range ps.AllowedIPs().All() { + if allowedIP == ipPrefix { + prefixes = append(prefixes, ipPrefix) + } + } + } + } + + return prefixes +} diff --git a/cmd/containerboot/main_test.go b/cmd/containerboot/main_test.go index f92f35333..7007cc152 100644 --- a/cmd/containerboot/main_test.go +++ b/cmd/containerboot/main_test.go @@ -46,7 +46,7 @@ func TestContainerBoot(t *testing.T) { if err := exec.Command("go", "build", "-ldflags", "-X main.testSleepDuration=1ms", "-o", boot, "tailscale.com/cmd/containerboot").Run(); err != nil { t.Fatalf("Building containerboot: %v", err) } - egressStatus := egressSvcStatus("foo", "foo.tailnetxyz.ts.net") + egressStatus := egressSvcStatus("foo", "foo.tailnetxyz.ts.net", "100.64.0.2") metricsURL := func(port int) string { return fmt.Sprintf("http://127.0.0.1:%d/metrics", port) @@ -99,7 +99,7 @@ func TestContainerBoot(t *testing.T) { NetMap: &netmap.NetworkMap{ SelfNode: (&tailcfg.Node{ StableID: tailcfg.StableNodeID("myID"), - Name: "test-node.test.ts.net", + Name: "test-node.test.ts.net.", Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, }).View(), }, @@ -356,7 +356,7 @@ func TestContainerBoot(t *testing.T) { return testCase{ Env: map[string]string{ "TS_AUTHKEY": "tskey-key", - "TS_TAILNET_TARGET_FQDN": "ipv6-node.test.ts.net", // resolves to IPv6 address + "TS_TAILNET_TARGET_FQDN": "ipv6-node.test.ts.net.", // resolves to IPv6 address "TS_USERSPACE": "false", "TS_TEST_FAKE_NETFILTER_6": "false", }, @@ -377,13 +377,13 @@ func TestContainerBoot(t *testing.T) { NetMap: &netmap.NetworkMap{ SelfNode: (&tailcfg.Node{ StableID: tailcfg.StableNodeID("myID"), - Name: "test-node.test.ts.net", + Name: "test-node.test.ts.net.", Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, }).View(), Peers: []tailcfg.NodeView{ (&tailcfg.Node{ StableID: tailcfg.StableNodeID("ipv6ID"), - Name: "ipv6-node.test.ts.net", + Name: "ipv6-node.test.ts.net.", Addresses: []netip.Prefix{netip.MustParsePrefix("::1/128")}, }).View(), }, @@ -481,7 +481,7 @@ func TestContainerBoot(t *testing.T) { Notify: runningNotify, WantKubeSecret: map[string]string{ "authkey": "tskey-key", - "device_fqdn": "test-node.test.ts.net", + "device_fqdn": "test-node.test.ts.net.", "device_id": "myID", "device_ips": `["100.64.0.1"]`, kubetypes.KeyCapVer: capver, @@ -580,7 +580,7 @@ func TestContainerBoot(t *testing.T) { "/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false", }, WantKubeSecret: map[string]string{ - "device_fqdn": "test-node.test.ts.net", + "device_fqdn": "test-node.test.ts.net.", "device_id": "myID", "device_ips": `["100.64.0.1"]`, kubetypes.KeyCapVer: capver, @@ -613,7 +613,7 @@ func TestContainerBoot(t *testing.T) { Notify: runningNotify, WantKubeSecret: map[string]string{ "authkey": "tskey-key", - "device_fqdn": "test-node.test.ts.net", + "device_fqdn": "test-node.test.ts.net.", "device_id": "myID", "device_ips": `["100.64.0.1"]`, kubetypes.KeyCapVer: capver, @@ -625,14 +625,14 @@ func TestContainerBoot(t *testing.T) { NetMap: &netmap.NetworkMap{ SelfNode: (&tailcfg.Node{ StableID: tailcfg.StableNodeID("newID"), - Name: "new-name.test.ts.net", + Name: "new-name.test.ts.net.", Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, }).View(), }, }, WantKubeSecret: map[string]string{ "authkey": "tskey-key", - "device_fqdn": "new-name.test.ts.net", + "device_fqdn": "new-name.test.ts.net.", "device_id": "newID", "device_ips": `["100.64.0.1"]`, kubetypes.KeyCapVer: capver, @@ -927,7 +927,7 @@ func TestContainerBoot(t *testing.T) { Notify: runningNotify, WantKubeSecret: map[string]string{ "authkey": "tskey-key", - "device_fqdn": "test-node.test.ts.net", + "device_fqdn": "test-node.test.ts.net.", "device_id": "myID", "device_ips": `["100.64.0.1"]`, "https_endpoint": "no-https", @@ -963,11 +963,27 @@ func TestContainerBoot(t *testing.T) { }, }, { - Notify: runningNotify, + Notify: &ipn.Notify{ + State: ptr.To(ipn.Running), + NetMap: &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + StableID: tailcfg.StableNodeID("myID"), + Name: "test-node.test.ts.net.", + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, + }).View(), + Peers: []tailcfg.NodeView{ + (&tailcfg.Node{ + StableID: tailcfg.StableNodeID("fooID"), + Name: "foo.tailnetxyz.ts.net.", + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.2/32")}, + }).View(), + }, + }, + }, WantKubeSecret: map[string]string{ "egress-services": string(mustJSON(t, egressStatus)), "authkey": "tskey-key", - "device_fqdn": "test-node.test.ts.net", + "device_fqdn": "test-node.test.ts.net.", "device_id": "myID", "device_ips": `["100.64.0.1"]`, kubetypes.KeyCapVer: capver, @@ -1338,6 +1354,11 @@ func (lc *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { } w.Write([]byte("fake metrics")) return + case "/localapi/v0/prefs": + if r.Method != "GET" { + panic(fmt.Sprintf("unsupported method %q", r.Method)) + } + return default: panic(fmt.Sprintf("unsupported path %q", r.URL.Path)) } @@ -1563,13 +1584,14 @@ func mustJSON(t *testing.T, v any) []byte { } // egress services status given one named tailnet target specified by FQDN. As written by the proxy to its state Secret. -func egressSvcStatus(name, fqdn string) egressservices.Status { +func egressSvcStatus(name, fqdn, ip string) egressservices.Status { return egressservices.Status{ Services: map[string]*egressservices.ServiceStatus{ name: { TailnetTarget: egressservices.TailnetTarget{ FQDN: fqdn, }, + TailnetTargetIPs: []netip.Addr{netip.MustParseAddr(ip)}, }, }, } diff --git a/cmd/tailscale/cli/configure-kube.go b/cmd/tailscale/cli/configure-kube.go index e74e88779..bf5624856 100644 --- a/cmd/tailscale/cli/configure-kube.go +++ b/cmd/tailscale/cli/configure-kube.go @@ -247,7 +247,7 @@ func nodeOrServiceDNSNameFromArg(st *ipnstate.Status, nm *netmap.NetworkMap, arg } // If not found, check for a Tailscale Service DNS name. - rec, ok := serviceDNSRecordFromNetMap(nm, st.CurrentTailnet.MagicDNSSuffix, arg) + rec, ok := serviceDNSRecordFromNetMap(nm, arg) if !ok { return "", fmt.Errorf("no peer found for %q", arg) } @@ -287,7 +287,7 @@ func getNetMap(ctx context.Context) (*netmap.NetworkMap, error) { return n.NetMap, nil } -func serviceDNSRecordFromNetMap(nm *netmap.NetworkMap, tcd, arg string) (rec tailcfg.DNSRecord, ok bool) { +func serviceDNSRecordFromNetMap(nm *netmap.NetworkMap, arg string) (rec tailcfg.DNSRecord, ok bool) { argIP, _ := netip.ParseAddr(arg) argFQDN, err := dnsname.ToFQDN(arg) argFQDNValid := err == nil