mirror of
https://github.com/juanfont/headscale.git
synced 2025-04-23 12:05:44 -04: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
|
||||||
|
if dns.OverrideLocalDNS {
|
||||||
cfg.Resolvers = dns.globalResolvers()
|
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"
|
||||||
@ -36,6 +37,7 @@ func TestReadConfig(t *testing.T) {
|
|||||||
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"},
|
||||||
@ -101,6 +103,7 @@ func TestReadConfig(t *testing.T) {
|
|||||||
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",
|
||||||
|
|
||||||
@ -274,6 +312,7 @@ func TestReadConfigFromEnv(t *testing.T) {
|
|||||||
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