From b7655b1f685ba06a1e223d9d921ae16f236a0c37 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 28 Feb 2021 00:58:09 +0100 Subject: [PATCH] Initial multi-user support using namespaces --- api.go | 65 +++++++++++--------- app.go | 7 ++- cmd/headscale/headscale.go | 55 ++++++++++++++++- db.go | 1 + machine.go | 20 +++--- namespaces.go | 122 +++++++++++++++++++++++++++++++++++++ 6 files changed, 229 insertions(+), 41 deletions(-) create mode 100644 namespaces.go diff --git a/api.go b/api.go index dfda0c2..e78bdb9 100644 --- a/api.go +++ b/api.go @@ -18,10 +18,44 @@ import ( "tailscale.com/wgengine/wgcfg" ) +// KeyHandler provides the Headscale pub key +// Listens in /key func (h *Headscale) KeyHandler(c *gin.Context) { c.Data(200, "text/plain; charset=utf-8", []byte(h.publicKey.HexString())) } +// RegisterWebAPI shows a simple message in the browser to point to the CLI +// Listens in /register +func (h *Headscale) RegisterWebAPI(c *gin.Context) { + mKeyStr := c.Query("key") + if mKeyStr == "" { + c.String(http.StatusBadRequest, "Wrong params") + return + } + + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` + + +

headscale

+

+ Run the command below in the headscale server to add this machine to your network: +

+ +

+ + headscale register %s + +

+ + + + + `, mKeyStr))) + return +} + +// RegistrationHandler handles the actual registration process of a machine +// Endpoint /machine/:id func (h *Headscale) RegistrationHandler(c *gin.Context) { body, _ := io.ReadAll(c.Request.Body) mKeyStr := c.Param("id") @@ -59,6 +93,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { if m.Registered { log.Println("Client is registered and we have the current key. All clear to /map") resp.AuthURL = "" + resp.User = *m.Namespace.toUser() respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Printf("Cannot encode message: %s", err) @@ -89,6 +124,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { m.NodeKey = wgcfg.Key(req.NodeKey).HexString() db.Save(&m) resp.AuthURL = "" + resp.User = *m.Namespace.toUser() respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Printf("Cannot encode message: %s", err) @@ -318,35 +354,6 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgcfg.Key, req tailcfg.MapReque return &data, nil } -// RegisterWebAPI shows a simple message in the browser to point to the CLI -func (h *Headscale) RegisterWebAPI(c *gin.Context) { - mKeyStr := c.Query("key") - if mKeyStr == "" { - c.String(http.StatusBadRequest, "Wrong params") - return - } - - c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` - - -

headscale

-

- Run the command below in the headscale server to add this machine to your network: -

- -

- - headscale register %s - -

- - - - - `, mKeyStr))) - return -} - func (h *Headscale) handleNewServer(c *gin.Context, db *gorm.DB, idKey wgcfg.Key, req tailcfg.RegisterRequest) { mNew := Machine{ MachineKey: idKey.HexString(), diff --git a/app.go b/app.go index a6c10ec..b29ff98 100644 --- a/app.go +++ b/app.go @@ -74,7 +74,11 @@ func (h *Headscale) Serve() error { } // RegisterMachine is executed from the CLI to register a new Machine using its MachineKey -func (h *Headscale) RegisterMachine(key string) error { +func (h *Headscale) RegisterMachine(key string, namespace string) error { + ns, err := h.GetNamespace(namespace) + if err != nil { + return err + } mKey, err := wgcfg.ParseHexKey(key) if err != nil { log.Printf("Cannot parse client key: %s", err) @@ -103,6 +107,7 @@ func (h *Headscale) RegisterMachine(key string) error { return err } m.IPAddress = ip.String() + m.NamespaceID = ns.ID m.Registered = true db.Save(&m) fmt.Println("Machine registered 🎉") diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index 3b3ea12..b300c42 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -50,8 +50,31 @@ var serveCmd = &cobra.Command{ } var registerCmd = &cobra.Command{ - Use: "register machineID", + Use: "register machineID namespace", Short: "Registers a machine to your network", + 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) { + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + h.RegisterMachine(args[0], args[1]) + }, +} + +var namespaceCmd = &cobra.Command{ + Use: "namespace", + Short: "Manage the namespaces of Headscale", +} + +var createNamespaceCmd = &cobra.Command{ + Use: "create NAME", + Short: "Creates a new namespace", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("Missing parameters") @@ -63,7 +86,32 @@ var registerCmd = &cobra.Command{ if err != nil { log.Fatalf("Error initializing: %s", err) } - h.RegisterMachine(args[0]) + _, err = h.CreateNamespace(args[0]) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("Ook.\n") + }, +} + +var listNamespacesCmd = &cobra.Command{ + Use: "list", + Short: "Creates a new namespace", + Run: func(cmd *cobra.Command, args []string) { + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + ns, err := h.ListNamespaces() + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("ID\tName\n") + for _, n := range *ns { + fmt.Printf("%d\t%s\n", n.ID, n.Name) + } }, } @@ -79,6 +127,9 @@ func main() { headscaleCmd.AddCommand(versionCmd) headscaleCmd.AddCommand(serveCmd) headscaleCmd.AddCommand(registerCmd) + headscaleCmd.AddCommand(namespaceCmd) + namespaceCmd.AddCommand(createNamespaceCmd) + namespaceCmd.AddCommand(listNamespacesCmd) if err := headscaleCmd.Execute(); err != nil { fmt.Println(err) diff --git a/db.go b/db.go index b905782..7e1e0cc 100644 --- a/db.go +++ b/db.go @@ -23,6 +23,7 @@ func (h *Headscale) initDB() error { db.Exec("create extension if not exists \"uuid-ossp\";") db.AutoMigrate(&Machine{}) db.AutoMigrate(&KV{}) + db.AutoMigrate(&Namespace{}) db.Close() h.setValue("db_version", dbVersion) diff --git a/machine.go b/machine.go index 648478a..ea15cb2 100644 --- a/machine.go +++ b/machine.go @@ -16,12 +16,14 @@ import ( // Machine is a Headscale client type Machine struct { - ID uint64 `gorm:"primary_key"` - MachineKey string `gorm:"type:varchar(64);unique_index"` - NodeKey string - DiscoKey string - IPAddress string - Name string + ID uint64 `gorm:"primary_key"` + MachineKey string `gorm:"type:varchar(64);unique_index"` + NodeKey string + DiscoKey string + IPAddress string + Name string + NamespaceID uint + Namespace Namespace Registered bool // temp LastSeen *time.Time @@ -106,7 +108,7 @@ func (m Machine) toNode() (*tailcfg.Node, error) { 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 permantent Name: hostinfo.Hostname, - User: 1, + User: tailcfg.UserID(m.NamespaceID), Key: tailcfg.NodeKey(nKey), KeyExpiry: *m.Expiry, Machine: tailcfg.MachineKey(mKey), @@ -136,9 +138,9 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { } defer db.Close() - // Add user management here machines := []Machine{} - if err = db.Where("machine_key <> ? AND registered", m.MachineKey).Find(&machines).Error; err != nil { + if err = 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) return nil, err } diff --git a/namespaces.go b/namespaces.go new file mode 100644 index 0000000..4c9132b --- /dev/null +++ b/namespaces.go @@ -0,0 +1,122 @@ +package headscale + +import ( + "fmt" + "log" + "time" + + "github.com/jinzhu/gorm" + "tailscale.com/tailcfg" +) + +// Namespace is the way Headscale implements the concept of users in Tailscale +// +// At the end of the day, users in Tailscale are some kind of 'bubbles' or namespaces +// that contain our machines. +type Namespace struct { + gorm.Model + Name string `gorm:"unique"` +} + +// CreateNamespace creates a new Namespace. Returns error if could not be created +// or another namespace already exists +func (h *Headscale) CreateNamespace(name string) (*Namespace, error) { + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return nil, err + } + defer db.Close() + + n := Namespace{} + if err := db.Where("name = ?", name).First(&n).Error; err == nil { + return nil, fmt.Errorf("Namespace already exists") + } + n.Name = name + if err := db.Create(&n).Error; err != nil { + log.Printf("Could not create row: %s", err) + return nil, err + } + return &n, nil +} + +// GetNamespace fetches a namespace by name +func (h *Headscale) GetNamespace(name string) (*Namespace, error) { + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return nil, err + } + defer db.Close() + + n := Namespace{} + if db.First(&n, "name = ?", name).RecordNotFound() { + return nil, fmt.Errorf("Namespace not found") + } + return &n, nil +} + +// ListNamespaces gets all the existing namespaces +func (h *Headscale) ListNamespaces() (*[]Namespace, error) { + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return nil, err + } + defer db.Close() + namespaces := []Namespace{} + if err := db.Find(&namespaces).Error; err != nil { + return nil, err + } + return &namespaces, nil +} + +// ListMachinesInNamespace gets all the nodes in a given namespace +func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) { + n, err := h.GetNamespace(name) + if err != nil { + return nil, err + } + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return nil, err + } + defer db.Close() + + machines := []Machine{} + if err := db.Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { + return nil, err + } + return &machines, nil +} + +func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error { + n, err := h.GetNamespace(namespaceName) + if err != nil { + return err + } + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return err + } + defer db.Close() + m.NamespaceID = n.ID + db.Save(&m) + return nil +} + +func (n *Namespace) toUser() *tailcfg.User { + u := tailcfg.User{ + ID: tailcfg.UserID(n.ID), + LoginName: "", + DisplayName: n.Name, + ProfilePicURL: "", + Domain: "", + Logins: []tailcfg.LoginID{}, + Roles: []tailcfg.RoleID{}, + Created: time.Time{}, + } + return &u +}