Replace database locks with transactions (#1701)

This commits removes the locks used to guard data integrity for the
database and replaces them with Transactions, turns out that SQL had
a way to deal with this all along.

This reduces the complexity we had with multiple locks that might stack
or recurse (database, nofitifer, mapper). All notifications and state
updates are now triggered _after_ a database change.


Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2024-02-08 17:28:19 +01:00 committed by GitHub
parent cbf57e27a7
commit 83769ba715
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1496 additions and 1128 deletions

View file

@ -26,7 +26,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/
- Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1473](https://github.com/juanfont/headscale/pull/1473) - Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1473](https://github.com/juanfont/headscale/pull/1473)
- API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553) - API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553)
- Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611) - Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611)
- The latest supported client is 1.36 - The latest supported client is 1.38
- Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564) - Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564)
- If no DERP is configured, the server will fail to start, this can be because it cannot load the DERPMap from file or url. - If no DERP is configured, the server will fail to start, this can be because it cannot load the DERPMap from file or url.
- Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611) - Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611)

View file

@ -6,25 +6,11 @@ import (
"github.com/efekarakus/termcolor" "github.com/efekarakus/termcolor"
"github.com/juanfont/headscale/cmd/headscale/cli" "github.com/juanfont/headscale/cmd/headscale/cli"
"github.com/pkg/profile"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func main() { func main() {
if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile {
if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok {
err := os.MkdirAll(profilePath, os.ModePerm)
if err != nil {
log.Fatal().Err(err).Msg("failed to create profiling directory")
}
defer profile.Start(profile.ProfilePath(profilePath)).Stop()
} else {
defer profile.Start().Stop()
}
}
var colors bool var colors bool
switch l := termcolor.SupportLevel(os.Stderr); l { switch l := termcolor.SupportLevel(os.Stderr); l {
case termcolor.Level16M: case termcolor.Level16M:

View file

@ -33,6 +33,7 @@ import (
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog" zerolog "github.com/philip-bui/grpc-zerolog"
"github.com/pkg/profile"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
zl "github.com/rs/zerolog" zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -48,6 +49,7 @@ import (
"google.golang.org/grpc/peer" "google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"gorm.io/gorm"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
@ -61,7 +63,7 @@ var (
"unknown value for Lets Encrypt challenge type", "unknown value for Lets Encrypt challenge type",
) )
errEmptyInitialDERPMap = errors.New( errEmptyInitialDERPMap = errors.New(
"initial DERPMap is empty, Headscale requries at least one entry", "initial DERPMap is empty, Headscale requires at least one entry",
) )
) )
@ -166,7 +168,6 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
cfg.DBtype, cfg.DBtype,
dbString, dbString,
app.dbDebug, app.dbDebug,
app.nodeNotifier,
cfg.IPPrefixes, cfg.IPPrefixes,
cfg.BaseDomain) cfg.BaseDomain)
if err != nil { if err != nil {
@ -234,8 +235,23 @@ func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout. // seen for longer than h.cfg.EphemeralNodeInactivityTimeout.
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) { func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond) ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
var update types.StateUpdate
var changed bool
for range ticker.C { for range ticker.C {
h.db.ExpireEphemeralNodes(h.cfg.EphemeralNodeInactivityTimeout) if err := h.db.DB.Transaction(func(tx *gorm.DB) error {
update, changed = db.ExpireEphemeralNodes(tx, h.cfg.EphemeralNodeInactivityTimeout)
return nil
}); err != nil {
log.Error().Err(err).Msg("database error while expiring ephemeral nodes")
continue
}
if changed && update.Valid() {
ctx := types.NotifyCtx(context.Background(), "expire-ephemeral", "na")
h.nodeNotifier.NotifyAll(ctx, update)
}
} }
} }
@ -246,9 +262,24 @@ func (h *Headscale) expireExpiredMachines(intervalMs int64) {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
lastCheck := time.Unix(0, 0) lastCheck := time.Unix(0, 0)
var update types.StateUpdate
var changed bool
for range ticker.C { for range ticker.C {
lastCheck = h.db.ExpireExpiredNodes(lastCheck) if err := h.db.DB.Transaction(func(tx *gorm.DB) error {
lastCheck, update, changed = db.ExpireExpiredNodes(tx, lastCheck)
return nil
}); err != nil {
log.Error().Err(err).Msg("database error while expiring nodes")
continue
}
log.Trace().Str("nodes", update.ChangeNodes.String()).Msgf("expiring nodes")
if changed && update.Valid() {
ctx := types.NotifyCtx(context.Background(), "expire-expired", "na")
h.nodeNotifier.NotifyAll(ctx, update)
}
} }
} }
@ -278,7 +309,8 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
DERPMap: h.DERPMap, DERPMap: h.DERPMap,
} }
if stateUpdate.Valid() { if stateUpdate.Valid() {
h.nodeNotifier.NotifyAll(stateUpdate) ctx := types.NotifyCtx(context.Background(), "derpmap-update", "na")
h.nodeNotifier.NotifyAll(ctx, stateUpdate)
} }
} }
} }
@ -485,6 +517,19 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
// Serve launches a GIN server with the Headscale API. // Serve launches a GIN server with the Headscale API.
func (h *Headscale) Serve() error { func (h *Headscale) Serve() error {
if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile {
if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok {
err := os.MkdirAll(profilePath, os.ModePerm)
if err != nil {
log.Fatal().Err(err).Msg("failed to create profiling directory")
}
defer profile.Start(profile.ProfilePath(profilePath)).Stop()
} else {
defer profile.Start().Stop()
}
}
var err error var err error
// Fetch an initial DERP Map before we start serving // Fetch an initial DERP Map before we start serving
@ -753,7 +798,8 @@ func (h *Headscale) Serve() error {
Str("path", aclPath). Str("path", aclPath).
Msg("ACL policy successfully reloaded, notifying nodes of change") Msg("ACL policy successfully reloaded, notifying nodes of change")
h.nodeNotifier.NotifyAll(types.StateUpdate{ ctx := types.NotifyCtx(context.Background(), "acl-sighup", "na")
h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{
Type: types.StateFullUpdate, Type: types.StateFullUpdate,
}) })
} }

View file

@ -1,6 +1,7 @@
package hscontrol package hscontrol
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -8,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -243,8 +245,6 @@ func (h *Headscale) handleRegister(
// handleAuthKey contains the logic to manage auth key client registration // handleAuthKey contains the logic to manage auth key client registration
// When using Noise, the machineKey is Zero. // When using Noise, the machineKey is Zero.
//
// TODO: check if any locks are needed around IP allocation.
func (h *Headscale) handleAuthKey( func (h *Headscale) handleAuthKey(
writer http.ResponseWriter, writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
@ -311,6 +311,9 @@ func (h *Headscale) handleAuthKey(
nodeKey := registerRequest.NodeKey nodeKey := registerRequest.NodeKey
var update types.StateUpdate
var mkey key.MachinePublic
// retrieve node information if it exist // retrieve node information if it exist
// The error is not important, because if it does not // The error is not important, because if it does not
// exist, then this is a new node and we will move // exist, then this is a new node and we will move
@ -324,7 +327,7 @@ func (h *Headscale) handleAuthKey(
node.NodeKey = nodeKey node.NodeKey = nodeKey
node.AuthKeyID = uint(pak.ID) node.AuthKeyID = uint(pak.ID)
err := h.db.NodeSetExpiry(node, registerRequest.Expiry) err := h.db.NodeSetExpiry(node.ID, registerRequest.Expiry)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@ -335,10 +338,13 @@ func (h *Headscale) handleAuthKey(
return return
} }
mkey = node.MachineKey
update = types.StateUpdateExpire(node.ID, registerRequest.Expiry)
aclTags := pak.Proto().GetAclTags() aclTags := pak.Proto().GetAclTags()
if len(aclTags) > 0 { if len(aclTags) > 0 {
// This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login // This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login
err = h.db.SetTags(node, aclTags) err = h.db.SetTags(node.ID, aclTags)
if err != nil { if err != nil {
log.Error(). log.Error().
@ -370,6 +376,7 @@ func (h *Headscale) handleAuthKey(
Hostname: registerRequest.Hostinfo.Hostname, Hostname: registerRequest.Hostinfo.Hostname,
GivenName: givenName, GivenName: givenName,
UserID: pak.User.ID, UserID: pak.User.ID,
User: pak.User,
MachineKey: machineKey, MachineKey: machineKey,
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
Expiry: &registerRequest.Expiry, Expiry: &registerRequest.Expiry,
@ -393,9 +400,18 @@ func (h *Headscale) handleAuthKey(
return return
} }
mkey = node.MachineKey
update = types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node},
Message: "called from auth.handleAuthKey",
}
} }
err = h.db.UsePreAuthKey(pak) err = h.db.DB.Transaction(func(tx *gorm.DB) error {
return db.UsePreAuthKey(tx, pak)
})
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@ -437,6 +453,13 @@ func (h *Headscale) handleAuthKey(
Caller(). Caller().
Err(err). Err(err).
Msg("Failed to write response") Msg("Failed to write response")
return
}
// TODO(kradalby): if notifying after register make sense.
if update.Valid() {
ctx := types.NotifyCtx(context.Background(), "handle-authkey", "na")
h.nodeNotifier.NotifyWithIgnore(ctx, update, mkey.String())
} }
log.Info(). log.Info().
@ -502,7 +525,7 @@ func (h *Headscale) handleNodeLogOut(
Msg("Client requested logout") Msg("Client requested logout")
now := time.Now() now := time.Now()
err := h.db.NodeSetExpiry(&node, now) err := h.db.NodeSetExpiry(node.ID, now)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@ -513,17 +536,10 @@ func (h *Headscale) handleNodeLogOut(
return return
} }
stateUpdate := types.StateUpdate{ stateUpdate := types.StateUpdateExpire(node.ID, now)
Type: types.StatePeerChangedPatch,
ChangePatches: []*tailcfg.PeerChange{
{
NodeID: tailcfg.NodeID(node.ID),
KeyExpiry: &now,
},
},
}
if stateUpdate.Valid() { if stateUpdate.Valid() {
h.nodeNotifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String()) ctx := types.NotifyCtx(context.Background(), "logout-expiry", "na")
h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String())
} }
resp.AuthURL = "" resp.AuthURL = ""
@ -554,7 +570,7 @@ func (h *Headscale) handleNodeLogOut(
} }
if node.IsEphemeral() { if node.IsEphemeral() {
err = h.db.DeleteNode(&node) err = h.db.DeleteNode(&node, h.nodeNotifier.ConnectedMap())
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -562,6 +578,15 @@ func (h *Headscale) handleNodeLogOut(
Msg("Cannot delete ephemeral node from the database") Msg("Cannot delete ephemeral node from the database")
} }
stateUpdate := types.StateUpdate{
Type: types.StatePeerRemoved,
Removed: []tailcfg.NodeID{tailcfg.NodeID(node.ID)},
}
if stateUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "logout-ephemeral", "na")
h.nodeNotifier.NotifyAll(ctx, stateUpdate)
}
return return
} }
@ -633,7 +658,9 @@ func (h *Headscale) handleNodeKeyRefresh(
Str("node", node.Hostname). Str("node", node.Hostname).
Msg("We have the OldNodeKey in the database. This is a key refresh") Msg("We have the OldNodeKey in the database. This is a key refresh")
err := h.db.NodeSetNodeKey(&node, registerRequest.NodeKey) err := h.db.DB.Transaction(func(tx *gorm.DB) error {
return db.NodeSetNodeKey(tx, &node, registerRequest.NodeKey)
})
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().

View file

@ -13,16 +13,23 @@ import (
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx" "go4.org/netipx"
"gorm.io/gorm"
) )
var ErrCouldNotAllocateIP = errors.New("could not find any suitable IP") var ErrCouldNotAllocateIP = errors.New("could not find any suitable IP")
func (hsdb *HSDatabase) getAvailableIPs() (types.NodeAddresses, error) { func (hsdb *HSDatabase) getAvailableIPs() (types.NodeAddresses, error) {
return Read(hsdb.DB, func(rx *gorm.DB) (types.NodeAddresses, error) {
return getAvailableIPs(rx, hsdb.ipPrefixes)
})
}
func getAvailableIPs(rx *gorm.DB, ipPrefixes []netip.Prefix) (types.NodeAddresses, error) {
var ips types.NodeAddresses var ips types.NodeAddresses
var err error var err error
for _, ipPrefix := range hsdb.ipPrefixes { for _, ipPrefix := range ipPrefixes {
var ip *netip.Addr var ip *netip.Addr
ip, err = hsdb.getAvailableIP(ipPrefix) ip, err = getAvailableIP(rx, ipPrefix)
if err != nil { if err != nil {
return ips, err return ips, err
} }
@ -32,8 +39,8 @@ func (hsdb *HSDatabase) getAvailableIPs() (types.NodeAddresses, error) {
return ips, err return ips, err
} }
func (hsdb *HSDatabase) getAvailableIP(ipPrefix netip.Prefix) (*netip.Addr, error) { func getAvailableIP(rx *gorm.DB, ipPrefix netip.Prefix) (*netip.Addr, error) {
usedIps, err := hsdb.getUsedIPs() usedIps, err := getUsedIPs(rx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -64,12 +71,12 @@ func (hsdb *HSDatabase) getAvailableIP(ipPrefix netip.Prefix) (*netip.Addr, erro
} }
} }
func (hsdb *HSDatabase) getUsedIPs() (*netipx.IPSet, error) { func getUsedIPs(rx *gorm.DB) (*netipx.IPSet, error) {
// FIXME: This really deserves a better data model, // FIXME: This really deserves a better data model,
// but this was quick to get running and it should be enough // but this was quick to get running and it should be enough
// to begin experimenting with a dual stack tailnet. // to begin experimenting with a dual stack tailnet.
var addressesSlices []string var addressesSlices []string
hsdb.db.Model(&types.Node{}).Pluck("ip_addresses", &addressesSlices) rx.Model(&types.Node{}).Pluck("ip_addresses", &addressesSlices)
var ips netipx.IPSetBuilder var ips netipx.IPSetBuilder
for _, slice := range addressesSlices { for _, slice := range addressesSlices {

View file

@ -7,10 +7,16 @@ import (
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx" "go4.org/netipx"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"gorm.io/gorm"
) )
func (s *Suite) TestGetAvailableIp(c *check.C) { func (s *Suite) TestGetAvailableIp(c *check.C) {
ips, err := db.getAvailableIPs() tx := db.DB.Begin()
defer tx.Rollback()
ips, err := getAvailableIPs(tx, []netip.Prefix{
netip.MustParsePrefix("10.27.0.0/23"),
})
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -30,7 +36,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "testnode") _, err = db.getNode("test", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
node := types.Node{ node := types.Node{
@ -41,10 +47,13 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
IPAddresses: ips, IPAddresses: ips,
} }
db.db.Save(&node) db.Write(func(tx *gorm.DB) error {
return tx.Save(&node).Error
usedIps, err := db.getUsedIPs() })
usedIps, err := Read(db.DB, func(rx *gorm.DB) (*netipx.IPSet, error) {
return getUsedIPs(rx)
})
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
expected := netip.MustParseAddr("10.27.0.1") expected := netip.MustParseAddr("10.27.0.1")
@ -63,19 +72,23 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
} }
func (s *Suite) TestGetMultiIp(c *check.C) { func (s *Suite) TestGetMultiIp(c *check.C) {
user, err := db.CreateUser("test-ip-multi") user, err := db.CreateUser("test-ip")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
ipPrefixes := []netip.Prefix{
netip.MustParsePrefix("10.27.0.0/23"),
}
for index := 1; index <= 350; index++ { for index := 1; index <= 350; index++ {
db.ipAllocationMutex.Lock() tx := db.DB.Begin()
ips, err := db.getAvailableIPs() ips, err := getAvailableIPs(tx, ipPrefixes)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := CreatePreAuthKey(tx, user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "testnode") _, err = getNode(tx, "test", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
node := types.Node{ node := types.Node{
@ -86,12 +99,13 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
IPAddresses: ips, IPAddresses: ips,
} }
db.db.Save(&node) tx.Save(&node)
c.Assert(tx.Commit().Error, check.IsNil)
db.ipAllocationMutex.Unlock()
} }
usedIps, err := db.getUsedIPs() usedIps, err := Read(db.DB, func(rx *gorm.DB) (*netipx.IPSet, error) {
return getUsedIPs(rx)
})
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
expected0 := netip.MustParseAddr("10.27.0.1") expected0 := netip.MustParseAddr("10.27.0.1")
@ -162,7 +176,7 @@ func (s *Suite) TestGetAvailableIpNodeWithoutIP(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "testnode") _, err = db.getNode("test", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
node := types.Node{ node := types.Node{
@ -172,7 +186,7 @@ func (s *Suite) TestGetAvailableIpNodeWithoutIP(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
ips2, err := db.getAvailableIPs() ips2, err := db.getAvailableIPs()
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View file

@ -22,9 +22,6 @@ var ErrAPIKeyFailedToParse = errors.New("failed to parse ApiKey")
func (hsdb *HSDatabase) CreateAPIKey( func (hsdb *HSDatabase) CreateAPIKey(
expiration *time.Time, expiration *time.Time,
) (string, *types.APIKey, error) { ) (string, *types.APIKey, error) {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
prefix, err := util.GenerateRandomStringURLSafe(apiPrefixLength) prefix, err := util.GenerateRandomStringURLSafe(apiPrefixLength)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
@ -49,7 +46,7 @@ func (hsdb *HSDatabase) CreateAPIKey(
Expiration: expiration, Expiration: expiration,
} }
if err := hsdb.db.Save(&key).Error; err != nil { if err := hsdb.DB.Save(&key).Error; err != nil {
return "", nil, fmt.Errorf("failed to save API key to database: %w", err) return "", nil, fmt.Errorf("failed to save API key to database: %w", err)
} }
@ -58,11 +55,8 @@ func (hsdb *HSDatabase) CreateAPIKey(
// ListAPIKeys returns the list of ApiKeys for a user. // ListAPIKeys returns the list of ApiKeys for a user.
func (hsdb *HSDatabase) ListAPIKeys() ([]types.APIKey, error) { func (hsdb *HSDatabase) ListAPIKeys() ([]types.APIKey, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
keys := []types.APIKey{} keys := []types.APIKey{}
if err := hsdb.db.Find(&keys).Error; err != nil { if err := hsdb.DB.Find(&keys).Error; err != nil {
return nil, err return nil, err
} }
@ -71,11 +65,8 @@ func (hsdb *HSDatabase) ListAPIKeys() ([]types.APIKey, error) {
// GetAPIKey returns a ApiKey for a given key. // GetAPIKey returns a ApiKey for a given key.
func (hsdb *HSDatabase) GetAPIKey(prefix string) (*types.APIKey, error) { func (hsdb *HSDatabase) GetAPIKey(prefix string) (*types.APIKey, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
key := types.APIKey{} key := types.APIKey{}
if result := hsdb.db.First(&key, "prefix = ?", prefix); result.Error != nil { if result := hsdb.DB.First(&key, "prefix = ?", prefix); result.Error != nil {
return nil, result.Error return nil, result.Error
} }
@ -84,11 +75,8 @@ func (hsdb *HSDatabase) GetAPIKey(prefix string) (*types.APIKey, error) {
// GetAPIKeyByID returns a ApiKey for a given id. // GetAPIKeyByID returns a ApiKey for a given id.
func (hsdb *HSDatabase) GetAPIKeyByID(id uint64) (*types.APIKey, error) { func (hsdb *HSDatabase) GetAPIKeyByID(id uint64) (*types.APIKey, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
key := types.APIKey{} key := types.APIKey{}
if result := hsdb.db.Find(&types.APIKey{ID: id}).First(&key); result.Error != nil { if result := hsdb.DB.Find(&types.APIKey{ID: id}).First(&key); result.Error != nil {
return nil, result.Error return nil, result.Error
} }
@ -98,10 +86,7 @@ func (hsdb *HSDatabase) GetAPIKeyByID(id uint64) (*types.APIKey, error) {
// DestroyAPIKey destroys a ApiKey. Returns error if the ApiKey // DestroyAPIKey destroys a ApiKey. Returns error if the ApiKey
// does not exist. // does not exist.
func (hsdb *HSDatabase) DestroyAPIKey(key types.APIKey) error { func (hsdb *HSDatabase) DestroyAPIKey(key types.APIKey) error {
hsdb.mu.Lock() if result := hsdb.DB.Unscoped().Delete(key); result.Error != nil {
defer hsdb.mu.Unlock()
if result := hsdb.db.Unscoped().Delete(key); result.Error != nil {
return result.Error return result.Error
} }
@ -110,10 +95,7 @@ func (hsdb *HSDatabase) DestroyAPIKey(key types.APIKey) error {
// ExpireAPIKey marks a ApiKey as expired. // ExpireAPIKey marks a ApiKey as expired.
func (hsdb *HSDatabase) ExpireAPIKey(key *types.APIKey) error { func (hsdb *HSDatabase) ExpireAPIKey(key *types.APIKey) error {
hsdb.mu.Lock() if err := hsdb.DB.Model(&key).Update("Expiration", time.Now()).Error; err != nil {
defer hsdb.mu.Unlock()
if err := hsdb.db.Model(&key).Update("Expiration", time.Now()).Error; err != nil {
return err return err
} }
@ -121,9 +103,6 @@ func (hsdb *HSDatabase) ExpireAPIKey(key *types.APIKey) error {
} }
func (hsdb *HSDatabase) ValidateAPIKey(keyStr string) (bool, error) { func (hsdb *HSDatabase) ValidateAPIKey(keyStr string) (bool, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
prefix, hash, found := strings.Cut(keyStr, ".") prefix, hash, found := strings.Cut(keyStr, ".")
if !found { if !found {
return false, ErrAPIKeyFailedToParse return false, ErrAPIKeyFailedToParse

View file

@ -7,12 +7,10 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"strings" "strings"
"sync"
"time" "time"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/go-gormigrate/gormigrate/v2" "github.com/go-gormigrate/gormigrate/v2"
"github.com/juanfont/headscale/hscontrol/notifier"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -36,12 +34,7 @@ type KV struct {
} }
type HSDatabase struct { type HSDatabase struct {
db *gorm.DB DB *gorm.DB
notifier *notifier.Notifier
mu sync.RWMutex
ipAllocationMutex sync.Mutex
ipPrefixes []netip.Prefix ipPrefixes []netip.Prefix
baseDomain string baseDomain string
@ -52,7 +45,6 @@ type HSDatabase struct {
func NewHeadscaleDatabase( func NewHeadscaleDatabase(
dbType, connectionAddr string, dbType, connectionAddr string,
debug bool, debug bool,
notifier *notifier.Notifier,
ipPrefixes []netip.Prefix, ipPrefixes []netip.Prefix,
baseDomain string, baseDomain string,
) (*HSDatabase, error) { ) (*HSDatabase, error) {
@ -147,7 +139,9 @@ func NewHeadscaleDatabase(
DiscoKey string DiscoKey string
} }
var results []result var results []result
err = tx.Raw("SELECT id, node_key, machine_key, disco_key FROM nodes").Find(&results).Error err = tx.Raw("SELECT id, node_key, machine_key, disco_key FROM nodes").
Find(&results).
Error
if err != nil { if err != nil {
return err return err
} }
@ -180,7 +174,8 @@ func NewHeadscaleDatabase(
} }
if tx.Migrator().HasColumn(&types.Node{}, "enabled_routes") { if tx.Migrator().HasColumn(&types.Node{}, "enabled_routes") {
log.Info().Msgf("Database has legacy enabled_routes column in node, migrating...") log.Info().
Msgf("Database has legacy enabled_routes column in node, migrating...")
type NodeAux struct { type NodeAux struct {
ID uint64 ID uint64
@ -317,8 +312,7 @@ func NewHeadscaleDatabase(
} }
db := HSDatabase{ db := HSDatabase{
db: dbConn, DB: dbConn,
notifier: notifier,
ipPrefixes: ipPrefixes, ipPrefixes: ipPrefixes,
baseDomain: baseDomain, baseDomain: baseDomain,
@ -376,7 +370,7 @@ func openDB(dbType, connectionAddr string, debug bool) (*gorm.DB, error) {
func (hsdb *HSDatabase) PingDB(ctx context.Context) error { func (hsdb *HSDatabase) PingDB(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, time.Second) ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel() defer cancel()
sqlDB, err := hsdb.db.DB() sqlDB, err := hsdb.DB.DB()
if err != nil { if err != nil {
return err return err
} }
@ -385,10 +379,48 @@ func (hsdb *HSDatabase) PingDB(ctx context.Context) error {
} }
func (hsdb *HSDatabase) Close() error { func (hsdb *HSDatabase) Close() error {
db, err := hsdb.db.DB() db, err := hsdb.DB.DB()
if err != nil { if err != nil {
return err return err
} }
return db.Close() return db.Close()
} }
func (hsdb *HSDatabase) Read(fn func(rx *gorm.DB) error) error {
rx := hsdb.DB.Begin()
defer rx.Rollback()
return fn(rx)
}
func Read[T any](db *gorm.DB, fn func(rx *gorm.DB) (T, error)) (T, error) {
rx := db.Begin()
defer rx.Rollback()
ret, err := fn(rx)
if err != nil {
var no T
return no, err
}
return ret, nil
}
func (hsdb *HSDatabase) Write(fn func(tx *gorm.DB) error) error {
tx := hsdb.DB.Begin()
defer tx.Rollback()
if err := fn(tx); err != nil {
return err
}
return tx.Commit().Error
}
func Write[T any](db *gorm.DB, fn func(tx *gorm.DB) (T, error)) (T, error) {
tx := db.Begin()
defer tx.Rollback()
ret, err := fn(tx)
if err != nil {
var no T
return no, err
}
return ret, tx.Commit().Error
}

View file

@ -34,22 +34,21 @@ var (
) )
) )
// ListPeers returns all peers of node, regardless of any Policy or if the node is expired.
func (hsdb *HSDatabase) ListPeers(node *types.Node) (types.Nodes, error) { func (hsdb *HSDatabase) ListPeers(node *types.Node) (types.Nodes, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
defer hsdb.mu.RUnlock() return ListPeers(rx, node)
})
return hsdb.listPeers(node)
} }
func (hsdb *HSDatabase) listPeers(node *types.Node) (types.Nodes, error) { // ListPeers returns all peers of node, regardless of any Policy or if the node is expired.
func ListPeers(tx *gorm.DB, node *types.Node) (types.Nodes, error) {
log.Trace(). log.Trace().
Caller(). Caller().
Str("node", node.Hostname). Str("node", node.Hostname).
Msg("Finding direct peers") Msg("Finding direct peers")
nodes := types.Nodes{} nodes := types.Nodes{}
if err := hsdb.db. if err := tx.
Preload("AuthKey"). Preload("AuthKey").
Preload("AuthKey.User"). Preload("AuthKey.User").
Preload("User"). Preload("User").
@ -64,16 +63,15 @@ func (hsdb *HSDatabase) listPeers(node *types.Node) (types.Nodes, error) {
return nodes, nil return nodes, nil
} }
func (hsdb *HSDatabase) ListNodes() ([]types.Node, error) { func (hsdb *HSDatabase) ListNodes() (types.Nodes, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
defer hsdb.mu.RUnlock() return ListNodes(rx)
})
return hsdb.listNodes()
} }
func (hsdb *HSDatabase) listNodes() ([]types.Node, error) { func ListNodes(tx *gorm.DB) (types.Nodes, error) {
nodes := []types.Node{} nodes := types.Nodes{}
if err := hsdb.db. if err := tx.
Preload("AuthKey"). Preload("AuthKey").
Preload("AuthKey.User"). Preload("AuthKey.User").
Preload("User"). Preload("User").
@ -85,16 +83,9 @@ func (hsdb *HSDatabase) listNodes() ([]types.Node, error) {
return nodes, nil return nodes, nil
} }
func (hsdb *HSDatabase) ListNodesByGivenName(givenName string) (types.Nodes, error) { func listNodesByGivenName(tx *gorm.DB, givenName string) (types.Nodes, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.listNodesByGivenName(givenName)
}
func (hsdb *HSDatabase) listNodesByGivenName(givenName string) (types.Nodes, error) {
nodes := types.Nodes{} nodes := types.Nodes{}
if err := hsdb.db. if err := tx.
Preload("AuthKey"). Preload("AuthKey").
Preload("AuthKey.User"). Preload("AuthKey.User").
Preload("User"). Preload("User").
@ -106,12 +97,15 @@ func (hsdb *HSDatabase) listNodesByGivenName(givenName string) (types.Nodes, err
return nodes, nil return nodes, nil
} }
// GetNode finds a Node by name and user and returns the Node struct. func (hsdb *HSDatabase) getNode(user string, name string) (*types.Node, error) {
func (hsdb *HSDatabase) GetNode(user string, name string) (*types.Node, error) { return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
hsdb.mu.RLock() return getNode(rx, user, name)
defer hsdb.mu.RUnlock() })
}
nodes, err := hsdb.ListNodesByUser(user) // getNode finds a Node by name and user and returns the Node struct.
func getNode(tx *gorm.DB, user string, name string) (*types.Node, error) {
nodes, err := ListNodesByUser(tx, user)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,34 +119,16 @@ func (hsdb *HSDatabase) GetNode(user string, name string) (*types.Node, error) {
return nil, ErrNodeNotFound return nil, ErrNodeNotFound
} }
// GetNodeByGivenName finds a Node by given name and user and returns the Node struct. func (hsdb *HSDatabase) GetNodeByID(id uint64) (*types.Node, error) {
func (hsdb *HSDatabase) GetNodeByGivenName( return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
user string, return GetNodeByID(rx, id)
givenName string, })
) (*types.Node, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
node := types.Node{}
if err := hsdb.db.
Preload("AuthKey").
Preload("AuthKey.User").
Preload("User").
Preload("Routes").
Where("given_name = ?", givenName).First(&node).Error; err != nil {
return nil, err
}
return nil, ErrNodeNotFound
} }
// GetNodeByID finds a Node by ID and returns the Node struct. // GetNodeByID finds a Node by ID and returns the Node struct.
func (hsdb *HSDatabase) GetNodeByID(id uint64) (*types.Node, error) { func GetNodeByID(tx *gorm.DB, id uint64) (*types.Node, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
mach := types.Node{} mach := types.Node{}
if result := hsdb.db. if result := tx.
Preload("AuthKey"). Preload("AuthKey").
Preload("AuthKey.User"). Preload("AuthKey.User").
Preload("User"). Preload("User").
@ -164,21 +140,19 @@ func (hsdb *HSDatabase) GetNodeByID(id uint64) (*types.Node, error) {
return &mach, nil return &mach, nil
} }
// GetNodeByMachineKey finds a Node by its MachineKey and returns the Node struct. func (hsdb *HSDatabase) GetNodeByMachineKey(machineKey key.MachinePublic) (*types.Node, error) {
func (hsdb *HSDatabase) GetNodeByMachineKey( return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
machineKey key.MachinePublic, return GetNodeByMachineKey(rx, machineKey)
) (*types.Node, error) { })
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.getNodeByMachineKey(machineKey)
} }
func (hsdb *HSDatabase) getNodeByMachineKey( // GetNodeByMachineKey finds a Node by its MachineKey and returns the Node struct.
func GetNodeByMachineKey(
tx *gorm.DB,
machineKey key.MachinePublic, machineKey key.MachinePublic,
) (*types.Node, error) { ) (*types.Node, error) {
mach := types.Node{} mach := types.Node{}
if result := hsdb.db. if result := tx.
Preload("AuthKey"). Preload("AuthKey").
Preload("AuthKey.User"). Preload("AuthKey.User").
Preload("User"). Preload("User").
@ -190,36 +164,24 @@ func (hsdb *HSDatabase) getNodeByMachineKey(
return &mach, nil return &mach, nil
} }
// GetNodeByNodeKey finds a Node by its current NodeKey. func (hsdb *HSDatabase) GetNodeByAnyKey(
func (hsdb *HSDatabase) GetNodeByNodeKey( machineKey key.MachinePublic,
nodeKey key.NodePublic, nodeKey key.NodePublic,
oldNodeKey key.NodePublic,
) (*types.Node, error) { ) (*types.Node, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
defer hsdb.mu.RUnlock() return GetNodeByAnyKey(rx, machineKey, nodeKey, oldNodeKey)
})
node := types.Node{}
if result := hsdb.db.
Preload("AuthKey").
Preload("AuthKey.User").
Preload("User").
Preload("Routes").
First(&node, "node_key = ?",
nodeKey.String()); result.Error != nil {
return nil, result.Error
}
return &node, nil
} }
// GetNodeByAnyKey finds a Node by its MachineKey, its current NodeKey or the old one, and returns the Node struct. // GetNodeByAnyKey finds a Node by its MachineKey, its current NodeKey or the old one, and returns the Node struct.
func (hsdb *HSDatabase) GetNodeByAnyKey( // TODO(kradalby): see if we can remove this.
func GetNodeByAnyKey(
tx *gorm.DB,
machineKey key.MachinePublic, nodeKey key.NodePublic, oldNodeKey key.NodePublic, machineKey key.MachinePublic, nodeKey key.NodePublic, oldNodeKey key.NodePublic,
) (*types.Node, error) { ) (*types.Node, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
node := types.Node{} node := types.Node{}
if result := hsdb.db. if result := tx.
Preload("AuthKey"). Preload("AuthKey").
Preload("AuthKey.User"). Preload("AuthKey.User").
Preload("User"). Preload("User").
@ -234,60 +196,44 @@ func (hsdb *HSDatabase) GetNodeByAnyKey(
return &node, nil return &node, nil
} }
func (hsdb *HSDatabase) NodeReloadFromDatabase(node *types.Node) error { func (hsdb *HSDatabase) SetTags(
hsdb.mu.RLock() nodeID uint64,
defer hsdb.mu.RUnlock() tags []string,
) error {
if result := hsdb.db.Find(node).First(&node); result.Error != nil { return hsdb.Write(func(tx *gorm.DB) error {
return result.Error return SetTags(tx, nodeID, tags)
} })
return nil
} }
// SetTags takes a Node struct pointer and update the forced tags. // SetTags takes a Node struct pointer and update the forced tags.
func (hsdb *HSDatabase) SetTags( func SetTags(
node *types.Node, tx *gorm.DB,
nodeID uint64,
tags []string, tags []string,
) error { ) error {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
if len(tags) == 0 { if len(tags) == 0 {
return nil return nil
} }
newTags := []string{} newTags := types.StringList{}
for _, tag := range tags { for _, tag := range tags {
if !util.StringOrPrefixListContains(newTags, tag) { if !util.StringOrPrefixListContains(newTags, tag) {
newTags = append(newTags, tag) newTags = append(newTags, tag)
} }
} }
if err := hsdb.db.Model(node).Updates(types.Node{ if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("forced_tags", newTags).Error; err != nil {
ForcedTags: newTags,
}).Error; err != nil {
return fmt.Errorf("failed to update tags for node in the database: %w", err) return fmt.Errorf("failed to update tags for node in the database: %w", err)
} }
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node},
Message: "called from db.SetTags",
}
if stateUpdate.Valid() {
hsdb.notifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
}
return nil return nil
} }
// RenameNode takes a Node struct and a new GivenName for the nodes // RenameNode takes a Node struct and a new GivenName for the nodes
// and renames it. // and renames it.
func (hsdb *HSDatabase) RenameNode(node *types.Node, newName string) error { func RenameNode(tx *gorm.DB,
hsdb.mu.Lock() nodeID uint64, newName string,
defer hsdb.mu.Unlock() ) error {
err := util.CheckForFQDNRules( err := util.CheckForFQDNRules(
newName, newName,
) )
@ -295,129 +241,74 @@ func (hsdb *HSDatabase) RenameNode(node *types.Node, newName string) error {
log.Error(). log.Error().
Caller(). Caller().
Str("func", "RenameNode"). Str("func", "RenameNode").
Str("node", node.Hostname). Uint64("nodeID", nodeID).
Str("newName", newName). Str("newName", newName).
Err(err). Err(err).
Msg("failed to rename node") Msg("failed to rename node")
return err return err
} }
node.GivenName = newName
if err := hsdb.db.Model(node).Updates(types.Node{ if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil {
GivenName: newName,
}).Error; err != nil {
return fmt.Errorf("failed to rename node in the database: %w", err) return fmt.Errorf("failed to rename node in the database: %w", err)
} }
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node},
Message: "called from db.RenameNode",
}
if stateUpdate.Valid() {
hsdb.notifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
}
return nil return nil
} }
func (hsdb *HSDatabase) NodeSetExpiry(nodeID uint64, expiry time.Time) error {
return hsdb.Write(func(tx *gorm.DB) error {
return NodeSetExpiry(tx, nodeID, expiry)
})
}
// NodeSetExpiry takes a Node struct and a new expiry time. // NodeSetExpiry takes a Node struct and a new expiry time.
func (hsdb *HSDatabase) NodeSetExpiry(node *types.Node, expiry time.Time) error { func NodeSetExpiry(tx *gorm.DB,
hsdb.mu.Lock() nodeID uint64, expiry time.Time,
defer hsdb.mu.Unlock() ) error {
return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("expiry", expiry).Error
return hsdb.nodeSetExpiry(node, expiry)
} }
func (hsdb *HSDatabase) nodeSetExpiry(node *types.Node, expiry time.Time) error { func (hsdb *HSDatabase) DeleteNode(node *types.Node, isConnected map[key.MachinePublic]bool) error {
if err := hsdb.db.Model(node).Updates(types.Node{ return hsdb.Write(func(tx *gorm.DB) error {
Expiry: &expiry, return DeleteNode(tx, node, isConnected)
}).Error; err != nil { })
return fmt.Errorf(
"failed to refresh node (update expiration) in the database: %w",
err,
)
}
node.Expiry = &expiry
stateSelfUpdate := types.StateUpdate{
Type: types.StateSelfUpdate,
ChangeNodes: types.Nodes{node},
}
if stateSelfUpdate.Valid() {
hsdb.notifier.NotifyByMachineKey(stateSelfUpdate, node.MachineKey)
}
stateUpdate := types.StateUpdate{
Type: types.StatePeerChangedPatch,
ChangePatches: []*tailcfg.PeerChange{
{
NodeID: tailcfg.NodeID(node.ID),
KeyExpiry: &expiry,
},
},
}
if stateUpdate.Valid() {
hsdb.notifier.NotifyWithIgnore(stateUpdate, node.MachineKey.String())
}
return nil
} }
// DeleteNode deletes a Node from the database. // DeleteNode deletes a Node from the database.
func (hsdb *HSDatabase) DeleteNode(node *types.Node) error { // Caller is responsible for notifying all of change.
hsdb.mu.Lock() func DeleteNode(tx *gorm.DB,
defer hsdb.mu.Unlock() node *types.Node,
isConnected map[key.MachinePublic]bool,
return hsdb.deleteNode(node) ) error {
} err := deleteNodeRoutes(tx, node, map[key.MachinePublic]bool{})
func (hsdb *HSDatabase) deleteNode(node *types.Node) error {
err := hsdb.deleteNodeRoutes(node)
if err != nil { if err != nil {
return err return err
} }
// Unscoped causes the node to be fully removed from the database. // Unscoped causes the node to be fully removed from the database.
if err := hsdb.db.Unscoped().Delete(&node).Error; err != nil { if err := tx.Unscoped().Delete(&node).Error; err != nil {
return err return err
} }
stateUpdate := types.StateUpdate{
Type: types.StatePeerRemoved,
Removed: []tailcfg.NodeID{tailcfg.NodeID(node.ID)},
}
if stateUpdate.Valid() {
hsdb.notifier.NotifyAll(stateUpdate)
}
return nil return nil
} }
// UpdateLastSeen sets a node's last seen field indicating that we // UpdateLastSeen sets a node's last seen field indicating that we
// have recently communicating with this node. // have recently communicating with this node.
// This is mostly used to indicate if a node is online and is not func UpdateLastSeen(tx *gorm.DB, nodeID uint64, lastSeen time.Time) error {
// extremely important to make sure is fully correct and to avoid return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("last_seen", lastSeen).Error
// holding up the hot path, does not contain any locks and isnt
// concurrency safe. But that should be ok.
func (hsdb *HSDatabase) UpdateLastSeen(node *types.Node) error {
return hsdb.db.Model(node).Updates(types.Node{
LastSeen: node.LastSeen,
}).Error
} }
func (hsdb *HSDatabase) RegisterNodeFromAuthCallback( func RegisterNodeFromAuthCallback(
tx *gorm.DB,
cache *cache.Cache, cache *cache.Cache,
mkey key.MachinePublic, mkey key.MachinePublic,
userName string, userName string,
nodeExpiry *time.Time, nodeExpiry *time.Time,
registrationMethod string, registrationMethod string,
ipPrefixes []netip.Prefix,
) (*types.Node, error) { ) (*types.Node, error) {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
log.Debug(). log.Debug().
Str("machine_key", mkey.ShortString()). Str("machine_key", mkey.ShortString()).
Str("userName", userName). Str("userName", userName).
@ -427,7 +318,7 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
if nodeInterface, ok := cache.Get(mkey.String()); ok { if nodeInterface, ok := cache.Get(mkey.String()); ok {
if registrationNode, ok := nodeInterface.(types.Node); ok { if registrationNode, ok := nodeInterface.(types.Node); ok {
user, err := hsdb.getUser(userName) user, err := GetUser(tx, userName)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"failed to find user in register node from auth callback, %w", "failed to find user in register node from auth callback, %w",
@ -442,14 +333,17 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
} }
registrationNode.UserID = user.ID registrationNode.UserID = user.ID
registrationNode.User = *user
registrationNode.RegisterMethod = registrationMethod registrationNode.RegisterMethod = registrationMethod
if nodeExpiry != nil { if nodeExpiry != nil {
registrationNode.Expiry = nodeExpiry registrationNode.Expiry = nodeExpiry
} }
node, err := hsdb.registerNode( node, err := RegisterNode(
tx,
registrationNode, registrationNode,
ipPrefixes,
) )
if err == nil { if err == nil {
@ -465,15 +359,14 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
return nil, ErrNodeNotFoundRegistrationCache return nil, ErrNodeNotFoundRegistrationCache
} }
// RegisterNode is executed from the CLI to register a new Node using its MachineKey.
func (hsdb *HSDatabase) RegisterNode(node types.Node) (*types.Node, error) { func (hsdb *HSDatabase) RegisterNode(node types.Node) (*types.Node, error) {
hsdb.mu.Lock() return Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) {
defer hsdb.mu.Unlock() return RegisterNode(tx, node, hsdb.ipPrefixes)
})
return hsdb.registerNode(node)
} }
func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) { // RegisterNode is executed from the CLI to register a new Node using its MachineKey.
func RegisterNode(tx *gorm.DB, node types.Node, ipPrefixes []netip.Prefix) (*types.Node, error) {
log.Debug(). log.Debug().
Str("node", node.Hostname). Str("node", node.Hostname).
Str("machine_key", node.MachineKey.ShortString()). Str("machine_key", node.MachineKey.ShortString()).
@ -485,7 +378,7 @@ func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
// so we store the node.Expire and node.Nodekey that has been set when // so we store the node.Expire and node.Nodekey that has been set when
// adding it to the registrationCache // adding it to the registrationCache
if len(node.IPAddresses) > 0 { if len(node.IPAddresses) > 0 {
if err := hsdb.db.Save(&node).Error; err != nil { if err := tx.Save(&node).Error; err != nil {
return nil, fmt.Errorf("failed register existing node in the database: %w", err) return nil, fmt.Errorf("failed register existing node in the database: %w", err)
} }
@ -500,10 +393,7 @@ func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
return &node, nil return &node, nil
} }
hsdb.ipAllocationMutex.Lock() ips, err := getAvailableIPs(tx, ipPrefixes)
defer hsdb.ipAllocationMutex.Unlock()
ips, err := hsdb.getAvailableIPs()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
@ -516,7 +406,7 @@ func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
node.IPAddresses = ips node.IPAddresses = ips
if err := hsdb.db.Save(&node).Error; err != nil { if err := tx.Save(&node).Error; err != nil {
return nil, fmt.Errorf("failed register(save) node in the database: %w", err) return nil, fmt.Errorf("failed register(save) node in the database: %w", err)
} }
@ -530,61 +420,50 @@ func (hsdb *HSDatabase) registerNode(node types.Node) (*types.Node, error) {
} }
// NodeSetNodeKey sets the node key of a node and saves it to the database. // NodeSetNodeKey sets the node key of a node and saves it to the database.
func (hsdb *HSDatabase) NodeSetNodeKey(node *types.Node, nodeKey key.NodePublic) error { func NodeSetNodeKey(tx *gorm.DB, node *types.Node, nodeKey key.NodePublic) error {
hsdb.mu.Lock() return tx.Model(node).Updates(types.Node{
defer hsdb.mu.Unlock()
if err := hsdb.db.Model(node).Updates(types.Node{
NodeKey: nodeKey, NodeKey: nodeKey,
}).Error; err != nil { }).Error
return err
}
return nil
} }
// NodeSetMachineKey sets the node key of a node and saves it to the database.
func (hsdb *HSDatabase) NodeSetMachineKey( func (hsdb *HSDatabase) NodeSetMachineKey(
node *types.Node, node *types.Node,
machineKey key.MachinePublic, machineKey key.MachinePublic,
) error { ) error {
hsdb.mu.Lock() return hsdb.Write(func(tx *gorm.DB) error {
defer hsdb.mu.Unlock() return NodeSetMachineKey(tx, node, machineKey)
})
}
if err := hsdb.db.Model(node).Updates(types.Node{ // NodeSetMachineKey sets the node key of a node and saves it to the database.
func NodeSetMachineKey(
tx *gorm.DB,
node *types.Node,
machineKey key.MachinePublic,
) error {
return tx.Model(node).Updates(types.Node{
MachineKey: machineKey, MachineKey: machineKey,
}).Error; err != nil { }).Error
return err
}
return nil
} }
// NodeSave saves a node object to the database, prefer to use a specific save method rather // NodeSave saves a node object to the database, prefer to use a specific save method rather
// than this. It is intended to be used when we are changing or. // than this. It is intended to be used when we are changing or.
func (hsdb *HSDatabase) NodeSave(node *types.Node) error { // TODO(kradalby): Remove this func, just use Save.
hsdb.mu.Lock() func NodeSave(tx *gorm.DB, node *types.Node) error {
defer hsdb.mu.Unlock() return tx.Save(node).Error
}
if err := hsdb.db.Save(node).Error; err != nil { func (hsdb *HSDatabase) GetAdvertisedRoutes(node *types.Node) ([]netip.Prefix, error) {
return err return Read(hsdb.DB, func(rx *gorm.DB) ([]netip.Prefix, error) {
} return GetAdvertisedRoutes(rx, node)
})
return nil
} }
// GetAdvertisedRoutes returns the routes that are be advertised by the given node. // GetAdvertisedRoutes returns the routes that are be advertised by the given node.
func (hsdb *HSDatabase) GetAdvertisedRoutes(node *types.Node) ([]netip.Prefix, error) { func GetAdvertisedRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.getAdvertisedRoutes(node)
}
func (hsdb *HSDatabase) getAdvertisedRoutes(node *types.Node) ([]netip.Prefix, error) {
routes := types.Routes{} routes := types.Routes{}
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Where("node_id = ? AND advertised = ?", node.ID, true).Find(&routes).Error Where("node_id = ? AND advertised = ?", node.ID, true).Find(&routes).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@ -605,18 +484,17 @@ func (hsdb *HSDatabase) getAdvertisedRoutes(node *types.Node) ([]netip.Prefix, e
return prefixes, nil return prefixes, nil
} }
// GetEnabledRoutes returns the routes that are enabled for the node.
func (hsdb *HSDatabase) GetEnabledRoutes(node *types.Node) ([]netip.Prefix, error) { func (hsdb *HSDatabase) GetEnabledRoutes(node *types.Node) ([]netip.Prefix, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) ([]netip.Prefix, error) {
defer hsdb.mu.RUnlock() return GetEnabledRoutes(rx, node)
})
return hsdb.getEnabledRoutes(node)
} }
func (hsdb *HSDatabase) getEnabledRoutes(node *types.Node) ([]netip.Prefix, error) { // GetEnabledRoutes returns the routes that are enabled for the node.
func GetEnabledRoutes(tx *gorm.DB, node *types.Node) ([]netip.Prefix, error) {
routes := types.Routes{} routes := types.Routes{}
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Where("node_id = ? AND advertised = ? AND enabled = ?", node.ID, true, true). Where("node_id = ? AND advertised = ? AND enabled = ?", node.ID, true, true).
Find(&routes).Error Find(&routes).Error
@ -638,16 +516,13 @@ func (hsdb *HSDatabase) getEnabledRoutes(node *types.Node) ([]netip.Prefix, erro
return prefixes, nil return prefixes, nil
} }
func (hsdb *HSDatabase) IsRoutesEnabled(node *types.Node, routeStr string) bool { func IsRoutesEnabled(tx *gorm.DB, node *types.Node, routeStr string) bool {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
route, err := netip.ParsePrefix(routeStr) route, err := netip.ParsePrefix(routeStr)
if err != nil { if err != nil {
return false return false
} }
enabledRoutes, err := hsdb.getEnabledRoutes(node) enabledRoutes, err := GetEnabledRoutes(tx, node)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not get enabled routes") log.Error().Err(err).Msg("Could not get enabled routes")
@ -663,26 +538,37 @@ func (hsdb *HSDatabase) IsRoutesEnabled(node *types.Node, routeStr string) bool
return false return false
} }
func (hsdb *HSDatabase) enableRoutes(
node *types.Node,
routeStrs ...string,
) (*types.StateUpdate, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
return enableRoutes(tx, node, routeStrs...)
})
}
// enableRoutes enables new routes based on a list of new routes. // enableRoutes enables new routes based on a list of new routes.
func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) error { func enableRoutes(tx *gorm.DB,
node *types.Node, routeStrs ...string,
) (*types.StateUpdate, error) {
newRoutes := make([]netip.Prefix, len(routeStrs)) newRoutes := make([]netip.Prefix, len(routeStrs))
for index, routeStr := range routeStrs { for index, routeStr := range routeStrs {
route, err := netip.ParsePrefix(routeStr) route, err := netip.ParsePrefix(routeStr)
if err != nil { if err != nil {
return err return nil, err
} }
newRoutes[index] = route newRoutes[index] = route
} }
advertisedRoutes, err := hsdb.getAdvertisedRoutes(node) advertisedRoutes, err := GetAdvertisedRoutes(tx, node)
if err != nil { if err != nil {
return err return nil, err
} }
for _, newRoute := range newRoutes { for _, newRoute := range newRoutes {
if !util.StringOrPrefixListContains(advertisedRoutes, newRoute) { if !util.StringOrPrefixListContains(advertisedRoutes, newRoute) {
return fmt.Errorf( return nil, fmt.Errorf(
"route (%s) is not available on node %s: %w", "route (%s) is not available on node %s: %w",
node.Hostname, node.Hostname,
newRoute, ErrNodeRouteIsNotAvailable, newRoute, ErrNodeRouteIsNotAvailable,
@ -693,7 +579,7 @@ func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) erro
// Separate loop so we don't leave things in a half-updated state // Separate loop so we don't leave things in a half-updated state
for _, prefix := range newRoutes { for _, prefix := range newRoutes {
route := types.Route{} route := types.Route{}
err := hsdb.db.Preload("Node"). err := tx.Preload("Node").
Where("node_id = ? AND prefix = ?", node.ID, types.IPPrefix(prefix)). Where("node_id = ? AND prefix = ?", node.ID, types.IPPrefix(prefix)).
First(&route).Error First(&route).Error
if err == nil { if err == nil {
@ -702,23 +588,23 @@ func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) erro
// Mark already as primary if there is only this node offering this subnet // Mark already as primary if there is only this node offering this subnet
// (and is not an exit route) // (and is not an exit route)
if !route.IsExitRoute() { if !route.IsExitRoute() {
route.IsPrimary = hsdb.isUniquePrefix(route) route.IsPrimary = isUniquePrefix(tx, route)
} }
err = hsdb.db.Save(&route).Error err = tx.Save(&route).Error
if err != nil { if err != nil {
return fmt.Errorf("failed to enable route: %w", err) return nil, fmt.Errorf("failed to enable route: %w", err)
} }
} else { } else {
return fmt.Errorf("failed to find route: %w", err) return nil, fmt.Errorf("failed to find route: %w", err)
} }
} }
// Ensure the node has the latest routes when notifying the other // Ensure the node has the latest routes when notifying the other
// nodes // nodes
nRoutes, err := hsdb.getNodeRoutes(node) nRoutes, err := GetNodeRoutes(tx, node)
if err != nil { if err != nil {
return fmt.Errorf("failed to read back routes: %w", err) return nil, fmt.Errorf("failed to read back routes: %w", err)
} }
node.Routes = nRoutes node.Routes = nRoutes
@ -729,30 +615,11 @@ func (hsdb *HSDatabase) enableRoutes(node *types.Node, routeStrs ...string) erro
Strs("routes", routeStrs). Strs("routes", routeStrs).
Msg("enabling routes") Msg("enabling routes")
stateUpdate := types.StateUpdate{ return &types.StateUpdate{
Type: types.StatePeerChanged, Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node}, ChangeNodes: types.Nodes{node},
Message: "called from db.enableRoutes", Message: "created in db.enableRoutes",
} }, nil
if stateUpdate.Valid() {
hsdb.notifier.NotifyWithIgnore(
stateUpdate, node.MachineKey.String())
}
// Send an update to the node itself with to ensure it
// has an updated packetfilter allowing the new route
// if it is defined in the ACL.
selfUpdate := types.StateUpdate{
Type: types.StateSelfUpdate,
ChangeNodes: types.Nodes{node},
}
if selfUpdate.Valid() {
hsdb.notifier.NotifyByMachineKey(
selfUpdate,
node.MachineKey)
}
return nil
} }
func generateGivenName(suppliedName string, randomSuffix bool) (string, error) { func generateGivenName(suppliedName string, randomSuffix bool) (string, error) {
@ -785,16 +652,23 @@ func (hsdb *HSDatabase) GenerateGivenName(
mkey key.MachinePublic, mkey key.MachinePublic,
suppliedName string, suppliedName string,
) (string, error) { ) (string, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) (string, error) {
defer hsdb.mu.RUnlock() return GenerateGivenName(rx, mkey, suppliedName)
})
}
func GenerateGivenName(
tx *gorm.DB,
mkey key.MachinePublic,
suppliedName string,
) (string, error) {
givenName, err := generateGivenName(suppliedName, false) givenName, err := generateGivenName(suppliedName, false)
if err != nil { if err != nil {
return "", err return "", err
} }
// Tailscale rules (may differ) https://tailscale.com/kb/1098/machine-names/ // Tailscale rules (may differ) https://tailscale.com/kb/1098/machine-names/
nodes, err := hsdb.listNodesByGivenName(givenName) nodes, err := listNodesByGivenName(tx, givenName)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -818,29 +692,28 @@ func (hsdb *HSDatabase) GenerateGivenName(
return givenName, nil return givenName, nil
} }
func (hsdb *HSDatabase) ExpireEphemeralNodes(inactivityThreshhold time.Duration) { func ExpireEphemeralNodes(tx *gorm.DB,
hsdb.mu.Lock() inactivityThreshhold time.Duration,
defer hsdb.mu.Unlock() ) (types.StateUpdate, bool) {
users, err := ListUsers(tx)
users, err := hsdb.listUsers()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error listing users") log.Error().Err(err).Msg("Error listing users")
return return types.StateUpdate{}, false
} }
expired := make([]tailcfg.NodeID, 0)
for _, user := range users { for _, user := range users {
nodes, err := hsdb.listNodesByUser(user.Name) nodes, err := ListNodesByUser(tx, user.Name)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Str("user", user.Name). Str("user", user.Name).
Msg("Error listing nodes in user") Msg("Error listing nodes in user")
return return types.StateUpdate{}, false
} }
expired := make([]tailcfg.NodeID, 0)
for idx, node := range nodes { for idx, node := range nodes {
if node.IsEphemeral() && node.LastSeen != nil && if node.IsEphemeral() && node.LastSeen != nil &&
time.Now(). time.Now().
@ -851,7 +724,8 @@ func (hsdb *HSDatabase) ExpireEphemeralNodes(inactivityThreshhold time.Duration)
Str("node", node.Hostname). Str("node", node.Hostname).
Msg("Ephemeral client removed from database") Msg("Ephemeral client removed from database")
err = hsdb.deleteNode(nodes[idx]) // empty isConnected map as ephemeral nodes are not routes
err = DeleteNode(tx, nodes[idx], map[key.MachinePublic]bool{})
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -861,33 +735,35 @@ func (hsdb *HSDatabase) ExpireEphemeralNodes(inactivityThreshhold time.Duration)
} }
} }
if len(expired) > 0 { // TODO(kradalby): needs to be moved out of transaction
hsdb.notifier.NotifyAll(types.StateUpdate{
Type: types.StatePeerRemoved,
Removed: expired,
})
}
} }
if len(expired) > 0 {
return types.StateUpdate{
Type: types.StatePeerRemoved,
Removed: expired,
}, true
}
return types.StateUpdate{}, false
} }
func (hsdb *HSDatabase) ExpireExpiredNodes(lastCheck time.Time) time.Time { func ExpireExpiredNodes(tx *gorm.DB,
hsdb.mu.Lock() lastCheck time.Time,
defer hsdb.mu.Unlock() ) (time.Time, types.StateUpdate, bool) {
// use the time of the start of the function to ensure we // use the time of the start of the function to ensure we
// dont miss some nodes by returning it _after_ we have // dont miss some nodes by returning it _after_ we have
// checked everything. // checked everything.
started := time.Now() started := time.Now()
expiredNodes := make([]*types.Node, 0) expired := make([]*tailcfg.PeerChange, 0)
nodes, err := hsdb.listNodes() nodes, err := ListNodes(tx)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Msg("Error listing nodes to find expired nodes") Msg("Error listing nodes to find expired nodes")
return time.Unix(0, 0) return time.Unix(0, 0), types.StateUpdate{}, false
} }
for index, node := range nodes { for index, node := range nodes {
if node.IsExpired() && if node.IsExpired() &&
@ -895,13 +771,17 @@ func (hsdb *HSDatabase) ExpireExpiredNodes(lastCheck time.Time) time.Time {
// It will notify about all nodes that has been expired. // It will notify about all nodes that has been expired.
// It should only notify about expired nodes since _last check_. // It should only notify about expired nodes since _last check_.
node.Expiry.After(lastCheck) { node.Expiry.After(lastCheck) {
expiredNodes = append(expiredNodes, &nodes[index]) expired = append(expired, &tailcfg.PeerChange{
NodeID: tailcfg.NodeID(node.ID),
KeyExpiry: node.Expiry,
})
now := time.Now()
// Do not use setNodeExpiry as that has a notifier hook, which // Do not use setNodeExpiry as that has a notifier hook, which
// can cause a deadlock, we are updating all changed nodes later // can cause a deadlock, we are updating all changed nodes later
// and there is no point in notifiying twice. // and there is no point in notifiying twice.
if err := hsdb.db.Model(&nodes[index]).Updates(types.Node{ if err := tx.Model(&nodes[index]).Updates(types.Node{
Expiry: &started, Expiry: &now,
}).Error; err != nil { }).Error; err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -917,33 +797,12 @@ func (hsdb *HSDatabase) ExpireExpiredNodes(lastCheck time.Time) time.Time {
} }
} }
expired := make([]*tailcfg.PeerChange, len(expiredNodes)) if len(expired) > 0 {
for idx, node := range expiredNodes { return started, types.StateUpdate{
expired[idx] = &tailcfg.PeerChange{ Type: types.StatePeerChangedPatch,
NodeID: tailcfg.NodeID(node.ID), ChangePatches: expired,
KeyExpiry: &started, }, true
}
} }
// Inform the peers of a node with a lightweight update. return started, types.StateUpdate{}, false
stateUpdate := types.StateUpdate{
Type: types.StatePeerChangedPatch,
ChangePatches: expired,
}
if stateUpdate.Valid() {
hsdb.notifier.NotifyAll(stateUpdate)
}
// Inform the node itself that it has expired.
for _, node := range expiredNodes {
stateSelfUpdate := types.StateUpdate{
Type: types.StateSelfUpdate,
ChangeNodes: types.Nodes{node},
}
if stateSelfUpdate.Valid() {
hsdb.notifier.NotifyByMachineKey(stateSelfUpdate, node.MachineKey)
}
}
return started
} }

View file

@ -23,7 +23,7 @@ func (s *Suite) TestGetNode(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "testnode") _, err = db.getNode("test", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
nodeKey := key.NewNode() nodeKey := key.NewNode()
@ -38,9 +38,9 @@ func (s *Suite) TestGetNode(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(node) db.DB.Save(node)
_, err = db.GetNode("test", "testnode") _, err = db.getNode("test", "testnode")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }
@ -66,40 +66,12 @@ func (s *Suite) TestGetNodeByID(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
_, err = db.GetNodeByID(0) _, err = db.GetNodeByID(0)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }
func (s *Suite) TestGetNodeByNodeKey(c *check.C) {
user, err := db.CreateUser("test")
c.Assert(err, check.IsNil)
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil)
_, err = db.GetNodeByID(0)
c.Assert(err, check.NotNil)
nodeKey := key.NewNode()
machineKey := key.NewMachine()
node := types.Node{
ID: 0,
MachineKey: machineKey.Public(),
NodeKey: nodeKey.Public(),
Hostname: "testnode",
UserID: user.ID,
RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID),
}
db.db.Save(&node)
_, err = db.GetNodeByNodeKey(nodeKey.Public())
c.Assert(err, check.IsNil)
}
func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
user, err := db.CreateUser("test") user, err := db.CreateUser("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -124,7 +96,7 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
_, err = db.GetNodeByAnyKey(machineKey.Public(), nodeKey.Public(), oldNodeKey.Public()) _, err = db.GetNodeByAnyKey(machineKey.Public(), nodeKey.Public(), oldNodeKey.Public())
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -146,12 +118,12 @@ func (s *Suite) TestHardDeleteNode(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(1), AuthKeyID: uint(1),
} }
db.db.Save(&node) db.DB.Save(&node)
err = db.DeleteNode(&node) err = db.DeleteNode(&node, map[key.MachinePublic]bool{})
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode(user.Name, "testnode3") _, err = db.getNode(user.Name, "testnode3")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
@ -178,7 +150,7 @@ func (s *Suite) TestListPeers(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
} }
node0ByID, err := db.GetNodeByID(0) node0ByID, err := db.GetNodeByID(0)
@ -228,7 +200,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(stor[index%2].key.ID), AuthKeyID: uint(stor[index%2].key.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
} }
aclPolicy := &policy.ACLPolicy{ aclPolicy := &policy.ACLPolicy{
@ -295,7 +267,7 @@ func (s *Suite) TestExpireNode(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "testnode") _, err = db.getNode("test", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
nodeKey := key.NewNode() nodeKey := key.NewNode()
@ -311,16 +283,19 @@ func (s *Suite) TestExpireNode(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
Expiry: &time.Time{}, Expiry: &time.Time{},
} }
db.db.Save(node) db.DB.Save(node)
nodeFromDB, err := db.GetNode("test", "testnode") nodeFromDB, err := db.getNode("test", "testnode")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(nodeFromDB, check.NotNil) c.Assert(nodeFromDB, check.NotNil)
c.Assert(nodeFromDB.IsExpired(), check.Equals, false) c.Assert(nodeFromDB.IsExpired(), check.Equals, false)
now := time.Now() now := time.Now()
err = db.NodeSetExpiry(nodeFromDB, now) err = db.NodeSetExpiry(nodeFromDB.ID, now)
c.Assert(err, check.IsNil)
nodeFromDB, err = db.getNode("test", "testnode")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(nodeFromDB.IsExpired(), check.Equals, true) c.Assert(nodeFromDB.IsExpired(), check.Equals, true)
@ -354,7 +329,7 @@ func (s *Suite) TestGenerateGivenName(c *check.C) {
pak, err := db.CreatePreAuthKey(user1.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user1.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("user-1", "testnode") _, err = db.getNode("user-1", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
nodeKey := key.NewNode() nodeKey := key.NewNode()
@ -372,7 +347,7 @@ func (s *Suite) TestGenerateGivenName(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(node) db.DB.Save(node)
givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2") givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2")
comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict") comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict")
@ -397,7 +372,7 @@ func (s *Suite) TestSetTags(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "testnode") _, err = db.getNode("test", "testnode")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
nodeKey := key.NewNode() nodeKey := key.NewNode()
@ -412,21 +387,21 @@ func (s *Suite) TestSetTags(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(node) db.DB.Save(node)
// assign simple tags // assign simple tags
sTags := []string{"tag:test", "tag:foo"} sTags := []string{"tag:test", "tag:foo"}
err = db.SetTags(node, sTags) err = db.SetTags(node.ID, sTags)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
node, err = db.GetNode("test", "testnode") node, err = db.getNode("test", "testnode")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(node.ForcedTags, check.DeepEquals, types.StringList(sTags)) c.Assert(node.ForcedTags, check.DeepEquals, types.StringList(sTags))
// assign duplicat tags, expect no errors but no doubles in DB // assign duplicat tags, expect no errors but no doubles in DB
eTags := []string{"tag:bar", "tag:test", "tag:unknown", "tag:test"} eTags := []string{"tag:bar", "tag:test", "tag:unknown", "tag:test"}
err = db.SetTags(node, eTags) err = db.SetTags(node.ID, eTags)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
node, err = db.GetNode("test", "testnode") node, err = db.getNode("test", "testnode")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert( c.Assert(
node.ForcedTags, node.ForcedTags,
@ -601,7 +576,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
} }
db.db.Save(&node) db.DB.Save(&node)
sendUpdate, err := db.SaveNodeRoutes(&node) sendUpdate, err := db.SaveNodeRoutes(&node)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -610,7 +585,8 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
node0ByID, err := db.GetNodeByID(0) node0ByID, err := db.GetNodeByID(0)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
err = db.EnableAutoApprovedRoutes(pol, node0ByID) // TODO(kradalby): Check state update
_, err = db.EnableAutoApprovedRoutes(pol, node0ByID)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
enabledRoutes, err := db.GetEnabledRoutes(node0ByID) enabledRoutes, err := db.GetEnabledRoutes(node0ByID)

View file

@ -20,7 +20,6 @@ var (
ErrPreAuthKeyACLTagInvalid = errors.New("AuthKey tag is invalid") ErrPreAuthKeyACLTagInvalid = errors.New("AuthKey tag is invalid")
) )
// CreatePreAuthKey creates a new PreAuthKey in a user, and returns it.
func (hsdb *HSDatabase) CreatePreAuthKey( func (hsdb *HSDatabase) CreatePreAuthKey(
userName string, userName string,
reusable bool, reusable bool,
@ -28,11 +27,21 @@ func (hsdb *HSDatabase) CreatePreAuthKey(
expiration *time.Time, expiration *time.Time,
aclTags []string, aclTags []string,
) (*types.PreAuthKey, error) { ) (*types.PreAuthKey, error) {
// TODO(kradalby): figure out this lock return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
// hsdb.mu.Lock() return CreatePreAuthKey(tx, userName, reusable, ephemeral, expiration, aclTags)
// defer hsdb.mu.Unlock() })
}
user, err := hsdb.GetUser(userName) // CreatePreAuthKey creates a new PreAuthKey in a user, and returns it.
func CreatePreAuthKey(
tx *gorm.DB,
userName string,
reusable bool,
ephemeral bool,
expiration *time.Time,
aclTags []string,
) (*types.PreAuthKey, error) {
user, err := GetUser(tx, userName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,7 +57,7 @@ func (hsdb *HSDatabase) CreatePreAuthKey(
} }
now := time.Now().UTC() now := time.Now().UTC()
kstr, err := hsdb.generateKey() kstr, err := generateKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,29 +72,25 @@ func (hsdb *HSDatabase) CreatePreAuthKey(
Expiration: expiration, Expiration: expiration,
} }
err = hsdb.db.Transaction(func(db *gorm.DB) error { if err := tx.Save(&key).Error; err != nil {
if err := db.Save(&key).Error; err != nil { return nil, fmt.Errorf("failed to create key in the database: %w", err)
return fmt.Errorf("failed to create key in the database: %w", err) }
}
if len(aclTags) > 0 { if len(aclTags) > 0 {
seenTags := map[string]bool{} seenTags := map[string]bool{}
for _, tag := range aclTags { for _, tag := range aclTags {
if !seenTags[tag] { if !seenTags[tag] {
if err := db.Save(&types.PreAuthKeyACLTag{PreAuthKeyID: key.ID, Tag: tag}).Error; err != nil { if err := tx.Save(&types.PreAuthKeyACLTag{PreAuthKeyID: key.ID, Tag: tag}).Error; err != nil {
return fmt.Errorf( return nil, fmt.Errorf(
"failed to ceate key tag in the database: %w", "failed to ceate key tag in the database: %w",
err, err,
) )
}
seenTags[tag] = true
} }
seenTags[tag] = true
} }
} }
}
return nil
})
if err != nil { if err != nil {
return nil, err return nil, err
@ -94,22 +99,21 @@ func (hsdb *HSDatabase) CreatePreAuthKey(
return &key, nil return &key, nil
} }
// ListPreAuthKeys returns the list of PreAuthKeys for a user.
func (hsdb *HSDatabase) ListPreAuthKeys(userName string) ([]types.PreAuthKey, error) { func (hsdb *HSDatabase) ListPreAuthKeys(userName string) ([]types.PreAuthKey, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
defer hsdb.mu.RUnlock() return ListPreAuthKeys(rx, userName)
})
return hsdb.listPreAuthKeys(userName)
} }
func (hsdb *HSDatabase) listPreAuthKeys(userName string) ([]types.PreAuthKey, error) { // ListPreAuthKeys returns the list of PreAuthKeys for a user.
user, err := hsdb.getUser(userName) func ListPreAuthKeys(tx *gorm.DB, userName string) ([]types.PreAuthKey, error) {
user, err := GetUser(tx, userName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
keys := []types.PreAuthKey{} keys := []types.PreAuthKey{}
if err := hsdb.db.Preload("User").Preload("ACLTags").Where(&types.PreAuthKey{UserID: user.ID}).Find(&keys).Error; err != nil { if err := tx.Preload("User").Preload("ACLTags").Where(&types.PreAuthKey{UserID: user.ID}).Find(&keys).Error; err != nil {
return nil, err return nil, err
} }
@ -117,11 +121,8 @@ func (hsdb *HSDatabase) listPreAuthKeys(userName string) ([]types.PreAuthKey, er
} }
// GetPreAuthKey returns a PreAuthKey for a given key. // GetPreAuthKey returns a PreAuthKey for a given key.
func (hsdb *HSDatabase) GetPreAuthKey(user string, key string) (*types.PreAuthKey, error) { func GetPreAuthKey(tx *gorm.DB, user string, key string) (*types.PreAuthKey, error) {
hsdb.mu.RLock() pak, err := ValidatePreAuthKey(tx, key)
defer hsdb.mu.RUnlock()
pak, err := hsdb.ValidatePreAuthKey(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -135,15 +136,8 @@ func (hsdb *HSDatabase) GetPreAuthKey(user string, key string) (*types.PreAuthKe
// DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey // DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey
// does not exist. // does not exist.
func (hsdb *HSDatabase) DestroyPreAuthKey(pak types.PreAuthKey) error { func DestroyPreAuthKey(tx *gorm.DB, pak types.PreAuthKey) error {
hsdb.mu.Lock() return tx.Transaction(func(db *gorm.DB) error {
defer hsdb.mu.Unlock()
return hsdb.destroyPreAuthKey(pak)
}
func (hsdb *HSDatabase) destroyPreAuthKey(pak types.PreAuthKey) error {
return hsdb.db.Transaction(func(db *gorm.DB) error {
if result := db.Unscoped().Where(types.PreAuthKeyACLTag{PreAuthKeyID: pak.ID}).Delete(&types.PreAuthKeyACLTag{}); result.Error != nil { if result := db.Unscoped().Where(types.PreAuthKeyACLTag{PreAuthKeyID: pak.ID}).Delete(&types.PreAuthKeyACLTag{}); result.Error != nil {
return result.Error return result.Error
} }
@ -156,12 +150,15 @@ func (hsdb *HSDatabase) destroyPreAuthKey(pak types.PreAuthKey) error {
}) })
} }
// MarkExpirePreAuthKey marks a PreAuthKey as expired.
func (hsdb *HSDatabase) ExpirePreAuthKey(k *types.PreAuthKey) error { func (hsdb *HSDatabase) ExpirePreAuthKey(k *types.PreAuthKey) error {
hsdb.mu.Lock() return hsdb.Write(func(tx *gorm.DB) error {
defer hsdb.mu.Unlock() return ExpirePreAuthKey(tx, k)
})
}
if err := hsdb.db.Model(&k).Update("Expiration", time.Now()).Error; err != nil { // MarkExpirePreAuthKey marks a PreAuthKey as expired.
func ExpirePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
if err := tx.Model(&k).Update("Expiration", time.Now()).Error; err != nil {
return err return err
} }
@ -169,26 +166,26 @@ func (hsdb *HSDatabase) ExpirePreAuthKey(k *types.PreAuthKey) error {
} }
// UsePreAuthKey marks a PreAuthKey as used. // UsePreAuthKey marks a PreAuthKey as used.
func (hsdb *HSDatabase) UsePreAuthKey(k *types.PreAuthKey) error { func UsePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
k.Used = true k.Used = true
if err := hsdb.db.Save(k).Error; err != nil { if err := tx.Save(k).Error; err != nil {
return fmt.Errorf("failed to update key used status in the database: %w", err) return fmt.Errorf("failed to update key used status in the database: %w", err)
} }
return nil return nil
} }
func (hsdb *HSDatabase) ValidatePreAuthKey(k string) (*types.PreAuthKey, error) {
return Read(hsdb.DB, func(rx *gorm.DB) (*types.PreAuthKey, error) {
return ValidatePreAuthKey(rx, k)
})
}
// ValidatePreAuthKey does the heavy lifting for validation of the PreAuthKey coming from a node // ValidatePreAuthKey does the heavy lifting for validation of the PreAuthKey coming from a node
// If returns no error and a PreAuthKey, it can be used. // If returns no error and a PreAuthKey, it can be used.
func (hsdb *HSDatabase) ValidatePreAuthKey(k string) (*types.PreAuthKey, error) { func ValidatePreAuthKey(tx *gorm.DB, k string) (*types.PreAuthKey, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
pak := types.PreAuthKey{} pak := types.PreAuthKey{}
if result := hsdb.db.Preload("User").Preload("ACLTags").First(&pak, "key = ?", k); errors.Is( if result := tx.Preload("User").Preload("ACLTags").First(&pak, "key = ?", k); errors.Is(
result.Error, result.Error,
gorm.ErrRecordNotFound, gorm.ErrRecordNotFound,
) { ) {
@ -204,7 +201,7 @@ func (hsdb *HSDatabase) ValidatePreAuthKey(k string) (*types.PreAuthKey, error)
} }
nodes := types.Nodes{} nodes := types.Nodes{}
if err := hsdb.db. if err := tx.
Preload("AuthKey"). Preload("AuthKey").
Where(&types.Node{AuthKeyID: uint(pak.ID)}). Where(&types.Node{AuthKeyID: uint(pak.ID)}).
Find(&nodes).Error; err != nil { Find(&nodes).Error; err != nil {
@ -218,7 +215,7 @@ func (hsdb *HSDatabase) ValidatePreAuthKey(k string) (*types.PreAuthKey, error)
return &pak, nil return &pak, nil
} }
func (hsdb *HSDatabase) generateKey() (string, error) { func generateKey() (string, error) {
size := 24 size := 24
bytes := make([]byte, size) bytes := make([]byte, size)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {

View file

@ -6,6 +6,7 @@ import (
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"gorm.io/gorm"
) )
func (*Suite) TestCreatePreAuthKey(c *check.C) { func (*Suite) TestCreatePreAuthKey(c *check.C) {
@ -41,7 +42,7 @@ func (*Suite) TestExpiredPreAuthKey(c *check.C) {
user, err := db.CreateUser("test2") user, err := db.CreateUser("test2")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
now := time.Now() now := time.Now().Add(-5 * time.Second)
pak, err := db.CreatePreAuthKey(user.Name, true, false, &now, nil) pak, err := db.CreatePreAuthKey(user.Name, true, false, &now, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -82,7 +83,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
key, err := db.ValidatePreAuthKey(pak.Key) key, err := db.ValidatePreAuthKey(pak.Key)
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed) c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)
@ -103,7 +104,7 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
key, err := db.ValidatePreAuthKey(pak.Key) key, err := db.ValidatePreAuthKey(pak.Key)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -138,19 +139,22 @@ func (*Suite) TestEphemeralKey(c *check.C) {
LastSeen: &now, LastSeen: &now,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
_, err = db.ValidatePreAuthKey(pak.Key) _, err = db.ValidatePreAuthKey(pak.Key)
// Ephemeral keys are by definition reusable // Ephemeral keys are by definition reusable
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test7", "testest") _, err = db.getNode("test7", "testest")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db.ExpireEphemeralNodes(time.Second * 20) db.DB.Transaction(func(tx *gorm.DB) error {
ExpireEphemeralNodes(tx, time.Second*20)
return nil
})
// The machine record should have been deleted // The machine record should have been deleted
_, err = db.GetNode("test7", "testest") _, err = db.getNode("test7", "testest")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
} }
@ -178,7 +182,7 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
pak.Used = true pak.Used = true
db.db.Save(&pak) db.DB.Save(&pak)
_, err = db.ValidatePreAuthKey(pak.Key) _, err = db.ValidatePreAuthKey(pak.Key)
c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed) c.Assert(err, check.Equals, ErrSingleUseAuthKeyHasBeenUsed)

View file

@ -7,23 +7,15 @@ import (
"github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/samber/lo"
"gorm.io/gorm" "gorm.io/gorm"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
var ErrRouteIsNotAvailable = errors.New("route is not available") var ErrRouteIsNotAvailable = errors.New("route is not available")
func (hsdb *HSDatabase) GetRoutes() (types.Routes, error) { func GetRoutes(tx *gorm.DB) (types.Routes, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.getRoutes()
}
func (hsdb *HSDatabase) getRoutes() (types.Routes, error) {
var routes types.Routes var routes types.Routes
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Preload("Node.User"). Preload("Node.User").
Find(&routes).Error Find(&routes).Error
@ -34,9 +26,9 @@ func (hsdb *HSDatabase) getRoutes() (types.Routes, error) {
return routes, nil return routes, nil
} }
func (hsdb *HSDatabase) getAdvertisedAndEnabledRoutes() (types.Routes, error) { func getAdvertisedAndEnabledRoutes(tx *gorm.DB) (types.Routes, error) {
var routes types.Routes var routes types.Routes
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Preload("Node.User"). Preload("Node.User").
Where("advertised = ? AND enabled = ?", true, true). Where("advertised = ? AND enabled = ?", true, true).
@ -48,9 +40,9 @@ func (hsdb *HSDatabase) getAdvertisedAndEnabledRoutes() (types.Routes, error) {
return routes, nil return routes, nil
} }
func (hsdb *HSDatabase) getRoutesByPrefix(pref netip.Prefix) (types.Routes, error) { func getRoutesByPrefix(tx *gorm.DB, pref netip.Prefix) (types.Routes, error) {
var routes types.Routes var routes types.Routes
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Preload("Node.User"). Preload("Node.User").
Where("prefix = ?", types.IPPrefix(pref)). Where("prefix = ?", types.IPPrefix(pref)).
@ -62,16 +54,9 @@ func (hsdb *HSDatabase) getRoutesByPrefix(pref netip.Prefix) (types.Routes, erro
return routes, nil return routes, nil
} }
func (hsdb *HSDatabase) GetNodeAdvertisedRoutes(node *types.Node) (types.Routes, error) { func GetNodeAdvertisedRoutes(tx *gorm.DB, node *types.Node) (types.Routes, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.getNodeAdvertisedRoutes(node)
}
func (hsdb *HSDatabase) getNodeAdvertisedRoutes(node *types.Node) (types.Routes, error) {
var routes types.Routes var routes types.Routes
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Preload("Node.User"). Preload("Node.User").
Where("node_id = ? AND advertised = true", node.ID). Where("node_id = ? AND advertised = true", node.ID).
@ -84,15 +69,14 @@ func (hsdb *HSDatabase) getNodeAdvertisedRoutes(node *types.Node) (types.Routes,
} }
func (hsdb *HSDatabase) GetNodeRoutes(node *types.Node) (types.Routes, error) { func (hsdb *HSDatabase) GetNodeRoutes(node *types.Node) (types.Routes, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) (types.Routes, error) {
defer hsdb.mu.RUnlock() return GetNodeRoutes(rx, node)
})
return hsdb.getNodeRoutes(node)
} }
func (hsdb *HSDatabase) getNodeRoutes(node *types.Node) (types.Routes, error) { func GetNodeRoutes(tx *gorm.DB, node *types.Node) (types.Routes, error) {
var routes types.Routes var routes types.Routes
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Preload("Node.User"). Preload("Node.User").
Where("node_id = ?", node.ID). Where("node_id = ?", node.ID).
@ -104,16 +88,9 @@ func (hsdb *HSDatabase) getNodeRoutes(node *types.Node) (types.Routes, error) {
return routes, nil return routes, nil
} }
func (hsdb *HSDatabase) GetRoute(id uint64) (*types.Route, error) { func GetRoute(tx *gorm.DB, id uint64) (*types.Route, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.getRoute(id)
}
func (hsdb *HSDatabase) getRoute(id uint64) (*types.Route, error) {
var route types.Route var route types.Route
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Preload("Node.User"). Preload("Node.User").
First(&route, id).Error First(&route, id).Error
@ -124,40 +101,34 @@ func (hsdb *HSDatabase) getRoute(id uint64) (*types.Route, error) {
return &route, nil return &route, nil
} }
func (hsdb *HSDatabase) EnableRoute(id uint64) error { func EnableRoute(tx *gorm.DB, id uint64) (*types.StateUpdate, error) {
hsdb.mu.Lock() route, err := GetRoute(tx, id)
defer hsdb.mu.Unlock()
return hsdb.enableRoute(id)
}
func (hsdb *HSDatabase) enableRoute(id uint64) error {
route, err := hsdb.getRoute(id)
if err != nil { if err != nil {
return err return nil, err
} }
// Tailscale requires both IPv4 and IPv6 exit routes to // Tailscale requires both IPv4 and IPv6 exit routes to
// be enabled at the same time, as per // be enabled at the same time, as per
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
if route.IsExitRoute() { if route.IsExitRoute() {
return hsdb.enableRoutes( return enableRoutes(
tx,
&route.Node, &route.Node,
types.ExitRouteV4.String(), types.ExitRouteV4.String(),
types.ExitRouteV6.String(), types.ExitRouteV6.String(),
) )
} }
return hsdb.enableRoutes(&route.Node, netip.Prefix(route.Prefix).String()) return enableRoutes(tx, &route.Node, netip.Prefix(route.Prefix).String())
} }
func (hsdb *HSDatabase) DisableRoute(id uint64) error { func DisableRoute(tx *gorm.DB,
hsdb.mu.Lock() id uint64,
defer hsdb.mu.Unlock() isConnected map[key.MachinePublic]bool,
) (*types.StateUpdate, error) {
route, err := hsdb.getRoute(id) route, err := GetRoute(tx, id)
if err != nil { if err != nil {
return err return nil, err
} }
var routes types.Routes var routes types.Routes
@ -166,64 +137,79 @@ func (hsdb *HSDatabase) DisableRoute(id uint64) error {
// Tailscale requires both IPv4 and IPv6 exit routes to // Tailscale requires both IPv4 and IPv6 exit routes to
// be enabled at the same time, as per // be enabled at the same time, as per
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
var update *types.StateUpdate
if !route.IsExitRoute() { if !route.IsExitRoute() {
err = hsdb.failoverRouteWithNotify(route) update, err = failoverRouteReturnUpdate(tx, isConnected, route)
if err != nil { if err != nil {
return err return nil, err
} }
route.Enabled = false route.Enabled = false
route.IsPrimary = false route.IsPrimary = false
err = hsdb.db.Save(route).Error err = tx.Save(route).Error
if err != nil { if err != nil {
return err return nil, err
} }
} else { } else {
routes, err = hsdb.getNodeRoutes(&node) routes, err = GetNodeRoutes(tx, &node)
if err != nil { if err != nil {
return err return nil, err
} }
for i := range routes { for i := range routes {
if routes[i].IsExitRoute() { if routes[i].IsExitRoute() {
routes[i].Enabled = false routes[i].Enabled = false
routes[i].IsPrimary = false routes[i].IsPrimary = false
err = hsdb.db.Save(&routes[i]).Error err = tx.Save(&routes[i]).Error
if err != nil { if err != nil {
return err return nil, err
} }
} }
} }
} }
if routes == nil { if routes == nil {
routes, err = hsdb.getNodeRoutes(&node) routes, err = GetNodeRoutes(tx, &node)
if err != nil { if err != nil {
return err return nil, err
} }
} }
node.Routes = routes node.Routes = routes
stateUpdate := types.StateUpdate{ // If update is empty, it means that one was not created
Type: types.StatePeerChanged, // by failover (as a failover was not necessary), create
ChangeNodes: types.Nodes{&node}, // one and return to the caller.
Message: "called from db.DisableRoute", if update == nil {
} update = &types.StateUpdate{
if stateUpdate.Valid() { Type: types.StatePeerChanged,
hsdb.notifier.NotifyAll(stateUpdate) ChangeNodes: types.Nodes{
&node,
},
Message: "called from db.DisableRoute",
}
} }
return nil return update, nil
} }
func (hsdb *HSDatabase) DeleteRoute(id uint64) error { func (hsdb *HSDatabase) DeleteRoute(
hsdb.mu.Lock() id uint64,
defer hsdb.mu.Unlock() isConnected map[key.MachinePublic]bool,
) (*types.StateUpdate, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
return DeleteRoute(tx, id, isConnected)
})
}
route, err := hsdb.getRoute(id) func DeleteRoute(
tx *gorm.DB,
id uint64,
isConnected map[key.MachinePublic]bool,
) (*types.StateUpdate, error) {
route, err := GetRoute(tx, id)
if err != nil { if err != nil {
return err return nil, err
} }
var routes types.Routes var routes types.Routes
@ -232,19 +218,20 @@ func (hsdb *HSDatabase) DeleteRoute(id uint64) error {
// Tailscale requires both IPv4 and IPv6 exit routes to // Tailscale requires both IPv4 and IPv6 exit routes to
// be enabled at the same time, as per // be enabled at the same time, as per
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
var update *types.StateUpdate
if !route.IsExitRoute() { if !route.IsExitRoute() {
err := hsdb.failoverRouteWithNotify(route) update, err = failoverRouteReturnUpdate(tx, isConnected, route)
if err != nil { if err != nil {
return nil return nil, nil
} }
if err := hsdb.db.Unscoped().Delete(&route).Error; err != nil { if err := tx.Unscoped().Delete(&route).Error; err != nil {
return err return nil, err
} }
} else { } else {
routes, err := hsdb.getNodeRoutes(&node) routes, err := GetNodeRoutes(tx, &node)
if err != nil { if err != nil {
return err return nil, err
} }
routesToDelete := types.Routes{} routesToDelete := types.Routes{}
@ -254,56 +241,59 @@ func (hsdb *HSDatabase) DeleteRoute(id uint64) error {
} }
} }
if err := hsdb.db.Unscoped().Delete(&routesToDelete).Error; err != nil { if err := tx.Unscoped().Delete(&routesToDelete).Error; err != nil {
return err return nil, err
} }
} }
// If update is empty, it means that one was not created
// by failover (as a failover was not necessary), create
// one and return to the caller.
if routes == nil { if routes == nil {
routes, err = hsdb.getNodeRoutes(&node) routes, err = GetNodeRoutes(tx, &node)
if err != nil { if err != nil {
return err return nil, err
} }
} }
node.Routes = routes node.Routes = routes
stateUpdate := types.StateUpdate{ if update == nil {
Type: types.StatePeerChanged, update = &types.StateUpdate{
ChangeNodes: types.Nodes{&node}, Type: types.StatePeerChanged,
Message: "called from db.DeleteRoute", ChangeNodes: types.Nodes{
} &node,
if stateUpdate.Valid() { },
hsdb.notifier.NotifyAll(stateUpdate) Message: "called from db.DeleteRoute",
}
} }
return nil return update, nil
} }
func (hsdb *HSDatabase) deleteNodeRoutes(node *types.Node) error { func deleteNodeRoutes(tx *gorm.DB, node *types.Node, isConnected map[key.MachinePublic]bool) error {
routes, err := hsdb.getNodeRoutes(node) routes, err := GetNodeRoutes(tx, node)
if err != nil { if err != nil {
return err return err
} }
for i := range routes { for i := range routes {
if err := hsdb.db.Unscoped().Delete(&routes[i]).Error; err != nil { if err := tx.Unscoped().Delete(&routes[i]).Error; err != nil {
return err return err
} }
// TODO(kradalby): This is a bit too aggressive, we could probably // TODO(kradalby): This is a bit too aggressive, we could probably
// figure out which routes needs to be failed over rather than all. // figure out which routes needs to be failed over rather than all.
hsdb.failoverRouteWithNotify(&routes[i]) failoverRouteReturnUpdate(tx, isConnected, &routes[i])
} }
return nil return nil
} }
// isUniquePrefix returns if there is another node providing the same route already. // isUniquePrefix returns if there is another node providing the same route already.
func (hsdb *HSDatabase) isUniquePrefix(route types.Route) bool { func isUniquePrefix(tx *gorm.DB, route types.Route) bool {
var count int64 var count int64
hsdb.db. tx.Model(&types.Route{}).
Model(&types.Route{}).
Where("prefix = ? AND node_id != ? AND advertised = ? AND enabled = ?", Where("prefix = ? AND node_id != ? AND advertised = ? AND enabled = ?",
route.Prefix, route.Prefix,
route.NodeID, route.NodeID,
@ -312,9 +302,9 @@ func (hsdb *HSDatabase) isUniquePrefix(route types.Route) bool {
return count == 0 return count == 0
} }
func (hsdb *HSDatabase) getPrimaryRoute(prefix netip.Prefix) (*types.Route, error) { func getPrimaryRoute(tx *gorm.DB, prefix netip.Prefix) (*types.Route, error) {
var route types.Route var route types.Route
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Where("prefix = ? AND advertised = ? AND enabled = ? AND is_primary = ?", types.IPPrefix(prefix), true, true, true). Where("prefix = ? AND advertised = ? AND enabled = ? AND is_primary = ?", types.IPPrefix(prefix), true, true, true).
First(&route).Error First(&route).Error
@ -329,14 +319,17 @@ func (hsdb *HSDatabase) getPrimaryRoute(prefix netip.Prefix) (*types.Route, erro
return &route, nil return &route, nil
} }
func (hsdb *HSDatabase) GetNodePrimaryRoutes(node *types.Node) (types.Routes, error) {
return Read(hsdb.DB, func(rx *gorm.DB) (types.Routes, error) {
return GetNodePrimaryRoutes(rx, node)
})
}
// getNodePrimaryRoutes returns the routes that are enabled and marked as primary (for subnet failover) // getNodePrimaryRoutes returns the routes that are enabled and marked as primary (for subnet failover)
// Exit nodes are not considered for this, as they are never marked as Primary. // Exit nodes are not considered for this, as they are never marked as Primary.
func (hsdb *HSDatabase) GetNodePrimaryRoutes(node *types.Node) (types.Routes, error) { func GetNodePrimaryRoutes(tx *gorm.DB, node *types.Node) (types.Routes, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
var routes types.Routes var routes types.Routes
err := hsdb.db. err := tx.
Preload("Node"). Preload("Node").
Where("node_id = ? AND advertised = ? AND enabled = ? AND is_primary = ?", node.ID, true, true, true). Where("node_id = ? AND advertised = ? AND enabled = ? AND is_primary = ?", node.ID, true, true, true).
Find(&routes).Error Find(&routes).Error
@ -347,22 +340,21 @@ func (hsdb *HSDatabase) GetNodePrimaryRoutes(node *types.Node) (types.Routes, er
return routes, nil return routes, nil
} }
func (hsdb *HSDatabase) SaveNodeRoutes(node *types.Node) (bool, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (bool, error) {
return SaveNodeRoutes(tx, node)
})
}
// SaveNodeRoutes takes a node and updates the database with // SaveNodeRoutes takes a node and updates the database with
// the new routes. // the new routes.
// It returns a bool whether an update should be sent as the // It returns a bool whether an update should be sent as the
// saved route impacts nodes. // saved route impacts nodes.
func (hsdb *HSDatabase) SaveNodeRoutes(node *types.Node) (bool, error) { func SaveNodeRoutes(tx *gorm.DB, node *types.Node) (bool, error) {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
return hsdb.saveNodeRoutes(node)
}
func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) (bool, error) {
sendUpdate := false sendUpdate := false
currentRoutes := types.Routes{} currentRoutes := types.Routes{}
err := hsdb.db.Where("node_id = ?", node.ID).Find(&currentRoutes).Error err := tx.Where("node_id = ?", node.ID).Find(&currentRoutes).Error
if err != nil { if err != nil {
return sendUpdate, err return sendUpdate, err
} }
@ -382,7 +374,7 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) (bool, error) {
if _, ok := advertisedRoutes[netip.Prefix(route.Prefix)]; ok { if _, ok := advertisedRoutes[netip.Prefix(route.Prefix)]; ok {
if !route.Advertised { if !route.Advertised {
currentRoutes[pos].Advertised = true currentRoutes[pos].Advertised = true
err := hsdb.db.Save(&currentRoutes[pos]).Error err := tx.Save(&currentRoutes[pos]).Error
if err != nil { if err != nil {
return sendUpdate, err return sendUpdate, err
} }
@ -398,7 +390,7 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) (bool, error) {
} else if route.Advertised { } else if route.Advertised {
currentRoutes[pos].Advertised = false currentRoutes[pos].Advertised = false
currentRoutes[pos].Enabled = false currentRoutes[pos].Enabled = false
err := hsdb.db.Save(&currentRoutes[pos]).Error err := tx.Save(&currentRoutes[pos]).Error
if err != nil { if err != nil {
return sendUpdate, err return sendUpdate, err
} }
@ -413,7 +405,7 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) (bool, error) {
Advertised: true, Advertised: true,
Enabled: false, Enabled: false,
} }
err := hsdb.db.Create(&route).Error err := tx.Create(&route).Error
if err != nil { if err != nil {
return sendUpdate, err return sendUpdate, err
} }
@ -425,127 +417,89 @@ func (hsdb *HSDatabase) saveNodeRoutes(node *types.Node) (bool, error) {
// EnsureFailoverRouteIsAvailable takes a node and checks if the node's route // EnsureFailoverRouteIsAvailable takes a node and checks if the node's route
// currently have a functioning host that exposes the network. // currently have a functioning host that exposes the network.
func (hsdb *HSDatabase) EnsureFailoverRouteIsAvailable(node *types.Node) error { func EnsureFailoverRouteIsAvailable(
nodeRoutes, err := hsdb.getNodeRoutes(node) tx *gorm.DB,
isConnected map[key.MachinePublic]bool,
node *types.Node,
) (*types.StateUpdate, error) {
nodeRoutes, err := GetNodeRoutes(tx, node)
if err != nil { if err != nil {
return nil return nil, nil
} }
var changedNodes types.Nodes
for _, nodeRoute := range nodeRoutes { for _, nodeRoute := range nodeRoutes {
routes, err := hsdb.getRoutesByPrefix(netip.Prefix(nodeRoute.Prefix)) routes, err := getRoutesByPrefix(tx, netip.Prefix(nodeRoute.Prefix))
if err != nil { if err != nil {
return err return nil, err
} }
for _, route := range routes { for _, route := range routes {
if route.IsPrimary { if route.IsPrimary {
// if we have a primary route, and the node is connected // if we have a primary route, and the node is connected
// nothing needs to be done. // nothing needs to be done.
if hsdb.notifier.IsConnected(route.Node.MachineKey) { if isConnected[route.Node.MachineKey] {
continue continue
} }
// if not, we need to failover the route // if not, we need to failover the route
err := hsdb.failoverRouteWithNotify(&route) update, err := failoverRouteReturnUpdate(tx, isConnected, &route)
if err != nil { if err != nil {
return err return nil, err
}
if update != nil {
changedNodes = append(changedNodes, update.ChangeNodes...)
} }
} }
} }
} }
return nil if len(changedNodes) != 0 {
} return &types.StateUpdate{
func (hsdb *HSDatabase) FailoverNodeRoutesWithNotify(node *types.Node) error {
routes, err := hsdb.getNodeRoutes(node)
if err != nil {
return nil
}
var changedKeys []key.MachinePublic
for _, route := range routes {
changed, err := hsdb.failoverRoute(&route)
if err != nil {
return err
}
changedKeys = append(changedKeys, changed...)
}
changedKeys = lo.Uniq(changedKeys)
var nodes types.Nodes
for _, key := range changedKeys {
node, err := hsdb.GetNodeByMachineKey(key)
if err != nil {
return err
}
nodes = append(nodes, node)
}
if nodes != nil {
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged, Type: types.StatePeerChanged,
ChangeNodes: nodes, ChangeNodes: changedNodes,
Message: "called from db.FailoverNodeRoutesWithNotify", Message: "called from db.EnsureFailoverRouteIsAvailable",
} }, nil
if stateUpdate.Valid() {
hsdb.notifier.NotifyAll(stateUpdate)
}
} }
return nil return nil, nil
} }
func (hsdb *HSDatabase) failoverRouteWithNotify(r *types.Route) error { func failoverRouteReturnUpdate(
changedKeys, err := hsdb.failoverRoute(r) tx *gorm.DB,
isConnected map[key.MachinePublic]bool,
r *types.Route,
) (*types.StateUpdate, error) {
changedKeys, err := failoverRoute(tx, isConnected, r)
if err != nil { if err != nil {
return err return nil, err
} }
log.Trace().
Interface("isConnected", isConnected).
Interface("changedKeys", changedKeys).
Msg("building route failover")
if len(changedKeys) == 0 { if len(changedKeys) == 0 {
return nil return nil, nil
} }
var nodes types.Nodes var nodes types.Nodes
log.Trace().
Str("hostname", r.Node.Hostname).
Msg("loading machines with new primary routes from db")
for _, key := range changedKeys { for _, key := range changedKeys {
node, err := hsdb.getNodeByMachineKey(key) node, err := GetNodeByMachineKey(tx, key)
if err != nil { if err != nil {
return err return nil, err
} }
nodes = append(nodes, node) nodes = append(nodes, node)
} }
log.Trace(). return &types.StateUpdate{
Str("hostname", r.Node.Hostname). Type: types.StatePeerChanged,
Msg("notifying peers about primary route change") ChangeNodes: nodes,
Message: "called from db.failoverRouteReturnUpdate",
if nodes != nil { }, nil
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: nodes,
Message: "called from db.failoverRouteWithNotify",
}
if stateUpdate.Valid() {
hsdb.notifier.NotifyAll(stateUpdate)
}
}
log.Trace().
Str("hostname", r.Node.Hostname).
Msg("notified peers about primary route change")
return nil
} }
// failoverRoute takes a route that is no longer available, // failoverRoute takes a route that is no longer available,
@ -556,12 +510,16 @@ func (hsdb *HSDatabase) failoverRouteWithNotify(r *types.Route) error {
// //
// and tries to find a new route to take over its place. // and tries to find a new route to take over its place.
// If the given route was not primary, it returns early. // If the given route was not primary, it returns early.
func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, error) { func failoverRoute(
tx *gorm.DB,
isConnected map[key.MachinePublic]bool,
r *types.Route,
) ([]key.MachinePublic, error) {
if r == nil { if r == nil {
return nil, nil return nil, nil
} }
// This route is not a primary route, and it isnt // This route is not a primary route, and it is not
// being served to nodes. // being served to nodes.
if !r.IsPrimary { if !r.IsPrimary {
return nil, nil return nil, nil
@ -572,7 +530,7 @@ func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, erro
return nil, nil return nil, nil
} }
routes, err := hsdb.getRoutesByPrefix(netip.Prefix(r.Prefix)) routes, err := getRoutesByPrefix(tx, netip.Prefix(r.Prefix))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -589,14 +547,14 @@ func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, erro
continue continue
} }
if hsdb.notifier.IsConnected(route.Node.MachineKey) { if isConnected[route.Node.MachineKey] {
newPrimary = &routes[idx] newPrimary = &routes[idx]
break break
} }
} }
// If a new route was not found/available, // If a new route was not found/available,
// return with an error. // return without an error.
// We do not want to update the database as // We do not want to update the database as
// the one currently marked as primary is the // the one currently marked as primary is the
// best we got. // best we got.
@ -610,7 +568,7 @@ func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, erro
// Remove primary from the old route // Remove primary from the old route
r.IsPrimary = false r.IsPrimary = false
err = hsdb.db.Save(&r).Error err = tx.Save(&r).Error
if err != nil { if err != nil {
log.Error().Err(err).Msg("error disabling new primary route") log.Error().Err(err).Msg("error disabling new primary route")
@ -623,7 +581,7 @@ func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, erro
// Set primary for the new primary // Set primary for the new primary
newPrimary.IsPrimary = true newPrimary.IsPrimary = true
err = hsdb.db.Save(&newPrimary).Error err = tx.Save(&newPrimary).Error
if err != nil { if err != nil {
log.Error().Err(err).Msg("error enabling new primary route") log.Error().Err(err).Msg("error enabling new primary route")
@ -638,25 +596,26 @@ func (hsdb *HSDatabase) failoverRoute(r *types.Route) ([]key.MachinePublic, erro
return []key.MachinePublic{r.Node.MachineKey, newPrimary.Node.MachineKey}, nil return []key.MachinePublic{r.Node.MachineKey, newPrimary.Node.MachineKey}, nil
} }
// EnableAutoApprovedRoutes enables any routes advertised by a node that match the ACL autoApprovers policy.
func (hsdb *HSDatabase) EnableAutoApprovedRoutes( func (hsdb *HSDatabase) EnableAutoApprovedRoutes(
aclPolicy *policy.ACLPolicy, aclPolicy *policy.ACLPolicy,
node *types.Node, node *types.Node,
) error { ) (*types.StateUpdate, error) {
if len(aclPolicy.AutoApprovers.ExitNode) == 0 && len(aclPolicy.AutoApprovers.Routes) == 0 { return Write(hsdb.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
// No autoapprovers configured return EnableAutoApprovedRoutes(tx, aclPolicy, node)
return nil })
} }
// EnableAutoApprovedRoutes enables any routes advertised by a node that match the ACL autoApprovers policy.
func EnableAutoApprovedRoutes(
tx *gorm.DB,
aclPolicy *policy.ACLPolicy,
node *types.Node,
) (*types.StateUpdate, error) {
if len(node.IPAddresses) == 0 { if len(node.IPAddresses) == 0 {
// This node has no IPAddresses, so can't possibly match any autoApprovers ACLs return nil, nil // This node has no IPAddresses, so can't possibly match any autoApprovers ACLs
return nil
} }
hsdb.mu.Lock() routes, err := GetNodeAdvertisedRoutes(tx, node)
defer hsdb.mu.Unlock()
routes, err := hsdb.getNodeAdvertisedRoutes(node)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
log.Error(). log.Error().
Caller(). Caller().
@ -664,7 +623,7 @@ func (hsdb *HSDatabase) EnableAutoApprovedRoutes(
Str("node", node.Hostname). Str("node", node.Hostname).
Msg("Could not get advertised routes for node") Msg("Could not get advertised routes for node")
return err return nil, err
} }
log.Trace().Interface("routes", routes).Msg("routes for autoapproving") log.Trace().Interface("routes", routes).Msg("routes for autoapproving")
@ -685,7 +644,7 @@ func (hsdb *HSDatabase) EnableAutoApprovedRoutes(
Uint64("nodeId", node.ID). Uint64("nodeId", node.ID).
Msg("Failed to resolve autoApprovers for advertised route") Msg("Failed to resolve autoApprovers for advertised route")
return err return nil, err
} }
log.Trace(). log.Trace().
@ -706,7 +665,7 @@ func (hsdb *HSDatabase) EnableAutoApprovedRoutes(
Str("alias", approvedAlias). Str("alias", approvedAlias).
Msg("Failed to expand alias when processing autoApprovers policy") Msg("Failed to expand alias when processing autoApprovers policy")
return err return nil, err
} }
// approvedIPs should contain all of node's IPs if it matches the rule, so check for first // approvedIPs should contain all of node's IPs if it matches the rule, so check for first
@ -717,17 +676,25 @@ func (hsdb *HSDatabase) EnableAutoApprovedRoutes(
} }
} }
update := &types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{},
Message: "created in db.EnableAutoApprovedRoutes",
}
for _, approvedRoute := range approvedRoutes { for _, approvedRoute := range approvedRoutes {
err := hsdb.enableRoute(uint64(approvedRoute.ID)) perHostUpdate, err := EnableRoute(tx, uint64(approvedRoute.ID))
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Str("approvedRoute", approvedRoute.String()). Str("approvedRoute", approvedRoute.String()).
Uint64("nodeId", node.ID). Uint64("nodeId", node.ID).
Msg("Failed to enable approved route") Msg("Failed to enable approved route")
return err return nil, err
} }
update.ChangeNodes = append(update.ChangeNodes, perHostUpdate.ChangeNodes...)
} }
return nil return update, nil
} }

View file

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/juanfont/headscale/hscontrol/notifier"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -24,7 +23,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "test_get_route_node") _, err = db.getNode("test", "test_get_route_node")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
route, err := netip.ParsePrefix("10.0.0.0/24") route, err := netip.ParsePrefix("10.0.0.0/24")
@ -42,7 +41,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
Hostinfo: &hostInfo, Hostinfo: &hostInfo,
} }
db.db.Save(&node) db.DB.Save(&node)
su, err := db.SaveNodeRoutes(&node) su, err := db.SaveNodeRoutes(&node)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -52,10 +51,11 @@ func (s *Suite) TestGetRoutes(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(len(advertisedRoutes), check.Equals, 1) c.Assert(len(advertisedRoutes), check.Equals, 1)
err = db.enableRoutes(&node, "192.168.0.0/24") // TODO(kradalby): check state update
_, err = db.enableRoutes(&node, "192.168.0.0/24")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
err = db.enableRoutes(&node, "10.0.0.0/24") _, err = db.enableRoutes(&node, "10.0.0.0/24")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }
@ -66,7 +66,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "test_enable_route_node") _, err = db.getNode("test", "test_enable_route_node")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
route, err := netip.ParsePrefix( route, err := netip.ParsePrefix(
@ -91,7 +91,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
Hostinfo: &hostInfo, Hostinfo: &hostInfo,
} }
db.db.Save(&node) db.DB.Save(&node)
sendUpdate, err := db.SaveNodeRoutes(&node) sendUpdate, err := db.SaveNodeRoutes(&node)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@ -106,10 +106,10 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(len(noEnabledRoutes), check.Equals, 0) c.Assert(len(noEnabledRoutes), check.Equals, 0)
err = db.enableRoutes(&node, "192.168.0.0/24") _, err = db.enableRoutes(&node, "192.168.0.0/24")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
err = db.enableRoutes(&node, "10.0.0.0/24") _, err = db.enableRoutes(&node, "10.0.0.0/24")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
enabledRoutes, err := db.GetEnabledRoutes(&node) enabledRoutes, err := db.GetEnabledRoutes(&node)
@ -117,14 +117,14 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
c.Assert(len(enabledRoutes), check.Equals, 1) c.Assert(len(enabledRoutes), check.Equals, 1)
// Adding it twice will just let it pass through // Adding it twice will just let it pass through
err = db.enableRoutes(&node, "10.0.0.0/24") _, err = db.enableRoutes(&node, "10.0.0.0/24")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
enableRoutesAfterDoubleApply, err := db.GetEnabledRoutes(&node) enableRoutesAfterDoubleApply, err := db.GetEnabledRoutes(&node)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(len(enableRoutesAfterDoubleApply), check.Equals, 1) c.Assert(len(enableRoutesAfterDoubleApply), check.Equals, 1)
err = db.enableRoutes(&node, "150.0.10.0/25") _, err = db.enableRoutes(&node, "150.0.10.0/25")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
enabledRoutesWithAdditionalRoute, err := db.GetEnabledRoutes(&node) enabledRoutesWithAdditionalRoute, err := db.GetEnabledRoutes(&node)
@ -139,7 +139,7 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "test_enable_route_node") _, err = db.getNode("test", "test_enable_route_node")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
route, err := netip.ParsePrefix( route, err := netip.ParsePrefix(
@ -163,16 +163,16 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
Hostinfo: &hostInfo1, Hostinfo: &hostInfo1,
} }
db.db.Save(&node1) db.DB.Save(&node1)
sendUpdate, err := db.SaveNodeRoutes(&node1) sendUpdate, err := db.SaveNodeRoutes(&node1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(sendUpdate, check.Equals, false) c.Assert(sendUpdate, check.Equals, false)
err = db.enableRoutes(&node1, route.String()) _, err = db.enableRoutes(&node1, route.String())
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
err = db.enableRoutes(&node1, route2.String()) _, err = db.enableRoutes(&node1, route2.String())
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
hostInfo2 := tailcfg.Hostinfo{ hostInfo2 := tailcfg.Hostinfo{
@ -186,13 +186,13 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
Hostinfo: &hostInfo2, Hostinfo: &hostInfo2,
} }
db.db.Save(&node2) db.DB.Save(&node2)
sendUpdate, err = db.SaveNodeRoutes(&node2) sendUpdate, err = db.SaveNodeRoutes(&node2)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(sendUpdate, check.Equals, false) c.Assert(sendUpdate, check.Equals, false)
err = db.enableRoutes(&node2, route2.String()) _, err = db.enableRoutes(&node2, route2.String())
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
enabledRoutes1, err := db.GetEnabledRoutes(&node1) enabledRoutes1, err := db.GetEnabledRoutes(&node1)
@ -219,7 +219,7 @@ func (s *Suite) TestDeleteRoutes(c *check.C) {
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
_, err = db.GetNode("test", "test_enable_route_node") _, err = db.getNode("test", "test_enable_route_node")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
prefix, err := netip.ParsePrefix( prefix, err := netip.ParsePrefix(
@ -246,22 +246,23 @@ func (s *Suite) TestDeleteRoutes(c *check.C) {
Hostinfo: &hostInfo1, Hostinfo: &hostInfo1,
LastSeen: &now, LastSeen: &now,
} }
db.db.Save(&node1) db.DB.Save(&node1)
sendUpdate, err := db.SaveNodeRoutes(&node1) sendUpdate, err := db.SaveNodeRoutes(&node1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(sendUpdate, check.Equals, false) c.Assert(sendUpdate, check.Equals, false)
err = db.enableRoutes(&node1, prefix.String()) _, err = db.enableRoutes(&node1, prefix.String())
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
err = db.enableRoutes(&node1, prefix2.String()) _, err = db.enableRoutes(&node1, prefix2.String())
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
routes, err := db.GetNodeRoutes(&node1) routes, err := db.GetNodeRoutes(&node1)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
err = db.DeleteRoute(uint64(routes[0].ID)) // TODO(kradalby): check stateupdate
_, err = db.DeleteRoute(uint64(routes[0].ID), map[key.MachinePublic]bool{})
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
enabledRoutes1, err := db.GetEnabledRoutes(&node1) enabledRoutes1, err := db.GetEnabledRoutes(&node1)
@ -269,17 +270,9 @@ func (s *Suite) TestDeleteRoutes(c *check.C) {
c.Assert(len(enabledRoutes1), check.Equals, 1) c.Assert(len(enabledRoutes1), check.Equals, 1)
} }
var ipp = func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) }
func TestFailoverRoute(t *testing.T) { func TestFailoverRoute(t *testing.T) {
ipp := func(s string) types.IPPrefix { return types.IPPrefix(netip.MustParsePrefix(s)) }
// TODO(kradalby): Count/verify updates
var sink chan types.StateUpdate
go func() {
for range sink {
}
}()
machineKeys := []key.MachinePublic{ machineKeys := []key.MachinePublic{
key.NewMachine().Public(), key.NewMachine().Public(),
key.NewMachine().Public(), key.NewMachine().Public(),
@ -291,6 +284,7 @@ func TestFailoverRoute(t *testing.T) {
name string name string
failingRoute types.Route failingRoute types.Route
routes types.Routes routes types.Routes
isConnected map[key.MachinePublic]bool
want []key.MachinePublic want []key.MachinePublic
wantErr bool wantErr bool
}{ }{
@ -397,6 +391,10 @@ func TestFailoverRoute(t *testing.T) {
Enabled: true, Enabled: true,
}, },
}, },
isConnected: map[key.MachinePublic]bool{
machineKeys[0]: false,
machineKeys[1]: true,
},
want: []key.MachinePublic{ want: []key.MachinePublic{
machineKeys[0], machineKeys[0],
machineKeys[1], machineKeys[1],
@ -491,6 +489,11 @@ func TestFailoverRoute(t *testing.T) {
Enabled: true, Enabled: true,
}, },
}, },
isConnected: map[key.MachinePublic]bool{
machineKeys[0]: true,
machineKeys[1]: true,
machineKeys[2]: true,
},
want: []key.MachinePublic{ want: []key.MachinePublic{
machineKeys[1], machineKeys[1],
machineKeys[0], machineKeys[0],
@ -535,6 +538,10 @@ func TestFailoverRoute(t *testing.T) {
Enabled: true, Enabled: true,
}, },
}, },
isConnected: map[key.MachinePublic]bool{
machineKeys[0]: true,
machineKeys[3]: false,
},
want: nil, want: nil,
wantErr: false, wantErr: false,
}, },
@ -587,6 +594,11 @@ func TestFailoverRoute(t *testing.T) {
Enabled: true, Enabled: true,
}, },
}, },
isConnected: map[key.MachinePublic]bool{
machineKeys[0]: false,
machineKeys[1]: true,
machineKeys[3]: false,
},
want: []key.MachinePublic{ want: []key.MachinePublic{
machineKeys[0], machineKeys[0],
machineKeys[1], machineKeys[1],
@ -641,13 +653,10 @@ func TestFailoverRoute(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "failover-db-test") tmpDir, err := os.MkdirTemp("", "failover-db-test")
assert.NoError(t, err) assert.NoError(t, err)
notif := notifier.NewNotifier()
db, err = NewHeadscaleDatabase( db, err = NewHeadscaleDatabase(
"sqlite3", "sqlite3",
tmpDir+"/headscale_test.db", tmpDir+"/headscale_test.db",
false, false,
notif,
[]netip.Prefix{ []netip.Prefix{
netip.MustParsePrefix("10.27.0.0/23"), netip.MustParsePrefix("10.27.0.0/23"),
}, },
@ -655,23 +664,15 @@ func TestFailoverRoute(t *testing.T) {
) )
assert.NoError(t, err) assert.NoError(t, err)
// Pretend that all the nodes are connected to control
for idx, key := range machineKeys {
// Pretend one node is offline
if idx == 3 {
continue
}
notif.AddNode(key, sink)
}
for _, route := range tt.routes { for _, route := range tt.routes {
if err := db.db.Save(&route).Error; err != nil { if err := db.DB.Save(&route).Error; err != nil {
t.Fatalf("failed to create route: %s", err) t.Fatalf("failed to create route: %s", err)
} }
} }
got, err := db.failoverRoute(&tt.failingRoute) got, err := Write(db.DB, func(tx *gorm.DB) ([]key.MachinePublic, error) {
return failoverRoute(tx, tt.isConnected, &tt.failingRoute)
})
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("failoverRoute() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("failoverRoute() error = %v, wantErr %v", err, tt.wantErr)
@ -685,3 +686,231 @@ func TestFailoverRoute(t *testing.T) {
}) })
} }
} }
// func TestDisableRouteFailover(t *testing.T) {
// machineKeys := []key.MachinePublic{
// key.NewMachine().Public(),
// key.NewMachine().Public(),
// key.NewMachine().Public(),
// key.NewMachine().Public(),
// }
// tests := []struct {
// name string
// nodes types.Nodes
// routeID uint64
// isConnected map[key.MachinePublic]bool
// wantMachineKey key.MachinePublic
// wantErr string
// }{
// {
// name: "single-route",
// nodes: types.Nodes{
// &types.Node{
// ID: 0,
// MachineKey: machineKeys[0],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 1,
// },
// Prefix: ipp("10.0.0.0/24"),
// Node: types.Node{
// MachineKey: machineKeys[0],
// },
// IsPrimary: true,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// },
// routeID: 1,
// wantMachineKey: machineKeys[0],
// },
// {
// name: "failover-simple",
// nodes: types.Nodes{
// &types.Node{
// ID: 0,
// MachineKey: machineKeys[0],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 1,
// },
// Prefix: ipp("10.0.0.0/24"),
// IsPrimary: true,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// &types.Node{
// ID: 1,
// MachineKey: machineKeys[1],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 2,
// },
// Prefix: ipp("10.0.0.0/24"),
// IsPrimary: false,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// },
// routeID: 1,
// wantMachineKey: machineKeys[1],
// },
// {
// name: "no-failover-offline",
// nodes: types.Nodes{
// &types.Node{
// ID: 0,
// MachineKey: machineKeys[0],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 1,
// },
// Prefix: ipp("10.0.0.0/24"),
// IsPrimary: true,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// &types.Node{
// ID: 1,
// MachineKey: machineKeys[1],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 2,
// },
// Prefix: ipp("10.0.0.0/24"),
// IsPrimary: false,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// },
// isConnected: map[key.MachinePublic]bool{
// machineKeys[0]: true,
// machineKeys[1]: false,
// },
// routeID: 1,
// wantMachineKey: machineKeys[1],
// },
// {
// name: "failover-to-online",
// nodes: types.Nodes{
// &types.Node{
// ID: 0,
// MachineKey: machineKeys[0],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 1,
// },
// Prefix: ipp("10.0.0.0/24"),
// IsPrimary: true,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// &types.Node{
// ID: 1,
// MachineKey: machineKeys[1],
// Routes: []types.Route{
// {
// Model: gorm.Model{
// ID: 2,
// },
// Prefix: ipp("10.0.0.0/24"),
// IsPrimary: false,
// },
// },
// Hostinfo: &tailcfg.Hostinfo{
// RoutableIPs: []netip.Prefix{
// netip.MustParsePrefix("10.0.0.0/24"),
// },
// },
// },
// },
// isConnected: map[key.MachinePublic]bool{
// machineKeys[0]: true,
// machineKeys[1]: true,
// },
// routeID: 1,
// wantMachineKey: machineKeys[1],
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// datab, err := NewHeadscaleDatabase("sqlite3", ":memory:", false, []netip.Prefix{}, "")
// assert.NoError(t, err)
// // bootstrap db
// datab.DB.Transaction(func(tx *gorm.DB) error {
// for _, node := range tt.nodes {
// err := tx.Save(node).Error
// if err != nil {
// return err
// }
// _, err = SaveNodeRoutes(tx, node)
// if err != nil {
// return err
// }
// }
// return nil
// })
// got, err := Write(datab.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
// return DisableRoute(tx, tt.routeID, tt.isConnected)
// })
// // if (err.Error() != "") != tt.wantErr {
// // t.Errorf("failoverRoute() error = %v, wantErr %v", err, tt.wantErr)
// // return
// // }
// if len(got.ChangeNodes) != 1 {
// t.Errorf("expected update with one machine, got %d", len(got.ChangeNodes))
// }
// if diff := cmp.Diff(tt.wantMachineKey, got.ChangeNodes[0].MachineKey, util.Comparers...); diff != "" {
// t.Errorf("DisableRoute() unexpected result (-want +got):\n%s", diff)
// }
// })
// }
// }

View file

@ -6,7 +6,6 @@ import (
"os" "os"
"testing" "testing"
"github.com/juanfont/headscale/hscontrol/notifier"
"gopkg.in/check.v1" "gopkg.in/check.v1"
) )
@ -48,7 +47,6 @@ func (s *Suite) ResetDB(c *check.C) {
"sqlite3", "sqlite3",
tmpDir+"/headscale_test.db", tmpDir+"/headscale_test.db",
false, false,
notifier.NewNotifier(),
[]netip.Prefix{ []netip.Prefix{
netip.MustParsePrefix("10.27.0.0/23"), netip.MustParsePrefix("10.27.0.0/23"),
}, },

View file

@ -15,22 +15,25 @@ var (
ErrUserStillHasNodes = errors.New("user not empty: node(s) found") ErrUserStillHasNodes = errors.New("user not empty: node(s) found")
) )
func (hsdb *HSDatabase) CreateUser(name string) (*types.User, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.User, error) {
return CreateUser(tx, name)
})
}
// CreateUser creates a new User. Returns error if could not be created // CreateUser creates a new User. Returns error if could not be created
// or another user already exists. // or another user already exists.
func (hsdb *HSDatabase) CreateUser(name string) (*types.User, error) { func CreateUser(tx *gorm.DB, name string) (*types.User, error) {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
err := util.CheckForFQDNRules(name) err := util.CheckForFQDNRules(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
user := types.User{} user := types.User{}
if err := hsdb.db.Where("name = ?", name).First(&user).Error; err == nil { if err := tx.Where("name = ?", name).First(&user).Error; err == nil {
return nil, ErrUserExists return nil, ErrUserExists
} }
user.Name = name user.Name = name
if err := hsdb.db.Create(&user).Error; err != nil { if err := tx.Create(&user).Error; err != nil {
log.Error(). log.Error().
Str("func", "CreateUser"). Str("func", "CreateUser").
Err(err). Err(err).
@ -42,18 +45,21 @@ func (hsdb *HSDatabase) CreateUser(name string) (*types.User, error) {
return &user, nil return &user, nil
} }
func (hsdb *HSDatabase) DestroyUser(name string) error {
return hsdb.Write(func(tx *gorm.DB) error {
return DestroyUser(tx, name)
})
}
// DestroyUser destroys a User. Returns error if the User does // DestroyUser destroys a User. Returns error if the User does
// not exist or if there are nodes associated with it. // not exist or if there are nodes associated with it.
func (hsdb *HSDatabase) DestroyUser(name string) error { func DestroyUser(tx *gorm.DB, name string) error {
hsdb.mu.Lock() user, err := GetUser(tx, name)
defer hsdb.mu.Unlock()
user, err := hsdb.getUser(name)
if err != nil { if err != nil {
return ErrUserNotFound return ErrUserNotFound
} }
nodes, err := hsdb.listNodesByUser(name) nodes, err := ListNodesByUser(tx, name)
if err != nil { if err != nil {
return err return err
} }
@ -61,32 +67,35 @@ func (hsdb *HSDatabase) DestroyUser(name string) error {
return ErrUserStillHasNodes return ErrUserStillHasNodes
} }
keys, err := hsdb.listPreAuthKeys(name) keys, err := ListPreAuthKeys(tx, name)
if err != nil { if err != nil {
return err return err
} }
for _, key := range keys { for _, key := range keys {
err = hsdb.destroyPreAuthKey(key) err = DestroyPreAuthKey(tx, key)
if err != nil { if err != nil {
return err return err
} }
} }
if result := hsdb.db.Unscoped().Delete(&user); result.Error != nil { if result := tx.Unscoped().Delete(&user); result.Error != nil {
return result.Error return result.Error
} }
return nil return nil
} }
func (hsdb *HSDatabase) RenameUser(oldName, newName string) error {
return hsdb.Write(func(tx *gorm.DB) error {
return RenameUser(tx, oldName, newName)
})
}
// RenameUser renames a User. Returns error if the User does // RenameUser renames a User. Returns error if the User does
// not exist or if another User exists with the new name. // not exist or if another User exists with the new name.
func (hsdb *HSDatabase) RenameUser(oldName, newName string) error { func RenameUser(tx *gorm.DB, oldName, newName string) error {
hsdb.mu.Lock()
defer hsdb.mu.Unlock()
var err error var err error
oldUser, err := hsdb.getUser(oldName) oldUser, err := GetUser(tx, oldName)
if err != nil { if err != nil {
return err return err
} }
@ -94,7 +103,7 @@ func (hsdb *HSDatabase) RenameUser(oldName, newName string) error {
if err != nil { if err != nil {
return err return err
} }
_, err = hsdb.getUser(newName) _, err = GetUser(tx, newName)
if err == nil { if err == nil {
return ErrUserExists return ErrUserExists
} }
@ -104,24 +113,22 @@ func (hsdb *HSDatabase) RenameUser(oldName, newName string) error {
oldUser.Name = newName oldUser.Name = newName
if result := hsdb.db.Save(&oldUser); result.Error != nil { if result := tx.Save(&oldUser); result.Error != nil {
return result.Error return result.Error
} }
return nil return nil
} }
// GetUser fetches a user by name.
func (hsdb *HSDatabase) GetUser(name string) (*types.User, error) { func (hsdb *HSDatabase) GetUser(name string) (*types.User, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) (*types.User, error) {
defer hsdb.mu.RUnlock() return GetUser(rx, name)
})
return hsdb.getUser(name)
} }
func (hsdb *HSDatabase) getUser(name string) (*types.User, error) { func GetUser(tx *gorm.DB, name string) (*types.User, error) {
user := types.User{} user := types.User{}
if result := hsdb.db.First(&user, "name = ?", name); errors.Is( if result := tx.First(&user, "name = ?", name); errors.Is(
result.Error, result.Error,
gorm.ErrRecordNotFound, gorm.ErrRecordNotFound,
) { ) {
@ -131,17 +138,16 @@ func (hsdb *HSDatabase) getUser(name string) (*types.User, error) {
return &user, nil return &user, nil
} }
// ListUsers gets all the existing users.
func (hsdb *HSDatabase) ListUsers() ([]types.User, error) { func (hsdb *HSDatabase) ListUsers() ([]types.User, error) {
hsdb.mu.RLock() return Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
defer hsdb.mu.RUnlock() return ListUsers(rx)
})
return hsdb.listUsers()
} }
func (hsdb *HSDatabase) listUsers() ([]types.User, error) { // ListUsers gets all the existing users.
func ListUsers(tx *gorm.DB) ([]types.User, error) {
users := []types.User{} users := []types.User{}
if err := hsdb.db.Find(&users).Error; err != nil { if err := tx.Find(&users).Error; err != nil {
return nil, err return nil, err
} }
@ -149,46 +155,42 @@ func (hsdb *HSDatabase) listUsers() ([]types.User, error) {
} }
// ListNodesByUser gets all the nodes in a given user. // ListNodesByUser gets all the nodes in a given user.
func (hsdb *HSDatabase) ListNodesByUser(name string) (types.Nodes, error) { func ListNodesByUser(tx *gorm.DB, name string) (types.Nodes, error) {
hsdb.mu.RLock()
defer hsdb.mu.RUnlock()
return hsdb.listNodesByUser(name)
}
func (hsdb *HSDatabase) listNodesByUser(name string) (types.Nodes, error) {
err := util.CheckForFQDNRules(name) err := util.CheckForFQDNRules(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
user, err := hsdb.getUser(name) user, err := GetUser(tx, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nodes := types.Nodes{} nodes := types.Nodes{}
if err := hsdb.db.Preload("AuthKey").Preload("AuthKey.User").Preload("User").Where(&types.Node{UserID: user.ID}).Find(&nodes).Error; err != nil { if err := tx.Preload("AuthKey").Preload("AuthKey.User").Preload("User").Where(&types.Node{UserID: user.ID}).Find(&nodes).Error; err != nil {
return nil, err return nil, err
} }
return nodes, nil return nodes, nil
} }
// AssignNodeToUser assigns a Node to a user.
func (hsdb *HSDatabase) AssignNodeToUser(node *types.Node, username string) error { func (hsdb *HSDatabase) AssignNodeToUser(node *types.Node, username string) error {
hsdb.mu.Lock() return hsdb.Write(func(tx *gorm.DB) error {
defer hsdb.mu.Unlock() return AssignNodeToUser(tx, node, username)
})
}
// AssignNodeToUser assigns a Node to a user.
func AssignNodeToUser(tx *gorm.DB, node *types.Node, username string) error {
err := util.CheckForFQDNRules(username) err := util.CheckForFQDNRules(username)
if err != nil { if err != nil {
return err return err
} }
user, err := hsdb.getUser(username) user, err := GetUser(tx, username)
if err != nil { if err != nil {
return err return err
} }
node.User = *user node.User = *user
if result := hsdb.db.Save(&node); result.Error != nil { if result := tx.Save(&node); result.Error != nil {
return result.Error return result.Error
} }

View file

@ -36,7 +36,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
err = db.DestroyUser("test") err = db.DestroyUser("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
result := db.db.Preload("User").First(&pak, "key = ?", pak.Key) result := db.DB.Preload("User").First(&pak, "key = ?", pak.Key)
// destroying a user also deletes all associated preauthkeys // destroying a user also deletes all associated preauthkeys
c.Assert(result.Error, check.Equals, gorm.ErrRecordNotFound) c.Assert(result.Error, check.Equals, gorm.ErrRecordNotFound)
@ -53,7 +53,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
err = db.DestroyUser("test") err = db.DestroyUser("test")
c.Assert(err, check.Equals, ErrUserStillHasNodes) c.Assert(err, check.Equals, ErrUserStillHasNodes)
@ -105,7 +105,7 @@ func (s *Suite) TestSetMachineUser(c *check.C) {
RegisterMethod: util.RegisterMethodAuthKey, RegisterMethod: util.RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.db.Save(&node) db.DB.Save(&node)
c.Assert(node.UserID, check.Equals, oldUser.ID) c.Assert(node.UserID, check.Equals, oldUser.ID)
err = db.AssignNodeToUser(&node, newUser.Name) err = db.AssignNodeToUser(&node, newUser.Name)

View file

@ -211,7 +211,7 @@ func DERPProbeHandler(
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406 // The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale. // They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns // An example implementation is found here https://derp.tailscale.com/bootstrap-dns
// Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL // Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL.
func DERPBootstrapDNSHandler( func DERPBootstrapDNSHandler(
derpMap *tailcfg.DERPMap, derpMap *tailcfg.DERPMap,
) func(http.ResponseWriter, *http.Request) { ) func(http.ResponseWriter, *http.Request) {

View file

@ -8,11 +8,13 @@ import (
"time" "time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"gorm.io/gorm"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
@ -136,12 +138,14 @@ func (api headscaleV1APIServer) ExpirePreAuthKey(
ctx context.Context, ctx context.Context,
request *v1.ExpirePreAuthKeyRequest, request *v1.ExpirePreAuthKeyRequest,
) (*v1.ExpirePreAuthKeyResponse, error) { ) (*v1.ExpirePreAuthKeyResponse, error) {
preAuthKey, err := api.h.db.GetPreAuthKey(request.GetUser(), request.Key) err := api.h.db.DB.Transaction(func(tx *gorm.DB) error {
if err != nil { preAuthKey, err := db.GetPreAuthKey(tx, request.GetUser(), request.Key)
return nil, err if err != nil {
} return err
}
err = api.h.db.ExpirePreAuthKey(preAuthKey) return db.ExpirePreAuthKey(tx, preAuthKey)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -181,17 +185,31 @@ func (api headscaleV1APIServer) RegisterNode(
return nil, err return nil, err
} }
node, err := api.h.db.RegisterNodeFromAuthCallback( node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
api.h.registrationCache, return db.RegisterNodeFromAuthCallback(
mkey, tx,
request.GetUser(), api.h.registrationCache,
nil, mkey,
util.RegisterMethodCLI, request.GetUser(),
) nil,
util.RegisterMethodCLI,
api.h.cfg.IPPrefixes,
)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node},
Message: "called from api.RegisterNode",
}
if stateUpdate.Valid() {
ctx := types.NotifyCtx(ctx, "cli-registernode", node.Hostname)
api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String())
}
return &v1.RegisterNodeResponse{Node: node.Proto()}, nil return &v1.RegisterNodeResponse{Node: node.Proto()}, nil
} }
@ -217,25 +235,35 @@ func (api headscaleV1APIServer) SetTags(
ctx context.Context, ctx context.Context,
request *v1.SetTagsRequest, request *v1.SetTagsRequest,
) (*v1.SetTagsResponse, error) { ) (*v1.SetTagsResponse, error) {
node, err := api.h.db.GetNodeByID(request.GetNodeId())
if err != nil {
return nil, err
}
for _, tag := range request.GetTags() { for _, tag := range request.GetTags() {
err := validateTag(tag) err := validateTag(tag)
if err != nil { if err != nil {
return &v1.SetTagsResponse{ return nil, err
Node: nil,
}, status.Error(codes.InvalidArgument, err.Error())
} }
} }
err = api.h.db.SetTags(node, request.GetTags()) node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
err := db.SetTags(tx, request.GetNodeId(), request.GetTags())
if err != nil {
return nil, err
}
return db.GetNodeByID(tx, request.GetNodeId())
})
if err != nil { if err != nil {
return &v1.SetTagsResponse{ return &v1.SetTagsResponse{
Node: nil, Node: nil,
}, status.Error(codes.Internal, err.Error()) }, status.Error(codes.InvalidArgument, err.Error())
}
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node},
Message: "called from api.SetTags",
}
if stateUpdate.Valid() {
ctx := types.NotifyCtx(ctx, "cli-settags", node.Hostname)
api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String())
} }
log.Trace(). log.Trace().
@ -270,11 +298,21 @@ func (api headscaleV1APIServer) DeleteNode(
err = api.h.db.DeleteNode( err = api.h.db.DeleteNode(
node, node,
api.h.nodeNotifier.ConnectedMap(),
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
stateUpdate := types.StateUpdate{
Type: types.StatePeerRemoved,
Removed: []tailcfg.NodeID{tailcfg.NodeID(node.ID)},
}
if stateUpdate.Valid() {
ctx := types.NotifyCtx(ctx, "cli-deletenode", node.Hostname)
api.h.nodeNotifier.NotifyAll(ctx, stateUpdate)
}
return &v1.DeleteNodeResponse{}, nil return &v1.DeleteNodeResponse{}, nil
} }
@ -282,17 +320,38 @@ func (api headscaleV1APIServer) ExpireNode(
ctx context.Context, ctx context.Context,
request *v1.ExpireNodeRequest, request *v1.ExpireNodeRequest,
) (*v1.ExpireNodeResponse, error) { ) (*v1.ExpireNodeResponse, error) {
node, err := api.h.db.GetNodeByID(request.GetNodeId()) now := time.Now()
node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
db.NodeSetExpiry(
tx,
request.GetNodeId(),
now,
)
return db.GetNodeByID(tx, request.GetNodeId())
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
now := time.Now() selfUpdate := types.StateUpdate{
Type: types.StateSelfUpdate,
ChangeNodes: types.Nodes{node},
}
if selfUpdate.Valid() {
ctx := types.NotifyCtx(ctx, "cli-expirenode-self", node.Hostname)
api.h.nodeNotifier.NotifyByMachineKey(
ctx,
selfUpdate,
node.MachineKey)
}
api.h.db.NodeSetExpiry( stateUpdate := types.StateUpdateExpire(node.ID, now)
node, if stateUpdate.Valid() {
now, ctx := types.NotifyCtx(ctx, "cli-expirenode-peers", node.Hostname)
) api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String())
}
log.Trace(). log.Trace().
Str("node", node.Hostname). Str("node", node.Hostname).
@ -306,17 +365,30 @@ func (api headscaleV1APIServer) RenameNode(
ctx context.Context, ctx context.Context,
request *v1.RenameNodeRequest, request *v1.RenameNodeRequest,
) (*v1.RenameNodeResponse, error) { ) (*v1.RenameNodeResponse, error) {
node, err := api.h.db.GetNodeByID(request.GetNodeId()) node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
err := db.RenameNode(
tx,
request.GetNodeId(),
request.GetNewName(),
)
if err != nil {
return nil, err
}
return db.GetNodeByID(tx, request.GetNodeId())
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = api.h.db.RenameNode( stateUpdate := types.StateUpdate{
node, Type: types.StatePeerChanged,
request.GetNewName(), ChangeNodes: types.Nodes{node},
) Message: "called from api.RenameNode",
if err != nil { }
return nil, err if stateUpdate.Valid() {
ctx := types.NotifyCtx(ctx, "cli-renamenode", node.Hostname)
api.h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String())
} }
log.Trace(). log.Trace().
@ -331,8 +403,11 @@ func (api headscaleV1APIServer) ListNodes(
ctx context.Context, ctx context.Context,
request *v1.ListNodesRequest, request *v1.ListNodesRequest,
) (*v1.ListNodesResponse, error) { ) (*v1.ListNodesResponse, error) {
isConnected := api.h.nodeNotifier.ConnectedMap()
if request.GetUser() != "" { if request.GetUser() != "" {
nodes, err := api.h.db.ListNodesByUser(request.GetUser()) nodes, err := db.Read(api.h.db.DB, func(rx *gorm.DB) (types.Nodes, error) {
return db.ListNodesByUser(rx, request.GetUser())
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -343,7 +418,7 @@ func (api headscaleV1APIServer) ListNodes(
// Populate the online field based on // Populate the online field based on
// currently connected nodes. // currently connected nodes.
resp.Online = api.h.nodeNotifier.IsConnected(node.MachineKey) resp.Online = isConnected[node.MachineKey]
response[index] = resp response[index] = resp
} }
@ -362,10 +437,10 @@ func (api headscaleV1APIServer) ListNodes(
// Populate the online field based on // Populate the online field based on
// currently connected nodes. // currently connected nodes.
resp.Online = api.h.nodeNotifier.IsConnected(node.MachineKey) resp.Online = isConnected[node.MachineKey]
validTags, invalidTags := api.h.ACLPolicy.TagsOfNode( validTags, invalidTags := api.h.ACLPolicy.TagsOfNode(
&node, node,
) )
resp.InvalidTags = invalidTags resp.InvalidTags = invalidTags
resp.ValidTags = validTags resp.ValidTags = validTags
@ -396,7 +471,9 @@ func (api headscaleV1APIServer) GetRoutes(
ctx context.Context, ctx context.Context,
request *v1.GetRoutesRequest, request *v1.GetRoutesRequest,
) (*v1.GetRoutesResponse, error) { ) (*v1.GetRoutesResponse, error) {
routes, err := api.h.db.GetRoutes() routes, err := db.Read(api.h.db.DB, func(rx *gorm.DB) (types.Routes, error) {
return db.GetRoutes(rx)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -410,11 +487,19 @@ func (api headscaleV1APIServer) EnableRoute(
ctx context.Context, ctx context.Context,
request *v1.EnableRouteRequest, request *v1.EnableRouteRequest,
) (*v1.EnableRouteResponse, error) { ) (*v1.EnableRouteResponse, error) {
err := api.h.db.EnableRoute(request.GetRouteId()) update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
return db.EnableRoute(tx, request.GetRouteId())
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if update != nil && update.Valid() {
ctx := types.NotifyCtx(ctx, "cli-enableroute", "unknown")
api.h.nodeNotifier.NotifyAll(
ctx, *update)
}
return &v1.EnableRouteResponse{}, nil return &v1.EnableRouteResponse{}, nil
} }
@ -422,11 +507,19 @@ func (api headscaleV1APIServer) DisableRoute(
ctx context.Context, ctx context.Context,
request *v1.DisableRouteRequest, request *v1.DisableRouteRequest,
) (*v1.DisableRouteResponse, error) { ) (*v1.DisableRouteResponse, error) {
err := api.h.db.DisableRoute(request.GetRouteId()) isConnected := api.h.nodeNotifier.ConnectedMap()
update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
return db.DisableRoute(tx, request.GetRouteId(), isConnected)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if update != nil && update.Valid() {
ctx := types.NotifyCtx(ctx, "cli-disableroute", "unknown")
api.h.nodeNotifier.NotifyAll(ctx, *update)
}
return &v1.DisableRouteResponse{}, nil return &v1.DisableRouteResponse{}, nil
} }
@ -453,11 +546,19 @@ func (api headscaleV1APIServer) DeleteRoute(
ctx context.Context, ctx context.Context,
request *v1.DeleteRouteRequest, request *v1.DeleteRouteRequest,
) (*v1.DeleteRouteResponse, error) { ) (*v1.DeleteRouteResponse, error) {
err := api.h.db.DeleteRoute(request.GetRouteId()) isConnected := api.h.nodeNotifier.ConnectedMap()
update, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
return db.DeleteRoute(tx, request.GetRouteId(), isConnected)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if update != nil && update.Valid() {
ctx := types.NotifyCtx(ctx, "cli-deleteroute", "unknown")
api.h.nodeNotifier.NotifyWithIgnore(ctx, *update)
}
return &v1.DeleteRouteResponse{}, nil return &v1.DeleteRouteResponse{}, nil
} }

View file

@ -272,6 +272,7 @@ func (m *Mapper) LiteMapResponse(
mapRequest tailcfg.MapRequest, mapRequest tailcfg.MapRequest,
node *types.Node, node *types.Node,
pol *policy.ACLPolicy, pol *policy.ACLPolicy,
messages ...string,
) ([]byte, error) { ) ([]byte, error) {
resp, err := m.baseWithConfigMapResponse(node, pol, mapRequest.Version) resp, err := m.baseWithConfigMapResponse(node, pol, mapRequest.Version)
if err != nil { if err != nil {
@ -290,7 +291,7 @@ func (m *Mapper) LiteMapResponse(
resp.PacketFilter = policy.ReduceFilterRules(node, rules) resp.PacketFilter = policy.ReduceFilterRules(node, rules)
resp.SSHPolicy = sshPolicy resp.SSHPolicy = sshPolicy
return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress) return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress, messages...)
} }
func (m *Mapper) KeepAliveResponse( func (m *Mapper) KeepAliveResponse(
@ -392,9 +393,7 @@ func (m *Mapper) PeerChangedPatchResponse(
} }
if patches, ok := m.patches[uint64(change.NodeID)]; ok { if patches, ok := m.patches[uint64(change.NodeID)]; ok {
patches := append(patches, p) m.patches[uint64(change.NodeID)] = append(patches, p)
m.patches[uint64(change.NodeID)] = patches
} else { } else {
m.patches[uint64(change.NodeID)] = []patch{p} m.patches[uint64(change.NodeID)] = []patch{p}
} }
@ -470,6 +469,8 @@ func (m *Mapper) marshalMapResponse(
switch { switch {
case resp.Peers != nil && len(resp.Peers) > 0: case resp.Peers != nil && len(resp.Peers) > 0:
responseType = "full" responseType = "full"
case isSelfUpdate(messages...):
responseType = "self"
case resp.Peers == nil && resp.PeersChanged == nil && resp.PeersChangedPatch == nil: case resp.Peers == nil && resp.PeersChanged == nil && resp.PeersChangedPatch == nil:
responseType = "lite" responseType = "lite"
case resp.PeersChanged != nil && len(resp.PeersChanged) > 0: case resp.PeersChanged != nil && len(resp.PeersChanged) > 0:
@ -668,3 +669,13 @@ func appendPeerChanges(
return nil return nil
} }
func isSelfUpdate(messages ...string) bool {
for _, message := range messages {
if strings.Contains(message, types.SelfUpdateIdentifier) {
return true
}
}
return false
}

View file

@ -72,7 +72,7 @@ func tailNode(
} }
var derp string var derp string
if node.Hostinfo.NetInfo != nil { if node.Hostinfo != nil && node.Hostinfo.NetInfo != nil {
derp = fmt.Sprintf("127.3.3.40:%d", node.Hostinfo.NetInfo.PreferredDERP) derp = fmt.Sprintf("127.3.3.40:%d", node.Hostinfo.NetInfo.PreferredDERP)
} else { } else {
derp = "127.3.3.40:0" // Zero means disconnected or unknown. derp = "127.3.3.40:0" // Zero means disconnected or unknown.

View file

@ -1,6 +1,7 @@
package notifier package notifier
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -12,26 +13,30 @@ import (
) )
type Notifier struct { type Notifier struct {
l sync.RWMutex l sync.RWMutex
nodes map[string]chan<- types.StateUpdate nodes map[string]chan<- types.StateUpdate
connected map[key.MachinePublic]bool
} }
func NewNotifier() *Notifier { func NewNotifier() *Notifier {
return &Notifier{} return &Notifier{
nodes: make(map[string]chan<- types.StateUpdate),
connected: make(map[key.MachinePublic]bool),
}
} }
func (n *Notifier) AddNode(machineKey key.MachinePublic, c chan<- types.StateUpdate) { func (n *Notifier) AddNode(machineKey key.MachinePublic, c chan<- types.StateUpdate) {
log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to add node") log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to add node")
defer log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("releasing lock to add node") defer log.Trace().
Caller().
Str("key", machineKey.ShortString()).
Msg("releasing lock to add node")
n.l.Lock() n.l.Lock()
defer n.l.Unlock() defer n.l.Unlock()
if n.nodes == nil {
n.nodes = make(map[string]chan<- types.StateUpdate)
}
n.nodes[machineKey.String()] = c n.nodes[machineKey.String()] = c
n.connected[machineKey] = true
log.Trace(). log.Trace().
Str("machine_key", machineKey.ShortString()). Str("machine_key", machineKey.ShortString()).
@ -41,16 +46,20 @@ func (n *Notifier) AddNode(machineKey key.MachinePublic, c chan<- types.StateUpd
func (n *Notifier) RemoveNode(machineKey key.MachinePublic) { func (n *Notifier) RemoveNode(machineKey key.MachinePublic) {
log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to remove node") log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("acquiring lock to remove node")
defer log.Trace().Caller().Str("key", machineKey.ShortString()).Msg("releasing lock to remove node") defer log.Trace().
Caller().
Str("key", machineKey.ShortString()).
Msg("releasing lock to remove node")
n.l.Lock() n.l.Lock()
defer n.l.Unlock() defer n.l.Unlock()
if n.nodes == nil { if len(n.nodes) == 0 {
return return
} }
delete(n.nodes, machineKey.String()) delete(n.nodes, machineKey.String())
n.connected[machineKey] = false
log.Trace(). log.Trace().
Str("machine_key", machineKey.ShortString()). Str("machine_key", machineKey.ShortString()).
@ -64,23 +73,28 @@ func (n *Notifier) IsConnected(machineKey key.MachinePublic) bool {
n.l.RLock() n.l.RLock()
defer n.l.RUnlock() defer n.l.RUnlock()
if _, ok := n.nodes[machineKey.String()]; ok { return n.connected[machineKey]
return true
}
return false
} }
func (n *Notifier) NotifyAll(update types.StateUpdate) { // TODO(kradalby): This returns a pointer and can be dangerous.
n.NotifyWithIgnore(update) func (n *Notifier) ConnectedMap() map[key.MachinePublic]bool {
return n.connected
} }
func (n *Notifier) NotifyWithIgnore(update types.StateUpdate, ignore ...string) { func (n *Notifier) NotifyAll(ctx context.Context, update types.StateUpdate) {
n.NotifyWithIgnore(ctx, update)
}
func (n *Notifier) NotifyWithIgnore(
ctx context.Context,
update types.StateUpdate,
ignore ...string,
) {
log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify") log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify")
defer log.Trace(). defer log.Trace().
Caller(). Caller().
Interface("type", update.Type). Interface("type", update.Type).
Msg("releasing lock, finished notifing") Msg("releasing lock, finished notifying")
n.l.RLock() n.l.RLock()
defer n.l.RUnlock() defer n.l.RUnlock()
@ -90,23 +104,58 @@ func (n *Notifier) NotifyWithIgnore(update types.StateUpdate, ignore ...string)
continue continue
} }
log.Trace().Caller().Str("machine", key).Strs("ignoring", ignore).Msg("sending update") select {
c <- update case <-ctx.Done():
log.Error().
Err(ctx.Err()).
Str("mkey", key).
Any("origin", ctx.Value("origin")).
Any("hostname", ctx.Value("hostname")).
Msgf("update not sent, context cancelled")
return
case c <- update:
log.Trace().
Str("mkey", key).
Any("origin", ctx.Value("origin")).
Any("hostname", ctx.Value("hostname")).
Msgf("update successfully sent on chan")
}
} }
} }
func (n *Notifier) NotifyByMachineKey(update types.StateUpdate, mKey key.MachinePublic) { func (n *Notifier) NotifyByMachineKey(
ctx context.Context,
update types.StateUpdate,
mKey key.MachinePublic,
) {
log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify") log.Trace().Caller().Interface("type", update.Type).Msg("acquiring lock to notify")
defer log.Trace(). defer log.Trace().
Caller(). Caller().
Interface("type", update.Type). Interface("type", update.Type).
Msg("releasing lock, finished notifing") Msg("releasing lock, finished notifying")
n.l.RLock() n.l.RLock()
defer n.l.RUnlock() defer n.l.RUnlock()
if c, ok := n.nodes[mKey.String()]; ok { if c, ok := n.nodes[mKey.String()]; ok {
c <- update select {
case <-ctx.Done():
log.Error().
Err(ctx.Err()).
Str("mkey", mKey.String()).
Any("origin", ctx.Value("origin")).
Any("hostname", ctx.Value("hostname")).
Msgf("update not sent, context cancelled")
return
case c <- update:
log.Trace().
Str("mkey", mKey.String()).
Any("origin", ctx.Value("origin")).
Any("hostname", ctx.Value("hostname")).
Msgf("update successfully sent on chan")
}
} }
} }

View file

@ -20,6 +20,7 @@ import (
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gorm.io/gorm"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
@ -492,7 +493,7 @@ func (h *Headscale) validateNodeForOIDCCallback(
Str("node", node.Hostname). Str("node", node.Hostname).
Msg("node already registered, reauthenticating") Msg("node already registered, reauthenticating")
err := h.db.NodeSetExpiry(node, expiry) err := h.db.NodeSetExpiry(node.ID, expiry)
if err != nil { if err != nil {
util.LogErr(err, "Failed to refresh node") util.LogErr(err, "Failed to refresh node")
http.Error( http.Error(
@ -536,6 +537,12 @@ func (h *Headscale) validateNodeForOIDCCallback(
util.LogErr(err, "Failed to write response") util.LogErr(err, "Failed to write response")
} }
stateUpdate := types.StateUpdateExpire(node.ID, expiry)
if stateUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "oidc-expiry", "na")
h.nodeNotifier.NotifyWithIgnore(ctx, stateUpdate, node.MachineKey.String())
}
return nil, true, nil return nil, true, nil
} }
@ -613,14 +620,22 @@ func (h *Headscale) registerNodeForOIDCCallback(
machineKey *key.MachinePublic, machineKey *key.MachinePublic,
expiry time.Time, expiry time.Time,
) error { ) error {
if _, err := h.db.RegisterNodeFromAuthCallback( if err := h.db.DB.Transaction(func(tx *gorm.DB) error {
// TODO(kradalby): find a better way to use the cache across modules if _, err := db.RegisterNodeFromAuthCallback(
h.registrationCache, // TODO(kradalby): find a better way to use the cache across modules
*machineKey, tx,
user.Name, h.registrationCache,
&expiry, *machineKey,
util.RegisterMethodOIDC, user.Name,
); err != nil { &expiry,
util.RegisterMethodOIDC,
h.cfg.IPPrefixes,
); err != nil {
return err
}
return nil
}); err != nil {
util.LogErr(err, "could not register node") util.LogErr(err, "could not register node")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError) writer.WriteHeader(http.StatusInternalServerError)

View file

@ -905,32 +905,39 @@ func (pol *ACLPolicy) TagsOfNode(
validTags := make([]string, 0) validTags := make([]string, 0)
invalidTags := make([]string, 0) invalidTags := make([]string, 0)
// TODO(kradalby): Why is this sometimes nil? coming from tailNode?
if node == nil {
return validTags, invalidTags
}
validTagMap := make(map[string]bool) validTagMap := make(map[string]bool)
invalidTagMap := make(map[string]bool) invalidTagMap := make(map[string]bool)
for _, tag := range node.Hostinfo.RequestTags { if node.Hostinfo != nil {
owners, err := expandOwnersFromTag(pol, tag) for _, tag := range node.Hostinfo.RequestTags {
if errors.Is(err, ErrInvalidTag) { owners, err := expandOwnersFromTag(pol, tag)
invalidTagMap[tag] = true if errors.Is(err, ErrInvalidTag) {
invalidTagMap[tag] = true
continue continue
} }
var found bool var found bool
for _, owner := range owners { for _, owner := range owners {
if node.User.Name == owner { if node.User.Name == owner {
found = true found = true
}
}
if found {
validTagMap[tag] = true
} else {
invalidTagMap[tag] = true
} }
} }
if found { for tag := range invalidTagMap {
validTagMap[tag] = true invalidTags = append(invalidTags, tag)
} else { }
invalidTagMap[tag] = true for tag := range validTagMap {
validTags = append(validTags, tag)
} }
}
for tag := range invalidTagMap {
invalidTags = append(invalidTags, tag)
}
for tag := range validTagMap {
validTags = append(validTags, tag)
} }
return validTags, invalidTags return validTags, invalidTags

View file

@ -4,12 +4,15 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/mapper"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
xslices "golang.org/x/exp/slices" xslices "golang.org/x/exp/slices"
"gorm.io/gorm"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@ -128,10 +131,14 @@ func (h *Headscale) handlePoll(
if h.ACLPolicy != nil { if h.ACLPolicy != nil {
// update routes with peer information // update routes with peer information
err = h.db.EnableAutoApprovedRoutes(h.ACLPolicy, node) update, err := h.db.EnableAutoApprovedRoutes(h.ACLPolicy, node)
if err != nil { if err != nil {
logErr(err, "Error running auto approved routes") logErr(err, "Error running auto approved routes")
} }
if update != nil {
sendUpdate = true
}
} }
} }
@ -146,7 +153,7 @@ func (h *Headscale) handlePoll(
} }
if sendUpdate { if sendUpdate {
if err := h.db.NodeSave(node); err != nil { if err := h.db.DB.Save(node).Error; err != nil {
logErr(err, "Failed to persist/update node in the database") logErr(err, "Failed to persist/update node in the database")
http.Error(writer, "", http.StatusInternalServerError) http.Error(writer, "", http.StatusInternalServerError)
@ -161,7 +168,9 @@ func (h *Headscale) handlePoll(
Message: "called from handlePoll -> update -> new hostinfo", Message: "called from handlePoll -> update -> new hostinfo",
} }
if stateUpdate.Valid() { if stateUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-hostinfochange", node.Hostname)
h.nodeNotifier.NotifyWithIgnore( h.nodeNotifier.NotifyWithIgnore(
ctx,
stateUpdate, stateUpdate,
node.MachineKey.String()) node.MachineKey.String())
} }
@ -174,7 +183,9 @@ func (h *Headscale) handlePoll(
ChangeNodes: types.Nodes{node}, ChangeNodes: types.Nodes{node},
} }
if selfUpdate.Valid() { if selfUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-self-hostinfochange", node.Hostname)
h.nodeNotifier.NotifyByMachineKey( h.nodeNotifier.NotifyByMachineKey(
ctx,
selfUpdate, selfUpdate,
node.MachineKey) node.MachineKey)
} }
@ -183,7 +194,7 @@ func (h *Headscale) handlePoll(
} }
} }
if err := h.db.NodeSave(node); err != nil { if err := h.db.DB.Save(node).Error; err != nil {
logErr(err, "Failed to persist/update node in the database") logErr(err, "Failed to persist/update node in the database")
http.Error(writer, "", http.StatusInternalServerError) http.Error(writer, "", http.StatusInternalServerError)
@ -195,7 +206,9 @@ func (h *Headscale) handlePoll(
ChangePatches: []*tailcfg.PeerChange{&change}, ChangePatches: []*tailcfg.PeerChange{&change},
} }
if stateUpdate.Valid() { if stateUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-patch", node.Hostname)
h.nodeNotifier.NotifyWithIgnore( h.nodeNotifier.NotifyWithIgnore(
ctx,
stateUpdate, stateUpdate,
node.MachineKey.String()) node.MachineKey.String())
} }
@ -251,7 +264,7 @@ func (h *Headscale) handlePoll(
} }
} }
if err := h.db.NodeSave(node); err != nil { if err := h.db.DB.Save(node).Error; err != nil {
logErr(err, "Failed to persist/update node in the database") logErr(err, "Failed to persist/update node in the database")
http.Error(writer, "", http.StatusInternalServerError) http.Error(writer, "", http.StatusInternalServerError)
@ -288,7 +301,10 @@ func (h *Headscale) handlePoll(
// update ACLRules with peer informations (to update server tags if necessary) // update ACLRules with peer informations (to update server tags if necessary)
if h.ACLPolicy != nil { if h.ACLPolicy != nil {
// update routes with peer information // update routes with peer information
err = h.db.EnableAutoApprovedRoutes(h.ACLPolicy, node) // This state update is ignored as it will be sent
// as part of the whole node
// TODO(kradalby): figure out if that is actually correct
_, err = h.db.EnableAutoApprovedRoutes(h.ACLPolicy, node)
if err != nil { if err != nil {
logErr(err, "Error running auto approved routes") logErr(err, "Error running auto approved routes")
} }
@ -324,11 +340,17 @@ func (h *Headscale) handlePoll(
Message: "called from handlePoll -> new node added", Message: "called from handlePoll -> new node added",
} }
if stateUpdate.Valid() { if stateUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "poll-newnode-peers", node.Hostname)
h.nodeNotifier.NotifyWithIgnore( h.nodeNotifier.NotifyWithIgnore(
ctx,
stateUpdate, stateUpdate,
node.MachineKey.String()) node.MachineKey.String())
} }
if len(node.Routes) > 0 {
go h.pollFailoverRoutes(logErr, "new node", node)
}
// Set up the client stream // Set up the client stream
h.pollNetMapStreamWG.Add(1) h.pollNetMapStreamWG.Add(1)
defer h.pollNetMapStreamWG.Done() defer h.pollNetMapStreamWG.Done()
@ -346,15 +368,9 @@ func (h *Headscale) handlePoll(
keepAliveTicker := time.NewTicker(keepAliveInterval) keepAliveTicker := time.NewTicker(keepAliveInterval)
ctx = context.WithValue(ctx, nodeNameContextKey, node.Hostname) ctx, cancel := context.WithCancel(context.WithValue(ctx, nodeNameContextKey, node.Hostname))
ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
if len(node.Routes) > 0 {
go h.db.EnsureFailoverRouteIsAvailable(node)
}
for { for {
logInfo("Waiting for update on stream channel") logInfo("Waiting for update on stream channel")
select { select {
@ -403,6 +419,7 @@ func (h *Headscale) handlePoll(
return return
} }
startMapResp := time.Now()
switch update.Type { switch update.Type {
case types.StateFullUpdate: case types.StateFullUpdate:
logInfo("Sending Full MapResponse") logInfo("Sending Full MapResponse")
@ -411,6 +428,7 @@ func (h *Headscale) handlePoll(
case types.StatePeerChanged: case types.StatePeerChanged:
logInfo(fmt.Sprintf("Sending Changed MapResponse: %s", update.Message)) logInfo(fmt.Sprintf("Sending Changed MapResponse: %s", update.Message))
isConnectedMap := h.nodeNotifier.ConnectedMap()
for _, node := range update.ChangeNodes { for _, node := range update.ChangeNodes {
// If a node is not reported to be online, it might be // If a node is not reported to be online, it might be
// because the value is outdated, check with the notifier. // because the value is outdated, check with the notifier.
@ -418,7 +436,7 @@ func (h *Headscale) handlePoll(
// this might be because it has announced itself, but not // this might be because it has announced itself, but not
// reached the stage to actually create the notifier channel. // reached the stage to actually create the notifier channel.
if node.IsOnline != nil && !*node.IsOnline { if node.IsOnline != nil && !*node.IsOnline {
isOnline := h.nodeNotifier.IsConnected(node.MachineKey) isOnline := isConnectedMap[node.MachineKey]
node.IsOnline = &isOnline node.IsOnline = &isOnline
} }
} }
@ -434,7 +452,7 @@ func (h *Headscale) handlePoll(
if len(update.ChangeNodes) == 1 { if len(update.ChangeNodes) == 1 {
logInfo("Sending SelfUpdate MapResponse") logInfo("Sending SelfUpdate MapResponse")
node = update.ChangeNodes[0] node = update.ChangeNodes[0]
data, err = mapp.LiteMapResponse(mapRequest, node, h.ACLPolicy) data, err = mapp.LiteMapResponse(mapRequest, node, h.ACLPolicy, types.SelfUpdateIdentifier)
} else { } else {
logInfo("SelfUpdate contained too many nodes, this is likely a bug in the code, please report.") logInfo("SelfUpdate contained too many nodes, this is likely a bug in the code, please report.")
} }
@ -449,8 +467,11 @@ func (h *Headscale) handlePoll(
return return
} }
log.Trace().Str("node", node.Hostname).TimeDiff("timeSpent", time.Now(), startMapResp).Str("mkey", node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished making map response")
// Only send update if there is change // Only send update if there is change
if data != nil { if data != nil {
startWrite := time.Now()
_, err = writer.Write(data) _, err = writer.Write(data)
if err != nil { if err != nil {
logErr(err, "Could not write the map response") logErr(err, "Could not write the map response")
@ -468,6 +489,7 @@ func (h *Headscale) handlePoll(
return return
} }
log.Trace().Str("node", node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", node.MachineKey.String()).Int("type", int(update.Type)).Msg("finished writing mapresp to node")
log.Info(). log.Info().
Caller(). Caller().
@ -487,7 +509,7 @@ func (h *Headscale) handlePoll(
go h.updateNodeOnlineStatus(false, node) go h.updateNodeOnlineStatus(false, node)
// Failover the node's routes if any. // Failover the node's routes if any.
go h.db.FailoverNodeRoutesWithNotify(node) go h.pollFailoverRoutes(logErr, "node closing connection", node)
// The connection has been closed, so we can stop polling. // The connection has been closed, so we can stop polling.
return return
@ -500,6 +522,22 @@ func (h *Headscale) handlePoll(
} }
} }
func (h *Headscale) pollFailoverRoutes(logErr func(error, string), where string, node *types.Node) {
update, err := db.Write(h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
return db.EnsureFailoverRouteIsAvailable(tx, h.nodeNotifier.ConnectedMap(), node)
})
if err != nil {
logErr(err, fmt.Sprintf("failed to ensure failover routes, %s", where))
return
}
if update != nil && !update.Empty() && update.Valid() {
ctx := types.NotifyCtx(context.Background(), fmt.Sprintf("poll-%s-routes-ensurefailover", strings.ReplaceAll(where, " ", "-")), node.Hostname)
h.nodeNotifier.NotifyWithIgnore(ctx, *update, node.MachineKey.String())
}
}
// updateNodeOnlineStatus records the last seen status of a node and notifies peers // updateNodeOnlineStatus records the last seen status of a node and notifies peers
// about change in their online/offline status. // about change in their online/offline status.
// It takes a StateUpdateType of either StatePeerOnlineChanged or StatePeerOfflineChanged. // It takes a StateUpdateType of either StatePeerOnlineChanged or StatePeerOfflineChanged.
@ -519,10 +557,13 @@ func (h *Headscale) updateNodeOnlineStatus(online bool, node *types.Node) {
}, },
} }
if statusUpdate.Valid() { if statusUpdate.Valid() {
h.nodeNotifier.NotifyWithIgnore(statusUpdate, node.MachineKey.String()) ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-onlinestatus", node.Hostname)
h.nodeNotifier.NotifyWithIgnore(ctx, statusUpdate, node.MachineKey.String())
} }
err := h.db.UpdateLastSeen(node) err := h.db.DB.Transaction(func(tx *gorm.DB) error {
return db.UpdateLastSeen(tx, node.ID, *node.LastSeen)
})
if err != nil { if err != nil {
log.Error().Err(err).Msg("Cannot update node LastSeen") log.Error().Err(err).Msg("Cannot update node LastSeen")

View file

@ -13,7 +13,7 @@ import (
) )
const ( const (
MinimumCapVersion tailcfg.CapabilityVersion = 56 MinimumCapVersion tailcfg.CapabilityVersion = 58
) )
// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol // NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol

View file

@ -1,15 +1,19 @@
package types package types
import ( import (
"context"
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"time"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
const SelfUpdateIdentifier = "self-update"
var ErrCannotParsePrefix = errors.New("cannot parse prefix") var ErrCannotParsePrefix = errors.New("cannot parse prefix")
type IPPrefix netip.Prefix type IPPrefix netip.Prefix
@ -160,3 +164,37 @@ func (su *StateUpdate) Valid() bool {
return true return true
} }
// Empty reports if there are any updates in the StateUpdate.
func (su *StateUpdate) Empty() bool {
switch su.Type {
case StatePeerChanged:
return len(su.ChangeNodes) == 0
case StatePeerChangedPatch:
return len(su.ChangePatches) == 0
case StatePeerRemoved:
return len(su.Removed) == 0
}
return false
}
func StateUpdateExpire(nodeID uint64, expiry time.Time) StateUpdate {
return StateUpdate{
Type: StatePeerChangedPatch,
ChangePatches: []*tailcfg.PeerChange{
{
NodeID: tailcfg.NodeID(nodeID),
KeyExpiry: &expiry,
},
},
}
}
func NotifyCtx(ctx context.Context, origin, hostname string) context.Context {
ctx2, _ := context.WithTimeout(
context.WithValue(context.WithValue(ctx, "hostname", hostname), "origin", origin),
3*time.Second,
)
return ctx2
}

View file

@ -2,7 +2,6 @@ package types
import ( import (
"strconv" "strconv"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
@ -22,12 +21,13 @@ type User struct {
func (n *User) TailscaleUser() *tailcfg.User { func (n *User) TailscaleUser() *tailcfg.User {
user := tailcfg.User{ user := tailcfg.User{
ID: tailcfg.UserID(n.ID), ID: tailcfg.UserID(n.ID),
LoginName: n.Name, LoginName: n.Name,
DisplayName: n.Name, DisplayName: n.Name,
// TODO(kradalby): See if we can fill in Gravatar here
ProfilePicURL: "", ProfilePicURL: "",
Logins: []tailcfg.LoginID{}, Logins: []tailcfg.LoginID{},
Created: time.Time{}, Created: n.CreatedAt,
} }
return &user return &user
@ -35,9 +35,10 @@ func (n *User) TailscaleUser() *tailcfg.User {
func (n *User) TailscaleLogin() *tailcfg.Login { func (n *User) TailscaleLogin() *tailcfg.Login {
login := tailcfg.Login{ login := tailcfg.Login{
ID: tailcfg.LoginID(n.ID), ID: tailcfg.LoginID(n.ID),
LoginName: n.Name, LoginName: n.Name,
DisplayName: n.Name, DisplayName: n.Name,
// TODO(kradalby): See if we can fill in Gravatar here
ProfilePicURL: "", ProfilePicURL: "",
} }

View file

@ -1240,7 +1240,7 @@ func TestNodeRenameCommand(t *testing.T) {
assert.Contains(t, listAll[4].GetGivenName(), "node-5") assert.Contains(t, listAll[4].GetGivenName(), "node-5")
for idx := 0; idx < 3; idx++ { for idx := 0; idx < 3; idx++ {
_, err := headscale.Execute( res, err := headscale.Execute(
[]string{ []string{
"headscale", "headscale",
"nodes", "nodes",
@ -1251,6 +1251,8 @@ func TestNodeRenameCommand(t *testing.T) {
}, },
) )
assert.Nil(t, err) assert.Nil(t, err)
assert.Contains(t, res, "Node renamed")
} }
var listAllAfterRename []v1.Node var listAllAfterRename []v1.Node

View file

@ -26,6 +26,8 @@ func TestPingAllByIP(t *testing.T) {
assertNoErr(t, err) assertNoErr(t, err)
defer scenario.Shutdown() defer scenario.Shutdown()
// TODO(kradalby): it does not look like the user thing works, only second
// get created? maybe only when many?
spec := map[string]int{ spec := map[string]int{
"user1": len(MustTestVersions), "user1": len(MustTestVersions),
"user2": len(MustTestVersions), "user2": len(MustTestVersions),
@ -321,7 +323,12 @@ func TestTaildrop(t *testing.T) {
t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err) t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err)
} }
} }
curlCommand := []string{"curl", "--unix-socket", "/var/run/tailscale/tailscaled.sock", "http://local-tailscaled.sock/localapi/v0/file-targets"} curlCommand := []string{
"curl",
"--unix-socket",
"/var/run/tailscale/tailscaled.sock",
"http://local-tailscaled.sock/localapi/v0/file-targets",
}
err = retry(10, 1*time.Second, func() error { err = retry(10, 1*time.Second, func() error {
result, _, err := client.Execute(curlCommand) result, _, err := client.Execute(curlCommand)
if err != nil { if err != nil {
@ -338,13 +345,23 @@ func TestTaildrop(t *testing.T) {
for _, ft := range fts { for _, ft := range fts {
ftStr += fmt.Sprintf("\t%s\n", ft.Node.Name) ftStr += fmt.Sprintf("\t%s\n", ft.Node.Name)
} }
return fmt.Errorf("client %s does not have all its peers as FileTargets, got %d, want: %d\n%s", client.Hostname(), len(fts), len(allClients)-1, ftStr) return fmt.Errorf(
"client %s does not have all its peers as FileTargets, got %d, want: %d\n%s",
client.Hostname(),
len(fts),
len(allClients)-1,
ftStr,
)
} }
return err return err
}) })
if err != nil { if err != nil {
t.Errorf("failed to query localapi for filetarget on %s, err: %s", client.Hostname(), err) t.Errorf(
"failed to query localapi for filetarget on %s, err: %s",
client.Hostname(),
err,
)
} }
} }
@ -434,72 +451,6 @@ func TestTaildrop(t *testing.T) {
} }
} }
func TestResolveMagicDNS(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
scenario, err := NewScenario()
assertNoErr(t, err)
defer scenario.Shutdown()
spec := map[string]int{
"magicdns1": len(MustTestVersions),
"magicdns2": len(MustTestVersions),
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns"))
assertNoErrHeadscaleEnv(t, err)
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
// Poor mans cache
_, err = scenario.ListTailscaleClientsFQDNs()
assertNoErrListFQDN(t, err)
_, err = scenario.ListTailscaleClientsIPs()
assertNoErrListClientIPs(t, err)
for _, client := range allClients {
for _, peer := range allClients {
// It is safe to ignore this error as we handled it when caching it
peerFQDN, _ := peer.FQDN()
command := []string{
"tailscale",
"ip", peerFQDN,
}
result, _, err := client.Execute(command)
if err != nil {
t.Fatalf(
"failed to execute resolve/ip command %s from %s: %s",
peerFQDN,
client.Hostname(),
err,
)
}
ips, err := peer.IPs()
if err != nil {
t.Fatalf(
"failed to get ips for %s: %s",
peer.Hostname(),
err,
)
}
for _, ip := range ips {
if !strings.Contains(result, ip.String()) {
t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result)
}
}
}
}
}
func TestExpireNode(t *testing.T) { func TestExpireNode(t *testing.T) {
IntegrationSkip(t) IntegrationSkip(t)
t.Parallel() t.Parallel()
@ -545,7 +496,7 @@ func TestExpireNode(t *testing.T) {
// TODO(kradalby): This is Headscale specific and would not play nicely // TODO(kradalby): This is Headscale specific and would not play nicely
// with other implementations of the ControlServer interface // with other implementations of the ControlServer interface
result, err := headscale.Execute([]string{ result, err := headscale.Execute([]string{
"headscale", "nodes", "expire", "--identifier", "0", "--output", "json", "headscale", "nodes", "expire", "--identifier", "1", "--output", "json",
}) })
assertNoErr(t, err) assertNoErr(t, err)
@ -576,16 +527,38 @@ func TestExpireNode(t *testing.T) {
assertNotNil(t, peerStatus.Expired) assertNotNil(t, peerStatus.Expired)
assert.NotNil(t, peerStatus.KeyExpiry) assert.NotNil(t, peerStatus.KeyExpiry)
t.Logf("node %q should have a key expire before %s, was %s", peerStatus.HostName, now.String(), peerStatus.KeyExpiry) t.Logf(
"node %q should have a key expire before %s, was %s",
peerStatus.HostName,
now.String(),
peerStatus.KeyExpiry,
)
if peerStatus.KeyExpiry != nil { if peerStatus.KeyExpiry != nil {
assert.Truef(t, peerStatus.KeyExpiry.Before(now), "node %q should have a key expire before %s, was %s", peerStatus.HostName, now.String(), peerStatus.KeyExpiry) assert.Truef(
t,
peerStatus.KeyExpiry.Before(now),
"node %q should have a key expire before %s, was %s",
peerStatus.HostName,
now.String(),
peerStatus.KeyExpiry,
)
} }
assert.Truef(t, peerStatus.Expired, "node %q should be expired, expired is %v", peerStatus.HostName, peerStatus.Expired) assert.Truef(
t,
peerStatus.Expired,
"node %q should be expired, expired is %v",
peerStatus.HostName,
peerStatus.Expired,
)
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName()}) _, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName()})
if !strings.Contains(stderr, "node key has expired") { if !strings.Contains(stderr, "node key has expired") {
t.Errorf("expected to be unable to ping expired host %q from %q", node.GetName(), client.Hostname()) t.Errorf(
"expected to be unable to ping expired host %q from %q",
node.GetName(),
client.Hostname(),
)
} }
} else { } else {
t.Errorf("failed to find node %q with nodekey (%s) in mapresponse, should be present even if it is expired", node.GetName(), expiredNodeKey) t.Errorf("failed to find node %q with nodekey (%s) in mapresponse, should be present even if it is expired", node.GetName(), expiredNodeKey)
@ -597,7 +570,7 @@ func TestExpireNode(t *testing.T) {
// NeedsLogin means that the node has understood that it is no longer // NeedsLogin means that the node has understood that it is no longer
// valid. // valid.
assert.Equal(t, "NeedsLogin", status.BackendState) assert.Equalf(t, "NeedsLogin", status.BackendState, "checking node %q", status.Self.HostName)
} }
} }
} }
@ -690,7 +663,8 @@ func TestNodeOnlineLastSeenStatus(t *testing.T) {
assert.Truef( assert.Truef(
t, t,
lastSeen.After(lastSeenThreshold), lastSeen.After(lastSeenThreshold),
"lastSeen (%v) was not %s after the threshold (%v)", "node (%s) lastSeen (%v) was not %s after the threshold (%v)",
node.GetName(),
lastSeen, lastSeen,
keepAliveInterval, keepAliveInterval,
lastSeenThreshold, lastSeenThreshold,

View file

@ -88,9 +88,9 @@ func TestEnablingRoutes(t *testing.T) {
assert.Len(t, routes, 3) assert.Len(t, routes, 3)
for _, route := range routes { for _, route := range routes {
assert.Equal(t, route.GetAdvertised(), true) assert.Equal(t, true, route.GetAdvertised())
assert.Equal(t, route.GetEnabled(), false) assert.Equal(t, false, route.GetEnabled())
assert.Equal(t, route.GetIsPrimary(), false) assert.Equal(t, false, route.GetIsPrimary())
} }
// Verify that no routes has been sent to the client, // Verify that no routes has been sent to the client,
@ -135,9 +135,9 @@ func TestEnablingRoutes(t *testing.T) {
assert.Len(t, enablingRoutes, 3) assert.Len(t, enablingRoutes, 3)
for _, route := range enablingRoutes { for _, route := range enablingRoutes {
assert.Equal(t, route.GetAdvertised(), true) assert.Equal(t, true, route.GetAdvertised())
assert.Equal(t, route.GetEnabled(), true) assert.Equal(t, true, route.GetEnabled())
assert.Equal(t, route.GetIsPrimary(), true) assert.Equal(t, true, route.GetIsPrimary())
} }
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
@ -191,6 +191,8 @@ func TestEnablingRoutes(t *testing.T) {
}) })
assertNoErr(t, err) assertNoErr(t, err)
time.Sleep(5 * time.Second)
var disablingRoutes []*v1.Route var disablingRoutes []*v1.Route
err = executeAndUnmarshal( err = executeAndUnmarshal(
headscale, headscale,
@ -209,16 +211,14 @@ func TestEnablingRoutes(t *testing.T) {
assert.Equal(t, true, route.GetAdvertised()) assert.Equal(t, true, route.GetAdvertised())
if route.GetId() == routeToBeDisabled.GetId() { if route.GetId() == routeToBeDisabled.GetId() {
assert.Equal(t, route.GetEnabled(), false) assert.Equal(t, false, route.GetEnabled())
assert.Equal(t, route.GetIsPrimary(), false) assert.Equal(t, false, route.GetIsPrimary())
} else { } else {
assert.Equal(t, route.GetEnabled(), true) assert.Equal(t, true, route.GetEnabled())
assert.Equal(t, route.GetIsPrimary(), true) assert.Equal(t, true, route.GetIsPrimary())
} }
} }
time.Sleep(5 * time.Second)
// Verify that the clients can see the new routes // Verify that the clients can see the new routes
for _, client := range allClients { for _, client := range allClients {
status, err := client.Status() status, err := client.Status()
@ -294,7 +294,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
// advertise HA route on node 1 and 2 // advertise HA route on node 1 and 2
// ID 1 will be primary // ID 1 will be primary
// ID 2 will be secondary // ID 2 will be secondary
for _, client := range allClients { for _, client := range allClients[:2] {
status, err := client.Status() status, err := client.Status()
assertNoErr(t, err) assertNoErr(t, err)
@ -306,6 +306,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
} }
_, _, err = client.Execute(command) _, _, err = client.Execute(command)
assertNoErrf(t, "failed to advertise route: %s", err) assertNoErrf(t, "failed to advertise route: %s", err)
} else {
t.Fatalf("failed to find route for Node %s (id: %s)", status.Self.HostName, status.Self.ID)
} }
} }
@ -328,6 +330,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
assertNoErr(t, err) assertNoErr(t, err)
assert.Len(t, routes, 2) assert.Len(t, routes, 2)
t.Logf("initial routes %#v", routes)
for _, route := range routes { for _, route := range routes {
assert.Equal(t, true, route.GetAdvertised()) assert.Equal(t, true, route.GetAdvertised())
assert.Equal(t, false, route.GetEnabled()) assert.Equal(t, false, route.GetEnabled())
@ -644,6 +648,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
assertNoErr(t, err) assertNoErr(t, err)
assert.Len(t, routesAfterDisabling1, 2) assert.Len(t, routesAfterDisabling1, 2)
t.Logf("routes after disabling1 %#v", routesAfterDisabling1)
// Node 1 is not primary // Node 1 is not primary
assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised()) assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised())
assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled()) assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled())

View file

@ -56,8 +56,8 @@ var (
"1.44": true, // CapVer: 63 "1.44": true, // CapVer: 63
"1.42": true, // CapVer: 61 "1.42": true, // CapVer: 61
"1.40": true, // CapVer: 61 "1.40": true, // CapVer: 61
"1.38": true, // CapVer: 58 "1.38": true, // Oldest supported version, CapVer: 58
"1.36": true, // Oldest supported version, CapVer: 56 "1.36": false, // CapVer: 56
"1.34": false, // CapVer: 51 "1.34": false, // CapVer: 51
"1.32": false, // CapVer: 46 "1.32": false, // CapVer: 46
"1.30": false, "1.30": false,