mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Restore support for "Override local DNS" (#2438)
Tailscale allows to override the local DNS settings of a node via
"Override local DNS" [1]. Restore this flag with the same config setting
name `dns.override_local_dns` but disable it by default to align it with
Tailscale's default behaviour.
Tested with Tailscale 1.80.2 and systemd-resolved on Debian 12.
With `dns.override_local_dns: false`:
```
Link 12 (tailscale0)
Current Scopes: DNS
     Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
   DNS Servers: 100.100.100.100
    DNS Domain: tn.example.com ~0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa [snip]
```
With `dns.override_local_dns: true`:
```
Link 12 (tailscale0)
Current Scopes: DNS
     Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
   DNS Servers: 100.100.100.100
    DNS Domain: tn.example.com ~.
```
[1] https://tailscale.com/kb/1054/dns#override-local-dns
Fixes: #2256
			
			
This commit is contained in:
		
							parent
							
								
									0fbe392499
								
							
						
					
					
						commit
						1e0516b99d
					
				@ -92,6 +92,8 @@ The new policy can be used by setting the environment variable
 | 
			
		||||
- node FQDNs in the netmap will now contain a dot (".") at the end. This aligns
 | 
			
		||||
  with behaviour of tailscale.com
 | 
			
		||||
  [#2503](https://github.com/juanfont/headscale/pull/2503)
 | 
			
		||||
- Restore support for "Override local DNS"
 | 
			
		||||
  [#2438](https://github.com/juanfont/headscale/pull/2438)
 | 
			
		||||
 | 
			
		||||
## 0.25.1 (2025-02-25)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -270,6 +270,10 @@ dns:
 | 
			
		||||
  # `hostname.base_domain` (e.g., _myhost.example.com_).
 | 
			
		||||
  base_domain: example.com
 | 
			
		||||
 | 
			
		||||
  # Whether to use the local DNS settings of a node (default) or override the
 | 
			
		||||
  # local DNS settings and force the use of Headscale's DNS configuration.
 | 
			
		||||
  override_local_dns: false
 | 
			
		||||
 | 
			
		||||
  # List of DNS servers to expose to clients.
 | 
			
		||||
  nameservers:
 | 
			
		||||
    global:
 | 
			
		||||
 | 
			
		||||
@ -102,6 +102,7 @@ type Config struct {
 | 
			
		||||
type DNSConfig struct {
 | 
			
		||||
	MagicDNS         bool   `mapstructure:"magic_dns"`
 | 
			
		||||
	BaseDomain       string `mapstructure:"base_domain"`
 | 
			
		||||
	OverrideLocalDNS bool   `mapstructure:"override_local_dns"`
 | 
			
		||||
	Nameservers      Nameservers
 | 
			
		||||
	SearchDomains    []string            `mapstructure:"search_domains"`
 | 
			
		||||
	ExtraRecords     []tailcfg.DNSRecord `mapstructure:"extra_records"`
 | 
			
		||||
@ -287,6 +288,7 @@ func LoadConfig(path string, isFile bool) error {
 | 
			
		||||
 | 
			
		||||
	viper.SetDefault("dns.magic_dns", true)
 | 
			
		||||
	viper.SetDefault("dns.base_domain", "")
 | 
			
		||||
	viper.SetDefault("dns.override_local_dns", true)
 | 
			
		||||
	viper.SetDefault("dns.nameservers.global", []string{})
 | 
			
		||||
	viper.SetDefault("dns.nameservers.split", map[string]string{})
 | 
			
		||||
	viper.SetDefault("dns.search_domains", []string{})
 | 
			
		||||
@ -351,9 +353,9 @@ func validateServerConfig() error {
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("policy.path", "acl_policy_path")
 | 
			
		||||
 | 
			
		||||
	// Move dns_config -> dns
 | 
			
		||||
	depr.warn("dns_config.override_local_dns")
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("dns.magic_dns", "dns_config.magic_dns")
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("dns.base_domain", "dns_config.base_domain")
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("dns.override_local_dns", "dns_config.override_local_dns")
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("dns.nameservers.global", "dns_config.nameservers")
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("dns.nameservers.split", "dns_config.restricted_nameservers")
 | 
			
		||||
	depr.fatalIfNewKeyIsNotUsed("dns.search_domains", "dns_config.domains")
 | 
			
		||||
@ -417,6 +419,12 @@ func validateServerConfig() error {
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if viper.GetBool("dns.override_local_dns") {
 | 
			
		||||
		if global := viper.GetStringSlice("dns.nameservers.global"); len(global) == 0 {
 | 
			
		||||
			errorText += "Fatal config error: dns.nameservers.global must be set when dns.override_local_dns is true\n"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if errorText != "" {
 | 
			
		||||
		// nolint
 | 
			
		||||
		return errors.New(strings.TrimSuffix(errorText, "\n"))
 | 
			
		||||
@ -616,6 +624,7 @@ func dns() (DNSConfig, error) {
 | 
			
		||||
 | 
			
		||||
	dns.MagicDNS = viper.GetBool("dns.magic_dns")
 | 
			
		||||
	dns.BaseDomain = viper.GetString("dns.base_domain")
 | 
			
		||||
	dns.OverrideLocalDNS = viper.GetBool("dns.override_local_dns")
 | 
			
		||||
	dns.Nameservers.Global = viper.GetStringSlice("dns.nameservers.global")
 | 
			
		||||
	dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split")
 | 
			
		||||
	dns.SearchDomains = viper.GetStringSlice("dns.search_domains")
 | 
			
		||||
@ -721,7 +730,11 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig {
 | 
			
		||||
 | 
			
		||||
	cfg.Proxied = dns.MagicDNS
 | 
			
		||||
	cfg.ExtraRecords = dns.ExtraRecords
 | 
			
		||||
	cfg.Resolvers = dns.globalResolvers()
 | 
			
		||||
	if dns.OverrideLocalDNS {
 | 
			
		||||
		cfg.Resolvers = dns.globalResolvers()
 | 
			
		||||
	} else {
 | 
			
		||||
		cfg.FallbackResolvers = dns.globalResolvers()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	routes := dns.splitResolvers()
 | 
			
		||||
	cfg.Routes = routes
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
	"github.com/google/go-cmp/cmp/cmpopts"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
@ -34,8 +35,9 @@ func TestReadConfig(t *testing.T) {
 | 
			
		||||
				return dns, nil
 | 
			
		||||
			},
 | 
			
		||||
			want: DNSConfig{
 | 
			
		||||
				MagicDNS:   true,
 | 
			
		||||
				BaseDomain: "example.com",
 | 
			
		||||
				MagicDNS:         true,
 | 
			
		||||
				BaseDomain:       "example.com",
 | 
			
		||||
				OverrideLocalDNS: false,
 | 
			
		||||
				Nameservers: Nameservers{
 | 
			
		||||
					Global: []string{
 | 
			
		||||
						"1.1.1.1",
 | 
			
		||||
@ -70,7 +72,7 @@ func TestReadConfig(t *testing.T) {
 | 
			
		||||
			want: &tailcfg.DNSConfig{
 | 
			
		||||
				Proxied: true,
 | 
			
		||||
				Domains: []string{"example.com", "test.com", "bar.com"},
 | 
			
		||||
				Resolvers: []*dnstype.Resolver{
 | 
			
		||||
				FallbackResolvers: []*dnstype.Resolver{
 | 
			
		||||
					{Addr: "1.1.1.1"},
 | 
			
		||||
					{Addr: "1.0.0.1"},
 | 
			
		||||
					{Addr: "2606:4700:4700::1111"},
 | 
			
		||||
@ -99,8 +101,9 @@ func TestReadConfig(t *testing.T) {
 | 
			
		||||
				return dns, nil
 | 
			
		||||
			},
 | 
			
		||||
			want: DNSConfig{
 | 
			
		||||
				MagicDNS:   false,
 | 
			
		||||
				BaseDomain: "example.com",
 | 
			
		||||
				MagicDNS:         false,
 | 
			
		||||
				BaseDomain:       "example.com",
 | 
			
		||||
				OverrideLocalDNS: false,
 | 
			
		||||
				Nameservers: Nameservers{
 | 
			
		||||
					Global: []string{
 | 
			
		||||
						"1.1.1.1",
 | 
			
		||||
@ -135,7 +138,7 @@ func TestReadConfig(t *testing.T) {
 | 
			
		||||
			want: &tailcfg.DNSConfig{
 | 
			
		||||
				Proxied: false,
 | 
			
		||||
				Domains: []string{"example.com", "test.com", "bar.com"},
 | 
			
		||||
				Resolvers: []*dnstype.Resolver{
 | 
			
		||||
				FallbackResolvers: []*dnstype.Resolver{
 | 
			
		||||
					{Addr: "1.1.1.1"},
 | 
			
		||||
					{Addr: "1.0.0.1"},
 | 
			
		||||
					{Addr: "2606:4700:4700::1111"},
 | 
			
		||||
@ -181,6 +184,40 @@ func TestReadConfig(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "dns-override-true-errors",
 | 
			
		||||
			configPath: "testdata/dns-override-true-error.yaml",
 | 
			
		||||
			setup: func(t *testing.T) (any, error) {
 | 
			
		||||
				return LoadServerConfig()
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "Fatal config error: dns.nameservers.global must be set when dns.override_local_dns is true",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "dns-override-true",
 | 
			
		||||
			configPath: "testdata/dns-override-true.yaml",
 | 
			
		||||
			setup: func(t *testing.T) (any, error) {
 | 
			
		||||
				_, err := LoadServerConfig()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				dns, err := dns()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return dnsToTailcfgDNS(dns), nil
 | 
			
		||||
			},
 | 
			
		||||
			want: &tailcfg.DNSConfig{
 | 
			
		||||
				Proxied: true,
 | 
			
		||||
				Domains: []string{"derp2.no"},
 | 
			
		||||
				Routes:  map[string][]*dnstype.Resolver{},
 | 
			
		||||
				Resolvers: []*dnstype.Resolver{
 | 
			
		||||
					{Addr: "1.1.1.1"},
 | 
			
		||||
					{Addr: "1.0.0.1"},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "policy-path-is-loaded",
 | 
			
		||||
			configPath: "testdata/policy-path-is-loaded.yaml",
 | 
			
		||||
@ -254,6 +291,7 @@ func TestReadConfigFromEnv(t *testing.T) {
 | 
			
		||||
			configEnv: map[string]string{
 | 
			
		||||
				"HEADSCALE_DNS_MAGIC_DNS":          "true",
 | 
			
		||||
				"HEADSCALE_DNS_BASE_DOMAIN":        "example.com",
 | 
			
		||||
				"HEADSCALE_DNS_OVERRIDE_LOCAL_DNS": "false",
 | 
			
		||||
				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": `1.1.1.1 8.8.8.8`,
 | 
			
		||||
				"HEADSCALE_DNS_SEARCH_DOMAINS":     "test.com bar.com",
 | 
			
		||||
 | 
			
		||||
@ -272,8 +310,9 @@ func TestReadConfigFromEnv(t *testing.T) {
 | 
			
		||||
				return dns, nil
 | 
			
		||||
			},
 | 
			
		||||
			want: DNSConfig{
 | 
			
		||||
				MagicDNS:   true,
 | 
			
		||||
				BaseDomain: "example.com",
 | 
			
		||||
				MagicDNS:         true,
 | 
			
		||||
				BaseDomain:       "example.com",
 | 
			
		||||
				OverrideLocalDNS: false,
 | 
			
		||||
				Nameservers: Nameservers{
 | 
			
		||||
					Global: []string{"1.1.1.1", "8.8.8.8"},
 | 
			
		||||
					Split:  map[string][]string{
 | 
			
		||||
@ -301,7 +340,7 @@ func TestReadConfigFromEnv(t *testing.T) {
 | 
			
		||||
			conf, err := tt.setup(t)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			if diff := cmp.Diff(tt.want, conf); diff != "" {
 | 
			
		||||
			if diff := cmp.Diff(tt.want, conf, cmpopts.EquateEmpty()); diff != "" {
 | 
			
		||||
				t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
@ -13,3 +13,4 @@ server_url: "https://server.derp.no"
 | 
			
		||||
dns:
 | 
			
		||||
  magic_dns: true
 | 
			
		||||
  base_domain: derp.no
 | 
			
		||||
  override_local_dns: false
 | 
			
		||||
 | 
			
		||||
@ -13,3 +13,4 @@ server_url: "https://derp.no"
 | 
			
		||||
dns:
 | 
			
		||||
  magic_dns: true
 | 
			
		||||
  base_domain: clients.derp.no
 | 
			
		||||
  override_local_dns: false
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								hscontrol/types/testdata/dns-override-true-error.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								hscontrol/types/testdata/dns-override-true-error.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
noise:
 | 
			
		||||
  private_key_path: "private_key.pem"
 | 
			
		||||
 | 
			
		||||
prefixes:
 | 
			
		||||
  v6: fd7a:115c:a1e0::/48
 | 
			
		||||
  v4: 100.64.0.0/10
 | 
			
		||||
 | 
			
		||||
database:
 | 
			
		||||
  type: sqlite3
 | 
			
		||||
 | 
			
		||||
server_url: "https://server.derp.no"
 | 
			
		||||
 | 
			
		||||
dns:
 | 
			
		||||
  magic_dns: true
 | 
			
		||||
  base_domain: derp.no
 | 
			
		||||
  override_local_dns: true
 | 
			
		||||
							
								
								
									
										20
									
								
								hscontrol/types/testdata/dns-override-true.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								hscontrol/types/testdata/dns-override-true.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
noise:
 | 
			
		||||
  private_key_path: "private_key.pem"
 | 
			
		||||
 | 
			
		||||
prefixes:
 | 
			
		||||
  v6: fd7a:115c:a1e0::/48
 | 
			
		||||
  v4: 100.64.0.0/10
 | 
			
		||||
 | 
			
		||||
database:
 | 
			
		||||
  type: sqlite3
 | 
			
		||||
 | 
			
		||||
server_url: "https://server.derp.no"
 | 
			
		||||
 | 
			
		||||
dns:
 | 
			
		||||
  magic_dns: true
 | 
			
		||||
  base_domain: derp2.no
 | 
			
		||||
  override_local_dns: true
 | 
			
		||||
  nameservers:
 | 
			
		||||
    global:
 | 
			
		||||
      - 1.1.1.1
 | 
			
		||||
      - 1.0.0.1
 | 
			
		||||
							
								
								
									
										1
									
								
								hscontrol/types/testdata/dns_full.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								hscontrol/types/testdata/dns_full.yaml
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,7 @@ dns:
 | 
			
		||||
  magic_dns: true
 | 
			
		||||
  base_domain: example.com
 | 
			
		||||
 | 
			
		||||
  override_local_dns: false
 | 
			
		||||
  nameservers:
 | 
			
		||||
    global:
 | 
			
		||||
      - 1.1.1.1
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ dns:
 | 
			
		||||
  magic_dns: false
 | 
			
		||||
  base_domain: example.com
 | 
			
		||||
 | 
			
		||||
  override_local_dns: false
 | 
			
		||||
  nameservers:
 | 
			
		||||
    global:
 | 
			
		||||
      - 1.1.1.1
 | 
			
		||||
 | 
			
		||||
@ -15,4 +15,6 @@ policy:
 | 
			
		||||
  type: file
 | 
			
		||||
  path: "/etc/policy.hujson"
 | 
			
		||||
 | 
			
		||||
dns.magic_dns: false
 | 
			
		||||
dns:
 | 
			
		||||
  magic_dns: false
 | 
			
		||||
  override_local_dns: false
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ func DefaultConfigEnv() map[string]string {
 | 
			
		||||
		"HEADSCALE_PREFIXES_V6":                       "fd7a:115c:a1e0::/48",
 | 
			
		||||
		"HEADSCALE_DNS_BASE_DOMAIN":                   "headscale.net",
 | 
			
		||||
		"HEADSCALE_DNS_MAGIC_DNS":                     "true",
 | 
			
		||||
		"HEADSCALE_DNS_OVERRIDE_LOCAL_DNS":            "false",
 | 
			
		||||
		"HEADSCALE_DNS_NAMESERVERS_GLOBAL":            "127.0.0.11 1.1.1.1",
 | 
			
		||||
		"HEADSCALE_PRIVATE_KEY_PATH":                  "/tmp/private.key",
 | 
			
		||||
		"HEADSCALE_NOISE_PRIVATE_KEY_PATH":            "/tmp/noise_private.key",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user