mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-24 13:46:53 +02:00
Added manual approval of nodes in the network
This commit is contained in:
parent
89f70595d1
commit
122d86f2fa
@ -48,6 +48,13 @@ func init() {
|
|||||||
}
|
}
|
||||||
nodeCmd.AddCommand(registerNodeCmd)
|
nodeCmd.AddCommand(registerNodeCmd)
|
||||||
|
|
||||||
|
approveNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||||
|
err = approveNodeCmd.MarkFlagRequired("identifier")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
nodeCmd.AddCommand(approveNodeCmd)
|
||||||
|
|
||||||
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
|
||||||
err = expireNodeCmd.MarkFlagRequired("identifier")
|
err = expireNodeCmd.MarkFlagRequired("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -206,6 +213,43 @@ var listNodesCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var approveNodeCmd = &cobra.Command{
|
||||||
|
Use: "approve",
|
||||||
|
Short: "Approve a node in your network",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
|
if err != nil {
|
||||||
|
ErrorOutput(
|
||||||
|
err,
|
||||||
|
fmt.Sprintf("Error converting ID to integer: %s", err),
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, client, conn, cancel := newHeadscaleCLIWithConfig()
|
||||||
|
defer cancel()
|
||||||
|
defer conn.Close()
|
||||||
|
request := &v1.ApproveNodeRequest{
|
||||||
|
NodeId: identifier,
|
||||||
|
}
|
||||||
|
response, err := client.ApproveNode(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
ErrorOutput(
|
||||||
|
err,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Cannot expire node: %s\n",
|
||||||
|
status.Convert(err).Message(),
|
||||||
|
),
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SuccessOutput(response.GetNode(), "Node approved", output)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var expireNodeCmd = &cobra.Command{
|
var expireNodeCmd = &cobra.Command{
|
||||||
Use: "expire",
|
Use: "expire",
|
||||||
Short: "Expire (log out) a node in your network",
|
Short: "Expire (log out) a node in your network",
|
||||||
|
@ -36,6 +36,8 @@ func init() {
|
|||||||
preauthkeysCmd.AddCommand(expirePreAuthKeyCmd)
|
preauthkeysCmd.AddCommand(expirePreAuthKeyCmd)
|
||||||
createPreAuthKeyCmd.PersistentFlags().
|
createPreAuthKeyCmd.PersistentFlags().
|
||||||
Bool("reusable", false, "Make the preauthkey reusable")
|
Bool("reusable", false, "Make the preauthkey reusable")
|
||||||
|
createPreAuthKeyCmd.PersistentFlags().
|
||||||
|
Bool("pre-approved", false, "Make the preauthkey with node pre-approval")
|
||||||
createPreAuthKeyCmd.PersistentFlags().
|
createPreAuthKeyCmd.PersistentFlags().
|
||||||
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
|
||||||
createPreAuthKeyCmd.Flags().
|
createPreAuthKeyCmd.Flags().
|
||||||
@ -90,6 +92,7 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
"ID",
|
"ID",
|
||||||
"Key",
|
"Key",
|
||||||
"Reusable",
|
"Reusable",
|
||||||
|
"Pre-Approved",
|
||||||
"Ephemeral",
|
"Ephemeral",
|
||||||
"Used",
|
"Used",
|
||||||
"Expiration",
|
"Expiration",
|
||||||
@ -115,6 +118,7 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
key.GetId(),
|
key.GetId(),
|
||||||
key.GetKey(),
|
key.GetKey(),
|
||||||
strconv.FormatBool(key.GetReusable()),
|
strconv.FormatBool(key.GetReusable()),
|
||||||
|
strconv.FormatBool(key.GetPreApproved()),
|
||||||
strconv.FormatBool(key.GetEphemeral()),
|
strconv.FormatBool(key.GetEphemeral()),
|
||||||
strconv.FormatBool(key.GetUsed()),
|
strconv.FormatBool(key.GetUsed()),
|
||||||
expiration,
|
expiration,
|
||||||
@ -147,12 +151,14 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||||
|
preApproved, _ := cmd.Flags().GetBool("pre-approved")
|
||||||
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
||||||
tags, _ := cmd.Flags().GetStringSlice("tags")
|
tags, _ := cmd.Flags().GetStringSlice("tags")
|
||||||
|
|
||||||
request := &v1.CreatePreAuthKeyRequest{
|
request := &v1.CreatePreAuthKeyRequest{
|
||||||
User: user,
|
User: user,
|
||||||
Reusable: reusable,
|
Reusable: reusable,
|
||||||
|
PreApproved: preApproved,
|
||||||
Ephemeral: ephemeral,
|
Ephemeral: ephemeral,
|
||||||
AclTags: tags,
|
AclTags: tags,
|
||||||
}
|
}
|
||||||
|
@ -395,3 +395,9 @@ logtail:
|
|||||||
# default static port 41641. This option is intended as a workaround for some buggy
|
# default static port 41641. This option is intended as a workaround for some buggy
|
||||||
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
|
# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information.
|
||||||
randomize_client_port: false
|
randomize_client_port: false
|
||||||
|
|
||||||
|
# Node management
|
||||||
|
# See https://tailscale.com/kb/1099/device-approval for more information.
|
||||||
|
node_management:
|
||||||
|
# Require new nodes to be approved by admins before they can access the network.
|
||||||
|
manual_approve_new_node: false
|
@ -137,6 +137,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
|
|||||||
app.db, err = db.NewHeadscaleDatabase(
|
app.db, err = db.NewHeadscaleDatabase(
|
||||||
cfg.Database,
|
cfg.Database,
|
||||||
cfg.BaseDomain,
|
cfg.BaseDomain,
|
||||||
|
cfg.NodeManagement,
|
||||||
registrationCache,
|
registrationCache,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -292,6 +292,11 @@ func (h *Headscale) handleAuthKey(
|
|||||||
|
|
||||||
nodeKey := registerRequest.NodeKey
|
nodeKey := registerRequest.NodeKey
|
||||||
|
|
||||||
|
nodeApproved := true
|
||||||
|
if h.cfg.NodeManagement.ManualApproveNewNode {
|
||||||
|
nodeApproved = pak.PreApproved
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -308,6 +313,10 @@ func (h *Headscale) handleAuthKey(
|
|||||||
node.AuthKeyID = ptr.To(pak.ID)
|
node.AuthKeyID = ptr.To(pak.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if node.Approved == false {
|
||||||
|
node.Approved = nodeApproved
|
||||||
|
}
|
||||||
|
|
||||||
node.Expiry = ®isterRequest.Expiry
|
node.Expiry = ®isterRequest.Expiry
|
||||||
node.User = pak.User
|
node.User = pak.User
|
||||||
node.UserID = pak.UserID
|
node.UserID = pak.UserID
|
||||||
@ -349,6 +358,7 @@ func (h *Headscale) handleAuthKey(
|
|||||||
User: pak.User,
|
User: pak.User,
|
||||||
MachineKey: machineKey,
|
MachineKey: machineKey,
|
||||||
RegisterMethod: util.RegisterMethodAuthKey,
|
RegisterMethod: util.RegisterMethodAuthKey,
|
||||||
|
Approved: nodeApproved,
|
||||||
Expiry: ®isterRequest.Expiry,
|
Expiry: ®isterRequest.Expiry,
|
||||||
NodeKey: nodeKey,
|
NodeKey: nodeKey,
|
||||||
LastSeen: &now,
|
LastSeen: &now,
|
||||||
@ -406,7 +416,7 @@ func (h *Headscale) handleAuthKey(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.MachineAuthorized = true
|
resp.MachineAuthorized = node.IsApproved()
|
||||||
resp.User = *pak.User.TailscaleUser()
|
resp.User = *pak.User.TailscaleUser()
|
||||||
// Provide LoginName when registering with pre-auth key
|
// Provide LoginName when registering with pre-auth key
|
||||||
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
|
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
|
||||||
@ -569,7 +579,7 @@ func (h *Headscale) handleNodeWithValidRegistration(
|
|||||||
Msg("Client is registered and we have the current NodeKey. All clear to /map")
|
Msg("Client is registered and we have the current NodeKey. All clear to /map")
|
||||||
|
|
||||||
resp.AuthURL = ""
|
resp.AuthURL = ""
|
||||||
resp.MachineAuthorized = true
|
resp.MachineAuthorized = node.IsApproved()
|
||||||
resp.User = *node.User.TailscaleUser()
|
resp.User = *node.User.TailscaleUser()
|
||||||
resp.Login = *node.User.TailscaleLogin()
|
resp.Login = *node.User.TailscaleLogin()
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ type HSDatabase struct {
|
|||||||
regCache *zcache.Cache[string, types.Node]
|
regCache *zcache.Cache[string, types.Node]
|
||||||
|
|
||||||
baseDomain string
|
baseDomain string
|
||||||
|
nodeManagement *types.NodeManagement
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): assemble this struct from toptions or something typed
|
// TODO(kradalby): assemble this struct from toptions or something typed
|
||||||
@ -51,6 +52,7 @@ type HSDatabase struct {
|
|||||||
func NewHeadscaleDatabase(
|
func NewHeadscaleDatabase(
|
||||||
cfg types.DatabaseConfig,
|
cfg types.DatabaseConfig,
|
||||||
baseDomain string,
|
baseDomain string,
|
||||||
|
nodeManagement types.NodeManagement,
|
||||||
regCache *zcache.Cache[string, types.Node],
|
regCache *zcache.Cache[string, types.Node],
|
||||||
) (*HSDatabase, error) {
|
) (*HSDatabase, error) {
|
||||||
dbConn, err := openDB(cfg)
|
dbConn, err := openDB(cfg)
|
||||||
@ -521,6 +523,48 @@ func NewHeadscaleDatabase(
|
|||||||
},
|
},
|
||||||
Rollback: func(db *gorm.DB) error { return nil },
|
Rollback: func(db *gorm.DB) error { return nil },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "202410071005",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
err = tx.AutoMigrate(&types.PreAuthKey{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.AutoMigrate(&types.Node{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx.Migrator().HasColumn(&types.Node{}, "approved") {
|
||||||
|
nodes := types.Nodes{}
|
||||||
|
if err := tx.Find(&nodes).Error; err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error accessing db")
|
||||||
|
}
|
||||||
|
|
||||||
|
for item, node := range nodes {
|
||||||
|
if node.IsApproved() == false {
|
||||||
|
err = tx.Model(nodes[item]).Updates(types.Node{
|
||||||
|
Approved: true,
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("hostname", node.Hostname).
|
||||||
|
Bool("approved", node.IsApproved()).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to add approval option to existing nodes during database migration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no node approved column in DB")
|
||||||
|
},
|
||||||
|
Rollback: func(db *gorm.DB) error { return nil },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -534,6 +578,7 @@ func NewHeadscaleDatabase(
|
|||||||
regCache: regCache,
|
regCache: regCache,
|
||||||
|
|
||||||
baseDomain: baseDomain,
|
baseDomain: baseDomain,
|
||||||
|
nodeManagement: &nodeManagement,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &db, err
|
return &db, err
|
||||||
|
@ -265,10 +265,10 @@ func isTailscaleReservedIP(ip netip.Addr) bool {
|
|||||||
// it will be added.
|
// it will be added.
|
||||||
// If a prefix type has been removed (IPv4 or IPv6), it
|
// If a prefix type has been removed (IPv4 or IPv6), it
|
||||||
// will remove the IPs in that family from the node.
|
// will remove the IPs in that family from the node.
|
||||||
func (db *HSDatabase) BackfillNodeIPs(i *IPAllocator) ([]string, error) {
|
func (hsdb *HSDatabase) BackfillNodeIPs(i *IPAllocator) ([]string, error) {
|
||||||
var err error
|
var err error
|
||||||
var ret []string
|
var ret []string
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = hsdb.Write(func(tx *gorm.DB) error {
|
||||||
if i == nil {
|
if i == nil {
|
||||||
return errors.New("backfilling IPs: ip allocator was nil")
|
return errors.New("backfilling IPs: ip allocator was nil")
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,9 @@ func ListPeers(tx *gorm.DB, nodeID types.NodeID) (types.Nodes, error) {
|
|||||||
Preload("AuthKey.User").
|
Preload("AuthKey.User").
|
||||||
Preload("User").
|
Preload("User").
|
||||||
Preload("Routes").
|
Preload("Routes").
|
||||||
Where("id <> ?",
|
Where("id <> ?", nodeID).
|
||||||
nodeID).Find(&nodes).Error; err != nil {
|
Where("approved = ?", true).
|
||||||
|
Find(&nodes).Error; err != nil {
|
||||||
return types.Nodes{}, err
|
return types.Nodes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +262,19 @@ func RenameNode(tx *gorm.DB,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hsdb *HSDatabase) NodeSetApprove(nodeID types.NodeID, approved bool) error {
|
||||||
|
return hsdb.Write(func(tx *gorm.DB) error {
|
||||||
|
return NodeSetApprove(tx, nodeID, approved)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeSetApprove takes a Node struct and a set approval option
|
||||||
|
func NodeSetApprove(tx *gorm.DB,
|
||||||
|
nodeID types.NodeID, approved bool,
|
||||||
|
) error {
|
||||||
|
return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("approved", approved).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry time.Time) error {
|
func (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry time.Time) error {
|
||||||
return hsdb.Write(func(tx *gorm.DB) error {
|
return hsdb.Write(func(tx *gorm.DB) error {
|
||||||
return NodeSetExpiry(tx, nodeID, expiry)
|
return NodeSetExpiry(tx, nodeID, expiry)
|
||||||
@ -328,6 +342,8 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
|
|||||||
ipv6 *netip.Addr,
|
ipv6 *netip.Addr,
|
||||||
) (*types.Node, error) {
|
) (*types.Node, error) {
|
||||||
return Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) {
|
return Write(hsdb.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||||
|
manualApprovedNode := hsdb.nodeManagement.ManualApproveNewNode
|
||||||
|
|
||||||
if node, ok := hsdb.regCache.Get(mkey.String()); ok {
|
if node, ok := hsdb.regCache.Get(mkey.String()); ok {
|
||||||
user, err := GetUserByID(tx, userID)
|
user, err := GetUserByID(tx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -341,6 +357,7 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
|
|||||||
Str("machine_key", mkey.ShortString()).
|
Str("machine_key", mkey.ShortString()).
|
||||||
Str("username", user.Username()).
|
Str("username", user.Username()).
|
||||||
Str("registrationMethod", registrationMethod).
|
Str("registrationMethod", registrationMethod).
|
||||||
|
Bool("manualApprovedNode", manualApprovedNode).
|
||||||
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
|
Str("expiresAt", fmt.Sprintf("%v", nodeExpiry)).
|
||||||
Msg("Registering node from API/CLI or auth callback")
|
Msg("Registering node from API/CLI or auth callback")
|
||||||
|
|
||||||
@ -354,6 +371,10 @@ func (hsdb *HSDatabase) RegisterNodeFromAuthCallback(
|
|||||||
node.User = *user
|
node.User = *user
|
||||||
node.RegisterMethod = registrationMethod
|
node.RegisterMethod = registrationMethod
|
||||||
|
|
||||||
|
if node.IsApproved() == false && manualApprovedNode == false {
|
||||||
|
node.Approved = true
|
||||||
|
}
|
||||||
|
|
||||||
if nodeExpiry != nil {
|
if nodeExpiry != nil {
|
||||||
node.Expiry = nodeExpiry
|
node.Expiry = nodeExpiry
|
||||||
}
|
}
|
||||||
@ -388,6 +409,7 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
|
|||||||
Str("machine_key", node.MachineKey.ShortString()).
|
Str("machine_key", node.MachineKey.ShortString()).
|
||||||
Str("node_key", node.NodeKey.ShortString()).
|
Str("node_key", node.NodeKey.ShortString()).
|
||||||
Str("user", node.User.Username()).
|
Str("user", node.User.Username()).
|
||||||
|
Bool("approved", node.IsApproved()).
|
||||||
Msg("Registering node")
|
Msg("Registering node")
|
||||||
|
|
||||||
// If the node exists and it already has IP(s), we just save it
|
// If the node exists and it already has IP(s), we just save it
|
||||||
@ -404,6 +426,7 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
|
|||||||
Str("machine_key", node.MachineKey.ShortString()).
|
Str("machine_key", node.MachineKey.ShortString()).
|
||||||
Str("node_key", node.NodeKey.ShortString()).
|
Str("node_key", node.NodeKey.ShortString()).
|
||||||
Str("user", node.User.Username()).
|
Str("user", node.User.Username()).
|
||||||
|
Bool("approved", node.IsApproved()).
|
||||||
Msg("Node authorized again")
|
Msg("Node authorized again")
|
||||||
|
|
||||||
return &node, nil
|
return &node, nil
|
||||||
@ -428,6 +451,7 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
|
|||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Str("node", node.Hostname).
|
Str("node", node.Hostname).
|
||||||
|
Bool("approved", node.IsApproved()).
|
||||||
Msg("Node registered with the database")
|
Msg("Node registered with the database")
|
||||||
|
|
||||||
return &node, nil
|
return &node, nil
|
||||||
|
@ -25,12 +25,13 @@ var (
|
|||||||
func (hsdb *HSDatabase) CreatePreAuthKey(
|
func (hsdb *HSDatabase) CreatePreAuthKey(
|
||||||
uid types.UserID,
|
uid types.UserID,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
|
preApproved bool,
|
||||||
ephemeral bool,
|
ephemeral bool,
|
||||||
expiration *time.Time,
|
expiration *time.Time,
|
||||||
aclTags []string,
|
aclTags []string,
|
||||||
) (*types.PreAuthKey, error) {
|
) (*types.PreAuthKey, error) {
|
||||||
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
|
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
|
||||||
return CreatePreAuthKey(tx, uid, reusable, ephemeral, expiration, aclTags)
|
return CreatePreAuthKey(tx, uid, reusable, preApproved, ephemeral, expiration, aclTags)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ func CreatePreAuthKey(
|
|||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
uid types.UserID,
|
uid types.UserID,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
|
preApproved bool,
|
||||||
ephemeral bool,
|
ephemeral bool,
|
||||||
expiration *time.Time,
|
expiration *time.Time,
|
||||||
aclTags []string,
|
aclTags []string,
|
||||||
@ -74,6 +76,7 @@ func CreatePreAuthKey(
|
|||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
User: *user,
|
User: *user,
|
||||||
Reusable: reusable,
|
Reusable: reusable,
|
||||||
|
PreApproved: preApproved,
|
||||||
Ephemeral: ephemeral,
|
Ephemeral: ephemeral,
|
||||||
CreatedAt: &now,
|
CreatedAt: &now,
|
||||||
Expiration: expiration,
|
Expiration: expiration,
|
||||||
|
@ -158,6 +158,7 @@ func (api headscaleV1APIServer) CreatePreAuthKey(
|
|||||||
preAuthKey, err := api.h.db.CreatePreAuthKey(
|
preAuthKey, err := api.h.db.CreatePreAuthKey(
|
||||||
types.UserID(user.ID),
|
types.UserID(user.ID),
|
||||||
request.GetReusable(),
|
request.GetReusable(),
|
||||||
|
request.GetPreApproved(),
|
||||||
request.GetEphemeral(),
|
request.GetEphemeral(),
|
||||||
&expiration,
|
&expiration,
|
||||||
request.AclTags,
|
request.AclTags,
|
||||||
@ -362,6 +363,54 @@ func (api headscaleV1APIServer) DeleteNode(
|
|||||||
return &v1.DeleteNodeResponse{}, nil
|
return &v1.DeleteNodeResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api headscaleV1APIServer) ApproveNode(
|
||||||
|
ctx context.Context,
|
||||||
|
request *v1.ApproveNodeRequest,
|
||||||
|
) (*v1.ApproveNodeResponse, error) {
|
||||||
|
node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||||
|
if err := db.NodeSetApprove(
|
||||||
|
tx,
|
||||||
|
types.NodeID(request.GetNodeId()),
|
||||||
|
true,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.GetNodeByID(tx, types.NodeID(request.GetNodeId()))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = types.NotifyCtx(ctx, "cli-approved-node-self", node.Hostname)
|
||||||
|
api.h.nodeNotifier.NotifyByNodeID(
|
||||||
|
ctx,
|
||||||
|
types.StateUpdate{
|
||||||
|
Type: types.StateSelfUpdate,
|
||||||
|
ChangeNodes: []types.NodeID{node.ID},
|
||||||
|
},
|
||||||
|
node.ID)
|
||||||
|
|
||||||
|
ctx = types.NotifyCtx(ctx, "cli-approved-node-peers", node.Hostname)
|
||||||
|
api.h.nodeNotifier.NotifyWithIgnore(
|
||||||
|
ctx,
|
||||||
|
types.StateUpdate{
|
||||||
|
Type: types.StatePeerChangedPatch,
|
||||||
|
ChangePatches: []*tailcfg.PeerChange{
|
||||||
|
{
|
||||||
|
NodeID: node.ID.NodeID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
node.ID)
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Str("node", node.Hostname).
|
||||||
|
Msg("node approved")
|
||||||
|
|
||||||
|
return &v1.ApproveNodeResponse{Node: node.Proto()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (api headscaleV1APIServer) ExpireNode(
|
func (api headscaleV1APIServer) ExpireNode(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *v1.ExpireNodeRequest,
|
request *v1.ExpireNodeRequest,
|
||||||
|
@ -110,7 +110,7 @@ func tailNode(
|
|||||||
|
|
||||||
PrimaryRoutes: primaryPrefixes,
|
PrimaryRoutes: primaryPrefixes,
|
||||||
|
|
||||||
MachineAuthorized: !node.IsExpired(),
|
MachineAuthorized: node.IsApproved(),
|
||||||
Expired: node.IsExpired(),
|
Expired: node.IsExpired(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,8 @@ type Config struct {
|
|||||||
Policy PolicyConfig
|
Policy PolicyConfig
|
||||||
|
|
||||||
Tuning Tuning
|
Tuning Tuning
|
||||||
|
|
||||||
|
NodeManagement NodeManagement
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSConfig struct {
|
type DNSConfig struct {
|
||||||
@ -214,6 +216,10 @@ type Tuning struct {
|
|||||||
NodeMapSessionBufferedChanSize int
|
NodeMapSessionBufferedChanSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeManagement struct {
|
||||||
|
ManualApproveNewNode bool
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig prepares and loads the Headscale configuration into Viper.
|
// LoadConfig prepares and loads the Headscale configuration into Viper.
|
||||||
// This means it sets the default values, reads the configuration file and
|
// This means it sets the default values, reads the configuration file and
|
||||||
// environment variables, and handles deprecated configuration options.
|
// environment variables, and handles deprecated configuration options.
|
||||||
@ -292,6 +298,8 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
viper.SetDefault("tuning.batch_change_delay", "800ms")
|
viper.SetDefault("tuning.batch_change_delay", "800ms")
|
||||||
viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30)
|
viper.SetDefault("tuning.node_mapsession_buffered_chan_size", 30)
|
||||||
|
|
||||||
|
viper.SetDefault("node_management.manual_approve_new_node", false)
|
||||||
|
|
||||||
viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential))
|
viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential))
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
@ -749,6 +757,14 @@ func prefixV6() (*netip.Prefix, error) {
|
|||||||
return &prefixV6, nil
|
return &prefixV6, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nodeManagementConfig() NodeManagement {
|
||||||
|
manualApproveNewNode := viper.GetBool("node_management.manual_approve_new_node")
|
||||||
|
|
||||||
|
return NodeManagement{
|
||||||
|
ManualApproveNewNode: manualApproveNewNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadCLIConfig returns the needed configuration for the CLI client
|
// LoadCLIConfig returns the needed configuration for the CLI client
|
||||||
// of Headscale to connect to a Headscale server.
|
// of Headscale to connect to a Headscale server.
|
||||||
func LoadCLIConfig() (*Config, error) {
|
func LoadCLIConfig() (*Config, error) {
|
||||||
@ -845,6 +861,8 @@ func LoadServerConfig() (*Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeManagement := nodeManagementConfig()
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
ServerURL: serverURL,
|
ServerURL: serverURL,
|
||||||
Addr: viper.GetString("listen_addr"),
|
Addr: viper.GetString("listen_addr"),
|
||||||
@ -936,6 +954,8 @@ func LoadServerConfig() (*Config, error) {
|
|||||||
"tuning.node_mapsession_buffered_chan_size",
|
"tuning.node_mapsession_buffered_chan_size",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
NodeManagement: nodeManagement,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,7 @@ type Node struct {
|
|||||||
|
|
||||||
LastSeen *time.Time
|
LastSeen *time.Time
|
||||||
Expiry *time.Time
|
Expiry *time.Time
|
||||||
|
Approved bool `sql:"DEFAULT:false"`
|
||||||
|
|
||||||
Routes []Route `gorm:"constraint:OnDelete:CASCADE;"`
|
Routes []Route `gorm:"constraint:OnDelete:CASCADE;"`
|
||||||
|
|
||||||
@ -114,6 +115,11 @@ func (node Node) IsExpired() bool {
|
|||||||
return time.Since(*node.Expiry) > 0
|
return time.Since(*node.Expiry) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsApproved returns whether the node is approved.
|
||||||
|
func (node Node) IsApproved() bool {
|
||||||
|
return node.Approved == true
|
||||||
|
}
|
||||||
|
|
||||||
// IsEphemeral returns if the node is registered as an Ephemeral node.
|
// IsEphemeral returns if the node is registered as an Ephemeral node.
|
||||||
// https://tailscale.com/kb/1111/ephemeral-nodes/
|
// https://tailscale.com/kb/1111/ephemeral-nodes/
|
||||||
func (node *Node) IsEphemeral() bool {
|
func (node *Node) IsEphemeral() bool {
|
||||||
@ -249,6 +255,7 @@ func (node *Node) Proto() *v1.Node {
|
|||||||
ForcedTags: node.ForcedTags,
|
ForcedTags: node.ForcedTags,
|
||||||
|
|
||||||
RegisterMethod: node.RegisterMethodToV1Enum(),
|
RegisterMethod: node.RegisterMethodToV1Enum(),
|
||||||
|
Approved: node.Approved,
|
||||||
|
|
||||||
CreatedAt: timestamppb.New(node.CreatedAt),
|
CreatedAt: timestamppb.New(node.CreatedAt),
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ type PreAuthKey struct {
|
|||||||
UserID uint
|
UserID uint
|
||||||
User User `gorm:"constraint:OnDelete:CASCADE;"`
|
User User `gorm:"constraint:OnDelete:CASCADE;"`
|
||||||
Reusable bool
|
Reusable bool
|
||||||
|
PreApproved bool `gorm:"default:false"`
|
||||||
Ephemeral bool `gorm:"default:false"`
|
Ephemeral bool `gorm:"default:false"`
|
||||||
Used bool `gorm:"default:false"`
|
Used bool `gorm:"default:false"`
|
||||||
Tags []string `gorm:"serializer:json"`
|
Tags []string `gorm:"serializer:json"`
|
||||||
@ -30,6 +31,7 @@ func (key *PreAuthKey) Proto() *v1.PreAuthKey {
|
|||||||
Id: strconv.FormatUint(key.ID, util.Base10),
|
Id: strconv.FormatUint(key.ID, util.Base10),
|
||||||
Key: key.Key,
|
Key: key.Key,
|
||||||
Ephemeral: key.Ephemeral,
|
Ephemeral: key.Ephemeral,
|
||||||
|
PreApproved: key.PreApproved,
|
||||||
Reusable: key.Reusable,
|
Reusable: key.Reusable,
|
||||||
Used: key.Used,
|
Used: key.Used,
|
||||||
AclTags: key.Tags,
|
AclTags: key.Tags,
|
||||||
|
Loading…
Reference in New Issue
Block a user