From 3f3cfedffac94ea2df5ed03204ac6ab6741a8d09 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 28 Sep 2021 00:22:29 +0200 Subject: [PATCH 01/29] Add support for MagicDNS --- cmd/headscale/cli/utils.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 7ada6693..f5c2a6db 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -108,7 +108,12 @@ func GetDNSConfig() *tailcfg.DNSConfig { dnsConfig.Domains = viper.GetStringSlice("dns_config.domains") } + if viper.IsSet("dns_config.magic_dns") { + dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") + } + return dnsConfig + } return nil From c9e4da3ff5d4879a27ba8d4ca1618010d7573d7a Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 11:11:18 +0200 Subject: [PATCH 02/29] Improving documentation for DNS config --- config.json.postgres.example | 5 ++++- config.json.sqlite.example | 5 ++++- docs/DNS.md | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 docs/DNS.md diff --git a/config.json.postgres.example b/config.json.postgres.example index aba72063..5e5c5395 100644 --- a/config.json.postgres.example +++ b/config.json.postgres.example @@ -20,6 +20,9 @@ "dns_config": { "nameservers": [ "1.1.1.1" - ] + ], + "domains": [], + "magic_dns": true, + "base_domain": "example.com" } } diff --git a/config.json.sqlite.example b/config.json.sqlite.example index b22e5ace..a357bc85 100644 --- a/config.json.sqlite.example +++ b/config.json.sqlite.example @@ -16,6 +16,9 @@ "dns_config": { "nameservers": [ "1.1.1.1" - ] + ], + "domains": [], + "magic_dns": true, + "base_domain": "example.com" } } diff --git a/docs/DNS.md b/docs/DNS.md new file mode 100644 index 00000000..ca151bf2 --- /dev/null +++ b/docs/DNS.md @@ -0,0 +1,33 @@ +# DNS in Headscale + +Headscale supports Tailscale's DNS configuration and MagicDNS. Please have a look to their KB to better understand what this means: + +- https://tailscale.com/kb/1054/dns/ +- https://tailscale.com/kb/1081/magicdns/ +- https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ + +Long story short, you can define the DNS servers you want to use in your tailnets, activate MagicDNS (so you don't have to remember the IP addresses of your nodes), define search domains, as well as predefined hosts. Headscale will inject that settings into your nodes. + + +## Configuration reference + +The setup is done via the `config.json` file, under the `dns_config` key. + +```json +{ + "server_url": "http://127.0.0.1:8001", + "listen_addr": "0.0.0.0:8001", + "private_key_path": "private.key", + //... + "dns_config": { + "nameservers": ["1.1.1.1", "8.8.8.8"], + "domains": [], + "magic_dns": true, + "base_domain": "example.com" + } +} +``` +- `nameservers`: The list of DNS servers to use. +- `domains`: Search domains to inject. +- `magic_dns`: Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). +- `base_domain`: Defines the base domain to create the hostnames for MagicDNS. `base_domain` must be a FQDNs, without the trailing dot. The FQDN of the hosts will be `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_). \ No newline at end of file From 5dbf6b512741c4955dd9b7016616ea0849357f52 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 11:14:18 +0200 Subject: [PATCH 03/29] Extended DNS config unit tests --- cmd/headscale/headscale_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 58bf5899..bddea94c 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -117,12 +117,12 @@ func (*Suite) TestDNSConfigLoading(c *check.C) { err = cli.LoadConfig(tmpDir) c.Assert(err, check.IsNil) - dnsConfig := cli.GetDNSConfig() - fmt.Println(dnsConfig) + dnsConfig, baseDomain := cli.GetDNSConfig() c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1") - c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1") + c.Assert(dnsConfig.Proxied, check.Equals, true) + c.Assert(baseDomain, check.Equals, "example.com") } func writeConfig(c *check.C, tmpDir string, configYaml []byte) { From 656237e167fcee22c3f88107c3ac93f91d25043f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 11:20:42 +0200 Subject: [PATCH 04/29] Propagate dns config vales across Headscale --- api.go | 16 +++++----------- app.go | 1 + cmd/headscale/cli/utils.go | 18 +++++++++++++----- integration_test.go | 13 +++++++++++++ machine.go | 14 +++++++++----- poll.go | 2 +- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/api.go b/api.go index e2a56185..c7fbf8a1 100644 --- a/api.go +++ b/api.go @@ -218,7 +218,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac Str("func", "getMapResponse"). Str("machine", req.Hostinfo.Hostname). Msg("Creating Map response") - node, err := m.toNode(true) + node, err := h.toNode(m, true) if err != nil { log.Error(). Str("func", "getMapResponse"). @@ -242,17 +242,11 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac } resp := tailcfg.MapResponse{ - KeepAlive: false, - Node: node, - Peers: *peers, - //TODO(kradalby): As per tailscale docs, if DNSConfig is nil, - // it means its not updated, maybe we can have some logic - // to check and only pass updates when its updates. - // This is probably more relevant if we try to implement - // "MagicDNS" + KeepAlive: false, + Node: node, + Peers: *peers, DNSConfig: h.cfg.DNSConfig, - SearchPaths: []string{}, - Domain: "headscale.net", + Domain: h.cfg.BaseDomain, PacketFilter: *h.aclRules, DERPMap: h.cfg.DerpMap, UserProfiles: []tailcfg.UserProfile{profile}, diff --git a/app.go b/app.go index dc398eb7..9e29640f 100644 --- a/app.go +++ b/app.go @@ -27,6 +27,7 @@ type Config struct { DerpMap *tailcfg.DERPMap EphemeralNodeInactivityTimeout time.Duration IPPrefix netaddr.IPPrefix + BaseDomain string DBtype string DBpath string diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index f5c2a6db..bff7693d 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -76,7 +76,7 @@ func LoadConfig(path string) error { } -func GetDNSConfig() *tailcfg.DNSConfig { +func GetDNSConfig() (*tailcfg.DNSConfig, string) { if viper.IsSet("dns_config") { dnsConfig := &tailcfg.DNSConfig{} @@ -112,11 +112,16 @@ func GetDNSConfig() *tailcfg.DNSConfig { dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") } - return dnsConfig - + var baseDomain string + if viper.IsSet("dns_config.base_domain") { + baseDomain = viper.GetString("dns_config.base_domain") + } else { + baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled + } + return dnsConfig, baseDomain } - return nil + return nil, "" } func absPath(path string) string { @@ -149,12 +154,15 @@ func getHeadscaleApp() (*headscale.Headscale, error) { return nil, err } + dnsConfig, baseDomain := GetDNSConfig() + cfg := headscale.Config{ ServerURL: viper.GetString("server_url"), Addr: viper.GetString("listen_addr"), PrivateKeyPath: absPath(viper.GetString("private_key_path")), DerpMap: derpMap, IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), + BaseDomain: baseDomain, EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"), @@ -174,7 +182,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { TLSCertPath: absPath(viper.GetString("tls_cert_path")), TLSKeyPath: absPath(viper.GetString("tls_key_path")), - DNSConfig: GetDNSConfig(), + DNSConfig: dnsConfig, } h, err := headscale.NewHeadscale(cfg) diff --git a/integration_test.go b/integration_test.go index f62ca1da..e80ff46b 100644 --- a/integration_test.go +++ b/integration_test.go @@ -642,6 +642,19 @@ func (s *IntegrationTestSuite) TestTailDrop() { } } +// func (s *IntegrationTestSuite) TestMagicDNS() { +// for _, scales := range s.namespaces { +// ips, err := getIPs(scales.tailscales) +// assert.Nil(s.T(), err) +// apiURLs, err := getAPIURLs(scales.tailscales) +// assert.Nil(s.T(), err) + +// for hostname, tailscale := range scales.tailscales { + +// } +// } +// } + func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) { ips := make(map[string]netaddr.IP) for hostname, tailscale := range tailscales { diff --git a/machine.go b/machine.go index 1d4939c1..d963d35f 100644 --- a/machine.go +++ b/machine.go @@ -52,7 +52,7 @@ func (m Machine) isAlreadyRegistered() bool { // toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes // as per the expected behaviour in the official SaaS -func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { +func (h *Headscale) toNode(m Machine, includeRoutes bool) (*tailcfg.Node, error) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { return nil, err @@ -147,10 +147,12 @@ func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { keyExpiry = time.Time{} } + hostname := fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, h.cfg.BaseDomain) + n := tailcfg.Node{ ID: tailcfg.NodeID(m.ID), // this is the actual ID StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent - Name: hostinfo.Hostname, + Name: hostname, User: tailcfg.UserID(m.NamespaceID), Key: tailcfg.NodeKey(nKey), KeyExpiry: keyExpiry, @@ -169,6 +171,8 @@ func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { MachineAuthorized: m.Registered, Capabilities: []string{tailcfg.CapabilityFileSharing}, } + // TODO(juanfont): Node also has Sharer when is a shared node with info on the profile + return &n, nil } @@ -179,7 +183,7 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { Msg("Finding peers") machines := []Machine{} - if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", + if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered", m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { log.Error().Err(err).Msg("Error accessing db") return nil, err @@ -194,14 +198,14 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { peers := []*tailcfg.Node{} for _, mn := range machines { - peer, err := mn.toNode(true) + peer, err := h.toNode(mn, true) if err != nil { return nil, err } peers = append(peers, peer) } for _, sharedMachine := range sharedMachines { - peer, err := sharedMachine.Machine.toNode(false) // shared nodes do not expose their routes + peer, err := h.toNode(sharedMachine.Machine, false) // shared nodes do not expose their routes if err != nil { return nil, err } diff --git a/poll.go b/poll.go index 60bfa9ea..40b3e28a 100644 --- a/poll.go +++ b/poll.go @@ -440,7 +440,7 @@ func (h *Headscale) scheduledPollWorker( case <-updateCheckerTicker.C: // Send an update request regardless of outdated or not, if data is sent // to the node is determined in the updateChan consumer block - n, _ := m.toNode(true) + n, _ := h.toNode(m, true) err := h.sendRequestOnUpdateChannel(n) if err != nil { log.Error(). From e432e98413226e3d2779452bcd817bcbaccdf480 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 12:12:22 +0200 Subject: [PATCH 05/29] Send hostname in toNode --- machine.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/machine.go b/machine.go index d963d35f..fa643c5c 100644 --- a/machine.go +++ b/machine.go @@ -147,7 +147,12 @@ func (h *Headscale) toNode(m Machine, includeRoutes bool) (*tailcfg.Node, error) keyExpiry = time.Time{} } - hostname := fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, h.cfg.BaseDomain) + var hostname string + if h.cfg.DNSConfig.Proxied { // MagicDNS + hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, h.cfg.BaseDomain) + } else { + hostname = m.Name + } n := tailcfg.Node{ ID: tailcfg.NodeID(m.ID), // this is the actual ID From 45e71ecba09e22611b5b092c8b1c4a632eb901c4 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 12:13:05 +0200 Subject: [PATCH 06/29] Generated MagicDNS search domains (only in 100.64.0.0/10) --- app.go | 12 ++++++++++++ dns.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 dns.go diff --git a/app.go b/app.go index 9e29640f..c8c799b1 100644 --- a/app.go +++ b/app.go @@ -16,6 +16,7 @@ import ( "gorm.io/gorm" "inet.af/netaddr" "tailscale.com/tailcfg" + "tailscale.com/types/dnstype" "tailscale.com/types/wgkey" ) @@ -104,6 +105,17 @@ func NewHeadscale(cfg Config) (*Headscale, error) { return nil, err } + if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS + magicDNSDomains, err := h.generateMagicDNSRootDomains() + if err != nil { + return nil, err + } + h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) + for _, d := range *magicDNSDomains { + h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil + } + } + return &h, nil } diff --git a/dns.go b/dns.go new file mode 100644 index 00000000..91afe519 --- /dev/null +++ b/dns.go @@ -0,0 +1,30 @@ +package headscale + +import ( + "fmt" + + "tailscale.com/util/dnsname" +) + +func (h *Headscale) generateMagicDNSRootDomains() (*[]dnsname.FQDN, error) { + base, err := dnsname.ToFQDN(h.cfg.BaseDomain) + if err != nil { + return nil, err + } + + // TODO(juanfont): we are not handing over IPv6 addresses yet + // and in fact this is Tailscale.com's range (not the fd7a:115c:a1e0: range in the fc00::/7 network) + ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") + fqdns := []dnsname.FQDN{base, ipv6base} + + for i := 64; i <= 127; i++ { + fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.100.in-addr.arpa.", i)) + if err != nil { + // TODO: propagate error + continue + } + fqdns = append(fqdns, fqdn) + } + + return &fqdns, nil +} From 36ae14bccf9af1a940d262cb6945b67606876654 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 12:13:19 +0200 Subject: [PATCH 07/29] Send search domains --- api.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api.go b/api.go index c7fbf8a1..be087eb1 100644 --- a/api.go +++ b/api.go @@ -241,11 +241,27 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac DisplayName: m.Namespace.Name, } + var dnsConfig *tailcfg.DNSConfig + if h.cfg.DNSConfig.Proxied { // if MagicDNS is enabled + // TODO(juanfont): We should not be regenerating this all the time + // And we should only send the domains of the peers (this own namespace + those from the shared peers) + namespaces, err := h.ListNamespaces() + if err != nil { + return nil, err + } + dnsConfig := h.cfg.DNSConfig.Clone() + for _, ns := range *namespaces { + dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", ns.Name, h.cfg.BaseDomain)) + } + } else { + dnsConfig = h.cfg.DNSConfig + } + resp := tailcfg.MapResponse{ KeepAlive: false, Node: node, Peers: *peers, - DNSConfig: h.cfg.DNSConfig, + DNSConfig: dnsConfig, Domain: h.cfg.BaseDomain, PacketFilter: *h.aclRules, DERPMap: h.cfg.DerpMap, From 19492650d4781ae6c3b4ace9d166274069229dc1 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 13:03:08 +0200 Subject: [PATCH 08/29] Fixed error on assign --- api.go | 2 +- cmd/headscale/cli/utils.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api.go b/api.go index be087eb1..84754e41 100644 --- a/api.go +++ b/api.go @@ -249,7 +249,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac if err != nil { return nil, err } - dnsConfig := h.cfg.DNSConfig.Clone() + dnsConfig = h.cfg.DNSConfig.Clone() for _, ns := range *namespaces { dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", ns.Name, h.cfg.BaseDomain)) } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index bff7693d..53f96055 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -118,6 +118,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { } else { baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled } + return dnsConfig, baseDomain } From 8d60ae2c7e9b77b95d1226f9dd3615b5ad4d1964 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 13:03:41 +0200 Subject: [PATCH 09/29] Tidy gomod --- app.go | 1 + go.mod | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index c8c799b1..57fa47bb 100644 --- a/app.go +++ b/app.go @@ -116,6 +116,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } } + fmt.Printf("dns: %+v\n", h.cfg.DNSConfig) return &h, nil } diff --git a/go.mod b/go.mod index 390fac91..4e910d11 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/docker/cli v20.10.8+incompatible // indirect github.com/docker/docker v20.10.8+incompatible // indirect github.com/efekarakus/termcolor v1.0.1 - github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gin-gonic/gin v1.7.4 + github.com/gofrs/uuid v4.0.0+incompatible github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.5 github.com/lib/pq v1.10.3 // indirect From 47dcc940c0f3baac6e6eba452a6e202c42bcd3e6 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 14:49:14 +0200 Subject: [PATCH 10/29] Fixed issue in tests --- machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine.go b/machine.go index fa643c5c..3b33e82c 100644 --- a/machine.go +++ b/machine.go @@ -148,7 +148,7 @@ func (h *Headscale) toNode(m Machine, includeRoutes bool) (*tailcfg.Node, error) } var hostname string - if h.cfg.DNSConfig.Proxied { // MagicDNS + if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // MagicDNS hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, h.cfg.BaseDomain) } else { hostname = m.Name From 64185cc2bcf6f7550f67787debec974228847ce7 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 2 Oct 2021 15:18:05 +0200 Subject: [PATCH 11/29] Fixed go mod --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 4912508c..3a49c0fd 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/efekarakus/termcolor v1.0.1 github.com/gin-gonic/gin v1.7.4 github.com/gofrs/uuid v4.0.0+incompatible + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.5 github.com/lib/pq v1.10.3 // indirect @@ -24,7 +26,7 @@ require ( github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.7.0 github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88 - github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e // indirect + github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect From e60ceefea9c7c9db0a667e88b295652d748d5dc1 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 18:03:44 +0200 Subject: [PATCH 12/29] Fixing nil issue --- api.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/api.go b/api.go index 84754e41..2ac6f299 100644 --- a/api.go +++ b/api.go @@ -242,7 +242,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac } var dnsConfig *tailcfg.DNSConfig - if h.cfg.DNSConfig.Proxied { // if MagicDNS is enabled + if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS is enabled // TODO(juanfont): We should not be regenerating this all the time // And we should only send the domains of the peers (this own namespace + those from the shared peers) namespaces, err := h.ListNamespaces() @@ -329,6 +329,11 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, resp := tailcfg.RegisterResponse{} pak, err := h.checkKeyValidity(req.Auth.AuthKey) if err != nil { + log.Error(). + Str("func", "handleAuthKey"). + Str("machine", m.Name). + Err(err). + Msg("Failed authentication via AuthKey") resp.MachineAuthorized = false respBody, err := encode(resp, &idKey, h.privateKey) if err != nil { @@ -341,10 +346,6 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, return } c.Data(200, "application/json; charset=utf-8", respBody) - log.Error(). - Str("func", "handleAuthKey"). - Str("machine", m.Name). - Msg("Failed authentication via AuthKey") return } From ef0f7c0c0992ab9759b74d21e12f4e45a89f5507 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 18:04:08 +0200 Subject: [PATCH 13/29] Integration tests for MagicDNS working --- app.go | 1 - integration_test.go | 43 +++++++++++++++++++++++--------- integration_test/etc/config.json | 12 +++++++-- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app.go b/app.go index 57fa47bb..c8c799b1 100644 --- a/app.go +++ b/app.go @@ -116,7 +116,6 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } } - fmt.Printf("dns: %+v\n", h.cfg.DNSConfig) return &h, nil } diff --git a/integration_test.go b/integration_test.go index e80ff46b..00cff388 100644 --- a/integration_test.go +++ b/integration_test.go @@ -589,7 +589,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { _, err = executeCommand( &tailscale, command, - []string{"ALL_PROXY=socks5://localhost:1055/"}, + []string{"ALL_PROXY=socks5://localhost:1055"}, ) if err == nil { break @@ -642,18 +642,37 @@ func (s *IntegrationTestSuite) TestTailDrop() { } } -// func (s *IntegrationTestSuite) TestMagicDNS() { -// for _, scales := range s.namespaces { -// ips, err := getIPs(scales.tailscales) -// assert.Nil(s.T(), err) -// apiURLs, err := getAPIURLs(scales.tailscales) -// assert.Nil(s.T(), err) +func (s *IntegrationTestSuite) TestMagicDNS() { + for namespace, scales := range s.namespaces { + ips, err := getIPs(scales.tailscales) + assert.Nil(s.T(), err) + for hostname, tailscale := range scales.tailscales { + for peername, ip := range ips { + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + if peername != hostname { + command := []string{ + "tailscale", "ping", + "--timeout=10s", + "--c=20", + "--until-direct=true", + fmt.Sprintf("%s.%s.headscale.net", peername, namespace), + } -// for hostname, tailscale := range scales.tailscales { - -// } -// } -// } + fmt.Printf("Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) + result, err := executeCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", hostname, result) + assert.Contains(t, result, "pong") + } + }) + } + } + } +} func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) { ips := make(map[string]netaddr.IP) diff --git a/integration_test/etc/config.json b/integration_test/etc/config.json index 8a6fd962..8868d1be 100644 --- a/integration_test/etc/config.json +++ b/integration_test/etc/config.json @@ -7,5 +7,13 @@ "db_type": "sqlite3", "db_path": "/tmp/integration_test_db.sqlite3", "acl_policy_path": "", - "log_level": "debug" -} + "log_level": "debug", + "dns_config": { + "nameservers": [ + "1.1.1.1" + ], + "domains": [], + "magic_dns": true, + "base_domain": "headscale.net" + } +} \ No newline at end of file From ec911981c217dea05564e47e9eb70c9e4cc40a09 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 19:43:58 +0200 Subject: [PATCH 14/29] Do not allow magicdns if not nameservers set up --- cmd/headscale/cli/utils.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 53f96055..8f2fbdf1 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -108,8 +108,10 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { dnsConfig.Domains = viper.GetStringSlice("dns_config.domains") } - if viper.IsSet("dns_config.magic_dns") { - dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") + if len(dnsConfig.Nameservers) > 0 { + if viper.IsSet("dns_config.magic_dns") { + dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") + } } var baseDomain string From 1a41a9f2c781a9ac4bc56a008dae8b483044ec92 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 20:27:45 +0200 Subject: [PATCH 15/29] Updated readme --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5f691a6c..326ae9e3 100644 --- a/README.md +++ b/README.md @@ -28,18 +28,18 @@ Headscale implements this coordination server. - [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [x] DNS (passing DNS servers to nodes) - [x] Share nodes between ~~users~~ namespaces -- [ ] MagicDNS / Smart DNS +- [x] MagicDNS (see `docs/`) ## Client OS support -| OS | Supports headscale | -| --- | --- | -| Linux | Yes | -| OpenBSD | Yes | -| macOS | Yes (see `/apple` on your headscale for more information) | -| Windows | Yes | +| OS | Supports headscale | +| ------- | ----------------------------------------------------------------------------------------------------------------- | +| Linux | Yes | +| OpenBSD | Yes | +| macOS | Yes (see `/apple` on your headscale for more information) | +| Windows | Yes | | Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) | -| iOS | Not yet | +| iOS | Not yet | ## Roadmap 🤷 From 02bc7314f4bce6c29171d85343e771062e252074 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 4 Oct 2021 21:47:09 +0200 Subject: [PATCH 16/29] Update dns.go Co-authored-by: Kristoffer Dalby --- dns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dns.go b/dns.go index 91afe519..74a85aee 100644 --- a/dns.go +++ b/dns.go @@ -12,7 +12,7 @@ func (h *Headscale) generateMagicDNSRootDomains() (*[]dnsname.FQDN, error) { return nil, err } - // TODO(juanfont): we are not handing over IPv6 addresses yet + // TODO(juanfont): we are not handing out IPv6 addresses yet // and in fact this is Tailscale.com's range (not the fd7a:115c:a1e0: range in the fc00::/7 network) ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") fqdns := []dnsname.FQDN{base, ipv6base} From da4a9dadd52e0e56aa8cd852a3b543738df0fb63 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 22:16:53 +0200 Subject: [PATCH 17/29] Warn users when MagicDNS is set with no DNS servers --- cmd/headscale/cli/utils.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 9aa63525..f879f91e 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -108,9 +108,13 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) { dnsConfig.Domains = viper.GetStringSlice("dns_config.domains") } - if len(dnsConfig.Nameservers) > 0 { - if viper.IsSet("dns_config.magic_dns") { - dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") + if viper.IsSet("dns_config.magic_dns") { + magicDNS := viper.GetBool("dns_config.magic_dns") + if len(dnsConfig.Nameservers) > 0 { + dnsConfig.Proxied = magicDNS + } else if magicDNS { + log.Warn(). + Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.") } } @@ -186,7 +190,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { TLSKeyPath: absPath(viper.GetString("tls_key_path")), DNSConfig: dnsConfig, - + ACMEEmail: viper.GetString("acme_email"), ACMEURL: viper.GetString("acme_url"), } From 088e8248d3a3691596df2c218b1abc0647e58ec1 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 22:50:33 +0200 Subject: [PATCH 18/29] Improved doc --- docs/DNS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DNS.md b/docs/DNS.md index ca151bf2..85bf9f44 100644 --- a/docs/DNS.md +++ b/docs/DNS.md @@ -29,5 +29,5 @@ The setup is done via the `config.json` file, under the `dns_config` key. ``` - `nameservers`: The list of DNS servers to use. - `domains`: Search domains to inject. -- `magic_dns`: Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). +- `magic_dns`: Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). Only works if there is at least a nameserver defined. - `base_domain`: Defines the base domain to create the hostnames for MagicDNS. `base_domain` must be a FQDNs, without the trailing dot. The FQDN of the hosts will be `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_). \ No newline at end of file From 61870a275f11733100bf5881fc414062368caf84 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 22:51:05 +0200 Subject: [PATCH 19/29] WIP preparation for merge --- machine.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/machine.go b/machine.go index ca0e5faa..5b045e3b 100644 --- a/machine.go +++ b/machine.go @@ -52,7 +52,7 @@ func (m Machine) isAlreadyRegistered() bool { // toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes // as per the expected behaviour in the official SaaS -func (h *Headscale) toNode(m Machine, includeRoutes bool) (*tailcfg.Node, error) { +func (m *Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) (*tailcfg.Node, error) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { return nil, err @@ -148,8 +148,8 @@ func (h *Headscale) toNode(m Machine, includeRoutes bool) (*tailcfg.Node, error) } var hostname string - if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // MagicDNS - hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, h.cfg.BaseDomain) + if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS + hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, baseDomain) } else { hostname = m.Name } @@ -203,14 +203,14 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { peers := []*tailcfg.Node{} for _, mn := range machines { - peer, err := h.toNode(mn, true) + peer, err := mn.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) if err != nil { return nil, err } peers = append(peers, peer) } for _, sharedMachine := range sharedMachines { - peer, err := h.toNode(sharedMachine.Machine, false) // shared nodes do not expose their routes + peer, err := sharedMachine.Machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, false) // shared nodes do not expose their routes if err != nil { return nil, err } From a0fa652449ec52aa00e268796b9e4eb28e6deaf8 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 23:49:16 +0200 Subject: [PATCH 20/29] MagicDNS changes merged back --- api.go | 4 ++-- machine.go | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/api.go b/api.go index d090a2a0..e360187c 100644 --- a/api.go +++ b/api.go @@ -225,7 +225,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma Str("func", "getMapResponse"). Str("machine", req.Hostinfo.Hostname). Msg("Creating Map response") - node, err := m.toNode(true) + node, err := m.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true) if err != nil { log.Error(). Str("func", "getMapResponse"). @@ -249,7 +249,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma DisplayName: m.Namespace.Name, } - nodePeers, err := peers.toNodes(true) + nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true) if err != nil { log.Error(). Str("func", "getMapResponse"). diff --git a/machine.go b/machine.go index 7a9df2e8..0ea97d91 100644 --- a/machine.go +++ b/machine.go @@ -360,11 +360,11 @@ func (ms MachinesP) String() string { return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp)) } -func (ms Machines) toNodes(includeRoutes bool) ([]*tailcfg.Node, error) { +func (ms Machines) toNodes(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) ([]*tailcfg.Node, error) { nodes := make([]*tailcfg.Node, len(ms)) for index, machine := range ms { - node, err := machine.toNode(includeRoutes) + node, err := machine.toNode(baseDomain, dnsConfig, includeRoutes) if err != nil { return nil, err } @@ -377,7 +377,7 @@ func (ms Machines) toNodes(includeRoutes bool) ([]*tailcfg.Node, error) { // toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes // as per the expected behaviour in the official SaaS -func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { +func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) (*tailcfg.Node, error) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { return nil, err @@ -472,10 +472,17 @@ func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { keyExpiry = time.Time{} } + var hostname string + if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS + hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, baseDomain) + } else { + hostname = m.Name + } + n := tailcfg.Node{ ID: tailcfg.NodeID(m.ID), // this is the actual ID StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent - Name: hostinfo.Hostname, + Name: hostname, User: tailcfg.UserID(m.NamespaceID), Key: tailcfg.NodeKey(nKey), KeyExpiry: keyExpiry, From b02a9f9769856cf7e2b83bc37a95b5d7f5b902d7 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Mon, 4 Oct 2021 23:50:26 +0200 Subject: [PATCH 21/29] Go mod updates --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 70428315..704bba40 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/opencontainers/runc v1.0.2 // indirect github.com/ory/dockertest/v3 v3.7.0 - github.com/prometheus/client_golang v1.11.0 // indirect + github.com/prometheus/client_golang v1.11.0 github.com/pterm/pterm v0.12.30 github.com/rs/zerolog v1.25.0 github.com/spf13/cobra v1.2.1 @@ -29,7 +29,7 @@ require ( github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/zsais/go-gin-prometheus v0.1.0 // indirect + github.com/zsais/go-gin-prometheus v0.1.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect From c9a411e341582a4a9be4acefc4b1176d1ba2221b Mon Sep 17 00:00:00 2001 From: Juan Font Date: Tue, 5 Oct 2021 17:47:21 +0200 Subject: [PATCH 22/29] Preload namespace --- machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine.go b/machine.go index 0ea97d91..688dc783 100644 --- a/machine.go +++ b/machine.go @@ -63,7 +63,7 @@ func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) { Msg("Finding direct peers") machines := Machines{} - if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", + if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered", m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { log.Error().Err(err).Msg("Error accessing db") return nil, err From 6981543db68ca7e1008871c4c1564932e5831d31 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Tue, 5 Oct 2021 19:00:40 +0200 Subject: [PATCH 23/29] Only search domain from current namespace in MapResponse --- api.go | 11 ++--------- dns.go | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/api.go b/api.go index e360187c..dd762a07 100644 --- a/api.go +++ b/api.go @@ -260,16 +260,9 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma var dnsConfig *tailcfg.DNSConfig if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS is enabled - // TODO(juanfont): We should not be regenerating this all the time - // And we should only send the domains of the peers (this own namespace + those from the shared peers) - namespaces, err := h.ListNamespaces() - if err != nil { - return nil, err - } + // Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN dnsConfig = h.cfg.DNSConfig.Clone() - for _, ns := range *namespaces { - dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", ns.Name, h.cfg.BaseDomain)) - } + dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", m.Namespace.Name, h.cfg.BaseDomain)) } else { dnsConfig = h.cfg.DNSConfig } diff --git a/dns.go b/dns.go index 74a85aee..9cd747f6 100644 --- a/dns.go +++ b/dns.go @@ -13,7 +13,7 @@ func (h *Headscale) generateMagicDNSRootDomains() (*[]dnsname.FQDN, error) { } // TODO(juanfont): we are not handing out IPv6 addresses yet - // and in fact this is Tailscale.com's range (not the fd7a:115c:a1e0: range in the fc00::/7 network) + // and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network) ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") fqdns := []dnsname.FQDN{base, ipv6base} From 1a0f6f6e39669160ef0d3784d4f62bf4a257d7f8 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Tue, 5 Oct 2021 19:01:56 +0200 Subject: [PATCH 24/29] Added note on TODO --- dns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dns.go b/dns.go index 9cd747f6..68df4605 100644 --- a/dns.go +++ b/dns.go @@ -17,10 +17,10 @@ func (h *Headscale) generateMagicDNSRootDomains() (*[]dnsname.FQDN, error) { ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") fqdns := []dnsname.FQDN{base, ipv6base} + // TODO(juanfont): This only works for the 100.64.0.0/10 range. for i := 64; i <= 127; i++ { fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.100.in-addr.arpa.", i)) if err != nil { - // TODO: propagate error continue } fqdns = append(fqdns, fqdn) From fc5153af3e92ff029a964b10af8f24ec0bb9efd4 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 9 Oct 2021 12:22:13 +0200 Subject: [PATCH 25/29] Generate MagicDNS search domains for any tailnet range --- app.go | 4 ++-- dns.go | 27 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 31e306dc..2afc75b6 100644 --- a/app.go +++ b/app.go @@ -12,7 +12,7 @@ import ( "github.com/rs/zerolog/log" "github.com/gin-gonic/gin" - "github.com/zsais/go-gin-prometheus" + ginprometheus "github.com/zsais/go-gin-prometheus" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" "gorm.io/gorm" @@ -111,7 +111,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS - magicDNSDomains, err := h.generateMagicDNSRootDomains() + magicDNSDomains, err := generateMagicDNSRootDomains(h.cfg.IPPrefix, h.cfg.BaseDomain) if err != nil { return nil, err } diff --git a/dns.go b/dns.go index 74a85aee..c53849c9 100644 --- a/dns.go +++ b/dns.go @@ -2,12 +2,14 @@ package headscale import ( "fmt" + "strings" + "inet.af/netaddr" "tailscale.com/util/dnsname" ) -func (h *Headscale) generateMagicDNSRootDomains() (*[]dnsname.FQDN, error) { - base, err := dnsname.ToFQDN(h.cfg.BaseDomain) +func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (*[]dnsname.FQDN, error) { + base, err := dnsname.ToFQDN(baseDomain) if err != nil { return nil, err } @@ -17,14 +19,27 @@ func (h *Headscale) generateMagicDNSRootDomains() (*[]dnsname.FQDN, error) { ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") fqdns := []dnsname.FQDN{base, ipv6base} - for i := 64; i <= 127; i++ { - fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.100.in-addr.arpa.", i)) + netRange := ipPrefix.IPNet() + maskBits, _ := netRange.Mask.Size() + + lastByte := maskBits / 8 + unmaskedBits := 8 - maskBits%8 + min := uint(netRange.IP[lastByte]) + max := uint((min + 1<= 0; i-- { + rdnsSlice = append(rdnsSlice, fmt.Sprintf("%d", netRange.IP[i])) + } + rdnsSlice = append(rdnsSlice, "in-addr.arpa.") + rdnsBase := strings.Join(rdnsSlice, ".") + + for i := min; i <= max; i++ { + fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase)) if err != nil { - // TODO: propagate error continue } fqdns = append(fqdns, fqdn) } - return &fqdns, nil } From d4dc133e20158d7641ab8791ef19a77ca1765182 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 9 Oct 2021 12:22:21 +0200 Subject: [PATCH 26/29] Added unit tests --- dns_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 dns_test.go diff --git a/dns_test.go b/dns_test.go new file mode 100644 index 00000000..542674a9 --- /dev/null +++ b/dns_test.go @@ -0,0 +1,63 @@ +package headscale + +import ( + "gopkg.in/check.v1" + "inet.af/netaddr" +) + +func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { + prefix := netaddr.MustParseIPPrefix("100.64.0.0/10") + domains, err := generateMagicDNSRootDomains(prefix, "headscale.net") + c.Assert(err, check.IsNil) + + found := false + for _, domain := range *domains { + if domain == "64.100.in-addr.arpa." { + found = true + break + } + } + c.Assert(found, check.Equals, true) + + found = false + for _, domain := range *domains { + if domain == "100.100.in-addr.arpa." { + found = true + break + } + } + c.Assert(found, check.Equals, true) + + found = false + for _, domain := range *domains { + if domain == "127.100.in-addr.arpa." { + found = true + break + } + } + c.Assert(found, check.Equals, true) +} + +func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { + prefix := netaddr.MustParseIPPrefix("172.16.0.0/16") + domains, err := generateMagicDNSRootDomains(prefix, "headscale.net") + c.Assert(err, check.IsNil) + + found := false + for _, domain := range *domains { + if domain == "0.16.172.in-addr.arpa." { + found = true + break + } + } + c.Assert(found, check.Equals, true) + + found = false + for _, domain := range *domains { + if domain == "255.16.172.in-addr.arpa." { + found = true + break + } + } + c.Assert(found, check.Equals, true) +} From 68847984047b2b4bddb53e0eefc70cb878670f25 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 10 Oct 2021 00:40:25 +0200 Subject: [PATCH 27/29] Added some comments --- dns.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dns.go b/dns.go index d8f587f5..db09337d 100644 --- a/dns.go +++ b/dns.go @@ -8,6 +8,9 @@ import ( "tailscale.com/util/dnsname" ) +// generateMagicDNSRootDomains generates a list of DNS entries to be included in the +// routing for DNS in the MapResponse struct. This list of DNS instructs the OS +// on what domains the Tailscale embedded DNS server should be used for. func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (*[]dnsname.FQDN, error) { base, err := dnsname.ToFQDN(baseDomain) if err != nil { @@ -19,14 +22,22 @@ func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) ( ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") fqdns := []dnsname.FQDN{base, ipv6base} + // Conversion to the std lib net.IPnet, a bit easier to operate netRange := ipPrefix.IPNet() maskBits, _ := netRange.Mask.Size() + // lastByte is the last IP byte covered by the mask lastByte := maskBits / 8 + + // unmaskedBits is the number of bits not under the mask in the byte lastByte unmaskedBits := 8 - maskBits%8 + + // min is the value in the lastByte byte of the IP + // max is basically 2^unmaskedBits - i.e., the value when all the unmaskedBits are set to 1 min := uint(netRange.IP[lastByte]) max := uint((min + 1<= 0; i-- { rdnsSlice = append(rdnsSlice, fmt.Sprintf("%d", netRange.IP[i])) From d70c3d61893d823410374e0197c601079f48d3cb Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 10 Oct 2021 12:34:55 +0200 Subject: [PATCH 28/29] Added more comments, plus renamed vars with better names --- dns.go | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/dns.go b/dns.go index db09337d..9826a414 100644 --- a/dns.go +++ b/dns.go @@ -8,9 +8,26 @@ import ( "tailscale.com/util/dnsname" ) -// generateMagicDNSRootDomains generates a list of DNS entries to be included in the -// routing for DNS in the MapResponse struct. This list of DNS instructs the OS -// on what domains the Tailscale embedded DNS server should be used for. +// 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. +// +// Tailscale.com includes in the list: +// - the `BaseDomain` of the user +// - the reverse DNS entry for IPv6 (0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa., see below more on IPv6) +// - the reverse DNS entries for the IPv4 subnets covered by the user's `IPPrefix`. +// In the public SaaS this is [64-127].100.in-addr.arpa. +// +// The main purpose of this function is then generating the list of IPv4 entries. For the 100.64.0.0/10, this +// is clear, and could be hardcoded. But we are allowing any range as `IPPrefix`, so we need to find out the +// subnets when we have 172.16.0.0/16 (i.e., [0-255].16.172.in-addr.arpa.), or any other subnet. +// +// How IN-ADDR.ARPA domains work is defined in RFC1035 (section 3.5). Tailscale.com seems to adhere to this, +// and do not make use of RFC2317 ("Classless IN-ADDR.ARPA delegation") - hence generating the entries for the next +// class block only. + +// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). +// This allows us to then calculate the subnets included in the subsequent class block and generate the entries. func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (*[]dnsname.FQDN, error) { base, err := dnsname.ToFQDN(baseDomain) if err != nil { @@ -26,20 +43,20 @@ func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) ( netRange := ipPrefix.IPNet() maskBits, _ := netRange.Mask.Size() - // lastByte is the last IP byte covered by the mask - lastByte := maskBits / 8 + // lastOctet is the last IP byte covered by the mask + lastOctet := maskBits / 8 - // unmaskedBits is the number of bits not under the mask in the byte lastByte - unmaskedBits := 8 - maskBits%8 + // wildcardBits is the number of bits not under the mask in the lastOctet + wildcardBits := 8 - maskBits%8 - // min is the value in the lastByte byte of the IP - // max is basically 2^unmaskedBits - i.e., the value when all the unmaskedBits are set to 1 - min := uint(netRange.IP[lastByte]) - max := uint((min + 1<= 0; i-- { + for i := lastOctet - 1; i >= 0; i-- { rdnsSlice = append(rdnsSlice, fmt.Sprintf("%d", netRange.IP[i])) } rdnsSlice = append(rdnsSlice, "in-addr.arpa.") From 5ce1526a06e21720f22998e7dccd23a2cdb26794 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 10 Oct 2021 12:43:41 +0200 Subject: [PATCH 29/29] Do not return a pointer --- app.go | 2 +- dns.go | 4 ++-- dns_test.go | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 0442fb19..864ae16a 100644 --- a/app.go +++ b/app.go @@ -114,7 +114,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) { return nil, err } h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) - for _, d := range *magicDNSDomains { + for _, d := range magicDNSDomains { h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil } } diff --git a/dns.go b/dns.go index 9826a414..353e10be 100644 --- a/dns.go +++ b/dns.go @@ -28,7 +28,7 @@ import ( // From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). // This allows us to then calculate the subnets included in the subsequent class block and generate the entries. -func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (*[]dnsname.FQDN, error) { +func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) ([]dnsname.FQDN, error) { base, err := dnsname.ToFQDN(baseDomain) if err != nil { return nil, err @@ -69,5 +69,5 @@ func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) ( } fqdns = append(fqdns, fqdn) } - return &fqdns, nil + return fqdns, nil } diff --git a/dns_test.go b/dns_test.go index 542674a9..87813203 100644 --- a/dns_test.go +++ b/dns_test.go @@ -11,7 +11,7 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { c.Assert(err, check.IsNil) found := false - for _, domain := range *domains { + for _, domain := range domains { if domain == "64.100.in-addr.arpa." { found = true break @@ -20,7 +20,7 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { c.Assert(found, check.Equals, true) found = false - for _, domain := range *domains { + for _, domain := range domains { if domain == "100.100.in-addr.arpa." { found = true break @@ -29,7 +29,7 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { c.Assert(found, check.Equals, true) found = false - for _, domain := range *domains { + for _, domain := range domains { if domain == "127.100.in-addr.arpa." { found = true break @@ -44,7 +44,7 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { c.Assert(err, check.IsNil) found := false - for _, domain := range *domains { + for _, domain := range domains { if domain == "0.16.172.in-addr.arpa." { found = true break @@ -53,7 +53,7 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { c.Assert(found, check.Equals, true) found = false - for _, domain := range *domains { + for _, domain := range domains { if domain == "255.16.172.in-addr.arpa." { found = true break