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:
Kristoffer Dalby 2026-05-11 14:47:58 +00:00
parent a4f05b0962
commit 6fcff9e352
5 changed files with 312 additions and 19 deletions

View file

@ -42,6 +42,7 @@ type serverConfig struct {
ephemeralTimeout time.Duration
nodeExpiry time.Duration
batcherWorkers int
taildropEnabled bool
}
func defaultServerConfig() *serverConfig {
@ -50,6 +51,7 @@ func defaultServerConfig() *serverConfig {
bufferedChanSize: 30,
batcherWorkers: 1,
ephemeralTimeout: 30 * time.Second,
taildropEnabled: true,
}
}
@ -73,6 +75,15 @@ func WithNodeExpiry(d time.Duration) ServerOption {
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.
// The server is fully functional and accepts real Tailscale control
// protocol connections over Noise.
@ -111,6 +122,7 @@ func NewServer(tb testing.TB, opts ...ServerOption) *TestServer {
Policy: types.PolicyConfig{
Mode: types.PolicyModeDB,
},
Taildrop: types.TaildropConfig{Enabled: sc.taildropEnabled},
Tuning: types.Tuning{
BatchChangeDelay: sc.batchDelay,
BatcherWorkers: sc.batcherWorkers,