From 1ecd0d7ca4a9ef453ec3f2a67349f954b8c47d54 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Thu, 2 Sep 2021 16:57:26 +0200 Subject: [PATCH 01/20] 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 0630252..4435f04 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 0000000..b52b900 --- /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 02/20] 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 621eeb8..e2a5618 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 4cdadd9..a6f8d1f 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 bea1616..60bfa9e 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 03/20] 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 0000000..2c8a7a1 --- /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 04/20] 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 ff9eeac..8d9543e 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 05/20] README updated --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index fef7020..712abe1 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 06/20] 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 d72201c..557d93a 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 07/20] 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 8d9543e..fb2db01 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 28d8683..0ef0178 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 b52b900..feab1fb 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 08/20] 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 a6f8d1f..bbacee5 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 fb2db01..57674ff 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 09/20] 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 557d93a..33dd3f7 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 10/20] 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 33dd3f7..171e98d 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 feab1fb..54d9976 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 2c8a7a1..7c3ff82 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 11/20] 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 12/20] 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 4435f04..42c5eee 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 bbacee5..c5a8a2a 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 57674ff..e7d207b 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 54d9976..db16ef3 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 13/20] 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 171e98d..623f7f8 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 c5a8a2a..40fabee 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 e7d207b..8204f96 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 db16ef3..c507707 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 7c3ff82..ec4951d 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 14/20] Update machine.go Added comment on toNode --- machine.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/machine.go b/machine.go index 40fabee..3e9786a 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 15/20] 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 c507707..98811e5 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 16/20] 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 98811e5..93c299c 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 17/20] 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 623f7f8..07d21f3 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 18/20] 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 98dbc46..0961297 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 19/20] 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 98dea9a..5f30dc1 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 20/20] 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 0961297..d1c21f7 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