mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-30 23:51:03 +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 | - node FQDNs in the netmap will now contain a dot (".") at the end. This aligns | ||||||
|   with behaviour of tailscale.com |   with behaviour of tailscale.com | ||||||
|   [#2503](https://github.com/juanfont/headscale/pull/2503) |   [#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) | ## 0.25.1 (2025-02-25) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -270,6 +270,10 @@ dns: | |||||||
|   # `hostname.base_domain` (e.g., _myhost.example.com_). |   # `hostname.base_domain` (e.g., _myhost.example.com_). | ||||||
|   base_domain: 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. |   # List of DNS servers to expose to clients. | ||||||
|   nameservers: |   nameservers: | ||||||
|     global: |     global: | ||||||
|  | |||||||
| @ -102,6 +102,7 @@ type Config struct { | |||||||
| type DNSConfig struct { | type DNSConfig struct { | ||||||
| 	MagicDNS         bool   `mapstructure:"magic_dns"` | 	MagicDNS         bool   `mapstructure:"magic_dns"` | ||||||
| 	BaseDomain       string `mapstructure:"base_domain"` | 	BaseDomain       string `mapstructure:"base_domain"` | ||||||
|  | 	OverrideLocalDNS bool   `mapstructure:"override_local_dns"` | ||||||
| 	Nameservers      Nameservers | 	Nameservers      Nameservers | ||||||
| 	SearchDomains    []string            `mapstructure:"search_domains"` | 	SearchDomains    []string            `mapstructure:"search_domains"` | ||||||
| 	ExtraRecords     []tailcfg.DNSRecord `mapstructure:"extra_records"` | 	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.magic_dns", true) | ||||||
| 	viper.SetDefault("dns.base_domain", "") | 	viper.SetDefault("dns.base_domain", "") | ||||||
|  | 	viper.SetDefault("dns.override_local_dns", true) | ||||||
| 	viper.SetDefault("dns.nameservers.global", []string{}) | 	viper.SetDefault("dns.nameservers.global", []string{}) | ||||||
| 	viper.SetDefault("dns.nameservers.split", map[string]string{}) | 	viper.SetDefault("dns.nameservers.split", map[string]string{}) | ||||||
| 	viper.SetDefault("dns.search_domains", []string{}) | 	viper.SetDefault("dns.search_domains", []string{}) | ||||||
| @ -351,9 +353,9 @@ func validateServerConfig() error { | |||||||
| 	depr.fatalIfNewKeyIsNotUsed("policy.path", "acl_policy_path") | 	depr.fatalIfNewKeyIsNotUsed("policy.path", "acl_policy_path") | ||||||
| 
 | 
 | ||||||
| 	// Move dns_config -> dns | 	// Move dns_config -> dns | ||||||
| 	depr.warn("dns_config.override_local_dns") |  | ||||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.magic_dns", "dns_config.magic_dns") | 	depr.fatalIfNewKeyIsNotUsed("dns.magic_dns", "dns_config.magic_dns") | ||||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.base_domain", "dns_config.base_domain") | 	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.global", "dns_config.nameservers") | ||||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.nameservers.split", "dns_config.restricted_nameservers") | 	depr.fatalIfNewKeyIsNotUsed("dns.nameservers.split", "dns_config.restricted_nameservers") | ||||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.search_domains", "dns_config.domains") | 	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 != "" { | 	if errorText != "" { | ||||||
| 		// nolint | 		// nolint | ||||||
| 		return errors.New(strings.TrimSuffix(errorText, "\n")) | 		return errors.New(strings.TrimSuffix(errorText, "\n")) | ||||||
| @ -616,6 +624,7 @@ func dns() (DNSConfig, error) { | |||||||
| 
 | 
 | ||||||
| 	dns.MagicDNS = viper.GetBool("dns.magic_dns") | 	dns.MagicDNS = viper.GetBool("dns.magic_dns") | ||||||
| 	dns.BaseDomain = viper.GetString("dns.base_domain") | 	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.Global = viper.GetStringSlice("dns.nameservers.global") | ||||||
| 	dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split") | 	dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split") | ||||||
| 	dns.SearchDomains = viper.GetStringSlice("dns.search_domains") | 	dns.SearchDomains = viper.GetStringSlice("dns.search_domains") | ||||||
| @ -721,7 +730,11 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { | |||||||
| 
 | 
 | ||||||
| 	cfg.Proxied = dns.MagicDNS | 	cfg.Proxied = dns.MagicDNS | ||||||
| 	cfg.ExtraRecords = dns.ExtraRecords | 	cfg.ExtraRecords = dns.ExtraRecords | ||||||
| 	cfg.Resolvers = dns.globalResolvers() | 	if dns.OverrideLocalDNS { | ||||||
|  | 		cfg.Resolvers = dns.globalResolvers() | ||||||
|  | 	} else { | ||||||
|  | 		cfg.FallbackResolvers = dns.globalResolvers() | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	routes := dns.splitResolvers() | 	routes := dns.splitResolvers() | ||||||
| 	cfg.Routes = routes | 	cfg.Routes = routes | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/google/go-cmp/cmp" | 	"github.com/google/go-cmp/cmp" | ||||||
|  | 	"github.com/google/go-cmp/cmp/cmpopts" | ||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| @ -34,8 +35,9 @@ func TestReadConfig(t *testing.T) { | |||||||
| 				return dns, nil | 				return dns, nil | ||||||
| 			}, | 			}, | ||||||
| 			want: DNSConfig{ | 			want: DNSConfig{ | ||||||
| 				MagicDNS:   true, | 				MagicDNS:         true, | ||||||
| 				BaseDomain: "example.com", | 				BaseDomain:       "example.com", | ||||||
|  | 				OverrideLocalDNS: false, | ||||||
| 				Nameservers: Nameservers{ | 				Nameservers: Nameservers{ | ||||||
| 					Global: []string{ | 					Global: []string{ | ||||||
| 						"1.1.1.1", | 						"1.1.1.1", | ||||||
| @ -70,7 +72,7 @@ func TestReadConfig(t *testing.T) { | |||||||
| 			want: &tailcfg.DNSConfig{ | 			want: &tailcfg.DNSConfig{ | ||||||
| 				Proxied: true, | 				Proxied: true, | ||||||
| 				Domains: []string{"example.com", "test.com", "bar.com"}, | 				Domains: []string{"example.com", "test.com", "bar.com"}, | ||||||
| 				Resolvers: []*dnstype.Resolver{ | 				FallbackResolvers: []*dnstype.Resolver{ | ||||||
| 					{Addr: "1.1.1.1"}, | 					{Addr: "1.1.1.1"}, | ||||||
| 					{Addr: "1.0.0.1"}, | 					{Addr: "1.0.0.1"}, | ||||||
| 					{Addr: "2606:4700:4700::1111"}, | 					{Addr: "2606:4700:4700::1111"}, | ||||||
| @ -99,8 +101,9 @@ func TestReadConfig(t *testing.T) { | |||||||
| 				return dns, nil | 				return dns, nil | ||||||
| 			}, | 			}, | ||||||
| 			want: DNSConfig{ | 			want: DNSConfig{ | ||||||
| 				MagicDNS:   false, | 				MagicDNS:         false, | ||||||
| 				BaseDomain: "example.com", | 				BaseDomain:       "example.com", | ||||||
|  | 				OverrideLocalDNS: false, | ||||||
| 				Nameservers: Nameservers{ | 				Nameservers: Nameservers{ | ||||||
| 					Global: []string{ | 					Global: []string{ | ||||||
| 						"1.1.1.1", | 						"1.1.1.1", | ||||||
| @ -135,7 +138,7 @@ func TestReadConfig(t *testing.T) { | |||||||
| 			want: &tailcfg.DNSConfig{ | 			want: &tailcfg.DNSConfig{ | ||||||
| 				Proxied: false, | 				Proxied: false, | ||||||
| 				Domains: []string{"example.com", "test.com", "bar.com"}, | 				Domains: []string{"example.com", "test.com", "bar.com"}, | ||||||
| 				Resolvers: []*dnstype.Resolver{ | 				FallbackResolvers: []*dnstype.Resolver{ | ||||||
| 					{Addr: "1.1.1.1"}, | 					{Addr: "1.1.1.1"}, | ||||||
| 					{Addr: "1.0.0.1"}, | 					{Addr: "1.0.0.1"}, | ||||||
| 					{Addr: "2606:4700:4700::1111"}, | 					{Addr: "2606:4700:4700::1111"}, | ||||||
| @ -181,6 +184,40 @@ func TestReadConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			wantErr: "", | 			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", | 			name:       "policy-path-is-loaded", | ||||||
| 			configPath: "testdata/policy-path-is-loaded.yaml", | 			configPath: "testdata/policy-path-is-loaded.yaml", | ||||||
| @ -254,6 +291,7 @@ func TestReadConfigFromEnv(t *testing.T) { | |||||||
| 			configEnv: map[string]string{ | 			configEnv: map[string]string{ | ||||||
| 				"HEADSCALE_DNS_MAGIC_DNS":          "true", | 				"HEADSCALE_DNS_MAGIC_DNS":          "true", | ||||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":        "example.com", | 				"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_NAMESERVERS_GLOBAL": `1.1.1.1 8.8.8.8`, | ||||||
| 				"HEADSCALE_DNS_SEARCH_DOMAINS":     "test.com bar.com", | 				"HEADSCALE_DNS_SEARCH_DOMAINS":     "test.com bar.com", | ||||||
| 
 | 
 | ||||||
| @ -272,8 +310,9 @@ func TestReadConfigFromEnv(t *testing.T) { | |||||||
| 				return dns, nil | 				return dns, nil | ||||||
| 			}, | 			}, | ||||||
| 			want: DNSConfig{ | 			want: DNSConfig{ | ||||||
| 				MagicDNS:   true, | 				MagicDNS:         true, | ||||||
| 				BaseDomain: "example.com", | 				BaseDomain:       "example.com", | ||||||
|  | 				OverrideLocalDNS: false, | ||||||
| 				Nameservers: Nameservers{ | 				Nameservers: Nameservers{ | ||||||
| 					Global: []string{"1.1.1.1", "8.8.8.8"}, | 					Global: []string{"1.1.1.1", "8.8.8.8"}, | ||||||
| 					Split:  map[string][]string{ | 					Split:  map[string][]string{ | ||||||
| @ -301,7 +340,7 @@ func TestReadConfigFromEnv(t *testing.T) { | |||||||
| 			conf, err := tt.setup(t) | 			conf, err := tt.setup(t) | ||||||
| 			require.NoError(t, err) | 			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) | 				t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
|  | |||||||
| @ -13,3 +13,4 @@ server_url: "https://server.derp.no" | |||||||
| dns: | dns: | ||||||
|   magic_dns: true |   magic_dns: true | ||||||
|   base_domain: derp.no |   base_domain: derp.no | ||||||
|  |   override_local_dns: false | ||||||
|  | |||||||
| @ -13,3 +13,4 @@ server_url: "https://derp.no" | |||||||
| dns: | dns: | ||||||
|   magic_dns: true |   magic_dns: true | ||||||
|   base_domain: clients.derp.no |   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 |   magic_dns: true | ||||||
|   base_domain: example.com |   base_domain: example.com | ||||||
| 
 | 
 | ||||||
|  |   override_local_dns: false | ||||||
|   nameservers: |   nameservers: | ||||||
|     global: |     global: | ||||||
|       - 1.1.1.1 |       - 1.1.1.1 | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ dns: | |||||||
|   magic_dns: false |   magic_dns: false | ||||||
|   base_domain: example.com |   base_domain: example.com | ||||||
| 
 | 
 | ||||||
|  |   override_local_dns: false | ||||||
|   nameservers: |   nameservers: | ||||||
|     global: |     global: | ||||||
|       - 1.1.1.1 |       - 1.1.1.1 | ||||||
|  | |||||||
| @ -15,4 +15,6 @@ policy: | |||||||
|   type: file |   type: file | ||||||
|   path: "/etc/policy.hujson" |   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_PREFIXES_V6":                       "fd7a:115c:a1e0::/48", | ||||||
| 		"HEADSCALE_DNS_BASE_DOMAIN":                   "headscale.net", | 		"HEADSCALE_DNS_BASE_DOMAIN":                   "headscale.net", | ||||||
| 		"HEADSCALE_DNS_MAGIC_DNS":                     "true", | 		"HEADSCALE_DNS_MAGIC_DNS":                     "true", | ||||||
|  | 		"HEADSCALE_DNS_OVERRIDE_LOCAL_DNS":            "false", | ||||||
| 		"HEADSCALE_DNS_NAMESERVERS_GLOBAL":            "127.0.0.11 1.1.1.1", | 		"HEADSCALE_DNS_NAMESERVERS_GLOBAL":            "127.0.0.11 1.1.1.1", | ||||||
| 		"HEADSCALE_PRIVATE_KEY_PATH":                  "/tmp/private.key", | 		"HEADSCALE_PRIVATE_KEY_PATH":                  "/tmp/private.key", | ||||||
| 		"HEADSCALE_NOISE_PRIVATE_KEY_PATH":            "/tmp/noise_private.key", | 		"HEADSCALE_NOISE_PRIVATE_KEY_PATH":            "/tmp/noise_private.key", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user