Merge pull request #21 from juanfont/working-preauth

Support for pre auth keys
This commit is contained in:
Juan Font 2021-05-06 23:50:28 +02:00 committed by GitHub
commit 8ca940ad30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 206 additions and 8 deletions

54
api.go
View File

@ -33,6 +33,8 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
return return
} }
// spew.Dump(c.Params)
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
<html> <html>
<body> <body>
@ -71,6 +73,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
c.String(http.StatusInternalServerError, "Very sad!") c.String(http.StatusInternalServerError, "Very sad!")
return return
} }
db, err := h.db() db, err := h.db()
if err != nil { if err != nil {
log.Printf("Cannot open DB: %s", err) log.Printf("Cannot open DB: %s", err)
@ -93,6 +96,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
log.Println("Client is registered and we have the current key. All clear to /map") log.Println("Client is registered and we have the current key. All clear to /map")
resp.AuthURL = "" resp.AuthURL = ""
resp.User = *m.Namespace.toUser() resp.User = *m.Namespace.toUser()
resp.MachineAuthorized = true
respBody, err := encode(resp, &mKey, h.privateKey) respBody, err := encode(resp, &mKey, h.privateKey)
if err != nil { if err != nil {
log.Printf("Cannot encode message: %s", err) log.Printf("Cannot encode message: %s", err)
@ -135,6 +139,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
} }
log.Println("We dont know anything about the new key. WTF") log.Println("We dont know anything about the new key. WTF")
// spew.Dump(req)
} }
// PollNetMapHandler takes care of /machine/:id/map // PollNetMapHandler takes care of /machine/:id/map
@ -359,21 +364,60 @@ 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) { func (h *Headscale) handleNewServer(c *gin.Context, db *gorm.DB, idKey wgcfg.Key, req tailcfg.RegisterRequest) {
mNew := Machine{ m := Machine{
MachineKey: idKey.HexString(), MachineKey: idKey.HexString(),
NodeKey: wgcfg.Key(req.NodeKey).HexString(), NodeKey: wgcfg.Key(req.NodeKey).HexString(),
Expiry: &req.Expiry, Expiry: &req.Expiry,
Name: req.Hostinfo.Hostname, 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) log.Printf("Could not create row: %s", err)
return return
} }
resp := tailcfg.RegisterResponse{
AuthURL: fmt.Sprintf("%s/register?key=%s", resp := tailcfg.RegisterResponse{}
h.cfg.ServerURL, idKey.HexString()),
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.RegisterMethod = "authKey"
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) respBody, err := encode(resp, &idKey, h.privateKey)
if err != nil { if err != nil {
log.Printf("Cannot encode message: %s", err) log.Printf("Cannot encode message: %s", err)

1
cli.go
View File

@ -43,6 +43,7 @@ func (h *Headscale) RegisterMachine(key string, namespace string) error {
m.IPAddress = ip.String() m.IPAddress = ip.String()
m.NamespaceID = ns.ID m.NamespaceID = ns.ID
m.Registered = true m.Registered = true
m.RegisterMethod = "cli"
db.Save(&m) db.Save(&m)
fmt.Println("Machine registered 🎉") fmt.Println("Machine registered 🎉")
return nil return nil

View File

@ -25,9 +25,13 @@ type Machine struct {
NamespaceID uint NamespaceID uint
Namespace Namespace Namespace Namespace
Registered bool // temp Registered bool // temp
LastSeen *time.Time RegisterMethod string
Expiry *time.Time AuthKeyID uint
AuthKey *PreAuthKey
LastSeen *time.Time
Expiry *time.Time
HostInfo postgres.Jsonb HostInfo postgres.Jsonb
Endpoints postgres.Jsonb Endpoints postgres.Jsonb

View File

@ -7,6 +7,10 @@ import (
"time" "time"
) )
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 // PreAuthKey describes a pre-authorization key usable in a particular namespace
type PreAuthKey struct { type PreAuthKey struct {
ID uint64 `gorm:"primary_key"` ID uint64 `gorm:"primary_key"`
@ -72,6 +76,41 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error)
return &keys, nil 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.Preload("Namespace").First(&pak, "key = ?", k).RecordNotFound() {
return nil, errorAuthKeyNotFound
}
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
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
}
func (h *Headscale) generateKey() (string, error) { func (h *Headscale) generateKey() (string, error) {
size := 24 size := 24
bytes := make([]byte, size) bytes := make([]byte, size)

View File

@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"time"
_ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver _ "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) { func (*Suite) TestCreatePreAuthKey(c *check.C) {
_, err := h.CreatePreAuthKey("bogus", true, nil) _, err := h.CreatePreAuthKey("bogus", true, nil)
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
n, err := h.CreateNamespace("test") n, err := h.CreateNamespace("test")
@ -73,3 +75,106 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) {
// Make sure the Namespace association is populated // Make sure the Namespace association is populated
c.Assert((*keys)[0].Namespace.Name, check.Equals, n.Name) 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)
}
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)
}

View File

@ -21,6 +21,11 @@ import (
"tailscale.com/wgengine/wgcfg" "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) }
func decode(msg []byte, v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) error { func decode(msg []byte, v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) error {
return decodeMsg(msg, v, pubKey, privKey) return decodeMsg(msg, v, pubKey, privKey)
} }