diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 98dbc464..d1c21f72 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 @@ -36,4 +36,6 @@ jobs: sudo apt install -y make - name: Run lint + with: + args: --timeout 4m run: make lint 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! 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/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 7afc6026..5f30dc1b 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(shareMachineCmd) } var nodeCmd = &cobra.Command{ @@ -79,9 +80,26 @@ var listNodesCmd = &cobra.Command{ if err != nil { log.Fatalf("Error initializing: %s", err) } + + namespace, err := h.GetNamespace(n) + if err != nil { + log.Fatalf("Error fetching namespace: %s", err) + } + 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 } @@ -89,7 +107,7 @@ var listNodesCmd = &cobra.Command{ log.Fatalf("Error getting nodes: %s", err) } - d, err := nodesToPtables(*machines) + d, err := nodesToPtables(*namespace, allMachines) if err != nil { log.Fatalf("Error converting to table: %s", err) } @@ -145,21 +163,75 @@ var deleteNodeCmd = &cobra.Command{ }, } -func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { - d := pterm.TableData{{"ID", "Name", "NodeKey", "IP address", "Ephemeral", "Last seen", "Online"}} +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 { + if len(args) < 2 { + return fmt.Errorf("missing parameters") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + log.Fatalf("Error getting namespace: %s", err) + } + output, _ := cmd.Flags().GetString("output") - for _, m := range m { + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + + _, err = h.GetNamespace(namespace) + if err != nil { + log.Fatalf("Error fetching origin namespace: %s", err) + } + + destinationNamespace, 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) + } + machine, err := h.GetMachineByID(uint64(id)) + if err != nil { + log.Fatalf("Error getting node: %s", err) + } + + err = h.AddSharedMachineToNamespace(machine, destinationNamespace) + if strings.HasPrefix(output, "json") { + JsonOutput(map[string]string{"Result": "Node shared"}, err, output) + return + } + if err != nil { + fmt.Printf("Error sharing node: %s\n", err) + return + } + + fmt.Println("Node shared!") + }, +} + +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 _, 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 var lastSeenTime string - if m.LastSeen != nil { - lastSeen = *m.LastSeen + if machine.LastSeen != nil { + lastSeen = *machine.LastSeen lastSeenTime = lastSeen.Format("2006-01-02 15:04:05") } - nKey, err := wgkey.ParseHex(m.NodeKey) + nKey, err := wgkey.ParseHex(machine.NodeKey) if err != nil { return nil, err } @@ -171,7 +243,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), lastSeenTime, online}) + + var namespace string + if currentNamespace.ID == machine.NamespaceID { + namespace = pterm.LightMagenta(machine.Namespace.Name) + } else { + namespace = pterm.LightYellow(machine.Namespace.Name) + } + d = append(d, []string{strconv.FormatUint(machine.ID, 10), machine.Name, nodeKey.ShortString(), namespace, machine.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online}) } return d, nil } diff --git a/db.go b/db.go index 06302523..42c5eee9 100644 --- a/db.go +++ b/db.go @@ -44,6 +44,11 @@ func (h *Headscale) initDB() error { return err } + err = db.AutoMigrate(&SharedMachine{}) + if err != nil { + return err + } + err = h.setValue("db_version", dbVersion) return err } diff --git a/machine.go b/machine.go index 4cdadd98..3e9786a2 100644 --- a/machine.go +++ b/machine.go @@ -50,7 +50,9 @@ func (m Machine) isAlreadyRegistered() bool { return m.Registered } -func (m Machine) toNode() (*tailcfg.Node, error) { +// toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes +// as per the expected behaviour in the official SaaS +func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) { nKey, err := wgkey.ParseHex(m.NodeKey) if err != nil { return nil, err @@ -85,24 +87,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 _, routeStr := range routesStr { + ip, err := netaddr.ParseIPPrefix(routeStr) + if err != nil { + return nil, err + } + allowedIPs = append(allowedIPs, ip) } - allowedIPs = append(allowedIPs, ip) } endpoints := []string{} @@ -136,13 +140,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 +176,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 +184,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 + sharedMachines := []SharedMachine{} + if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?", + m.NamespaceID).Find(&sharedMachines).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 _, sharedMachine := range sharedMachines { + peer, err := sharedMachine.Machine.toNode(false) // shared nodes do not expose their routes if err != nil { return nil, err } @@ -201,13 +227,13 @@ 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 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 @@ -260,7 +286,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/namespaces.go b/namespaces.go index ff9eeacc..8204f96d 100644 --- a/namespaces.go +++ b/namespaces.go @@ -91,12 +91,34 @@ 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 } +// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace +func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) { + namespace, err := h.GetNamespace(name) + if err != nil { + return nil, err + } + sharedMachines := []SharedMachine{} + if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil { + return nil, err + } + + machines := []Machine{} + for _, sharedMachine := range sharedMachines { + machine, err := h.GetMachineByID(sharedMachine.MachineID) // otherwise not everything comes filled + if err != nil { + return nil, err + } + machines = append(machines, *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) 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(). 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.go b/sharing.go new file mode 100644 index 00000000..93c299c7 --- /dev/null +++ b/sharing.go @@ -0,0 +1,37 @@ +package headscale + +import "gorm.io/gorm" + +const errorSameNamespace = Error("Destination namespace same as origin") +const errorMachineAlreadyShared = Error("Node already shared to this namespace") + +// SharedMachine is a join table to support sharing nodes between namespaces +type SharedMachine struct { + gorm.Model + MachineID uint64 + Machine Machine + NamespaceID uint + Namespace Namespace +} + +// 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 + } + + sharedMachine := SharedMachine{} + if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil { + return errorMachineAlreadyShared + } + + sharedMachine = SharedMachine{ + MachineID: m.ID, + Machine: *m, + NamespaceID: ns.ID, + Namespace: *ns, + } + h.db.Save(&sharedMachine) + + return nil +} diff --git a/sharing_test.go b/sharing_test.go new file mode 100644 index 00000000..ec4951de --- /dev/null +++ b/sharing_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.AddSharedMachineToNamespace(&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.AddSharedMachineToNamespace(&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.AddSharedMachineToNamespace(&m2, n1) + c.Assert(err, check.IsNil) + err = h.AddSharedMachineToNamespace(&m2, n1) + c.Assert(err, check.Equals, errorMachineAlreadyShared) +} + +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.AddSharedMachineToNamespace(&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.AddSharedMachineToNamespace(&m2, n1) + c.Assert(err, check.IsNil) + + p1sAfter, err := h.getPeers(m1) + c.Assert(err, check.IsNil) + c.Assert(len(*p1sAfter), check.Equals, 2) // nodes 1, 2, 4 + + pAlone, err := h.getPeers(m3) + c.Assert(err, check.IsNil) + c.Assert(len(*pAlone), check.Equals, 0) // node 3 is alone +}