From 486faa9656e622c82e768169dd2d5c6cc063c0a0 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Wed, 5 May 2021 23:00:04 +0200 Subject: [PATCH 1/9] WIP Working on authkeys + tests --- preauth_keys.go | 25 +++++++++++++++++++++++++ preauth_keys_test.go | 21 +++++++++++++++++++++ utils.go | 4 ++++ 3 files changed, 50 insertions(+) diff --git a/preauth_keys.go b/preauth_keys.go index de89b04..4f6ec4d 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -7,6 +7,9 @@ import ( "time" ) +const errorAuthKeyNotFound = Error("AuthKey not found") +const errorAuthKeyExpired = Error("AuthKey expired") + // PreAuthKey describes a pre-authorization key usable in a particular namespace type PreAuthKey struct { ID uint64 `gorm:"primary_key"` @@ -72,6 +75,28 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error) return &keys, 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) { + db, err := h.db() + if err != nil { + return nil, err + } + defer db.Close() + + pak := PreAuthKey{} + if db.First(&pak, "key = ?", k).RecordNotFound() { + return nil, errorAuthKeyNotFound + } + + if pak.Expiration != nil && pak.Expiration.Before(time.Now()) { + return nil, errorAuthKeyExpired + } + + // missing here validation on current usage + return &pak, nil +} + func (h *Headscale) generateKey() (string, error) { size := 24 bytes := make([]byte, size) diff --git a/preauth_keys_test.go b/preauth_keys_test.go index 5ac3bcf..4a98dbe 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "testing" + "time" _ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver @@ -48,6 +49,7 @@ func (s *Suite) TearDownSuite(c *check.C) { func (*Suite) TestCreatePreAuthKey(c *check.C) { _, err := h.CreatePreAuthKey("bogus", true, nil) + c.Assert(err, check.NotNil) n, err := h.CreateNamespace("test") @@ -73,3 +75,22 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) { // Make sure the Namespace association is populated c.Assert((*keys)[0].Namespace.Name, check.Equals, n.Name) } + +func (*Suite) TestExpiredPreAuthKey(c *check.C) { + n, err := h.CreateNamespace("test2") + c.Assert(err, check.IsNil) + + now := time.Now() + pak, err := h.CreatePreAuthKey(n.Name, true, &now) + c.Assert(err, check.IsNil) + + p, err := h.checkKeyValidity(pak.Key) + c.Assert(err, check.Equals, errorAuthKeyExpired) + c.Assert(p, check.IsNil) +} + +func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) { + p, err := h.checkKeyValidity("potatoKey") + c.Assert(err, check.Equals, errorAuthKeyNotFound) + c.Assert(p, check.IsNil) +} diff --git a/utils.go b/utils.go index e787b1d..f85a4ff 100644 --- a/utils.go +++ b/utils.go @@ -21,6 +21,10 @@ import ( "tailscale.com/wgengine/wgcfg" ) +type Error string + +func (e Error) Error() string { return string(e) } + func decode(msg []byte, v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) error { return decodeMsg(msg, v, pubKey, privKey) } From 3110dd15756af6718dadf5376b7f5cfc05017d51 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 00:08:36 +0200 Subject: [PATCH 2/9] Added fields in Machine to store authkey + validation tests --- machine.go | 10 ++++-- preauth_keys.go | 14 ++++++++ preauth_keys_test.go | 84 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/machine.go b/machine.go index d72e660..b67c2a6 100644 --- a/machine.go +++ b/machine.go @@ -25,9 +25,13 @@ type Machine struct { NamespaceID uint Namespace Namespace - Registered bool // temp - LastSeen *time.Time - Expiry *time.Time + Registered bool // temp + RegisterMethod string + AuthKeyID uint + AuthKey *PreAuthKey + + LastSeen *time.Time + Expiry *time.Time HostInfo postgres.Jsonb Endpoints postgres.Jsonb diff --git a/preauth_keys.go b/preauth_keys.go index 4f6ec4d..11803b5 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -9,6 +9,7 @@ import ( const errorAuthKeyNotFound = Error("AuthKey not found") const errorAuthKeyExpired = Error("AuthKey expired") +const errorAuthKeyNotReusableAlreadyUsed = Error("AuthKey not reusable already used") // PreAuthKey describes a pre-authorization key usable in a particular namespace type PreAuthKey struct { @@ -93,6 +94,19 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { return nil, errorAuthKeyExpired } + if pak.Reusable { // we don't need to check if has been used before + return &pak, nil + } + + machines := []Machine{} + if err := db.Preload("AuthKey").Where(&Machine{AuthKeyID: uint(pak.ID)}).Find(&machines).Error; err != nil { + return nil, err + } + + if len(machines) != 0 { + return nil, errorAuthKeyNotReusableAlreadyUsed + } + // missing here validation on current usage return &pak, nil } diff --git a/preauth_keys_test.go b/preauth_keys_test.go index 4a98dbe..cf13bb9 100644 --- a/preauth_keys_test.go +++ b/preauth_keys_test.go @@ -94,3 +94,87 @@ func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) { c.Assert(err, check.Equals, errorAuthKeyNotFound) c.Assert(p, check.IsNil) } + +func (*Suite) TestValidateKeyOk(c *check.C) { + n, err := h.CreateNamespace("test3") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, true, nil) + c.Assert(err, check.IsNil) + + p, err := h.checkKeyValidity(pak.Key) + c.Assert(err, check.IsNil) + c.Assert(p.ID, check.Equals, pak.ID) +} + +func (*Suite) TestAlreadyUsedKey(c *check.C) { + n, err := h.CreateNamespace("test4") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, nil) + c.Assert(err, check.IsNil) + + db, err := h.db() + if err != nil { + c.Fatal(err) + } + defer db.Close() + m := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testest", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + } + db.Save(&m) + + p, err := h.checkKeyValidity(pak.Key) + c.Assert(err, check.Equals, errorAuthKeyNotReusableAlreadyUsed) + c.Assert(p, check.IsNil) +} + +func (*Suite) TestReusableBeingUsedKey(c *check.C) { + n, err := h.CreateNamespace("test5") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, true, nil) + c.Assert(err, check.IsNil) + + db, err := h.db() + if err != nil { + c.Fatal(err) + } + defer db.Close() + m := Machine{ + ID: 1, + MachineKey: "foo", + NodeKey: "bar", + DiscoKey: "faa", + Name: "testest", + NamespaceID: n.ID, + Registered: true, + RegisterMethod: "authKey", + AuthKeyID: uint(pak.ID), + } + db.Save(&m) + + p, err := h.checkKeyValidity(pak.Key) + c.Assert(err, check.IsNil) + c.Assert(p.ID, check.Equals, pak.ID) +} + +func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) { + n, err := h.CreateNamespace("test6") + c.Assert(err, check.IsNil) + + pak, err := h.CreatePreAuthKey(n.Name, false, nil) + c.Assert(err, check.IsNil) + + p, err := h.checkKeyValidity(pak.Key) + c.Assert(err, check.IsNil) + c.Assert(p.ID, check.Equals, pak.ID) +} From 9ce8dc3fb62a9ee4ab4c5c720f58611aa831d23f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 00:59:16 +0200 Subject: [PATCH 3/9] Preload the namespace --- preauth_keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preauth_keys.go b/preauth_keys.go index 11803b5..7488a2e 100644 --- a/preauth_keys.go +++ b/preauth_keys.go @@ -86,7 +86,7 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { defer db.Close() pak := PreAuthKey{} - if db.First(&pak, "key = ?", k).RecordNotFound() { + if db.Preload("Namespace").First(&pak, "key = ?", k).RecordNotFound() { return nil, errorAuthKeyNotFound } From e17e10e0b724a9b2d3c2e7c957a460b579706d5d Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 00:59:26 +0200 Subject: [PATCH 4/9] Preauth keys kinda working --- api.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/api.go b/api.go index f875259..752b230 100644 --- a/api.go +++ b/api.go @@ -9,6 +9,7 @@ import ( "net/http" "time" + "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/jinzhu/gorm/dialects/postgres" @@ -33,6 +34,8 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) { return } + spew.Dump(c.Params) + c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` @@ -71,6 +74,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { c.String(http.StatusInternalServerError, "Very sad!") return } + db, err := h.db() if err != nil { log.Printf("Cannot open DB: %s", err) @@ -359,21 +363,59 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgcfg.Key, req tailcfg.MapReque } func (h *Headscale) handleNewServer(c *gin.Context, db *gorm.DB, idKey wgcfg.Key, req tailcfg.RegisterRequest) { - mNew := Machine{ + m := Machine{ MachineKey: idKey.HexString(), NodeKey: wgcfg.Key(req.NodeKey).HexString(), Expiry: &req.Expiry, Name: req.Hostinfo.Hostname, } - if err := db.Create(&mNew).Error; err != nil { + if err := db.Create(&m).Error; err != nil { log.Printf("Could not create row: %s", err) return } - resp := tailcfg.RegisterResponse{ - AuthURL: fmt.Sprintf("%s/register?key=%s", - h.cfg.ServerURL, idKey.HexString()), + + resp := tailcfg.RegisterResponse{} + + if req.Auth.AuthKey != "" { + 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) + c.String(http.StatusInternalServerError, "") + return + } + c.Data(200, "application/json; charset=utf-8", respBody) + return + } + ip, err := h.getAvailableIP() + if err != nil { + log.Println(err) + return + } + + m.IPAddress = ip.String() + m.NamespaceID = pak.NamespaceID + m.AuthKeyID = uint(pak.ID) + m.Registered = true + db.Save(&m) + + resp.MachineAuthorized = true + resp.User = *pak.Namespace.toUser() + respBody, err := encode(resp, &idKey, h.privateKey) + if err != nil { + log.Printf("Cannot encode message: %s", err) + c.String(http.StatusInternalServerError, "Extremely sad!") + return + } + c.Data(200, "application/json; charset=utf-8", respBody) + return } + resp.AuthURL = fmt.Sprintf("%s/register?key=%s", + h.cfg.ServerURL, idKey.HexString()) + respBody, err := encode(resp, &idKey, h.privateKey) if err != nil { log.Printf("Cannot encode message: %s", err) From 744c687d3721b8e5930e9f786f09122d990a0421 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 01:01:45 +0200 Subject: [PATCH 5/9] Fixed linting issues --- utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/utils.go b/utils.go index f85a4ff..2785745 100644 --- a/utils.go +++ b/utils.go @@ -21,6 +21,7 @@ import ( "tailscale.com/wgengine/wgcfg" ) +// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors type Error string func (e Error) Error() string { return string(e) } From 9bac805d5044d042641f52a4a88e00a6271637ea Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 01:03:43 +0200 Subject: [PATCH 6/9] Removed spew --- api.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api.go b/api.go index 752b230..e0c5e75 100644 --- a/api.go +++ b/api.go @@ -9,7 +9,6 @@ import ( "net/http" "time" - "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/jinzhu/gorm/dialects/postgres" @@ -34,7 +33,7 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) { return } - spew.Dump(c.Params) + // spew.Dump(c.Params) c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` From fc0c519206bb0d65db98275fd5903dc488519096 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 18:26:01 +0200 Subject: [PATCH 7/9] Fill RegisterMethod field in Machine --- api.go | 1 + cli.go | 1 + 2 files changed, 2 insertions(+) diff --git a/api.go b/api.go index e0c5e75..7b98dfa 100644 --- a/api.go +++ b/api.go @@ -397,6 +397,7 @@ func (h *Headscale) handleNewServer(c *gin.Context, db *gorm.DB, idKey wgcfg.Key m.IPAddress = ip.String() m.NamespaceID = pak.NamespaceID m.AuthKeyID = uint(pak.ID) + m.RegisterMethod = "authKey" m.Registered = true db.Save(&m) diff --git a/cli.go b/cli.go index 0cb5333..f75bb47 100644 --- a/cli.go +++ b/cli.go @@ -43,6 +43,7 @@ func (h *Headscale) RegisterMachine(key string, namespace string) error { m.IPAddress = ip.String() m.NamespaceID = ns.ID m.Registered = true + m.RegisterMethod = "cli" db.Save(&m) fmt.Println("Machine registered 🎉") return nil From 4183db840e1f308ba01df0c879e0c99bceaeb621 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 23:25:40 +0200 Subject: [PATCH 8/9] Update README (although do not advirtise preauth keys that much yet) --- api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api.go b/api.go index 7b98dfa..a9a23dc 100644 --- a/api.go +++ b/api.go @@ -9,6 +9,7 @@ import ( "net/http" "time" + "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/jinzhu/gorm/dialects/postgres" @@ -96,6 +97,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { log.Println("Client is registered and we have the current key. All clear to /map") resp.AuthURL = "" resp.User = *m.Namespace.toUser() + resp.MachineAuthorized = true respBody, err := encode(resp, &mKey, h.privateKey) if err != nil { log.Printf("Cannot encode message: %s", err) @@ -138,6 +140,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } log.Println("We dont know anything about the new key. WTF") + spew.Dump(req) } // PollNetMapHandler takes care of /machine/:id/map From 13e086980a28e83164c7a6401dad5c6d3f082643 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Thu, 6 May 2021 23:49:55 +0200 Subject: [PATCH 9/9] Removed logging --- api.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api.go b/api.go index a9a23dc..914a938 100644 --- a/api.go +++ b/api.go @@ -9,7 +9,6 @@ import ( "net/http" "time" - "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/jinzhu/gorm/dialects/postgres" @@ -140,7 +139,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { } log.Println("We dont know anything about the new key. WTF") - spew.Dump(req) + // spew.Dump(req) } // PollNetMapHandler takes care of /machine/:id/map