Files
tailscale/k8s-operator/utils.go
BeckyPauley 35a1a413f9 cmd/{containerboot,k8s-operator}: add 4via6 support in singleton egress (#19983)
Add support for configuring egress to destinations reachable via 4via6
subnet routes, using either the synthesized 4via6 address or the MagicDNS
name (in the form <IPv4-with-hyphens>-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.

This change only affects standalone egress proxies — ProxyGroup egress
requires IPv6 support before it can use 4via6.

Updates #19334

Change-Id: I6faecd6eb61ab55fc0cd97fe417af6b6a12fe7fc

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-06-18 16:13:10 +01:00

111 lines
3.5 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
// Package kube contains types and utilities for the Tailscale Kubernetes Operator.
package kube
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/netip"
"strconv"
"strings"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/util/dnsname"
)
const (
Alpha1Version = "v1alpha1"
DNSRecordsCMName = "dnsrecords"
DNSRecordsCMKey = "records.json"
)
type Records struct {
// Version is the version of this Records configuration. Version is
// written by the operator, i.e when it first populates the Records.
// k8s-nameserver must verify that it knows how to parse a given
// version.
Version string `json:"version"`
// IP4 contains a mapping of DNS names to IPv4 address(es).
IP4 map[string][]string `json:"ip4"`
// IP6 contains a mapping of DNS names to IPv6 address(es).
// This field is optional and will be omitted from JSON if empty.
// It enables dual-stack DNS support in Kubernetes clusters.
// +optional
IP6 map[string][]string `json:"ip6,omitempty"`
}
// TailscaledConfigFileName returns a tailscaled config file name in
// format expected by containerboot for the given CapVer.
func TailscaledConfigFileName(cap tailcfg.CapabilityVersion) string {
return fmt.Sprintf("cap-%v.hujson", cap)
}
// CapVerFromFileName parses the capability version from a tailscaled
// config file name previously generated by TailscaledConfigFileNameForCap.
func CapVerFromFileName(name string) (tailcfg.CapabilityVersion, error) {
if name == "tailscaled" {
return 0, nil
}
var cap tailcfg.CapabilityVersion
_, err := fmt.Sscanf(name, "cap-%d.hujson", &cap)
return cap, err
}
// ResolveViaDomain parses an FQDN (with or without trailing dot) as a
// 4via6 domain in the format "<ipv4-with-hyphens>-via-<siteID>[.domain]"
// and returns the synthesized 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(name string) (netip.Addr, bool) {
// The minimum length of a valid 4via6 FQDN i.e. "0-0-0-0-via-X".
const minFQDNLength = 13
fqdn := strings.TrimSuffix(name, ".")
if len(fqdn) < minFQDNLength {
return netip.Addr{}, false // too short to be valid
}
if !strings.Contains(fqdn, "-via-") {
return netip.Addr{}, false
}
firstLabel, domain, _ := strings.Cut(fqdn, ".")
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
}
prefix, 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(prefix), netip.PrefixFrom(ip4, ip4.BitLen()))
return out.Addr(), true
}
// TruncateLabelValue truncates a Kubernetes label value to fit within the
// 63-character limit. If the value exceeds the limit, it is truncated and a
// short hash suffix is appended to preserve uniqueness.
func TruncateLabelValue(val string) string {
const maxLen = 63
if len(val) <= maxLen {
return val
}
hash := sha256.Sum256([]byte(val))
suffix := hex.EncodeToString(hash[:4]) // 8 hex chars
truncated := val[:maxLen-len(suffix)-1]
return truncated + "-" + suffix
}