cmd/k8s-operator, net/netutil: support 4via6 in egress proxy and connector (#19863)

Add support for configuring egress to destinations reachable via 4via6
subnet routes. This change affects standalone egress proxy only- egress
ProxyGroup needs IPv6 support before being able to support 4via6. Egress may
be configured using either the synthesized 4via6 address or the MagicDNS
name (in the form
<IPv4-address-with-hyphens-instead-of-dots>-via-<siteid>[.*]).

Also update the Connector to validate and advertise 4via6 subnet routes.
Export net/netutil.ValidateViaPrefix so it can be reused by the Connector
validation logic.

Updates #19334

Signed-off-by: Becky Pauley <becky@tailscale.com>
This commit is contained in:
BeckyPauley
2026-05-27 10:54:35 +01:00
committed by GitHub
parent e5a8cf3b18
commit 0ed6da2826
4 changed files with 83 additions and 3 deletions
+54 -1
View File
@@ -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 "<ipv4-with-hyphens>-via-<siteID>[.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
}