From aa7b2ab404498fb445f14a812a484515b0083119 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 31 Oct 2026 14:06:29 +0000 Subject: [PATCH] policyutil: send a router no capability for its subnet via-grant-v84 and v86 show SaaS sending none. Updates #3626 --- hscontrol/policy/policyutil/reduce.go | 27 ++++--------------- hscontrol/policy/policyutil/reduce_test.go | 31 +++++----------------- 2 files changed, 12 insertions(+), 46 deletions(-) diff --git a/hscontrol/policy/policyutil/reduce.go b/hscontrol/policy/policyutil/reduce.go index 020b8ddea..a42f262bb 100644 --- a/hscontrol/policy/policyutil/reduce.go +++ b/hscontrol/policy/policyutil/reduce.go @@ -131,7 +131,9 @@ func destinationNeedsOwnAddressExpansion( // [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 // narrowed to the node's specific /32 or /128 prefix. Returns nil if -// no [tailcfg.CapGrant] entries are relevant to this node. +// no [tailcfg.CapGrant] entries are relevant to this node. A destination the +// node only routes is not relevant: via-grant-v84 and v86 show SaaS sending a +// router no capability for its subnet. func reduceCapGrantRule( node types.NodeView, rule tailcfg.FilterRule, @@ -139,7 +141,6 @@ func reduceCapGrantRule( var capGrants []tailcfg.CapGrant nodeIPs := node.IPs() - subnetRoutes := node.SubnetRoutes() for _, cg := range rule.CapGrant { // Collect the node's IPs that fall within any of this @@ -164,27 +165,9 @@ func reduceCapGrantRule( } } - // Asymmetric on purpose: the IP-match loop above narrows broad - // prefixes to node-specific /32 or /128 so peers receive only - // the minimum routing surface. The route-match loop below - // preserves the original prefix so the subnet-serving node - // receives the full CapGrant scope. [types.NodeView.SubnetRoutes] - // excludes both unapproved and exit routes, matching Tailscale - // SaaS behavior. - for _, dst := range cg.Dsts { - for _, subnetRoute := range subnetRoutes { - if dst.Overlaps(subnetRoute) { - // For route overlaps, keep the original prefix. - matchingDsts = append(matchingDsts, dst) - } - } - } - if len(matchingDsts) > 0 { - // A Dst can be appended twice when a broad prefix both - // contains a node IP and overlaps one of its approved - // subnet routes. Sort + Compact dedups; [netip.Prefix] is - // comparable so Compact works with ==. + // Several Dsts can narrow to the same node IP. Sort + Compact + // dedups; [netip.Prefix] is comparable so Compact works with ==. slices.SortFunc(matchingDsts, netip.Prefix.Compare) matchingDsts = slices.Compact(matchingDsts) diff --git a/hscontrol/policy/policyutil/reduce_test.go b/hscontrol/policy/policyutil/reduce_test.go index 06bca135e..9fde0088b 100644 --- a/hscontrol/policy/policyutil/reduce_test.go +++ b/hscontrol/policy/policyutil/reduce_test.go @@ -1072,25 +1072,9 @@ func TestReduceFilterRulesCapGrant(t *testing.T) { }, }, }, - want: []tailcfg.FilterRule{ - { - SrcIPs: []string{"*"}, - CapGrant: []tailcfg.CapGrant{ - { - Dsts: []netip.Prefix{ - // 10.0.0.0/8 doesn't contain node's - // CGNAT IP (100.64.0.1), so no IP - // narrowing. Only route overlap kept - // as original prefix. - netip.MustParsePrefix("10.0.0.0/8"), - }, - CapMap: tailcfg.PeerCapMap{ - "tailscale.com/cap/relay-target": nil, - }, - }, - }, - }, - }, + // 10.0.0.0/8 doesn't contain the node's CGNAT IP (100.64.0.1), + // and routing part of it delivers no capability. + want: []tailcfg.FilterRule{}, }, { name: "capgrant-exit-route-skipped", @@ -1264,10 +1248,9 @@ func TestReduceFilterRulesCapGrant(t *testing.T) { want: []tailcfg.FilterRule{}, }, { - // A broad prefix that both contains the node IP (/32 narrow) - // and overlaps one of its approved subnet routes (route - // preserved) would otherwise be emitted twice. - name: "capgrant-ip-and-route-overlap-dedup", + // Broad prefixes that contain the node IP narrow to the same + // /32, and routing them keeps nothing more. + name: "capgrant-broad-dsts-narrow-once", node: &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), @@ -1287,6 +1270,7 @@ func TestReduceFilterRulesCapGrant(t *testing.T) { { Dsts: []netip.Prefix{ netip.MustParsePrefix("100.64.0.0/24"), + netip.MustParsePrefix("100.64.0.0/16"), }, CapMap: tailcfg.PeerCapMap{ "tailscale.com/cap/relay-target": nil, @@ -1302,7 +1286,6 @@ func TestReduceFilterRulesCapGrant(t *testing.T) { { Dsts: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), - netip.MustParsePrefix("100.64.0.0/24"), }, CapMap: tailcfg.PeerCapMap{ "tailscale.com/cap/relay-target": nil,