net/dns: make MagicDNS IPv6 registration opt-out now, not opt-in

This adds a new ControlKnob to make MagicDNS IPv6 registration
(telling systemd/etc) opt-out rather than opt-in.

Updates #15404

Change-Id: If008e1cb046b792c6aff7bb1d7c58638f7d650b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
main
Brad Fitzpatrick 2 months ago committed by Brad Fitzpatrick
parent a6390ca008
commit a7a864419d
  1. 16
      control/controlknobs/controlknobs.go
  2. 8
      net/dns/config.go
  3. 4
      net/dns/manager_tcp_test.go
  4. 88
      net/dns/manager_test.go
  5. 9
      tailcfg/tailcfg.go

@ -113,6 +113,14 @@ type Knobs struct {
// resolver on Windows or when the host is domain-joined and its primary domain
// takes precedence over MagicDNS. As of 2026-02-13, it is only used on Windows.
DisableHostsFileUpdates atomic.Bool
// ForceRegisterMagicDNSIPv4Only is whether the node should only register
// its IPv4 MagicDNS service IP and not its IPv6 one. The IPv6 one,
// tsaddr.TailscaleServiceIPv6String, still works in either case. This knob
// controls only whether we tell systemd/etc about the IPv6 one.
// See https://github.com/tailscale/tailscale/issues/15404.
// TODO(bradfitz): remove this a few releases after 2026-02-16.
ForceRegisterMagicDNSIPv4Only atomic.Bool
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
@ -144,6 +152,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection)
disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue)
disableHostsFileUpdates = has(tailcfg.NodeAttrDisableHostsFileUpdates)
forceRegisterMagicDNSIPv4Only = has(tailcfg.NodeAttrForceRegisterMagicDNSIPv4Only)
)
if has(tailcfg.NodeAttrOneCGNATEnable) {
@ -171,6 +180,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection)
k.DisableSkipStatusQueue.Store(disableSkipStatusQueue)
k.DisableHostsFileUpdates.Store(disableHostsFileUpdates)
k.ForceRegisterMagicDNSIPv4Only.Store(forceRegisterMagicDNSIPv4Only)
// If both attributes are present, then "enable" should win. This reflects
// the history of seamless key renewal.
@ -210,3 +220,9 @@ func (k *Knobs) AsDebugJSON() map[string]any {
}
return ret
}
// ShouldForceRegisterMagicDNSIPv4Only reports the value of
// ForceRegisterMagicDNSIPv4Only, or false if k is nil.
func (k *Knobs) ShouldForceRegisterMagicDNSIPv4Only() bool {
return k != nil && k.ForceRegisterMagicDNSIPv4Only.Load()
}

@ -73,11 +73,9 @@ func (c *Config) serviceIPs(knobs *controlknobs.Knobs) []netip.Addr {
return []netip.Addr{tsaddr.TailscaleServiceIPv6()}
}
// TODO(bradfitz,mikeodr,raggi): include IPv6 here too; tailscale/tailscale#15404
// And add a controlknobs knob to disable dual stack.
//
// For now, opt-in for testing.
if magicDNSDualStack() {
// See https://github.com/tailscale/tailscale/issues/15404 for the background
// on the opt-in debug knob and the controlknob opt-out.
if magicDNSDualStack() || !knobs.ShouldForceRegisterMagicDNSIPv4Only() {
return []netip.Addr{
tsaddr.TailscaleServiceIP(),
tsaddr.TailscaleServiceIPv6(),

@ -15,6 +15,7 @@ import (
"github.com/google/go-cmp/cmp"
dns "golang.org/x/net/dns/dnsmessage"
"tailscale.com/control/controlknobs"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
@ -93,7 +94,8 @@ func TestDNSOverTCP(t *testing.T) {
bus := eventbustest.NewBus(t)
dialer := tsdial.NewDialer(netmon.NewStatic())
dialer.SetBus(bus)
m := NewManager(t.Logf, &f, health.NewTracker(bus), dialer, nil, nil, "", bus)
cknobs := &controlknobs.Knobs{}
m := NewManager(t.Logf, &f, health.NewTracker(bus), dialer, nil, cknobs, "", bus)
m.resolver.TestOnlySetHook(f.SetResolver)
m.Set(Config{
Hosts: hosts(

@ -28,6 +28,7 @@ import (
"tailscale.com/net/dns/publicdns"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/netmon"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
@ -172,6 +173,8 @@ func TestCompileHostEntries(t *testing.T) {
}
}
var serviceAddr46 = []netip.Addr{tsaddr.TailscaleServiceIP(), tsaddr.TailscaleServiceIPv6()}
func TestManager(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skipf("test's assumptions break because of https://github.com/tailscale/corp/issues/1662")
@ -189,6 +192,7 @@ func TestManager(t *testing.T) {
split bool
bs OSConfig
os OSConfig
knobs *controlknobs.Knobs
rs resolver.Config
goos string // empty means "linux"
}{
@ -231,7 +235,7 @@ func TestManager(t *testing.T) {
"bar.tld.", "2.3.4.5"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
},
rs: resolver.Config{
Hosts: hosts(
@ -317,7 +321,7 @@ func TestManager(t *testing.T) {
"bradfitz.ts.com.", "2.3.4.5"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -340,7 +344,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -359,7 +363,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -377,7 +381,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -386,6 +390,33 @@ func TestManager(t *testing.T) {
"corp.com.", "2.2.2.2"),
},
},
{
name: "controlknob-disable-v6-registration",
in: Config{
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Routes: upstreams("ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
},
knobs: (func() *controlknobs.Knobs {
k := new(controlknobs.Knobs)
k.ForceRegisterMagicDNSIPv4Only.Store(true)
return k
})(),
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), // without IPv6
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
{
name: "routes",
in: Config{
@ -397,7 +428,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
@ -432,7 +463,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
@ -452,7 +483,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("bigco.net", "corp.com"),
},
@ -477,7 +508,7 @@ func TestManager(t *testing.T) {
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -502,7 +533,7 @@ func TestManager(t *testing.T) {
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -527,7 +558,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
@ -549,7 +580,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("ts.com"),
},
@ -575,7 +606,7 @@ func TestManager(t *testing.T) {
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -601,7 +632,7 @@ func TestManager(t *testing.T) {
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -627,7 +658,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
@ -653,7 +684,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("corp.com", "ts.com"),
},
@ -683,7 +714,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -715,7 +746,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -740,7 +771,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
@ -768,7 +799,7 @@ func TestManager(t *testing.T) {
DefaultResolvers: mustRes("2a07:a8c0::c3:a884"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
},
rs: resolver.Config{
Routes: upstreams(".", "2a07:a8c0::c3:a884"),
@ -780,7 +811,7 @@ func TestManager(t *testing.T) {
DefaultResolvers: mustRes("https://dns.nextdns.io/c3a884"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
},
rs: resolver.Config{
Routes: upstreams(".", "https://dns.nextdns.io/c3a884"),
@ -796,7 +827,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("optimistic-display.ts.net"),
MatchDomains: fqdns("ts.net"),
},
@ -821,7 +852,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("optimistic-display.ts.net"),
},
rs: resolver.Config{
@ -844,7 +875,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("optimistic-display.ts.net"),
},
rs: resolver.Config{
@ -885,7 +916,7 @@ func TestManager(t *testing.T) {
},
},
},
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("ts.com", "universe.tf"),
MatchDomains: fqdns("corp.com", "ts.com"),
},
@ -912,7 +943,7 @@ func TestManager(t *testing.T) {
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
Nameservers: serviceAddr46,
SearchDomains: fqdns("ts.com", "universe.tf"),
MatchDomains: fqdns("corp.com", "ts.com"),
},
@ -946,7 +977,10 @@ func TestManager(t *testing.T) {
if goos == "" {
goos = "linux"
}
knobs := &controlknobs.Knobs{}
knobs := test.knobs
if knobs == nil {
knobs = &controlknobs.Knobs{}
}
bus := eventbustest.NewBus(t)
dialer := tsdial.NewDialer(netmon.NewStatic())
dialer.SetBus(bus)

@ -179,7 +179,8 @@ type CapabilityVersion int
// - 130: 2025-10-06: client can send key.HardwareAttestationPublic and key.HardwareAttestationKeySignature in MapRequest
// - 131: 2025-11-25: client respects [NodeAttrDefaultAutoUpdate]
// - 132: 2026-02-13: client respects [NodeAttrDisableHostsFileUpdates]
const CurrentCapabilityVersion CapabilityVersion = 132
// - 133: 2026-02-17: client understands [NodeAttrForceRegisterMagicDNSIPv4Only]; MagicDNS IPv6 registered w/ OS by default
const CurrentCapabilityVersion CapabilityVersion = 133
// ID is an integer ID for a user, node, or login allocated by the
// control plane.
@ -2748,6 +2749,12 @@ const (
// primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only
// used on Windows.
NodeAttrDisableHostsFileUpdates NodeCapability = "disable-hosts-file-updates"
// NodeAttrForceRegisterMagicDNSIPv4Only forces the client to only register
// its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses.
// See https://github.com/tailscale/tailscale/issues/15404.
// TODO(bradfitz): remove this a few releases after 2026-02-16.
NodeAttrForceRegisterMagicDNSIPv4Only NodeCapability = "force-register-magicdns-ipv4-only"
)
// SetDNSRequest is a request to add a DNS record.

Loading…
Cancel
Save