diff --git a/CHANGELOG.md b/CHANGELOG.md index fb60de1d..e92ed5b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Fix some DNS config issues [#660](https://github.com/juanfont/headscale/issues/660) - Make it possible to disable TS2019 with build flag [#928](https://github.com/juanfont/headscale/pull/928) - Fix OIDC registration issues [#960](https://github.com/juanfont/headscale/pull/960) and [#971](https://github.com/juanfont/headscale/pull/971) +- Add support for specifying NextDNS DNS-over-HTTPS resolver [#940](https://github.com/juanfont/headscale/pull/940) ## 0.16.4 (2022-08-21) diff --git a/config-example.yaml b/config-example.yaml index 9e33539e..d1e46e26 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -214,6 +214,18 @@ dns_config: nameservers: - 1.1.1.1 + # NextDNS (see https://tailscale.com/kb/1218/nextdns/). + # "abc123" is example NextDNS ID, replace with yours. + # + # With metadata sharing: + # nameservers: + # - https://dns.nextdns.io/abc123 + # + # Without metadata sharing: + # nameservers: + # - 2a07:a8c0::ab:c123 + # - 2a07:a8c1::ab:c123 + # Split DNS (see https://tailscale.com/kb/1054/dns/), # list of search domains and the DNS to query for each one. # diff --git a/config.go b/config.go index e38a9f2a..06617752 100644 --- a/config.go +++ b/config.go @@ -383,10 +383,21 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { if viper.IsSet("dns_config.nameservers") { nameserversStr := viper.GetStringSlice("dns_config.nameservers") - nameservers := make([]netip.Addr, len(nameserversStr)) - resolvers := make([]*dnstype.Resolver, len(nameserversStr)) + nameservers := []netip.Addr{} + resolvers := []*dnstype.Resolver{} - for index, nameserverStr := range nameserversStr { + for _, nameserverStr := range nameserversStr { + // Search for explicit DNS-over-HTTPS resolvers + if strings.HasPrefix(nameserverStr, "https://") { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nameserverStr, + }) + + // This nameserver can not be parsed as an IP address + continue + } + + // Parse nameserver as a regular IP nameserver, err := netip.ParseAddr(nameserverStr) if err != nil { log.Error(). @@ -395,10 +406,10 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { Msgf("Could not parse nameserver IP: %s", nameserverStr) } - nameservers[index] = nameserver - resolvers[index] = &dnstype.Resolver{ + nameservers = append(nameservers, nameserver) + resolvers = append(resolvers, &dnstype.Resolver{ Addr: nameserver.String(), - } + }) } dnsConfig.Nameservers = nameservers diff --git a/dns.go b/dns.go index 8f1e88da..6636627f 100644 --- a/dns.go +++ b/dns.go @@ -3,11 +3,13 @@ package headscale import ( "fmt" "net/netip" + "net/url" "strings" mapset "github.com/deckarep/golang-set/v2" "go4.org/netipx" "tailscale.com/tailcfg" + "tailscale.com/types/dnstype" "tailscale.com/util/dnsname" ) @@ -20,6 +22,10 @@ const ( ipv6AddressLength = 128 ) +const ( + nextDNSDoHPrefix = "https://dns.nextdns.io" +) + // generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. // This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS // server (listening in 100.100.100.100 udp/53) should be used for. @@ -152,16 +158,39 @@ func generateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { return fqdns } +// If any nextdns DoH resolvers are present in the list of resolvers it will +// take metadata from the machine metadata and instruct tailscale to add it +// to the requests. This makes it possible to identify from which device the +// requests come in the NextDNS dashboard. +// +// This will produce a resolver like: +// `https://dns.nextdns.io/?device_name=node-name&device_model=linux&device_ip=100.64.0.1` +func addNextDNSMetadata(resolvers []*dnstype.Resolver, machine Machine) { + for _, resolver := range resolvers { + if strings.HasPrefix(resolver.Addr, nextDNSDoHPrefix) { + attrs := url.Values{ + "device_name": []string{machine.Hostname}, + "device_model": []string{machine.HostInfo.OS}, + } + + if len(machine.IPAddresses) > 0 { + attrs.Add("device_ip", machine.IPAddresses[0].String()) + } + + resolver.Addr = fmt.Sprintf("%s?%s", resolver.Addr, attrs.Encode()) + } + } +} + func getMapResponseDNSConfig( dnsConfigOrig *tailcfg.DNSConfig, baseDomain string, machine Machine, peers Machines, ) *tailcfg.DNSConfig { - var dnsConfig *tailcfg.DNSConfig + var dnsConfig *tailcfg.DNSConfig = dnsConfigOrig.Clone() if dnsConfigOrig != nil && dnsConfigOrig.Proxied { // if MagicDNS is enabled // Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN - dnsConfig = dnsConfigOrig.Clone() dnsConfig.Domains = append( dnsConfig.Domains, fmt.Sprintf( @@ -184,5 +213,7 @@ func getMapResponseDNSConfig( dnsConfig = dnsConfigOrig } + addNextDNSMetadata(dnsConfig.Resolvers, machine) + return dnsConfig }