diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go index 1a11c3150..705b73bc6 100644 --- a/cmd/containerboot/main.go +++ b/cmd/containerboot/main.go @@ -129,6 +129,7 @@ import ( "os/signal" "path/filepath" "slices" + "strconv" "strings" "sync" "sync/atomic" @@ -147,6 +148,7 @@ import ( klc "tailscale.com/kube/localclient" "tailscale.com/kube/metrics" "tailscale.com/kube/services" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/netmap" @@ -995,7 +997,8 @@ func fetchNetMap(ctx context.Context, lc *local.Client) (*netmap.NetworkMap, err } // resolveTailnetFQDN resolves a tailnet FQDN to a list of IP prefixes, which -// can be either a peer device or a Tailscale Service. +// can be either a peer device, a Tailscale Service, or a 4via6 synthesized +// DNS name (e.g. "10-1-0-5-via-7.tailnet.ts.net"). func resolveTailnetFQDN(nm *netmap.NetworkMap, fqdn string) ([]netip.Prefix, error) { dnsFQDN, err := dnsname.ToFQDN(fqdn) if err != nil { @@ -1014,6 +1017,19 @@ func resolveTailnetFQDN(nm *netmap.NetworkMap, fqdn string) ([]netip.Prefix, err return svcIPs, nil } + // If not found yet, check for a matching 4via6 DNS name. + if addr, ok := resolveViaDomain(dnsFQDN); ok { + prefix := netip.PrefixFrom(addr, addr.BitLen()) + for _, nn := range nm.Peers { + for _, allowedIP := range nn.AllowedIPs().All() { + if allowedIP.Contains(addr) { + return []netip.Prefix{prefix}, nil + } + } + } + return nil, fmt.Errorf("resolved 4via6 address %v for %q but no peer advertises a route containing it", addr, fqdn) + } + return nil, fmt.Errorf("could not find Tailscale node or service %q; it either does not exist, or not reachable because of ACLs", fqdn) } @@ -1055,3 +1071,40 @@ func serviceIPsFromNetMap(nm *netmap.NetworkMap, fqdn dnsname.FQDN) []netip.Pref return prefixes } + +// resolveViaDomain parses an FQDN as a 4via6 in the format "-via-[.domain]" +// and returns the IPv6 via address. +// This borrows heavily from net/dns/resolver.(*Resolver).resolveViaDomain. +// TODO(beckypauley): consider a refactor of the above to remove duplication. +func resolveViaDomain(fqdn dnsname.FQDN) (netip.Addr, bool) { + // The minimum length of a valid 4via6 FQDN i.e. "via-X.0.0.0.0". + const minFQDNLength = 13 + name := string(fqdn.WithoutTrailingDot()) + // This is not a fqdn. + if !strings.Contains(name, "-via-") { + return netip.Addr{}, false + } + if len(name) < minFQDNLength { + return netip.Addr{}, false // too short to be valid + } + firstLabel, domain, _ := strings.Cut(name, ".") + if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) { + return netip.Addr{}, false + } + v4hyphens, siteIDStr, ok := strings.Cut(firstLabel, "-via-") + if !ok { + return netip.Addr{}, false + } + ip4Str := strings.ReplaceAll(v4hyphens, "-", ".") + ip4, err := netip.ParseAddr(ip4Str) + if err != nil || !ip4.Is4() { + return netip.Addr{}, false + } + siteID, err := strconv.ParseUint(siteIDStr, 0, 32) + if err != nil { + return netip.Addr{}, false + } + // MapVia will never error when given an IPv4 netip.Prefix. + out, _ := tsaddr.MapVia(uint32(siteID), netip.PrefixFrom(ip4, ip4.BitLen())) + return out.Addr(), true +} diff --git a/cmd/k8s-operator/connector.go b/cmd/k8s-operator/connector.go index 0c2d32482..323dc7b86 100644 --- a/cmd/k8s-operator/connector.go +++ b/cmd/k8s-operator/connector.go @@ -29,6 +29,8 @@ import ( tsoperator "tailscale.com/k8s-operator" tsapi "tailscale.com/k8s-operator/apis/v1alpha1" "tailscale.com/kube/kubetypes" + "tailscale.com/net/netutil" + "tailscale.com/net/tsaddr" "tailscale.com/tstime" "tailscale.com/util/clientmetric" "tailscale.com/util/set" @@ -356,6 +358,11 @@ func validateRoutes(routes tsapi.Routes) error { if pfx.Masked() != pfx { errs = append(errs, fmt.Errorf("route %s has non-address bits set; expected %s", pfx, pfx.Masked())) } + if tsaddr.IsViaPrefix(pfx) { + if err := netutil.ValidateViaPrefix(pfx); err != nil { + errs = append(errs, err) + } + } } return errors.Join(errs...) } diff --git a/cmd/k8s-operator/connector_test.go b/cmd/k8s-operator/connector_test.go index 69e8e287d..b366dac38 100644 --- a/cmd/k8s-operator/connector_test.go +++ b/cmd/k8s-operator/connector_test.go @@ -145,6 +145,22 @@ func TestConnector(t *testing.T) { expectReconciled(t, cr, "", "test") expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs) + // Set an invalid 4via6 route (site ID too large). + mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { + conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"fd7a:115c:a1e0:b1a:1:0:a2c:0/116"} + }) + expectReconciled(t, cr, "", "test") + // STS should still have the previous valid route, unchanged. + expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs) + + // Set a valid 4via6 route. + mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { + conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"fd7a:115c:a1e0:b1a:0:1:a2c:0/116"} + }) + opts.subnetRoutes = "fd7a:115c:a1e0:b1a:0:1:a2c:0/116" + expectReconciled(t, cr, "", "test") + expectEqual(t, fc, expectedSTS(t, fc, opts), removeResourceReqs) + // Delete the Connector. if err = fc.Delete(context.Background(), cn); err != nil { t.Fatalf("error deleting Connector: %v", err) diff --git a/net/netutil/routes.go b/net/netutil/routes.go index 26f2de97c..510572b6d 100644 --- a/net/netutil/routes.go +++ b/net/netutil/routes.go @@ -13,7 +13,11 @@ import ( "tailscale.com/net/tsaddr" ) -func validateViaPrefix(ipp netip.Prefix) error { +// ValidateViaPrefix checks that the IP prefix is a valid 4via6 route. +// It verifies that the prefix is in the Tailscale via range, has a prefix +// length between /96 and /128, and that the embedded site ID is in the +// range 0–65535. +func ValidateViaPrefix(ipp netip.Prefix) error { if !tsaddr.IsViaPrefix(ipp) { return fmt.Errorf("%v is not a 4-in-6 prefix", ipp) } @@ -51,7 +55,7 @@ func CalcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([] return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked()) } if tsaddr.IsViaPrefix(ipp) { - if err := validateViaPrefix(ipp); err != nil { + if err := ValidateViaPrefix(ipp); err != nil { return nil, err } }