util: consolidate parsing and encoding helpers

This commit is contained in:
Kristoffer Dalby
2026-06-16 07:53:29 +00:00
parent 45e6e90d11
commit fdc7e26c8a
7 changed files with 64 additions and 93 deletions
+1 -8
View File
@@ -120,12 +120,5 @@ func (m *Match) DestsIsTheInternet() bool {
// Superset-of-[util.TheInternet] check handles merged filter rules
// where the internet prefixes are combined with other dests.
theInternet := util.TheInternet()
for _, prefix := range theInternet.Prefixes() {
if !m.dests.ContainsPrefix(prefix) {
return false
}
}
return true
return util.IPSetSubsetOf(util.TheInternet(), m.dests)
}
+1 -16
View File
@@ -6,7 +6,6 @@ import (
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx"
"tailscale.com/tailcfg"
)
@@ -66,7 +65,7 @@ func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcf
// Exit-route advertisers need rules targeting the
// public internet so the kernel filter accepts
// traffic forwarded by autogroup:internet sources.
if hasExitRoutes && ipSetSubsetOf(expanded, util.TheInternet()) {
if hasExitRoutes && util.IPSetSubsetOf(expanded, util.TheInternet()) {
dests = append(dests, dest)
}
}
@@ -83,20 +82,6 @@ func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcf
return ret
}
func ipSetSubsetOf(candidate, container *netipx.IPSet) bool {
if candidate == nil || container == nil {
return false
}
for _, pref := range candidate.Prefixes() {
if !container.ContainsPrefix(pref) {
return false
}
}
return true
}
// reduceCapGrantRule filters a [tailcfg.CapGrant] rule to only include
// [tailcfg.CapGrant] entries whose Dsts match the given node's IPs. When a
// broad prefix (e.g. 100.64.0.0/10 from dst:*) contains a node's IP, it is
+3 -11
View File
@@ -12,10 +12,6 @@ import (
// This is borrowed from, and updated to use [netipx.IPSet]
// https://github.com/tailscale/tailscale/blob/71029cea2ddf82007b80f465b256d027eab0f02d/wgengine/filter/tailcfg.go#L97-L162
// TODO(kradalby): contribute upstream and make public.
var (
zeroIP4 = netip.AddrFrom4([4]byte{})
zeroIP6 = netip.AddrFrom16([16]byte{})
)
// parseIPSet parses arg as one:
//
@@ -30,8 +26,8 @@ var (
func ParseIPSet(arg string, bits *int) (*netipx.IPSet, error) {
var ipSet netipx.IPSetBuilder
if arg == "*" {
ipSet.AddPrefix(netip.PrefixFrom(zeroIP4, 0))
ipSet.AddPrefix(netip.PrefixFrom(zeroIP6, 0))
ipSet.AddPrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
ipSet.AddPrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
return ipSet.IPSet()
}
@@ -90,13 +86,9 @@ func ParseIPSet(arg string, bits *int) (*netipx.IPSet, error) {
}
func GetIPPrefixEndpoints(na netip.Prefix) (netip.Addr, netip.Addr) {
var network, broadcast netip.Addr
ipRange := netipx.RangeOfPrefix(na)
network = ipRange.From()
broadcast = ipRange.To()
return network, broadcast
return ipRange.From(), ipRange.To()
}
func StringToIPPrefix(prefixes []string) ([]netip.Prefix, error) {
+6 -13
View File
@@ -130,8 +130,8 @@ func GenerateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
// here we generate the base domain (e.g., 100.in-addr.arpa., 16.172.in-addr.arpa., etc.)
rdnsSlice := []string{}
for i := lastOctet - 1; i >= 0; i-- {
rdnsSlice = append(rdnsSlice, strconv.FormatUint(uint64(netRange.IP[i]), 10))
for _, b := range slices.Backward(netRange.IP[:lastOctet]) {
rdnsSlice = append(rdnsSlice, strconv.FormatUint(uint64(b), 10))
}
rdnsSlice = append(rdnsSlice, "in-addr.arpa.")
@@ -158,13 +158,7 @@ func GenerateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
maskBits, _ := netipx.PrefixIPNet(ipPrefix).Mask.Size()
expanded := ipPrefix.Addr().StringExpanded()
nibbleStr := strings.Map(func(r rune) rune {
if r == ':' {
return -1
}
return r
}, expanded)
nibbleStr := strings.ReplaceAll(expanded, ":", "")
// TODO?: that does not look the most efficient implementation,
// but the inputs are not so long as to cause problems,
@@ -172,12 +166,11 @@ func GenerateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
// function is called only once over the lifetime of a server process.
prefixConstantParts := []string{}
for i := range maskBits / nibbleLen {
prefixConstantParts = append(
[]string{string(nibbleStr[i])},
prefixConstantParts...,
)
prefixConstantParts = append(prefixConstantParts, string(nibbleStr[i]))
}
slices.Reverse(prefixConstantParts)
makeDomain := func(variablePrefix ...string) (dnsname.FQDN, error) {
prefix := strings.Join(append(variablePrefix, prefixConstantParts...), ".")
+20 -5
View File
@@ -26,13 +26,12 @@ func PrefixesToString(prefixes []netip.Prefix) []string {
}
func MustStringsToPrefixes(strings []string) []netip.Prefix {
ret := make([]netip.Prefix, 0, len(strings))
for _, str := range strings {
prefix := netip.MustParsePrefix(str)
ret = append(ret, prefix)
prefixes, err := StringToIPPrefix(strings)
if err != nil {
panic(err)
}
return ret
return prefixes
}
// TheInternet returns the [netipx.IPSet] for the Internet.
@@ -61,3 +60,19 @@ var TheInternet = sync.OnceValue(func() *netipx.IPSet {
return theInternetSet
})
// IPSetSubsetOf reports whether every prefix of candidate is contained in
// container. A nil candidate or container returns false.
func IPSetSubsetOf(candidate, container *netipx.IPSet) bool {
if candidate == nil || container == nil {
return false
}
for _, pref := range candidate.Prefixes() {
if !container.ContainsPrefix(pref) {
return false
}
}
return true
}
+11 -11
View File
@@ -21,20 +21,20 @@ var AddrPortComparer = cmp.Comparer(func(x, y netip.AddrPort) bool {
return x == y
})
var MkeyComparer = cmp.Comparer(func(x, y key.MachinePublic) bool {
return x.String() == y.String()
})
func strComparer[T interface{ String() string }]() cmp.Option {
return cmp.Comparer(func(x, y T) bool {
return x.String() == y.String()
})
}
var NkeyComparer = cmp.Comparer(func(x, y key.NodePublic) bool {
return x.String() == y.String()
})
var DkeyComparer = cmp.Comparer(func(x, y key.DiscoPublic) bool {
return x.String() == y.String()
})
var (
MkeyComparer = strComparer[key.MachinePublic]()
NkeyComparer = strComparer[key.NodePublic]()
DkeyComparer = strComparer[key.DiscoPublic]()
)
var ViewSliceIPProtoComparer = cmp.Comparer(views.SliceEqual[ipproto.Proto])
var Comparers []cmp.Option = []cmp.Option{
var Comparers = []cmp.Option{
IPComparer, PrefixComparer, AddrPortComparer, MkeyComparer, NkeyComparer, DkeyComparer, ViewSliceIPProtoComparer,
}
+22 -29
View File
@@ -24,13 +24,9 @@ var (
)
func TailscaleVersionNewerOrEqual(minimum, toCheck string) bool {
if cmpver.Compare(minimum, toCheck) <= 0 ||
return cmpver.Compare(minimum, toCheck) <= 0 ||
toCheck == "unstable" ||
toCheck == "head" {
return true
}
return false
toCheck == "head"
}
// ParseLoginURLFromCLILogin parses the output of the tailscale up command to extract the login URL.
@@ -94,6 +90,19 @@ type Traceroute struct {
Err error
}
// parseLatency parses a traceroute latency token such as "1.5" or "<1",
// returning the duration rounded to the nearest microsecond. The second
// return value reports whether the token was a valid number.
func parseLatency(tok string) (time.Duration, bool) {
ms, err := strconv.ParseFloat(strings.TrimPrefix(tok, "<"), 64)
if err != nil {
return 0, false
}
// Round to nearest microsecond to avoid floating point precision issues.
return time.Duration(ms * float64(time.Millisecond)).Round(time.Microsecond), true
}
// ParseTraceroute parses the output of the traceroute command and returns a [Traceroute] struct.
func ParseTraceroute(output string) (Traceroute, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
@@ -185,13 +194,8 @@ func ParseTraceroute(output string) (Traceroute, error) {
break
}
// Extract and remove the latency from the beginning
latStr := strings.TrimPrefix(remainder[latMatch[2]:latMatch[3]], "<")
ms, err := strconv.ParseFloat(latStr, 64)
if err == nil {
// Round to nearest microsecond to avoid floating point precision issues
duration := time.Duration(ms * float64(time.Millisecond))
latencies = append(latencies, duration.Round(time.Microsecond))
if d, ok := parseLatency(remainder[latMatch[2]:latMatch[3]]); ok {
latencies = append(latencies, d)
}
remainder = strings.TrimSpace(remainder[latMatch[1]:])
@@ -232,14 +236,8 @@ func ParseTraceroute(output string) (Traceroute, error) {
latencyMatches := latencyRegex.FindAllStringSubmatch(remainder, -1)
for _, match := range latencyMatches {
if len(match) > 1 {
// Remove '<' prefix if present (e.g., "<1 ms")
latStr := strings.TrimPrefix(match[1], "<")
ms, err := strconv.ParseFloat(latStr, 64)
if err == nil {
// Round to nearest microsecond to avoid floating point precision issues
duration := time.Duration(ms * float64(time.Millisecond))
latencies = append(latencies, duration.Round(time.Microsecond))
if d, ok := parseLatency(match[1]); ok {
latencies = append(latencies, d)
}
}
}
@@ -269,15 +267,10 @@ func ParseTraceroute(output string) (Traceroute, error) {
}
func IsCI() bool {
if _, ok := os.LookupEnv("CI"); ok {
return true
}
_, ci := os.LookupEnv("CI")
_, gh := os.LookupEnv("GITHUB_RUN_ID")
if _, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
return true
}
return false
return ci || gh
}
// GenerateRegistrationKey generates a vanity key for tracking web authentication