From 6f265f2baaa95e0c74e94ee0a60623b05c42df94 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 21 Oct 2026 16:20:36 +0000 Subject: [PATCH] policy/v2: write a single-address via destination bare Via rules wrote every destination with its prefix bits; ACL and grant rules already drop them for a single address. Both use one formatter. --- hscontrol/policy/v2/compiled.go | 2 +- hscontrol/policy/v2/filter.go | 22 +++++++++++-------- .../policy/v2/tailscale_grants_compat_test.go | 5 ----- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/hscontrol/policy/v2/compiled.go b/hscontrol/policy/v2/compiled.go index 3bc0f7907..4f6d322c5 100644 --- a/hscontrol/policy/v2/compiled.go +++ b/hscontrol/policy/v2/compiled.go @@ -1027,7 +1027,7 @@ func compileViaForNode( destPorts = append( destPorts, tailcfg.NetPortRange{ - IP: prefix.String(), + IP: filterIP(prefix), Ports: port, }, ) diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go index 54d93b4fb..53dc7e915 100644 --- a/hscontrol/policy/v2/filter.go +++ b/hscontrol/policy/v2/filter.go @@ -173,16 +173,10 @@ func (pol *Policy) destinationsToNetPortRange( for _, pref := range prefixes { for _, port := range ports { - pr := tailcfg.NetPortRange{ - IP: pref.String(), + ret = append(ret, tailcfg.NetPortRange{ + IP: filterIP(pref), Ports: port, - } - // Drop the prefix bits if its a single IP. - if pref.IsSingleIP() { - pr.IP = pref.Addr().String() - } - - ret = append(ret, pr) + }) } } } @@ -190,6 +184,16 @@ func (pol *Policy) destinationsToNetPortRange( return ret } +// filterIP writes pref as a filter destination: a single address bare, +// any other prefix with its bits, as Tailscale SaaS writes them. +func filterIP(pref netip.Prefix) string { + if pref.IsSingleIP() { + return pref.Addr().String() + } + + return pref.String() +} + // compileFilterRulesForNode compiles filter rules for a specific node. func (pol *Policy) compileFilterRulesForNode( users types.Users, diff --git a/hscontrol/policy/v2/tailscale_grants_compat_test.go b/hscontrol/policy/v2/tailscale_grants_compat_test.go index c084b4344..a8799ef81 100644 --- a/hscontrol/policy/v2/tailscale_grants_compat_test.go +++ b/hscontrol/policy/v2/tailscale_grants_compat_test.go @@ -172,9 +172,6 @@ var grantSkipReasons = map[string]string{ // Checks keyed //filter: a listed check that matches // fails, so a fix must delete its entry. - "via-grant-v63/blastoise/filter": viaDstAddressForm, - "via-grant-v63/squirtle/filter": viaDstAddressForm, - "grant-dxp-comma-ports/refused": grantPortList, // Contiguous pinned addresses; TestViaGrantHACompat compares these @@ -195,8 +192,6 @@ var grantSkipReasons = map[string]string{ const grantPortList = "GRANT_PORT_LIST: SaaS refuses a comma port list in a grant ip entry (only one port range allowed); headscale accepts it" -const viaDstAddressForm = "FILTER_ADDRESS_FORM: SaaS writes a single-address via destination as a bare address, headscale as a /32 prefix" - // TestGrantsCompat is a data-driven test that loads all GRANT-*.json // test files captured from Tailscale SaaS and compares headscale's grants // engine output against the real Tailscale behavior.