Use tailscale key types instead of strings (#1609)

* upgrade tailscale

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* make Node object use actualy tailscale key types

This commit changes the Node struct to have both a field for strings
to store the keys in the database and a dedicated Key for each type
of key.

The keys are populated and stored with Gorm hooks to ensure the data
is stored in the db.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use key types throughout the code

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* make sure machinekey is concistently used

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use machine key in auth url

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix web register

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use key type in notifier

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix relogin with webauth

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2023-11-19 22:37:04 +01:00 committed by GitHub
parent c0fd06e3f5
commit ed4e19996b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 550 additions and 471 deletions

View file

@ -2,6 +2,7 @@ package db
import (
"context"
"database/sql"
"errors"
"fmt"
"net/netip"
@ -99,7 +100,7 @@ func NewHeadscaleDatabase(
// node was registered.
_ = dbConn.Migrator().RenameColumn(&types.Node{}, "nickname", "given_name")
// If the MacNodehine table has a column for registered,
// If the Node table has a column for registered,
// find all occourences of "false" and drop them. Then
// remove the column.
if dbConn.Migrator().HasColumn(&types.Node{}, "registered") {
@ -114,13 +115,13 @@ func NewHeadscaleDatabase(
for _, node := range nodes {
log.Info().
Str("node", node.Hostname).
Str("machine_key", node.MachineKey).
Str("machine_key", node.MachineKey.ShortString()).
Msg("Deleting unregistered node")
if err := dbConn.Delete(&types.Node{}, node.ID).Error; err != nil {
log.Error().
Err(err).
Str("node", node.Hostname).
Str("machine_key", node.MachineKey).
Str("machine_key", node.MachineKey.ShortString()).
Msg("Error deleting unregistered node")
}
}
@ -136,6 +137,50 @@ func NewHeadscaleDatabase(
return nil, err
}
err = dbConn.AutoMigrate(&types.Node{})
if err != nil {
return nil, err
}
// Ensure all keys have correct prefixes
// https://github.com/tailscale/tailscale/blob/main/types/key/node.go#L35
type result struct {
ID uint64
MachineKey string
NodeKey string
DiscoKey string
}
var results []result
err = db.db.Raw("SELECT id, node_key, machine_key, disco_key FROM nodes").Find(&results).Error
if err != nil {
return nil, err
}
for _, node := range results {
mKey := node.MachineKey
if !strings.HasPrefix(node.MachineKey, "mkey:") {
mKey = "mkey:" + node.MachineKey
}
nKey := node.NodeKey
if !strings.HasPrefix(node.NodeKey, "nodekey:") {
nKey = "nodekey:" + node.NodeKey
}
dKey := node.DiscoKey
if !strings.HasPrefix(node.DiscoKey, "discokey:") {
dKey = "discokey:" + node.DiscoKey
}
err := db.db.Exec("UPDATE nodes SET machine_key = @mKey, node_key = @nKey, disco_key = @dKey WHERE ID = @id",
sql.Named("mKey", mKey),
sql.Named("nKey", nKey),
sql.Named("dKey", dKey),
sql.Named("id", node.ID)).Error
if err != nil {
return nil, err
}
}
if dbConn.Migrator().HasColumn(&types.Node{}, "enabled_routes") {
log.Info().Msgf("Database has legacy enabled_routes column in node, migrating...")
@ -195,11 +240,6 @@ func NewHeadscaleDatabase(
}
}
err = dbConn.AutoMigrate(&types.Node{})
if err != nil {
return nil, err
}
if dbConn.Migrator().HasColumn(&types.Node{}, "given_name") {
nodes := types.Nodes{}
if err := dbConn.Find(&nodes).Error; err != nil {
@ -253,27 +293,6 @@ func NewHeadscaleDatabase(
return nil, err
}
// Ensure all keys have correct prefixes
// https://github.com/tailscale/tailscale/blob/main/types/key/node.go#L35
nodes := types.Nodes{}
if err := dbConn.Find(&nodes).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
}
for _, node := range nodes {
if !strings.HasPrefix(node.DiscoKey, "discokey:") {
node.DiscoKey = "discokey:" + node.DiscoKey
}
if !strings.HasPrefix(node.NodeKey, "nodekey:") {
node.NodeKey = "nodekey:" + node.NodeKey
}
if !strings.HasPrefix(node.MachineKey, "mkey:") {
node.MachineKey = "mkey:" + node.MachineKey
}
}
// TODO(kradalby): is this needed?
err = db.setValue("db_version", dbVersion)

View file

@ -55,7 +55,7 @@ func (hsdb *HSDatabase) listPeers(node *types.Node) (types.Nodes, error) {
Preload("User").
Preload("Routes").
Where("node_key <> ?",
node.NodeKey).Find(&nodes).Error; err != nil {
node.NodeKey.String()).Find(&nodes).Error; err != nil {
return types.Nodes{}, err
}
@ -268,7 +268,7 @@ func (hsdb *HSDatabase) SetTags(
hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
Type: types.StatePeerChanged,
Changed: types.Nodes{node},
}, node.MachineKey)
}, node.MachineKey.String())
return nil
}
@ -304,7 +304,7 @@ func (hsdb *HSDatabase) RenameNode(node *types.Node, newName string) error {
hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
Type: types.StatePeerChanged,
Changed: types.Nodes{node},
}, node.MachineKey)
}, node.MachineKey.String())
return nil
}
@ -330,7 +330,7 @@ func (hsdb *HSDatabase) nodeSetExpiry(node *types.Node, expiry time.Time) error
hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
Type: types.StatePeerChanged,
Changed: types.Nodes{node},
}, node.MachineKey)
}, node.MachineKey.String())
return nil
}
@ -376,7 +376,7 @@ func (hsdb *HSDatabase) UpdateLastSeen(node *types.Node) error {
func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
cache *cache.Cache,
nodeKeyStr string,
mkey key.MachinePublic,
userName string,
nodeExpiry *time.Time,
registrationMethod string,
@ -384,20 +384,14 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
nodeKey := key.NodePublic{}
err := nodeKey.UnmarshalText([]byte(nodeKeyStr))
if err != nil {
return nil, err
}
log.Debug().
Str("nodeKey", nodeKey.ShortString()).
Str("machine_key", mkey.ShortString()).
Str("userName", userName).
Str("registrationMethod", registrationMethod).
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
Msg("Registering node from API/CLI or auth callback")
if nodeInterface, ok := cache.Get(nodeKey.String()); ok {
if nodeInterface, ok := cache.Get(mkey.String()); ok {
if registrationNode, ok := nodeInterface.(types.Node); ok {
user, err := hsdb.getUser(userName)
if err != nil {
@ -425,7 +419,7 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
)
if err == nil {
cache.Delete(nodeKeyStr)
cache.Delete(mkey.String())
}
return node, err
@ -448,8 +442,8 @@ func (hsdb *HSDatabase) RegisterNode(node types.Node) (*types.Node, error) {
func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
log.Debug().
Str("node", node.Hostname).
Str("machine_key", node.MachineKey).
Str("node_key", node.NodeKey).
Str("machine_key", node.MachineKey.ShortString()).
Str("node_key", node.NodeKey.ShortString()).
Str("user", node.User.Name).
Msg("Registering node")
@ -464,8 +458,8 @@ func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
log.Trace().
Caller().
Str("node", node.Hostname).
Str("machine_key", node.MachineKey).
Str("node_key", node.NodeKey).
Str("machine_key", node.MachineKey.ShortString()).
Str("node_key", node.NodeKey.ShortString()).
Str("user", node.User.Name).
Msg("Node authorized again")
@ -507,7 +501,7 @@ func (hsdb *HSDatabase) NodeSetNodeKey(node *types.Node, nodeKey key.NodePublic)
defer hsdb.mu.Unlock()
if err := hsdb.db.Model(node).Updates(types.Node{
NodeKey: nodeKey.String(),
NodeKey: nodeKey,
}).Error; err != nil {
return err
}
@ -524,7 +518,7 @@ func (hsdb *HSDatabase) NodeSetMachineKey(
defer hsdb.mu.Unlock()
if err := hsdb.db.Model(node).Updates(types.Node{
MachineKey: machineKey.String(),
MachineKey: machineKey,
}).Error; err != nil {
return err
}
@ -703,7 +697,7 @@ func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) erro
hsdb.notifier.NotifyWithIgnore(types.StateUpdate{
Type: types.StatePeerChanged,
Changed: types.Nodes{node},
}, node.MachineKey)
}, node.MachineKey.String())
return nil
}
@ -734,7 +728,7 @@ func generateGivenName(suppliedName string, randomSuffix bool) (string, error) {
return normalizedHostname, nil
}
func (hsdb *HSDatabase) GenerateGivenName(machineKey string, suppliedName string) (string, error) {
func (hsdb *HSDatabase) GenerateGivenName(mkey key.MachinePublic, suppliedName string) (string, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
@ -749,17 +743,22 @@ func (hsdb *HSDatabase) GenerateGivenName(machineKey string, suppliedName string
return "", err
}
for _, node := range nodes {
if node.MachineKey != machineKey && node.GivenName == givenName {
postfixedName, err := generateGivenName(suppliedName, true)
if err != nil {
return "", err
}
givenName = postfixedName
var nodeFound *types.Node
for idx, node := range nodes {
if node.GivenName == givenName {
nodeFound = nodes[idx]
}
}
if nodeFound != nil && nodeFound.MachineKey.String() != mkey.String() {
postfixedName, err := generateGivenName(suppliedName, true)
if err != nil {
return "", err
}
givenName = postfixedName
}
return givenName, nil
}

View file

@ -25,11 +25,13 @@ func (s *Suite) TestGetNode(c *check.C) {
_, err = db.GetNode("test", "testnode")
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := &types.Node{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -51,11 +53,13 @@ func (s *Suite) TestGetNodeByID(c *check.C) {
_, err = db.GetNodeByID(0)
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := types.Node{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -82,9 +86,8 @@ func (s *Suite) TestGetNodeByNodeKey(c *check.C) {
node := types.Node{
ID: 0,
MachineKey: machineKey.Public().String(),
NodeKey: nodeKey.Public().String(),
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -113,9 +116,8 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
node := types.Node{
ID: 0,
MachineKey: machineKey.Public().String(),
NodeKey: nodeKey.Public().String(),
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -130,11 +132,14 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
func (s *Suite) TestHardDeleteNode(c *check.C) {
user, err := db.CreateUser("test")
c.Assert(err, check.IsNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := types.Node{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode3",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -160,11 +165,13 @@ func (s *Suite) TestListPeers(c *check.C) {
c.Assert(err, check.NotNil)
for index := 0; index <= 10; index++ {
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := types.Node{
ID: uint64(index),
MachineKey: "foo" + strconv.Itoa(index),
NodeKey: "bar" + strconv.Itoa(index),
DiscoKey: "faa" + strconv.Itoa(index),
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode" + strconv.Itoa(index),
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -205,11 +212,13 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
c.Assert(err, check.NotNil)
for index := 0; index <= 10; index++ {
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := types.Node{
ID: uint64(index),
MachineKey: "foo" + strconv.Itoa(index),
NodeKey: "bar" + strconv.Itoa(index),
DiscoKey: "faa" + strconv.Itoa(index),
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
IPAddresses: types.NodeAddresses{
netip.MustParseAddr(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
},
@ -288,11 +297,13 @@ func (s *Suite) TestExpireNode(c *check.C) {
_, err = db.GetNode("test", "testnode")
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := &types.Node{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -345,11 +356,15 @@ func (s *Suite) TestGenerateGivenName(c *check.C) {
_, err = db.GetNode("user-1", "testnode")
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
machineKey2 := key.NewMachine()
node := &types.Node{
ID: 0,
MachineKey: "node-key-1",
NodeKey: "node-key-1",
DiscoKey: "disco-key-1",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "hostname-1",
GivenName: "hostname-1",
UserID: user1.ID,
@ -358,25 +373,20 @@ func (s *Suite) TestGenerateGivenName(c *check.C) {
}
db.db.Save(node)
givenName, err := db.GenerateGivenName("node-key-2", "hostname-2")
givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2")
comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Equals, "hostname-2", comment)
givenName, err = db.GenerateGivenName("node-key-1", "hostname-1")
givenName, err = db.GenerateGivenName(machineKey.Public(), "hostname-1")
comment = check.Commentf("Same user, same node, same hostname, no conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Equals, "hostname-1", comment)
givenName, err = db.GenerateGivenName("node-key-2", "hostname-1")
givenName, err = db.GenerateGivenName(machineKey2.Public(), "hostname-1")
comment = check.Commentf("Same user, unique nodes, same hostname, conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Matches, fmt.Sprintf("^hostname-1-[a-z0-9]{%d}$", NodeGivenNameHashLength), comment)
givenName, err = db.GenerateGivenName("node-key-2", "hostname-1")
comment = check.Commentf("Unique users, unique nodes, same hostname, conflict")
c.Assert(err, check.IsNil, comment)
c.Assert(givenName, check.Matches, fmt.Sprintf("^hostname-1-[a-z0-9]{%d}$", NodeGivenNameHashLength), comment)
}
func (s *Suite) TestSetTags(c *check.C) {
@ -389,11 +399,13 @@ func (s *Suite) TestSetTags(c *check.C) {
_, err = db.GetNode("test", "testnode")
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := &types.Node{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
@ -565,6 +577,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
c.Assert(err, check.IsNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
defaultRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
defaultRouteV6 := netip.MustParsePrefix("::/0")
@ -574,9 +587,8 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
node := types.Node{
ID: 0,
MachineKey: "foo",
NodeKey: nodeKey.Public().String(),
DiscoKey: "faa",
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "test",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,

View file

@ -1,6 +1,7 @@
package db
import (
"log"
"net/netip"
"os"
"testing"
@ -27,19 +28,22 @@ func (s *Suite) SetUpTest(c *check.C) {
}
func (s *Suite) TearDownTest(c *check.C) {
os.RemoveAll(tmpDir)
// os.RemoveAll(tmpDir)
}
func (s *Suite) ResetDB(c *check.C) {
if len(tmpDir) != 0 {
os.RemoveAll(tmpDir)
}
// if len(tmpDir) != 0 {
// os.RemoveAll(tmpDir)
// }
var err error
tmpDir, err = os.MkdirTemp("", "autoygg-client-test")
tmpDir, err = os.MkdirTemp("", "headscale-db-test-*")
if err != nil {
c.Fatal(err)
}
log.Printf("database path: %s", tmpDir+"/headscale_test.db")
db, err = NewHeadscaleDatabase(
"sqlite3",
tmpDir+"/headscale_test.db",