mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-13 11:01:07 -04:00
this commit moves all of the read and write logic, and all different parts of headscale that manages some sort of persistent and in memory state into a separate package. The goal of this is to clearly define the boundry between parts of the app which accesses and modifies data, and where it happens. Previously, different state (routes, policy, db and so on) was used directly, and sometime passed to functions as pointers. Now all access has to go through state. In the initial implementation, most of the same functions exists and have just been moved. In the future centralising this will allow us to optimise bottle necks with the database (in memory state) and make the different parts talking to eachother do so in the same way across headscale components. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
813 lines
25 KiB
Go
813 lines
25 KiB
Go
// Package state provides core state management for Headscale, coordinating
|
|
// between subsystems like database, IP allocation, policy management, and DERP routing.
|
|
package state
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"os"
|
|
"time"
|
|
|
|
hsdb "github.com/juanfont/headscale/hscontrol/db"
|
|
"github.com/juanfont/headscale/hscontrol/derp"
|
|
"github.com/juanfont/headscale/hscontrol/policy"
|
|
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
|
"github.com/juanfont/headscale/hscontrol/routes"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/sasha-s/go-deadlock"
|
|
"gorm.io/gorm"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/ptr"
|
|
zcache "zgo.at/zcache/v2"
|
|
)
|
|
|
|
const (
|
|
// registerCacheExpiration defines how long node registration entries remain in cache.
|
|
registerCacheExpiration = time.Minute * 15
|
|
|
|
// registerCacheCleanup defines the interval for cleaning up expired cache entries.
|
|
registerCacheCleanup = time.Minute * 20
|
|
)
|
|
|
|
// ErrUnsupportedPolicyMode is returned for invalid policy modes. Valid modes are "file" and "db".
|
|
var ErrUnsupportedPolicyMode = errors.New("unsupported policy mode")
|
|
|
|
// State manages Headscale's core state, coordinating between database, policy management,
|
|
// IP allocation, and DERP routing. All methods are thread-safe.
|
|
type State struct {
|
|
// mu protects all in-memory data structures from concurrent access
|
|
mu deadlock.RWMutex
|
|
// cfg holds the current Headscale configuration
|
|
cfg *types.Config
|
|
|
|
// in-memory data, protected by mu
|
|
// nodes contains the current set of registered nodes
|
|
nodes types.Nodes
|
|
// users contains the current set of users/namespaces
|
|
users types.Users
|
|
|
|
// subsystem keeping state
|
|
// db provides persistent storage and database operations
|
|
db *hsdb.HSDatabase
|
|
// ipAlloc manages IP address allocation for nodes
|
|
ipAlloc *hsdb.IPAllocator
|
|
// derpMap contains the current DERP relay configuration
|
|
derpMap *tailcfg.DERPMap
|
|
// polMan handles policy evaluation and management
|
|
polMan policy.PolicyManager
|
|
// registrationCache caches node registration data to reduce database load
|
|
registrationCache *zcache.Cache[types.RegistrationID, types.RegisterNode]
|
|
// primaryRoutes tracks primary route assignments for nodes
|
|
primaryRoutes *routes.PrimaryRoutes
|
|
}
|
|
|
|
// NewState creates and initializes a new State instance, setting up the database,
|
|
// IP allocator, DERP map, policy manager, and loading existing users and nodes.
|
|
func NewState(cfg *types.Config) (*State, error) {
|
|
registrationCache := zcache.New[types.RegistrationID, types.RegisterNode](
|
|
registerCacheExpiration,
|
|
registerCacheCleanup,
|
|
)
|
|
|
|
db, err := hsdb.NewHeadscaleDatabase(
|
|
cfg.Database,
|
|
cfg.BaseDomain,
|
|
registrationCache,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("init database: %w", err)
|
|
}
|
|
|
|
ipAlloc, err := hsdb.NewIPAllocator(db, cfg.PrefixV4, cfg.PrefixV6, cfg.IPAllocation)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("init ip allocatior: %w", err)
|
|
}
|
|
|
|
derpMap := derp.GetDERPMap(cfg.DERP)
|
|
|
|
nodes, err := db.ListNodes()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading nodes: %w", err)
|
|
}
|
|
users, err := db.ListUsers()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading users: %w", err)
|
|
}
|
|
|
|
pol, err := policyBytes(db, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading policy: %w", err)
|
|
}
|
|
|
|
polMan, err := policy.NewPolicyManager(pol, users, nodes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("init policy manager: %w", err)
|
|
}
|
|
|
|
return &State{
|
|
cfg: cfg,
|
|
|
|
nodes: nodes,
|
|
users: users,
|
|
|
|
db: db,
|
|
ipAlloc: ipAlloc,
|
|
// TODO(kradalby): Update DERPMap
|
|
derpMap: derpMap,
|
|
polMan: polMan,
|
|
registrationCache: registrationCache,
|
|
primaryRoutes: routes.New(),
|
|
}, nil
|
|
}
|
|
|
|
// Close gracefully shuts down the State instance and releases all resources.
|
|
func (s *State) Close() error {
|
|
if err := s.db.Close(); err != nil {
|
|
return fmt.Errorf("closing database: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// policyBytes loads policy configuration from file or database based on the configured mode.
|
|
// Returns nil if no policy is configured, which is valid.
|
|
func policyBytes(db *hsdb.HSDatabase, cfg *types.Config) ([]byte, error) {
|
|
switch cfg.Policy.Mode {
|
|
case types.PolicyModeFile:
|
|
path := cfg.Policy.Path
|
|
|
|
// It is fine to start headscale without a policy file.
|
|
if len(path) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
absPath := util.AbsolutePathFromConfigPath(path)
|
|
policyFile, err := os.Open(absPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer policyFile.Close()
|
|
|
|
return io.ReadAll(policyFile)
|
|
|
|
case types.PolicyModeDB:
|
|
p, err := db.GetPolicy()
|
|
if err != nil {
|
|
if errors.Is(err, types.ErrPolicyNotFound) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
if p.Data == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return []byte(p.Data), err
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, cfg.Policy.Mode)
|
|
}
|
|
|
|
// DERPMap returns the current DERP relay configuration for peer-to-peer connectivity.
|
|
func (s *State) DERPMap() *tailcfg.DERPMap {
|
|
return s.derpMap
|
|
}
|
|
|
|
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
|
|
// Returns true if the policy changed.
|
|
func (s *State) ReloadPolicy() (bool, error) {
|
|
pol, err := policyBytes(s.db, s.cfg)
|
|
if err != nil {
|
|
return false, fmt.Errorf("loading policy: %w", err)
|
|
}
|
|
|
|
changed, err := s.polMan.SetPolicy(pol)
|
|
if err != nil {
|
|
return false, fmt.Errorf("setting policy: %w", err)
|
|
}
|
|
|
|
if changed {
|
|
err := s.autoApproveNodes()
|
|
if err != nil {
|
|
return false, fmt.Errorf("auto approving nodes: %w", err)
|
|
}
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// AutoApproveNodes processes pending nodes and auto-approves those meeting policy criteria.
|
|
func (s *State) AutoApproveNodes() error {
|
|
return s.autoApproveNodes()
|
|
}
|
|
|
|
// CreateUser creates a new user and updates the policy manager.
|
|
// Returns the created user, whether policies changed, and any error.
|
|
func (s *State) CreateUser(user types.User) (*types.User, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err := s.db.DB.Save(&user).Error; err != nil {
|
|
return nil, false, fmt.Errorf("creating user: %w", err)
|
|
}
|
|
|
|
// Check if policy manager needs updating
|
|
policyChanged, err := s.updatePolicyManagerUsers()
|
|
if err != nil {
|
|
// Log the error but don't fail the user creation
|
|
return &user, false, fmt.Errorf("failed to update policy manager after user creation: %w", err)
|
|
}
|
|
|
|
// TODO(kradalby): implement the user in-memory cache
|
|
|
|
return &user, policyChanged, nil
|
|
}
|
|
|
|
// UpdateUser modifies an existing user using the provided update function within a transaction.
|
|
// Returns the updated user, whether policies changed, and any error.
|
|
func (s *State) UpdateUser(userID types.UserID, updateFn func(*types.User) error) (*types.User, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
user, err := hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.User, error) {
|
|
user, err := hsdb.GetUserByID(tx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := updateFn(user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tx.Save(user).Error; err != nil {
|
|
return nil, fmt.Errorf("updating user: %w", err)
|
|
}
|
|
|
|
return user, nil
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Check if policy manager needs updating
|
|
policyChanged, err := s.updatePolicyManagerUsers()
|
|
if err != nil {
|
|
return user, false, fmt.Errorf("failed to update policy manager after user update: %w", err)
|
|
}
|
|
|
|
// TODO(kradalby): implement the user in-memory cache
|
|
|
|
return user, policyChanged, nil
|
|
}
|
|
|
|
// DeleteUser permanently removes a user and all associated data (nodes, API keys, etc).
|
|
// This operation is irreversible.
|
|
func (s *State) DeleteUser(userID types.UserID) error {
|
|
return s.db.DestroyUser(userID)
|
|
}
|
|
|
|
// RenameUser changes a user's name. The new name must be unique.
|
|
func (s *State) RenameUser(userID types.UserID, newName string) (*types.User, bool, error) {
|
|
return s.UpdateUser(userID, func(user *types.User) error {
|
|
user.Name = newName
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// GetUserByID retrieves a user by ID.
|
|
func (s *State) GetUserByID(userID types.UserID) (*types.User, error) {
|
|
return s.db.GetUserByID(userID)
|
|
}
|
|
|
|
// GetUserByName retrieves a user by name.
|
|
func (s *State) GetUserByName(name string) (*types.User, error) {
|
|
return s.db.GetUserByName(name)
|
|
}
|
|
|
|
// GetUserByOIDCIdentifier retrieves a user by their OIDC identifier.
|
|
func (s *State) GetUserByOIDCIdentifier(id string) (*types.User, error) {
|
|
return s.db.GetUserByOIDCIdentifier(id)
|
|
}
|
|
|
|
// ListUsersWithFilter retrieves users matching the specified filter criteria.
|
|
func (s *State) ListUsersWithFilter(filter *types.User) ([]types.User, error) {
|
|
return s.db.ListUsers(filter)
|
|
}
|
|
|
|
// ListAllUsers retrieves all users in the system.
|
|
func (s *State) ListAllUsers() ([]types.User, error) {
|
|
return s.db.ListUsers()
|
|
}
|
|
|
|
// CreateNode creates a new node and updates the policy manager.
|
|
// Returns the created node, whether policies changed, and any error.
|
|
func (s *State) CreateNode(node *types.Node) (*types.Node, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err := s.db.DB.Save(node).Error; err != nil {
|
|
return nil, false, fmt.Errorf("creating node: %w", err)
|
|
}
|
|
|
|
// Check if policy manager needs updating
|
|
policyChanged, err := s.updatePolicyManagerNodes()
|
|
if err != nil {
|
|
return node, false, fmt.Errorf("failed to update policy manager after node creation: %w", err)
|
|
}
|
|
|
|
// TODO(kradalby): implement the node in-memory cache
|
|
|
|
return node, policyChanged, nil
|
|
}
|
|
|
|
// updateNodeTx performs a database transaction to update a node and refresh the policy manager.
|
|
func (s *State) updateNodeTx(nodeID types.NodeID, updateFn func(tx *gorm.DB) error) (*types.Node, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
node, err := hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
|
if err := updateFn(tx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node, err := hsdb.GetNodeByID(tx, nodeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tx.Save(node).Error; err != nil {
|
|
return nil, fmt.Errorf("updating node: %w", err)
|
|
}
|
|
|
|
return node, nil
|
|
})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Check if policy manager needs updating
|
|
policyChanged, err := s.updatePolicyManagerNodes()
|
|
if err != nil {
|
|
return node, false, fmt.Errorf("failed to update policy manager after node update: %w", err)
|
|
}
|
|
|
|
// TODO(kradalby): implement the node in-memory cache
|
|
|
|
return node, policyChanged, nil
|
|
}
|
|
|
|
// SaveNode persists an existing node to the database and updates the policy manager.
|
|
func (s *State) SaveNode(node *types.Node) (*types.Node, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err := s.db.DB.Save(node).Error; err != nil {
|
|
return nil, false, fmt.Errorf("saving node: %w", err)
|
|
}
|
|
|
|
// Check if policy manager needs updating
|
|
policyChanged, err := s.updatePolicyManagerNodes()
|
|
if err != nil {
|
|
return node, false, fmt.Errorf("failed to update policy manager after node save: %w", err)
|
|
}
|
|
|
|
// TODO(kradalby): implement the node in-memory cache
|
|
|
|
return node, policyChanged, nil
|
|
}
|
|
|
|
// DeleteNode permanently removes a node and cleans up associated resources.
|
|
// Returns whether policies changed and any error. This operation is irreversible.
|
|
func (s *State) DeleteNode(node *types.Node) (bool, error) {
|
|
err := s.db.DeleteNode(node)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Check if policy manager needs updating after node deletion
|
|
policyChanged, err := s.updatePolicyManagerNodes()
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to update policy manager after node deletion: %w", err)
|
|
}
|
|
|
|
return policyChanged, nil
|
|
}
|
|
|
|
func (s *State) Connect(node *types.Node) bool {
|
|
_ = s.primaryRoutes.SetRoutes(node.ID, node.SubnetRoutes()...)
|
|
|
|
// TODO(kradalby): this should be more granular, allowing us to
|
|
// only send a online update change.
|
|
return true
|
|
}
|
|
|
|
func (s *State) Disconnect(node *types.Node) (bool, error) {
|
|
// TODO(kradalby): This node should update the in memory state
|
|
_, polChanged, err := s.SetLastSeen(node.ID, time.Now())
|
|
if err != nil {
|
|
return false, fmt.Errorf("disconnecting node: %w", err)
|
|
}
|
|
|
|
changed := s.primaryRoutes.SetRoutes(node.ID, node.SubnetRoutes()...)
|
|
|
|
// TODO(kradalby): the returned change should be more nuanced allowing us to
|
|
// send more directed updates.
|
|
return changed || polChanged, nil
|
|
}
|
|
|
|
// GetNodeByID retrieves a node by ID.
|
|
func (s *State) GetNodeByID(nodeID types.NodeID) (*types.Node, error) {
|
|
return s.db.GetNodeByID(nodeID)
|
|
}
|
|
|
|
// GetNodeByNodeKey retrieves a node by its Tailscale public key.
|
|
func (s *State) GetNodeByNodeKey(nodeKey key.NodePublic) (*types.Node, error) {
|
|
return s.db.GetNodeByNodeKey(nodeKey)
|
|
}
|
|
|
|
// ListNodes retrieves specific nodes by ID, or all nodes if no IDs provided.
|
|
func (s *State) ListNodes(nodeIDs ...types.NodeID) (types.Nodes, error) {
|
|
if len(nodeIDs) == 0 {
|
|
return s.db.ListNodes()
|
|
}
|
|
|
|
return s.db.ListNodes(nodeIDs...)
|
|
}
|
|
|
|
// ListNodesByUser retrieves all nodes belonging to a specific user.
|
|
func (s *State) ListNodesByUser(userID types.UserID) (types.Nodes, error) {
|
|
return hsdb.Read(s.db.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
|
return hsdb.ListNodesByUser(rx, userID)
|
|
})
|
|
}
|
|
|
|
// ListPeers retrieves nodes that can communicate with the specified node based on policy.
|
|
func (s *State) ListPeers(nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {
|
|
return s.db.ListPeers(nodeID, peerIDs...)
|
|
}
|
|
|
|
// ListEphemeralNodes retrieves all ephemeral (temporary) nodes in the system.
|
|
func (s *State) ListEphemeralNodes() (types.Nodes, error) {
|
|
return s.db.ListEphemeralNodes()
|
|
}
|
|
|
|
// SetNodeExpiry updates the expiration time for a node.
|
|
func (s *State) SetNodeExpiry(nodeID types.NodeID, expiry time.Time) (*types.Node, bool, error) {
|
|
return s.updateNodeTx(nodeID, func(tx *gorm.DB) error {
|
|
return hsdb.NodeSetExpiry(tx, nodeID, expiry)
|
|
})
|
|
}
|
|
|
|
// SetNodeTags assigns tags to a node for use in access control policies.
|
|
func (s *State) SetNodeTags(nodeID types.NodeID, tags []string) (*types.Node, bool, error) {
|
|
return s.updateNodeTx(nodeID, func(tx *gorm.DB) error {
|
|
return hsdb.SetTags(tx, nodeID, tags)
|
|
})
|
|
}
|
|
|
|
// SetApprovedRoutes sets the network routes that a node is approved to advertise.
|
|
func (s *State) SetApprovedRoutes(nodeID types.NodeID, routes []netip.Prefix) (*types.Node, bool, error) {
|
|
return s.updateNodeTx(nodeID, func(tx *gorm.DB) error {
|
|
return hsdb.SetApprovedRoutes(tx, nodeID, routes)
|
|
})
|
|
}
|
|
|
|
// RenameNode changes the display name of a node.
|
|
func (s *State) RenameNode(nodeID types.NodeID, newName string) (*types.Node, bool, error) {
|
|
return s.updateNodeTx(nodeID, func(tx *gorm.DB) error {
|
|
return hsdb.RenameNode(tx, nodeID, newName)
|
|
})
|
|
}
|
|
|
|
// SetLastSeen updates when a node was last seen, used for connectivity monitoring.
|
|
func (s *State) SetLastSeen(nodeID types.NodeID, lastSeen time.Time) (*types.Node, bool, error) {
|
|
return s.updateNodeTx(nodeID, func(tx *gorm.DB) error {
|
|
return hsdb.SetLastSeen(tx, nodeID, lastSeen)
|
|
})
|
|
}
|
|
|
|
// AssignNodeToUser transfers a node to a different user.
|
|
func (s *State) AssignNodeToUser(nodeID types.NodeID, userID types.UserID) (*types.Node, bool, error) {
|
|
return s.updateNodeTx(nodeID, func(tx *gorm.DB) error {
|
|
return hsdb.AssignNodeToUser(tx, nodeID, userID)
|
|
})
|
|
}
|
|
|
|
// BackfillNodeIPs assigns IP addresses to nodes that don't have them.
|
|
func (s *State) BackfillNodeIPs() ([]string, error) {
|
|
return s.db.BackfillNodeIPs(s.ipAlloc)
|
|
}
|
|
|
|
// ExpireExpiredNodes finds and processes expired nodes since the last check.
|
|
// Returns next check time, state update with expired nodes, and whether any were found.
|
|
func (s *State) ExpireExpiredNodes(lastCheck time.Time) (time.Time, types.StateUpdate, bool) {
|
|
return hsdb.ExpireExpiredNodes(s.db.DB, lastCheck)
|
|
}
|
|
|
|
// SSHPolicy returns the SSH access policy for a node.
|
|
func (s *State) SSHPolicy(node *types.Node) (*tailcfg.SSHPolicy, error) {
|
|
return s.polMan.SSHPolicy(node)
|
|
}
|
|
|
|
// Filter returns the current network filter rules and matches.
|
|
func (s *State) Filter() ([]tailcfg.FilterRule, []matcher.Match) {
|
|
return s.polMan.Filter()
|
|
}
|
|
|
|
// NodeCanHaveTag checks if a node is allowed to have a specific tag.
|
|
func (s *State) NodeCanHaveTag(node *types.Node, tag string) bool {
|
|
return s.polMan.NodeCanHaveTag(node, tag)
|
|
}
|
|
|
|
// SetPolicy updates the policy configuration.
|
|
func (s *State) SetPolicy(pol []byte) (bool, error) {
|
|
return s.polMan.SetPolicy(pol)
|
|
}
|
|
|
|
// AutoApproveRoutes checks if a node's routes should be auto-approved.
|
|
func (s *State) AutoApproveRoutes(node *types.Node) bool {
|
|
return policy.AutoApproveRoutes(s.polMan, node)
|
|
}
|
|
|
|
// PolicyDebugString returns a debug representation of the current policy.
|
|
func (s *State) PolicyDebugString() string {
|
|
return s.polMan.DebugString()
|
|
}
|
|
|
|
// GetPolicy retrieves the current policy from the database.
|
|
func (s *State) GetPolicy() (*types.Policy, error) {
|
|
return s.db.GetPolicy()
|
|
}
|
|
|
|
// SetPolicyInDB stores policy data in the database.
|
|
func (s *State) SetPolicyInDB(data string) (*types.Policy, error) {
|
|
return s.db.SetPolicy(data)
|
|
}
|
|
|
|
// SetNodeRoutes sets the primary routes for a node.
|
|
func (s *State) SetNodeRoutes(nodeID types.NodeID, routes ...netip.Prefix) bool {
|
|
return s.primaryRoutes.SetRoutes(nodeID, routes...)
|
|
}
|
|
|
|
// GetNodePrimaryRoutes returns the primary routes for a node.
|
|
func (s *State) GetNodePrimaryRoutes(nodeID types.NodeID) []netip.Prefix {
|
|
return s.primaryRoutes.PrimaryRoutes(nodeID)
|
|
}
|
|
|
|
// PrimaryRoutesString returns a string representation of all primary routes.
|
|
func (s *State) PrimaryRoutesString() string {
|
|
return s.primaryRoutes.String()
|
|
}
|
|
|
|
// ValidateAPIKey checks if an API key is valid and active.
|
|
func (s *State) ValidateAPIKey(keyStr string) (bool, error) {
|
|
return s.db.ValidateAPIKey(keyStr)
|
|
}
|
|
|
|
// CreateAPIKey generates a new API key with optional expiration.
|
|
func (s *State) CreateAPIKey(expiration *time.Time) (string, *types.APIKey, error) {
|
|
return s.db.CreateAPIKey(expiration)
|
|
}
|
|
|
|
// GetAPIKey retrieves an API key by its prefix.
|
|
func (s *State) GetAPIKey(prefix string) (*types.APIKey, error) {
|
|
return s.db.GetAPIKey(prefix)
|
|
}
|
|
|
|
// ExpireAPIKey marks an API key as expired.
|
|
func (s *State) ExpireAPIKey(key *types.APIKey) error {
|
|
return s.db.ExpireAPIKey(key)
|
|
}
|
|
|
|
// ListAPIKeys returns all API keys in the system.
|
|
func (s *State) ListAPIKeys() ([]types.APIKey, error) {
|
|
return s.db.ListAPIKeys()
|
|
}
|
|
|
|
// DestroyAPIKey permanently removes an API key.
|
|
func (s *State) DestroyAPIKey(key types.APIKey) error {
|
|
return s.db.DestroyAPIKey(key)
|
|
}
|
|
|
|
// CreatePreAuthKey generates a new pre-authentication key for a user.
|
|
func (s *State) CreatePreAuthKey(userID types.UserID, reusable bool, ephemeral bool, expiration *time.Time, aclTags []string) (*types.PreAuthKey, error) {
|
|
return s.db.CreatePreAuthKey(userID, reusable, ephemeral, expiration, aclTags)
|
|
}
|
|
|
|
// GetPreAuthKey retrieves a pre-authentication key by ID.
|
|
func (s *State) GetPreAuthKey(id string) (*types.PreAuthKey, error) {
|
|
return s.db.GetPreAuthKey(id)
|
|
}
|
|
|
|
// ListPreAuthKeys returns all pre-authentication keys for a user.
|
|
func (s *State) ListPreAuthKeys(userID types.UserID) ([]types.PreAuthKey, error) {
|
|
return s.db.ListPreAuthKeys(userID)
|
|
}
|
|
|
|
// ExpirePreAuthKey marks a pre-authentication key as expired.
|
|
func (s *State) ExpirePreAuthKey(preAuthKey *types.PreAuthKey) error {
|
|
return s.db.ExpirePreAuthKey(preAuthKey)
|
|
}
|
|
|
|
// GetRegistrationCacheEntry retrieves a node registration from cache.
|
|
func (s *State) GetRegistrationCacheEntry(id types.RegistrationID) (*types.RegisterNode, bool) {
|
|
entry, found := s.registrationCache.Get(id)
|
|
if !found {
|
|
return nil, false
|
|
}
|
|
|
|
return &entry, true
|
|
}
|
|
|
|
// SetRegistrationCacheEntry stores a node registration in cache.
|
|
func (s *State) SetRegistrationCacheEntry(id types.RegistrationID, entry types.RegisterNode) {
|
|
s.registrationCache.Set(id, entry)
|
|
}
|
|
|
|
// HandleNodeFromAuthPath handles node registration through authentication flow (like OIDC).
|
|
func (s *State) HandleNodeFromAuthPath(
|
|
registrationID types.RegistrationID,
|
|
userID types.UserID,
|
|
expiry *time.Time,
|
|
registrationMethod string,
|
|
) (*types.Node, bool, error) {
|
|
ipv4, ipv6, err := s.ipAlloc.Next()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return s.db.HandleNodeFromAuthPath(
|
|
registrationID,
|
|
userID,
|
|
expiry,
|
|
util.RegisterMethodOIDC,
|
|
ipv4, ipv6,
|
|
)
|
|
}
|
|
|
|
// HandleNodeFromPreAuthKey handles node registration using a pre-authentication key.
|
|
func (s *State) HandleNodeFromPreAuthKey(
|
|
regReq tailcfg.RegisterRequest,
|
|
machineKey key.MachinePublic,
|
|
) (*types.Node, bool, error) {
|
|
pak, err := s.GetPreAuthKey(regReq.Auth.AuthKey)
|
|
|
|
err = pak.Validate()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
nodeToRegister := types.Node{
|
|
Hostname: regReq.Hostinfo.Hostname,
|
|
UserID: pak.User.ID,
|
|
User: pak.User,
|
|
MachineKey: machineKey,
|
|
NodeKey: regReq.NodeKey,
|
|
Hostinfo: regReq.Hostinfo,
|
|
LastSeen: ptr.To(time.Now()),
|
|
RegisterMethod: util.RegisterMethodAuthKey,
|
|
|
|
// TODO(kradalby): This should not be set on the node,
|
|
// they should be looked up through the key, which is
|
|
// attached to the node.
|
|
ForcedTags: pak.Proto().GetAclTags(),
|
|
AuthKey: pak,
|
|
AuthKeyID: &pak.ID,
|
|
}
|
|
|
|
if !regReq.Expiry.IsZero() {
|
|
nodeToRegister.Expiry = ®Req.Expiry
|
|
}
|
|
|
|
ipv4, ipv6, err := s.ipAlloc.Next()
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("allocating IPs: %w", err)
|
|
}
|
|
|
|
node, err := hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
|
node, err := hsdb.RegisterNode(tx,
|
|
nodeToRegister,
|
|
ipv4, ipv6,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("registering node: %w", err)
|
|
}
|
|
|
|
if !pak.Reusable {
|
|
err = hsdb.UsePreAuthKey(tx, pak)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("using pre auth key: %w", err)
|
|
}
|
|
}
|
|
|
|
return node, nil
|
|
})
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("writing node to database: %w", err)
|
|
}
|
|
|
|
// Check if policy manager needs updating
|
|
// This is necessary because we just created a new node.
|
|
// We need to ensure that the policy manager is aware of this new node.
|
|
policyChanged, err := s.updatePolicyManagerNodes()
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("failed to update policy manager after node registration: %w", err)
|
|
}
|
|
|
|
return node, policyChanged, nil
|
|
}
|
|
|
|
// AllocateNextIPs allocates the next available IPv4 and IPv6 addresses.
|
|
func (s *State) AllocateNextIPs() (*netip.Addr, *netip.Addr, error) {
|
|
return s.ipAlloc.Next()
|
|
}
|
|
|
|
// updatePolicyManagerUsers updates the policy manager with current users.
|
|
// Returns true if the policy changed and notifications should be sent.
|
|
// TODO(kradalby): This is a temporary stepping stone, ultimately we should
|
|
// have the list already available so it could go much quicker. Alternatively
|
|
// the policy manager could have a remove or add list for users.
|
|
// updatePolicyManagerUsers refreshes the policy manager with current user data.
|
|
func (s *State) updatePolicyManagerUsers() (bool, error) {
|
|
users, err := s.ListAllUsers()
|
|
if err != nil {
|
|
return false, fmt.Errorf("listing users for policy update: %w", err)
|
|
}
|
|
|
|
changed, err := s.polMan.SetUsers(users)
|
|
if err != nil {
|
|
return false, fmt.Errorf("updating policy manager users: %w", err)
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// updatePolicyManagerNodes updates the policy manager with current nodes.
|
|
// Returns true if the policy changed and notifications should be sent.
|
|
// TODO(kradalby): This is a temporary stepping stone, ultimately we should
|
|
// have the list already available so it could go much quicker. Alternatively
|
|
// the policy manager could have a remove or add list for nodes.
|
|
// updatePolicyManagerNodes refreshes the policy manager with current node data.
|
|
func (s *State) updatePolicyManagerNodes() (bool, error) {
|
|
nodes, err := s.ListNodes()
|
|
if err != nil {
|
|
return false, fmt.Errorf("listing nodes for policy update: %w", err)
|
|
}
|
|
|
|
changed, err := s.polMan.SetNodes(nodes)
|
|
if err != nil {
|
|
return false, fmt.Errorf("updating policy manager nodes: %w", err)
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// PingDB checks if the database connection is healthy.
|
|
func (s *State) PingDB(ctx context.Context) error {
|
|
return s.db.PingDB(ctx)
|
|
}
|
|
|
|
// autoApproveNodes mass approves routes on all nodes. It is _only_ intended for
|
|
// use when the policy is replaced. It is not sending or reporting any changes
|
|
// or updates as we send full updates after replacing the policy.
|
|
// TODO(kradalby): This is kind of messy, maybe this is another +1
|
|
// for an event bus. See example comments here.
|
|
// autoApproveNodes automatically approves nodes based on policy rules.
|
|
func (s *State) autoApproveNodes() error {
|
|
err := s.db.Write(func(tx *gorm.DB) error {
|
|
nodes, err := hsdb.ListNodes(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, node := range nodes {
|
|
// TODO(kradalby): This change should probably be sent to the rest of the system.
|
|
changed := policy.AutoApproveRoutes(s.polMan, node)
|
|
if changed {
|
|
err = tx.Save(node).Error
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(kradalby): This should probably be done outside of the transaction,
|
|
// and the result of this should be propagated to the system.
|
|
s.primaryRoutes.SetRoutes(node.ID, node.SubnetRoutes()...)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("auto approving routes for nodes: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|