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
5 changed files with 312 additions and 19 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package mapper
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
|
|
@ -90,6 +91,14 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
|
|||
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
|
||||
|
||||
return b
|
||||
|
|
@ -158,7 +167,7 @@ func (b *MapResponseBuilder) WithDNSConfig() *MapResponseBuilder {
|
|||
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
|
||||
}
|
||||
|
|
@ -266,6 +275,22 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue