Merge pull request #21 from juanfont/working-preauth
Support for pre auth keys
This commit is contained in:
commit
8ca940ad30
6 changed files with 206 additions and 8 deletions
54
api.go
54
api.go
|
@ -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
1
cli.go
|
@ -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
|
||||||
|
|
10
machine.go
10
machine.go
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
5
utils.go
5
utils.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue