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