mapper, policy/v2: stamp suggest-exit-node on Peer.CapMap when exit routes approved
The Tailscale client surfaces 'use this peer as your exit node' when the peer's CapMap carries the tailcfg.NodeAttrSuggestExitNode cap. SaaS emits it only on peers whose advertised exit routes are approved -- not every peer that just has the cap in its own nodeAttrs slot. policyv2.PeerCapMap encodes that emission rule: it walks the peer's own self-CapMap (built from compileNodeAttrs) and surfaces the gated entries (today just suggest-exit-node when the peer IsExitNode). Mapper.buildTailPeers calls it for each peer instead of merging the peer's full nodeAttrs CapMap onto its peer view. allCapMaps snapshots the full per-node CapMap once per peer-list build so pm.mu is acquired once rather than per peer.
This commit is contained in:
parent
078b9e308f
commit
b3f795f0b4
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
"github.com/juanfont/headscale/hscontrol/policy"
|
||||||
|
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
@ -264,6 +265,11 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
|||||||
changedViews = peers
|
changedViews = peers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot the per-node policy CapMap once per peer-list build
|
||||||
|
// instead of locking the policy manager per peer. The per-call
|
||||||
|
// path used to take pm.mu N times for an N-peer response.
|
||||||
|
allCapMaps := b.mapper.state.NodeCapMaps()
|
||||||
|
|
||||||
// Build tail nodes with per-peer via-aware route function.
|
// Build tail nodes with per-peer via-aware route function.
|
||||||
tailPeers := make([]*tailcfg.Node, 0, changedViews.Len())
|
tailPeers := make([]*tailcfg.Node, 0, changedViews.Len())
|
||||||
|
|
||||||
@ -275,21 +281,15 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each peer's CapMap travels alongside the peer entry --
|
// [tailcfg.Node.CapMap] on a peer carries the small set of
|
||||||
// Tailscale's client reads it for `NodeAttrSuggestExitNode`,
|
// caps the Tailscale client reads from the peer view rather
|
||||||
// `NodeAttrDNSSubdomainResolve`, and other peer-self attrs
|
// than the self view (suggest-exit-node, dns-subdomain-resolve
|
||||||
// (see tstest/integration/testcontrol/testcontrol.go:1350,
|
// — see ipn/ipnlocal/local.go:7534 and node_backend.go:745).
|
||||||
// ipn/ipnlocal/local.go:7562, ipn/ipnlocal/node_backend.go:745).
|
// The Tailscale-hosted control plane stamps these only when
|
||||||
// TailNode already stamped the baseline; merge the
|
// the peer satisfies the cap's emission condition; every other
|
||||||
// peer's own policy nodeAttrs delta on top so peer-side
|
// cap stays off the peer view, leaving CapMap empty for most
|
||||||
// consumers see the same value the peer sees on its self entry.
|
// peers. [policyv2.PeerCapMap] encodes those conditions.
|
||||||
if policyCaps := b.mapper.state.NodeCapMap(peer.ID()); len(policyCaps) > 0 {
|
tn.CapMap = policyv2.PeerCapMap(peer, allCapMaps[peer.ID()])
|
||||||
if tn.CapMap == nil {
|
|
||||||
tn.CapMap = make(tailcfg.NodeCapMap, len(policyCaps))
|
|
||||||
}
|
|
||||||
|
|
||||||
maps.Copy(tn.CapMap, policyCaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
tailPeers = append(tailPeers, tn)
|
tailPeers = append(tailPeers, tn)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user