policy: add NodeNeedsPeerRecompute predicate

Reports whether a node's online/offline transition forces peers to recompute their netmap. True for subnet routers, relay targets (tailscale.com/cap/relay), and via targets; false otherwise.

The relay-target IP set and via-target tag set are precompiled from the grants in updateLocked, alongside the existing filter, so the per-node check is a cheap set lookup. Keyed on the node itself, so an ordinary node in a tailnet that uses relay or via for other nodes is still classified as not needing a recompute.

Updates #3293
This commit is contained in:
Kristoffer Dalby 2026-06-03 10:25:20 +00:00
parent 171fd7a3c5
commit bceac495f9
4 changed files with 253 additions and 0 deletions

View file

@ -594,6 +594,58 @@ func hasPerNodeGrants(grants []compiledGrant) bool {
return false
}
// collectRelayTargetIPs returns the set of IPs that are destinations of a
// tailscale.com/cap/relay grant. A node whose IP is in this set is a relay
// target: when it goes offline, peers holding a PeerRelay allocation through
// it must recompute their netmap to drop the now-dead allocation. The relay
// cap is carried on each grant's [tailcfg.CapGrant] with Dsts set to the
// resolved relay destinations (see [Policy.compileOtherDests]); the reversed
// companion rule carries [tailcfg.PeerCapabilityRelayTarget] instead and is
// intentionally skipped.
func collectRelayTargetIPs(grants []compiledGrant) (*netipx.IPSet, error) {
var b netipx.IPSetBuilder
for i := range grants {
for _, rule := range grants[i].rules {
for _, cg := range rule.CapGrant {
if _, ok := cg.CapMap[tailcfg.PeerCapabilityRelay]; !ok {
continue
}
for _, dst := range cg.Dsts {
b.AddPrefix(dst)
}
}
}
}
return b.IPSet()
}
// collectViaTargetTags returns the set of tags used as via targets across all
// grants. A node carrying any of these tags is a via target: peers steering
// traffic through it must recompute when it goes offline. Returns nil when no
// via grants exist.
func collectViaTargetTags(grants []compiledGrant) map[Tag]struct{} {
var tags map[Tag]struct{}
for i := range grants {
if grants[i].via == nil {
continue
}
for _, t := range grants[i].via.viaTags {
if tags == nil {
tags = make(map[Tag]struct{})
}
tags[t] = struct{}{}
}
}
return tags
}
// globalFilterRules extracts global filter rules from [compiledGrant]s.
// Via grants produce no global rules (they are per-node only); regular
// grants contribute their full pre-compiled ruleset; self grants