cmd/containerboot: support egress to Tailscale Service FQDNs (#17493)

Adds support for targeting FQDNs that are a Tailscale Service. Uses the
same method of searching for Services as the tailscale configure
kubeconfig command. This fixes using the tailscale.com/tailnet-fqdn
annotation for Kubernetes Service when the specified FQDN is a Tailscale
Service.

Fixes #16534

Change-Id: I422795de76dc83ae30e7e757bc4fbd8eec21cc64

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
Signed-off-by: Becky Pauley <becky@tailscale.com>
This commit is contained in:
Tom Proctor
2025-12-18 17:06:42 +00:00
committed by GitHub
parent eed5e95e27
commit bb3529fcd4
4 changed files with 126 additions and 58 deletions

View File

@@ -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
}