// 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 "-via-[.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 }