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/48] 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 7ada669..f5c2a6d 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/48] 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 aba7206..5e5c539 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 b22e5ac..a357bc8 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 0000000..ca151bf --- /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/48] 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 58bf589..bddea94 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/48] 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 e2a5618..c7fbf8a 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 dc398eb..9e29640 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 f5c2a6d..bff7693 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 f62ca1d..e80ff46 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 1d4939c..d963d35 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 60bfa9e..40b3e28 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/48] Send hostname in toNode --- machine.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/machine.go b/machine.go index d963d35..fa643c5 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/48] 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 9e29640..c8c799b 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 0000000..91afe51 --- /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/48] Send search domains --- api.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api.go b/api.go index c7fbf8a..be087eb 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/48] 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 be087eb..84754e4 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 bff7693..53f9605 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/48] Tidy gomod --- app.go | 1 + go.mod | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index c8c799b..57fa47b 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 390fac9..4e910d1 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/48] Fixed issue in tests --- machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine.go b/machine.go index fa643c5..3b33e82 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/48] Fixed go mod --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 4912508..3a49c0f 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/48] Fixing nil issue --- api.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/api.go b/api.go index 84754e4..2ac6f29 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/48] 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 57fa47b..c8c799b 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 e80ff46..00cff38 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 8a6fd96..8868d1b 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/48] 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 53f9605..8f2fbdf 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/48] Updated readme --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5f691a6..326ae9e 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/48] 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 91afe51..74a85ae 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/48] 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 9aa6352..f879f91 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/48] Improved doc --- docs/DNS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DNS.md b/docs/DNS.md index ca151bf..85bf9f4 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/48] WIP preparation for merge --- machine.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/machine.go b/machine.go index ca0e5fa..5b045e3 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/48] 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 d090a2a..e360187 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 7a9df2e..0ea97d9 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/48] Go mod updates --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 7042831..704bba4 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/48] Preload namespace --- machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine.go b/machine.go index 0ea97d9..688dc78 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/48] 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 e360187..dd762a0 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 74a85ae..9cd747f 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/48] Added note on TODO --- dns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dns.go b/dns.go index 9cd747f..68df460 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/48] 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 31e306d..2afc75b 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 74a85ae..c53849c 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/48] 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 0000000..542674a --- /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/48] Added some comments --- dns.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dns.go b/dns.go index d8f587f..db09337 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/48] 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 db09337..9826a41 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/48] 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 0442fb1..864ae16 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 9826a41..353e10b 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 542674a..8781320 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 From 4c4c95198be19ce55b43558d5bd08b3452d73c9f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 10 Oct 2021 12:00:45 +0100 Subject: [PATCH 30/48] Remove gitter for discord --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f691a6..282a315 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# Headscale +# headscale -[![Join the chat at https://gitter.im/headscale-dev/community](https://badges.gitter.im/headscale-dev/community.svg)](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg) +![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg) An open source, self-hosted implementation of the Tailscale coordination server. +Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat. + ## Overview Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/). From 48f5a9a18c1ffc33a9be31d8f26b384c267f336f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 10 Oct 2021 23:55:03 +0200 Subject: [PATCH 31/48] Fix error 500 when deleting shared node (fixes #133) --- machine.go | 11 +++++++++++ sharing.go | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/machine.go b/machine.go index ecea378..2fa4d9e 100644 --- a/machine.go +++ b/machine.go @@ -179,6 +179,11 @@ func (h *Headscale) UpdateMachine(m *Machine) error { // DeleteMachine softs deletes a Machine from the database func (h *Headscale) DeleteMachine(m *Machine) error { + err := h.RemoveSharedMachineFromAllNamespaces(m) + if err != nil && err != errorMachineNotShared { + return err + } + m.Registered = false namespaceID := m.NamespaceID h.db.Save(&m) // we mark it as unregistered, just in case @@ -191,10 +196,16 @@ func (h *Headscale) DeleteMachine(m *Machine) error { // HardDeleteMachine hard deletes a Machine from the database func (h *Headscale) HardDeleteMachine(m *Machine) error { + err := h.RemoveSharedMachineFromAllNamespaces(m) + if err != nil && err != errorMachineNotShared { + return err + } + namespaceID := m.NamespaceID if err := h.db.Unscoped().Delete(&m).Error; err != nil { return err } + return h.RequestMapUpdates(namespaceID) } diff --git a/sharing.go b/sharing.go index 93c299c..83ce526 100644 --- a/sharing.go +++ b/sharing.go @@ -4,6 +4,7 @@ import "gorm.io/gorm" const errorSameNamespace = Error("Destination namespace same as origin") const errorMachineAlreadyShared = Error("Node already shared to this namespace") +const errorMachineNotShared = Error("Machine not shared to this namespace") // SharedMachine is a join table to support sharing nodes between namespaces type SharedMachine struct { @@ -35,3 +36,13 @@ func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error return nil } + +// RemoveSharedMachineFromAllNamespaces removes a machine as a shared node from all namespaces +func (h *Headscale) RemoveSharedMachineFromAllNamespaces(m *Machine) error { + sharedMachine := SharedMachine{} + if result := h.db.Where("machine_id = ?", m.ID).Unscoped().Delete(&sharedMachine); result.Error != nil { + return result.Error + } + + return nil +} From 7f2027d7f23039c0ce8aade9f1951dc283091601 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 10 Oct 2021 23:55:18 +0200 Subject: [PATCH 32/48] Added unit tests --- sharing_test.go | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/sharing_test.go b/sharing_test.go index baa90d0..a39160b 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -364,3 +364,131 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { c.Assert(err, check.IsNil) c.Assert(len(pAlone), check.Equals, 0) // node 3 is alone } + +func (s *Suite) TestDeleteSharedMachine(c *check.C) { + n1, err := h.CreateNamespace("shared1") + c.Assert(err, check.IsNil) + + n2, err := h.CreateNamespace("shared2") + c.Assert(err, check.IsNil) + + n3, err := h.CreateNamespace("shared3") + c.Assert(err, check.IsNil) + + pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1") + c.Assert(err, check.NotNil) + + m1 := &Machine{ + ID: 0, + MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", + NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", + DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", + Name: "test_get_shared_nodes_1", + NamespaceID: n1.ID, + Registered: true, + RegisterMethod: "authKey", + IPAddress: "100.64.0.1", + AuthKeyID: uint(pak1n1.ID), + } + h.db.Save(m1) + + _, err = h.GetMachine(n1.Name, m1.Name) + c.Assert(err, check.IsNil) + + m2 := &Machine{ + ID: 1, + MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + Name: "test_get_shared_nodes_2", + NamespaceID: n2.ID, + Registered: true, + RegisterMethod: "authKey", + IPAddress: "100.64.0.2", + AuthKeyID: uint(pak2n2.ID), + } + h.db.Save(m2) + + _, err = h.GetMachine(n2.Name, m2.Name) + c.Assert(err, check.IsNil) + + m3 := &Machine{ + ID: 2, + MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + Name: "test_get_shared_nodes_3", + NamespaceID: n3.ID, + Registered: true, + RegisterMethod: "authKey", + IPAddress: "100.64.0.3", + AuthKeyID: uint(pak3n3.ID), + } + h.db.Save(m3) + + _, err = h.GetMachine(n3.Name, m3.Name) + c.Assert(err, check.IsNil) + + m4 := &Machine{ + ID: 3, + MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", + Name: "test_get_shared_nodes_4", + NamespaceID: n1.ID, + Registered: true, + RegisterMethod: "authKey", + IPAddress: "100.64.0.4", + AuthKeyID: uint(pak4n1.ID), + } + h.db.Save(m4) + + _, err = h.GetMachine(n1.Name, m4.Name) + c.Assert(err, check.IsNil) + + p1s, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(p1s), check.Equals, 1) // nodes 1 and 4 + c.Assert(p1s[0].Name, check.Equals, "test_get_shared_nodes_4") + + err = h.AddSharedMachineToNamespace(m2, n1) + c.Assert(err, check.IsNil) + + p1sAfter, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(p1sAfter), check.Equals, 2) // nodes 1, 2, 4 + c.Assert(p1sAfter[0].Name, check.Equals, "test_get_shared_nodes_2") + c.Assert(p1sAfter[1].Name, check.Equals, "test_get_shared_nodes_4") + + node1shared, err := h.getShared(m1) + c.Assert(err, check.IsNil) + c.Assert(len(node1shared), check.Equals, 1) // nodes 1, 2, 4 + c.Assert(node1shared[0].Name, check.Equals, "test_get_shared_nodes_2") + + pAlone, err := h.getPeers(m3) + c.Assert(err, check.IsNil) + c.Assert(len(pAlone), check.Equals, 0) // node 3 is alone + + sharedMachines, err := h.ListSharedMachinesInNamespace(n1.Name) + c.Assert(err, check.IsNil) + c.Assert(len(*sharedMachines), check.Equals, 1) + + err = h.DeleteMachine(m2) + c.Assert(err, check.IsNil) + + sharedMachines, err = h.ListSharedMachinesInNamespace(n1.Name) + c.Assert(err, check.IsNil) + c.Assert(len(*sharedMachines), check.Equals, 0) +} From fa8cd96108d4e71e2c499469046f5fb22a3cdda2 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 12 Oct 2021 17:20:14 +0200 Subject: [PATCH 33/48] Get peers from namespaces where shared nodes are shared to This is rather shameful. Shared nodes should have never worked without this. --- machine.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/machine.go b/machine.go index 2fa4d9e..f97169b 100644 --- a/machine.go +++ b/machine.go @@ -78,13 +78,13 @@ func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) { return machines, nil } +// getShared fetches machines that are shared to the `Namespace` of the machine we are getting peers for func (h *Headscale) getShared(m *Machine) (Machines, error) { log.Trace(). Str("func", "getShared"). Str("machine", m.Name). Msg("Finding shared peers") - // We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for sharedMachines := []SharedMachine{} if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", m.NamespaceID).Find(&sharedMachines).Error; err != nil { @@ -105,6 +105,37 @@ func (h *Headscale) getShared(m *Machine) (Machines, error) { return peers, nil } +// getSharedTo fetches the machines of the namespaces this machine is shared in +func (h *Headscale) getSharedTo(m *Machine) (Machines, error) { + log.Trace(). + Str("func", "getSharedTo"). + Str("machine", m.Name). + Msg("Finding peers in namespaces this machine is shared with") + + sharedMachines := []SharedMachine{} + if err := h.db.Preload("Namespace").Preload("Machine").Where("machine_id = ?", + m.ID).Find(&sharedMachines).Error; err != nil { + return Machines{}, err + } + + peers := make(Machines, 0) + for _, sharedMachine := range sharedMachines { + namespaceMachines, err := h.ListMachinesInNamespace(sharedMachine.Namespace.Name) + if err != nil { + return Machines{}, err + } + peers = append(peers, *namespaceMachines...) + } + + sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) + + log.Trace(). + Str("func", "getSharedTo"). + Str("machine", m.Name). + Msgf("Found peers we are shared with: %s", peers.String()) + return peers, nil +} + func (h *Headscale) getPeers(m *Machine) (Machines, error) { direct, err := h.getDirectPeers(m) if err != nil { @@ -118,13 +149,24 @@ func (h *Headscale) getPeers(m *Machine) (Machines, error) { shared, err := h.getShared(m) if err != nil { log.Error(). - Str("func", "getDirectPeers"). + Str("func", "getShared"). + Err(err). + Msg("Cannot fetch peers") + return Machines{}, err + } + + sharedTo, err := h.getSharedTo(m) + if err != nil { + log.Error(). + Str("func", "sharedTo"). Err(err). Msg("Cannot fetch peers") return Machines{}, err } peers := append(direct, shared...) + peers = append(peers, sharedTo...) + sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) log.Trace(). From 6924b7bf4c4b6b24d697d38a0f35ed244979c12e Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Tue, 12 Oct 2021 23:48:08 +0200 Subject: [PATCH 34/48] Output json when deleting node (fixes #152) --- cmd/headscale/cli/nodes.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 5f30dc1..c01bce9 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -129,6 +129,7 @@ var deleteNodeCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { + output, _ := cmd.Flags().GetString("output") h, err := getHeadscaleApp() if err != nil { log.Fatalf("Error initializing: %s", err) @@ -153,11 +154,19 @@ var deleteNodeCmd = &cobra.Command{ if confirm { err = h.DeleteMachine(m) + if strings.HasPrefix(output, "json") { + JsonOutput(map[string]string{"Result": "Node deleted"}, err, output) + return + } if err != nil { log.Fatalf("Error deleting node: %s", err) } fmt.Printf("Node deleted\n") } else { + if strings.HasPrefix(output, "json") { + JsonOutput(map[string]string{"Result": "Node not deleted"}, err, output) + return + } fmt.Printf("Node not deleted\n") } }, From 27947c67462ad14519df4800e7817d2cb9f84a01 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Wed, 13 Oct 2021 00:18:55 +0200 Subject: [PATCH 35/48] This commit disables the version checker when JSON output (#153) --- cmd/headscale/cli/utils.go | 9 +++++++++ cmd/headscale/headscale.go | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index f879f91..95555e9 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -262,3 +262,12 @@ func JsonOutput(result interface{}, errResult error, outputFormat string) { } fmt.Println(string(j)) } + +func HasJsonOutputFlag() bool { + for _, arg := range os.Args { + if arg == "json" || arg == "json-line" { + return true + } + } + return false +} diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index f815001..6b1a843 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -62,7 +62,8 @@ func main() { zerolog.SetGlobalLevel(zerolog.DebugLevel) } - if !viper.GetBool("disable_check_updates") { + jsonOutput := cli.HasJsonOutputFlag() + if !viper.GetBool("disable_check_updates") && !jsonOutput { if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && cli.Version != "dev" { githubTag := &latest.GithubTag{ Owner: "juanfont", From 30788e1a70969f541bfa699d93f675b24775a5bb Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Wed, 13 Oct 2021 18:13:26 +0200 Subject: [PATCH 36/48] Add AlreadyUsed field to Auth Keys (fixes #157 and #158) --- api.go | 3 +++ cmd/headscale/cli/preauthkeys.go | 3 ++- preauth_keys.go | 4 ++-- preauth_keys_test.go | 13 +++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/api.go b/api.go index 13955d3..f68852d 100644 --- a/api.go +++ b/api.go @@ -395,6 +395,9 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, m.RegisterMethod = "authKey" db.Save(&m) + pak.AlreadyUsed = true + db.Save(&pak) + resp.MachineAuthorized = true resp.User = *pak.Namespace.toUser() respBody, err := encode(resp, &idKey, h.privateKey) diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 1340267..28bbb7e 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -57,7 +57,7 @@ var listPreAuthKeys = &cobra.Command{ return } - d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Expiration", "Created"}} + d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "AlreadyUsed", "Expiration", "Created"}} for _, k := range *keys { expiration := "-" if k.Expiration != nil { @@ -76,6 +76,7 @@ var listPreAuthKeys = &cobra.Command{ k.Key, reusable, strconv.FormatBool(k.Ephemeral), + fmt.Sprintf("%v", k.AlreadyUsed), expiration, k.CreatedAt.Format("2006-01-02 15:04:05"), }) diff --git a/preauth_keys.go b/preauth_keys.go index cc849fc..babc2ff 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -21,6 +21,7 @@ type PreAuthKey struct { Namespace Namespace Reusable bool Ephemeral bool `gorm:"default:false"` + AlreadyUsed bool `gorm:"default:false"` CreatedAt *time.Time Expiration *time.Time @@ -110,11 +111,10 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { return nil, err } - if len(machines) != 0 { + if len(machines) != 0 || pak.AlreadyUsed { return nil, errorAuthKeyNotReusableAlreadyUsed } - // missing here validation on current usage return &pak, nil } diff --git a/preauth_keys_test.go b/preauth_keys_test.go index 37f2e4d..80d24e3 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -180,3 +180,16 @@ func (*Suite) TestExpirePreauthKey(c *check.C) { c.Assert(err, check.Equals, errorAuthKeyExpired) c.Assert(p, check.IsNil) } + +func (*Suite) TestNotReusableMarkedAsAlreadyUsed(c *check.C) { + n, err := h.CreateNamespace("test6") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + pak.AlreadyUsed = true + h.db.Save(&pak) + + _, err = h.checkKeyValidity(pak.Key) + c.Assert(err, check.Equals, errorAuthKeyNotReusableAlreadyUsed) +} From ebfb8c8c5ee512fec956b325aeca03d93b4619dd Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Wed, 13 Oct 2021 20:48:50 +0200 Subject: [PATCH 37/48] Fix tests, as IDs of Machines where wrongly starting in 0 --- sharing_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sharing_test.go b/sharing_test.go index a39160b..45ba40d 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -274,7 +274,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { c.Assert(err, check.NotNil) m1 := &Machine{ - ID: 0, + ID: 1, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -291,7 +291,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { c.Assert(err, check.IsNil) m2 := &Machine{ - ID: 1, + ID: 2, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -308,7 +308,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { c.Assert(err, check.IsNil) m3 := &Machine{ - ID: 2, + ID: 3, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -325,7 +325,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { c.Assert(err, check.IsNil) m4 := &Machine{ - ID: 3, + ID: 4, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -391,7 +391,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) { c.Assert(err, check.NotNil) m1 := &Machine{ - ID: 0, + ID: 1, MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", @@ -408,7 +408,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) { c.Assert(err, check.IsNil) m2 := &Machine{ - ID: 1, + ID: 2, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -425,7 +425,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) { c.Assert(err, check.IsNil) m3 := &Machine{ - ID: 2, + ID: 3, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", @@ -442,7 +442,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) { c.Assert(err, check.IsNil) m4 := &Machine{ - ID: 3, + ID: 4, MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", From 6aa763a1ae3e9063c901385d5a405b50ce79c9e5 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Wed, 13 Oct 2021 20:56:32 +0200 Subject: [PATCH 38/48] Expanded unit tests to better cover sharing nodes --- sharing_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/sharing_test.go b/sharing_test.go index 45ba40d..d8cd802 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -343,7 +343,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { p1s, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(p1s), check.Equals, 1) // nodes 1 and 4 + c.Assert(len(p1s), check.Equals, 1) // node1 can see node4 c.Assert(p1s[0].Name, check.Equals, "test_get_shared_nodes_4") err = h.AddSharedMachineToNamespace(m2, n1) @@ -351,18 +351,24 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { p1sAfter, err := h.getPeers(m1) c.Assert(err, check.IsNil) - c.Assert(len(p1sAfter), check.Equals, 2) // nodes 1, 2, 4 + c.Assert(len(p1sAfter), check.Equals, 2) // node1 can see node2 (shared) and node4 (same namespace) c.Assert(p1sAfter[0].Name, check.Equals, "test_get_shared_nodes_2") c.Assert(p1sAfter[1].Name, check.Equals, "test_get_shared_nodes_4") node1shared, err := h.getShared(m1) c.Assert(err, check.IsNil) - c.Assert(len(node1shared), check.Equals, 1) // nodes 1, 2, 4 + c.Assert(len(node1shared), check.Equals, 1) // node1 can see node2 as shared c.Assert(node1shared[0].Name, check.Equals, "test_get_shared_nodes_2") pAlone, err := h.getPeers(m3) c.Assert(err, check.IsNil) - c.Assert(len(pAlone), check.Equals, 0) // node 3 is alone + c.Assert(len(pAlone), check.Equals, 0) // node3 is alone + + pSharedTo, err := h.getPeers(m2) + c.Assert(err, check.IsNil) + c.Assert(len(pSharedTo), check.Equals, 2) // node2 should see node1 (sharedTo) and node4 (sharedTo), as is shared in namespace1 + c.Assert(pSharedTo[0].Name, check.Equals, "test_get_shared_nodes_1") + c.Assert(pSharedTo[1].Name, check.Equals, "test_get_shared_nodes_4") } func (s *Suite) TestDeleteSharedMachine(c *check.C) { From 93517aa6f897c37902a5cb3c24c16296cc09d0be Mon Sep 17 00:00:00 2001 From: Juan Font Date: Wed, 13 Oct 2021 22:51:55 +0200 Subject: [PATCH 39/48] Apply suggestions from code review Renamed AlreadyUsed to Used Co-authored-by: Kristoffer Dalby --- api.go | 2 +- cmd/headscale/cli/preauthkeys.go | 4 ++-- preauth_keys.go | 4 ++-- preauth_keys_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api.go b/api.go index f68852d..0aad5ee 100644 --- a/api.go +++ b/api.go @@ -395,7 +395,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, m.RegisterMethod = "authKey" db.Save(&m) - pak.AlreadyUsed = true + pak.Used = true db.Save(&pak) resp.MachineAuthorized = true diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 28bbb7e..cb75b28 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -57,7 +57,7 @@ var listPreAuthKeys = &cobra.Command{ return } - d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "AlreadyUsed", "Expiration", "Created"}} + d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"}} for _, k := range *keys { expiration := "-" if k.Expiration != nil { @@ -76,7 +76,7 @@ var listPreAuthKeys = &cobra.Command{ k.Key, reusable, strconv.FormatBool(k.Ephemeral), - fmt.Sprintf("%v", k.AlreadyUsed), + fmt.Sprintf("%v", k.Used), expiration, k.CreatedAt.Format("2006-01-02 15:04:05"), }) diff --git a/preauth_keys.go b/preauth_keys.go index babc2ff..05af926 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -21,7 +21,7 @@ type PreAuthKey struct { Namespace Namespace Reusable bool Ephemeral bool `gorm:"default:false"` - AlreadyUsed bool `gorm:"default:false"` + Used bool `gorm:"default:false"` CreatedAt *time.Time Expiration *time.Time @@ -111,7 +111,7 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { return nil, err } - if len(machines) != 0 || pak.AlreadyUsed { + if len(machines) != 0 || pak.Used { return nil, errorAuthKeyNotReusableAlreadyUsed } diff --git a/preauth_keys_test.go b/preauth_keys_test.go index 80d24e3..364733a 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -181,13 +181,13 @@ func (*Suite) TestExpirePreauthKey(c *check.C) { c.Assert(p, check.IsNil) } -func (*Suite) TestNotReusableMarkedAsAlreadyUsed(c *check.C) { +func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) { n, err := h.CreateNamespace("test6") c.Assert(err, check.IsNil) pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) c.Assert(err, check.IsNil) - pak.AlreadyUsed = true + pak.Used = true h.db.Save(&pak) _, err = h.checkKeyValidity(pak.Key) From 9a6ac6e3e6dc610293670c68429c8361de195978 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Wed, 13 Oct 2021 23:23:07 +0200 Subject: [PATCH 40/48] Reword errSingleUseAuthKeyHasBeenUsed --- preauth_keys.go | 4 ++-- preauth_keys_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/preauth_keys.go b/preauth_keys.go index cc849fc..ed83253 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -11,7 +11,7 @@ import ( const errorAuthKeyNotFound = Error("AuthKey not found") const errorAuthKeyExpired = Error("AuthKey expired") -const errorAuthKeyNotReusableAlreadyUsed = Error("AuthKey not reusable already used") +const errSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used") // PreAuthKey describes a pre-authorization key usable in a particular namespace type PreAuthKey struct { @@ -111,7 +111,7 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { } if len(machines) != 0 { - return nil, errorAuthKeyNotReusableAlreadyUsed + return nil, errSingleUseAuthKeyHasBeenUsed } // missing here validation on current usage diff --git a/preauth_keys_test.go b/preauth_keys_test.go index 37f2e4d..53616bb 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -87,7 +87,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) { h.db.Save(&m) p, err := h.checkKeyValidity(pak.Key) - c.Assert(err, check.Equals, errorAuthKeyNotReusableAlreadyUsed) + c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed) c.Assert(p, check.IsNil) } From 3e1e07e8c153517155beb727b5be4cec2c1e9903 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 14 Oct 2021 22:37:44 +0200 Subject: [PATCH 41/48] Fixed integration tests for shared nodes --- integration_test.go | 70 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/integration_test.go b/integration_test.go index 9bc51f9..8041036 100644 --- a/integration_test.go +++ b/integration_test.go @@ -58,11 +58,11 @@ func TestIntegrationTestSuite(t *testing.T) { s.namespaces = map[string]TestNamespace{ "main": { - count: 20, + count: 5, tailscales: make(map[string]dockertest.Resource), }, "shared": { - count: 5, + count: 2, tailscales: make(map[string]dockertest.Resource), }, } @@ -503,44 +503,42 @@ func (s *IntegrationTestSuite) TestSharedNodes() { for hostname := range shared.tailscales { assert.Contains(s.T(), result, hostname) } + time.Sleep(100 * time.Second) // Wait for the nodes to receive updates - // TODO(kradalby): Figure out why these connections are not set up - // // TODO: See if we can have a more deterministic wait here. - // time.Sleep(100 * time.Second) + mainIps, err := getIPs(main.tailscales) + assert.Nil(s.T(), err) - // mainIps, err := getIPs(main.tailscales) - // assert.Nil(s.T(), err) + sharedIps, err := getIPs(shared.tailscales) + assert.Nil(s.T(), err) - // sharedIps, err := getIPs(shared.tailscales) - // assert.Nil(s.T(), err) + for hostname, tailscale := range main.tailscales { + for peername, ip := range sharedIps { + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + // We currently cant ping ourselves, so skip that. + if peername != hostname { + // We are only interested in "direct ping" which means what we + // might need a couple of more attempts before reaching the node. + command := []string{ + "tailscale", "ping", + "--timeout=15s", + "--c=20", + "--until-direct=true", + ip.String(), + } - // for hostname, tailscale := range main.tailscales { - // for peername, ip := range sharedIps { - // s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - // // We currently cant ping ourselves, so skip that. - // if peername != hostname { - // // We are only interested in "direct ping" which means what we - // // might need a couple of more attempts before reaching the node. - // command := []string{ - // "tailscale", "ping", - // "--timeout=1s", - // "--c=20", - // "--until-direct=true", - // ip.String(), - // } - - // fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, mainIps[hostname], peername, ip) - // result, err := executeCommand( - // &tailscale, - // command, - // ) - // assert.Nil(t, err) - // fmt.Printf("Result for %s: %s\n", hostname, result) - // assert.Contains(t, result, "pong") - // } - // }) - // } - // } + fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, mainIps[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 (s *IntegrationTestSuite) TestTailDrop() { From 91d135e06927a7fdfb8f1315c941d25df39cbe4f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 14 Oct 2021 23:54:07 +0200 Subject: [PATCH 42/48] Show JSON when error on expire key (fixes #162) --- cmd/headscale/cli/preauthkeys.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index cb75b28..caa9201 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -153,6 +153,10 @@ var expirePreAuthKeyCmd = &cobra.Command{ k, err := h.GetPreAuthKey(n, args[0]) if err != nil { + if strings.HasPrefix(o, "json") { + JsonOutput(k, err, o) + return + } log.Fatalf("Error getting the key: %s", err) } From 0d13e16fed9a57b100fe3262f3cc6f533950a431 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 14 Oct 2021 23:58:15 +0200 Subject: [PATCH 43/48] Improve help message to expire key (fixes #161) --- cmd/headscale/cli/preauthkeys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index cb75b28..421e7ef 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -131,7 +131,7 @@ var createPreAuthKeyCmd = &cobra.Command{ } var expirePreAuthKeyCmd = &cobra.Command{ - Use: "expire", + Use: "expire KEY", Short: "Expire a preauthkey", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { From 985c6e7cc90b8d896afac8e1a236756b8aa46011 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 15 Oct 2021 00:04:04 +0200 Subject: [PATCH 44/48] Preload AuthKey Namespace on list nodes (fixes #163) --- namespaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/namespaces.go b/namespaces.go index 2bf62bb..e109b9a 100644 --- a/namespaces.go +++ b/namespaces.go @@ -91,7 +91,7 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) { } machines := []Machine{} - if err := h.db.Preload("AuthKey").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { + if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { return nil, err } return &machines, nil From 5807562b56a584124ac5ebe8af63766c75188937 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Fri, 15 Oct 2021 17:00:04 +0200 Subject: [PATCH 45/48] Add arm64 binaries --- .goreleaser.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 7b1ea60..f735510 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -20,6 +20,7 @@ builds: - -mod=readonly ldflags: - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} + - id: linux-armhf main: ./cmd/headscale/headscale.go mod_timestamp: '{{ .CommitTimestamp }}' @@ -49,9 +50,16 @@ builds: - linux goarch: - amd64 - goarm: - - 6 - - 7 + main: ./cmd/headscale/headscale.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}} + + - id: linux-arm64 + goos: + - linux + goarch: + - arm64 main: ./cmd/headscale/headscale.go mod_timestamp: '{{ .CommitTimestamp }}' ldflags: @@ -63,6 +71,7 @@ archives: - darwin-amd64 - linux-armhf - linux-amd64 + - linux-arm64 name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format: binary From be36480a64770ff508aafe1f72a267fa2c8d3dc9 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 16 Oct 2021 11:06:33 +0200 Subject: [PATCH 46/48] Reverted back values in integration tests --- integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_test.go b/integration_test.go index 8041036..aed805e 100644 --- a/integration_test.go +++ b/integration_test.go @@ -58,11 +58,11 @@ func TestIntegrationTestSuite(t *testing.T) { s.namespaces = map[string]TestNamespace{ "main": { - count: 5, + count: 20, tailscales: make(map[string]dockertest.Resource), }, "shared": { - count: 2, + count: 5, tailscales: make(map[string]dockertest.Resource), }, } From d0daff180e82db621189d6916595535d2ebc06f1 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 16 Oct 2021 11:36:16 +0200 Subject: [PATCH 47/48] Added TODO in waiting --- integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration_test.go b/integration_test.go index aed805e..3c51215 100644 --- a/integration_test.go +++ b/integration_test.go @@ -503,6 +503,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() { for hostname := range shared.tailscales { assert.Contains(s.T(), result, hostname) } + + // TODO(juanfont): We have to find out why do we need to wait time.Sleep(100 * time.Second) // Wait for the nodes to receive updates mainIps, err := getIPs(main.tailscales) From be3a379d1018f4ebecd9cfd77bfcca4adc851b32 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sat, 16 Oct 2021 12:30:52 +0200 Subject: [PATCH 48/48] Added --force flag on node delete (fixes #164) --- cmd/headscale/cli/nodes.go | 17 ++++++++++------- cmd/headscale/cli/root.go | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index c01bce9..1246192 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -144,15 +144,18 @@ var deleteNodeCmd = &cobra.Command{ } confirm := false - prompt := &survey.Confirm{ - Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name), - } - err = survey.AskOne(prompt, &confirm) - if err != nil { - return + force, _ := cmd.Flags().GetBool("force") + if !force { + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name), + } + err = survey.AskOne(prompt, &confirm) + if err != nil { + return + } } - if confirm { + if confirm || force { err = h.DeleteMachine(m) if strings.HasPrefix(output, "json") { JsonOutput(map[string]string{"Result": "Node deleted"}, err, output) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 21857d8..794cd0d 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -9,6 +9,7 @@ import ( func init() { rootCmd.PersistentFlags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json' or 'json-line'") + rootCmd.PersistentFlags().Bool("force", false, "Disable prompts and forces the execution") } var rootCmd = &cobra.Command{