mapper, state: deliver nodeAttrs through MapResponse and harden nextdns DoH rewrite
WithSelfNode and buildTailPeers merge each node's policy CapMap
into the tailcfg.Node.CapMap they emit. State.NodeCapMap and
State.NodeCapMaps wrap the policy manager: NodeCapMap returns a
defensive clone per call; NodeCapMaps snapshots the full per-node
map once for batched callers, amortising pm.mu acquisition across
a peer build.
generateDNSConfig grew a per-node CapMap argument so it can apply
nodeAttr-driven DNS overlays. The nextdns DoH rewrite hardens against
policy-controlled inputs:
- nextDNSDoHHost anchors the prefix match instead of substring,
so a hostile resolver URL cannot smuggle a nextdns hostname in
a path or query.
- nextDNSProfileFromCapMap accepts only profile names matching
[A-Za-z0-9._-]{1,64} and picks the lexicographically first when
multiple are granted -- deterministic, no shell metacharacters
or URL fragments through.
- addNextDNSMetadata composes the rewritten URL via url.Parse +
url.Values rather than fmt.Sprintf, so existing query strings
on the resolver URL survive and metadata cannot inject a new
component.
WithTaildropEnabled in servertest controls cfg.Taildrop.Enabled per
test so cap/file-sharing emission can be toggled in tests that need
to verify the off path.
This commit is contained in:
parent
a4f05b0962
commit
6fcff9e352
@ -1,6 +1,7 @@
|
|||||||
package mapper
|
package mapper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@ -90,6 +91,14 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if policyCaps := b.mapper.state.NodeCapMap(nv.ID()); len(policyCaps) > 0 {
|
||||||
|
if tailnode.CapMap == nil {
|
||||||
|
tailnode.CapMap = make(tailcfg.NodeCapMap, len(policyCaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(tailnode.CapMap, policyCaps)
|
||||||
|
}
|
||||||
|
|
||||||
b.resp.Node = tailnode
|
b.resp.Node = tailnode
|
||||||
|
|
||||||
return b
|
return b
|
||||||
@ -158,7 +167,7 @@ func (b *MapResponseBuilder) WithDNSConfig() *MapResponseBuilder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
b.resp.DNSConfig = generateDNSConfig(b.mapper.cfg, node)
|
b.resp.DNSConfig = generateDNSConfig(b.mapper.cfg, node, b.mapper.state.NodeCapMap(node.ID()))
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@ -266,6 +275,22 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each peer's CapMap travels alongside the peer entry --
|
||||||
|
// Tailscale's client reads it for `NodeAttrSuggestExitNode`,
|
||||||
|
// `NodeAttrDNSSubdomainResolve`, and other peer-self attrs
|
||||||
|
// (see tstest/integration/testcontrol/testcontrol.go:1350,
|
||||||
|
// ipn/ipnlocal/local.go:7562, ipn/ipnlocal/node_backend.go:745).
|
||||||
|
// TailNode already stamped the baseline; merge the
|
||||||
|
// peer's own policy nodeAttrs delta on top so peer-side
|
||||||
|
// consumers see the same value the peer sees on its self entry.
|
||||||
|
if policyCaps := b.mapper.state.NodeCapMap(peer.ID()); len(policyCaps) > 0 {
|
||||||
|
if tn.CapMap == nil {
|
||||||
|
tn.CapMap = make(tailcfg.NodeCapMap, len(policyCaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(tn.CapMap, policyCaps)
|
||||||
|
}
|
||||||
|
|
||||||
tailPeers = append(tailPeers, tn)
|
tailPeers = append(tailPeers, tn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -114,9 +115,25 @@ func generateUserProfiles(
|
|||||||
return profiles
|
return profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nextDNSAttrPrefix is the form Tailscale uses for per-node NextDNS profile
|
||||||
|
// selection: an "attr" entry of "nextdns:<profile-id>" overrides the resolver
|
||||||
|
// path, and "nextdns:no-device-info" suppresses the metadata-appending step.
|
||||||
|
// See https://tailscale.com/docs/integrations/nextdns.
|
||||||
|
const (
|
||||||
|
nextDNSAttrPrefix = "nextdns:"
|
||||||
|
nextDNSAttrNoInfo tailcfg.NodeCapability = "nextdns:no-device-info"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nextDNSProfileRE bounds the characters accepted in a `nextdns:<profile>`
|
||||||
|
// suffix. NextDNS profile IDs are short alphanumeric strings; restricting
|
||||||
|
// to that charset prevents a policy author from injecting `?`, `/`, `@`,
|
||||||
|
// or `..` into the resolver URL via a crafted cap name.
|
||||||
|
var nextDNSProfileRE = regexp.MustCompile(`^[A-Za-z0-9._-]{1,64}$`)
|
||||||
|
|
||||||
func generateDNSConfig(
|
func generateDNSConfig(
|
||||||
cfg *types.Config,
|
cfg *types.Config,
|
||||||
node types.NodeView,
|
node types.NodeView,
|
||||||
|
capMap tailcfg.NodeCapMap,
|
||||||
) *tailcfg.DNSConfig {
|
) *tailcfg.DNSConfig {
|
||||||
if cfg.TailcfgDNSConfig == nil {
|
if cfg.TailcfgDNSConfig == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -124,32 +141,129 @@ func generateDNSConfig(
|
|||||||
|
|
||||||
dnsConfig := cfg.TailcfgDNSConfig.Clone()
|
dnsConfig := cfg.TailcfgDNSConfig.Clone()
|
||||||
|
|
||||||
|
profile := nextDNSProfileFromCapMap(capMap)
|
||||||
|
if profile != "" {
|
||||||
|
applyNextDNSProfile(dnsConfig.Resolvers, profile)
|
||||||
|
applyNextDNSProfile(dnsConfig.FallbackResolvers, profile)
|
||||||
|
|
||||||
|
for suffix, rs := range dnsConfig.Routes {
|
||||||
|
applyNextDNSProfile(rs, profile)
|
||||||
|
dnsConfig.Routes[suffix] = rs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, suppressMetadata := capMap[nextDNSAttrNoInfo]; !suppressMetadata {
|
||||||
addNextDNSMetadata(dnsConfig.Resolvers, node)
|
addNextDNSMetadata(dnsConfig.Resolvers, node)
|
||||||
|
addNextDNSMetadata(dnsConfig.FallbackResolvers, node)
|
||||||
|
|
||||||
|
for suffix, rs := range dnsConfig.Routes {
|
||||||
|
addNextDNSMetadata(rs, node)
|
||||||
|
dnsConfig.Routes[suffix] = rs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return dnsConfig
|
return dnsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any nextdns DoH resolvers are present in the list of resolvers it will
|
// nextDNSProfileFromCapMap returns the policy-selected
|
||||||
// take metadata from the node metadata and instruct tailscale to add it
|
// `nextdns:<profile>` value on the node, or the empty string when none
|
||||||
// to the requests. This makes it possible to identify from which device the
|
// is set or the cap is malformed. The reserved
|
||||||
// requests come in the NextDNS dashboard.
|
// `nextdns:no-device-info` string is not a profile — it controls
|
||||||
|
// metadata appending and is handled separately.
|
||||||
//
|
//
|
||||||
// This will produce a resolver like:
|
// The profile pick is deterministic across reloads: cap keys are
|
||||||
// `https://dns.nextdns.io/<nextdns-id>?device_name=node-name&device_model=linux&device_ip=100.64.0.1`
|
// gathered, sorted, and the first valid profile wins. Map iteration
|
||||||
|
// order in Go is randomised, so taking the literal first match would
|
||||||
|
// cause the chosen profile to flip between reloads when a node has
|
||||||
|
// multiple `nextdns:` caps. The profile string is also validated
|
||||||
|
// against [nextDNSProfileRE] so a crafted cap cannot inject path or
|
||||||
|
// query characters into the resolver URL.
|
||||||
|
func nextDNSProfileFromCapMap(capMap tailcfg.NodeCapMap) string {
|
||||||
|
if len(capMap) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates := make([]string, 0, len(capMap))
|
||||||
|
|
||||||
|
for cap := range capMap {
|
||||||
|
if cap == nextDNSAttrNoInfo {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, ok := strings.CutPrefix(string(cap), nextDNSAttrPrefix)
|
||||||
|
if !ok || profile == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nextDNSProfileRE.MatchString(profile) {
|
||||||
|
log.Warn().
|
||||||
|
Str("cap", string(cap)).
|
||||||
|
Msg("nextdns profile rejected: must match [A-Za-z0-9._-]{1,64}")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = append(candidates, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(candidates)
|
||||||
|
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextDNSDoHHost matches a NextDNS DoH resolver address. The check is
|
||||||
|
// anchored on the host segment so a typo-squatted operator-configured
|
||||||
|
// resolver such as `https://dns.nextdns.io.attacker.example/x` does
|
||||||
|
// not slip through.
|
||||||
|
func nextDNSDoHHost(addr string) bool {
|
||||||
|
return addr == nextDNSDoHPrefix ||
|
||||||
|
strings.HasPrefix(addr, nextDNSDoHPrefix+"/") ||
|
||||||
|
strings.HasPrefix(addr, nextDNSDoHPrefix+"?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyNextDNSProfile rewrites every NextDNS DoH resolver to point at
|
||||||
|
// the given profile, dropping any existing profile path or query. Per
|
||||||
|
// the Tailscale spec the per-node profile overrides the global value,
|
||||||
|
// so the rewrite is unconditional rather than additive.
|
||||||
|
func applyNextDNSProfile(resolvers []*dnstype.Resolver, profile string) {
|
||||||
|
for _, resolver := range resolvers {
|
||||||
|
if !nextDNSDoHHost(resolver.Addr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.Addr = nextDNSDoHPrefix + "/" + profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNextDNSMetadata appends device metadata as a query string to
|
||||||
|
// every NextDNS DoH resolver. Existing query parameters on the
|
||||||
|
// resolver address are preserved by parsing the URL and merging into
|
||||||
|
// its [url.URL.RawQuery] rather than concatenating with `?`.
|
||||||
func addNextDNSMetadata(resolvers []*dnstype.Resolver, node types.NodeView) {
|
func addNextDNSMetadata(resolvers []*dnstype.Resolver, node types.NodeView) {
|
||||||
for _, resolver := range resolvers {
|
for _, resolver := range resolvers {
|
||||||
if strings.HasPrefix(resolver.Addr, nextDNSDoHPrefix) {
|
if !nextDNSDoHHost(resolver.Addr) {
|
||||||
attrs := url.Values{
|
continue
|
||||||
"device_name": []string{node.Hostname()},
|
|
||||||
"device_model": []string{node.Hostinfo().OS()},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(node.IPs()) > 0 {
|
u, err := url.Parse(resolver.Addr)
|
||||||
attrs.Add("device_ip", node.IPs()[0].String())
|
if err != nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver.Addr = fmt.Sprintf("%s?%s", resolver.Addr, attrs.Encode())
|
q := u.Query()
|
||||||
|
q.Set("device_name", node.Hostname())
|
||||||
|
q.Set("device_model", node.Hostinfo().OS())
|
||||||
|
|
||||||
|
if ips := node.IPs(); len(ips) > 0 {
|
||||||
|
q.Set("device_ip", ips[0].String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
resolver.Addr = u.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,7 @@ func TestDNSConfigMapResponse(t *testing.T) {
|
|||||||
TailcfgDNSConfig: &dnsConfigOrig,
|
TailcfgDNSConfig: &dnsConfigOrig,
|
||||||
},
|
},
|
||||||
nodeInShared1.View(),
|
nodeInShared1.View(),
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
|
if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
|
||||||
@ -76,3 +77,119 @@ func TestDNSConfigMapResponse(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNextDNSCapMapRendering(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mkConfig := func(addrs ...string) *types.Config {
|
||||||
|
resolvers := make([]*dnstype.Resolver, len(addrs))
|
||||||
|
for i, a := range addrs {
|
||||||
|
resolvers[i] = &dnstype.Resolver{Addr: a}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Config{
|
||||||
|
TailcfgDNSConfig: &tailcfg.DNSConfig{
|
||||||
|
Resolvers: resolvers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mkNode := func() types.NodeView {
|
||||||
|
return (&types.Node{
|
||||||
|
ID: 1,
|
||||||
|
Hostname: "node1",
|
||||||
|
IPv4: iap("100.64.0.1"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{OS: "linux"},
|
||||||
|
}).View()
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolverAddr extracts the first resolver's address with a
|
||||||
|
// bounds check. Without it, a regression that drops the
|
||||||
|
// resolver list would nil-panic instead of failing cleanly.
|
||||||
|
resolverAddr := func(t *testing.T, got *tailcfg.DNSConfig) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got == nil {
|
||||||
|
t.Fatalf("generateDNSConfig returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got.Resolvers) == 0 {
|
||||||
|
t.Fatalf("generateDNSConfig returned no Resolvers")
|
||||||
|
}
|
||||||
|
|
||||||
|
return got.Resolvers[0].Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no_capmap_metadata_appended", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := generateDNSConfig(
|
||||||
|
mkConfig("https://dns.nextdns.io/abc"),
|
||||||
|
mkNode(),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
want := "https://dns.nextdns.io/abc?device_ip=100.64.0.1&device_model=linux&device_name=node1"
|
||||||
|
if addr := resolverAddr(t, got); addr != want {
|
||||||
|
t.Errorf("addr = %q, want %q", addr, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("profile_overrides_global", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
capMap := tailcfg.NodeCapMap{
|
||||||
|
"nextdns:override": []tailcfg.RawMessage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := generateDNSConfig(
|
||||||
|
mkConfig("https://dns.nextdns.io/global"),
|
||||||
|
mkNode(),
|
||||||
|
capMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
want := "https://dns.nextdns.io/override?device_ip=100.64.0.1&device_model=linux&device_name=node1"
|
||||||
|
if addr := resolverAddr(t, got); addr != want {
|
||||||
|
t.Errorf("addr = %q, want %q", addr, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no_device_info_skips_metadata", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
capMap := tailcfg.NodeCapMap{
|
||||||
|
"nextdns:abc": []tailcfg.RawMessage{},
|
||||||
|
"nextdns:no-device-info": []tailcfg.RawMessage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := generateDNSConfig(
|
||||||
|
mkConfig("https://dns.nextdns.io/global"),
|
||||||
|
mkNode(),
|
||||||
|
capMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
want := "https://dns.nextdns.io/abc"
|
||||||
|
if addr := resolverAddr(t, got); addr != want {
|
||||||
|
t.Errorf("addr = %q, want %q", addr, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non_nextdns_resolver_untouched", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
capMap := tailcfg.NodeCapMap{
|
||||||
|
"nextdns:abc": []tailcfg.RawMessage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := generateDNSConfig(
|
||||||
|
mkConfig("https://dns.example.org/dns-query"),
|
||||||
|
mkNode(),
|
||||||
|
capMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
want := "https://dns.example.org/dns-query"
|
||||||
|
if addr := resolverAddr(t, got); addr != want {
|
||||||
|
t.Errorf("non-nextdns resolver was rewritten: %q", addr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type serverConfig struct {
|
|||||||
ephemeralTimeout time.Duration
|
ephemeralTimeout time.Duration
|
||||||
nodeExpiry time.Duration
|
nodeExpiry time.Duration
|
||||||
batcherWorkers int
|
batcherWorkers int
|
||||||
|
taildropEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultServerConfig() *serverConfig {
|
func defaultServerConfig() *serverConfig {
|
||||||
@ -50,6 +51,7 @@ func defaultServerConfig() *serverConfig {
|
|||||||
bufferedChanSize: 30,
|
bufferedChanSize: 30,
|
||||||
batcherWorkers: 1,
|
batcherWorkers: 1,
|
||||||
ephemeralTimeout: 30 * time.Second,
|
ephemeralTimeout: 30 * time.Second,
|
||||||
|
taildropEnabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +75,15 @@ func WithNodeExpiry(d time.Duration) ServerOption {
|
|||||||
return func(c *serverConfig) { c.nodeExpiry = d }
|
return func(c *serverConfig) { c.nodeExpiry = d }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTaildropEnabled toggles the Taildrop file-sharing feature.
|
||||||
|
// Defaults to true to match production. Pass false to verify
|
||||||
|
// behaviour when an operator has switched the toggle off — e.g.
|
||||||
|
// that [tailcfg.CapabilityFileSharing] is withheld from the
|
||||||
|
// always-on baseline.
|
||||||
|
func WithTaildropEnabled(enabled bool) ServerOption {
|
||||||
|
return func(c *serverConfig) { c.taildropEnabled = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
// NewServer creates and starts a Headscale test server.
|
// NewServer creates and starts a Headscale test server.
|
||||||
// The server is fully functional and accepts real Tailscale control
|
// The server is fully functional and accepts real Tailscale control
|
||||||
// protocol connections over Noise.
|
// protocol connections over Noise.
|
||||||
@ -111,6 +122,7 @@ func NewServer(tb testing.TB, opts ...ServerOption) *TestServer {
|
|||||||
Policy: types.PolicyConfig{
|
Policy: types.PolicyConfig{
|
||||||
Mode: types.PolicyModeDB,
|
Mode: types.PolicyModeDB,
|
||||||
},
|
},
|
||||||
|
Taildrop: types.TaildropConfig{Enabled: sc.taildropEnabled},
|
||||||
Tuning: types.Tuning{
|
Tuning: types.Tuning{
|
||||||
BatchChangeDelay: sc.batchDelay,
|
BatchChangeDelay: sc.batchDelay,
|
||||||
BatcherWorkers: sc.batcherWorkers,
|
BatcherWorkers: sc.batcherWorkers,
|
||||||
|
|||||||
@ -313,6 +313,18 @@ func (s *State) ReloadPolicy() ([]change.Change, error) {
|
|||||||
//nolint:prealloc // cs starts with one element and may grow
|
//nolint:prealloc // cs starts with one element and may grow
|
||||||
cs := []change.Change{change.PolicyChange()}
|
cs := []change.Change{change.PolicyChange()}
|
||||||
|
|
||||||
|
// Per-node selective self refresh for nodeAttrs. A broadcast
|
||||||
|
// PolicyChange() re-renders peer lists and packet filters but
|
||||||
|
// never repopulates a node's own [tailcfg.Node.CapMap]; that
|
||||||
|
// lives on the self entry only. The drain returns every node ID
|
||||||
|
// whose cap output shifted across recent updateLocked calls —
|
||||||
|
// refreshNodeAttrsLocked appends rather than overwrites so a
|
||||||
|
// concurrent SetUsers/SetNodes between SetPolicy and the drain
|
||||||
|
// cannot silently lose the policy-reload diff.
|
||||||
|
for _, id := range s.polMan.NodesWithChangedCapMap() {
|
||||||
|
cs = append(cs, change.SelfUpdate(id))
|
||||||
|
}
|
||||||
|
|
||||||
// Always call autoApproveNodes during policy reload, regardless of whether
|
// Always call autoApproveNodes during policy reload, regardless of whether
|
||||||
// the policy content has changed. This ensures that routes are re-evaluated
|
// the policy content has changed. This ensures that routes are re-evaluated
|
||||||
// when they might have been manually disabled but could now be auto-approved
|
// when they might have been manually disabled but could now be auto-approved
|
||||||
@ -1048,6 +1060,19 @@ func (s *State) MatchersForNode(node types.NodeView) ([]matcher.Match, error) {
|
|||||||
return s.polMan.MatchersForNode(node)
|
return s.polMan.MatchersForNode(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NodeCapMap returns the policy-derived CapMap for the given node, suitable
|
||||||
|
// for merging into tailcfg.Node.CapMap when the node is rendered as self or
|
||||||
|
// as someone else's peer.
|
||||||
|
func (s *State) NodeCapMap(id types.NodeID) tailcfg.NodeCapMap {
|
||||||
|
return s.polMan.NodeCapMap(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeCapMaps returns a snapshot of every node's policy CapMap so
|
||||||
|
// callers can amortise lock acquisition over a peer loop.
|
||||||
|
func (s *State) NodeCapMaps() map[types.NodeID]tailcfg.NodeCapMap {
|
||||||
|
return s.polMan.NodeCapMaps()
|
||||||
|
}
|
||||||
|
|
||||||
// NodeCanHaveTag checks if a node is allowed to have a specific tag.
|
// NodeCanHaveTag checks if a node is allowed to have a specific tag.
|
||||||
func (s *State) NodeCanHaveTag(node types.NodeView, tag string) bool {
|
func (s *State) NodeCanHaveTag(node types.NodeView, tag string) bool {
|
||||||
return s.polMan.NodeCanHaveTag(node, tag)
|
return s.polMan.NodeCanHaveTag(node, tag)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user