mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
various: create a catch-all NRPT rule when "Override local DNS" is enabled on Windows
Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers associated with all network adapters, even when "Override local DNS" is enabled and/or a Mullvad exit node is being used, resulting in DNS leaks. This also adds "disable-local-dns-override-via-nrpt" nodeAttr that can be used to disable the new behavior if needed. Fixes tailscale/corp#20718 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
7354547bd8
commit
c32efd9118
@ -698,7 +698,7 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
|||||||
// configuration being unavailable (from the noop
|
// configuration being unavailable (from the noop
|
||||||
// manager). More in Issue 4017.
|
// manager). More in Issue 4017.
|
||||||
// TODO(bradfitz): add a Synology-specific DNS manager.
|
// TODO(bradfitz): add a Synology-specific DNS manager.
|
||||||
conf.DNS, err = dns.NewOSConfigurator(logf, sys.HealthTracker(), "") // empty interface name
|
conf.DNS, err = dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), "") // empty interface name
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||||
}
|
}
|
||||||
@ -726,7 +726,7 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
|||||||
return false, fmt.Errorf("creating router: %w", err)
|
return false, fmt.Errorf("creating router: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), devName)
|
d, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), devName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dev.Close()
|
dev.Close()
|
||||||
r.Close()
|
r.Close()
|
||||||
|
@ -90,6 +90,15 @@ type Knobs struct {
|
|||||||
// This is for now (2024-06-06) an iOS-specific battery life optimization,
|
// This is for now (2024-06-06) an iOS-specific battery life optimization,
|
||||||
// and this knob allows us to disable the optimization remotely if needed.
|
// and this knob allows us to disable the optimization remotely if needed.
|
||||||
DisableSplitDNSWhenNoCustomResolvers atomic.Bool
|
DisableSplitDNSWhenNoCustomResolvers atomic.Bool
|
||||||
|
|
||||||
|
// DisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not
|
||||||
|
// create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled.
|
||||||
|
// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
|
||||||
|
// associated with all network adapters, even when "Override local DNS" is enabled and/or
|
||||||
|
// a Mullvad exit node is being used, resulting in DNS leaks.
|
||||||
|
// We began creating this rule on 2024-06-14, and this knob
|
||||||
|
// allows us to disable the new behavior remotely if needed.
|
||||||
|
DisableLocalDNSOverrideViaNRPT atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||||
@ -117,6 +126,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
|||||||
appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes)
|
appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes)
|
||||||
userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes)
|
userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes)
|
||||||
disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers)
|
disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers)
|
||||||
|
disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT)
|
||||||
)
|
)
|
||||||
|
|
||||||
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
||||||
@ -142,6 +152,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
|||||||
k.AppCStoreRoutes.Store(appCStoreRoutes)
|
k.AppCStoreRoutes.Store(appCStoreRoutes)
|
||||||
k.UserDialUseRoutes.Store(userDialUseRoutes)
|
k.UserDialUseRoutes.Store(userDialUseRoutes)
|
||||||
k.DisableSplitDNSWhenNoCustomResolvers.Store(disableSplitDNSWhenNoCustomResolvers)
|
k.DisableSplitDNSWhenNoCustomResolvers.Store(disableSplitDNSWhenNoCustomResolvers)
|
||||||
|
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||||
@ -168,5 +179,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
|||||||
"AppCStoreRoutes": k.AppCStoreRoutes.Load(),
|
"AppCStoreRoutes": k.AppCStoreRoutes.Load(),
|
||||||
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
|
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
|
||||||
"DisableSplitDNSWhenNoCustomResolvers": k.DisableSplitDNSWhenNoCustomResolvers.Load(),
|
"DisableSplitDNSWhenNoCustomResolvers": k.DisableSplitDNSWhenNoCustomResolvers.Load(),
|
||||||
|
"DisableLocalDNSOverrideViaNRPT": k.DisableLocalDNSOverrideViaNRPT.Load(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -485,7 +485,7 @@ func (m *Manager) FlushCaches() error {
|
|||||||
// in case the Tailscale daemon terminated without closing the router.
|
// in case the Tailscale daemon terminated without closing the router.
|
||||||
// No other state needs to be instantiated before this runs.
|
// No other state needs to be instantiated before this runs.
|
||||||
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, interfaceName string) {
|
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, interfaceName string) {
|
||||||
oscfg, err := NewOSConfigurator(logf, nil, interfaceName)
|
oscfg, err := NewOSConfigurator(logf, nil, nil, interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logf("creating dns cleanup: %v", err)
|
logf("creating dns cleanup: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -8,12 +8,16 @@
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"go4.org/mem"
|
"go4.org/mem"
|
||||||
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, ifName string) (OSConfigurator, error) {
|
// NewOSConfigurator creates a new OS configurator.
|
||||||
|
//
|
||||||
|
// The health tracker and the knobs may be nil and are ignored on this platform.
|
||||||
|
func NewOSConfigurator(logf logger.Logf, _ *health.Tracker, _ *controlknobs.Knobs, ifName string) (OSConfigurator, error) {
|
||||||
return &darwinConfigurator{logf: logf, ifName: ifName}, nil
|
return &darwinConfigurator{logf: logf, ifName: ifName}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,10 +6,14 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOSConfigurator(logger.Logf, *health.Tracker, string) (OSConfigurator, error) {
|
// NewOSConfigurator creates a new OS configurator.
|
||||||
|
//
|
||||||
|
// The health tracker and the knobs may be nil and are ignored on this platform.
|
||||||
|
func NewOSConfigurator(logger.Logf, *health.Tracker, *controlknobs.Knobs, string) (OSConfigurator, error) {
|
||||||
return NewNoopManager()
|
return NewNoopManager()
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,15 @@
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ string) (OSConfigurator, error) {
|
// NewOSConfigurator creates a new OS configurator.
|
||||||
|
//
|
||||||
|
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||||
|
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, _ string) (OSConfigurator, error) {
|
||||||
bs, err := os.ReadFile("/etc/resolv.conf")
|
bs, err := os.ReadFile("/etc/resolv.conf")
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return newDirectManager(logf, health), nil
|
return newDirectManager(logf, health), nil
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/net/netaddr"
|
"tailscale.com/net/netaddr"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
@ -31,7 +32,10 @@ func (kv kv) String() string {
|
|||||||
|
|
||||||
var publishOnce sync.Once
|
var publishOnce sync.Once
|
||||||
|
|
||||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string) (ret OSConfigurator, err error) {
|
// NewOSConfigurator created a new OS configurator.
|
||||||
|
//
|
||||||
|
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||||
|
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) {
|
||||||
env := newOSConfigEnv{
|
env := newOSConfigEnv{
|
||||||
fs: directFS{},
|
fs: directFS{},
|
||||||
dbusPing: dbusPing,
|
dbusPing: dbusPing,
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
@ -20,7 +21,10 @@ func (kv kv) String() string {
|
|||||||
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error) {
|
// NewOSConfigurator created a new OS configurator.
|
||||||
|
//
|
||||||
|
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||||
|
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (OSConfigurator, error) {
|
||||||
return newOSConfigurator(logf, health, interfaceName,
|
return newOSConfigurator(logf, health, interfaceName,
|
||||||
newOSConfigEnv{
|
newOSConfigEnv{
|
||||||
rcIsResolvd: rcIsResolvd,
|
rcIsResolvd: rcIsResolvd,
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||||
"tailscale.com/atomicfile"
|
"tailscale.com/atomicfile"
|
||||||
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
@ -38,6 +39,7 @@
|
|||||||
type windowsManager struct {
|
type windowsManager struct {
|
||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
guid string
|
guid string
|
||||||
|
knobs *controlknobs.Knobs // or nil
|
||||||
nrptDB *nrptRuleDatabase
|
nrptDB *nrptRuleDatabase
|
||||||
wslManager *wslManager
|
wslManager *wslManager
|
||||||
|
|
||||||
@ -45,10 +47,14 @@ type windowsManager struct {
|
|||||||
closing bool
|
closing bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error) {
|
// NewOSConfigurator created a new OS configurator.
|
||||||
|
//
|
||||||
|
// The health tracker and the knobs may be nil.
|
||||||
|
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, knobs *controlknobs.Knobs, interfaceName string) (OSConfigurator, error) {
|
||||||
ret := &windowsManager{
|
ret := &windowsManager{
|
||||||
logf: logf,
|
logf: logf,
|
||||||
guid: interfaceName,
|
guid: interfaceName,
|
||||||
|
knobs: knobs,
|
||||||
wslManager: newWSLManager(logf, health),
|
wslManager: newWSLManager(logf, health),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,6 +294,10 @@ func (m *windowsManager) setPrimaryDNS(resolvers []netip.Addr, domains []dnsname
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *windowsManager) disableLocalDNSOverrideViaNRPT() bool {
|
||||||
|
return m.knobs != nil && m.knobs.DisableLocalDNSOverrideViaNRPT.Load()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *windowsManager) SetDNS(cfg OSConfig) error {
|
func (m *windowsManager) SetDNS(cfg OSConfig) error {
|
||||||
// We can configure Windows DNS in one of two ways:
|
// We can configure Windows DNS in one of two ways:
|
||||||
//
|
//
|
||||||
@ -322,7 +332,17 @@ func (m *windowsManager) SetDNS(cfg OSConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.MatchDomains) == 0 {
|
if len(cfg.MatchDomains) == 0 {
|
||||||
if err := m.setSplitDNS(nil, nil); err != nil {
|
var resolvers []netip.Addr
|
||||||
|
var domains []dnsname.FQDN
|
||||||
|
if !m.disableLocalDNSOverrideViaNRPT() {
|
||||||
|
// Create a default catch-all rule to make ourselves the actual primary resolver.
|
||||||
|
// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
|
||||||
|
// associated with all network adapters, even when "Override local DNS" is enabled and/or
|
||||||
|
// a Mullvad exit node is being used, resulting in DNS leaks.
|
||||||
|
resolvers = cfg.Nameservers
|
||||||
|
domains = []dnsname.FQDN{"."}
|
||||||
|
}
|
||||||
|
if err := m.setSplitDNS(resolvers, domains); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := m.setHosts(nil); err != nil {
|
if err := m.setHosts(nil); err != nil {
|
||||||
@ -331,8 +351,6 @@ func (m *windowsManager) SetDNS(cfg OSConfig) error {
|
|||||||
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil {
|
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if m.nrptDB == nil {
|
|
||||||
return errors.New("cannot set per-domain resolvers on Windows 7")
|
|
||||||
} else {
|
} else {
|
||||||
if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil {
|
if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -84,7 +84,7 @@ func TestManagerWindowsGPCopy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer delIfKey()
|
defer delIfKey()
|
||||||
|
|
||||||
cfg, err := NewOSConfigurator(logf, nil, fakeInterface.String())
|
cfg, err := NewOSConfigurator(logf, nil, nil, fakeInterface.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewOSConfigurator: %v\n", err)
|
t.Fatalf("NewOSConfigurator: %v\n", err)
|
||||||
}
|
}
|
||||||
@ -213,7 +213,7 @@ func runTest(t *testing.T, isLocal bool) {
|
|||||||
}
|
}
|
||||||
defer delIfKey()
|
defer delIfKey()
|
||||||
|
|
||||||
cfg, err := NewOSConfigurator(logf, nil, fakeInterface.String())
|
cfg, err := NewOSConfigurator(logf, nil, nil, fakeInterface.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewOSConfigurator: %v\n", err)
|
t.Fatalf("NewOSConfigurator: %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,8 @@
|
|||||||
// - 96: 2024-05-29: Client understands NodeAttrSSHBehaviorV1
|
// - 96: 2024-05-29: Client understands NodeAttrSSHBehaviorV1
|
||||||
// - 97: 2024-06-06: Client understands NodeAttrDisableSplitDNSWhenNoCustomResolvers
|
// - 97: 2024-06-06: Client understands NodeAttrDisableSplitDNSWhenNoCustomResolvers
|
||||||
// - 98: 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
|
// - 98: 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 98
|
// - 99: 2024-06-14: Client understands NodeAttrDisableLocalDNSOverrideViaNRPT
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 99
|
||||||
|
|
||||||
type StableID string
|
type StableID string
|
||||||
|
|
||||||
@ -2306,6 +2307,15 @@ type Oauth2Token struct {
|
|||||||
// and this node attribute allows us to disable the optimization remotely
|
// and this node attribute allows us to disable the optimization remotely
|
||||||
// if needed.
|
// if needed.
|
||||||
NodeAttrDisableSplitDNSWhenNoCustomResolvers NodeCapability = "disable-split-dns-when-no-custom-resolvers"
|
NodeAttrDisableSplitDNSWhenNoCustomResolvers NodeCapability = "disable-split-dns-when-no-custom-resolvers"
|
||||||
|
|
||||||
|
// NodeAttrDisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not
|
||||||
|
// create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled.
|
||||||
|
// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
|
||||||
|
// associated with all network adapters, even when "Override local DNS" is enabled and/or
|
||||||
|
// a Mullvad exit node is being used, resulting in DNS leaks.
|
||||||
|
// We began creating this rule on 2024-06-14, and this node attribute
|
||||||
|
// allows us to disable the new behavior remotely if needed.
|
||||||
|
NodeAttrDisableLocalDNSOverrideViaNRPT NodeCapability = "disable-local-dns-override-via-nrpt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetDNSRequest is a request to add a DNS record.
|
// SetDNSRequest is a request to add a DNS record.
|
||||||
|
Loading…
Reference in New Issue
Block a user