mirror of
https://github.com/tailscale/tailscale.git
synced 2026-06-24 07:52:47 -04:00
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>
111 lines
3.5 KiB
Go
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
|
|
}
|