From 97f7c9009290566a9870580750bf495587906491 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 25 Jul 2021 17:59:48 +0200 Subject: [PATCH 001/125] Added communication between Serve and CLI using KV table (helps in #52) --- app.go | 17 ++++++++++ db.go | 2 ++ machine.go | 7 +++-- machine_test.go | 11 +++++++ namespaces.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index fa9b011b..139d2ed9 100644 --- a/app.go +++ b/app.go @@ -139,6 +139,20 @@ func (h *Headscale) expireEphemeralNodesWorker() { } } +// WatchForKVUpdates checks the KV DB table for requests to perform tailnet upgrades +// This is a way to communitate the CLI with the headscale server +func (h *Headscale) watchForKVUpdates(milliSeconds int64) { + ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) + for range ticker.C { + h.watchForKVUpdatesWorker() + } +} + +func (h *Headscale) watchForKVUpdatesWorker() { + h.checkForNamespacesPendingUpdates() + // more functions will come here in the future +} + // Serve launches a GIN server with the Headscale API func (h *Headscale) Serve() error { r := gin.Default() @@ -147,6 +161,9 @@ func (h *Headscale) Serve() error { r.POST("/machine/:id/map", h.PollNetMapHandler) r.POST("/machine/:id", h.RegistrationHandler) var err error + + go h.watchForKVUpdates(5000) + if h.cfg.TLSLetsEncryptHostname != "" { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { log.Println("WARNING: listening with TLS but ServerURL does not start with https://") diff --git a/db.go b/db.go index 6a057e16..06302523 100644 --- a/db.go +++ b/db.go @@ -79,6 +79,7 @@ func (h *Headscale) openDB() (*gorm.DB, error) { return db, nil } +// getValue returns the value for the given key in KV func (h *Headscale) getValue(key string) (string, error) { var row KV if result := h.db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) { @@ -87,6 +88,7 @@ func (h *Headscale) getValue(key string) (string, error) { return row.Value, nil } +// setValue sets value for the given key in KV func (h *Headscale) setValue(key string, value string) error { kv := KV{ Key: key, diff --git a/machine.go b/machine.go index 6f88e8d5..1895e464 100644 --- a/machine.go +++ b/machine.go @@ -200,19 +200,22 @@ func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { // DeleteMachine softs deletes a Machine from the database func (h *Headscale) DeleteMachine(m *Machine) error { m.Registered = false + namespaceID := m.NamespaceID h.db.Save(&m) // we mark it as unregistered, just in case if err := h.db.Delete(&m).Error; err != nil { return err } - return nil + + return h.RequestMapUpdates(namespaceID) } // HardDeleteMachine hard deletes a Machine from the database func (h *Headscale) HardDeleteMachine(m *Machine) error { + namespaceID := m.NamespaceID if err := h.db.Unscoped().Delete(&m).Error; err != nil { return err } - return nil + return h.RequestMapUpdates(namespaceID) } // GetHostInfo returns a Hostinfo struct for the machine diff --git a/machine_test.go b/machine_test.go index 1bd29a9d..d535be56 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1,6 +1,8 @@ package headscale import ( + "encoding/json" + "gopkg.in/check.v1" ) @@ -81,6 +83,15 @@ func (s *Suite) TestDeleteMachine(c *check.C) { h.db.Save(&m) err = h.DeleteMachine(&m) c.Assert(err, check.IsNil) + v, err := h.getValue("namespaces_pending_updates") + c.Assert(err, check.IsNil) + names := []string{} + err = json.Unmarshal([]byte(v), &names) + c.Assert(err, check.IsNil) + c.Assert(names, check.DeepEquals, []string{n.Name}) + h.checkForNamespacesPendingUpdates() + v, _ = h.getValue("namespaces_pending_updates") + c.Assert(v, check.Equals, "") _, err = h.GetMachine(n.Name, "testmachine") c.Assert(err, check.NotNil) } diff --git a/namespaces.go b/namespaces.go index 9897640c..c4e39e49 100644 --- a/namespaces.go +++ b/namespaces.go @@ -1,7 +1,9 @@ package headscale import ( + "encoding/json" "errors" + "fmt" "log" "time" @@ -103,6 +105,87 @@ func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error return nil } +// RequestMapUpdates signals the KV worker to update the maps for this namespace +func (h *Headscale) RequestMapUpdates(namespaceID uint) error { + namespace := Namespace{} + if err := h.db.First(&namespace, namespaceID).Error; err != nil { + return err + } + + v, err := h.getValue("namespaces_pending_updates") + if err != nil || v == "" { + err = h.setValue("namespaces_pending_updates", fmt.Sprintf(`["%s"]`, namespace.Name)) + if err != nil { + return err + } + return nil + } + names := []string{} + err = json.Unmarshal([]byte(v), &names) + if err != nil { + err = h.setValue("namespaces_pending_updates", fmt.Sprintf(`["%s"]`, namespace.Name)) + if err != nil { + return err + } + return nil + } + + names = append(names, namespace.Name) + data, err := json.Marshal(names) + if err != nil { + log.Printf("Could not marshal namespaces_pending_updates: %s", err) + return err + } + return h.setValue("namespaces_pending_updates", string(data)) +} + +func (h *Headscale) checkForNamespacesPendingUpdates() { + v, err := h.getValue("namespaces_pending_updates") + if err != nil { + return + } + if v == "" { + return + } + + names := []string{} + err = json.Unmarshal([]byte(v), &names) + if err != nil { + return + } + for _, name := range names { + machines, err := h.ListMachinesInNamespace(name) + if err != nil { + continue + } + for _, m := range *machines { + peers, _ := h.getPeers(m) + h.pollMu.Lock() + for _, p := range *peers { + pUp, ok := h.clientsPolling[uint64(p.ID)] + if ok { + log.Printf("[%s] Notifying peer %s (%s)", m.Name, p.Name, p.Addresses[0]) + pUp <- []byte{} + } else { + log.Printf("[%s] Peer %s does not appear to be polling", m.Name, p.Name) + } + } + h.pollMu.Unlock() + } + } + newV, err := h.getValue("namespaces_pending_updates") + if err != nil { + return + } + if v == newV { // only clear when no changes, so we notified everybody + err = h.setValue("namespaces_pending_updates", "") + if err != nil { + log.Printf("Could not save to KV: %s", err) + return + } + } +} + func (n *Namespace) toUser() *tailcfg.User { u := tailcfg.User{ ID: tailcfg.UserID(n.ID), From 461a893ee44349540bc2e4bb3e76154ff23cbc68 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 25 Jul 2021 20:47:51 +0200 Subject: [PATCH 002/125] Added log message when sending updates --- namespaces.go | 1 + 1 file changed, 1 insertion(+) diff --git a/namespaces.go b/namespaces.go index c4e39e49..840f8729 100644 --- a/namespaces.go +++ b/namespaces.go @@ -154,6 +154,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { return } for _, name := range names { + log.Printf("Sending updates to nodes in namespace %s", name) machines, err := h.ListMachinesInNamespace(name) if err != nil { continue From 309f868a2113c9cf1ba193f744ff135808829eb0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 20:06:26 +0100 Subject: [PATCH 003/125] Make IP prefix configurable This commit makes the IP prefix used to generate addresses configurable to users. This can be useful if you would like to use a smaller range or if your current setup is overlapping with the current range. The current range is left as a default --- app.go | 2 ++ app_test.go | 5 ++++- cmd/headscale/cli/utils.go | 4 ++++ utils.go | 9 +++++---- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index fa9b011b..e3978212 100644 --- a/app.go +++ b/app.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "golang.org/x/crypto/acme/autocert" "gorm.io/gorm" + "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/types/wgkey" ) @@ -24,6 +25,7 @@ type Config struct { PrivateKeyPath string DerpMap *tailcfg.DERPMap EphemeralNodeInactivityTimeout time.Duration + IPPrefix netaddr.IPPrefix DBtype string DBpath string diff --git a/app_test.go b/app_test.go index ad633334..ff3755e9 100644 --- a/app_test.go +++ b/app_test.go @@ -6,6 +6,7 @@ import ( "testing" "gopkg.in/check.v1" + "inet.af/netaddr" ) func Test(t *testing.T) { @@ -36,7 +37,9 @@ func (s *Suite) ResetDB(c *check.C) { if err != nil { c.Fatal(err) } - cfg := Config{} + cfg := Config{ + IPPrefix: netaddr.MustParseIPPrefix("127.0.0.1/32"), + } h = Headscale{ cfg: cfg, diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 5e47d157..1c259c74 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -14,6 +14,7 @@ import ( "github.com/juanfont/headscale" "github.com/spf13/viper" "gopkg.in/yaml.v2" + "inet.af/netaddr" "tailscale.com/tailcfg" ) @@ -36,6 +37,8 @@ func LoadConfig(path string) error { viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") + viper.SetDefault("ip_prefix", "100.64.0.0/10") + err := viper.ReadInConfig() if err != nil { return fmt.Errorf("Fatal error reading config file: %s \n", err) @@ -97,6 +100,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { Addr: viper.GetString("listen_addr"), PrivateKeyPath: absPath(viper.GetString("private_key_path")), DerpMap: derpMap, + IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"), diff --git a/utils.go b/utils.go index f21063b0..1da25084 100644 --- a/utils.go +++ b/utils.go @@ -19,6 +19,7 @@ import ( "golang.org/x/crypto/nacl/box" "gorm.io/gorm" + "inet.af/netaddr" "tailscale.com/types/wgkey" ) @@ -80,7 +81,7 @@ func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, err func (h *Headscale) getAvailableIP() (*net.IP, error) { i := 0 for { - ip, err := getRandomIP() + ip, err := getRandomIP(h.cfg.IPPrefix) if err != nil { return nil, err } @@ -93,12 +94,12 @@ func (h *Headscale) getAvailableIP() (*net.IP, error) { break } } - return nil, errors.New("Could not find an available IP address in 100.64.0.0/10") + return nil, errors.New(fmt.Sprintf("Could not find an available IP address in %s", h.cfg.IPPrefix.String())) } -func getRandomIP() (*net.IP, error) { +func getRandomIP(ipPrefix netaddr.IPPrefix) (*net.IP, error) { mathrand.Seed(time.Now().Unix()) - ipo, ipnet, err := net.ParseCIDR("100.64.0.0/10") + ipo, ipnet, err := net.ParseCIDR(ipPrefix.String()) if err == nil { ip := ipo.To4() // fmt.Println("In Randomize IPAddr: IP ", ip, " IPNET: ", ipnet) From b5841c8a8b088338db8cbda474a85243452c9433 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 21:57:45 +0100 Subject: [PATCH 004/125] Rework getAvailableIp This commit reworks getAvailableIp with a "simpler" version that will look for the first available IP address in our IP Prefix. There is a couple of ideas behind this: * Make the host IPs reasonably predictable and in within similar subnets, which should simplify ACLs for subnets * The code is not random, but deterministic so we can have tests * The code is a bit more understandable (no bit shift magic) --- app_test.go | 2 +- cli_test.go | 1 + utils.go | 107 +++++++++++++++++++++++++++++--------------------- utils_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 utils_test.go diff --git a/app_test.go b/app_test.go index ff3755e9..5e53f1cc 100644 --- a/app_test.go +++ b/app_test.go @@ -38,7 +38,7 @@ func (s *Suite) ResetDB(c *check.C) { c.Fatal(err) } cfg := Config{ - IPPrefix: netaddr.MustParseIPPrefix("127.0.0.1/32"), + IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"), } h = Headscale{ diff --git a/cli_test.go b/cli_test.go index 9616b4a2..528a115e 100644 --- a/cli_test.go +++ b/cli_test.go @@ -15,6 +15,7 @@ func (s *Suite) TestRegisterMachine(c *check.C) { DiscoKey: "faa", Name: "testmachine", NamespaceID: n.ID, + IPAddress: "10.0.0.1", } h.db.Save(&m) diff --git a/utils.go b/utils.go index 1da25084..404e3823 100644 --- a/utils.go +++ b/utils.go @@ -7,18 +7,11 @@ package headscale import ( "crypto/rand" - "encoding/binary" "encoding/json" - "errors" "fmt" "io" - "net" - "time" - - mathrand "math/rand" "golang.org/x/crypto/nacl/box" - "gorm.io/gorm" "inet.af/netaddr" "tailscale.com/types/wgkey" ) @@ -78,47 +71,73 @@ func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, err return msg, nil } -func (h *Headscale) getAvailableIP() (*net.IP, error) { - i := 0 +func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { + ipPrefix := h.cfg.IPPrefix + + usedIps, err := h.getUsedIPs() + if err != nil { + return nil, err + } + + // for _, ip := range usedIps { + // nextIP := ip.Next() + + // if !containsIPs(usedIps, nextIP) && ipPrefix.Contains(nextIP) { + // return &nextIP, nil + // } + // } + + // // If there are no IPs in use, we are starting fresh and + // // can issue IPs from the beginning of the prefix. + // ip := ipPrefix.IP() + // return &ip, nil + + // return nil, fmt.Errorf("failed to find any available IP in %s", ipPrefix) + + // Get the first IP in our prefix + ip := ipPrefix.IP() + for { - ip, err := getRandomIP(h.cfg.IPPrefix) + if !ipPrefix.Contains(ip) { + return nil, fmt.Errorf("could not find any suitable IP in %s", ipPrefix) + } + + if ip.IsZero() && + ip.IsLoopback() { + continue + } + + if !containsIPs(usedIps, ip) { + return &ip, nil + } + + ip = ip.Next() + } +} + +func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { + var addresses []string + h.db.Model(&Machine{}).Pluck("ip_address", &addresses) + + ips := make([]netaddr.IP, len(addresses)) + for index, addr := range addresses { + ip, err := netaddr.ParseIP(addr) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse ip from database, %w", err) } - m := Machine{} - if result := h.db.First(&m, "ip_address = ?", ip.String()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - return ip, nil - } - i++ - if i == 100 { // really random number - break - } - } - return nil, errors.New(fmt.Sprintf("Could not find an available IP address in %s", h.cfg.IPPrefix.String())) -} -func getRandomIP(ipPrefix netaddr.IPPrefix) (*net.IP, error) { - mathrand.Seed(time.Now().Unix()) - ipo, ipnet, err := net.ParseCIDR(ipPrefix.String()) - if err == nil { - ip := ipo.To4() - // fmt.Println("In Randomize IPAddr: IP ", ip, " IPNET: ", ipnet) - // fmt.Println("Final address is ", ip) - // fmt.Println("Broadcast address is ", ipb) - // fmt.Println("Network address is ", ipn) - r := mathrand.Uint32() - ipRaw := make([]byte, 4) - binary.LittleEndian.PutUint32(ipRaw, r) - // ipRaw[3] = 254 - // fmt.Println("ipRaw is ", ipRaw) - for i, v := range ipRaw { - // fmt.Println("IP Before: ", ip[i], " v is ", v, " Mask is: ", ipnet.Mask[i]) - ip[i] = ip[i] + (v &^ ipnet.Mask[i]) - // fmt.Println("IP After: ", ip[i]) - } - // fmt.Println("FINAL IP: ", ip.String()) - return &ip, nil + ips[index] = ip } - return nil, err + return ips, nil +} + +func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool { + for _, v := range ips { + if v == ip { + return true + } + } + + return false } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 00000000..471b8220 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,105 @@ +package headscale + +import ( + "gopkg.in/check.v1" + "inet.af/netaddr" +) + +func (s *Suite) TestGetAvailableIp(c *check.C) { + ip, err := h.getAvailableIP() + + c.Assert(err, check.IsNil) + + expected := netaddr.MustParseIP("10.27.0.0") + + c.Assert(ip.String(), check.Equals, expected.String()) +} + +func (s *Suite) TestGetUsedIps(c *check.C) { + ip, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + n, err := h.CreateNamespace("test_ip") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + IPAddress: ip.String(), + } + h.db.Save(&m) + + ips, err := h.getUsedIPs() + + c.Assert(err, check.IsNil) + + expected := netaddr.MustParseIP("10.27.0.0") + + c.Assert(ips[0], check.Equals, expected) +} + +func (s *Suite) TestGetMultiIp(c *check.C) { + n, err := h.CreateNamespace("test-ip-multi") + c.Assert(err, check.IsNil) + + for i := 1; i <= 350; i++ { + ip, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + IPAddress: ip.String(), + } + h.db.Save(&m) + } + + ips, err := h.getUsedIPs() + + c.Assert(err, check.IsNil) + + c.Assert(len(ips), check.Equals, 350) + + c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.0")) + c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.9")) + c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.44")) + + expectedNextIP := netaddr.MustParseIP("10.27.1.94") + nextIP, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + c.Assert(nextIP.String(), check.Equals, expectedNextIP.String()) + + // If we call get Available again, we should receive + // the same IP, as it has not been reserved. + nextIP2, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) +} From 9f85efffd5fc2b3d8c9cd7057c8dfab4a691d3c5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 22:06:15 +0100 Subject: [PATCH 005/125] Update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f90c831..16448534 100644 --- a/README.md +++ b/README.md @@ -113,9 +113,10 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal ``` "server_url": "http://192.168.1.12:8080", "listen_addr": "0.0.0.0:8080", + "ip_prefix": "100.64.0.0/10" ``` -`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. +`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated. ``` "private_key_path": "private.key", From 95de823b7241eec5ae8d763414d86cab7b1dfcf0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 22:39:18 +0100 Subject: [PATCH 006/125] Add test to ensure we can read back ips --- utils_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/utils_test.go b/utils_test.go index 471b8220..439fdc19 100644 --- a/utils_test.go +++ b/utils_test.go @@ -49,6 +49,11 @@ func (s *Suite) TestGetUsedIps(c *check.C) { expected := netaddr.MustParseIP("10.27.0.0") c.Assert(ips[0], check.Equals, expected) + + m1, err := h.GetMachineByID(0) + c.Assert(err, check.IsNil) + + c.Assert(m1.IPAddress, check.Equals, expected.String()) } func (s *Suite) TestGetMultiIp(c *check.C) { @@ -66,7 +71,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(err, check.NotNil) m := Machine{ - ID: 0, + ID: uint64(i), MachineKey: "foo", NodeKey: "bar", DiscoKey: "faa", @@ -90,6 +95,15 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.9")) c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.44")) + // Check that we can read back the IPs + m1, err := h.GetMachineByID(1) + c.Assert(err, check.IsNil) + c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.0").String()) + + m50, err := h.GetMachineByID(50) + c.Assert(err, check.IsNil) + c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.49").String()) + expectedNextIP := netaddr.MustParseIP("10.27.1.94") nextIP, err := h.getAvailableIP() c.Assert(err, check.IsNil) From eda6e560c369ea39e286cd1d916bc7e369fd7e03 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 2 Aug 2021 22:51:50 +0100 Subject: [PATCH 007/125] debug logging --- api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api.go b/api.go index 088c337f..97ec4d41 100644 --- a/api.go +++ b/api.go @@ -445,6 +445,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, log.Println(err) return } + log.Printf("Assigning %s to %s", ip, m.Name) m.AuthKeyID = uint(pak.ID) m.IPAddress = ip.String() From 73207decfd13b703370d3ea2b57460111363fa4e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 3 Aug 2021 07:42:11 +0100 Subject: [PATCH 008/125] Check that IP is set before parsing Machine is saved to db before it is assigned an ip, so we might have empty ip fields coming back. --- utils.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utils.go b/utils.go index 404e3823..45d44562 100644 --- a/utils.go +++ b/utils.go @@ -121,12 +121,14 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { ips := make([]netaddr.IP, len(addresses)) for index, addr := range addresses { - ip, err := netaddr.ParseIP(addr) - if err != nil { - return nil, fmt.Errorf("failed to parse ip from database, %w", err) - } + if addr != "" { + ip, err := netaddr.ParseIP(addr) + if err != nil { + return nil, fmt.Errorf("failed to parse ip from database, %w", err) + } - ips[index] = ip + ips[index] = ip + } } return ips, nil From d3349aa4d13fca1ad6ef28424657d8e6219b1aab Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 3 Aug 2021 09:26:28 +0100 Subject: [PATCH 009/125] Add test to ensure we can deal with empty ips from database --- utils_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/utils_test.go b/utils_test.go index 439fdc19..8fbe1375 100644 --- a/utils_test.go +++ b/utils_test.go @@ -117,3 +117,39 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) } + +func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { + ip, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + expected := netaddr.MustParseIP("10.27.0.0") + + c.Assert(ip.String(), check.Equals, expected.String()) + + n, err := h.CreateNamespace("test_ip") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testmachine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + } + h.db.Save(&m) + + ip2, err := h.getAvailableIP() + c.Assert(err, check.IsNil) + + c.Assert(ip2.String(), check.Equals, expected.String()) +} From ea615e3a268126ec291e72481f016d3dfd3d7040 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 3 Aug 2021 10:06:42 +0100 Subject: [PATCH 010/125] Do not issue "network" or "broadcast" addresses (0 or 255) --- utils.go | 26 +++++++++++--------------- utils_test.go | 18 +++++++++--------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/utils.go b/utils.go index 45d44562..03dc673f 100644 --- a/utils.go +++ b/utils.go @@ -79,21 +79,6 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { return nil, err } - // for _, ip := range usedIps { - // nextIP := ip.Next() - - // if !containsIPs(usedIps, nextIP) && ipPrefix.Contains(nextIP) { - // return &nextIP, nil - // } - // } - - // // If there are no IPs in use, we are starting fresh and - // // can issue IPs from the beginning of the prefix. - // ip := ipPrefix.IP() - // return &ip, nil - - // return nil, fmt.Errorf("failed to find any available IP in %s", ipPrefix) - // Get the first IP in our prefix ip := ipPrefix.IP() @@ -102,8 +87,19 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { return nil, fmt.Errorf("could not find any suitable IP in %s", ipPrefix) } + // Some OS (including Linux) does not like when IPs ends with 0 or 255, which + // is typically called network or broadcast. Lets avoid them and continue + // to look when we get one of those traditionally reserved IPs. + ipRaw := ip.As4() + if ipRaw[3] == 0 || ipRaw[3] == 255 { + ip = ip.Next() + continue + } + if ip.IsZero() && ip.IsLoopback() { + + ip = ip.Next() continue } diff --git a/utils_test.go b/utils_test.go index 8fbe1375..f50cd117 100644 --- a/utils_test.go +++ b/utils_test.go @@ -10,7 +10,7 @@ func (s *Suite) TestGetAvailableIp(c *check.C) { c.Assert(err, check.IsNil) - expected := netaddr.MustParseIP("10.27.0.0") + expected := netaddr.MustParseIP("10.27.0.1") c.Assert(ip.String(), check.Equals, expected.String()) } @@ -46,7 +46,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) { c.Assert(err, check.IsNil) - expected := netaddr.MustParseIP("10.27.0.0") + expected := netaddr.MustParseIP("10.27.0.1") c.Assert(ips[0], check.Equals, expected) @@ -91,20 +91,20 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(len(ips), check.Equals, 350) - c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.0")) - c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.9")) - c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.44")) + c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) + c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) + c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.47")) // Check that we can read back the IPs m1, err := h.GetMachineByID(1) c.Assert(err, check.IsNil) - c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.0").String()) + c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.1").String()) m50, err := h.GetMachineByID(50) c.Assert(err, check.IsNil) - c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.49").String()) + c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.50").String()) - expectedNextIP := netaddr.MustParseIP("10.27.1.94") + expectedNextIP := netaddr.MustParseIP("10.27.1.97") nextIP, err := h.getAvailableIP() c.Assert(err, check.IsNil) @@ -122,7 +122,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { ip, err := h.getAvailableIP() c.Assert(err, check.IsNil) - expected := netaddr.MustParseIP("10.27.0.0") + expected := netaddr.MustParseIP("10.27.0.1") c.Assert(ip.String(), check.Equals, expected.String()) From 4aad3b7933c3cac33a50bdae8f23ec3736f7cadc Mon Sep 17 00:00:00 2001 From: Juan Font Date: Tue, 3 Aug 2021 20:38:23 +0200 Subject: [PATCH 011/125] Improved README.md on ip_prefix --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16448534..aea8fd6a 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Headscale implements this coordination server. - [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support) - [X] JSON-formatted output - [X] ACLs +- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [ ] Share nodes between ~~users~~ namespaces - [ ] DNS @@ -116,7 +117,7 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal "ip_prefix": "100.64.0.0/10" ``` -`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated. +`server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated (default 100.64.0.0/10, e.g., 192.168.4.0/24, 10.0.0.0/8) ``` "private_key_path": "private.key", From ee704f8ef35b6ebf54eaad0f7bfd65991ab29739 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 18:11:26 +0100 Subject: [PATCH 012/125] Initial port to zerologger --- api.go | 225 +++++++++++++++++++++++++++++++++++++++++++---------- app.go | 22 +++--- go.mod | 1 + go.sum | 2 + machine.go | 5 +- 5 files changed, 202 insertions(+), 53 deletions(-) diff --git a/api.go b/api.go index 97ec4d41..af8a6dcd 100644 --- a/api.go +++ b/api.go @@ -6,10 +6,11 @@ import ( "errors" "fmt" "io" - "log" "net/http" "time" + "github.com/rs/zerolog/log" + "github.com/gin-gonic/gin" "github.com/klauspost/compress/zstd" "gorm.io/datatypes" @@ -63,21 +64,27 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { mKeyStr := c.Param("id") mKey, err := wgkey.ParseHex(mKeyStr) if err != nil { - log.Printf("Cannot parse machine key: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot parse machine key") c.String(http.StatusInternalServerError, "Sad!") return } req := tailcfg.RegisterRequest{} err = decode(body, &req, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot decode message: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot decode message") c.String(http.StatusInternalServerError, "Very sad!") return } var m Machine if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - log.Println("New Machine!") + log.Info().Str("Name", req.Hostinfo.Hostname).Msg("New machine") m = Machine{ Expiry: &req.Expiry, MachineKey: mKey.HexString(), @@ -100,13 +107,20 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { // We have the updated key! if m.NodeKey == wgkey.Key(req.NodeKey).HexString() { if m.Registered { - log.Printf("[%s] Client is registered and we have the current NodeKey. All clear to /map", m.Name) + log.Debug(). + Str("Handler", "Registration"). + Str("Machine", m.Name). + Msg("Client is registered and we have the current NodeKey. All clear to /mSending keepaliveap") + resp.AuthURL = "" resp.MachineAuthorized = true resp.User = *m.Namespace.toUser() respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") return } @@ -114,12 +128,18 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { return } - log.Printf("[%s] Not registered and not NodeKey rotation. Sending a authurl to register", m.Name) + log.Debug(). + Str("Handler", "Registration"). + Str("Machine", m.Name). + Msg("Not registered and not NodeKey rotation. Sending a authurl to register") resp.AuthURL = fmt.Sprintf("%s/register?key=%s", h.cfg.ServerURL, mKey.HexString()) respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") return } @@ -129,7 +149,10 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { // The NodeKey we have matches OldNodeKey, which means this is a refresh after an key expiration if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() { - log.Printf("[%s] We have the OldNodeKey in the database. This is a key refresh", m.Name) + log.Debug(). + Str("Handler", "Registration"). + Str("Machine", m.Name). + Msg("We have the OldNodeKey in the database. This is a key refresh") m.NodeKey = wgkey.Key(req.NodeKey).HexString() h.db.Save(&m) @@ -137,7 +160,10 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { resp.User = *m.Namespace.toUser() respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "Extremely sad!") return } @@ -148,25 +174,38 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { // We arrive here after a client is restarted without finalizing the authentication flow or // when headscale is stopped in the middle of the auth process. if m.Registered { - log.Printf("[%s] The node is sending us a new NodeKey, but machine is registered. All clear for /map", m.Name) + log.Debug(). + Str("Handler", "Registration"). + Str("Machine", m.Name). + Msg("The node is sending us a new NodeKey, but machine is registered. All clear for /map") resp.AuthURL = "" resp.MachineAuthorized = true resp.User = *m.Namespace.toUser() respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") return } c.Data(200, "application/json; charset=utf-8", respBody) return } - log.Printf("[%s] The node is sending us a new NodeKey, sending auth url", m.Name) + + log.Debug(). + Str("Handler", "Registration"). + Str("Machine", m.Name). + Msg("The node is sending us a new NodeKey, sending auth url") resp.AuthURL = fmt.Sprintf("%s/register?key=%s", h.cfg.ServerURL, mKey.HexString()) respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") return } @@ -187,21 +226,29 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { mKeyStr := c.Param("id") mKey, err := wgkey.ParseHex(mKeyStr) if err != nil { - log.Printf("Cannot parse client key: %s", err) + log.Error(). + Str("Handler", "PollNetMap"). + Err(err). + Msg("Cannot parse client key") c.String(http.StatusBadRequest, "") return } req := tailcfg.MapRequest{} err = decode(body, &req, &mKey, h.privateKey) if err != nil { - log.Printf("Cannot decode message: %s", err) + log.Error(). + Str("Handler", "PollNetMap"). + Err(err). + Msg("Cannot decode message") c.String(http.StatusBadRequest, "") return } var m Machine if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - log.Printf("Ignoring request, cannot find machine with key %s", mKey.HexString()) + log.Warn(). + Str("Handler", "PollNetMap"). + Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString()) c.String(http.StatusUnauthorized, "") return } @@ -247,37 +294,66 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { // empty endpoints to peers) // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 - log.Printf("[%s] ReadOnly=%t OmitPeers=%t Stream=%t", m.Name, req.ReadOnly, req.OmitPeers, req.Stream) + log.Debug(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Bool("ReadOnly", req.ReadOnly). + Bool("OmitPeers", req.OmitPeers). + Bool("Stream", req.Stream) if req.ReadOnly { - log.Printf("[%s] Client is starting up. Asking for DERP map", m.Name) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Client is starting up. Asking for DERP map") c.Data(200, "application/json; charset=utf-8", *data) return } if req.OmitPeers && !req.Stream { - log.Printf("[%s] Client sent endpoint update and is ok with a response without peer list", m.Name) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Client sent endpoint update and is ok with a response without peer list") c.Data(200, "application/json; charset=utf-8", *data) return } else if req.OmitPeers && req.Stream { - log.Printf("[%s] Warning, ignoring request, don't know how to handle it", m.Name) + log.Warn(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Ignoring request, don't know how to handle it") c.String(http.StatusBadRequest, "") return } - log.Printf("[%s] Client is ready to access the tailnet", m.Name) - log.Printf("[%s] Sending initial map", m.Name) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Client is ready to access the tailnet") + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Sending initial map") pollData <- *data - log.Printf("[%s] Notifying peers", m.Name) + log.Printf("[PollMap] (%s) Notifying peers", m.Name) peers, _ := h.getPeers(m) h.pollMu.Lock() for _, p := range *peers { pUp, ok := h.clientsPolling[uint64(p.ID)] if ok { - log.Printf("[%s] Notifying peer %s (%s)", m.Name, p.Name, p.Addresses[0]) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Str("Peer", m.Name). + Str("Address", p.Addresses[0].String()). + Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) pUp <- []byte{} } else { - log.Printf("[%s] Peer %s does not appear to be polling", m.Name, p.Name) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Str("Peer", m.Name). + Msgf("Peer %s does not appear to be polling", p.Name) } } h.pollMu.Unlock() @@ -287,10 +363,18 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { c.Stream(func(w io.Writer) bool { select { case data := <-pollData: - log.Printf("[%s] Sending data (%d bytes)", m.Name, len(data)) + log.Trace(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Int("Bytes", len(data)). + Msg("Sending data") _, err := w.Write(data) if err != nil { - log.Printf("[%s] Cannot write data: %s", m.Name, err) + log.Error(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Err(err). + Msg("Cannot write data") } now := time.Now().UTC() m.LastSeen = &now @@ -298,19 +382,33 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { return true case <-update: - log.Printf("[%s] Received a request for update", m.Name) + log.Debug(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Received a request for update") data, err := h.getMapResponse(mKey, req, m) if err != nil { - log.Printf("[%s] Could not get the map update: %s", m.Name, err) + log.Error(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Err(err). + Msg("Could not get the map update") } _, err = w.Write(*data) if err != nil { - log.Printf("[%s] Could not write the map response: %s", m.Name, err) + log.Error(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Err(err). + Msg("Could not write the map response") } return true case <-c.Request.Context().Done(): - log.Printf("[%s] The client has closed the connection", m.Name) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("The client has closed the connection") now := time.Now().UTC() m.LastSeen = &now h.db.Save(&m) @@ -335,10 +433,16 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgk h.pollMu.Lock() data, err := h.getMapKeepAliveResponse(mKey, req, m) if err != nil { - log.Printf("Error generating the keep alive msg: %s", err) + log.Error(). + Str("Func", "keepAlive"). + Err(err). + Msg("Error generating the keep alive msg") return } - log.Printf("[%s] Sending keepalive", m.Name) + log.Debug(). + Str("Func", "keepAlive"). + Str("Machine", m.Name). + Msg("Sending keepalive") pollData <- *data h.pollMu.Unlock() time.Sleep(60 * time.Second) @@ -349,12 +453,18 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgk func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) { node, err := m.toNode() if err != nil { - log.Printf("Cannot convert to node: %s", err) + log.Error(). + Str("Func", "getMapResponse"). + Err(err). + Msg("Cannot convert to node") return nil, err } peers, err := h.getPeers(m) if err != nil { - log.Printf("Cannot fetch peers: %s", err) + log.Error(). + Str("Func", "getMapResponse"). + Err(err). + Msg("Cannot fetch peers") return nil, err } @@ -426,26 +536,49 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapReque } func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, req tailcfg.RegisterRequest, m Machine) { + log.Debug(). + Str("Func", "handleAuthKey"). + Str("Machine", req.Hostinfo.Hostname). + Msgf("Processing auth key for %s", req.Hostinfo.Hostname) resp := tailcfg.RegisterResponse{} pak, err := h.checkKeyValidity(req.Auth.AuthKey) if err != nil { resp.MachineAuthorized = false respBody, err := encode(resp, &idKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") return } c.Data(200, "application/json; charset=utf-8", respBody) - log.Printf("[%s] Failed authentication via AuthKey", m.Name) + log.Error(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Msg("Failed authentication via AuthKey") return } + + log.Debug(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Msg("Authentication key was valid, proceeding to acquire an IP address") ip, err := h.getAvailableIP() if err != nil { - log.Println(err) + log.Error(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Msg("Failed to find an available IP") return } - log.Printf("Assigning %s to %s", ip, m.Name) + log.Info(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Str("IP", ip.String()). + Msgf("Assining %s to %s", ip, m.Name) m.AuthKeyID = uint(pak.ID) m.IPAddress = ip.String() @@ -459,10 +592,18 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, resp.User = *pak.Namespace.toUser() respBody, err := encode(resp, &idKey, h.privateKey) if err != nil { - log.Printf("Cannot encode message: %s", err) + log.Error(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Err(err). + Msg("Cannot encode message") c.String(http.StatusInternalServerError, "Extremely sad!") return } c.Data(200, "application/json; charset=utf-8", respBody) - log.Printf("[%s] Successfully authenticated via AuthKey", m.Name) + log.Info(). + Str("Func", "handleAuthKey"). + Str("Machine", m.Name). + Str("IP", ip.String()). + Msg("Successfully authenticated via AuthKey") } diff --git a/app.go b/app.go index 70ea2cd1..012ab9ea 100644 --- a/app.go +++ b/app.go @@ -3,13 +3,14 @@ package headscale import ( "errors" "fmt" - "log" "net/http" "os" "strings" "sync" "time" + "github.com/rs/zerolog/log" + "github.com/gin-gonic/gin" "golang.org/x/crypto/acme/autocert" "gorm.io/gorm" @@ -120,21 +121,21 @@ func (h *Headscale) ExpireEphemeralNodes(milliSeconds int64) { func (h *Headscale) expireEphemeralNodesWorker() { namespaces, err := h.ListNamespaces() if err != nil { - log.Printf("Error listing namespaces: %s", err) + log.Error().Err(err).Msg("Error listing namespaces") return } for _, ns := range *namespaces { machines, err := h.ListMachinesInNamespace(ns.Name) if err != nil { - log.Printf("Error listing machines in namespace %s: %s", ns.Name, err) + log.Error().Err(err).Str("Namespace", ns.Name).Msg("Error listing machines in namespace") return } for _, m := range *machines { if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { - log.Printf("[%s] Ephemeral client removed from database\n", m.Name) + log.Info().Str("Machine", m.Name).Msg("Ephemeral client removed from database") err = h.db.Unscoped().Delete(m).Error if err != nil { - log.Printf("[%s] 🤮 Cannot delete ephemeral machine from the database: %s", m.Name, err) + log.Error().Err(err).Str("Name", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") } } } @@ -168,7 +169,7 @@ func (h *Headscale) Serve() error { if h.cfg.TLSLetsEncryptHostname != "" { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { - log.Println("WARNING: listening with TLS but ServerURL does not start with https://") + log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") } m := autocert.Manager{ @@ -191,7 +192,10 @@ func (h *Headscale) Serve() error { // port 80 for the certificate validation in addition to the headscale // service, which can be configured to run on any other port. go func() { - log.Fatal(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))) + + log.Fatal(). + Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))). + Msg("failed to set up a HTTP server") }() err = s.ListenAndServeTLS("", "") } else { @@ -199,12 +203,12 @@ func (h *Headscale) Serve() error { } } else if h.cfg.TLSCertPath == "" { if !strings.HasPrefix(h.cfg.ServerURL, "http://") { - log.Println("WARNING: listening without TLS but ServerURL does not start with http://") + log.Warn().Msg("Listening without TLS but ServerURL does not start with http://") } err = r.Run(h.cfg.Addr) } else { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { - log.Println("WARNING: listening with TLS but ServerURL does not start with https://") + log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") } err = r.RunTLS(h.cfg.Addr, h.cfg.TLSCertPath, h.cfg.TLSKeyPath) } diff --git a/go.mod b/go.mod index 0d8c86b5..818683ef 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/klauspost/compress v1.13.1 github.com/lib/pq v1.10.2 // indirect github.com/mattn/go-sqlite3 v1.14.7 // indirect + github.com/rs/zerolog v1.23.0 // indirect github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 diff --git a/go.sum b/go.sum index 4751eaac..4b189d72 100644 --- a/go.sum +++ b/go.sum @@ -683,6 +683,8 @@ github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.23.0 h1:UskrK+saS9P9Y789yNNulYKdARjPZuS35B8gJF2x60g= +github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM= github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= diff --git a/machine.go b/machine.go index 1895e464..f2745c06 100644 --- a/machine.go +++ b/machine.go @@ -3,11 +3,12 @@ package headscale import ( "encoding/json" "fmt" - "log" "sort" "strconv" "time" + "github.com/rs/zerolog/log" + "gorm.io/datatypes" "inet.af/netaddr" "tailscale.com/tailcfg" @@ -157,7 +158,7 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { machines := []Machine{} if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { - log.Printf("Error accessing db: %s", err) + log.Error().Err(err).Msg("Error accessing db") return nil, err } From 0bb2fabc6c6b6120bb29da1bea484bd9fe3b145a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 18:16:21 +0100 Subject: [PATCH 013/125] Convert missing from api.go --- api.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index af8a6dcd..960217dd 100644 --- a/api.go +++ b/api.go @@ -92,7 +92,10 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { NodeKey: wgkey.Key(req.NodeKey).HexString(), } if err := h.db.Create(&m).Error; err != nil { - log.Printf("Could not create row: %s", err) + log.Error(). + Str("Handler", "Registration"). + Err(err). + Msg("Could not create row") return } } @@ -335,7 +338,10 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Msg("Sending initial map") pollData <- *data - log.Printf("[PollMap] (%s) Notifying peers", m.Name) + log.Info(). + Str("Handler", "PollNetMap"). + Str("Machine", m.Name). + Msg("Notifying peers") peers, _ := h.getPeers(m) h.pollMu.Lock() for _, p := range *peers { From 42bf566ffffceb505eb7acbf7391fd9f12dbe566 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 18:18:18 +0100 Subject: [PATCH 014/125] Convert acls.go --- acls.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/acls.go b/acls.go index f4ed4c0e..fea72a7f 100644 --- a/acls.go +++ b/acls.go @@ -4,11 +4,12 @@ import ( "encoding/json" "fmt" "io" - "log" "os" "strconv" "strings" + "github.com/rs/zerolog/log" + "github.com/tailscale/hujson" "inet.af/netaddr" "tailscale.com/tailcfg" @@ -66,7 +67,8 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { for j, u := range a.Users { srcs, err := h.generateACLPolicySrcIP(u) if err != nil { - log.Printf("Error parsing ACL %d, User %d", i, j) + log.Error(). + Msgf("Error parsing ACL %d, User %d", i, j) return nil, err } srcIPs = append(srcIPs, *srcs...) @@ -77,7 +79,8 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { for j, d := range a.Ports { dests, err := h.generateACLPolicyDestPorts(d) if err != nil { - log.Printf("Error parsing ACL %d, Port %d", i, j) + log.Error(). + Msgf("Error parsing ACL %d, Port %d", i, j) return nil, err } destPorts = append(destPorts, *dests...) From d10b57b317ec3e8dff17f815541a1c43f50dd51c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 18:23:02 +0100 Subject: [PATCH 015/125] Convert namespaces.go --- namespaces.go | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/namespaces.go b/namespaces.go index 840f8729..520cbe55 100644 --- a/namespaces.go +++ b/namespaces.go @@ -4,9 +4,9 @@ import ( "encoding/json" "errors" "fmt" - "log" "time" + "github.com/rs/zerolog/log" "gorm.io/gorm" "tailscale.com/tailcfg" ) @@ -33,7 +33,10 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) { } n.Name = name if err := h.db.Create(&n).Error; err != nil { - log.Printf("Could not create row: %s", err) + log.Error(). + Str("Func", "CreateNamespace"). + Err(err). + Msg("Could not create row") return nil, err } return &n, nil @@ -133,7 +136,10 @@ func (h *Headscale) RequestMapUpdates(namespaceID uint) error { names = append(names, namespace.Name) data, err := json.Marshal(names) if err != nil { - log.Printf("Could not marshal namespaces_pending_updates: %s", err) + log.Error(). + Str("Func", "RequestMapUpdates"). + Err(err). + Msg("Could not marshal namespaces_pending_updates") return err } return h.setValue("namespaces_pending_updates", string(data)) @@ -154,7 +160,10 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { return } for _, name := range names { - log.Printf("Sending updates to nodes in namespace %s", name) + log.Trace(). + Str("Func", "RequestMapUpdates"). + Str("Machine", name). + Msg("Sending updates to nodes in namespace") machines, err := h.ListMachinesInNamespace(name) if err != nil { continue @@ -165,10 +174,19 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { for _, p := range *peers { pUp, ok := h.clientsPolling[uint64(p.ID)] if ok { - log.Printf("[%s] Notifying peer %s (%s)", m.Name, p.Name, p.Addresses[0]) + log.Info(). + Str("Func", "checkForNamespacesPendingUpdates"). + Str("Machine", m.Name). + Str("Peer", m.Name). + Str("Address", p.Addresses[0].String()). + Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) pUp <- []byte{} } else { - log.Printf("[%s] Peer %s does not appear to be polling", m.Name, p.Name) + log.Info(). + Str("Func", "checkForNamespacesPendingUpdates"). + Str("Machine", m.Name). + Str("Peer", m.Name). + Msgf("Peer %s does not appear to be polling", p.Name) } } h.pollMu.Unlock() @@ -181,7 +199,10 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { if v == newV { // only clear when no changes, so we notified everybody err = h.setValue("namespaces_pending_updates", "") if err != nil { - log.Printf("Could not save to KV: %s", err) + log.Error(). + Str("Func", "checkForNamespacesPendingUpdates"). + Err(err). + Msg("Could not save to KV") return } } From b1200140b870446fd66a7f8cc82ca9c323b62f0e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 18:26:49 +0100 Subject: [PATCH 016/125] Convert cli/utils.go --- cmd/headscale/cli/utils.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 1c259c74..ddaeca73 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -5,13 +5,13 @@ import ( "errors" "fmt" "io" - "log" "os" "path/filepath" "strings" "time" "github.com/juanfont/headscale" + "github.com/rs/zerolog/log" "github.com/spf13/viper" "gopkg.in/yaml.v2" "inet.af/netaddr" @@ -52,7 +52,8 @@ func LoadConfig(path string) error { if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { // this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule) - log.Println("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443") + log.Warn(). + Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443") } if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && (viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") { @@ -82,9 +83,13 @@ func absPath(path string) string { } func getHeadscaleApp() (*headscale.Headscale, error) { - derpMap, err := loadDerpMap(absPath(viper.GetString("derp_map_path"))) + derpPath := absPath(viper.GetString("derp_map_path")) + derpMap, err := loadDerpMap(derpPath) if err != nil { - log.Printf("Could not load DERP servers map file: %s", err) + log.Error(). + Str("Path", derpPath). + Err(err). + Msg("Could not load DERP servers map file") } // Minimum inactivity time out is keepalive timeout (60s) plus a few seconds @@ -129,9 +134,13 @@ func getHeadscaleApp() (*headscale.Headscale, error) { // We are doing this here, as in the future could be cool to have it also hot-reload if viper.GetString("acl_policy_path") != "" { - err = h.LoadACLPolicy(absPath(viper.GetString("acl_policy_path"))) + aclPath := absPath(viper.GetString("acl_policy_path")) + err = h.LoadACLPolicy(aclPath) if err != nil { - log.Printf("Could not load the ACL policy: %s", err) + log.Error(). + Str("Path", aclPath). + Err(err). + Msg("Could not load the ACL policy") } } From 0660867a1628250382c210b2af0b7a33c09864e8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 18:58:15 +0100 Subject: [PATCH 017/125] Correct url --- cmd/headscale/cli/root.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 0dd68064..82eb2a76 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -2,8 +2,9 @@ package cli import ( "fmt" - "github.com/spf13/cobra" "os" + + "github.com/spf13/cobra" ) func init() { @@ -17,7 +18,7 @@ var rootCmd = &cobra.Command{ headscale is an open source implementation of the Tailscale control server Juan Font Alonso - 2021 -https://gitlab.com/juanfont/headscale`, +https://github.com/juanfont/headscale`, } func Execute() { From cd2ca137c0a58d3ee911e8ff3a67abe5d56214f6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 19:19:25 +0100 Subject: [PATCH 018/125] Make log_level user configurable --- README.md | 5 +++++ cmd/headscale/cli/utils.go | 10 ++++++---- cmd/headscale/headscale.go | 30 ++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index aea8fd6a..cb42b666 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,11 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal `server_url` is the external URL via which Headscale is reachable. `listen_addr` is the IP address and port the Headscale program should listen on. `ip_prefix` is the IP prefix (range) in which IP addresses for nodes will be allocated (default 100.64.0.0/10, e.g., 192.168.4.0/24, 10.0.0.0/8) +``` + "log_level": "debug" +``` +`log_level` can be used to set the Log level for Headscale, it defaults to `debug`, and the available levels are: `trace`, `debug`, `info`, `warn` and `error`. + ``` "private_key_path": "private.key", ``` diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index ddaeca73..d104f044 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -39,6 +39,8 @@ func LoadConfig(path string) error { viper.SetDefault("ip_prefix", "100.64.0.0/10") + viper.SetDefault("log_level", "debug") + err := viper.ReadInConfig() if err != nil { return fmt.Errorf("Fatal error reading config file: %s \n", err) @@ -170,24 +172,24 @@ func JsonOutput(result interface{}, errResult error, outputFormat string) { if errResult != nil { j, err = json.MarshalIndent(ErrorOutput{errResult.Error()}, "", "\t") if err != nil { - log.Fatalln(err) + log.Fatal().Err(err) } } else { j, err = json.MarshalIndent(result, "", "\t") if err != nil { - log.Fatalln(err) + log.Fatal().Err(err) } } case "json-line": if errResult != nil { j, err = json.Marshal(ErrorOutput{errResult.Error()}) if err != nil { - log.Fatalln(err) + log.Fatal().Err(err) } } else { j, err = json.Marshal(result) if err != nil { - log.Fatalln(err) + log.Fatal().Err(err) } } } diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index c7b834c2..3769f534 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -1,15 +1,41 @@ package main import ( - "log" + "os" + "time" "github.com/juanfont/headscale/cmd/headscale/cli" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" ) func main() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + log.Logger = log.Output(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }) + err := cli.LoadConfig("") if err != nil { - log.Fatalf(err.Error()) + log.Fatal().Err(err) + } + + logLevel := viper.GetString("log_level") + switch logLevel { + case "trace": + zerolog.SetGlobalLevel(zerolog.TraceLevel) + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + zerolog.SetGlobalLevel(zerolog.DebugLevel) } cli.Execute() From a8c8a358d0dcad845158e96eb5eca41385e26733 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 20:57:47 +0100 Subject: [PATCH 019/125] Make log keys lowercase --- api.go | 164 ++++++++++++++++++------------------- app.go | 6 +- cmd/headscale/cli/utils.go | 4 +- namespaces.go | 24 +++--- 4 files changed, 99 insertions(+), 99 deletions(-) diff --git a/api.go b/api.go index 960217dd..575ac8a7 100644 --- a/api.go +++ b/api.go @@ -65,7 +65,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { mKey, err := wgkey.ParseHex(mKeyStr) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot parse machine key") c.String(http.StatusInternalServerError, "Sad!") @@ -75,7 +75,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { err = decode(body, &req, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot decode message") c.String(http.StatusInternalServerError, "Very sad!") @@ -84,7 +84,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { var m Machine if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - log.Info().Str("Name", req.Hostinfo.Hostname).Msg("New machine") + log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") m = Machine{ Expiry: &req.Expiry, MachineKey: mKey.HexString(), @@ -93,7 +93,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } if err := h.db.Create(&m).Error; err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Could not create row") return @@ -111,8 +111,8 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { if m.NodeKey == wgkey.Key(req.NodeKey).HexString() { if m.Registered { log.Debug(). - Str("Handler", "Registration"). - Str("Machine", m.Name). + Str("handler", "Registration"). + Str("machine", m.Name). Msg("Client is registered and we have the current NodeKey. All clear to /mSending keepaliveap") resp.AuthURL = "" @@ -121,7 +121,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") @@ -132,15 +132,15 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } log.Debug(). - Str("Handler", "Registration"). - Str("Machine", m.Name). + Str("handler", "Registration"). + Str("machine", m.Name). Msg("Not registered and not NodeKey rotation. Sending a authurl to register") resp.AuthURL = fmt.Sprintf("%s/register?key=%s", h.cfg.ServerURL, mKey.HexString()) respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") @@ -153,8 +153,8 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { // The NodeKey we have matches OldNodeKey, which means this is a refresh after an key expiration if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() { log.Debug(). - Str("Handler", "Registration"). - Str("Machine", m.Name). + Str("handler", "Registration"). + Str("machine", m.Name). Msg("We have the OldNodeKey in the database. This is a key refresh") m.NodeKey = wgkey.Key(req.NodeKey).HexString() h.db.Save(&m) @@ -164,7 +164,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "Extremely sad!") @@ -178,8 +178,8 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { // when headscale is stopped in the middle of the auth process. if m.Registered { log.Debug(). - Str("Handler", "Registration"). - Str("Machine", m.Name). + Str("handler", "Registration"). + Str("machine", m.Name). Msg("The node is sending us a new NodeKey, but machine is registered. All clear for /map") resp.AuthURL = "" resp.MachineAuthorized = true @@ -187,7 +187,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") @@ -198,15 +198,15 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } log.Debug(). - Str("Handler", "Registration"). - Str("Machine", m.Name). + Str("handler", "Registration"). + Str("machine", m.Name). Msg("The node is sending us a new NodeKey, sending auth url") resp.AuthURL = fmt.Sprintf("%s/register?key=%s", h.cfg.ServerURL, mKey.HexString()) respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "Registration"). + Str("handler", "Registration"). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") @@ -230,7 +230,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { mKey, err := wgkey.ParseHex(mKeyStr) if err != nil { log.Error(). - Str("Handler", "PollNetMap"). + Str("handler", "PollNetMap"). Err(err). Msg("Cannot parse client key") c.String(http.StatusBadRequest, "") @@ -240,7 +240,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { err = decode(body, &req, &mKey, h.privateKey) if err != nil { log.Error(). - Str("Handler", "PollNetMap"). + Str("handler", "PollNetMap"). Err(err). Msg("Cannot decode message") c.String(http.StatusBadRequest, "") @@ -250,7 +250,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { var m Machine if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { log.Warn(). - Str("Handler", "PollNetMap"). + Str("handler", "PollNetMap"). Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString()) c.String(http.StatusUnauthorized, "") return @@ -298,49 +298,49 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 log.Debug(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). - Bool("ReadOnly", req.ReadOnly). - Bool("OmitPeers", req.OmitPeers). - Bool("Stream", req.Stream) + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Bool("readOnly", req.ReadOnly). + Bool("omitPeers", req.OmitPeers). + Bool("stream", req.Stream) if req.ReadOnly { log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Client is starting up. Asking for DERP map") c.Data(200, "application/json; charset=utf-8", *data) return } if req.OmitPeers && !req.Stream { log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Client sent endpoint update and is ok with a response without peer list") c.Data(200, "application/json; charset=utf-8", *data) return } else if req.OmitPeers && req.Stream { log.Warn(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Ignoring request, don't know how to handle it") c.String(http.StatusBadRequest, "") return } log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Client is ready to access the tailnet") log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Sending initial map") pollData <- *data log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Notifying peers") peers, _ := h.getPeers(m) h.pollMu.Lock() @@ -348,17 +348,17 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { pUp, ok := h.clientsPolling[uint64(p.ID)] if ok { log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). - Str("Peer", m.Name). - Str("Address", p.Addresses[0].String()). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Str("peer", m.Name). + Str("address", p.Addresses[0].String()). Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) pUp <- []byte{} } else { log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). - Str("Peer", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Str("peer", m.Name). Msgf("Peer %s does not appear to be polling", p.Name) } } @@ -370,15 +370,15 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { select { case data := <-pollData: log.Trace(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). - Int("Bytes", len(data)). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Int("bytes", len(data)). Msg("Sending data") _, err := w.Write(data) if err != nil { log.Error(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Err(err). Msg("Cannot write data") } @@ -389,22 +389,22 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { case <-update: log.Debug(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("Received a request for update") data, err := h.getMapResponse(mKey, req, m) if err != nil { log.Error(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Err(err). Msg("Could not get the map update") } _, err = w.Write(*data) if err != nil { log.Error(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Err(err). Msg("Could not write the map response") } @@ -412,8 +412,8 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { case <-c.Request.Context().Done(): log.Info(). - Str("Handler", "PollNetMap"). - Str("Machine", m.Name). + Str("handler", "PollNetMap"). + Str("machine", m.Name). Msg("The client has closed the connection") now := time.Now().UTC() m.LastSeen = &now @@ -440,14 +440,14 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgk data, err := h.getMapKeepAliveResponse(mKey, req, m) if err != nil { log.Error(). - Str("Func", "keepAlive"). + Str("func", "keepAlive"). Err(err). Msg("Error generating the keep alive msg") return } log.Debug(). - Str("Func", "keepAlive"). - Str("Machine", m.Name). + Str("func", "keepAlive"). + Str("machine", m.Name). Msg("Sending keepalive") pollData <- *data h.pollMu.Unlock() @@ -460,7 +460,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac node, err := m.toNode() if err != nil { log.Error(). - Str("Func", "getMapResponse"). + Str("func", "getMapResponse"). Err(err). Msg("Cannot convert to node") return nil, err @@ -468,7 +468,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac peers, err := h.getPeers(m) if err != nil { log.Error(). - Str("Func", "getMapResponse"). + Str("func", "getMapResponse"). Err(err). Msg("Cannot fetch peers") return nil, err @@ -543,8 +543,8 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapReque func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, req tailcfg.RegisterRequest, m Machine) { log.Debug(). - Str("Func", "handleAuthKey"). - Str("Machine", req.Hostinfo.Hostname). + Str("func", "handleAuthKey"). + Str("machine", req.Hostinfo.Hostname). Msgf("Processing auth key for %s", req.Hostinfo.Hostname) resp := tailcfg.RegisterResponse{} pak, err := h.checkKeyValidity(req.Auth.AuthKey) @@ -553,8 +553,8 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, respBody, err := encode(resp, &idKey, h.privateKey) if err != nil { log.Error(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). + Str("func", "handleAuthKey"). + Str("machine", m.Name). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "") @@ -562,28 +562,28 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, } c.Data(200, "application/json; charset=utf-8", respBody) log.Error(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). + Str("func", "handleAuthKey"). + Str("machine", m.Name). Msg("Failed authentication via AuthKey") return } log.Debug(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). + Str("func", "handleAuthKey"). + Str("machine", m.Name). Msg("Authentication key was valid, proceeding to acquire an IP address") ip, err := h.getAvailableIP() if err != nil { log.Error(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). + Str("func", "handleAuthKey"). + Str("machine", m.Name). Msg("Failed to find an available IP") return } log.Info(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). - Str("IP", ip.String()). + Str("func", "handleAuthKey"). + Str("machine", m.Name). + Str("ip", ip.String()). Msgf("Assining %s to %s", ip, m.Name) m.AuthKeyID = uint(pak.ID) @@ -599,8 +599,8 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, respBody, err := encode(resp, &idKey, h.privateKey) if err != nil { log.Error(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). + Str("func", "handleAuthKey"). + Str("machine", m.Name). Err(err). Msg("Cannot encode message") c.String(http.StatusInternalServerError, "Extremely sad!") @@ -608,8 +608,8 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, } c.Data(200, "application/json; charset=utf-8", respBody) log.Info(). - Str("Func", "handleAuthKey"). - Str("Machine", m.Name). - Str("IP", ip.String()). + Str("func", "handleAuthKey"). + Str("machine", m.Name). + Str("ip", ip.String()). Msg("Successfully authenticated via AuthKey") } diff --git a/app.go b/app.go index 012ab9ea..45df01c0 100644 --- a/app.go +++ b/app.go @@ -127,15 +127,15 @@ func (h *Headscale) expireEphemeralNodesWorker() { for _, ns := range *namespaces { machines, err := h.ListMachinesInNamespace(ns.Name) if err != nil { - log.Error().Err(err).Str("Namespace", ns.Name).Msg("Error listing machines in namespace") + log.Error().Err(err).Str("namespace", ns.Name).Msg("Error listing machines in namespace") return } for _, m := range *machines { if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { - log.Info().Str("Machine", m.Name).Msg("Ephemeral client removed from database") + log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database") err = h.db.Unscoped().Delete(m).Error if err != nil { - log.Error().Err(err).Str("Name", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") + log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") } } } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index d104f044..4ada6408 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -89,7 +89,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { derpMap, err := loadDerpMap(derpPath) if err != nil { log.Error(). - Str("Path", derpPath). + Str("path", derpPath). Err(err). Msg("Could not load DERP servers map file") } @@ -140,7 +140,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { err = h.LoadACLPolicy(aclPath) if err != nil { log.Error(). - Str("Path", aclPath). + Str("path", aclPath). Err(err). Msg("Could not load the ACL policy") } diff --git a/namespaces.go b/namespaces.go index 520cbe55..9bbb6b32 100644 --- a/namespaces.go +++ b/namespaces.go @@ -34,7 +34,7 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) { n.Name = name if err := h.db.Create(&n).Error; err != nil { log.Error(). - Str("Func", "CreateNamespace"). + Str("func", "CreateNamespace"). Err(err). Msg("Could not create row") return nil, err @@ -137,7 +137,7 @@ func (h *Headscale) RequestMapUpdates(namespaceID uint) error { data, err := json.Marshal(names) if err != nil { log.Error(). - Str("Func", "RequestMapUpdates"). + Str("func", "RequestMapUpdates"). Err(err). Msg("Could not marshal namespaces_pending_updates") return err @@ -161,8 +161,8 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { } for _, name := range names { log.Trace(). - Str("Func", "RequestMapUpdates"). - Str("Machine", name). + Str("func", "RequestMapUpdates"). + Str("machine", name). Msg("Sending updates to nodes in namespace") machines, err := h.ListMachinesInNamespace(name) if err != nil { @@ -175,17 +175,17 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { pUp, ok := h.clientsPolling[uint64(p.ID)] if ok { log.Info(). - Str("Func", "checkForNamespacesPendingUpdates"). - Str("Machine", m.Name). - Str("Peer", m.Name). - Str("Address", p.Addresses[0].String()). + Str("func", "checkForNamespacesPendingUpdates"). + Str("machine", m.Name). + Str("peer", m.Name). + Str("address", p.Addresses[0].String()). Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) pUp <- []byte{} } else { log.Info(). - Str("Func", "checkForNamespacesPendingUpdates"). - Str("Machine", m.Name). - Str("Peer", m.Name). + Str("func", "checkForNamespacesPendingUpdates"). + Str("machine", m.Name). + Str("peer", m.Name). Msgf("Peer %s does not appear to be polling", p.Name) } } @@ -200,7 +200,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { err = h.setValue("namespaces_pending_updates", "") if err != nil { log.Error(). - Str("Func", "checkForNamespacesPendingUpdates"). + Str("func", "checkForNamespacesPendingUpdates"). Err(err). Msg("Could not save to KV") return From 575b15e5fa737124d7203ecd4f005196f64d71b2 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 21:47:06 +0100 Subject: [PATCH 020/125] Add more trace logging --- api.go | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/api.go b/api.go index 575ac8a7..9fd8c7bf 100644 --- a/api.go +++ b/api.go @@ -225,6 +225,10 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { // // At this moment the updates are sent in a quite horrendous way, but they kinda work. func (h *Headscale) PollNetMapHandler(c *gin.Context) { + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Msg("PollNetMapHandler called") body, _ := io.ReadAll(c.Request.Body) mKeyStr := c.Param("id") mKey, err := wgkey.ParseHex(mKeyStr) @@ -255,6 +259,11 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { c.String(http.StatusUnauthorized, "") return } + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Found machine in database") hostinfo, _ := json.Marshal(req.Hostinfo) m.Name = req.Hostinfo.Hostname @@ -277,17 +286,36 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { } h.db.Save(&m) - pollData := make(chan []byte, 1) update := make(chan []byte, 1) - cancelKeepAlive := make(chan []byte, 1) + + pollData := make(chan []byte, 1) defer close(pollData) + + cancelKeepAlive := make(chan []byte, 1) defer close(cancelKeepAlive) + + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Locking poll mutex") h.pollMu.Lock() h.clientsPolling[m.ID] = update h.pollMu.Unlock() + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Unlocking poll mutex") data, err := h.getMapResponse(mKey, req, m) if err != nil { + log.Error(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Err(err). + Msg("Failed to get Map response") c.String(http.StatusInternalServerError, ":(") return } @@ -299,10 +327,12 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 log.Debug(). Str("handler", "PollNetMap"). + Str("id", c.Param("id")). Str("machine", m.Name). Bool("readOnly", req.ReadOnly). Bool("omitPeers", req.OmitPeers). - Bool("stream", req.Stream) + Bool("stream", req.Stream). + Msg("Client map request processed") if req.ReadOnly { log.Info(). @@ -457,6 +487,10 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgk } func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) { + log.Trace(). + Str("func", "getMapResponse"). + Str("machine", req.Hostinfo.Hostname). + Msg("Creating Map response") node, err := m.toNode() if err != nil { log.Error(). From 1abc68ccf42794756a61e7c2e850051e79b5a1e0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Aug 2021 22:14:37 +0100 Subject: [PATCH 021/125] Removes locks causing deadlock This commit removes most of the locks in the PollingMap handler as there was combinations that caused deadlocks. Instead of doing a plain map and doing the locking ourselves, we use sync.Map which handles it for us. --- api.go | 14 ++++---------- app.go | 3 +-- namespaces.go | 6 ++---- routes.go | 4 ++-- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/api.go b/api.go index 9fd8c7bf..8589d07c 100644 --- a/api.go +++ b/api.go @@ -299,9 +299,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("id", c.Param("id")). Str("machine", m.Name). Msg("Locking poll mutex") - h.pollMu.Lock() - h.clientsPolling[m.ID] = update - h.pollMu.Unlock() + h.clientsPolling.Store(m.ID, update) log.Trace(). Str("handler", "PollNetMap"). Str("id", c.Param("id")). @@ -373,9 +371,8 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("machine", m.Name). Msg("Notifying peers") peers, _ := h.getPeers(m) - h.pollMu.Lock() for _, p := range *peers { - pUp, ok := h.clientsPolling[uint64(p.ID)] + pUp, ok := h.clientsPolling.Load(uint64(p.ID)) if ok { log.Info(). Str("handler", "PollNetMap"). @@ -383,7 +380,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("peer", m.Name). Str("address", p.Addresses[0].String()). Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - pUp <- []byte{} + pUp.(chan []byte) <- []byte{} } else { log.Info(). Str("handler", "PollNetMap"). @@ -392,7 +389,6 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Msgf("Peer %s does not appear to be polling", p.Name) } } - h.pollMu.Unlock() go h.keepAlive(cancelKeepAlive, pollData, mKey, req, m) @@ -448,11 +444,9 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { now := time.Now().UTC() m.LastSeen = &now h.db.Save(&m) - h.pollMu.Lock() cancelKeepAlive <- []byte{} - delete(h.clientsPolling, m.ID) + h.clientsPolling.Delete(m.ID) close(update) - h.pollMu.Unlock() return false } diff --git a/app.go b/app.go index 45df01c0..668e23b9 100644 --- a/app.go +++ b/app.go @@ -59,7 +59,7 @@ type Headscale struct { aclRules *[]tailcfg.FilterRule pollMu sync.Mutex - clientsPolling map[uint64]chan []byte // this is by all means a hackity hack + clientsPolling sync.Map } // NewHeadscale returns the Headscale app @@ -99,7 +99,6 @@ func NewHeadscale(cfg Config) (*Headscale, error) { return nil, err } - h.clientsPolling = make(map[uint64]chan []byte) return &h, nil } diff --git a/namespaces.go b/namespaces.go index 9bbb6b32..9b8d1904 100644 --- a/namespaces.go +++ b/namespaces.go @@ -170,9 +170,8 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { } for _, m := range *machines { peers, _ := h.getPeers(m) - h.pollMu.Lock() for _, p := range *peers { - pUp, ok := h.clientsPolling[uint64(p.ID)] + pUp, ok := h.clientsPolling.Load(uint64(p.ID)) if ok { log.Info(). Str("func", "checkForNamespacesPendingUpdates"). @@ -180,7 +179,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { Str("peer", m.Name). Str("address", p.Addresses[0].String()). Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - pUp <- []byte{} + pUp.(chan []byte) <- []byte{} } else { log.Info(). Str("func", "checkForNamespacesPendingUpdates"). @@ -189,7 +188,6 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { Msgf("Peer %s does not appear to be polling", p.Name) } } - h.pollMu.Unlock() } } newV, err := h.getValue("namespaces_pending_updates") diff --git a/routes.go b/routes.go index a02bed30..e188b91c 100644 --- a/routes.go +++ b/routes.go @@ -52,8 +52,8 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr peers, _ := h.getPeers(*m) h.pollMu.Lock() for _, p := range *peers { - if pUp, ok := h.clientsPolling[uint64(p.ID)]; ok { - pUp <- []byte{} + if pUp, ok := h.clientsPolling.Load(uint64(p.ID)); ok { + pUp.(chan []byte) <- []byte{} } } h.pollMu.Unlock() From c95cf157317cbc612c5e6e2e87fe363804776edc Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 6 Aug 2021 00:21:34 +0200 Subject: [PATCH 022/125] Fixed log message --- api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.go b/api.go index 8589d07c..0d2b15cb 100644 --- a/api.go +++ b/api.go @@ -113,7 +113,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { log.Debug(). Str("handler", "Registration"). Str("machine", m.Name). - Msg("Client is registered and we have the current NodeKey. All clear to /mSending keepaliveap") + Msg("Client is registered and we have the current NodeKey. All clear to /map") resp.AuthURL = "" resp.MachineAuthorized = true From 8a614dabc0b19354b477efcc17519cc00fe93a20 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 6 Aug 2021 00:23:07 +0200 Subject: [PATCH 023/125] Headscale is from no-juan --- cmd/headscale/cli/root.go | 1 - k8s/README.md | 1 - 2 files changed, 2 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 82eb2a76..21857d85 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -17,7 +17,6 @@ var rootCmd = &cobra.Command{ Long: ` headscale is an open source implementation of the Tailscale control server -Juan Font Alonso - 2021 https://github.com/juanfont/headscale`, } diff --git a/k8s/README.md b/k8s/README.md index 5cd18ce2..0f60794c 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -65,7 +65,6 @@ tasks like creating namespaces, authkeys, etc. headscale is an open source implementation of the Tailscale control server -Juan Font Alonso - 2021 https://gitlab.com/juanfont/headscale Usage: From 73a00c89ffcae08a9379514c1e5cb98687335499 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Aug 2021 07:29:57 +0100 Subject: [PATCH 024/125] Try to detect color support, make color configurable This commit tries to detect if users can render colors in their terminal and only enables color logs if that is true. It also adds no-color.org's NO_COLOR env var support to allow it to be disabled. --- cmd/headscale/headscale.go | 23 +++++++++++++++++++++++ go.mod | 1 + go.sum | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index 3769f534..ca443216 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -4,6 +4,7 @@ import ( "os" "time" + "github.com/efekarakus/termcolor" "github.com/juanfont/headscale/cmd/headscale/cli" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -11,10 +12,32 @@ import ( ) func main() { + var colors bool + switch l := termcolor.SupportLevel(os.Stderr); l { + case termcolor.Level16M: + colors = true + case termcolor.Level256: + colors = true + case termcolor.LevelBasic: + colors = true + default: + // no color, return text as is. + log.Trace().Msg("Colors are not supported, disabling") + colors = false + } + + // Adhere to no-color.org manifesto of allowing users to + // turn off color in cli/services + if _, noColorIsSet := os.LookupEnv("NO_COLOR"); noColorIsSet { + log.Trace().Msg("NO_COLOR is set, disabling colors") + colors = false + } + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Logger = log.Output(zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: time.RFC3339, + NoColor: !colors, }) err := cli.LoadConfig("") diff --git a/go.mod b/go.mod index 818683ef..1bfb4bbd 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.0.5 + github.com/efekarakus/termcolor v1.0.1 // indirect github.com/gin-gonic/gin v1.7.2 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.1 diff --git a/go.sum b/go.sum index 4b189d72..1f3779b4 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/efekarakus/termcolor v1.0.1 h1:YAKFO3bnLrqZGTWyNLcYoSIAQFKVOmbqmDnwsU/znzg= +github.com/efekarakus/termcolor v1.0.1/go.mod h1:AitrZNrE4nPO538fRsqf+p0WgLdAsGN5pUNrHEPsEMM= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -532,6 +534,7 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -1002,6 +1005,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 5bc5c5dc1bc55e5e2d6ceff4bc289884ab01e7bb Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Aug 2021 20:02:47 +0100 Subject: [PATCH 025/125] Remove forgotten lock --- routes.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/routes.go b/routes.go index e188b91c..4838cda6 100644 --- a/routes.go +++ b/routes.go @@ -50,13 +50,11 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr // Definetely not accessible from the CLI tool. // We need RPC to the server - or some kind of 'needsUpdate' field in the DB peers, _ := h.getPeers(*m) - h.pollMu.Lock() for _, p := range *peers { if pUp, ok := h.clientsPolling.Load(uint64(p.ID)); ok { pUp.(chan []byte) <- []byte{} } } - h.pollMu.Unlock() return &rIP, nil } } From 4243885246d1ac475ed46437d0729ff276f57344 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Aug 2021 20:03:25 +0100 Subject: [PATCH 026/125] Rewrite old lock error msg --- api.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/api.go b/api.go index 0d2b15cb..b17f3eb3 100644 --- a/api.go +++ b/api.go @@ -298,13 +298,8 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("id", c.Param("id")). Str("machine", m.Name). - Msg("Locking poll mutex") + Msg("Storing update channel") h.clientsPolling.Store(m.ID, update) - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Unlocking poll mutex") data, err := h.getMapResponse(mKey, req, m) if err != nil { From 15b8c8f4c5b43a21a8ae46171a0a05241d96f9c8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Aug 2021 20:08:51 +0100 Subject: [PATCH 027/125] Remove lock from keepAlive --- api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index b17f3eb3..820e3f48 100644 --- a/api.go +++ b/api.go @@ -455,7 +455,6 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgk return default: - h.pollMu.Lock() data, err := h.getMapKeepAliveResponse(mKey, req, m) if err != nil { log.Error(). @@ -464,12 +463,13 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgk Msg("Error generating the keep alive msg") return } + log.Debug(). Str("func", "keepAlive"). Str("machine", m.Name). Msg("Sending keepalive") pollData <- *data - h.pollMu.Unlock() + time.Sleep(60 * time.Second) } } From 99fd126219c1d2a0528cc08449470c07d5f68ff9 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Aug 2021 21:11:38 +0100 Subject: [PATCH 028/125] Remove unused mutex --- app.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app.go b/app.go index 668e23b9..363b38bc 100644 --- a/app.go +++ b/app.go @@ -58,7 +58,6 @@ type Headscale struct { aclPolicy *ACLPolicy aclRules *[]tailcfg.FilterRule - pollMu sync.Mutex clientsPolling sync.Map } From 3fa1ac9c793177d0238026020fe602deb2c2361f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sun, 8 Aug 2021 01:52:01 +0800 Subject: [PATCH 029/125] Correct a typo in routes.go --- routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes.go b/routes.go index 4838cda6..7c83436e 100644 --- a/routes.go +++ b/routes.go @@ -47,7 +47,7 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr // THIS IS COMPLETELY USELESS. // The peers map is stored in memory in the server process. - // Definetely not accessible from the CLI tool. + // Definitely not accessible from the CLI tool. // We need RPC to the server - or some kind of 'needsUpdate' field in the DB peers, _ := h.getPeers(*m) for _, p := range *peers { From 226cb89d97da6ab77ec4afecda18c4e7b5b5bd3a Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 7 Aug 2021 23:57:52 +0200 Subject: [PATCH 030/125] Added func to expire PAKs --- preauth_keys.go | 7 +++++++ preauth_keys_test.go | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/preauth_keys.go b/preauth_keys.go index 7cffceae..460797c4 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -67,6 +67,13 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error) return &keys, nil } +func (h *Headscale) MarkExpirePreAuthKey(k *PreAuthKey) error { + if err := h.db.Model(&k).Update("Expiration", time.Now()).Error; err != nil { + return err + } + return nil +} + // checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node // If returns no error and a PreAuthKey, it can be used func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { diff --git a/preauth_keys_test.go b/preauth_keys_test.go index 6f1369c5..37f2e4dd 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -163,3 +163,20 @@ func (*Suite) TestEphemeralKey(c *check.C) { _, err = h.GetMachine("test7", "testest") c.Assert(err, check.NotNil) } + +func (*Suite) TestExpirePreauthKey(c *check.C) { + n, err := h.CreateNamespace("test3") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, true, false, nil) + c.Assert(err, check.IsNil) + c.Assert(pak.Expiration, check.IsNil) + + err = h.MarkExpirePreAuthKey(pak) + c.Assert(err, check.IsNil) + c.Assert(pak.Expiration, check.NotNil) + + p, err := h.checkKeyValidity(pak.Key) + c.Assert(err, check.Equals, errorAuthKeyExpired) + c.Assert(p, check.IsNil) +} From 05e08e0ac724eeda8336223c9578d08af4f22c56 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 8 Aug 2021 00:10:30 +0200 Subject: [PATCH 031/125] Added cmd to expire preauth keys (requested in #78) --- cmd/headscale/cli/preauthkeys.go | 40 ++++++++++++++++++++++++++++++++ preauth_keys.go | 13 +++++++++++ 2 files changed, 53 insertions(+) diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index eb9d182c..c164610f 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -19,6 +19,7 @@ func init() { } preauthkeysCmd.AddCommand(listPreAuthKeys) preauthkeysCmd.AddCommand(createPreAuthKeyCmd) + preauthkeysCmd.AddCommand(expirePreAuthKeyCmd) createPreAuthKeyCmd.PersistentFlags().Bool("reusable", false, "Make the preauthkey reusable") createPreAuthKeyCmd.PersistentFlags().Bool("ephemeral", false, "Preauthkey for ephemeral nodes") createPreAuthKeyCmd.Flags().StringP("expiration", "e", "", "Human-readable expiration of the key (30m, 24h, 365d...)") @@ -119,3 +120,42 @@ var createPreAuthKeyCmd = &cobra.Command{ fmt.Printf("Key: %s\n", k.Key) }, } + +var expirePreAuthKeyCmd = &cobra.Command{ + Use: "expire", + Short: "Expire a preauthkey", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("missing parameters") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + n, err := cmd.Flags().GetString("namespace") + if err != nil { + log.Fatalf("Error getting namespace: %s", err) + } + o, _ := cmd.Flags().GetString("output") + + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + + k, err := h.GetPreAuthKey(n, args[0]) + if err != nil { + log.Fatalf("Error getting the key: %s", err) + } + + err = h.MarkExpirePreAuthKey(k) + if strings.HasPrefix(o, "json") { + JsonOutput(k, err, o) + return + } + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Expired") + }, +} diff --git a/preauth_keys.go b/preauth_keys.go index 460797c4..25efe64a 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -67,6 +67,19 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error) return &keys, nil } +func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, error) { + pak, err := h.checkKeyValidity(key) + if err != nil { + return nil, err + } + + if pak.Namespace.Name != namespace { + return nil, errors.New("Namespace mismatch") + } + + return pak, nil +} + func (h *Headscale) MarkExpirePreAuthKey(k *PreAuthKey) error { if err := h.db.Model(&k).Update("Expiration", time.Now()).Error; err != nil { return err From 033136cb9a1cace0ec79b6ef7c61fcf6549a972f Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 8 Aug 2021 00:13:44 +0200 Subject: [PATCH 032/125] fixed linting --- preauth_keys.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/preauth_keys.go b/preauth_keys.go index 25efe64a..f462fb56 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -67,6 +67,7 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error) return &keys, nil } +// GetPreAuthKey returns a PreAuthKey for a given key func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, error) { pak, err := h.checkKeyValidity(key) if err != nil { @@ -80,6 +81,7 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er return pak, nil } +// MarkExpirePreauthKey marks a PreAuthKey as expired func (h *Headscale) MarkExpirePreAuthKey(k *PreAuthKey) error { if err := h.db.Model(&k).Update("Expiration", time.Now()).Error; err != nil { return err From 01248997595abf8d8a24c4047a75af322acd07c4 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 8 Aug 2021 00:14:10 +0200 Subject: [PATCH 033/125] fixed linting x 2 --- preauth_keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preauth_keys.go b/preauth_keys.go index f462fb56..cc849fc0 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -81,7 +81,7 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er return pak, nil } -// MarkExpirePreauthKey marks a PreAuthKey as expired +// MarkExpirePreAuthKey marks a PreAuthKey as expired func (h *Headscale) MarkExpirePreAuthKey(k *PreAuthKey) error { if err := h.db.Model(&k).Update("Expiration", time.Now()).Error; err != nil { return err From 149279f3d5ae777dbe5adbfb238767d98ad398f0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:36:25 +0100 Subject: [PATCH 034/125] Add health endpoint Allow us to tell when the server is up and running and can answer requests --- app.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app.go b/app.go index 363b38bc..91605c03 100644 --- a/app.go +++ b/app.go @@ -157,6 +157,7 @@ func (h *Headscale) watchForKVUpdatesWorker() { // Serve launches a GIN server with the Headscale API func (h *Headscale) Serve() error { r := gin.Default() + r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"healthy": "ok"}) }) r.GET("/key", h.KeyHandler) r.GET("/register", h.RegisterWebAPI) r.POST("/machine/:id/map", h.PollNetMapHandler) From 642c7824a787ef1d47627c36219cabb1312b3b63 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:37:04 +0100 Subject: [PATCH 035/125] Add trace log for machine failing to parce ip in toNode --- machine.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/machine.go b/machine.go index f2745c06..4c7e5945 100644 --- a/machine.go +++ b/machine.go @@ -72,6 +72,10 @@ func (m Machine) toNode() (*tailcfg.Node, error) { addrs := []netaddr.IPPrefix{} ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", m.IPAddress)) if err != nil { + log.Trace(). + Str("func", "toNode"). + Str("ip", m.IPAddress). + Msgf("Failed to parse IP Prefix from IP: %s", m.IPAddress) return nil, err } addrs = append(addrs, ip) // missing the ipv6 ? From 91ffd101922dbb495cd8700fbe299bd7da71a1c0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:37:23 +0100 Subject: [PATCH 036/125] Remove "Keys: " from create auth key output This is based on the premis that "the user know what command they executed" and therefor know that the output is the key. This makes the command a lot more useful in scripts. --- 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 eb9d182c..e90aaeb6 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -116,6 +116,6 @@ var createPreAuthKeyCmd = &cobra.Command{ fmt.Println(err) return } - fmt.Printf("Key: %s\n", k.Key) + fmt.Printf("%s\n", k.Key) }, } From d86123195c39667946033f9e3554dc41716847b7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:38:44 +0100 Subject: [PATCH 037/125] Add a dockerignore file to speed up builds and make cachine better --- .dockerignore | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ebf1a2eb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +// integration tests are not needed in docker +// ignoring it let us speed up the integration test +// development +integration_test.go + +Dockerfile* +docker-compose* +.dockerignore +.goreleaser.yml +.git +.github +.gitignore +README.md +LICENSE +.vscode + From a43bb1bb40d73974f1541416b0eabd8e05fcdd04 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:39:39 +0100 Subject: [PATCH 038/125] Improve Dockerfile This commit makes several changes to the dockerfile: - Add go.mod and go.sum in a seperate stage, subsequently calling `go mod download` to make it cache dependencies and speed up builds - Use ubuntu:latest (28MB larger) instead of scratch, makes the image a lot easier to debug (e.g. it has a shell and a package manager) - Change ENTRYPOINT to CMD, this makes the behaviour of the image slightly different from a CLI perspective, but makes interacting with the image from code, docker-compose and kubernetes easier. --- Dockerfile | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index e3fd8efc..0c2af33a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,19 @@ FROM golang:latest AS build ENV GOPATH /go -COPY . /go/src/headscale + +COPY go.mod go.sum /go/src/headscale/ WORKDIR /go/src/headscale +RUN go mod download + +COPY . /go/src/headscale + RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale RUN test -e /go/bin/headscale -FROM scratch -COPY --from=build /go/bin/headscale /go/bin/headscale +FROM ubuntu:latest + +COPY --from=build /go/bin/headscale /usr/local/bin/headscale ENV TZ UTC + EXPOSE 8080/tcp -ENTRYPOINT ["/go/bin/headscale"] +CMD ["headscale"] From f973aef80c53aba07113a0f0592add4903b16fe7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:43:06 +0100 Subject: [PATCH 039/125] Add Dockerfile to build tailscale docker image for integration tests --- Dockerfile.tailscale | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Dockerfile.tailscale diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale new file mode 100644 index 00000000..c6830f8a --- /dev/null +++ b/Dockerfile.tailscale @@ -0,0 +1,9 @@ +FROM ubuntu:latest + +RUN apt-get update \ + && apt-get install -y gnupg curl \ + && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \ + && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ + && apt-get update \ + && apt-get install -y tailscale \ + && rm -rf /var/lib/apt/lists/* From 4e077b053c11a530149b86b3b8543f6f34f00077 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 8 Aug 2021 17:50:32 +0100 Subject: [PATCH 040/125] Initial work, add integration tests This commit adds integration tests to headscale. They are currently quite simple, but it lays the groundwork for more comprehensive testing and ensuring we dont break things with the official tailscale client. The test works by leveraging Docker (via dockertest) to spin up a Headscale container, and a number of tailscale containers (10). Each tailscale container is joined to the headscale and then "passed on" to the tests. Currently three tests have been implemented: - Have all tailscale containers join headscale (in the setup process) - Get IP from each container (I plan to extend this with cross-ping) - List nodes with headscales CLI and verify all has been registered This test depends on Docker, and currently, I have not looked into hooking it into Github Actions. --- .gitignore | 1 + Makefile | 3 + go.mod | 12 ++ go.sum | 89 ++++++++++++ integration_test.go | 246 +++++++++++++++++++++++++++++++++ integration_test/etc/derp.yaml | 0 6 files changed, 351 insertions(+) create mode 100644 integration_test.go create mode 100755 integration_test/etc/derp.yaml diff --git a/.gitignore b/.gitignore index ff4f6664..3a64648f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ config.json *.key /db.sqlite +*.sqlite3 diff --git a/Makefile b/Makefile index 18498462..65cba826 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ dev: lint test build test: @go test -coverprofile=coverage.out ./... +test_integration: + go test -tags integration ./... + coverprofile_func: go tool cover -func=coverage.out diff --git a/go.mod b/go.mod index 1bfb4bbd..1d0dc0c9 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,29 @@ go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.0.5 + github.com/Microsoft/go-winio v0.5.0 // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.1.1 // indirect + github.com/containerd/continuity v0.1.0 // indirect + 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 // indirect github.com/gin-gonic/gin v1.7.2 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.1 github.com/lib/pq v1.10.2 // indirect github.com/mattn/go-sqlite3 v1.14.7 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/opencontainers/runc v1.0.1 // indirect + github.com/ory/dockertest/v3 v3.7.0 // indirect github.com/rs/zerolog v1.23.0 // indirect github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 gorm.io/datatypes v1.0.1 diff --git a/go.sum b/go.sum index 1f3779b4..6431570d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ 4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -40,6 +41,9 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.0.5 h1:xpZp+Q55wi5C7Iaze+40onHnEkex1jSc34CltJjOoPM= github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= @@ -55,8 +59,12 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -79,6 +87,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= @@ -91,20 +100,28 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -114,7 +131,12 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -130,6 +152,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4= github.com/daixiang0/gci v0.2.7/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -140,7 +164,18 @@ github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/cli v20.10.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.8+incompatible h1:/zO/6y9IOpcehE49yMRTV9ea0nBpb8OeqSskXLNfH1E= +github.com/docker/cli v20.10.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM= +github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= @@ -225,6 +260,7 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -314,6 +350,8 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1/go.mod h1:+y9lKiqDhR4zkLl+V9h4q0rdyrYVsWWm6LLCQP33DIk= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -328,6 +366,7 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= @@ -376,6 +415,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= @@ -502,6 +543,7 @@ github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2G github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -514,6 +556,7 @@ github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQ github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= @@ -580,6 +623,10 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -588,6 +635,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= @@ -614,6 +662,16 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.1 h1:G18PGckGdAm3yVQRWDVQ1rLSLntiniKJ0cNRT2Tm5gs= +github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -622,6 +680,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/ory/dockertest/v3 v3.7.0 h1:Bijzonc69Ont3OU0a3TWKJ1Rzlh3TsDXP1JrTAkSmsM= +github.com/ory/dockertest/v3 v3.7.0/go.mod h1:PvCCgnP7AfBZeVrzwiUTjZx/IUXlGLC1zQlUQrLIlUE= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= @@ -696,6 +756,7 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -713,6 +774,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -731,6 +794,7 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= @@ -741,6 +805,7 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= @@ -763,6 +828,7 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI= @@ -780,6 +846,7 @@ github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d/go.mod h1:yiFB github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0= github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -794,10 +861,20 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -915,6 +992,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -952,6 +1030,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -995,6 +1075,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1006,6 +1087,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1026,7 +1108,9 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1052,11 +1136,14 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= @@ -1096,6 +1183,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1334,6 +1422,7 @@ gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.11 h1:CxkXW6Cc+VIBlL8yJEHq+Co4RYXdSLiMKNvgoZPjLK4= gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 00000000..a4caf87d --- /dev/null +++ b/integration_test.go @@ -0,0 +1,246 @@ +// +build integration + +package headscale + +import ( + "bytes" + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "inet.af/netaddr" + + "gopkg.in/check.v1" +) + +var _ = check.Suite(&IntegrationSuite{}) + +type IntegrationSuite struct{} + +var integrationTmpDir string +var ih Headscale + +var pool dockertest.Pool +var network dockertest.Network +var headscale dockertest.Resource +var tailscaleCount int = 10 +var tailscales map[string]dockertest.Resource + +func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode, err := resource.Exec( + cmd, + dockertest.ExecOptions{ + StdOut: &stdout, + StdErr: &stderr, + }, + ) + if err != nil { + return "", err + } + + if exitCode != 0 { + fmt.Println("Command: ", cmd) + fmt.Println("stdout: ", stdout.String()) + fmt.Println("stderr: ", stderr.String()) + return "", fmt.Errorf("command failed with: %s", stderr.String()) + } + + return stdout.String(), nil +} + +func dockerRestartPolicy(config *docker.HostConfig) { + // set AutoRemove to true so that stopped container goes away by itself + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{ + Name: "no", + } +} + +func (s *IntegrationSuite) SetUpSuite(c *check.C) { + var err error + h = Headscale{ + dbType: "sqlite3", + dbString: "integration_test_db.sqlite3", + } + + if ppool, err := dockertest.NewPool(""); err == nil { + pool = *ppool + } else { + log.Fatalf("Could not connect to docker: %s", err) + } + + if pnetwork, err := pool.CreateNetwork("headscale-test"); err == nil { + network = *pnetwork + } else { + log.Fatalf("Could not create network: %s", err) + } + + headscaleBuildOptions := &dockertest.BuildOptions{ + Dockerfile: "Dockerfile", + ContextDir: ".", + } + + tailscaleBuildOptions := &dockertest.BuildOptions{ + Dockerfile: "Dockerfile.tailscale", + ContextDir: ".", + } + + currentPath, err := os.Getwd() + if err != nil { + log.Fatalf("Could not determine current path: %s", err) + } + + headscaleOptions := &dockertest.RunOptions{ + Name: "headscale", + Mounts: []string{ + fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath), + fmt.Sprintf("%s/derp.yaml:/etc/headscale/derp.yaml", currentPath), + }, + Networks: []*dockertest.Network{&network}, + // Cmd: []string{"sleep", "3600"}, + Cmd: []string{"headscale", "serve"}, + PortBindings: map[docker.Port][]docker.PortBinding{ + "8080/tcp": []docker.PortBinding{{HostPort: "8080"}}, + }, + Env: []string{}, + } + + fmt.Println("Creating headscale container") + if pheadscale, err := pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, dockerRestartPolicy); err == nil { + headscale = *pheadscale + } else { + log.Fatalf("Could not start resource: %s", err) + } + fmt.Println("Created headscale container") + + fmt.Println("Creating tailscale containers") + tailscales = make(map[string]dockertest.Resource) + for i := 0; i < tailscaleCount; i++ { + hostname := fmt.Sprintf("tailscale%d", i) + tailscaleOptions := &dockertest.RunOptions{ + Name: hostname, + Networks: []*dockertest.Network{&network}, + // Make the container run until killed + // Cmd: []string{"sleep", "3600"}, + Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"}, + Env: []string{}, + } + + if pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy); err == nil { + tailscales[hostname] = *pts + } else { + log.Fatalf("Could not start resource: %s", err) + } + fmt.Printf("Created %s container\n", hostname) + } + + fmt.Println("Waiting for headscale to be ready") + hostEndpoint := fmt.Sprintf("localhost:%s", headscale.GetPort("8080/tcp")) + + if err := pool.Retry(func() error { + url := fmt.Sprintf("http://%s/health", hostEndpoint) + resp, err := http.Get(url) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status code not OK") + } + return nil + }); err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + fmt.Println("headscale container is ready") + + fmt.Println("Creating headscale namespace") + result, err := executeCommand( + &headscale, + []string{"headscale", "namespaces", "create", "test"}, + ) + c.Assert(err, check.IsNil) + + fmt.Println("Creating pre auth key") + authKey, err := executeCommand( + &headscale, + []string{"headscale", "-n", "test", "preauthkeys", "create", "--reusable", "--expiration", "24h"}, + ) + c.Assert(err, check.IsNil) + + headscaleEndpoint := fmt.Sprintf("http://headscale:%s", headscale.GetPort("8080/tcp")) + + fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint) + for hostname, tailscale := range tailscales { + command := []string{"tailscale", "up", "-login-server", headscaleEndpoint, "--authkey", strings.TrimSuffix(authKey, "\n"), "--hostname", headscaleEndpoint} + + fmt.Println("Join command:", command) + fmt.Printf("Running join command for %s\n", hostname) + result, err = executeCommand( + &tailscale, + command, + ) + fmt.Println("tailscale result: ", result) + c.Assert(err, check.IsNil) + fmt.Printf("%s joined\n", hostname) + } +} + +func (s *IntegrationSuite) TearDownSuite(c *check.C) { + if err := pool.Purge(&headscale); err != nil { + log.Printf("Could not purge resource: %s\n", err) + } + + for _, tailscale := range tailscales { + if err := pool.Purge(&tailscale); err != nil { + log.Printf("Could not purge resource: %s\n", err) + } + } + + if err := network.Close(); err != nil { + log.Printf("Could not close network: %s\n", err) + } +} + +func (s *IntegrationSuite) TestListNodes(c *check.C) { + fmt.Println("Listing nodes") + result, err := executeCommand( + &headscale, + []string{"headscale", "-n", "test", "nodes", "list"}, + ) + c.Assert(err, check.IsNil) + + for hostname, _ := range tailscales { + c.Assert(strings.Contains(result, hostname), check.Equals, true) + } +} + +func (s *IntegrationSuite) TestGetIpAddresses(c *check.C) { + ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10") + ips := make(map[string]netaddr.IP) + for hostname, tailscale := range tailscales { + command := []string{"tailscale", "ip"} + + result, err := executeCommand( + &tailscale, + command, + ) + c.Assert(err, check.IsNil) + + ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) + c.Assert(err, check.IsNil) + + fmt.Printf("IP for %s: %s", hostname, result) + + // c.Assert(ip.Valid(), check.IsTrue) + c.Assert(ip.Is4(), check.Equals, true) + c.Assert(ipPrefix.Contains(ip), check.Equals, true) + + ips[hostname] = ip + } +} diff --git a/integration_test/etc/derp.yaml b/integration_test/etc/derp.yaml new file mode 100755 index 00000000..e69de29b From 7141e2ed70aaa8e2350a28007c18f0a2c51b266c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Aug 2021 17:12:39 +0100 Subject: [PATCH 041/125] Fix hostname passed to join command --- integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.go b/integration_test.go index a4caf87d..e1e42408 100644 --- a/integration_test.go +++ b/integration_test.go @@ -177,7 +177,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint) for hostname, tailscale := range tailscales { - command := []string{"tailscale", "up", "-login-server", headscaleEndpoint, "--authkey", strings.TrimSuffix(authKey, "\n"), "--hostname", headscaleEndpoint} + command := []string{"tailscale", "up", "-login-server", headscaleEndpoint, "--authkey", strings.TrimSuffix(authKey, "\n"), "--hostname", hostname} fmt.Println("Join command:", command) fmt.Printf("Running join command for %s\n", hostname) From 54da1a4155d084de9392a57d349084ebaeba56d1 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 12 Aug 2021 07:05:26 +0100 Subject: [PATCH 042/125] Commit the correct integration etc files --- integration_test/.gitignore | 3 +++ integration_test/etc/config.json | 11 +++++++++++ integration_test/etc/derp.yaml | 0 integration_test/etc/private.key | 1 + 4 files changed, 15 insertions(+) create mode 100644 integration_test/.gitignore create mode 100644 integration_test/etc/config.json delete mode 100755 integration_test/etc/derp.yaml create mode 100644 integration_test/etc/private.key diff --git a/integration_test/.gitignore b/integration_test/.gitignore new file mode 100644 index 00000000..4e9cb7a1 --- /dev/null +++ b/integration_test/.gitignore @@ -0,0 +1,3 @@ +derp.yaml +*.sqlite +*.sqlite3 diff --git a/integration_test/etc/config.json b/integration_test/etc/config.json new file mode 100644 index 00000000..5454f2f7 --- /dev/null +++ b/integration_test/etc/config.json @@ -0,0 +1,11 @@ +{ + "server_url": "http://headscale:8080", + "listen_addr": "0.0.0.0:8080", + "private_key_path": "private.key", + "derp_map_path": "derp.yaml", + "ephemeral_node_inactivity_timeout": "30m", + "db_type": "sqlite3", + "db_path": "/tmp/integration_test_db.sqlite3", + "acl_policy_path": "", + "log_level": "trace" +} diff --git a/integration_test/etc/derp.yaml b/integration_test/etc/derp.yaml deleted file mode 100755 index e69de29b..00000000 diff --git a/integration_test/etc/private.key b/integration_test/etc/private.key new file mode 100644 index 00000000..b3a3ae6d --- /dev/null +++ b/integration_test/etc/private.key @@ -0,0 +1 @@ +SEmQwCu+tGywQWEUsf93TpTRUvlB7WhnCdHgWrSXjEA= From 0e1ddf9715aab887cb6a0a34f4f5264594f192ad Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 12 Aug 2021 07:36:38 +0100 Subject: [PATCH 043/125] Set longer timeout for integration tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 65cba826..7ffe1f9c 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: @go test -coverprofile=coverage.out ./... test_integration: - go test -tags integration ./... + go test -tags integration -timeout 30m ./... coverprofile_func: go tool cover -func=coverage.out From 4c849539fc672b72e9e992db017f0bab2106020e Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 12 Aug 2021 21:44:12 +0200 Subject: [PATCH 044/125] Expire the ephemeral nodes in the Serve method --- app.go | 1 + cmd/headscale/cli/server.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 91605c03..c5577917 100644 --- a/app.go +++ b/app.go @@ -165,6 +165,7 @@ func (h *Headscale) Serve() error { var err error go h.watchForKVUpdates(5000) + go h.ExpireEphemeralNodes(5000) if h.cfg.TLSLetsEncryptHostname != "" { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { diff --git a/cmd/headscale/cli/server.go b/cmd/headscale/cli/server.go index 1f8db6a2..6d9ad194 100644 --- a/cmd/headscale/cli/server.go +++ b/cmd/headscale/cli/server.go @@ -21,7 +21,7 @@ var serveCmd = &cobra.Command{ if err != nil { log.Fatalf("Error initializing: %s", err) } - go h.ExpireEphemeralNodes(5000) + err = h.Serve() if err != nil { log.Fatalf("Error initializing: %s", err) From c1e61578475f392e1fbe30236c2837e10e63fedf Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 12 Aug 2021 21:45:40 +0200 Subject: [PATCH 045/125] Expire ephemeral is internal --- app.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index c5577917..f58cccc4 100644 --- a/app.go +++ b/app.go @@ -107,9 +107,9 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, target, http.StatusFound) } -// ExpireEphemeralNodes deletes ephemeral machine records that have not been +// expireEphemeralNodes deletes ephemeral machine records that have not been // seen for longer than h.cfg.EphemeralNodeInactivityTimeout -func (h *Headscale) ExpireEphemeralNodes(milliSeconds int64) { +func (h *Headscale) expireEphemeralNodes(milliSeconds int64) { ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) for range ticker.C { h.expireEphemeralNodesWorker() @@ -165,7 +165,7 @@ func (h *Headscale) Serve() error { var err error go h.watchForKVUpdates(5000) - go h.ExpireEphemeralNodes(5000) + go h.expireEphemeralNodes(5000) if h.cfg.TLSLetsEncryptHostname != "" { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { From ab61c877019475d59fe0c8437cea539fca63af94 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 12 Aug 2021 21:53:37 +0200 Subject: [PATCH 046/125] Also notify peers when deleting ephemerals --- app.go | 1 + machine.go | 23 +++++++++++++++++++++++ namespaces.go | 20 +------------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app.go b/app.go index f58cccc4..fcf287f7 100644 --- a/app.go +++ b/app.go @@ -135,6 +135,7 @@ func (h *Headscale) expireEphemeralNodesWorker() { if err != nil { log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") } + h.notifyChangesToPeers(&m) } } } diff --git a/machine.go b/machine.go index 4c7e5945..81208a83 100644 --- a/machine.go +++ b/machine.go @@ -238,3 +238,26 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { } return &hostinfo, nil } + +func (h *Headscale) notifyChangesToPeers(m *Machine) error { + peers, _ := h.getPeers(*m) + for _, p := range *peers { + pUp, ok := h.clientsPolling.Load(uint64(p.ID)) + if ok { + log.Info(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", m.Name). + Str("address", p.Addresses[0].String()). + Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) + pUp.(chan []byte) <- []byte{} + } else { + log.Info(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", m.Name). + Msgf("Peer %s does not appear to be polling", p.Name) + } + } + return nil +} diff --git a/namespaces.go b/namespaces.go index 9b8d1904..ff9eeacc 100644 --- a/namespaces.go +++ b/namespaces.go @@ -169,25 +169,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { continue } for _, m := range *machines { - peers, _ := h.getPeers(m) - for _, p := range *peers { - pUp, ok := h.clientsPolling.Load(uint64(p.ID)) - if ok { - log.Info(). - Str("func", "checkForNamespacesPendingUpdates"). - Str("machine", m.Name). - Str("peer", m.Name). - Str("address", p.Addresses[0].String()). - Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - pUp.(chan []byte) <- []byte{} - } else { - log.Info(). - Str("func", "checkForNamespacesPendingUpdates"). - Str("machine", m.Name). - Str("peer", m.Name). - Msgf("Peer %s does not appear to be polling", p.Name) - } - } + h.notifyChangesToPeers(&m) } } newV, err := h.getValue("namespaces_pending_updates") From 8eb7d47072cb2c39ba9595668688f24b1f561d86 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 12 Aug 2021 21:57:20 +0200 Subject: [PATCH 047/125] Fixed linting --- app.go | 5 ++++- namespaces.go | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index fcf287f7..34fb77b1 100644 --- a/app.go +++ b/app.go @@ -135,7 +135,10 @@ func (h *Headscale) expireEphemeralNodesWorker() { if err != nil { log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") } - h.notifyChangesToPeers(&m) + err = h.notifyChangesToPeers(&m) + if err != nil { + continue + } } } } diff --git a/namespaces.go b/namespaces.go index ff9eeacc..1bf8c2df 100644 --- a/namespaces.go +++ b/namespaces.go @@ -169,7 +169,10 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { continue } for _, m := range *machines { - h.notifyChangesToPeers(&m) + err = h.notifyChangesToPeers(&m) + if err != nil { + continue + } } } newV, err := h.getValue("namespaces_pending_updates") From 9698abbfd5ae19d3a49bb876b8cffa0a7a746790 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 13 Aug 2021 10:33:19 +0100 Subject: [PATCH 048/125] Resolve merge conflict --- .dockerignore | 1 + api.go | 125 +++++++++----------------------------- app.go | 5 +- go.mod | 1 + go.sum | 1 + integration_test.go | 145 +++++++++++++++++++++++++++++++++++--------- machine.go | 12 +++- namespaces.go | 5 +- routes.go | 7 +-- utils.go | 17 ++++++ 10 files changed, 180 insertions(+), 139 deletions(-) diff --git a/.dockerignore b/.dockerignore index ebf1a2eb..f90134b3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ // ignoring it let us speed up the integration test // development integration_test.go +integration_test/ Dockerfile* docker-compose* diff --git a/api.go b/api.go index 820e3f48..0dc2bec5 100644 --- a/api.go +++ b/api.go @@ -286,21 +286,6 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { } h.db.Save(&m) - update := make(chan []byte, 1) - - pollData := make(chan []byte, 1) - defer close(pollData) - - cancelKeepAlive := make(chan []byte, 1) - defer close(cancelKeepAlive) - - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Storing update channel") - h.clientsPolling.Store(m.ID, update) - data, err := h.getMapResponse(mKey, req, m) if err != nil { log.Error(). @@ -351,6 +336,23 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { return } + // Only create update channel if it has not been created + var update chan []byte + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Creating or loading update channel") + if result, ok := h.clientsPolling.LoadOrStore(m.ID, make(chan []byte, 1)); ok { + update = result.(chan []byte) + } + + pollData := make(chan []byte, 1) + defer close(pollData) + + cancelKeepAlive := make(chan []byte, 1) + defer close(cancelKeepAlive) + log.Info(). Str("handler", "PollNetMap"). Str("machine", m.Name). @@ -365,87 +367,15 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("handler", "PollNetMap"). Str("machine", m.Name). Msg("Notifying peers") - peers, _ := h.getPeers(m) - for _, p := range *peers { - pUp, ok := h.clientsPolling.Load(uint64(p.ID)) - if ok { - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Str("peer", m.Name). - Str("address", p.Addresses[0].String()). - Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - pUp.(chan []byte) <- []byte{} - } else { - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Str("peer", m.Name). - Msgf("Peer %s does not appear to be polling", p.Name) - } - } + // TODO: Why does this block? + go h.notifyChangesToPeers(&m) - go h.keepAlive(cancelKeepAlive, pollData, mKey, req, m) - - c.Stream(func(w io.Writer) bool { - select { - case data := <-pollData: - log.Trace(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Int("bytes", len(data)). - Msg("Sending data") - _, err := w.Write(data) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Err(err). - Msg("Cannot write data") - } - now := time.Now().UTC() - m.LastSeen = &now - h.db.Save(&m) - return true - - case <-update: - log.Debug(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Received a request for update") - data, err := h.getMapResponse(mKey, req, m) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Err(err). - Msg("Could not get the map update") - } - _, err = w.Write(*data) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Err(err). - Msg("Could not write the map response") - } - return true - - case <-c.Request.Context().Done(): - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("The client has closed the connection") - now := time.Now().UTC() - m.LastSeen = &now - h.db.Save(&m) - cancelKeepAlive <- []byte{} - h.clientsPolling.Delete(m.ID) - close(update) - return false - - } - }) + h.PollNetMapStream(c, m, req, mKey, pollData, update, cancelKeepAlive) + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Finished stream, closing PollNetMap session") } func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgkey.Key, req tailcfg.MapRequest, m Machine) { @@ -514,10 +444,15 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac DERPMap: h.cfg.DerpMap, UserProfiles: []tailcfg.UserProfile{profile}, } + log.Trace(). + Str("func", "getMapResponse"). + Str("machine", req.Hostinfo.Hostname). + Msgf("Generated map response: %s", tailMapResponseToString(resp)) var respBody []byte if req.Compress == "zstd" { src, _ := json.Marshal(resp) + encoder, _ := zstd.NewWriter(nil) srcCompressed := encoder.EncodeAll(src, nil) respBody, err = encodeMsg(srcCompressed, &mKey, h.privateKey) diff --git a/app.go b/app.go index 34fb77b1..fcf287f7 100644 --- a/app.go +++ b/app.go @@ -135,10 +135,7 @@ func (h *Headscale) expireEphemeralNodesWorker() { if err != nil { log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") } - err = h.notifyChangesToPeers(&m) - if err != nil { - continue - } + h.notifyChangesToPeers(&m) } } } diff --git a/go.mod b/go.mod index 1d0dc0c9..0a38b5d3 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/rs/zerolog v1.23.0 // indirect github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 + github.com/stretchr/testify v1.7.0 // indirect github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e diff --git a/go.sum b/go.sum index 6431570d..95d40a28 100644 --- a/go.sum +++ b/go.sum @@ -817,6 +817,7 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/integration_test.go b/integration_test.go index e1e42408..c920a7e9 100644 --- a/integration_test.go +++ b/integration_test.go @@ -9,17 +9,24 @@ import ( "net/http" "os" "strings" + "testing" + "time" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" - "inet.af/netaddr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" - "gopkg.in/check.v1" + "inet.af/netaddr" ) -var _ = check.Suite(&IntegrationSuite{}) +type IntegrationTestSuite struct { + suite.Suite +} -type IntegrationSuite struct{} +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} var integrationTmpDir string var ih Headscale @@ -27,7 +34,7 @@ var ih Headscale var pool dockertest.Pool var network dockertest.Network var headscale dockertest.Resource -var tailscaleCount int = 10 +var tailscaleCount int = 5 var tailscales map[string]dockertest.Resource func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) { @@ -63,7 +70,7 @@ func dockerRestartPolicy(config *docker.HostConfig) { } } -func (s *IntegrationSuite) SetUpSuite(c *check.C) { +func (s *IntegrationTestSuite) SetupSuite() { var err error h = Headscale{ dbType: "sqlite3", @@ -104,8 +111,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { fmt.Sprintf("%s/derp.yaml:/etc/headscale/derp.yaml", currentPath), }, Networks: []*dockertest.Network{&network}, - // Cmd: []string{"sleep", "3600"}, - Cmd: []string{"headscale", "serve"}, + Cmd: []string{"headscale", "serve"}, PortBindings: map[docker.Port][]docker.PortBinding{ "8080/tcp": []docker.PortBinding{{HostPort: "8080"}}, }, @@ -127,10 +133,8 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { tailscaleOptions := &dockertest.RunOptions{ Name: hostname, Networks: []*dockertest.Network{&network}, - // Make the container run until killed - // Cmd: []string{"sleep", "3600"}, - Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"}, - Env: []string{}, + Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"}, + Env: []string{}, } if pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy); err == nil { @@ -141,6 +145,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { fmt.Printf("Created %s container\n", hostname) } + // TODO: Replace this logic with something that can be detected on Github Actions fmt.Println("Waiting for headscale to be ready") hostEndpoint := fmt.Sprintf("localhost:%s", headscale.GetPort("8080/tcp")) @@ -164,14 +169,14 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { &headscale, []string{"headscale", "namespaces", "create", "test"}, ) - c.Assert(err, check.IsNil) + assert.Nil(s.T(), err) fmt.Println("Creating pre auth key") authKey, err := executeCommand( &headscale, []string{"headscale", "-n", "test", "preauthkeys", "create", "--reusable", "--expiration", "24h"}, ) - c.Assert(err, check.IsNil) + assert.Nil(s.T(), err) headscaleEndpoint := fmt.Sprintf("http://headscale:%s", headscale.GetPort("8080/tcp")) @@ -186,12 +191,16 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { command, ) fmt.Println("tailscale result: ", result) - c.Assert(err, check.IsNil) + assert.Nil(s.T(), err) fmt.Printf("%s joined\n", hostname) } + + // The nodes need a bit of time to get their updated maps from headscale + // TODO: See if we can have a more deterministic wait here. + time.Sleep(20 * time.Second) } -func (s *IntegrationSuite) TearDownSuite(c *check.C) { +func (s *IntegrationTestSuite) TearDownSuite() { if err := pool.Purge(&headscale); err != nil { log.Printf("Could not purge resource: %s\n", err) } @@ -207,21 +216,102 @@ func (s *IntegrationSuite) TearDownSuite(c *check.C) { } } -func (s *IntegrationSuite) TestListNodes(c *check.C) { +func (s *IntegrationTestSuite) TestListNodes() { fmt.Println("Listing nodes") result, err := executeCommand( &headscale, []string{"headscale", "-n", "test", "nodes", "list"}, ) - c.Assert(err, check.IsNil) + assert.Nil(s.T(), err) + + fmt.Printf("List nodes: \n%s\n", result) + + // Chck that the correct count of host is present in node list + lines := strings.Split(result, "\n") + assert.Equal(s.T(), len(tailscales), len(lines)-2) for hostname, _ := range tailscales { - c.Assert(strings.Contains(result, hostname), check.Equals, true) + assert.Contains(s.T(), result, hostname) } } -func (s *IntegrationSuite) TestGetIpAddresses(c *check.C) { +func (s *IntegrationTestSuite) TestGetIpAddresses() { ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10") + ips, err := getIPs() + assert.Nil(s.T(), err) + + for hostname, _ := range tailscales { + s.T().Run(hostname, func(t *testing.T) { + ip := ips[hostname] + + fmt.Printf("IP for %s: %s\n", hostname, ip) + + // c.Assert(ip.Valid(), check.IsTrue) + assert.True(t, ip.Is4()) + assert.True(t, ipPrefix.Contains(ip)) + + ips[hostname] = ip + }) + } +} + +func (s *IntegrationTestSuite) TestStatus() { + ips, err := getIPs() + assert.Nil(s.T(), err) + + for hostname, tailscale := range tailscales { + s.T().Run(hostname, func(t *testing.T) { + command := []string{"tailscale", "status"} + + fmt.Printf("Getting status for %s\n", hostname) + result, err := executeCommand( + &tailscale, + command, + ) + assert.Nil(t, err) + // fmt.Printf("Status for %s: %s", hostname, result) + + // Check if we have as many nodes in status + // as we have IPs/tailscales + lines := strings.Split(result, "\n") + assert.Equal(t, len(ips), len(lines)-1) + assert.Equal(t, len(tailscales), len(lines)-1) + + // Check that all hosts is present in all hosts status + for ipHostname, ip := range ips { + assert.Contains(t, result, ip.String()) + assert.Contains(t, result, ipHostname) + } + }) + } +} + +// func (s *IntegrationTestSuite) TestPingAllPeers() { +// ips, err := getIPs() +// assert.Nil(s.T(), err) +// +// for hostname, tailscale := range tailscales { +// for peername, ip := range ips { +// s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { +// // We currently cant ping ourselves, so skip that. +// if peername != hostname { +// command := []string{"tailscale", "ping", "--timeout=1s", "--c=1", ip.String()} +// +// fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[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") +// } +// }) +// } +// } +// } + +func getIPs() (map[string]netaddr.IP, error) { ips := make(map[string]netaddr.IP) for hostname, tailscale := range tailscales { command := []string{"tailscale", "ip"} @@ -230,17 +320,16 @@ func (s *IntegrationSuite) TestGetIpAddresses(c *check.C) { &tailscale, command, ) - c.Assert(err, check.IsNil) + if err != nil { + return nil, err + } ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) - c.Assert(err, check.IsNil) - - fmt.Printf("IP for %s: %s", hostname, result) - - // c.Assert(ip.Valid(), check.IsTrue) - c.Assert(ip.Is4(), check.Equals, true) - c.Assert(ipPrefix.Contains(ip), check.Equals, true) + if err != nil { + return nil, err + } ips[hostname] = ip } + return ips, nil } diff --git a/machine.go b/machine.go index 81208a83..69de453d 100644 --- a/machine.go +++ b/machine.go @@ -159,6 +159,10 @@ func (m Machine) toNode() (*tailcfg.Node, error) { } func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { + log.Trace(). + Str("func", "getPeers"). + Str("machine", m.Name). + Msg("Finding peers") machines := []Machine{} if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { @@ -175,6 +179,11 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { peers = append(peers, peer) } sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) + + log.Trace(). + Str("func", "getPeers"). + Str("machine", m.Name). + Msgf("Found peers: %s", tailNodesToString(peers)) return &peers, nil } @@ -239,7 +248,7 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { return &hostinfo, nil } -func (h *Headscale) notifyChangesToPeers(m *Machine) error { +func (h *Headscale) notifyChangesToPeers(m *Machine) { peers, _ := h.getPeers(*m) for _, p := range *peers { pUp, ok := h.clientsPolling.Load(uint64(p.ID)) @@ -259,5 +268,4 @@ func (h *Headscale) notifyChangesToPeers(m *Machine) error { Msgf("Peer %s does not appear to be polling", p.Name) } } - return nil } diff --git a/namespaces.go b/namespaces.go index 1bf8c2df..ff9eeacc 100644 --- a/namespaces.go +++ b/namespaces.go @@ -169,10 +169,7 @@ func (h *Headscale) checkForNamespacesPendingUpdates() { continue } for _, m := range *machines { - err = h.notifyChangesToPeers(&m) - if err != nil { - continue - } + h.notifyChangesToPeers(&m) } } newV, err := h.getValue("namespaces_pending_updates") diff --git a/routes.go b/routes.go index 7c83436e..3339634c 100644 --- a/routes.go +++ b/routes.go @@ -49,12 +49,7 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr // The peers map is stored in memory in the server process. // Definitely not accessible from the CLI tool. // We need RPC to the server - or some kind of 'needsUpdate' field in the DB - peers, _ := h.getPeers(*m) - for _, p := range *peers { - if pUp, ok := h.clientsPolling.Load(uint64(p.ID)); ok { - pUp.(chan []byte) <- []byte{} - } - } + h.notifyChangesToPeers(m) return &rIP, nil } } diff --git a/utils.go b/utils.go index 03dc673f..cbe1d870 100644 --- a/utils.go +++ b/utils.go @@ -10,9 +10,11 @@ import ( "encoding/json" "fmt" "io" + "strings" "golang.org/x/crypto/nacl/box" "inet.af/netaddr" + "tailscale.com/tailcfg" "tailscale.com/types/wgkey" ) @@ -58,6 +60,7 @@ func encode(v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, e if err != nil { return nil, err } + return encodeMsg(b, pubKey, privKey) } @@ -139,3 +142,17 @@ func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool { return false } + +func tailNodesToString(nodes []*tailcfg.Node) string { + temp := make([]string, len(nodes)) + + for index, node := range nodes { + temp[index] = node.Name + } + + return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp)) +} + +func tailMapResponseToString(resp tailcfg.MapResponse) string { + return fmt.Sprintf("{ Node: %s, Peers: %s }", resp.Node.Name, tailNodesToString(resp.Peers)) +} From 700382cba492fef8e1d923e4ae4343589463b26f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 13 Aug 2021 10:33:50 +0100 Subject: [PATCH 049/125] Split stream part of pollhandlermap into its own func --- poll.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 poll.go diff --git a/poll.go b/poll.go new file mode 100644 index 00000000..f0bfe706 --- /dev/null +++ b/poll.go @@ -0,0 +1,98 @@ +package headscale + +import ( + "io" + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" + "tailscale.com/types/wgkey" +) + +func (h *Headscale) PollNetMapStream( + c *gin.Context, + m Machine, + req tailcfg.MapRequest, + mKey wgkey.Key, + pollData chan []byte, + update chan []byte, + cancelKeepAlive chan []byte, +) { + + go h.keepAlive(cancelKeepAlive, pollData, mKey, req, m) + + c.Stream(func(w io.Writer) bool { + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Msg("Waiting for data to stream...") + select { + + case data := <-pollData: + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Int("bytes", len(data)). + Msg("Sending data received via pollData channel") + _, err := w.Write(data) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Err(err). + Msg("Cannot write data") + } + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Int("bytes", len(data)). + Msg("Data from pollData channel written successfully") + now := time.Now().UTC() + m.LastSeen = &now + h.db.Save(&m) + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Int("bytes", len(data)). + Msg("Machine updated successfully after sending pollData") + return true + + case <-update: + log.Debug(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Msg("Received a request for update") + data, err := h.getMapResponse(mKey, req, m) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Err(err). + Msg("Could not get the map update") + } + _, err = w.Write(*data) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Err(err). + Msg("Could not write the map response") + } + return true + + case <-c.Request.Context().Done(): + log.Info(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Msg("The client has closed the connection") + now := time.Now().UTC() + m.LastSeen = &now + h.db.Save(&m) + cancelKeepAlive <- []byte{} + h.clientsPolling.Delete(m.ID) + close(update) + return false + } + }) +} From a8d9fdce3c6e2bf4966841f46c9fa8fe04d1e0e8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 13 Aug 2021 11:01:23 +0100 Subject: [PATCH 050/125] Uncomment ping test --- integration_test.go | 48 ++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/integration_test.go b/integration_test.go index c920a7e9..dd96fb82 100644 --- a/integration_test.go +++ b/integration_test.go @@ -286,30 +286,30 @@ func (s *IntegrationTestSuite) TestStatus() { } } -// func (s *IntegrationTestSuite) TestPingAllPeers() { -// ips, err := getIPs() -// assert.Nil(s.T(), err) -// -// for hostname, tailscale := range tailscales { -// for peername, ip := range ips { -// s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { -// // We currently cant ping ourselves, so skip that. -// if peername != hostname { -// command := []string{"tailscale", "ping", "--timeout=1s", "--c=1", ip.String()} -// -// fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[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") -// } -// }) -// } -// } -// } +func (s *IntegrationTestSuite) TestPingAllPeers() { + ips, err := getIPs() + assert.Nil(s.T(), err) + + for hostname, tailscale := range tailscales { + for peername, ip := range ips { + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + // We currently cant ping ourselves, so skip that. + if peername != hostname { + command := []string{"tailscale", "ping", "--timeout=1s", "--c=1", ip.String()} + + fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[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") + } + }) + } + } +} func getIPs() (map[string]netaddr.IP, error) { ips := make(map[string]netaddr.IP) From 036061664ef1f1f97058a65eba1d97a82ef2cd20 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 13 Aug 2021 16:12:01 +0100 Subject: [PATCH 051/125] initial integration test file --- .github/workflows/test-integration.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/test-integration.yml diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 00000000..e939df22 --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,23 @@ +name: CI + +on: [pull_request] + +jobs: + # The "build" workflow + integration-test: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Setup Go + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: "1.16.3" + + - name: Run Integration tests + run: go test -tags integration -timeout 30m From 7d1a5c00a099d103103be9312823a8f3651d61b7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 13 Aug 2021 16:56:28 +0100 Subject: [PATCH 052/125] Try with longer timeout --- integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.go b/integration_test.go index dd96fb82..4c1c54b4 100644 --- a/integration_test.go +++ b/integration_test.go @@ -295,7 +295,7 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { // We currently cant ping ourselves, so skip that. if peername != hostname { - command := []string{"tailscale", "ping", "--timeout=1s", "--c=1", ip.String()} + command := []string{"tailscale", "ping", "--timeout=5s", "--c=1", ip.String()} fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) result, err := executeCommand( From 55fe5b0b41b8b722a8c7cb6f1b72412ef6d0c691 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 15 Aug 2021 23:10:39 +0200 Subject: [PATCH 053/125] Use pterm table in node list --- cmd/headscale/cli/nodes.go | 53 ++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 75000434..fdcaf760 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -8,7 +8,11 @@ import ( "time" survey "github.com/AlecAivazis/survey/v2" + "github.com/juanfont/headscale" + "github.com/pterm/pterm" "github.com/spf13/cobra" + "tailscale.com/tailcfg" + "tailscale.com/types/wgkey" ) func init() { @@ -33,7 +37,7 @@ var registerNodeCmd = &cobra.Command{ Short: "Registers a machine to your network", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return fmt.Errorf("Missing parameters") + return fmt.Errorf("missing parameters") } return nil }, @@ -85,19 +89,13 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error getting nodes: %s", err) } - fmt.Printf("ID\tname\t\tlast seen\t\tephemeral\n") - for _, m := range *machines { - var ephemeral bool - if m.AuthKey != nil && m.AuthKey.Ephemeral { - ephemeral = true - } - var lastSeen time.Time - if m.LastSeen != nil { - lastSeen = *m.LastSeen - } - fmt.Printf("%d\t%s\t%s\t%t\n", m.ID, m.Name, lastSeen.Format("2006-01-02 15:04:05"), ephemeral) + d, err := nodesToPtables(*machines) + if err != nil { + log.Fatalf("Error converting to table: %s", err) } + pterm.DefaultTable.WithHasHeader().WithData(d).Render() + }, } @@ -106,7 +104,7 @@ var deleteNodeCmd = &cobra.Command{ Short: "Delete a node", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return fmt.Errorf("Missing parameters") + return fmt.Errorf("missing parameters") } return nil }, @@ -144,3 +142,32 @@ var deleteNodeCmd = &cobra.Command{ } }, } + +func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { + d := pterm.TableData{{"ID", "Name", "NodeKey", "IP address", "Ephemeral", "Last seen", "Online"}} + + for _, m := range m { + var ephemeral bool + if m.AuthKey != nil && m.AuthKey.Ephemeral { + ephemeral = true + } + var lastSeen time.Time + if m.LastSeen != nil { + lastSeen = *m.LastSeen + } + nKey, err := wgkey.ParseHex(m.NodeKey) + if err != nil { + return nil, err + } + nodeKey := tailcfg.NodeKey(nKey) + + var online string + if m.LastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online + online = pterm.LightGreen("true") + } else { + online = pterm.LightRed("false") + } + d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) + } + return d, nil +} From 36f5f78f4675ed345c161b733296ad5b59c477a0 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 15 Aug 2021 23:10:50 +0200 Subject: [PATCH 054/125] pterm dependency --- go.mod | 10 +++++----- go.sum | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 1d0dc0c9..5b2fe064 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,11 @@ go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.0.5 github.com/Microsoft/go-winio v0.5.0 // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/containerd/continuity v0.1.0 // indirect 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 // indirect + github.com/efekarakus/termcolor v1.0.1 github.com/gin-gonic/gin v1.7.2 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/klauspost/compress v1.13.1 @@ -18,15 +17,16 @@ require ( github.com/mattn/go-sqlite3 v1.14.7 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/opencontainers/runc v1.0.1 // indirect - github.com/ory/dockertest/v3 v3.7.0 // indirect - github.com/rs/zerolog v1.23.0 // indirect + github.com/ory/dockertest/v3 v3.7.0 + github.com/pterm/pterm v0.12.29 + github.com/rs/zerolog v1.23.0 github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect - golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 gorm.io/datatypes v1.0.1 diff --git a/go.sum b/go.sum index 6431570d..36b3c9f0 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/MarvinJWendt/testza v0.1.0 h1:4m+JkB/4e0nUlXdIa10Mg0poUz9CanQKjB3L+xecjAo= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -92,6 +94,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.38.52/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= @@ -109,7 +113,6 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -150,8 +153,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4= @@ -357,6 +360,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.3.1/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= +github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/goreleaser/chglog v0.1.2/go.mod h1:tTZsFuSZK4epDXfjMkxzcGbrIOXprf0JFp47BjIr3B8= @@ -582,6 +587,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= @@ -731,12 +738,17 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29 h1:wWRNFkC3+fk/agzHIO4aaXtQuRYdXJKngP3ed+LZlMU= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= github.com/quasilyte/go-ruleguard v0.2.0/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw= github.com/quasilyte/go-ruleguard v0.2.1/go.mod h1:hN2rVc/uS4bQhQKTio2XaSJSafJwqBUWWwtssT3cQmc= github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -874,6 +886,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1028,7 +1042,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1140,13 +1153,15 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1422,6 +1437,7 @@ gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.11 h1:CxkXW6Cc+VIBlL8yJEHq+Co4RYXdSLiMKNvgoZPjLK4= gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1454,5 +1470,3 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= tailscale.com v1.10.0 h1:3EWYxpXkCmXsMh1WgqoEjQ/xalxzxU+YD5ZmtaHS5cY= tailscale.com v1.10.0/go.mod h1:kgFF5AZPTltwdXjX2/ci4ghlcO3qKNWVIjD9s39pr8c= -tailscale.com v1.10.2 h1:0EbwydLGDxw7//yB5/1GTKz3hDJvGTUCajPZZPMDDGQ= -tailscale.com v1.10.2/go.mod h1:kgFF5AZPTltwdXjX2/ci4ghlcO3qKNWVIjD9s39pr8c= From 350f7da55d26b8a424028d1bae001f68ed909610 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 15 Aug 2021 23:20:38 +0200 Subject: [PATCH 055/125] Better table in namespaces --- cmd/headscale/cli/namespaces.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/namespaces.go b/cmd/headscale/cli/namespaces.go index 7d3977f9..e0bc0f20 100644 --- a/cmd/headscale/cli/namespaces.go +++ b/cmd/headscale/cli/namespaces.go @@ -3,8 +3,10 @@ package cli import ( "fmt" "log" + "strconv" "strings" + "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -94,9 +96,11 @@ var listNamespacesCmd = &cobra.Command{ fmt.Println(err) return } - fmt.Printf("ID\tName\n") + + d := pterm.TableData{{"ID", "Name", "Created"}} for _, n := range *namespaces { - fmt.Printf("%d\t%s\n", n.ID, n.Name) + d = append(d, []string{strconv.FormatUint(uint64(n.ID), 10), n.Name, n.CreatedAt.Format("2006-01-02 15:04:05")}) } + pterm.DefaultTable.WithHasHeader().WithData(d).Render() }, } From f194b4143557115db4ac961a633dd12db7356963 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 15 Aug 2021 23:29:55 +0200 Subject: [PATCH 056/125] Better table in preauthkeys --- cmd/headscale/cli/preauthkeys.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 63acc21a..4d07928d 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -3,10 +3,12 @@ package cli import ( "fmt" "log" + "strconv" "strings" "time" "github.com/hako/durafmt" + "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -54,6 +56,8 @@ var listPreAuthKeys = &cobra.Command{ fmt.Printf("Error getting the list of keys: %s\n", err) return } + + d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Expiration", "Created"}} for _, k := range *keys { expiration := "-" if k.Expiration != nil { @@ -67,16 +71,17 @@ var listPreAuthKeys = &cobra.Command{ reusable = fmt.Sprintf("%v", k.Reusable) } - fmt.Printf( - "key: %s, namespace: %s, reusable: %s, ephemeral: %v, expiration: %s, created_at: %s\n", + d = append(d, []string{ + strconv.FormatUint(k.ID, 10), k.Key, - k.Namespace.Name, reusable, - k.Ephemeral, + strconv.FormatBool(k.Ephemeral), expiration, k.CreatedAt.Format("2006-01-02 15:04:05"), - ) + }) + } + pterm.DefaultTable.WithHasHeader().WithData(d).Render() }, } From bc2574680db6d401302cb8c507fb69c287e29668 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 15 Aug 2021 23:35:03 +0200 Subject: [PATCH 057/125] Linting --- cmd/headscale/cli/namespaces.go | 5 ++++- cmd/headscale/cli/nodes.go | 6 ++++-- cmd/headscale/cli/preauthkeys.go | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/headscale/cli/namespaces.go b/cmd/headscale/cli/namespaces.go index e0bc0f20..c35c02cc 100644 --- a/cmd/headscale/cli/namespaces.go +++ b/cmd/headscale/cli/namespaces.go @@ -101,6 +101,9 @@ var listNamespacesCmd = &cobra.Command{ for _, n := range *namespaces { d = append(d, []string{strconv.FormatUint(uint64(n.ID), 10), n.Name, n.CreatedAt.Format("2006-01-02 15:04:05")}) } - pterm.DefaultTable.WithHasHeader().WithData(d).Render() + err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() + if err != nil { + log.Fatal(err) + } }, } diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index fdcaf760..d72201c9 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -94,8 +94,10 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error converting to table: %s", err) } - pterm.DefaultTable.WithHasHeader().WithData(d).Render() - + err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() + if err != nil { + log.Fatal(err) + } }, } diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 4d07928d..1340267e 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -81,7 +81,10 @@ var listPreAuthKeys = &cobra.Command{ }) } - pterm.DefaultTable.WithHasHeader().WithData(d).Render() + err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() + if err != nil { + log.Fatal(err) + } }, } From a376b697c0804c9901b7c42d4574bc128a0090db Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 16 Aug 2021 00:17:26 +0200 Subject: [PATCH 058/125] Send notifications when enabling a route --- routes.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/routes.go b/routes.go index 7c83436e..202754b1 100644 --- a/routes.go +++ b/routes.go @@ -45,19 +45,12 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr m.EnabledRoutes = datatypes.JSON(routes) h.db.Save(&m) - // THIS IS COMPLETELY USELESS. - // The peers map is stored in memory in the server process. - // Definitely not accessible from the CLI tool. - // We need RPC to the server - or some kind of 'needsUpdate' field in the DB - peers, _ := h.getPeers(*m) - for _, p := range *peers { - if pUp, ok := h.clientsPolling.Load(uint64(p.ID)); ok { - pUp.(chan []byte) <- []byte{} - } + err = h.RequestMapUpdates(m.NamespaceID) + if err != nil { + return nil, err } return &rIP, nil } } - return nil, errors.New("could not find routable range") } From 6fa61380b25c522d43f4889993ee0ad55b432c5c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Aug 2021 23:17:09 +0100 Subject: [PATCH 059/125] Up client count, make arguments more explicit and clean up unused assignments --- integration_test.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/integration_test.go b/integration_test.go index 4c1c54b4..892c7ece 100644 --- a/integration_test.go +++ b/integration_test.go @@ -34,7 +34,7 @@ var ih Headscale var pool dockertest.Pool var network dockertest.Network var headscale dockertest.Resource -var tailscaleCount int = 5 +var tailscaleCount int = 20 var tailscales map[string]dockertest.Resource func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) { @@ -115,7 +115,6 @@ func (s *IntegrationTestSuite) SetupSuite() { PortBindings: map[docker.Port][]docker.PortBinding{ "8080/tcp": []docker.PortBinding{{HostPort: "8080"}}, }, - Env: []string{}, } fmt.Println("Creating headscale container") @@ -134,7 +133,6 @@ func (s *IntegrationTestSuite) SetupSuite() { Name: hostname, Networks: []*dockertest.Network{&network}, Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"}, - Env: []string{}, } if pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy); err == nil { @@ -145,7 +143,6 @@ func (s *IntegrationTestSuite) SetupSuite() { fmt.Printf("Created %s container\n", hostname) } - // TODO: Replace this logic with something that can be detected on Github Actions fmt.Println("Waiting for headscale to be ready") hostEndpoint := fmt.Sprintf("localhost:%s", headscale.GetPort("8080/tcp")) @@ -197,20 +194,20 @@ func (s *IntegrationTestSuite) SetupSuite() { // The nodes need a bit of time to get their updated maps from headscale // TODO: See if we can have a more deterministic wait here. - time.Sleep(20 * time.Second) + time.Sleep(120 * time.Second) } func (s *IntegrationTestSuite) TearDownSuite() { - if err := pool.Purge(&headscale); err != nil { - log.Printf("Could not purge resource: %s\n", err) - } - for _, tailscale := range tailscales { if err := pool.Purge(&tailscale); err != nil { log.Printf("Could not purge resource: %s\n", err) } } + if err := pool.Purge(&headscale); err != nil { + log.Printf("Could not purge resource: %s\n", err) + } + if err := network.Close(); err != nil { log.Printf("Could not close network: %s\n", err) } @@ -295,7 +292,15 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { // We currently cant ping ourselves, so skip that. if peername != hostname { - command := []string{"tailscale", "ping", "--timeout=5s", "--c=1", ip.String()} + // 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, ips[hostname], peername, ip) result, err := executeCommand( From 2f883410d2a53f291979260810878d8f062b0771 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Aug 2021 23:17:38 +0100 Subject: [PATCH 060/125] Add lastUpdate field to machine, function issue message on update channel This commit adds a new field to machine, lastSuccessfulUpdate which tracks when we last was able to send a proper mapupdate to the node. The purpose of this is to be able to compare to a "global" last updated time and determine if we need to send an update map request to a node. In addition it allows us to create a scheduled check to see if all known nodes are up to date. Also, add a helper function to send a message to the update channel of a machine. --- machine.go | 81 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/machine.go b/machine.go index 69de453d..14efd008 100644 --- a/machine.go +++ b/machine.go @@ -2,6 +2,7 @@ package headscale import ( "encoding/json" + "errors" "fmt" "sort" "strconv" @@ -31,8 +32,9 @@ type Machine struct { AuthKeyID uint AuthKey *PreAuthKey - LastSeen *time.Time - Expiry *time.Time + LastSeen *time.Time + LastSuccessfulUpdate *time.Time + Expiry *time.Time HostInfo datatypes.JSON Endpoints datatypes.JSON @@ -211,6 +213,13 @@ func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { return &m, nil } +func (h *Headscale) UpdateMachine(m *Machine) error { + if result := h.db.Find(m).First(&m); result.Error != nil { + return result.Error + } + return nil +} + // DeleteMachine softs deletes a Machine from the database func (h *Headscale) DeleteMachine(m *Machine) error { m.Registered = false @@ -251,21 +260,67 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { func (h *Headscale) notifyChangesToPeers(m *Machine) { peers, _ := h.getPeers(*m) for _, p := range *peers { - pUp, ok := h.clientsPolling.Load(uint64(p.ID)) - if ok { + log.Info(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", p.Name). + Str("address", p.Addresses[0].String()). + Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) + err := h.requestUpdate(p) + if err != nil { log.Info(). Str("func", "notifyChangesToPeers"). Str("machine", m.Name). - Str("peer", m.Name). - Str("address", p.Addresses[0].String()). - Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - pUp.(chan []byte) <- []byte{} - } else { - log.Info(). - Str("func", "notifyChangesToPeers"). - Str("machine", m.Name). - Str("peer", m.Name). + Str("peer", p.Name). Msgf("Peer %s does not appear to be polling", p.Name) } + log.Trace(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Str("peer", p.Name). + Str("address", p.Addresses[0].String()). + Msgf("Notified peer %s (%s)", p.Name, p.Addresses[0]) } } + +func (h *Headscale) requestUpdate(m *tailcfg.Node) error { + pUp, ok := h.clientsUpdateChannels.Load(uint64(m.ID)) + if ok { + log.Info(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Notifying peer %s", m.Name) + + if update, ok := pUp.(chan struct{}); ok { + log.Trace(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Update channel is %#v", update) + + update <- struct{}{} + + log.Trace(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Notified machine %s", m.Name) + } + } else { + log.Info(). + Str("func", "requestUpdate"). + Str("machine", m.Name). + Msgf("Machine %s does not appear to be polling", m.Name) + return errors.New("machine does not seem to be polling") + } + return nil +} + +func (h *Headscale) isOutdated(m *Machine) bool { + lastChange := h.getLastStateChange() + log.Trace(). + Str("func", "keepAlive"). + Str("machine", m.Name). + Time("last_successful_update", *m.LastSuccessfulUpdate). + Time("last_state_change", lastChange). + Msgf("Checking if %s is missing updates", m.Name) + return m.LastSuccessfulUpdate.Before(lastChange) +} From 57b79aa852973bbc9ac3ab6a952061b83c1d008b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Aug 2021 23:21:11 +0100 Subject: [PATCH 061/125] Set timeout, add lastupdate field This commit makes two reasonably major changes: Set a default timeout for the go HTTP server (which gin uses), which allows us to actually have broken long poll sessions fail so we can have the client re-establish them. The current 10s number is chosen randomly and we need more testing to ensure that the feature work as intended. The second is adding a last updated field to keep track of the last time we had an update that needs to be propagated to all of our clients/nodes. This will be used to keep track of our machines and if they are up to date or need us to push an update. --- app.go | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/app.go b/app.go index fcf287f7..255a7df1 100644 --- a/app.go +++ b/app.go @@ -58,7 +58,10 @@ type Headscale struct { aclPolicy *ACLPolicy aclRules *[]tailcfg.FilterRule - clientsPolling sync.Map + clientsUpdateChannels sync.Map + + lastStateChangeMutex sync.RWMutex + lastStateChange time.Time } // NewHeadscale returns the Headscale app @@ -85,12 +88,13 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } h := Headscale{ - cfg: cfg, - dbType: cfg.DBtype, - dbString: dbString, - privateKey: privKey, - publicKey: &pubKey, - aclRules: &tailcfg.FilterAllowAll, // default allowall + cfg: cfg, + dbType: cfg.DBtype, + dbString: dbString, + privateKey: privKey, + publicKey: &pubKey, + aclRules: &tailcfg.FilterAllowAll, // default allowall + lastStateChange: time.Now().UTC(), } err = h.initDB() @@ -168,6 +172,13 @@ func (h *Headscale) Serve() error { go h.watchForKVUpdates(5000) go h.expireEphemeralNodes(5000) + s := &http.Server{ + Addr: h.cfg.Addr, + Handler: r, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + if h.cfg.TLSLetsEncryptHostname != "" { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") @@ -179,9 +190,11 @@ func (h *Headscale) Serve() error { Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir), } s := &http.Server{ - Addr: h.cfg.Addr, - TLSConfig: m.TLSConfig(), - Handler: r, + Addr: h.cfg.Addr, + TLSConfig: m.TLSConfig(), + Handler: r, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, } if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" { // Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737) @@ -206,12 +219,27 @@ func (h *Headscale) Serve() error { if !strings.HasPrefix(h.cfg.ServerURL, "http://") { log.Warn().Msg("Listening without TLS but ServerURL does not start with http://") } - err = r.Run(h.cfg.Addr) + err = s.ListenAndServe() } else { if !strings.HasPrefix(h.cfg.ServerURL, "https://") { log.Warn().Msg("Listening with TLS but ServerURL does not start with https://") } - err = r.RunTLS(h.cfg.Addr, h.cfg.TLSCertPath, h.cfg.TLSKeyPath) + err = s.ListenAndServeTLS(h.cfg.TLSCertPath, h.cfg.TLSKeyPath) } return err } + +func (h *Headscale) setLastStateChangeToNow() { + h.lastStateChangeMutex.Lock() + + now := time.Now().UTC() + h.lastStateChange = now + + h.lastStateChangeMutex.Unlock() +} + +func (h *Headscale) getLastStateChange() time.Time { + h.lastStateChangeMutex.RLock() + defer h.lastStateChangeMutex.RUnlock() + return h.lastStateChange +} From dd8c0d1e9e2415247fc37414ed468ec25e8c5f37 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Aug 2021 23:24:22 +0100 Subject: [PATCH 062/125] Move most "poll" functionality to poll.go This function migrates more poll functions (including keepalive) to poll.go to keep it somehow in the same file. In addition it makes changes to improve the stability and ensure nodes get the appropriate updates from the headscale control and are not left in an inconsistent state. Two new additions is: omitpeers=true will now trigger an update if the clients are not already up to date keepalive has been extended with a timer that will check every 120s if all nodes are up to date. --- api.go | 203 ++---------------------------- poll.go | 385 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 365 insertions(+), 223 deletions(-) diff --git a/api.go b/api.go index 0dc2bec5..7a6b4b16 100644 --- a/api.go +++ b/api.go @@ -13,7 +13,6 @@ import ( "github.com/gin-gonic/gin" "github.com/klauspost/compress/zstd" - "gorm.io/datatypes" "gorm.io/gorm" "inet.af/netaddr" "tailscale.com/tailcfg" @@ -82,14 +81,16 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { return } + now := time.Now().UTC() var m Machine if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") m = Machine{ - Expiry: &req.Expiry, - MachineKey: mKey.HexString(), - Name: req.Hostinfo.Hostname, - NodeKey: wgkey.Key(req.NodeKey).HexString(), + Expiry: &req.Expiry, + MachineKey: mKey.HexString(), + Name: req.Hostinfo.Hostname, + NodeKey: wgkey.Key(req.NodeKey).HexString(), + LastSuccessfulUpdate: &now, } if err := h.db.Create(&m).Error; err != nil { log.Error(). @@ -215,196 +216,6 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { c.Data(200, "application/json; charset=utf-8", respBody) } -// PollNetMapHandler takes care of /machine/:id/map -// -// This is the busiest endpoint, as it keeps the HTTP long poll that updates -// the clients when something in the network changes. -// -// The clients POST stuff like HostInfo and their Endpoints here, but -// only after their first request (marked with the ReadOnly field). -// -// At this moment the updates are sent in a quite horrendous way, but they kinda work. -func (h *Headscale) PollNetMapHandler(c *gin.Context) { - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Msg("PollNetMapHandler called") - body, _ := io.ReadAll(c.Request.Body) - mKeyStr := c.Param("id") - mKey, err := wgkey.ParseHex(mKeyStr) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Err(err). - Msg("Cannot parse client key") - c.String(http.StatusBadRequest, "") - return - } - req := tailcfg.MapRequest{} - err = decode(body, &req, &mKey, h.privateKey) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Err(err). - Msg("Cannot decode message") - c.String(http.StatusBadRequest, "") - return - } - - var m Machine - if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { - log.Warn(). - Str("handler", "PollNetMap"). - Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString()) - c.String(http.StatusUnauthorized, "") - return - } - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Found machine in database") - - hostinfo, _ := json.Marshal(req.Hostinfo) - m.Name = req.Hostinfo.Hostname - m.HostInfo = datatypes.JSON(hostinfo) - m.DiscoKey = wgkey.Key(req.DiscoKey).HexString() - now := time.Now().UTC() - - // From Tailscale client: - // - // ReadOnly is whether the client just wants to fetch the MapResponse, - // without updating their Endpoints. The Endpoints field will be ignored and - // LastSeen will not be updated and peers will not be notified of changes. - // - // The intended use is for clients to discover the DERP map at start-up - // before their first real endpoint update. - if !req.ReadOnly { - endpoints, _ := json.Marshal(req.Endpoints) - m.Endpoints = datatypes.JSON(endpoints) - m.LastSeen = &now - } - h.db.Save(&m) - - data, err := h.getMapResponse(mKey, req, m) - if err != nil { - log.Error(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Err(err). - Msg("Failed to get Map response") - c.String(http.StatusInternalServerError, ":(") - return - } - - // We update our peers if the client is not sending ReadOnly in the MapRequest - // so we don't distribute its initial request (it comes with - // empty endpoints to peers) - - // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 - log.Debug(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Bool("readOnly", req.ReadOnly). - Bool("omitPeers", req.OmitPeers). - Bool("stream", req.Stream). - Msg("Client map request processed") - - if req.ReadOnly { - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Client is starting up. Asking for DERP map") - c.Data(200, "application/json; charset=utf-8", *data) - return - } - if req.OmitPeers && !req.Stream { - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Client sent endpoint update and is ok with a response without peer list") - c.Data(200, "application/json; charset=utf-8", *data) - return - } else if req.OmitPeers && req.Stream { - log.Warn(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Ignoring request, don't know how to handle it") - c.String(http.StatusBadRequest, "") - return - } - - // Only create update channel if it has not been created - var update chan []byte - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Creating or loading update channel") - if result, ok := h.clientsPolling.LoadOrStore(m.ID, make(chan []byte, 1)); ok { - update = result.(chan []byte) - } - - pollData := make(chan []byte, 1) - defer close(pollData) - - cancelKeepAlive := make(chan []byte, 1) - defer close(cancelKeepAlive) - - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Client is ready to access the tailnet") - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Sending initial map") - pollData <- *data - - log.Info(). - Str("handler", "PollNetMap"). - Str("machine", m.Name). - Msg("Notifying peers") - // TODO: Why does this block? - go h.notifyChangesToPeers(&m) - - h.PollNetMapStream(c, m, req, mKey, pollData, update, cancelKeepAlive) - log.Trace(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Finished stream, closing PollNetMap session") -} - -func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgkey.Key, req tailcfg.MapRequest, m Machine) { - for { - select { - case <-cancel: - return - - default: - data, err := h.getMapKeepAliveResponse(mKey, req, m) - if err != nil { - log.Error(). - Str("func", "keepAlive"). - Err(err). - Msg("Error generating the keep alive msg") - return - } - - log.Debug(). - Str("func", "keepAlive"). - Str("machine", m.Name). - Msg("Sending keepalive") - pollData <- *data - - time.Sleep(60 * time.Second) - } - } -} - func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) { log.Trace(). Str("func", "getMapResponse"). @@ -542,7 +353,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, Str("func", "handleAuthKey"). Str("machine", m.Name). Str("ip", ip.String()). - Msgf("Assining %s to %s", ip, m.Name) + Msgf("Assigning %s to %s", ip, m.Name) m.AuthKeyID = uint(pak.ID) m.IPAddress = ip.String() diff --git a/poll.go b/poll.go index f0bfe706..d58d45f1 100644 --- a/poll.go +++ b/poll.go @@ -1,38 +1,242 @@ package headscale import ( + "encoding/json" + "errors" "io" + "net/http" "time" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" + "gorm.io/datatypes" + "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/wgkey" ) +// PollNetMapHandler takes care of /machine/:id/map +// +// This is the busiest endpoint, as it keeps the HTTP long poll that updates +// the clients when something in the network changes. +// +// The clients POST stuff like HostInfo and their Endpoints here, but +// only after their first request (marked with the ReadOnly field). +// +// At this moment the updates are sent in a quite horrendous way, but they kinda work. +func (h *Headscale) PollNetMapHandler(c *gin.Context) { + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Msg("PollNetMapHandler called") + body, _ := io.ReadAll(c.Request.Body) + mKeyStr := c.Param("id") + mKey, err := wgkey.ParseHex(mKeyStr) + if err != nil { + log.Error(). + Str("handler", "PollNetMap"). + Err(err). + Msg("Cannot parse client key") + c.String(http.StatusBadRequest, "") + return + } + req := tailcfg.MapRequest{} + err = decode(body, &req, &mKey, h.privateKey) + if err != nil { + log.Error(). + Str("handler", "PollNetMap"). + Err(err). + Msg("Cannot decode message") + c.String(http.StatusBadRequest, "") + return + } + + var m Machine + if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { + log.Warn(). + Str("handler", "PollNetMap"). + Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString()) + c.String(http.StatusUnauthorized, "") + return + } + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Found machine in database") + + hostinfo, _ := json.Marshal(req.Hostinfo) + m.Name = req.Hostinfo.Hostname + m.HostInfo = datatypes.JSON(hostinfo) + m.DiscoKey = wgkey.Key(req.DiscoKey).HexString() + now := time.Now().UTC() + + // From Tailscale client: + // + // ReadOnly is whether the client just wants to fetch the MapResponse, + // without updating their Endpoints. The Endpoints field will be ignored and + // LastSeen will not be updated and peers will not be notified of changes. + // + // The intended use is for clients to discover the DERP map at start-up + // before their first real endpoint update. + if !req.ReadOnly { + endpoints, _ := json.Marshal(req.Endpoints) + m.Endpoints = datatypes.JSON(endpoints) + m.LastSeen = &now + } + h.db.Save(&m) + + data, err := h.getMapResponse(mKey, req, m) + if err != nil { + log.Error(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Err(err). + Msg("Failed to get Map response") + c.String(http.StatusInternalServerError, ":(") + return + } + + // We update our peers if the client is not sending ReadOnly in the MapRequest + // so we don't distribute its initial request (it comes with + // empty endpoints to peers) + + // Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696 + log.Debug(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Bool("readOnly", req.ReadOnly). + Bool("omitPeers", req.OmitPeers). + Bool("stream", req.Stream). + Msg("Client map request processed") + + if req.ReadOnly { + log.Info(). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Msg("Client is starting up. Probably interested in a DERP map") + c.Data(200, "application/json; charset=utf-8", *data) + return + } + + // There has been an update to _any_ of the nodes that the other nodes would + // need to know about + h.setLastStateChangeToNow() + + // The request is not ReadOnly, so we need to set up channels for updating + // peers via longpoll + + // Only create update channel if it has not been created + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Loading or creating update channel") + var updateChan chan struct{} + if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { + if wrapped, ok := storedChan.(chan struct{}); ok { + updateChan = wrapped + } else { + log.Error(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Failed to convert update channel to struct{}") + } + } else { + log.Debug(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Update channel not found, creating") + + updateChan = make(chan struct{}) + h.clientsUpdateChannels.Store(m.ID, updateChan) + } + + pollDataChan := make(chan []byte) + // defer close(pollData) + + keepAliveChan := make(chan []byte) + + cancelKeepAlive := make(chan struct{}) + defer close(cancelKeepAlive) + + if req.OmitPeers && !req.Stream { + log.Info(). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Msg("Client sent endpoint update and is ok with a response without peer list") + c.Data(200, "application/json; charset=utf-8", *data) + + // It sounds like we should update the nodes when we have received a endpoint update + // even tho the comments in the tailscale code dont explicitly say so. + go h.notifyChangesToPeers(&m) + return + } else if req.OmitPeers && req.Stream { + log.Warn(). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Msg("Ignoring request, don't know how to handle it") + c.String(http.StatusBadRequest, "") + return + } + + log.Info(). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Msg("Client is ready to access the tailnet") + log.Info(). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Msg("Sending initial map") + go func() { pollDataChan <- *data }() + + log.Info(). + Str("handler", "PollNetMap"). + Str("machine", m.Name). + Msg("Notifying peers") + go h.notifyChangesToPeers(&m) + + h.PollNetMapStream(c, m, req, mKey, pollDataChan, keepAliveChan, updateChan, cancelKeepAlive) + log.Trace(). + Str("handler", "PollNetMap"). + Str("id", c.Param("id")). + Str("machine", m.Name). + Msg("Finished stream, closing PollNetMap session") +} + func (h *Headscale) PollNetMapStream( c *gin.Context, m Machine, req tailcfg.MapRequest, mKey wgkey.Key, - pollData chan []byte, - update chan []byte, - cancelKeepAlive chan []byte, + pollDataChan chan []byte, + keepAliveChan chan []byte, + updateChan chan struct{}, + cancelKeepAlive chan struct{}, ) { - - go h.keepAlive(cancelKeepAlive, pollData, mKey, req, m) + go h.keepAlive(cancelKeepAlive, keepAliveChan, mKey, req, m) c.Stream(func(w io.Writer) bool { log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). Msg("Waiting for data to stream...") - select { - case data := <-pollData: + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan) + + select { + case data := <-pollDataChan: log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). + Str("channel", "pollData"). Int("bytes", len(data)). Msg("Sending data received via pollData channel") _, err := w.Write(data) @@ -40,44 +244,99 @@ func (h *Headscale) PollNetMapStream( log.Error(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). + Str("channel", "pollData"). Err(err). Msg("Cannot write data") } log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). + Str("channel", "pollData"). Int("bytes", len(data)). Msg("Data from pollData channel written successfully") now := time.Now().UTC() m.LastSeen = &now + m.LastSuccessfulUpdate = &now + h.db.Save(&m) + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "pollData"). + Int("bytes", len(data)). + Msg("Machine updated successfully after sending pollData") + return true + + case data := <-keepAliveChan: + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "keepAlive"). + Int("bytes", len(data)). + Msg("Sending keep alive message") + _, err := w.Write(data) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "keepAlive"). + Err(err). + Msg("Cannot write keep alive message") + } + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "keepAlive"). + Int("bytes", len(data)). + Msg("Keep alive sent successfully") + now := time.Now().UTC() + m.LastSeen = &now h.db.Save(&m) log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). + Str("channel", "keepAlive"). Int("bytes", len(data)). - Msg("Machine updated successfully after sending pollData") + Msg("Machine updated successfully after sending keep alive") return true - case <-update: - log.Debug(). - Str("handler", "PollNetMapStream"). - Str("machine", m.Name). - Msg("Received a request for update") - data, err := h.getMapResponse(mKey, req, m) - if err != nil { - log.Error(). + case <-updateChan: + if h.isOutdated(&m) { + log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). - Err(err). - Msg("Could not get the map update") - } - _, err = w.Write(*data) - if err != nil { - log.Error(). + Str("channel", "update"). + Msg("Received a request for update") + data, err := h.getMapResponse(mKey, req, m) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "update"). + Err(err). + Msg("Could not get the map update") + } + _, err = w.Write(*data) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "update"). + Err(err). + Msg("Could not write the map response") + } + log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). - Err(err). - Msg("Could not write the map response") + Str("channel", "update"). + Msg("Updated Map has been sent") + + // Keep track of the last successful update, + // we sometimes end in a state were the update + // is not picked up by a client and we use this + // to determine if we should "force" an update. + now := time.Now().UTC() + m.LastSuccessfulUpdate = &now + h.db.Save(&m) } return true @@ -89,10 +348,82 @@ func (h *Headscale) PollNetMapStream( now := time.Now().UTC() m.LastSeen = &now h.db.Save(&m) - cancelKeepAlive <- []byte{} - h.clientsPolling.Delete(m.ID) - close(update) + + cancelKeepAlive <- struct{}{} + + h.clientsUpdateChannels.Delete(m.ID) + // close(updateChan) + + close(pollDataChan) + + close(keepAliveChan) + return false } }) } + +// TODO: Rename this function to schedule ... +func (h *Headscale) keepAlive( + cancelChan <-chan struct{}, + keepAliveChan chan<- []byte, + mKey wgkey.Key, + req tailcfg.MapRequest, + m Machine, +) { + keepAliveTicker := time.NewTicker(60 * time.Second) + updateCheckerTicker := time.NewTicker(30 * time.Second) + + for { + select { + case <-cancelChan: + return + + case <-keepAliveTicker.C: + data, err := h.getMapKeepAliveResponse(mKey, req, m) + if err != nil { + log.Error(). + Str("func", "keepAlive"). + Err(err). + Msg("Error generating the keep alive msg") + return + } + + log.Debug(). + Str("func", "keepAlive"). + Str("machine", m.Name). + Msg("Sending keepalive") + keepAliveChan <- *data + + case <-updateCheckerTicker.C: + err := h.UpdateMachine(&m) + if err != nil { + log.Error(). + Str("func", "keepAlive"). + Str("machine", m.Name). + Err(err). + Msg("Could not refresh machine details from database") + return + } + if h.isOutdated(&m) { + log.Debug(). + Str("func", "keepAlive"). + Str("machine", m.Name). + Time("last_successful_update", *m.LastSuccessfulUpdate). + Time("last_state_change", h.getLastStateChange()). + Msgf("There has been updates since the last successful update to %s", m.Name) + + // TODO Error checking + n, _ := m.toNode() + h.requestUpdate(n) + } else { + log.Trace(). + Str("func", "keepAlive"). + Str("machine", m.Name). + Time("last_successful_update", *m.LastSuccessfulUpdate). + Time("last_state_change", h.getLastStateChange()). + Msgf("%s is up to date", m.Name) + } + } + } +} From 8d1adaaef3abac891f6794c63268b5ac47c747e2 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 19 Aug 2021 18:05:33 +0100 Subject: [PATCH 063/125] Move isOutdated logic to updateChan consumation --- machine.go | 5 +++++ poll.go | 46 +++++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/machine.go b/machine.go index 14efd008..13e3529a 100644 --- a/machine.go +++ b/machine.go @@ -315,6 +315,11 @@ func (h *Headscale) requestUpdate(m *tailcfg.Node) error { } func (h *Headscale) isOutdated(m *Machine) bool { + err := h.UpdateMachine(m) + if err != nil { + return true + } + lastChange := h.getLastStateChange() log.Trace(). Str("func", "keepAlive"). diff --git a/poll.go b/poll.go index d58d45f1..27358fc5 100644 --- a/poll.go +++ b/poll.go @@ -300,12 +300,18 @@ func (h *Headscale) PollNetMapStream( return true case <-updateChan: + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "update"). + Msg("Received a request for update") if h.isOutdated(&m) { - log.Trace(). + log.Debug(). Str("handler", "PollNetMapStream"). Str("machine", m.Name). - Str("channel", "update"). - Msg("Received a request for update") + Time("last_successful_update", *m.LastSuccessfulUpdate). + Time("last_state_change", h.getLastStateChange()). + Msgf("There has been updates since the last successful update to %s", m.Name) data, err := h.getMapResponse(mKey, req, m) if err != nil { log.Error(). @@ -337,6 +343,13 @@ func (h *Headscale) PollNetMapStream( now := time.Now().UTC() m.LastSuccessfulUpdate = &now h.db.Save(&m) + } else { + log.Trace(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Time("last_successful_update", *m.LastSuccessfulUpdate). + Time("last_state_change", h.getLastStateChange()). + Msgf("%s is up to date", m.Name) } return true @@ -396,33 +409,16 @@ func (h *Headscale) keepAlive( keepAliveChan <- *data case <-updateCheckerTicker.C: - err := h.UpdateMachine(&m) + // 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() + err := h.requestUpdate(n) if err != nil { log.Error(). Str("func", "keepAlive"). Str("machine", m.Name). Err(err). - Msg("Could not refresh machine details from database") - return - } - if h.isOutdated(&m) { - log.Debug(). - Str("func", "keepAlive"). - Str("machine", m.Name). - Time("last_successful_update", *m.LastSuccessfulUpdate). - Time("last_state_change", h.getLastStateChange()). - Msgf("There has been updates since the last successful update to %s", m.Name) - - // TODO Error checking - n, _ := m.toNode() - h.requestUpdate(n) - } else { - log.Trace(). - Str("func", "keepAlive"). - Str("machine", m.Name). - Time("last_successful_update", *m.LastSuccessfulUpdate). - Time("last_state_change", h.getLastStateChange()). - Msgf("%s is up to date", m.Name) + Msgf("Failed to send update request to %s", m.Name) } } } From 48ef6e5a6f492d80aae8e7467498b4053f11da58 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 19 Aug 2021 18:06:57 +0100 Subject: [PATCH 064/125] Rename keepAlive function, as it now does more things --- poll.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/poll.go b/poll.go index 27358fc5..522c529e 100644 --- a/poll.go +++ b/poll.go @@ -218,7 +218,7 @@ func (h *Headscale) PollNetMapStream( updateChan chan struct{}, cancelKeepAlive chan struct{}, ) { - go h.keepAlive(cancelKeepAlive, keepAliveChan, mKey, req, m) + go h.scheduledPollWorker(cancelKeepAlive, keepAliveChan, mKey, req, m) c.Stream(func(w io.Writer) bool { log.Trace(). @@ -376,8 +376,7 @@ func (h *Headscale) PollNetMapStream( }) } -// TODO: Rename this function to schedule ... -func (h *Headscale) keepAlive( +func (h *Headscale) scheduledPollWorker( cancelChan <-chan struct{}, keepAliveChan chan<- []byte, mKey wgkey.Key, From b0ec945dbb59196b7542386a2a543ddf5ba987b9 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 19 Aug 2021 18:19:26 +0100 Subject: [PATCH 065/125] Make lastStateChange namespaced --- app.go | 40 ++++++++++++++++++++-------------------- machine.go | 2 +- poll.go | 6 +++--- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app.go b/app.go index 255a7df1..76cf92ea 100644 --- a/app.go +++ b/app.go @@ -60,8 +60,7 @@ type Headscale struct { clientsUpdateChannels sync.Map - lastStateChangeMutex sync.RWMutex - lastStateChange time.Time + lastStateChange sync.Map } // NewHeadscale returns the Headscale app @@ -88,13 +87,12 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } h := Headscale{ - cfg: cfg, - dbType: cfg.DBtype, - dbString: dbString, - privateKey: privKey, - publicKey: &pubKey, - aclRules: &tailcfg.FilterAllowAll, // default allowall - lastStateChange: time.Now().UTC(), + cfg: cfg, + dbType: cfg.DBtype, + dbString: dbString, + privateKey: privKey, + publicKey: &pubKey, + aclRules: &tailcfg.FilterAllowAll, // default allowall } err = h.initDB() @@ -229,17 +227,19 @@ func (h *Headscale) Serve() error { return err } -func (h *Headscale) setLastStateChangeToNow() { - h.lastStateChangeMutex.Lock() +func (h *Headscale) setLastStateChangeToNow(namespace string) { + now := time.Now().UTC() + h.lastStateChange.Store(namespace, now) +} + +func (h *Headscale) getLastStateChange(namespace string) time.Time { + if wrapped, ok := h.lastStateChange.Load(namespace); ok { + lastChange, _ := wrapped.(time.Time) + return lastChange + + } now := time.Now().UTC() - h.lastStateChange = now - - h.lastStateChangeMutex.Unlock() -} - -func (h *Headscale) getLastStateChange() time.Time { - h.lastStateChangeMutex.RLock() - defer h.lastStateChangeMutex.RUnlock() - return h.lastStateChange + h.lastStateChange.Store(namespace, now) + return now } diff --git a/machine.go b/machine.go index 13e3529a..5352f741 100644 --- a/machine.go +++ b/machine.go @@ -320,7 +320,7 @@ func (h *Headscale) isOutdated(m *Machine) bool { return true } - lastChange := h.getLastStateChange() + lastChange := h.getLastStateChange(m.Namespace.Name) log.Trace(). Str("func", "keepAlive"). Str("machine", m.Name). diff --git a/poll.go b/poll.go index 522c529e..e85c7a9f 100644 --- a/poll.go +++ b/poll.go @@ -123,7 +123,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { // There has been an update to _any_ of the nodes that the other nodes would // need to know about - h.setLastStateChangeToNow() + h.setLastStateChangeToNow(m.Namespace.Name) // The request is not ReadOnly, so we need to set up channels for updating // peers via longpoll @@ -310,7 +310,7 @@ func (h *Headscale) PollNetMapStream( Str("handler", "PollNetMapStream"). Str("machine", m.Name). Time("last_successful_update", *m.LastSuccessfulUpdate). - Time("last_state_change", h.getLastStateChange()). + Time("last_state_change", h.getLastStateChange(m.Namespace.Name)). Msgf("There has been updates since the last successful update to %s", m.Name) data, err := h.getMapResponse(mKey, req, m) if err != nil { @@ -348,7 +348,7 @@ func (h *Headscale) PollNetMapStream( Str("handler", "PollNetMapStream"). Str("machine", m.Name). Time("last_successful_update", *m.LastSuccessfulUpdate). - Time("last_state_change", h.getLastStateChange()). + Time("last_state_change", h.getLastStateChange(m.Namespace.Name)). Msgf("%s is up to date", m.Name) } return true From 53168d54d803ef0c4a182ed6457a95e5681657e7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 19 Aug 2021 22:29:03 +0100 Subject: [PATCH 066/125] Make http timeout 30s instead of 10s --- app.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index 76cf92ea..fe1b954b 100644 --- a/app.go +++ b/app.go @@ -167,14 +167,16 @@ func (h *Headscale) Serve() error { r.POST("/machine/:id", h.RegistrationHandler) var err error + timeout := 30 * time.Second + go h.watchForKVUpdates(5000) go h.expireEphemeralNodes(5000) s := &http.Server{ Addr: h.cfg.Addr, Handler: r, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, + ReadTimeout: timeout, + WriteTimeout: timeout, } if h.cfg.TLSLetsEncryptHostname != "" { @@ -191,8 +193,8 @@ func (h *Headscale) Serve() error { Addr: h.cfg.Addr, TLSConfig: m.TLSConfig(), Handler: r, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, + ReadTimeout: timeout, + WriteTimeout: timeout, } if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" { // Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737) From 1f422af1c80e7226b8555a660338bbd30b4ef36c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 20 Aug 2021 16:50:55 +0100 Subject: [PATCH 067/125] Save headscale logs if jobs fail --- .gitignore | 2 + integration_test.go | 91 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 3a64648f..44bec691 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ config.json *.key /db.sqlite *.sqlite3 + +test_output/ diff --git a/integration_test.go b/integration_test.go index 892c7ece..fa0dfbf0 100644 --- a/integration_test.go +++ b/integration_test.go @@ -4,10 +4,13 @@ package headscale import ( "bytes" + "context" "fmt" + "io/ioutil" "log" "net/http" "os" + "path" "strings" "testing" "time" @@ -22,10 +25,35 @@ import ( type IntegrationTestSuite struct { suite.Suite + stats *suite.SuiteInformation } func TestIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(IntegrationTestSuite)) + s := new(IntegrationTestSuite) + suite.Run(t, s) + + // HandleStats, which allows us to check if we passed and save logs + // is called after TearDown, so we cannot tear down containers before + // we have potentially saved the logs. + for _, tailscale := range tailscales { + if err := pool.Purge(&tailscale); err != nil { + log.Printf("Could not purge resource: %s\n", err) + } + } + + if !s.stats.Passed() { + err := saveLog(&headscale, "test_output") + if err != nil { + log.Printf("Could not save log: %s\n", err) + } + } + if err := pool.Purge(&headscale); err != nil { + log.Printf("Could not purge resource: %s\n", err) + } + + if err := network.Close(); err != nil { + log.Printf("Could not close network: %s\n", err) + } } var integrationTmpDir string @@ -34,7 +62,7 @@ var ih Headscale var pool dockertest.Pool var network dockertest.Network var headscale dockertest.Resource -var tailscaleCount int = 20 +var tailscaleCount int = 25 var tailscales map[string]dockertest.Resource func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) { @@ -62,6 +90,48 @@ func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) return stdout.String(), nil } +func saveLog(resource *dockertest.Resource, basePath string) error { + err := os.MkdirAll(basePath, os.ModePerm) + if err != nil { + return err + } + + var stdout bytes.Buffer + var stderr bytes.Buffer + + err = pool.Client.Logs( + docker.LogsOptions{ + Context: context.TODO(), + Container: resource.Container.ID, + OutputStream: &stdout, + ErrorStream: &stderr, + Tail: "all", + RawTerminal: false, + Stdout: true, + Stderr: true, + Follow: false, + Timestamps: false, + }, + ) + if err != nil { + return err + } + + fmt.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath) + + err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stdout.log"), []byte(stdout.String()), 0644) + if err != nil { + return err + } + + err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stderr.log"), []byte(stdout.String()), 0644) + if err != nil { + return err + } + + return nil +} + func dockerRestartPolicy(config *docker.HostConfig) { // set AutoRemove to true so that stopped container goes away by itself config.AutoRemove = true @@ -194,23 +264,14 @@ func (s *IntegrationTestSuite) SetupSuite() { // The nodes need a bit of time to get their updated maps from headscale // TODO: See if we can have a more deterministic wait here. - time.Sleep(120 * time.Second) + time.Sleep(60 * time.Second) } func (s *IntegrationTestSuite) TearDownSuite() { - for _, tailscale := range tailscales { - if err := pool.Purge(&tailscale); err != nil { - log.Printf("Could not purge resource: %s\n", err) - } - } +} - if err := pool.Purge(&headscale); err != nil { - log.Printf("Could not purge resource: %s\n", err) - } - - if err := network.Close(); err != nil { - log.Printf("Could not close network: %s\n", err) - } +func (s *IntegrationTestSuite) HandleStats(suiteName string, stats *suite.SuiteInformation) { + s.stats = stats } func (s *IntegrationTestSuite) TestListNodes() { From 88d7ac04bf7a78f378cd3015c1b1bb083ba54cb3 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 20 Aug 2021 16:52:34 +0100 Subject: [PATCH 068/125] Account for racecondition in deleting/closing update channel This commit tries to address the possible raceondition that can happen if a client closes its connection after we have fetched it from the syncmap before sending the message. To try to avoid introducing new dead lock conditions, all messages sent to updateChannel has been moved into a function, which handles the locking (instead of calling it all over the place) The same lock is used around the delete/close function. --- app.go | 3 ++- machine.go | 42 ++++++++++++++++++++++++++++++++++++++++-- poll.go | 29 ++++------------------------- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/app.go b/app.go index fe1b954b..e5f44103 100644 --- a/app.go +++ b/app.go @@ -58,7 +58,8 @@ type Headscale struct { aclPolicy *ACLPolicy aclRules *[]tailcfg.FilterRule - clientsUpdateChannels sync.Map + clientsUpdateChannels sync.Map + clientsUpdateChannelMutex sync.Mutex lastStateChange sync.Map } diff --git a/machine.go b/machine.go index 5352f741..57c48ba8 100644 --- a/machine.go +++ b/machine.go @@ -266,7 +266,7 @@ func (h *Headscale) notifyChangesToPeers(m *Machine) { Str("peer", p.Name). Str("address", p.Addresses[0].String()). Msgf("Notifying peer %s (%s)", p.Name, p.Addresses[0]) - err := h.requestUpdate(p) + err := h.sendRequestOnUpdateChannel(p) if err != nil { log.Info(). Str("func", "notifyChangesToPeers"). @@ -283,7 +283,45 @@ func (h *Headscale) notifyChangesToPeers(m *Machine) { } } -func (h *Headscale) requestUpdate(m *tailcfg.Node) error { +func (h *Headscale) getOrOpenUpdateChannel(m *Machine) <-chan struct{} { + var updateChan chan struct{} + if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { + if unwrapped, ok := storedChan.(chan struct{}); ok { + updateChan = unwrapped + } else { + log.Error(). + Str("handler", "openUpdateChannel"). + Str("machine", m.Name). + Msg("Failed to convert update channel to struct{}") + } + } else { + log.Debug(). + Str("handler", "openUpdateChannel"). + Str("machine", m.Name). + Msg("Update channel not found, creating") + + updateChan = make(chan struct{}) + h.clientsUpdateChannels.Store(m.ID, updateChan) + } + return updateChan +} + +func (h *Headscale) closeUpdateChannel(m *Machine) { + h.clientsUpdateChannelMutex.Lock() + defer h.clientsUpdateChannelMutex.Unlock() + + if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { + if unwrapped, ok := storedChan.(chan struct{}); ok { + close(unwrapped) + } + } + h.clientsUpdateChannels.Delete(m.ID) +} + +func (h *Headscale) sendRequestOnUpdateChannel(m *tailcfg.Node) error { + h.clientsUpdateChannelMutex.Lock() + defer h.clientsUpdateChannelMutex.Unlock() + pUp, ok := h.clientsUpdateChannels.Load(uint64(m.ID)) if ok { log.Info(). diff --git a/poll.go b/poll.go index e85c7a9f..d086fc44 100644 --- a/poll.go +++ b/poll.go @@ -134,27 +134,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Str("id", c.Param("id")). Str("machine", m.Name). Msg("Loading or creating update channel") - var updateChan chan struct{} - if storedChan, ok := h.clientsUpdateChannels.Load(m.ID); ok { - if wrapped, ok := storedChan.(chan struct{}); ok { - updateChan = wrapped - } else { - log.Error(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Failed to convert update channel to struct{}") - } - } else { - log.Debug(). - Str("handler", "PollNetMap"). - Str("id", c.Param("id")). - Str("machine", m.Name). - Msg("Update channel not found, creating") - - updateChan = make(chan struct{}) - h.clientsUpdateChannels.Store(m.ID, updateChan) - } + updateChan := h.getOrOpenUpdateChannel(&m) pollDataChan := make(chan []byte) // defer close(pollData) @@ -215,7 +195,7 @@ func (h *Headscale) PollNetMapStream( mKey wgkey.Key, pollDataChan chan []byte, keepAliveChan chan []byte, - updateChan chan struct{}, + updateChan <-chan struct{}, cancelKeepAlive chan struct{}, ) { go h.scheduledPollWorker(cancelKeepAlive, keepAliveChan, mKey, req, m) @@ -364,8 +344,7 @@ func (h *Headscale) PollNetMapStream( cancelKeepAlive <- struct{}{} - h.clientsUpdateChannels.Delete(m.ID) - // close(updateChan) + h.closeUpdateChannel(&m) close(pollDataChan) @@ -411,7 +390,7 @@ func (h *Headscale) scheduledPollWorker( // 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() - err := h.requestUpdate(n) + err := h.sendRequestOnUpdateChannel(n) if err != nil { log.Error(). Str("func", "keepAlive"). From d93a7f2e02a4994a7b7f2c96630c4bde9fd989b4 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 20 Aug 2021 17:15:07 +0100 Subject: [PATCH 069/125] Make Info default log level --- cmd/headscale/cli/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 4ada6408..7e7e8f96 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -39,7 +39,7 @@ func LoadConfig(path string) error { viper.SetDefault("ip_prefix", "100.64.0.0/10") - viper.SetDefault("log_level", "debug") + viper.SetDefault("log_level", "info") err := viper.ReadInConfig() if err != nil { From 0adbd720bf921738f2c15942f776a14750650945 Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Fri, 20 Aug 2021 19:15:20 +0200 Subject: [PATCH 070/125] github/workflows: add docker release --- .github/workflows/release.yml | 46 ++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b191a12c..84bb73f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,10 @@ -name: goreleaser +--- +name: release on: push: tags: - - "*" # triggers only if push new tag version + - "*" # triggers only if push new tag version jobs: goreleaser: @@ -27,4 +28,43 @@ jobs: version: latest args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker-release: + runs-on: ubuntu-latest + steps: + - + name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + # list of Docker images to use as base name for tags + images: | + ${{ secrets.DOCKERHUB_USERNAME }}/headsacle + ghcr.io/${{ github.repository_owner }}/headscale + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Login to GHCR + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From 8bcc7e88f03825b7c6835a8f6b9c610f15d01bf9 Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Fri, 20 Aug 2021 19:37:15 +0200 Subject: [PATCH 071/125] github/workflows: add dispatch --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84bb73f1..80465925 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,7 @@ on: push: tags: - "*" # triggers only if push new tag version + workflow_dispatch: jobs: goreleaser: @@ -66,5 +67,6 @@ jobs: uses: docker/build-push-action@v2 with: push: true + context: . tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From f02beaf0758362426c2c0b3ed4c5d671beedfc15 Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Fri, 20 Aug 2021 19:45:01 +0200 Subject: [PATCH 072/125] github/workflows: add checkout --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80465925..9392db6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,6 +34,11 @@ jobs: docker-release: runs-on: ubuntu-latest steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Docker meta id: meta From 75afdc6306cebc5612ad5c9d433adffb2a75d99a Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Fri, 20 Aug 2021 20:10:34 +0200 Subject: [PATCH 073/125] github/workflows: remove version tag --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9392db6a..4c724f73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,6 @@ jobs: ${{ secrets.DOCKERHUB_USERNAME }}/headsacle ghcr.io/${{ github.repository_owner }}/headscale tags: | - type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha From c49fe26da7700592cbf7ef04f3ed1787cfeb08ef Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 09:15:16 +0100 Subject: [PATCH 074/125] Code clean up, loglevel debug for integration tests --- integration_test.go | 18 +++++++++--------- integration_test/etc/config.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/integration_test.go b/integration_test.go index fa0dfbf0..98cb925a 100644 --- a/integration_test.go +++ b/integration_test.go @@ -23,6 +23,15 @@ import ( "inet.af/netaddr" ) +var integrationTmpDir string +var ih Headscale + +var pool dockertest.Pool +var network dockertest.Network +var headscale dockertest.Resource +var tailscaleCount int = 50 +var tailscales map[string]dockertest.Resource + type IntegrationTestSuite struct { suite.Suite stats *suite.SuiteInformation @@ -56,15 +65,6 @@ func TestIntegrationTestSuite(t *testing.T) { } } -var integrationTmpDir string -var ih Headscale - -var pool dockertest.Pool -var network dockertest.Network -var headscale dockertest.Resource -var tailscaleCount int = 25 -var tailscales map[string]dockertest.Resource - func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) { var stdout bytes.Buffer var stderr bytes.Buffer diff --git a/integration_test/etc/config.json b/integration_test/etc/config.json index 5454f2f7..8a6fd962 100644 --- a/integration_test/etc/config.json +++ b/integration_test/etc/config.json @@ -7,5 +7,5 @@ "db_type": "sqlite3", "db_path": "/tmp/integration_test_db.sqlite3", "acl_policy_path": "", - "log_level": "trace" + "log_level": "debug" } From a054e2514ae363ea78052775b3fad44864f3a604 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 09:26:18 +0100 Subject: [PATCH 075/125] Keep tailscale count at 25 in integration tests --- integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.go b/integration_test.go index 98cb925a..8cdc1918 100644 --- a/integration_test.go +++ b/integration_test.go @@ -29,7 +29,7 @@ var ih Headscale var pool dockertest.Pool var network dockertest.Network var headscale dockertest.Resource -var tailscaleCount int = 50 +var tailscaleCount int = 25 var tailscales map[string]dockertest.Resource type IntegrationTestSuite struct { From a613501ff218797a4fbbad8df961234743aa4e7f Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Sat, 21 Aug 2021 11:17:21 +0200 Subject: [PATCH 076/125] Update .github/workflows/release.yml Fix typo Co-authored-by: Kristoffer Dalby --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c724f73..7f715ca7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: with: # list of Docker images to use as base name for tags images: | - ${{ secrets.DOCKERHUB_USERNAME }}/headsacle + ${{ secrets.DOCKERHUB_USERNAME }}/headscale ghcr.io/${{ github.repository_owner }}/headscale tags: | type=semver,pattern={{major}}.{{minor}} From c883e798849edb3d3babe25c24dba9d832da6bf8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 14:49:46 +0100 Subject: [PATCH 077/125] Enhance route command with ptables and multiple routes This commit rewrites the `routes list` command to use ptables to present a slightly nicer list, including a new field if the route is enabled or not (which is quite useful). In addition, it reworks the enable command to support enabling multiple routes (not only one route as per removed TODO). This allows users to actually take advantage of exit-nodes and subnet relays. --- cmd/headscale/cli/routes.go | 28 +++++--- routes.go | 129 ++++++++++++++++++++++++++++++------ routes_test.go | 81 ++++++++++++++++++++-- 3 files changed, 202 insertions(+), 36 deletions(-) diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go index 98b653f9..f58d4990 100644 --- a/cmd/headscale/cli/routes.go +++ b/cmd/headscale/cli/routes.go @@ -5,6 +5,7 @@ import ( "log" "strings" + "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -44,19 +45,25 @@ var listRoutesCmd = &cobra.Command{ if err != nil { log.Fatalf("Error initializing: %s", err) } - routes, err := h.GetNodeRoutes(n, args[0]) - - if strings.HasPrefix(o, "json") { - JsonOutput(routes, err, o) - return - } + availableRoutes, err := h.GetAdvertisedNodeRoutes(n, args[0]) if err != nil { fmt.Println(err) return } - fmt.Println(routes) + if strings.HasPrefix(o, "json") { + // TODO: Add enable/disabled information to this interface + JsonOutput(availableRoutes, err, o) + return + } + + d := h.RoutesToPtables(n, args[0], *availableRoutes) + + err = pterm.DefaultTable.WithHasHeader().WithData(d).Render() + if err != nil { + log.Fatal(err) + } }, } @@ -80,9 +87,10 @@ var enableRouteCmd = &cobra.Command{ if err != nil { log.Fatalf("Error initializing: %s", err) } - route, err := h.EnableNodeRoute(n, args[0], args[1]) + + err = h.EnableNodeRoute(n, args[0], args[1]) if strings.HasPrefix(o, "json") { - JsonOutput(route, err, o) + JsonOutput(args[1], err, o) return } @@ -90,6 +98,6 @@ var enableRouteCmd = &cobra.Command{ fmt.Println(err) return } - fmt.Printf("Enabled route %s\n", route) + fmt.Printf("Enabled route %s\n", args[1]) }, } diff --git a/routes.go b/routes.go index 202754b1..28d86837 100644 --- a/routes.go +++ b/routes.go @@ -2,55 +2,140 @@ package headscale import ( "encoding/json" - "errors" + "fmt" + "strconv" + "github.com/pterm/pterm" "gorm.io/datatypes" "inet.af/netaddr" ) -// GetNodeRoutes returns the subnet routes advertised by a node (identified by +// GetAdvertisedNodeRoutes returns the subnet routes advertised by a node (identified by // namespace and node name) -func (h *Headscale) GetNodeRoutes(namespace string, nodeName string) (*[]netaddr.IPPrefix, error) { +func (h *Headscale) GetAdvertisedNodeRoutes(namespace string, nodeName string) (*[]netaddr.IPPrefix, error) { m, err := h.GetMachine(namespace, nodeName) if err != nil { return nil, err } - hi, err := m.GetHostInfo() + hostInfo, err := m.GetHostInfo() if err != nil { return nil, err } - return &hi.RoutableIPs, nil + return &hostInfo.RoutableIPs, nil +} + +// GetEnabledNodeRoutes returns the subnet routes enabled by a node (identified by +// namespace and node name) +func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]netaddr.IPPrefix, error) { + m, err := h.GetMachine(namespace, nodeName) + if err != nil { + return nil, err + } + + data, err := m.EnabledRoutes.MarshalJSON() + if err != nil { + return nil, err + } + + routesStr := []string{} + err = json.Unmarshal(data, &routesStr) + if err != nil { + return nil, err + } + + routes := make([]netaddr.IPPrefix, len(routesStr)) + for index, routeStr := range routesStr { + route, err := netaddr.ParseIPPrefix(routeStr) + if err != nil { + return nil, err + } + routes[index] = route + } + + return routes, nil +} + +func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool { + route, err := netaddr.ParseIPPrefix(routeStr) + if err != nil { + return false + } + + enabledRoutes, err := h.GetEnabledNodeRoutes(namespace, nodeName) + if err != nil { + return false + } + + for _, enabledRoute := range enabledRoutes { + if route == enabledRoute { + return true + } + } + return false } // EnableNodeRoute enables a subnet route advertised by a node (identified by // namespace and node name) -func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr string) (*netaddr.IPPrefix, error) { +func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr string) error { m, err := h.GetMachine(namespace, nodeName) if err != nil { - return nil, err - } - hi, err := m.GetHostInfo() - if err != nil { - return nil, err + return err } + route, err := netaddr.ParseIPPrefix(routeStr) if err != nil { - return nil, err + return err } - for _, rIP := range hi.RoutableIPs { - if rIP == route { - routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest - m.EnabledRoutes = datatypes.JSON(routes) - h.db.Save(&m) + availableRoutes, err := h.GetAdvertisedNodeRoutes(namespace, nodeName) + if err != nil { + return err + } - err = h.RequestMapUpdates(m.NamespaceID) - if err != nil { - return nil, err + enabledRoutes, err := h.GetEnabledNodeRoutes(namespace, nodeName) + if err != nil { + return err + } + + available := false + for _, availableRoute := range *availableRoutes { + // If the route is available, and not yet enabled, add it to the new routing table + if route == availableRoute { + available = true + if !h.IsNodeRouteEnabled(namespace, nodeName, routeStr) { + enabledRoutes = append(enabledRoutes, route) } - return &rIP, nil } } - return nil, errors.New("could not find routable range") + + if !available { + return fmt.Errorf("route (%s) is not available on node %s", nodeName, routeStr) + } + + routes, err := json.Marshal(enabledRoutes) + if err != nil { + return err + } + + m.EnabledRoutes = datatypes.JSON(routes) + h.db.Save(&m) + + err = h.RequestMapUpdates(m.NamespaceID) + if err != nil { + return err + } + + return nil +} + +func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData { + d := pterm.TableData{{"Route", "Enabled"}} + + for _, route := range availableRoutes { + enabled := h.IsNodeRouteEnabled(namespace, nodeName, route.String()) + + d = append(d, []string{route.String(), strconv.FormatBool(enabled)}) + } + return d } diff --git a/routes_test.go b/routes_test.go index a05b7e16..33aaa9df 100644 --- a/routes_test.go +++ b/routes_test.go @@ -33,7 +33,7 @@ func (s *Suite) TestGetRoutes(c *check.C) { MachineKey: "foo", NodeKey: "bar", DiscoKey: "faa", - Name: "testmachine", + Name: "test_get_route_machine", NamespaceID: n.ID, Registered: true, RegisterMethod: "authKey", @@ -42,14 +42,87 @@ func (s *Suite) TestGetRoutes(c *check.C) { } h.db.Save(&m) - r, err := h.GetNodeRoutes("test", "testmachine") + r, err := h.GetAdvertisedNodeRoutes("test", "testmachine") c.Assert(err, check.IsNil) c.Assert(len(*r), check.Equals, 1) - _, err = h.EnableNodeRoute("test", "testmachine", "192.168.0.0/24") + err = h.EnableNodeRoute("test", "testmachine", "192.168.0.0/24") c.Assert(err, check.NotNil) - _, err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + c.Assert(err, check.IsNil) +} + +func (s *Suite) TestGetEnableRoutes(c *check.C) { + n, err := h.CreateNamespace("test") c.Assert(err, check.IsNil) + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) + + _, err = h.GetMachine("test", "testmachine") + c.Assert(err, check.NotNil) + + route, err := netaddr.ParseIPPrefix( + "10.0.0.0/24", + ) + c.Assert(err, check.IsNil) + + route2, err := netaddr.ParseIPPrefix( + "150.0.10.0/25", + ) + c.Assert(err, check.IsNil) + + hi := tailcfg.Hostinfo{ + RoutableIPs: []netaddr.IPPrefix{route, route2}, + } + hostinfo, err := json.Marshal(hi) + c.Assert(err, check.IsNil) + + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "test_enable_route_machine", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + HostInfo: datatypes.JSON(hostinfo), + } + h.db.Save(&m) + + availableRoutes, err := h.GetAdvertisedNodeRoutes("test", "testmachine") + c.Assert(err, check.IsNil) + c.Assert(len(*availableRoutes), check.Equals, 2) + + enabledRoutes, err := h.GetEnabledNodeRoutes("test", "testmachine") + c.Assert(err, check.IsNil) + c.Assert(len(enabledRoutes), check.Equals, 0) + + err = h.EnableNodeRoute("test", "testmachine", "192.168.0.0/24") + c.Assert(err, check.NotNil) + + err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + c.Assert(err, check.IsNil) + + enabledRoutes1, err := h.GetEnabledNodeRoutes("test", "testmachine") + c.Assert(err, check.IsNil) + c.Assert(len(enabledRoutes1), check.Equals, 1) + + // Adding it twice will just let it pass through + err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + c.Assert(err, check.IsNil) + + enabledRoutes2, err := h.GetEnabledNodeRoutes("test", "testmachine") + c.Assert(err, check.IsNil) + c.Assert(len(enabledRoutes2), check.Equals, 1) + + err = h.EnableNodeRoute("test", "testmachine", "150.0.10.0/25") + c.Assert(err, check.IsNil) + + enabledRoutes3, err := h.GetEnabledNodeRoutes("test", "testmachine") + c.Assert(err, check.IsNil) + c.Assert(len(enabledRoutes3), check.Equals, 2) } From 4f97e077db683ff03f6e558510c4117c88ba94cd Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 15:04:30 +0100 Subject: [PATCH 078/125] Add --all flag to routes enable command to enable all advertised routes --- cmd/headscale/cli/routes.go | 68 ++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go index f58d4990..72010864 100644 --- a/cmd/headscale/cli/routes.go +++ b/cmd/headscale/cli/routes.go @@ -16,6 +16,9 @@ func init() { if err != nil { log.Fatalf(err.Error()) } + + enableRouteCmd.Flags().BoolP("all", "a", false, "Enable all routes advertised by the node") + routesCmd.AddCommand(listRoutesCmd) routesCmd.AddCommand(enableRouteCmd) } @@ -71,33 +74,74 @@ var enableRouteCmd = &cobra.Command{ Use: "enable node-name route", Short: "Allows exposing a route declared by this node to the rest of the nodes", Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 2 { - return fmt.Errorf("Missing parameters") + all, err := cmd.Flags().GetBool("all") + if err != nil { + log.Fatalf("Error getting namespace: %s", err) + } + + if all { + if len(args) < 1 { + return fmt.Errorf("Missing parameters") + } + return nil + } else { + if len(args) < 2 { + return fmt.Errorf("Missing parameters") + } + return nil } - return nil }, Run: func(cmd *cobra.Command, args []string) { n, err := cmd.Flags().GetString("namespace") if err != nil { log.Fatalf("Error getting namespace: %s", err) } + o, _ := cmd.Flags().GetString("output") + all, err := cmd.Flags().GetBool("all") + if err != nil { + log.Fatalf("Error getting namespace: %s", err) + } + h, err := getHeadscaleApp() if err != nil { log.Fatalf("Error initializing: %s", err) } - err = h.EnableNodeRoute(n, args[0], args[1]) - if strings.HasPrefix(o, "json") { - JsonOutput(args[1], err, o) - return - } + if all { + availableRoutes, err := h.GetAdvertisedNodeRoutes(n, args[0]) + if err != nil { + fmt.Println(err) + return + } - if err != nil { - fmt.Println(err) - return + for _, availableRoute := range *availableRoutes { + err = h.EnableNodeRoute(n, args[0], availableRoute.String()) + if err != nil { + fmt.Println(err) + return + } + + if strings.HasPrefix(o, "json") { + JsonOutput(availableRoute, err, o) + } else { + fmt.Printf("Enabled route %s\n", availableRoute) + } + } + } else { + err = h.EnableNodeRoute(n, args[0], args[1]) + + if strings.HasPrefix(o, "json") { + JsonOutput(args[1], err, o) + return + } + + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("Enabled route %s\n", args[1]) } - fmt.Printf("Enabled route %s\n", args[1]) }, } From 693bce1b1050098aeee342c5dfd77a0735ea8fd5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 15:35:26 +0100 Subject: [PATCH 079/125] Update test machine name properly --- routes_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/routes_test.go b/routes_test.go index 33aaa9df..ad16d21c 100644 --- a/routes_test.go +++ b/routes_test.go @@ -16,7 +16,7 @@ func (s *Suite) TestGetRoutes(c *check.C) { pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) c.Assert(err, check.IsNil) - _, err = h.GetMachine("test", "testmachine") + _, err = h.GetMachine("test", "test_get_route_machine") c.Assert(err, check.NotNil) route, err := netaddr.ParseIPPrefix("10.0.0.0/24") @@ -42,14 +42,14 @@ func (s *Suite) TestGetRoutes(c *check.C) { } h.db.Save(&m) - r, err := h.GetAdvertisedNodeRoutes("test", "testmachine") + r, err := h.GetAdvertisedNodeRoutes("test", "test_get_route_machine") c.Assert(err, check.IsNil) c.Assert(len(*r), check.Equals, 1) - err = h.EnableNodeRoute("test", "testmachine", "192.168.0.0/24") + err = h.EnableNodeRoute("test", "test_get_route_machine", "192.168.0.0/24") c.Assert(err, check.NotNil) - err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + err = h.EnableNodeRoute("test", "test_get_route_machine", "10.0.0.0/24") c.Assert(err, check.IsNil) } @@ -60,7 +60,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) { pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) c.Assert(err, check.IsNil) - _, err = h.GetMachine("test", "testmachine") + _, err = h.GetMachine("test", "test_enable_route_machine") c.Assert(err, check.NotNil) route, err := netaddr.ParseIPPrefix( @@ -93,36 +93,36 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) { } h.db.Save(&m) - availableRoutes, err := h.GetAdvertisedNodeRoutes("test", "testmachine") + availableRoutes, err := h.GetAdvertisedNodeRoutes("test", "test_enable_route_machine") c.Assert(err, check.IsNil) c.Assert(len(*availableRoutes), check.Equals, 2) - enabledRoutes, err := h.GetEnabledNodeRoutes("test", "testmachine") + enabledRoutes, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine") c.Assert(err, check.IsNil) c.Assert(len(enabledRoutes), check.Equals, 0) - err = h.EnableNodeRoute("test", "testmachine", "192.168.0.0/24") + err = h.EnableNodeRoute("test", "test_enable_route_machine", "192.168.0.0/24") c.Assert(err, check.NotNil) - err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + err = h.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24") c.Assert(err, check.IsNil) - enabledRoutes1, err := h.GetEnabledNodeRoutes("test", "testmachine") + enabledRoutes1, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine") c.Assert(err, check.IsNil) c.Assert(len(enabledRoutes1), check.Equals, 1) // Adding it twice will just let it pass through - err = h.EnableNodeRoute("test", "testmachine", "10.0.0.0/24") + err = h.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24") c.Assert(err, check.IsNil) - enabledRoutes2, err := h.GetEnabledNodeRoutes("test", "testmachine") + enabledRoutes2, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine") c.Assert(err, check.IsNil) c.Assert(len(enabledRoutes2), check.Equals, 1) - err = h.EnableNodeRoute("test", "testmachine", "150.0.10.0/25") + err = h.EnableNodeRoute("test", "test_enable_route_machine", "150.0.10.0/25") c.Assert(err, check.IsNil) - enabledRoutes3, err := h.GetEnabledNodeRoutes("test", "testmachine") + enabledRoutes3, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine") c.Assert(err, check.IsNil) c.Assert(len(enabledRoutes3), check.Equals, 2) } From f749be1490f06e48b86e33c13e5cc8c5910cecda Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 15:40:27 +0100 Subject: [PATCH 080/125] Split lint and test CI files This commit splits the lint and test steps into two different jobs in github actions. Consider this a suggestion, the idea is that when we look at PRs we will see explicitly which one of the two types of checks fails without having to open Github actions. --- .github/workflows/lint.yml | 39 ++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 46 ++++++++++++++------------------------ 2 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..cff42e96 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: CI + +on: [push, pull_request] + +jobs: + # The "build" workflow + test: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Install and run golangci-lint as a separate step, it's much faster this + # way because this action has caching. It'll get run again in `make lint` + # below, but it's still much faster in the end than installing + # golangci-lint manually in the `Run lint` step. + - uses: golangci/golangci-lint-action@v2 + with: + args: --timeout 2m + + # Setup Go + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: "1.16.3" # The Go version to download (if necessary) and use. + + # Install all the dependencies + - name: Install dependencies + run: | + go version + go install golang.org/x/lint/golint@latest + sudo apt update + sudo apt install -y make + + - name: Run lint + run: make lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a96971a7..3d254fa6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,36 +10,24 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Install and run golangci-lint as a separate step, it's much faster this - # way because this action has caching. It'll get run again in `make lint` - # below, but it's still much faster in the end than installing - # golangci-lint manually in the `Run lint` step. - - uses: golangci/golangci-lint-action@v2 - with: - args: --timeout 2m - - # Setup Go - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: '1.16.3' # The Go version to download (if necessary) and use. + # Setup Go + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: "1.16.3" # The Go version to download (if necessary) and use. - # Install all the dependencies - - name: Install dependencies - run: | - go version - go install golang.org/x/lint/golint@latest - sudo apt update - sudo apt install -y make - - - name: Run tests - run: make test + # Install all the dependencies + - name: Install dependencies + run: | + go version + sudo apt update + sudo apt install -y make - - name: Run lint - run: make lint + - name: Run tests + run: make test - - name: Run build - run: make \ No newline at end of file + - name: Run build + run: make From 28ed8a5742adc73598a64ed10abad20886cd5268 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 15:42:23 +0100 Subject: [PATCH 081/125] Actually rename lint --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cff42e96..98dbc464 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: # The "build" workflow - test: + lint: # The type of runner that the job will run on runs-on: ubuntu-latest From 0aeeaac3614737861c53ae2ef5a956736d94fbc6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 21 Aug 2021 16:52:19 +0100 Subject: [PATCH 082/125] Always load machine object from DB before save/modify We are currently holding Machine objects in memory for a long time, while waiting for stream/longpoll, this might make us end up with stale objects, that we just call save on, potentially overwriting stuff in the database. A typical scenario would be someone changing something from the CLI, e.g. enabling routes, which in turn is overwritten again by the stale object in the longpolling function. The code has been left with TODO's and a discussion is available in #93. --- poll.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/poll.go b/poll.go index d086fc44..fdf522cd 100644 --- a/poll.go +++ b/poll.go @@ -234,6 +234,18 @@ func (h *Headscale) PollNetMapStream( Str("channel", "pollData"). Int("bytes", len(data)). Msg("Data from pollData channel written successfully") + // TODO: Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err = h.UpdateMachine(&m) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "pollData"). + Err(err). + Msg("Cannot update machine from database") + } now := time.Now().UTC() m.LastSeen = &now m.LastSuccessfulUpdate = &now @@ -268,6 +280,18 @@ func (h *Headscale) PollNetMapStream( Str("channel", "keepAlive"). Int("bytes", len(data)). Msg("Keep alive sent successfully") + // TODO: Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err = h.UpdateMachine(&m) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "keepAlive"). + Err(err). + Msg("Cannot update machine from database") + } now := time.Now().UTC() m.LastSeen = &now h.db.Save(&m) @@ -316,10 +340,22 @@ func (h *Headscale) PollNetMapStream( Str("channel", "update"). Msg("Updated Map has been sent") - // Keep track of the last successful update, - // we sometimes end in a state were the update - // is not picked up by a client and we use this - // to determine if we should "force" an update. + // Keep track of the last successful update, + // we sometimes end in a state were the update + // is not picked up by a client and we use this + // to determine if we should "force" an update. + // TODO: Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err = h.UpdateMachine(&m) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "update"). + Err(err). + Msg("Cannot update machine from database") + } now := time.Now().UTC() m.LastSuccessfulUpdate = &now h.db.Save(&m) @@ -338,6 +374,18 @@ func (h *Headscale) PollNetMapStream( Str("handler", "PollNetMapStream"). Str("machine", m.Name). Msg("The client has closed the connection") + // TODO: Abstract away all the database calls, this can cause race conditions + // when an outdated machine object is kept alive, e.g. db is update from + // command line, but then overwritten. + err := h.UpdateMachine(&m) + if err != nil { + log.Error(). + Str("handler", "PollNetMapStream"). + Str("machine", m.Name). + Str("channel", "Done"). + Err(err). + Msg("Cannot update machine from database") + } now := time.Now().UTC() m.LastSeen = &now h.db.Save(&m) From ebd27b46afabb3f160151e61c96b868e5cff3b2a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 23 Aug 2021 07:35:44 +0100 Subject: [PATCH 083/125] Add comment to updatemachine --- machine.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/machine.go b/machine.go index 57c48ba8..4cdadd98 100644 --- a/machine.go +++ b/machine.go @@ -213,6 +213,8 @@ func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { return &m, nil } +// UpdateMachine takes a Machine struct pointer (typically already loaded from database +// and updates it with the latest data from the database. func (h *Headscale) UpdateMachine(m *Machine) error { if result := h.db.Find(m).First(&m); result.Error != nil { return result.Error From 059f13fc9d8db9066c850433f5cb611746d17338 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 23 Aug 2021 07:38:14 +0100 Subject: [PATCH 084/125] Add missing comment for stream function --- poll.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/poll.go b/poll.go index fdf522cd..bea16164 100644 --- a/poll.go +++ b/poll.go @@ -188,6 +188,9 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Msg("Finished stream, closing PollNetMap session") } +// PollNetMapStream takes care of /machine/:id/map +// stream logic, ensuring we communicate updates and data +// to the connected clients. func (h *Headscale) PollNetMapStream( c *gin.Context, m Machine, From 987bbee1dbe9de611bc409e0f39647e460fda25e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 24 Aug 2021 07:09:47 +0100 Subject: [PATCH 085/125] Add DNSConfig field to configuration --- app.go | 2 ++ cmd/headscale/cli/utils.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/app.go b/app.go index e5f44103..c903d83f 100644 --- a/app.go +++ b/app.go @@ -43,6 +43,8 @@ type Config struct { TLSCertPath string TLSKeyPath string + + DNSConfig *tailcfg.DNSConfig } // Headscale represents the base app of the service diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 7e7e8f96..7ba78648 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -41,6 +41,8 @@ func LoadConfig(path string) error { viper.SetDefault("log_level", "info") + viper.SetDefault("dns_config", nil) + err := viper.ReadInConfig() if err != nil { return fmt.Errorf("Fatal error reading config file: %s \n", err) @@ -70,6 +72,40 @@ func LoadConfig(path string) error { } else { return nil } + +} + +func getDNSConfig() *tailcfg.DNSConfig { + if viper.IsSet("dns_config") { + dnsConfig := &tailcfg.DNSConfig{} + + if viper.IsSet("dns_config.nameservers") { + nameserversStr := viper.GetStringSlice("dns_config.nameservers") + + nameservers := make([]netaddr.IP, len(nameserversStr)) + + for index, nameserverStr := range nameserversStr { + nameserver, err := netaddr.ParseIP(nameserverStr) + if err != nil { + log.Error(). + Str("func", "getDNSConfig"). + Err(err). + Msgf("Could not parse nameserver IP: %s", nameserverStr) + } + + nameservers[index] = nameserver + } + + dnsConfig.Nameservers = nameservers + } + if viper.IsSet("dns_config.domains") { + dnsConfig.Domains = viper.GetStringSlice("dns_config.domains") + } + + return dnsConfig + } + + return nil } func absPath(path string) string { @@ -126,6 +162,8 @@ func getHeadscaleApp() (*headscale.Headscale, error) { TLSCertPath: absPath(viper.GetString("tls_cert_path")), TLSKeyPath: absPath(viper.GetString("tls_key_path")), + + DNSConfig: getDNSConfig(), } h, err := headscale.NewHeadscale(cfg) From e77c16b55a949d4d181a839771269e6335d03916 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 24 Aug 2021 07:10:09 +0100 Subject: [PATCH 086/125] Add DNSConfig to example and setup test --- cmd/headscale/headscale_test.go | 3 ++- config.json.postgres.example | 7 ++++++- config.json.sqlite.example | 7 ++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 8fcf8a54..58a0977e 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -58,7 +58,7 @@ func (*Suite) TestPostgresConfigLoading(c *check.C) { c.Assert(viper.GetString("db_port"), check.Equals, "5432") c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") - c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") + c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") } func (*Suite) TestSqliteConfigLoading(c *check.C) { @@ -92,6 +92,7 @@ func (*Suite) TestSqliteConfigLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") + c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") } func writeConfig(c *check.C, tmpDir string, configYaml []byte) { diff --git a/config.json.postgres.example b/config.json.postgres.example index fe772d73..aba72063 100644 --- a/config.json.postgres.example +++ b/config.json.postgres.example @@ -16,5 +16,10 @@ "tls_letsencrypt_challenge_type": "HTTP-01", "tls_cert_path": "", "tls_key_path": "", - "acl_policy_path": "" + "acl_policy_path": "", + "dns_config": { + "nameservers": [ + "1.1.1.1" + ] + } } diff --git a/config.json.sqlite.example b/config.json.sqlite.example index e965059a..b22e5ace 100644 --- a/config.json.sqlite.example +++ b/config.json.sqlite.example @@ -12,5 +12,10 @@ "tls_letsencrypt_challenge_type": "HTTP-01", "tls_cert_path": "", "tls_key_path": "", - "acl_policy_path": "" + "acl_policy_path": "", + "dns_config": { + "nameservers": [ + "1.1.1.1" + ] + } } From 01e781e546299627ecf47f5750bb211b15b99b0d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 24 Aug 2021 07:11:45 +0100 Subject: [PATCH 087/125] Pass DNSConfig to nodes in MapResponse --- api.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/api.go b/api.go index 7a6b4b16..621eeb8f 100644 --- a/api.go +++ b/api.go @@ -14,7 +14,6 @@ import ( "github.com/gin-gonic/gin" "github.com/klauspost/compress/zstd" "gorm.io/gorm" - "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/types/wgkey" ) @@ -245,10 +244,15 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac } resp := tailcfg.MapResponse{ - KeepAlive: false, - Node: node, - Peers: *peers, - DNS: []netaddr.IP{}, + 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" + DNSConfig: h.cfg.DNSConfig, SearchPaths: []string{}, Domain: "headscale.net", PacketFilter: *h.aclRules, From 104776ee84777999ba34e82afe6eec4319704017 Mon Sep 17 00:00:00 2001 From: Aaron Bieber Date: Tue, 24 Aug 2021 07:49:15 -0600 Subject: [PATCH 088/125] fix setting of version --- Makefile | 2 +- scripts/version-at-commit.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7ffe1f9c..8adf760f 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ version = $(shell ./scripts/version-at-commit.sh) build: - go build -ldflags "-s -w -X main.version=$(version)" cmd/headscale/headscale.go + go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.version=$(version)" cmd/headscale/headscale.go dev: lint test build diff --git a/scripts/version-at-commit.sh b/scripts/version-at-commit.sh index aebdb870..2f7fab84 100755 --- a/scripts/version-at-commit.sh +++ b/scripts/version-at-commit.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e -o pipefail commit="$1" From b3732e7fb9d9dd28da35ac9c9b62a99e483da860 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 25 Aug 2021 07:04:48 +0100 Subject: [PATCH 089/125] Add nameserver as resolver aswell --- 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 7ba78648..b5c7c219 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -83,6 +83,7 @@ func getDNSConfig() *tailcfg.DNSConfig { nameserversStr := viper.GetStringSlice("dns_config.nameservers") nameservers := make([]netaddr.IP, len(nameserversStr)) + resolvers := make([]tailcfg.DNSResolver, len(nameserversStr)) for index, nameserverStr := range nameserversStr { nameserver, err := netaddr.ParseIP(nameserverStr) @@ -94,9 +95,13 @@ func getDNSConfig() *tailcfg.DNSConfig { } nameservers[index] = nameserver + resolvers[index] = tailcfg.DNSResolver{ + Addr: nameserver.String() + ":53", + } } dnsConfig.Nameservers = nameservers + dnsConfig.Resolvers = resolvers } if viper.IsSet("dns_config.domains") { dnsConfig.Domains = viper.GetStringSlice("dns_config.domains") From 3f5e06a0f8c4344dcd41a7d433aa7ff1627c2d91 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 25 Aug 2021 18:43:13 +0100 Subject: [PATCH 090/125] Dont add the portnumber to the ip --- cmd/headscale/cli/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index b5c7c219..e3c4402a 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -96,7 +96,7 @@ func getDNSConfig() *tailcfg.DNSConfig { nameservers[index] = nameserver resolvers[index] = tailcfg.DNSResolver{ - Addr: nameserver.String() + ":53", + Addr: nameserver.String(), } } From 8735e5675cf61ea3ea4e797f5155943966b056eb Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 25 Aug 2021 19:03:04 +0100 Subject: [PATCH 091/125] Add a test for the getdnsconfig function --- cmd/headscale/cli/utils.go | 4 ++-- cmd/headscale/headscale_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index e3c4402a..aaf994d0 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -75,7 +75,7 @@ func LoadConfig(path string) error { } -func getDNSConfig() *tailcfg.DNSConfig { +func GetDNSConfig() *tailcfg.DNSConfig { if viper.IsSet("dns_config") { dnsConfig := &tailcfg.DNSConfig{} @@ -168,7 +168,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { TLSCertPath: absPath(viper.GetString("tls_cert_path")), TLSKeyPath: absPath(viper.GetString("tls_key_path")), - DNSConfig: getDNSConfig(), + DNSConfig: GetDNSConfig(), } h, err := headscale.NewHeadscale(cfg) diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 58a0977e..58bf5899 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -95,6 +95,36 @@ func (*Suite) TestSqliteConfigLoading(c *check.C) { c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") } +func (*Suite) TestDNSConfigLoading(c *check.C) { + tmpDir, err := ioutil.TempDir("", "headscale") + if err != nil { + c.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + path, err := os.Getwd() + if err != nil { + c.Fatal(err) + } + + // Symlink the example config file + err = os.Symlink(filepath.Clean(path+"/../../config.json.sqlite.example"), filepath.Join(tmpDir, "config.json")) + if err != nil { + c.Fatal(err) + } + + // Load example config, it should load without validation errors + err = cli.LoadConfig(tmpDir) + c.Assert(err, check.IsNil) + + dnsConfig := cli.GetDNSConfig() + fmt.Println(dnsConfig) + + c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1") + + c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1") +} + func writeConfig(c *check.C, tmpDir string, configYaml []byte) { // Populate a custom config file configFile := filepath.Join(tmpDir, "config.yaml") From ba3dffecbfeb616660f66500ae08299d6d43be14 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 25 Aug 2021 19:05:10 +0100 Subject: [PATCH 092/125] Update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb42b666..fef70207 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ Headscale implements this coordination server. - [X] JSON-formatted output - [X] ACLs - [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) +- [X] DNS (passing DNS servers to nodes) - [ ] Share nodes between ~~users~~ namespaces -- [ ] DNS +- [ ] MagicDNS / Smart DNS ## Roadmap 🤷 From 91a48d6a435be35e82424db71686cf7d4718098d Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 26 Aug 2021 10:23:45 +0200 Subject: [PATCH 093/125] Update Dockerfile Use explicit version in Dockerfile (addresses #95) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0c2af33a..9499af22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY . /go/src/headscale RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale RUN test -e /go/bin/headscale -FROM ubuntu:latest +FROM ubuntu:20.04 COPY --from=build /go/bin/headscale /usr/local/bin/headscale ENV TZ UTC From e4ef65be761dd1b4d65e0f3ccc4c07130d28b0b1 Mon Sep 17 00:00:00 2001 From: Silver Bullet Date: Thu, 2 Sep 2021 05:44:42 +0800 Subject: [PATCH 094/125] fix: check last seen time without possible null pointer --- cmd/headscale/cli/nodes.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index d72201c9..7afc6026 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -154,8 +154,10 @@ func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { ephemeral = true } var lastSeen time.Time + var lastSeenTime string if m.LastSeen != nil { lastSeen = *m.LastSeen + lastSeenTime = lastSeen.Format("2006-01-02 15:04:05") } nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { @@ -164,12 +166,12 @@ func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { nodeKey := tailcfg.NodeKey(nKey) var online string - if m.LastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online + if lastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online online = pterm.LightGreen("true") } else { online = pterm.LightRed("false") } - d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) + d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), m.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online}) } return d, nil } From 6faaae0c5f3aa1465b9cf6fa8145e73b72b679a2 Mon Sep 17 00:00:00 2001 From: Silver Bullet Date: Thu, 2 Sep 2021 06:08:12 +0800 Subject: [PATCH 095/125] docs: add notes on how to build own DERP server The official doc is hidden under a bunch of issues. Add a doc link here and hope it could be helpful. --- derp.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derp.yaml b/derp.yaml index 17bfc186..9434e712 100644 --- a/derp.yaml +++ b/derp.yaml @@ -1,7 +1,7 @@ # This file contains some of the official Tailscale DERP servers, # shamelessly taken from https://github.com/tailscale/tailscale/blob/main/net/dnsfallback/dns-fallback-servers.json # -# If you plan to somehow use headscale, please deploy your own DERP infra +# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/ regions: 1: regionid: 1 From 1ecd0d7ca4a9ef453ec3f2a67349f954b8c47d54 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 16:57:26 +0200 Subject: [PATCH 096/125] Added DB SharedNode model to support sharing nodes --- db.go | 5 +++++ sharing_nodes.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 sharing_nodes.go diff --git a/db.go b/db.go index 06302523..4435f048 100644 --- a/db.go +++ b/db.go @@ -44,6 +44,11 @@ func (h *Headscale) initDB() error { return err } + err = db.AutoMigrate(&SharedNode{}) + if err != nil { + return err + } + err = h.setValue("db_version", dbVersion) return err } diff --git a/sharing_nodes.go b/sharing_nodes.go new file mode 100644 index 00000000..b52b900c --- /dev/null +++ b/sharing_nodes.go @@ -0,0 +1,37 @@ +package headscale + +import "gorm.io/gorm" + +const errorSameNamespace = Error("Destination namespace same as origin") +const errorNodeAlreadyShared = Error("Node already shared to this namespace") + +// Sharing is a join table to support sharing nodes between namespaces +type SharedNode struct { + gorm.Model + MachineID uint64 + Machine Machine + NamespaceID uint + Namespace Namespace +} + +// ShareNodeInNamespace adds a machine as a shared node to a namespace +func (h *Headscale) ShareNodeInNamespace(m *Machine, ns *Namespace) error { + if m.NamespaceID == ns.ID { + return errorSameNamespace + } + + sn := SharedNode{} + if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sn).Error; err == nil { + return errorNodeAlreadyShared + } + + sn = SharedNode{ + MachineID: m.ID, + Machine: *m, + NamespaceID: ns.ID, + Namespace: *ns, + } + h.db.Save(&sn) + + return nil +} From 48b73fa12fe69a2c1cd7a58ca32821946b049164 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 16:59:03 +0200 Subject: [PATCH 097/125] Implement node sharing functionality --- api.go | 5 +--- machine.go | 71 +++++++++++++++++++++++++++++++++++++++--------------- poll.go | 4 +-- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/api.go b/api.go index 621eeb8f..e2a56185 100644 --- a/api.go +++ b/api.go @@ -33,8 +33,6 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) { return } - // spew.Dump(c.Params) - c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` @@ -220,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() + node, err := m.toNode(true) if err != nil { log.Error(). Str("func", "getMapResponse"). @@ -280,7 +278,6 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac return nil, err } } - // spew.Dump(resp) // declare the incoming size on the first 4 bytes data := make([]byte, 4) binary.LittleEndian.PutUint32(data, uint32(len(respBody))) diff --git a/machine.go b/machine.go index 4cdadd98..a6f8d1fb 100644 --- a/machine.go +++ b/machine.go @@ -50,7 +50,7 @@ func (m Machine) isAlreadyRegistered() bool { return m.Registered } -func (m Machine) toNode() (*tailcfg.Node, error) { +func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { return nil, err @@ -85,24 +85,26 @@ func (m Machine) toNode() (*tailcfg.Node, error) { allowedIPs := []netaddr.IPPrefix{} allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients - routesStr := []string{} - if len(m.EnabledRoutes) != 0 { - allwIps, err := m.EnabledRoutes.MarshalJSON() - if err != nil { - return nil, err + if includeRoutes { + routesStr := []string{} + if len(m.EnabledRoutes) != 0 { + allwIps, err := m.EnabledRoutes.MarshalJSON() + if err != nil { + return nil, err + } + err = json.Unmarshal(allwIps, &routesStr) + if err != nil { + return nil, err + } } - err = json.Unmarshal(allwIps, &routesStr) - if err != nil { - return nil, err - } - } - for _, aip := range routesStr { - ip, err := netaddr.ParseIPPrefix(aip) - if err != nil { - return nil, err + for _, aip := range routesStr { + ip, err := netaddr.ParseIPPrefix(aip) + if err != nil { + return nil, err + } + allowedIPs = append(allowedIPs, ip) } - allowedIPs = append(allowedIPs, ip) } endpoints := []string{} @@ -136,13 +138,20 @@ func (m Machine) toNode() (*tailcfg.Node, error) { derp = "127.3.3.40:0" // Zero means disconnected or unknown. } + var keyExpiry time.Time + if m.Expiry != nil { + keyExpiry = *m.Expiry + } else { + keyExpiry = time.Time{} + } + 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, User: tailcfg.UserID(m.NamespaceID), Key: tailcfg.NodeKey(nKey), - KeyExpiry: *m.Expiry, + KeyExpiry: keyExpiry, Machine: tailcfg.MachineKey(mKey), DiscoKey: discoKey, Addresses: addrs, @@ -165,6 +174,7 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { Str("func", "getPeers"). Str("machine", m.Name). Msg("Finding peers") + machines := []Machine{} if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { @@ -172,9 +182,23 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { return nil, err } + // We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for + sharedNodes := []SharedNode{} + if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", + m.NamespaceID).Find(&sharedNodes).Error; err != nil { + return nil, err + } + peers := []*tailcfg.Node{} for _, mn := range machines { - peer, err := mn.toNode() + peer, err := mn.toNode(true) + if err != nil { + return nil, err + } + peers = append(peers, peer) + } + for _, sn := range sharedNodes { + peer, err := sn.Machine.toNode(false) // shared nodes do not expose their routes if err != nil { return nil, err } @@ -201,7 +225,7 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error) return &m, nil } } - return nil, fmt.Errorf("not found") + return nil, fmt.Errorf("machine not found") } // GetMachineByID finds a Machine by ID and returns the Machine struct @@ -260,7 +284,14 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { } func (h *Headscale) notifyChangesToPeers(m *Machine) { - peers, _ := h.getPeers(*m) + peers, err := h.getPeers(*m) + if err != nil { + log.Error(). + Str("func", "notifyChangesToPeers"). + Str("machine", m.Name). + Msgf("Error getting peers: %s", err) + return + } for _, p := range *peers { log.Info(). Str("func", "notifyChangesToPeers"). diff --git a/poll.go b/poll.go index bea16164..60bfa9ea 100644 --- a/poll.go +++ b/poll.go @@ -188,7 +188,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) { Msg("Finished stream, closing PollNetMap session") } -// PollNetMapStream takes care of /machine/:id/map +// PollNetMapStream takes care of /machine/:id/map // stream logic, ensuring we communicate updates and data // to the connected clients. func (h *Headscale) PollNetMapStream( @@ -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() + n, _ := m.toNode(true) err := h.sendRequestOnUpdateChannel(n) if err != nil { log.Error(). From 7010f5afad50a161597d6a8453a7dc3eede0540b Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 16:59:12 +0200 Subject: [PATCH 098/125] Added unit tests on sharing nodes --- sharing_nodes_test.go | 359 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 sharing_nodes_test.go diff --git a/sharing_nodes_test.go b/sharing_nodes_test.go new file mode 100644 index 00000000..2c8a7a13 --- /dev/null +++ b/sharing_nodes_test.go @@ -0,0 +1,359 @@ +package headscale + +import ( + "gopkg.in/check.v1" + "tailscale.com/tailcfg" +) + +func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) { + n1, err := h.CreateNamespace("shared1") + c.Assert(err, check.IsNil) + + n2, err := h.CreateNamespace("shared2") + c.Assert(err, check.IsNil) + + pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak2, err := h.CreatePreAuthKey(n2.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(pak1.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(pak2.ID), + } + h.db.Save(&m2) + + _, err = h.GetMachine(n2.Name, m2.Name) + c.Assert(err, check.IsNil) + + p1s, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1s), check.Equals, 0) + + err = h.ShareNodeInNamespace(&m2, n1) + c.Assert(err, check.IsNil) + + p1sAfter, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1sAfter), check.Equals, 1) + c.Assert((*p1sAfter)[0].ID, check.Equals, tailcfg.NodeID(m2.ID)) +} + +func (s *Suite) TestSameNamespace(c *check.C) { + n1, err := h.CreateNamespace("shared1") + c.Assert(err, check.IsNil) + + n2, err := h.CreateNamespace("shared2") + c.Assert(err, check.IsNil) + + pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak2, err := h.CreatePreAuthKey(n2.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(pak1.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(pak2.ID), + } + h.db.Save(&m2) + + _, err = h.GetMachine(n2.Name, m2.Name) + c.Assert(err, check.IsNil) + + p1s, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1s), check.Equals, 0) + + err = h.ShareNodeInNamespace(&m1, n1) + c.Assert(err, check.Equals, errorSameNamespace) +} + +func (s *Suite) TestAlreadyShared(c *check.C) { + n1, err := h.CreateNamespace("shared1") + c.Assert(err, check.IsNil) + + n2, err := h.CreateNamespace("shared2") + c.Assert(err, check.IsNil) + + pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak2, err := h.CreatePreAuthKey(n2.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(pak1.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(pak2.ID), + } + h.db.Save(&m2) + + _, err = h.GetMachine(n2.Name, m2.Name) + c.Assert(err, check.IsNil) + + p1s, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1s), check.Equals, 0) + + err = h.ShareNodeInNamespace(&m2, n1) + c.Assert(err, check.IsNil) + err = h.ShareNodeInNamespace(&m2, n1) + c.Assert(err, check.Equals, errorNodeAlreadyShared) +} + +func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) { + n1, err := h.CreateNamespace("shared1") + c.Assert(err, check.IsNil) + + n2, err := h.CreateNamespace("shared2") + c.Assert(err, check.IsNil) + + pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak2, err := h.CreatePreAuthKey(n2.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(pak1.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(pak2.ID), + } + h.db.Save(&m2) + + _, err = h.GetMachine(n2.Name, m2.Name) + c.Assert(err, check.IsNil) + + p1s, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1s), check.Equals, 0) + + err = h.ShareNodeInNamespace(&m2, n1) + c.Assert(err, check.IsNil) + + p1sAfter, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1sAfter), check.Equals, 1) + c.Assert(len((*p1sAfter)[0].AllowedIPs), check.Equals, 1) +} + +func (s *Suite) TestComplexSharingAcrossNamespaces(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) + + pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak3, err := h.CreatePreAuthKey(n3.Name, false, false, nil) + c.Assert(err, check.IsNil) + + pak4, 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(pak1.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(pak2.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(pak3.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(pak4.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 + + err = h.ShareNodeInNamespace(&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 + + pAlone, err := h.getPeers(m3) + c.Assert(err, check.IsNil) + c.Assert(len(*pAlone), check.Equals, 0) // node 3 is alone +} From 187b016d099257ec3d6c10604703ef375b8b500a Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 16:59:50 +0200 Subject: [PATCH 099/125] Added helper function to get list of shared nodes --- namespaces.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/namespaces.go b/namespaces.go index ff9eeacc..8d9543e4 100644 --- a/namespaces.go +++ b/namespaces.go @@ -91,12 +91,30 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) { } machines := []Machine{} - if err := h.db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { + if err := h.db.Preload("AuthKey").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { return nil, err } return &machines, nil } +// ListSharedMachinesInNamespaces returns all the machines that are shared to the specified namespace +func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) { + n, err := h.GetNamespace(name) + if err != nil { + return nil, err + } + sharedNodes := []SharedNode{} + if err := h.db.Preload("Namespace").Preload("Machine").Where(&SharedNode{NamespaceID: n.ID}).Find(&sharedNodes).Error; err != nil { + return nil, err + } + + machines := []Machine{} + for _, sn := range sharedNodes { + machines = append(machines, sn.Machine) + } + return &machines, nil +} + // SetMachineNamespace assigns a Machine to a namespace func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error { n, err := h.GetNamespace(namespaceName) From 4ba107a765d9a3cbec404834684db0c0bf1b9468 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 17:00:46 +0200 Subject: [PATCH 100/125] README updated --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index fef70207..712abe1a 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,12 @@ Headscale implements this coordination server. - [X] ACLs - [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [X] DNS (passing DNS servers to nodes) -- [ ] Share nodes between ~~users~~ namespaces +- [X] Share nodes between ~~users~~ namespaces - [ ] MagicDNS / Smart DNS ## Roadmap 🤷 -We are now focusing on adding integration tests with the official clients. - Suggestions/PRs welcomed! From d86de68b409131ec5c77b7f8d0865cffb66d5ed1 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 17:06:47 +0200 Subject: [PATCH 101/125] Show namespace in node list table --- cmd/headscale/cli/nodes.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index d72201c9..557d93ac 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -79,6 +79,12 @@ var listNodesCmd = &cobra.Command{ if err != nil { log.Fatalf("Error initializing: %s", err) } + + ns, err := h.GetNamespace(n) + if err != nil { + log.Fatalf("Error fetching namespace: %s", err) + } + machines, err := h.ListMachinesInNamespace(n) if strings.HasPrefix(o, "json") { JsonOutput(machines, err, o) @@ -89,7 +95,7 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error getting nodes: %s", err) } - d, err := nodesToPtables(*machines) + d, err := nodesToPtables(*ns, *machines) if err != nil { log.Fatalf("Error converting to table: %s", err) } @@ -145,8 +151,8 @@ var deleteNodeCmd = &cobra.Command{ }, } -func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { - d := pterm.TableData{{"ID", "Name", "NodeKey", "IP address", "Ephemeral", "Last seen", "Online"}} +func nodesToPtables(currNs headscale.Namespace, m []headscale.Machine) (pterm.TableData, error) { + d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}} for _, m := range m { var ephemeral bool @@ -169,7 +175,14 @@ func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { } else { online = pterm.LightRed("false") } - d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) + + var namespace string + if currNs.ID == m.NamespaceID { + namespace = pterm.LightMagenta(m.Namespace.Name) + } else { + namespace = pterm.LightYellow(currNs.Name) + } + d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), namespace, m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) } return d, nil } From 7287e0259ccad80cd4a5026245a445ed1a2e5380 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 17:08:39 +0200 Subject: [PATCH 102/125] Minor linting issues --- namespaces.go | 2 +- routes.go | 2 ++ sharing_nodes.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/namespaces.go b/namespaces.go index 8d9543e4..fb2db01a 100644 --- a/namespaces.go +++ b/namespaces.go @@ -97,7 +97,7 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) { return &machines, nil } -// ListSharedMachinesInNamespaces returns all the machines that are shared to the specified namespace +// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) { n, err := h.GetNamespace(name) if err != nil { diff --git a/routes.go b/routes.go index 28d86837..0ef01780 100644 --- a/routes.go +++ b/routes.go @@ -56,6 +56,7 @@ func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]n return routes, nil } +// IsNodeRouteEnabled checks if a certain route has been enabled func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool { route, err := netaddr.ParseIPPrefix(routeStr) if err != nil { @@ -129,6 +130,7 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr return nil } +// RoutesToPtables converts the list of routes to a nice table func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData { d := pterm.TableData{{"Route", "Enabled"}} diff --git a/sharing_nodes.go b/sharing_nodes.go index b52b900c..feab1fb2 100644 --- a/sharing_nodes.go +++ b/sharing_nodes.go @@ -5,7 +5,7 @@ import "gorm.io/gorm" const errorSameNamespace = Error("Destination namespace same as origin") const errorNodeAlreadyShared = Error("Node already shared to this namespace") -// Sharing is a join table to support sharing nodes between namespaces +// SharedNode is a join table to support sharing nodes between namespaces type SharedNode struct { gorm.Model MachineID uint64 From 7ce4738d8a2f0a9233f15f3f300cbc190e48c2e5 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 3 Sep 2021 10:23:26 +0200 Subject: [PATCH 103/125] Preload namespace so the name can be shown --- machine.go | 2 +- namespaces.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/machine.go b/machine.go index a6f8d1fb..bbacee5a 100644 --- a/machine.go +++ b/machine.go @@ -231,7 +231,7 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error) // GetMachineByID finds a Machine by ID and returns the Machine struct func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { m := Machine{} - if result := h.db.Find(&Machine{ID: id}).First(&m); result.Error != nil { + if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil { return nil, result.Error } return &m, nil diff --git a/namespaces.go b/namespaces.go index fb2db01a..57674ff3 100644 --- a/namespaces.go +++ b/namespaces.go @@ -104,13 +104,17 @@ func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, erro return nil, err } sharedNodes := []SharedNode{} - if err := h.db.Preload("Namespace").Preload("Machine").Where(&SharedNode{NamespaceID: n.ID}).Find(&sharedNodes).Error; err != nil { + if err := h.db.Preload("Namespace").Where(&SharedNode{NamespaceID: n.ID}).Find(&sharedNodes).Error; err != nil { return nil, err } machines := []Machine{} for _, sn := range sharedNodes { - machines = append(machines, sn.Machine) + m, err := h.GetMachineByID(sn.MachineID) // otherwise not everything comes filled + if err != nil { + return nil, err + } + machines = append(machines, *m) } return &machines, nil } From 7edd0cd14cd7f7e0b66a221467f70e0dde14b1f4 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 3 Sep 2021 10:23:45 +0200 Subject: [PATCH 104/125] Added add node cli --- cmd/headscale/cli/nodes.go | 72 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 557d93ac..33dd3f79 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -25,6 +25,7 @@ func init() { nodeCmd.AddCommand(listNodesCmd) nodeCmd.AddCommand(registerNodeCmd) nodeCmd.AddCommand(deleteNodeCmd) + nodeCmd.AddCommand(shareNodeCmd) } var nodeCmd = &cobra.Command{ @@ -86,8 +87,19 @@ var listNodesCmd = &cobra.Command{ } machines, err := h.ListMachinesInNamespace(n) + if err != nil { + log.Fatalf("Error fetching machines: %s", err) + } + + sharedMachines, err := h.ListSharedMachinesInNamespace(n) + if err != nil { + log.Fatalf("Error fetching shared machines: %s", err) + } + + allMachines := append(*machines, *sharedMachines...) + if strings.HasPrefix(o, "json") { - JsonOutput(machines, err, o) + JsonOutput(allMachines, err, o) return } @@ -95,7 +107,7 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error getting nodes: %s", err) } - d, err := nodesToPtables(*ns, *machines) + d, err := nodesToPtables(*ns, allMachines) if err != nil { log.Fatalf("Error converting to table: %s", err) } @@ -151,6 +163,60 @@ var deleteNodeCmd = &cobra.Command{ }, } +var shareNodeCmd = &cobra.Command{ + Use: "share ID namespace", + Short: "Shares a node from the current namespace to the specified one", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("missing parameters") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + n, err := cmd.Flags().GetString("namespace") + if err != nil { + log.Fatalf("Error getting namespace: %s", err) + } + o, _ := cmd.Flags().GetString("output") + + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + + _, err = h.GetNamespace(n) + if err != nil { + log.Fatalf("Error fetching origin namespace: %s", err) + } + + destNs, err := h.GetNamespace(args[1]) + if err != nil { + log.Fatalf("Error fetching destination namespace: %s", err) + } + + id, err := strconv.Atoi(args[0]) + if err != nil { + log.Fatalf("Error converting ID to integer: %s", err) + } + m, err := h.GetMachineByID(uint64(id)) + if err != nil { + log.Fatalf("Error getting node: %s", err) + } + + err = h.ShareNodeInNamespace(m, destNs) + if strings.HasPrefix(o, "json") { + JsonOutput(map[string]string{"Result": "Node shared"}, err, o) + return + } + if err != nil { + fmt.Printf("Error sharing node: %s\n", err) + return + } + + fmt.Println("Node shared!") + }, +} + func nodesToPtables(currNs headscale.Namespace, m []headscale.Machine) (pterm.TableData, error) { d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}} @@ -180,7 +246,7 @@ func nodesToPtables(currNs headscale.Namespace, m []headscale.Machine) (pterm.Ta if currNs.ID == m.NamespaceID { namespace = pterm.LightMagenta(m.Namespace.Name) } else { - namespace = pterm.LightYellow(currNs.Name) + namespace = pterm.LightYellow(m.Namespace.Name) } d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), namespace, m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) } From 729cd54401dc122cb19b72eb868f9eee98dd999c Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 6 Sep 2021 14:39:52 +0200 Subject: [PATCH 105/125] Renamed sharing function --- cmd/headscale/cli/nodes.go | 2 +- sharing_nodes.go | 4 ++-- sharing_nodes_test.go | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 33dd3f79..171e98d1 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -203,7 +203,7 @@ var shareNodeCmd = &cobra.Command{ log.Fatalf("Error getting node: %s", err) } - err = h.ShareNodeInNamespace(m, destNs) + err = h.AddSharedMachineToNamespace(m, destNs) if strings.HasPrefix(o, "json") { JsonOutput(map[string]string{"Result": "Node shared"}, err, o) return diff --git a/sharing_nodes.go b/sharing_nodes.go index feab1fb2..54d9976b 100644 --- a/sharing_nodes.go +++ b/sharing_nodes.go @@ -14,8 +14,8 @@ type SharedNode struct { Namespace Namespace } -// ShareNodeInNamespace adds a machine as a shared node to a namespace -func (h *Headscale) ShareNodeInNamespace(m *Machine, ns *Namespace) error { +// AddSharedMachineToNamespace adds a machine as a shared node to a namespace +func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error { if m.NamespaceID == ns.ID { return errorSameNamespace } diff --git a/sharing_nodes_test.go b/sharing_nodes_test.go index 2c8a7a13..7c3ff828 100644 --- a/sharing_nodes_test.go +++ b/sharing_nodes_test.go @@ -59,7 +59,7 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) { c.Assert(err, check.IsNil) c.Assert(len(*p1s), check.Equals, 0) - err = h.ShareNodeInNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(&m2, n1) c.Assert(err, check.IsNil) p1sAfter, err := h.getPeers(m1) @@ -122,7 +122,7 @@ func (s *Suite) TestSameNamespace(c *check.C) { c.Assert(err, check.IsNil) c.Assert(len(*p1s), check.Equals, 0) - err = h.ShareNodeInNamespace(&m1, n1) + err = h.AddSharedMachineToNamespace(&m1, n1) c.Assert(err, check.Equals, errorSameNamespace) } @@ -180,9 +180,9 @@ func (s *Suite) TestAlreadyShared(c *check.C) { c.Assert(err, check.IsNil) c.Assert(len(*p1s), check.Equals, 0) - err = h.ShareNodeInNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(&m2, n1) c.Assert(err, check.IsNil) - err = h.ShareNodeInNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(&m2, n1) c.Assert(err, check.Equals, errorNodeAlreadyShared) } @@ -240,7 +240,7 @@ func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) { c.Assert(err, check.IsNil) c.Assert(len(*p1s), check.Equals, 0) - err = h.ShareNodeInNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(&m2, n1) c.Assert(err, check.IsNil) p1sAfter, err := h.getPeers(m1) @@ -346,7 +346,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { c.Assert(err, check.IsNil) c.Assert(len(*p1s), check.Equals, 1) // nodes 1 and 4 - err = h.ShareNodeInNamespace(&m2, n1) + err = h.AddSharedMachineToNamespace(&m2, n1) c.Assert(err, check.IsNil) p1sAfter, err := h.getPeers(m1) From 75a342f96eda8ea0ebc12cda39a495dca219d2ab Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 6 Sep 2021 14:40:37 +0200 Subject: [PATCH 106/125] Renamed files --- sharing_nodes.go => sharing.go | 0 sharing_nodes_test.go => sharing_test.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename sharing_nodes.go => sharing.go (100%) rename sharing_nodes_test.go => sharing_test.go (100%) diff --git a/sharing_nodes.go b/sharing.go similarity index 100% rename from sharing_nodes.go rename to sharing.go diff --git a/sharing_nodes_test.go b/sharing_test.go similarity index 100% rename from sharing_nodes_test.go rename to sharing_test.go From 2780623076ba92647565e40b954fb3fb41c26956 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 6 Sep 2021 14:43:43 +0200 Subject: [PATCH 107/125] Renamed SharedNode to SharedMachine --- db.go | 2 +- machine.go | 2 +- namespaces.go | 4 ++-- sharing.go | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/db.go b/db.go index 4435f048..42c5eee9 100644 --- a/db.go +++ b/db.go @@ -44,7 +44,7 @@ func (h *Headscale) initDB() error { return err } - err = db.AutoMigrate(&SharedNode{}) + err = db.AutoMigrate(&SharedMachine{}) if err != nil { return err } diff --git a/machine.go b/machine.go index bbacee5a..c5a8a2a9 100644 --- a/machine.go +++ b/machine.go @@ -183,7 +183,7 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { } // We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for - sharedNodes := []SharedNode{} + sharedNodes := []SharedMachine{} if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", m.NamespaceID).Find(&sharedNodes).Error; err != nil { return nil, err diff --git a/namespaces.go b/namespaces.go index 57674ff3..e7d207b2 100644 --- a/namespaces.go +++ b/namespaces.go @@ -103,8 +103,8 @@ func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, erro if err != nil { return nil, err } - sharedNodes := []SharedNode{} - if err := h.db.Preload("Namespace").Where(&SharedNode{NamespaceID: n.ID}).Find(&sharedNodes).Error; err != nil { + sharedNodes := []SharedMachine{} + if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: n.ID}).Find(&sharedNodes).Error; err != nil { return nil, err } diff --git a/sharing.go b/sharing.go index 54d9976b..db16ef33 100644 --- a/sharing.go +++ b/sharing.go @@ -5,8 +5,8 @@ import "gorm.io/gorm" const errorSameNamespace = Error("Destination namespace same as origin") const errorNodeAlreadyShared = Error("Node already shared to this namespace") -// SharedNode is a join table to support sharing nodes between namespaces -type SharedNode struct { +// SharedMachine is a join table to support sharing nodes between namespaces +type SharedMachine struct { gorm.Model MachineID uint64 Machine Machine @@ -20,12 +20,12 @@ func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error return errorSameNamespace } - sn := SharedNode{} + sn := SharedMachine{} if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sn).Error; err == nil { return errorNodeAlreadyShared } - sn = SharedNode{ + sn = SharedMachine{ MachineID: m.ID, Machine: *m, NamespaceID: ns.ID, From 55f3e07bd45bbe8e7d84004fee9c17c857fb8aa8 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:26:46 +0200 Subject: [PATCH 108/125] Apply suggestions from code review Removed one letter variables Co-authored-by: Kristoffer Dalby --- cmd/headscale/cli/nodes.go | 32 ++++++++++++++++---------------- machine.go | 12 ++++++------ namespaces.go | 12 ++++++------ sharing.go | 8 ++++---- sharing_test.go | 2 +- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 171e98d1..623f7f8c 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -25,7 +25,7 @@ func init() { nodeCmd.AddCommand(listNodesCmd) nodeCmd.AddCommand(registerNodeCmd) nodeCmd.AddCommand(deleteNodeCmd) - nodeCmd.AddCommand(shareNodeCmd) + nodeCmd.AddCommand(shareMachineCmd) } var nodeCmd = &cobra.Command{ @@ -81,7 +81,7 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error initializing: %s", err) } - ns, err := h.GetNamespace(n) + namespace, err := h.GetNamespace(n) if err != nil { log.Fatalf("Error fetching namespace: %s", err) } @@ -107,7 +107,7 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error getting nodes: %s", err) } - d, err := nodesToPtables(*ns, allMachines) + d, err := nodesToPtables(*namespace, allMachines) if err != nil { log.Fatalf("Error converting to table: %s", err) } @@ -163,7 +163,7 @@ var deleteNodeCmd = &cobra.Command{ }, } -var shareNodeCmd = &cobra.Command{ +var shareMachineCmd = &cobra.Command{ Use: "share ID namespace", Short: "Shares a node from the current namespace to the specified one", Args: func(cmd *cobra.Command, args []string) error { @@ -173,23 +173,23 @@ var shareNodeCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - n, err := cmd.Flags().GetString("namespace") + namespace, err := cmd.Flags().GetString("namespace") if err != nil { log.Fatalf("Error getting namespace: %s", err) } - o, _ := cmd.Flags().GetString("output") + output, _ := cmd.Flags().GetString("output") h, err := getHeadscaleApp() if err != nil { log.Fatalf("Error initializing: %s", err) } - _, err = h.GetNamespace(n) + _, err = h.GetNamespace(namespace) if err != nil { log.Fatalf("Error fetching origin namespace: %s", err) } - destNs, err := h.GetNamespace(args[1]) + destinationNamespace, err := h.GetNamespace(args[1]) if err != nil { log.Fatalf("Error fetching destination namespace: %s", err) } @@ -198,12 +198,12 @@ var shareNodeCmd = &cobra.Command{ if err != nil { log.Fatalf("Error converting ID to integer: %s", err) } - m, err := h.GetMachineByID(uint64(id)) + machine, err := h.GetMachineByID(uint64(id)) if err != nil { log.Fatalf("Error getting node: %s", err) } - err = h.AddSharedMachineToNamespace(m, destNs) + err = h.AddSharedMachineToNamespace(machine, destinationNamespace) if strings.HasPrefix(o, "json") { JsonOutput(map[string]string{"Result": "Node shared"}, err, o) return @@ -217,10 +217,10 @@ var shareNodeCmd = &cobra.Command{ }, } -func nodesToPtables(currNs headscale.Namespace, m []headscale.Machine) (pterm.TableData, error) { +func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.Machine) (pterm.TableData, error) { d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}} - for _, m := range m { + for _, machine := range machines { var ephemeral bool if m.AuthKey != nil && m.AuthKey.Ephemeral { ephemeral = true @@ -243,12 +243,12 @@ func nodesToPtables(currNs headscale.Namespace, m []headscale.Machine) (pterm.Ta } var namespace string - if currNs.ID == m.NamespaceID { - namespace = pterm.LightMagenta(m.Namespace.Name) + if currentNamespace.ID == machine.NamespaceID { + namespace = pterm.LightMagenta(machine.Namespace.Name) } else { - namespace = pterm.LightYellow(m.Namespace.Name) + namespace = pterm.LightYellow(machine.Namespace.Name) } - d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), namespace, m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) + d = append(d, []string{strconv.FormatUint(machine.ID, 10), machine.Name, nodeKey.ShortString(), namespace, machine.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online}) } return d, nil } diff --git a/machine.go b/machine.go index c5a8a2a9..40fabee6 100644 --- a/machine.go +++ b/machine.go @@ -98,8 +98,8 @@ func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { } } - for _, aip := range routesStr { - ip, err := netaddr.ParseIPPrefix(aip) + for _, routeStr := range routesStr { + ip, err := netaddr.ParseIPPrefix(routeStr) if err != nil { return nil, err } @@ -183,9 +183,9 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { } // We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for - sharedNodes := []SharedMachine{} + sharedMachines := []SharedMachine{} if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", - m.NamespaceID).Find(&sharedNodes).Error; err != nil { + m.NamespaceID).Find(&sharedMachines).Error; err != nil { return nil, err } @@ -197,8 +197,8 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { } peers = append(peers, peer) } - for _, sn := range sharedNodes { - peer, err := sn.Machine.toNode(false) // shared nodes do not expose their routes + for _, sharedMachine := range sharedMachines { + peer, err := sharedMachine.Machine.toNode(false) // shared nodes do not expose their routes if err != nil { return nil, err } diff --git a/namespaces.go b/namespaces.go index e7d207b2..8204f96d 100644 --- a/namespaces.go +++ b/namespaces.go @@ -99,22 +99,22 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) { // ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) { - n, err := h.GetNamespace(name) + namespace, err := h.GetNamespace(name) if err != nil { return nil, err } - sharedNodes := []SharedMachine{} - if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: n.ID}).Find(&sharedNodes).Error; err != nil { + sharedMachines := []SharedMachine{} + if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil { return nil, err } machines := []Machine{} - for _, sn := range sharedNodes { - m, err := h.GetMachineByID(sn.MachineID) // otherwise not everything comes filled + for _, sharedMachine := range sharedMachines { + machine, err := h.GetMachineByID(sharedMachine.MachineID) // otherwise not everything comes filled if err != nil { return nil, err } - machines = append(machines, *m) + machines = append(machines, *machine) } return &machines, nil } diff --git a/sharing.go b/sharing.go index db16ef33..c507707a 100644 --- a/sharing.go +++ b/sharing.go @@ -21,17 +21,17 @@ func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error } sn := SharedMachine{} - if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sn).Error; err == nil { - return errorNodeAlreadyShared + if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil { + return errorMachineAlreadyShared } - sn = SharedMachine{ + sharedMachine = SharedMachine{ MachineID: m.ID, Machine: *m, NamespaceID: ns.ID, Namespace: *ns, } - h.db.Save(&sn) + h.db.Save(&sharedMachine) return nil } diff --git a/sharing_test.go b/sharing_test.go index 7c3ff828..ec4951de 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -183,7 +183,7 @@ func (s *Suite) TestAlreadyShared(c *check.C) { err = h.AddSharedMachineToNamespace(&m2, n1) c.Assert(err, check.IsNil) err = h.AddSharedMachineToNamespace(&m2, n1) - c.Assert(err, check.Equals, errorNodeAlreadyShared) + c.Assert(err, check.Equals, errorMachineAlreadyShared) } func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) { From b937f9b7629ab1c40277fb04a773774d5355e606 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:30:02 +0200 Subject: [PATCH 109/125] Update machine.go Added comment on toNode --- machine.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/machine.go b/machine.go index 40fabee6..3e9786a2 100644 --- a/machine.go +++ b/machine.go @@ -50,6 +50,8 @@ func (m Machine) isAlreadyRegistered() bool { return m.Registered } +// 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) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { From b098d84557318d30e6bb1aa907c82a4d55d6a6bb Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:32:06 +0200 Subject: [PATCH 110/125] Apply suggestions from code review Changed more variable names Co-authored-by: Kristoffer Dalby --- sharing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharing.go b/sharing.go index c507707a..98811e5d 100644 --- a/sharing.go +++ b/sharing.go @@ -3,7 +3,7 @@ package headscale import "gorm.io/gorm" const errorSameNamespace = Error("Destination namespace same as origin") -const errorNodeAlreadyShared = Error("Node already shared to this namespace") +const errorMachineAlreadyShared = Error("Node already shared to this namespace") // SharedMachine is a join table to support sharing nodes between namespaces type SharedMachine struct { From 4b4a5a4b93feeb7404c853ac45e97ad2a208c123 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:32:42 +0200 Subject: [PATCH 111/125] Update sharing.go Co-authored-by: Kristoffer Dalby --- sharing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharing.go b/sharing.go index 98811e5d..93c299c7 100644 --- a/sharing.go +++ b/sharing.go @@ -20,7 +20,7 @@ func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error return errorSameNamespace } - sn := SharedMachine{} + sharedMachine := SharedMachine{} if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil { return errorMachineAlreadyShared } From bd6adfaec6ff9bd199dc26314cc43812a7018830 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:37:01 +0200 Subject: [PATCH 112/125] Changes a few more variables --- cmd/headscale/cli/nodes.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 623f7f8c..07d21f33 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -204,8 +204,8 @@ var shareMachineCmd = &cobra.Command{ } err = h.AddSharedMachineToNamespace(machine, destinationNamespace) - if strings.HasPrefix(o, "json") { - JsonOutput(map[string]string{"Result": "Node shared"}, err, o) + if strings.HasPrefix(output, "json") { + JsonOutput(map[string]string{"Result": "Node shared"}, err, output) return } if err != nil { @@ -222,21 +222,21 @@ func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.M for _, machine := range machines { var ephemeral bool - if m.AuthKey != nil && m.AuthKey.Ephemeral { + if machine.AuthKey != nil && machine.AuthKey.Ephemeral { ephemeral = true } var lastSeen time.Time - if m.LastSeen != nil { - lastSeen = *m.LastSeen + if machine.LastSeen != nil { + lastSeen = *machine.LastSeen } - nKey, err := wgkey.ParseHex(m.NodeKey) + nKey, err := wgkey.ParseHex(machine.NodeKey) if err != nil { return nil, err } nodeKey := tailcfg.NodeKey(nKey) var online string - if m.LastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online + if machine.LastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online online = pterm.LightGreen("true") } else { online = pterm.LightRed("false") From 8acaea0fbe8a683d380c430f286eeb6073dd1395 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:44:27 +0200 Subject: [PATCH 113/125] Increased timeout --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 98dbc464..0961297a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: # golangci-lint manually in the `Run lint` step. - uses: golangci/golangci-lint-action@v2 with: - args: --timeout 2m + args: --timeout 4m # Setup Go - name: Setup Go From c4e6ad1ec788fab5fee7f4b3bc6355154f0a154e Mon Sep 17 00:00:00 2001 From: Juan Font Date: Fri, 10 Sep 2021 00:52:08 +0200 Subject: [PATCH 114/125] Fixed some typos --- cmd/headscale/cli/nodes.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 98dea9aa..5f30dc1b 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -228,7 +228,7 @@ func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.M var lastSeen time.Time var lastSeenTime string if machine.LastSeen != nil { - lastSeen = *m.LastSeen + lastSeen = *machine.LastSeen lastSeenTime = lastSeen.Format("2006-01-02 15:04:05") } nKey, err := wgkey.ParseHex(machine.NodeKey) @@ -239,8 +239,7 @@ func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.M var online string if lastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online - online = pter - LightGreen("true") + online = pterm.LightGreen("true") } else { online = pterm.LightRed("false") } From 11fbef4bf072b4fc696be9f5670752cc7f554b3d Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 11 Sep 2021 23:21:45 +0200 Subject: [PATCH 115/125] Added extra timeout --- .github/workflows/lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0961297a..d1c21f72 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,4 +36,6 @@ jobs: sudo apt install -y make - name: Run lint + with: + args: --timeout 4m run: make lint From 203e6bc6b2ef60fdc41e243fb5fd2608cc18024f Mon Sep 17 00:00:00 2001 From: Aaron Bieber Date: Sun, 12 Sep 2021 07:30:35 -0600 Subject: [PATCH 116/125] Remove trace lines about NO_COLOR. --- cmd/headscale/headscale.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index ca443216..e4334e4f 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -22,14 +22,12 @@ func main() { colors = true default: // no color, return text as is. - log.Trace().Msg("Colors are not supported, disabling") colors = false } // Adhere to no-color.org manifesto of allowing users to // turn off color in cli/services if _, noColorIsSet := os.LookupEnv("NO_COLOR"); noColorIsSet { - log.Trace().Msg("NO_COLOR is set, disabling colors") colors = false } From 3b97c7bdecfaca7dcbaa02698c144aa298d476ec Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Sun, 22 Aug 2021 21:47:31 +0200 Subject: [PATCH 117/125] gitignore: add jetbrains --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3a64648f..36c91e58 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ config.json *.key /db.sqlite *.sqlite3 + +# Exclude Jetbrains Editors +.idea From 18867a4c8404acf9d60e752bee9c555451007de1 Mon Sep 17 00:00:00 2001 From: ohdearaugustin Date: Sun, 12 Sep 2021 18:08:03 +0200 Subject: [PATCH 118/125] update docu --- README.md | 28 ++++++++++++++++++++++++++-- k8s/README.md | 1 - 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb42b666..c13a5fcc 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,16 @@ Suggestions/PRs welcomed! ## Running it -1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH - +1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH or use the docker container + + ```shell + docker pull headscale/headscale:x.x.x + ``` + + or + ```shell + docker pull ghrc.io/juanfont/headscale:x.x.x + ``` 2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running @@ -65,11 +73,19 @@ Suggestions/PRs welcomed! ```shell headscale namespaces create myfirstnamespace ``` + or docker: + ```shell + docker run -v ./private.key:/private.key -v ./config.json:/config.json headscale/headscale:x.x.x headscale namespace create myfirstnamespace + ``` 5. Run the server ```shell headscale serve ``` + or docker: + ```shell + docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derb.yaml:/derb.yaml -p 127.0.0.1:8080:8080 headscale/headscale:x.x.x headscale serve + ``` 6. If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder ```shell @@ -89,6 +105,10 @@ Suggestions/PRs welcomed! ```shell headscale -n myfirstnamespace node register YOURMACHINEKEY ``` + or docker: + ```shell + docker run -v ./private.key:/private.key -v ./config.json:/config.json headscale/headscale:x.x.x headscale -n myfirstnamespace node register YOURMACHINEKEY + ``` Alternatively, you can use Auth Keys to register your machines: @@ -96,6 +116,10 @@ Alternatively, you can use Auth Keys to register your machines: ```shell headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h ``` + or docker: + ```shell + docker run -v ./private.key:/private.key -v ./config.json:/config.json headscale/headscale:x.x.x headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h + ``` 2. Use the authkey from your machine to register it ```shell diff --git a/k8s/README.md b/k8s/README.md index 0f60794c..2f187abb 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -89,7 +89,6 @@ Use "headscale [command] --help" for more information about a command. # TODO / Ideas -- Github action to publish the docker image - Interpolate `email:` option to the ClusterIssuer from site configuration. This probably needs to be done with a transformer, kustomize vars don't seem to work. - Add kustomize examples for cloud-native ingress, load balancer From 3095c1e15087c284ba7285f848a88044458068b1 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 13 Sep 2021 22:45:31 +0200 Subject: [PATCH 119/125] Trying to correct Actions issues --- .github/workflows/lint.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d1c21f72..98dbc464 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: # golangci-lint manually in the `Run lint` step. - uses: golangci/golangci-lint-action@v2 with: - args: --timeout 4m + args: --timeout 2m # Setup Go - name: Setup Go @@ -36,6 +36,4 @@ jobs: sudo apt install -y make - name: Run lint - with: - args: --timeout 4m run: make lint From 31cc61478fc67b7982cf07e0f78951b31480ae30 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 13 Sep 2021 22:47:38 +0200 Subject: [PATCH 120/125] More timeout in linting --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 98dbc464..8e631fd4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: # golangci-lint manually in the `Run lint` step. - uses: golangci/golangci-lint-action@v2 with: - args: --timeout 2m + args: --timeout 10m # Setup Go - name: Setup Go From eae1b6a3de43b4f81e2d57040278c48bded73726 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 13 Sep 2021 22:51:58 +0200 Subject: [PATCH 121/125] More timeout in linting --- .github/workflows/lint.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8e631fd4..10167eef 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: # golangci-lint manually in the `Run lint` step. - uses: golangci/golangci-lint-action@v2 with: - args: --timeout 10m + args: --timeout 5m # Setup Go - name: Setup Go @@ -37,3 +37,5 @@ jobs: - name: Run lint run: make lint + with: + args: --timeout 10m From acc43c39af6ca1fb6c022057858a14041b343835 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 13 Sep 2021 22:58:35 +0200 Subject: [PATCH 122/125] Increased linter timeout in makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8adf760f..482ae868 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ coverprofile_html: lint: golint - golangci-lint run + golangci-lint run --timeout 5m compress: build upx --brute headscale From 536e8b71bf6b8aaeb376d67b757b74cf0d7a8974 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 13 Sep 2021 22:59:33 +0200 Subject: [PATCH 123/125] Removed wrong syntax in actions --- .github/workflows/lint.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 10167eef..c0286571 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,5 +37,3 @@ jobs: - name: Run lint run: make lint - with: - args: --timeout 10m From 8f1f48b7d03f5dd8fad81b85ca2b378afc1c9438 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Mon, 13 Sep 2021 23:11:15 +0200 Subject: [PATCH 124/125] Update README.md Remove Google registry for the time being --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f432c182..c4ff7bc2 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ Suggestions/PRs welcomed! ```shell docker pull headscale/headscale:x.x.x ``` - + 2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running From e154e7a0fb0d4de7ad668f3cce6009e9369bc8f9 Mon Sep 17 00:00:00 2001 From: Felix Kronlage-Dammers Date: Sun, 19 Sep 2021 12:07:17 +0200 Subject: [PATCH 125/125] fix typo, it is 'relayed' not 'relied' --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4ff7bc2..1a60db18 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Headscale implements this coordination server. - [x] Base functionality (nodes can communicate with each other) - [x] Node registration through the web flow -- [x] Network changes are relied to the nodes +- [x] Network changes are relayed to the nodes - [x] Namespace support (~equivalent to multi-user in Tailscale.com) - [x] Routing (advertise & accept, including exit nodes) - [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)