2025-07-28 11:15:53 +02:00
|
|
|
//go:generate buf generate --template ../buf.gen.yaml -o .. ../proto
|
|
|
|
|
|
2022-11-05 16:07:22 +08:00
|
|
|
// nolint
|
2023-05-10 09:24:05 +02:00
|
|
|
package hscontrol
|
2021-10-26 20:42:20 +00:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2024-04-17 07:03:06 +02:00
|
|
|
"errors"
|
2024-08-30 16:58:29 +02:00
|
|
|
"fmt"
|
2024-07-18 11:08:25 +05:30
|
|
|
"io"
|
2025-02-26 07:22:55 -08:00
|
|
|
"net/netip"
|
2024-07-18 11:08:25 +05:30
|
|
|
"os"
|
2025-02-26 07:22:55 -08:00
|
|
|
"slices"
|
2024-02-18 19:31:29 +01:00
|
|
|
"sort"
|
2022-05-13 09:47:34 +02:00
|
|
|
"strings"
|
2021-11-04 22:19:27 +00:00
|
|
|
"time"
|
2021-10-26 20:42:20 +00:00
|
|
|
|
2021-11-04 22:19:27 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2022-05-13 10:17:52 +02:00
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
|
"google.golang.org/grpc/status"
|
2024-07-18 11:08:25 +05:30
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
2024-02-08 17:28:19 +01:00
|
|
|
"gorm.io/gorm"
|
2025-02-26 07:22:55 -08:00
|
|
|
"tailscale.com/net/tsaddr"
|
2021-11-04 22:19:27 +00:00
|
|
|
"tailscale.com/tailcfg"
|
2022-11-05 16:07:22 +08:00
|
|
|
"tailscale.com/types/key"
|
2025-07-05 23:30:47 +02:00
|
|
|
"tailscale.com/types/views"
|
2024-02-12 16:01:21 +05:30
|
|
|
|
|
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
policy/v2: evaluate the tests block on user-initiated writes
v2 silently dropped policy.tests, so a policy that contradicted its
own assertions still applied. Resolve src/dst via the existing Alias
machinery, walk the compiled global filter rules (acls and grants
both contribute), and run on every user-write boundary: SetPolicy,
the file watcher, and `headscale policy check`. A failing test
rejects the write before it mutates live state.
Boot-time reload skips evaluation; an already-stored policy that
references a deleted user shouldn't lock the server out.
`headscale policy check` is a thin frontend for the new CheckPolicy
gRPC method. The server-side handler builds a fresh PolicyManager
from the request bytes and the state's live users/nodes, runs
SetPolicy on the sandbox so the tests block executes, and returns
the result through gRPC status. No persistence, no policy_mode
coupling. --bypass-grpc-and-access-database-directly opens the DB
directly when the server is not running.
cmd/headscale/cli/root.go no longer special-cases `policy check` in
init() (the early return from PR #2580 broke --config registration
and viper priming for --bypass).
integration/cli_policy_test.go covers policy_mode={file,database} x
fixture={acl-only, acl+passing-tests, acl+failing-tests} x
bypass={false,true} = 12 rows.
Updates #1803
Co-authored-by: Janis Jansons <janhouse@gmail.com>
2026-04-29 14:27:12 +00:00
|
|
|
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
2025-05-27 16:27:16 +02:00
|
|
|
"github.com/juanfont/headscale/hscontrol/state"
|
2024-02-12 16:01:21 +05:30
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
|
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
2026-02-05 11:01:41 +00:00
|
|
|
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
2021-10-26 20:42:20 +00:00
|
|
|
)
|
|
|
|
|
|
2021-11-04 22:19:27 +00:00
|
|
|
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
|
|
|
|
|
v1.UnimplementedHeadscaleServiceServer
|
2021-10-26 20:42:20 +00:00
|
|
|
h *Headscale
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-04 22:19:27 +00:00
|
|
|
func newHeadscaleV1APIServer(h *Headscale) v1.HeadscaleServiceServer {
|
2021-10-26 20:42:20 +00:00
|
|
|
return headscaleV1APIServer{
|
|
|
|
|
h: h,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
func (api headscaleV1APIServer) CreateUser(
|
2021-10-29 16:44:32 +00:00
|
|
|
ctx context.Context,
|
2023-01-17 17:43:44 +01:00
|
|
|
request *v1.CreateUserRequest,
|
|
|
|
|
) (*v1.CreateUserResponse, error) {
|
2024-12-19 13:10:10 +01:00
|
|
|
newUser := types.User{
|
|
|
|
|
Name: request.GetName(),
|
|
|
|
|
DisplayName: request.GetDisplayName(),
|
|
|
|
|
Email: request.GetEmail(),
|
|
|
|
|
ProfilePicURL: request.GetPictureUrl(),
|
|
|
|
|
}
|
2025-05-27 16:27:16 +02:00
|
|
|
user, policyChanged, err := api.h.state.CreateUser(newUser)
|
2021-10-26 20:42:20 +00:00
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return nil, status.Errorf(codes.Internal, "creating user: %s", err)
|
2021-10-26 20:42:20 +00:00
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// [state.State.CreateUser] returns a policy change response if the user creation affected policy.
|
2025-12-15 14:36:21 +00:00
|
|
|
// This triggers a full policy re-evaluation for all connected nodes.
|
|
|
|
|
api.h.Change(policyChanged)
|
2025-07-28 11:15:53 +02:00
|
|
|
|
2023-05-21 19:37:59 +03:00
|
|
|
return &v1.CreateUserResponse{User: user.Proto()}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
func (api headscaleV1APIServer) RenameUser(
|
2021-11-04 22:19:27 +00:00
|
|
|
ctx context.Context,
|
2023-01-17 17:43:44 +01:00
|
|
|
request *v1.RenameUserRequest,
|
|
|
|
|
) (*v1.RenameUserResponse, error) {
|
2025-05-27 16:27:16 +02:00
|
|
|
oldUser, err := api.h.state.GetUserByID(types.UserID(request.GetOldId()))
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
_, c, err := api.h.state.RenameUser(types.UserID(oldUser.ID), request.GetNewName())
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
// Send policy update notifications if needed
|
2025-07-05 23:30:47 +02:00
|
|
|
api.h.Change(c)
|
2025-05-27 16:27:16 +02:00
|
|
|
|
|
|
|
|
newUser, err := api.h.state.GetUserByName(request.GetNewName())
|
2024-11-17 19:40:06 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.RenameUserResponse{User: newUser.Proto()}, nil
|
2021-10-29 16:44:32 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
func (api headscaleV1APIServer) DeleteUser(
|
2021-10-29 16:44:32 +00:00
|
|
|
ctx context.Context,
|
2023-01-17 17:43:44 +01:00
|
|
|
request *v1.DeleteUserRequest,
|
|
|
|
|
) (*v1.DeleteUserResponse, error) {
|
2025-05-27 16:27:16 +02:00
|
|
|
user, err := api.h.state.GetUserByID(types.UserID(request.GetId()))
|
2024-11-17 19:40:06 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 15:31:59 +00:00
|
|
|
policyChanged, err := api.h.state.DeleteUser(types.UserID(user.ID))
|
2021-10-29 16:44:32 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Use the change returned from [state.State.DeleteUser] which includes proper policy updates
|
2026-01-09 15:31:59 +00:00
|
|
|
api.h.Change(policyChanged)
|
2025-07-28 11:15:53 +02:00
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
return &v1.DeleteUserResponse{}, nil
|
2021-10-29 16:44:32 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
func (api headscaleV1APIServer) ListUsers(
|
2021-10-29 16:44:32 +00:00
|
|
|
ctx context.Context,
|
2023-01-17 17:43:44 +01:00
|
|
|
request *v1.ListUsersRequest,
|
|
|
|
|
) (*v1.ListUsersResponse, error) {
|
2024-12-10 16:23:55 +01:00
|
|
|
var err error
|
|
|
|
|
var users []types.User
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case request.GetName() != "":
|
2025-05-27 16:27:16 +02:00
|
|
|
users, err = api.h.state.ListUsersWithFilter(&types.User{Name: request.GetName()})
|
2024-12-10 16:23:55 +01:00
|
|
|
case request.GetEmail() != "":
|
2025-05-27 16:27:16 +02:00
|
|
|
users, err = api.h.state.ListUsersWithFilter(&types.User{Email: request.GetEmail()})
|
2024-12-10 16:23:55 +01:00
|
|
|
case request.GetId() != 0:
|
2025-05-27 16:27:16 +02:00
|
|
|
users, err = api.h.state.ListUsersWithFilter(&types.User{Model: gorm.Model{ID: uint(request.GetId())}})
|
2024-12-10 16:23:55 +01:00
|
|
|
default:
|
2025-05-27 16:27:16 +02:00
|
|
|
users, err = api.h.state.ListAllUsers()
|
2024-12-10 16:23:55 +01:00
|
|
|
}
|
2021-10-29 16:44:32 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
response := make([]*v1.User, len(users))
|
|
|
|
|
for index, user := range users {
|
2023-05-21 19:37:59 +03:00
|
|
|
response[index] = user.Proto()
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-18 19:31:29 +01:00
|
|
|
sort.Slice(response, func(i, j int) bool {
|
|
|
|
|
return response[i].Id < response[j].Id
|
|
|
|
|
})
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
return &v1.ListUsersResponse{Users: response}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api headscaleV1APIServer) CreatePreAuthKey(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.CreatePreAuthKeyRequest,
|
|
|
|
|
) (*v1.CreatePreAuthKeyResponse, error) {
|
2021-11-08 08:02:01 +00:00
|
|
|
var expiration time.Time
|
|
|
|
|
if request.GetExpiration() != nil {
|
|
|
|
|
expiration = request.GetExpiration().AsTime()
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-23 17:58:06 +10:00
|
|
|
for _, tag := range request.AclTags {
|
|
|
|
|
err := validateTag(tag)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return &v1.CreatePreAuthKeyResponse{
|
|
|
|
|
PreAuthKey: nil,
|
|
|
|
|
}, status.Error(codes.InvalidArgument, err.Error())
|
2022-08-25 20:43:15 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 15:31:28 +01:00
|
|
|
var userID *types.UserID
|
|
|
|
|
if request.GetUser() != 0 {
|
|
|
|
|
user, err := api.h.state.GetUserByID(types.UserID(request.GetUser()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
userID = user.TypedID()
|
2024-11-17 19:40:06 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
preAuthKey, err := api.h.state.CreatePreAuthKey(
|
2026-01-07 15:31:28 +01:00
|
|
|
userID,
|
2021-11-08 20:49:03 +00:00
|
|
|
request.GetReusable(),
|
2021-11-04 22:19:27 +00:00
|
|
|
request.GetEphemeral(),
|
|
|
|
|
&expiration,
|
2022-08-25 20:03:38 +10:00
|
|
|
request.AclTags,
|
2021-11-04 22:19:27 +00:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-21 19:37:59 +03:00
|
|
|
return &v1.CreatePreAuthKeyResponse{PreAuthKey: preAuthKey.Proto()}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api headscaleV1APIServer) ExpirePreAuthKey(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.ExpirePreAuthKeyRequest,
|
|
|
|
|
) (*v1.ExpirePreAuthKeyResponse, error) {
|
2026-01-07 13:36:51 +01:00
|
|
|
err := api.h.state.ExpirePreAuthKey(request.GetId())
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.ExpirePreAuthKeyResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 15:51:01 +01:00
|
|
|
func (api headscaleV1APIServer) DeletePreAuthKey(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.DeletePreAuthKeyRequest,
|
|
|
|
|
) (*v1.DeletePreAuthKeyResponse, error) {
|
2026-01-07 13:36:51 +01:00
|
|
|
err := api.h.state.DeletePreAuthKey(request.GetId())
|
2025-11-30 15:51:01 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.DeletePreAuthKeyResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-04 22:19:27 +00:00
|
|
|
func (api headscaleV1APIServer) ListPreAuthKeys(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.ListPreAuthKeysRequest,
|
|
|
|
|
) (*v1.ListPreAuthKeysResponse, error) {
|
2026-01-07 13:36:51 +01:00
|
|
|
preAuthKeys, err := api.h.state.ListPreAuthKeys()
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response := make([]*v1.PreAuthKey, len(preAuthKeys))
|
|
|
|
|
for index, key := range preAuthKeys {
|
2023-05-21 19:37:59 +03:00
|
|
|
response[index] = key.Proto()
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-18 19:31:29 +01:00
|
|
|
sort.Slice(response, func(i, j int) bool {
|
|
|
|
|
return response[i].Id < response[j].Id
|
|
|
|
|
})
|
|
|
|
|
|
2021-11-04 22:19:27 +00:00
|
|
|
return &v1.ListPreAuthKeysResponse{PreAuthKeys: response}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) RegisterNode(
|
2021-11-04 22:19:27 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.RegisterNodeRequest,
|
|
|
|
|
) (*v1.RegisterNodeResponse, error) {
|
2025-11-12 09:36:36 -06:00
|
|
|
// Generate ephemeral registration key for tracking this registration flow in logs
|
|
|
|
|
registrationKey, err := util.GenerateRegistrationKey()
|
|
|
|
|
if err != nil {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Warn().Err(err).Msg("failed to generate registration key")
|
2025-11-12 09:36:36 -06:00
|
|
|
registrationKey = "" // Continue without key if generation fails
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-13 08:36:45 +00:00
|
|
|
log.Trace().
|
2025-09-05 16:32:46 +02:00
|
|
|
Caller().
|
2026-02-05 11:01:41 +00:00
|
|
|
Str(zf.UserName, request.GetUser()).
|
|
|
|
|
Str(zf.RegistrationID, request.GetKey()).
|
|
|
|
|
Str(zf.RegistrationKey, registrationKey).
|
|
|
|
|
Msg("registering node")
|
2022-02-27 18:42:43 +01:00
|
|
|
|
2026-02-24 18:48:57 +00:00
|
|
|
registrationId, err := types.AuthIDFromString(request.GetKey())
|
2023-11-19 22:37:04 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
user, err := api.h.state.GetUserByName(request.GetUser())
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("looking up user: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
node, nodeChange, err := api.h.state.HandleNodeFromAuthPath(
|
2025-01-26 22:20:11 +01:00
|
|
|
registrationId,
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
types.UserID(user.ID),
|
|
|
|
|
nil,
|
|
|
|
|
util.RegisterMethodCLI,
|
|
|
|
|
)
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
2025-11-12 09:36:36 -06:00
|
|
|
log.Error().
|
2026-02-05 11:01:41 +00:00
|
|
|
Str(zf.RegistrationKey, registrationKey).
|
2025-11-12 09:36:36 -06:00
|
|
|
Err(err).
|
2026-02-05 11:01:41 +00:00
|
|
|
Msg("failed to register node")
|
2021-11-04 22:19:27 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 09:36:36 -06:00
|
|
|
log.Info().
|
2026-02-05 11:01:41 +00:00
|
|
|
Str(zf.RegistrationKey, registrationKey).
|
|
|
|
|
EmbedObject(node).
|
|
|
|
|
Msg("node registered successfully")
|
2025-11-12 09:36:36 -06:00
|
|
|
|
2025-05-01 08:05:42 +03:00
|
|
|
// This is a bit of a back and forth, but we have a bit of a chicken and egg
|
|
|
|
|
// dependency here.
|
|
|
|
|
// Because the way the policy manager works, we need to have the node
|
|
|
|
|
// in the database, then add it to the policy manager and then we can
|
|
|
|
|
// approve the route. This means we get this dance where the node is
|
|
|
|
|
// first added to the database, then we add it to the policy manager via
|
2025-05-27 16:27:16 +02:00
|
|
|
// SaveNode (which automatically updates the policy manager) and then we can auto approve the routes.
|
2025-05-01 08:05:42 +03:00
|
|
|
// As that only approves the struct object, we need to save it again and
|
|
|
|
|
// ensure we send an update.
|
|
|
|
|
// This works, but might be another good candidate for doing some sort of
|
|
|
|
|
// eventbus.
|
2025-11-30 19:02:15 +01:00
|
|
|
routeChange, err := api.h.state.AutoApproveRoutes(node)
|
2025-05-27 16:27:16 +02:00
|
|
|
if err != nil {
|
2025-11-30 19:02:15 +01:00
|
|
|
return nil, fmt.Errorf("auto approving routes: %w", err)
|
2025-05-01 08:05:42 +03:00
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Send both changes. Empty changes are ignored by [Headscale.Change].
|
2025-11-30 19:02:15 +01:00
|
|
|
api.h.Change(nodeChange, routeChange)
|
2024-11-26 15:16:06 +01:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
return &v1.RegisterNodeResponse{Node: node.Proto()}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) GetNode(
|
2021-11-04 22:19:27 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.GetNodeRequest,
|
|
|
|
|
) (*v1.GetNodeResponse, error) {
|
2025-07-05 23:30:47 +02:00
|
|
|
node, ok := api.h.state.GetNodeByID(types.NodeID(request.GetNodeId()))
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, status.Errorf(codes.NotFound, "node not found")
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-09 18:09:24 +01:00
|
|
|
resp := node.Proto()
|
|
|
|
|
|
|
|
|
|
return &v1.GetNodeResponse{Node: resp}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2022-04-25 21:16:14 +02:00
|
|
|
func (api headscaleV1APIServer) SetTags(
|
2022-04-15 13:11:41 +02:00
|
|
|
ctx context.Context,
|
2022-04-25 21:16:14 +02:00
|
|
|
request *v1.SetTagsRequest,
|
|
|
|
|
) (*v1.SetTagsResponse, error) {
|
2025-12-02 12:01:25 +01:00
|
|
|
// Validate tags not empty - tagged nodes must have at least one tag
|
|
|
|
|
if len(request.GetTags()) == 0 {
|
|
|
|
|
return &v1.SetTagsResponse{
|
2026-01-07 14:07:45 +01:00
|
|
|
Node: nil,
|
|
|
|
|
}, status.Error(
|
|
|
|
|
codes.InvalidArgument,
|
|
|
|
|
"cannot remove all tags from a node - tagged nodes must have at least one tag",
|
|
|
|
|
)
|
2025-12-02 12:01:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate tag format
|
2022-05-13 09:47:34 +02:00
|
|
|
for _, tag := range request.GetTags() {
|
2022-07-25 11:25:20 +02:00
|
|
|
err := validateTag(tag)
|
|
|
|
|
if err != nil {
|
2024-02-08 17:28:19 +01:00
|
|
|
return nil, err
|
2022-05-13 09:47:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// User XOR Tags: nodes are either tagged or user-owned, never both.
|
|
|
|
|
// Setting tags on a user-owned node converts it to a tagged node.
|
|
|
|
|
// Once tagged, a node cannot be converted back to user-owned.
|
|
|
|
|
_, found := api.h.state.GetNodeByID(types.NodeID(request.GetNodeId()))
|
|
|
|
|
if !found {
|
|
|
|
|
return &v1.SetTagsResponse{
|
|
|
|
|
Node: nil,
|
|
|
|
|
}, status.Error(codes.NotFound, "node not found")
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
node, nodeChange, err := api.h.state.SetNodeTags(types.NodeID(request.GetNodeId()), request.GetTags())
|
2022-05-13 11:09:28 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return &v1.SetTagsResponse{
|
2023-09-24 13:42:05 +02:00
|
|
|
Node: nil,
|
2024-02-08 17:28:19 +01:00
|
|
|
}, status.Error(codes.InvalidArgument, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
api.h.Change(nodeChange)
|
2022-04-15 13:11:41 +02:00
|
|
|
|
2022-04-25 21:16:14 +02:00
|
|
|
log.Trace().
|
2025-07-05 23:30:47 +02:00
|
|
|
Caller().
|
2026-02-05 11:01:41 +00:00
|
|
|
EmbedObject(node).
|
2022-04-25 21:16:14 +02:00
|
|
|
Strs("tags", request.GetTags()).
|
2026-02-05 11:01:41 +00:00
|
|
|
Msg("changing tags of node")
|
2022-04-15 13:11:41 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
return &v1.SetTagsResponse{Node: node.Proto()}, nil
|
2022-04-15 13:11:41 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
func (api headscaleV1APIServer) SetApprovedRoutes(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.SetApprovedRoutesRequest,
|
|
|
|
|
) (*v1.SetApprovedRoutesResponse, error) {
|
2025-07-05 23:30:47 +02:00
|
|
|
log.Debug().
|
|
|
|
|
Caller().
|
2026-02-05 11:01:41 +00:00
|
|
|
Uint64(zf.NodeID, request.GetNodeId()).
|
2025-07-05 23:30:47 +02:00
|
|
|
Strs("requestedRoutes", request.GetRoutes()).
|
|
|
|
|
Msg("gRPC SetApprovedRoutes called")
|
|
|
|
|
|
|
|
|
|
var newApproved []netip.Prefix
|
2025-02-26 07:22:55 -08:00
|
|
|
for _, route := range request.GetRoutes() {
|
|
|
|
|
prefix, err := netip.ParsePrefix(route)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parsing route: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the prefix is an exit route, add both. The client expect both
|
|
|
|
|
// to annotate the node as an exit node.
|
|
|
|
|
if prefix == tsaddr.AllIPv4() || prefix == tsaddr.AllIPv6() {
|
2025-07-05 23:30:47 +02:00
|
|
|
newApproved = append(newApproved, tsaddr.AllIPv4(), tsaddr.AllIPv6())
|
2025-02-26 07:22:55 -08:00
|
|
|
} else {
|
2025-07-05 23:30:47 +02:00
|
|
|
newApproved = append(newApproved, prefix)
|
2025-02-26 07:22:55 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 21:39:35 +00:00
|
|
|
slices.SortFunc(newApproved, netip.Prefix.Compare)
|
2025-07-05 23:30:47 +02:00
|
|
|
newApproved = slices.Compact(newApproved)
|
2025-02-26 07:22:55 -08:00
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
node, nodeChange, err := api.h.state.SetApprovedRoutes(types.NodeID(request.GetNodeId()), newApproved)
|
2025-02-26 07:22:55 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Always propagate node changes from [state.State.SetApprovedRoutes]
|
2025-07-28 11:15:53 +02:00
|
|
|
api.h.Change(nodeChange)
|
|
|
|
|
|
2025-03-28 13:22:15 +01:00
|
|
|
proto := node.Proto()
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Populate [types.Node.SubnetRoutes] with [tailcfg.Node.PrimaryRoutes] to ensure it includes only the
|
2025-07-05 23:30:47 +02:00
|
|
|
// routes that are actively served from the node (per architectural requirement in types/node.go)
|
|
|
|
|
primaryRoutes := api.h.state.GetNodePrimaryRoutes(node.ID())
|
|
|
|
|
proto.SubnetRoutes = util.PrefixesToString(primaryRoutes)
|
|
|
|
|
|
|
|
|
|
log.Debug().
|
|
|
|
|
Caller().
|
2026-02-05 09:44:23 +00:00
|
|
|
EmbedObject(node).
|
2025-07-05 23:30:47 +02:00
|
|
|
Strs("approvedRoutes", util.PrefixesToString(node.ApprovedRoutes().AsSlice())).
|
|
|
|
|
Strs("primaryRoutes", util.PrefixesToString(primaryRoutes)).
|
|
|
|
|
Strs("finalSubnetRoutes", proto.SubnetRoutes).
|
|
|
|
|
Msg("gRPC SetApprovedRoutes completed")
|
2025-03-28 13:22:15 +01:00
|
|
|
|
|
|
|
|
return &v1.SetApprovedRoutesResponse{Node: proto}, nil
|
2025-02-26 07:22:55 -08:00
|
|
|
}
|
|
|
|
|
|
2022-07-25 11:25:20 +02:00
|
|
|
func validateTag(tag string) error {
|
|
|
|
|
if strings.Index(tag, "tag:") != 0 {
|
2024-04-21 22:53:50 +08:00
|
|
|
return errors.New("tag must start with the string 'tag:'")
|
2022-07-25 11:25:20 +02:00
|
|
|
}
|
|
|
|
|
if strings.ToLower(tag) != tag {
|
2024-04-21 22:53:50 +08:00
|
|
|
return errors.New("tag should be lowercase")
|
2022-07-25 11:25:20 +02:00
|
|
|
}
|
|
|
|
|
if len(strings.Fields(tag)) > 1 {
|
2026-02-05 16:29:54 +00:00
|
|
|
return errors.New("tags must not contain spaces")
|
2022-07-25 11:25:20 +02:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) DeleteNode(
|
2021-11-04 22:19:27 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.DeleteNodeRequest,
|
|
|
|
|
) (*v1.DeleteNodeResponse, error) {
|
2025-07-05 23:30:47 +02:00
|
|
|
node, ok := api.h.state.GetNodeByID(types.NodeID(request.GetNodeId()))
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, status.Errorf(codes.NotFound, "node not found")
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
nodeChange, err := api.h.state.DeleteNode(node)
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
api.h.Change(nodeChange)
|
2024-02-23 10:59:24 +01:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
return &v1.DeleteNodeResponse{}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) ExpireNode(
|
2021-11-21 13:40:19 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.ExpireNodeRequest,
|
|
|
|
|
) (*v1.ExpireNodeResponse, error) {
|
2026-02-20 10:58:49 +00:00
|
|
|
if request.GetDisableExpiry() && request.GetExpiry() != nil {
|
|
|
|
|
return nil, status.Error(
|
|
|
|
|
codes.InvalidArgument,
|
|
|
|
|
"cannot set both disable_expiry and expiry",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle disable expiry request - node will never expire.
|
|
|
|
|
if request.GetDisableExpiry() {
|
|
|
|
|
node, nodeChange, err := api.h.state.SetNodeExpiry(
|
|
|
|
|
types.NodeID(request.GetNodeId()), nil,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
api.h.Change(nodeChange)
|
|
|
|
|
|
|
|
|
|
log.Trace().
|
|
|
|
|
Caller().
|
|
|
|
|
EmbedObject(node).
|
|
|
|
|
Msg("node expiry disabled")
|
|
|
|
|
|
|
|
|
|
return &v1.ExpireNodeResponse{Node: node.Proto()}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 09:09:13 +02:00
|
|
|
expiry := time.Now()
|
|
|
|
|
if request.GetExpiry() != nil {
|
|
|
|
|
expiry = request.GetExpiry().AsTime()
|
|
|
|
|
}
|
2024-02-08 17:28:19 +01:00
|
|
|
|
2026-02-20 10:58:49 +00:00
|
|
|
node, nodeChange, err := api.h.state.SetNodeExpiry(
|
|
|
|
|
types.NodeID(request.GetNodeId()), &expiry,
|
|
|
|
|
)
|
2021-11-21 13:40:19 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
// TODO(kradalby): Ensure that both the selfupdate and peer updates are sent
|
|
|
|
|
api.h.Change(nodeChange)
|
2021-11-21 13:40:19 +00:00
|
|
|
|
|
|
|
|
log.Trace().
|
2025-07-05 23:30:47 +02:00
|
|
|
Caller().
|
2026-02-05 11:01:41 +00:00
|
|
|
EmbedObject(node).
|
2026-02-20 10:58:49 +00:00
|
|
|
Time(zf.ExpiresAt, expiry).
|
2023-09-24 13:42:05 +02:00
|
|
|
Msg("node expired")
|
2021-11-21 13:40:19 +00:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
return &v1.ExpireNodeResponse{Node: node.Proto()}, nil
|
2021-11-21 13:40:19 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) RenameNode(
|
2022-03-13 21:03:20 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.RenameNodeRequest,
|
|
|
|
|
) (*v1.RenameNodeResponse, error) {
|
2025-07-28 11:15:53 +02:00
|
|
|
node, nodeChange, err := api.h.state.RenameNode(types.NodeID(request.GetNodeId()), request.GetNewName())
|
2022-03-13 21:03:20 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
// TODO(kradalby): investigate if we need selfupdate
|
|
|
|
|
api.h.Change(nodeChange)
|
2022-03-13 21:03:20 +00:00
|
|
|
|
|
|
|
|
log.Trace().
|
2025-07-05 23:30:47 +02:00
|
|
|
Caller().
|
2026-02-05 11:01:41 +00:00
|
|
|
EmbedObject(node).
|
|
|
|
|
Str(zf.NewName, request.GetNewName()).
|
2023-09-24 13:42:05 +02:00
|
|
|
Msg("node renamed")
|
2022-03-13 21:03:20 +00:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
return &v1.RenameNodeResponse{Node: node.Proto()}, nil
|
2022-03-13 21:03:20 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) ListNodes(
|
2021-11-04 22:19:27 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.ListNodesRequest,
|
|
|
|
|
) (*v1.ListNodesResponse, error) {
|
2024-11-17 19:40:06 -07:00
|
|
|
// TODO(kradalby): it looks like this can be simplified a lot,
|
|
|
|
|
// the filtering of nodes by user, vs nodes as a whole can
|
|
|
|
|
// probably be done once.
|
|
|
|
|
// TODO(kradalby): This should be done in one tx.
|
2023-01-17 17:43:44 +01:00
|
|
|
if request.GetUser() != "" {
|
2025-05-27 16:27:16 +02:00
|
|
|
user, err := api.h.state.GetUserByName(request.GetUser())
|
2024-11-17 19:40:06 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
nodes := api.h.state.ListNodesByUser(types.UserID(user.ID))
|
2021-11-04 22:19:27 +00:00
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
response := nodesToProto(api.h.state, nodes)
|
2023-09-24 13:42:05 +02:00
|
|
|
return &v1.ListNodesResponse{Nodes: response}, nil
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
nodes := api.h.state.ListNodes()
|
2024-02-18 19:31:29 +01:00
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
response := nodesToProto(api.h.state, nodes)
|
2024-12-19 13:10:10 +01:00
|
|
|
return &v1.ListNodesResponse{Nodes: response}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
func nodesToProto(state *state.State, nodes views.Slice[types.NodeView]) []*v1.Node {
|
|
|
|
|
response := make([]*v1.Node, nodes.Len())
|
|
|
|
|
for index, node := range nodes.All() {
|
2023-12-09 18:09:24 +01:00
|
|
|
resp := node.Proto()
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Tags-as-identity: tagged nodes show as [types.TaggedDevices] user in API responses
|
2025-12-02 12:01:25 +01:00
|
|
|
// (UserID may be set internally for "created by" tracking)
|
|
|
|
|
if node.IsTagged() {
|
|
|
|
|
resp.User = types.TaggedDevices.Proto()
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
resp.SubnetRoutes = util.PrefixesToString(append(state.GetNodePrimaryRoutes(node.ID()), node.ExitRoutes()...))
|
2023-12-09 18:09:24 +01:00
|
|
|
response[index] = resp
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
sort.Slice(response, func(i, j int) bool {
|
|
|
|
|
return response[i].Id < response[j].Id
|
|
|
|
|
})
|
|
|
|
|
|
2024-12-19 13:10:10 +01:00
|
|
|
return response
|
2021-11-04 22:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
func (api headscaleV1APIServer) BackfillNodeIPs(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.BackfillNodeIPsRequest,
|
|
|
|
|
) (*v1.BackfillNodeIPsResponse, error) {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Trace().Caller().Msg("backfill called")
|
2024-04-17 07:03:06 +02:00
|
|
|
|
|
|
|
|
if !request.Confirmed {
|
|
|
|
|
return nil, errors.New("not confirmed, aborting")
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
changes, err := api.h.state.BackfillNodeIPs()
|
2024-04-17 07:03:06 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.BackfillNodeIPsResponse{Changes: changes}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-25 22:11:15 +00:00
|
|
|
func (api headscaleV1APIServer) CreateApiKey(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.CreateApiKeyRequest,
|
|
|
|
|
) (*v1.CreateApiKeyResponse, error) {
|
|
|
|
|
var expiration time.Time
|
|
|
|
|
if request.GetExpiration() != nil {
|
|
|
|
|
expiration = request.GetExpiration().AsTime()
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
apiKey, _, err := api.h.state.CreateAPIKey(&expiration)
|
2022-01-25 22:11:15 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.CreateApiKeyResponse{ApiKey: apiKey}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 13:57:49 +00:00
|
|
|
// apiKeyIdentifier is implemented by requests that identify an API key.
|
|
|
|
|
type apiKeyIdentifier interface {
|
|
|
|
|
GetId() uint64
|
|
|
|
|
GetPrefix() string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getAPIKey retrieves an API key by ID or prefix from the request.
|
|
|
|
|
// Returns InvalidArgument if neither or both are provided.
|
|
|
|
|
func (api headscaleV1APIServer) getAPIKey(req apiKeyIdentifier) (*types.APIKey, error) {
|
|
|
|
|
hasID := req.GetId() != 0
|
|
|
|
|
hasPrefix := req.GetPrefix() != ""
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case hasID && hasPrefix:
|
|
|
|
|
return nil, status.Error(codes.InvalidArgument, "provide either id or prefix, not both")
|
|
|
|
|
case hasID:
|
|
|
|
|
return api.h.state.GetAPIKeyByID(req.GetId())
|
|
|
|
|
case hasPrefix:
|
|
|
|
|
return api.h.state.GetAPIKey(req.GetPrefix())
|
|
|
|
|
default:
|
|
|
|
|
return nil, status.Error(codes.InvalidArgument, "must provide id or prefix")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-25 22:11:15 +00:00
|
|
|
func (api headscaleV1APIServer) ExpireApiKey(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.ExpireApiKeyRequest,
|
|
|
|
|
) (*v1.ExpireApiKeyResponse, error) {
|
2026-01-16 13:57:49 +00:00
|
|
|
apiKey, err := api.getAPIKey(request)
|
2022-01-25 22:11:15 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
err = api.h.state.ExpireAPIKey(apiKey)
|
2022-01-25 22:11:15 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.ExpireApiKeyResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api headscaleV1APIServer) ListApiKeys(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.ListApiKeysRequest,
|
|
|
|
|
) (*v1.ListApiKeysResponse, error) {
|
2025-05-27 16:27:16 +02:00
|
|
|
apiKeys, err := api.h.state.ListAPIKeys()
|
2022-01-25 22:11:15 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response := make([]*v1.ApiKey, len(apiKeys))
|
|
|
|
|
for index, key := range apiKeys {
|
2023-05-21 19:37:59 +03:00
|
|
|
response[index] = key.Proto()
|
2022-01-25 22:11:15 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-18 19:31:29 +01:00
|
|
|
sort.Slice(response, func(i, j int) bool {
|
|
|
|
|
return response[i].Id < response[j].Id
|
|
|
|
|
})
|
|
|
|
|
|
2022-01-25 22:11:15 +00:00
|
|
|
return &v1.ListApiKeysResponse{ApiKeys: response}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-12 16:01:21 +05:30
|
|
|
func (api headscaleV1APIServer) DeleteApiKey(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.DeleteApiKeyRequest,
|
|
|
|
|
) (*v1.DeleteApiKeyResponse, error) {
|
2026-01-16 13:57:49 +00:00
|
|
|
apiKey, err := api.getAPIKey(request)
|
2024-02-12 16:01:21 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
if err := api.h.state.DestroyAPIKey(*apiKey); err != nil {
|
2024-02-12 16:01:21 +05:30
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.DeleteApiKeyResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-18 11:08:25 +05:30
|
|
|
func (api headscaleV1APIServer) GetPolicy(
|
|
|
|
|
_ context.Context,
|
|
|
|
|
_ *v1.GetPolicyRequest,
|
|
|
|
|
) (*v1.GetPolicyResponse, error) {
|
|
|
|
|
switch api.h.cfg.Policy.Mode {
|
|
|
|
|
case types.PolicyModeDB:
|
2025-05-27 16:27:16 +02:00
|
|
|
p, err := api.h.state.GetPolicy()
|
2024-07-18 11:08:25 +05:30
|
|
|
if err != nil {
|
2024-09-07 09:23:58 +02:00
|
|
|
return nil, fmt.Errorf("loading ACL from database: %w", err)
|
2024-07-18 11:08:25 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.GetPolicyResponse{
|
|
|
|
|
Policy: p.Data,
|
|
|
|
|
UpdatedAt: timestamppb.New(p.UpdatedAt),
|
|
|
|
|
}, nil
|
|
|
|
|
case types.PolicyModeFile:
|
|
|
|
|
// Read the file and return the contents as-is.
|
2024-08-12 18:11:59 +08:00
|
|
|
absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path)
|
|
|
|
|
f, err := os.Open(absPath)
|
2024-07-18 11:08:25 +05:30
|
|
|
if err != nil {
|
2024-09-07 09:23:58 +02:00
|
|
|
return nil, fmt.Errorf("reading policy from path %q: %w", absPath, err)
|
2024-07-18 11:08:25 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
b, err := io.ReadAll(f)
|
|
|
|
|
if err != nil {
|
2024-09-07 09:23:58 +02:00
|
|
|
return nil, fmt.Errorf("reading policy from file: %w", err)
|
2024-07-18 11:08:25 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.GetPolicyResponse{Policy: string(b)}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-07 09:23:58 +02:00
|
|
|
return nil, fmt.Errorf("no supported policy mode found in configuration, policy.mode: %q", api.h.cfg.Policy.Mode)
|
2024-07-18 11:08:25 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api headscaleV1APIServer) SetPolicy(
|
|
|
|
|
_ context.Context,
|
|
|
|
|
request *v1.SetPolicyRequest,
|
|
|
|
|
) (*v1.SetPolicyResponse, error) {
|
|
|
|
|
if api.h.cfg.Policy.Mode != types.PolicyModeDB {
|
|
|
|
|
return nil, types.ErrPolicyUpdateIsDisabled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p := request.GetPolicy()
|
|
|
|
|
|
2024-08-30 16:58:29 +02:00
|
|
|
// Validate and reject configuration that would error when applied
|
|
|
|
|
// when creating a map response. This requires nodes, so there is still
|
|
|
|
|
// a scenario where they might be allowed if the server has no nodes
|
|
|
|
|
// yet, but it should help for the general case and for hot reloading
|
|
|
|
|
// configurations.
|
2025-07-05 23:30:47 +02:00
|
|
|
nodes := api.h.state.ListNodes()
|
|
|
|
|
|
|
|
|
|
_, err := api.h.state.SetPolicy([]byte(p))
|
2024-08-30 16:58:29 +02:00
|
|
|
if err != nil {
|
2024-11-26 15:16:06 +01:00
|
|
|
return nil, fmt.Errorf("setting policy: %w", err)
|
2024-08-30 16:58:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
if nodes.Len() > 0 {
|
|
|
|
|
_, err = api.h.state.SSHPolicy(nodes.At(0))
|
2024-08-30 16:58:29 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("verifying SSH rules: %w", err)
|
|
|
|
|
}
|
2024-07-18 11:08:25 +05:30
|
|
|
}
|
|
|
|
|
|
2025-05-27 16:27:16 +02:00
|
|
|
updated, err := api.h.state.SetPolicyInDB(p)
|
2024-07-18 11:08:25 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
// Always reload policy to ensure route re-evaluation, even if policy content hasn't changed.
|
|
|
|
|
// This ensures that routes are re-evaluated for auto-approval in cases where routes
|
|
|
|
|
// were manually disabled but could now be auto-approved with the current policy.
|
|
|
|
|
cs, err := api.h.state.ReloadPolicy()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("reloading policy: %w", err)
|
|
|
|
|
}
|
2025-03-31 15:55:07 +02:00
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
if len(cs) > 0 {
|
|
|
|
|
api.h.Change(cs...)
|
|
|
|
|
} else {
|
|
|
|
|
log.Debug().
|
|
|
|
|
Caller().
|
|
|
|
|
Msg("No policy changes to distribute because ReloadPolicy returned empty changeset")
|
2024-11-26 15:16:06 +01:00
|
|
|
}
|
2024-07-18 11:08:25 +05:30
|
|
|
|
|
|
|
|
response := &v1.SetPolicyResponse{
|
|
|
|
|
Policy: updated.Data,
|
|
|
|
|
UpdatedAt: timestamppb.New(updated.UpdatedAt),
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 16:32:46 +02:00
|
|
|
log.Debug().
|
|
|
|
|
Caller().
|
|
|
|
|
Msg("gRPC SetPolicy completed successfully because response prepared")
|
|
|
|
|
|
2024-07-18 11:08:25 +05:30
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
policy/v2: evaluate the tests block on user-initiated writes
v2 silently dropped policy.tests, so a policy that contradicted its
own assertions still applied. Resolve src/dst via the existing Alias
machinery, walk the compiled global filter rules (acls and grants
both contribute), and run on every user-write boundary: SetPolicy,
the file watcher, and `headscale policy check`. A failing test
rejects the write before it mutates live state.
Boot-time reload skips evaluation; an already-stored policy that
references a deleted user shouldn't lock the server out.
`headscale policy check` is a thin frontend for the new CheckPolicy
gRPC method. The server-side handler builds a fresh PolicyManager
from the request bytes and the state's live users/nodes, runs
SetPolicy on the sandbox so the tests block executes, and returns
the result through gRPC status. No persistence, no policy_mode
coupling. --bypass-grpc-and-access-database-directly opens the DB
directly when the server is not running.
cmd/headscale/cli/root.go no longer special-cases `policy check` in
init() (the early return from PR #2580 broke --config registration
and viper priming for --bypass).
integration/cli_policy_test.go covers policy_mode={file,database} x
fixture={acl-only, acl+passing-tests, acl+failing-tests} x
bypass={false,true} = 12 rows.
Updates #1803
Co-authored-by: Janis Jansons <janhouse@gmail.com>
2026-04-29 14:27:12 +00:00
|
|
|
// CheckPolicy validates the given policy against the server's live users
|
|
|
|
|
// and nodes, running its `tests` block as a sandbox. Nothing is persisted
|
|
|
|
|
// and the live PolicyManager is not touched. Works regardless of
|
|
|
|
|
// policy.mode so operators can validate a policy file before storing it.
|
|
|
|
|
func (api headscaleV1APIServer) CheckPolicy(
|
|
|
|
|
_ context.Context,
|
|
|
|
|
request *v1.CheckPolicyRequest,
|
|
|
|
|
) (*v1.CheckPolicyResponse, error) {
|
|
|
|
|
polB := []byte(request.GetPolicy())
|
|
|
|
|
|
|
|
|
|
users, err := api.h.state.ListAllUsers()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, status.Errorf(codes.Internal, "loading users: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nodes := api.h.state.ListNodes()
|
|
|
|
|
|
|
|
|
|
pm, err := policyv2.NewPolicyManager(polB, users, nodes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := pm.SetPolicy(polB); err != nil {
|
|
|
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.CheckPolicyResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-04 22:19:27 +00:00
|
|
|
// The following service calls are for testing and debugging
|
2023-09-24 13:42:05 +02:00
|
|
|
func (api headscaleV1APIServer) DebugCreateNode(
|
2021-11-04 22:19:27 +00:00
|
|
|
ctx context.Context,
|
2023-09-24 13:42:05 +02:00
|
|
|
request *v1.DebugCreateNodeRequest,
|
|
|
|
|
) (*v1.DebugCreateNodeResponse, error) {
|
2025-05-27 16:27:16 +02:00
|
|
|
user, err := api.h.state.GetUserByName(request.GetUser())
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-11 09:09:18 +02:00
|
|
|
routes, err := util.StringToIPPrefix(request.GetRoutes())
|
2021-11-04 22:19:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-13 08:36:45 +00:00
|
|
|
log.Trace().
|
|
|
|
|
Caller().
|
|
|
|
|
Interface("route-prefix", routes).
|
|
|
|
|
Interface("route-str", request.GetRoutes()).
|
2025-09-05 16:32:46 +02:00
|
|
|
Msg("Creating routes for node")
|
2021-11-04 22:19:27 +00:00
|
|
|
|
2026-02-24 18:48:57 +00:00
|
|
|
registrationId, err := types.AuthIDFromString(request.GetKey())
|
2022-05-16 20:32:37 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 17:27:42 +00:00
|
|
|
regData := &types.RegistrationData{
|
2026-02-24 18:48:57 +00:00
|
|
|
NodeKey: key.NewNode().Public(),
|
|
|
|
|
MachineKey: key.NewMachine().Public(),
|
|
|
|
|
Hostname: request.GetName(),
|
2026-04-09 17:27:42 +00:00
|
|
|
Expiry: &time.Time{}, // zero time, not nil — preserves proto JSON round-trip semantics
|
2026-02-24 18:48:57 +00:00
|
|
|
}
|
2022-11-28 15:54:23 +00:00
|
|
|
|
2023-11-19 22:37:04 +01:00
|
|
|
log.Debug().
|
2025-09-05 16:32:46 +02:00
|
|
|
Caller().
|
2025-01-26 22:20:11 +01:00
|
|
|
Str("registration_id", registrationId.String()).
|
2023-11-19 22:37:04 +01:00
|
|
|
Msg("adding debug machine via CLI, appending to registration cache")
|
2021-11-04 22:19:27 +00:00
|
|
|
|
2026-04-09 17:27:42 +00:00
|
|
|
authRegReq := types.NewRegisterAuthRequest(regData)
|
2026-02-24 18:48:57 +00:00
|
|
|
api.h.state.SetAuthCacheEntry(registrationId, authRegReq)
|
2021-10-29 16:44:32 +00:00
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Echo back a synthetic [types.Node] so the debug response surface stays
|
|
|
|
|
// stable. The actual node is created later by [headscaleV1APIServer.AuthApprove] via
|
|
|
|
|
// [state.State.HandleNodeFromAuthPath] using the cached [types.RegistrationData].
|
2026-04-09 17:27:42 +00:00
|
|
|
echoNode := types.Node{
|
|
|
|
|
NodeKey: regData.NodeKey,
|
|
|
|
|
MachineKey: regData.MachineKey,
|
|
|
|
|
Hostname: regData.Hostname,
|
|
|
|
|
User: user,
|
|
|
|
|
Expiry: &time.Time{},
|
|
|
|
|
LastSeen: &time.Time{},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
Hostname: request.GetName(),
|
|
|
|
|
OS: "TestOS",
|
|
|
|
|
RoutableIPs: routes,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.DebugCreateNodeResponse{Node: echoNode.Proto()}, nil
|
2021-10-26 20:42:20 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-16 15:00:11 +03:00
|
|
|
func (api headscaleV1APIServer) Health(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.HealthRequest,
|
|
|
|
|
) (*v1.HealthResponse, error) {
|
|
|
|
|
var healthErr error
|
|
|
|
|
response := &v1.HealthResponse{}
|
|
|
|
|
|
|
|
|
|
if err := api.h.state.PingDB(ctx); err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
healthErr = fmt.Errorf("pinging database: %w", err)
|
2025-10-16 15:00:11 +03:00
|
|
|
} else {
|
|
|
|
|
response.DatabaseConnectivity = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if healthErr != nil {
|
2026-02-05 13:59:26 +00:00
|
|
|
log.Error().Err(healthErr).Msg("health check failed")
|
2025-10-16 15:00:11 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response, healthErr
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 18:51:11 +00:00
|
|
|
func (api headscaleV1APIServer) AuthRegister(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.AuthRegisterRequest,
|
|
|
|
|
) (*v1.AuthRegisterResponse, error) {
|
|
|
|
|
resp, err := api.RegisterNode(ctx, &v1.RegisterNodeRequest{
|
|
|
|
|
Key: request.GetAuthId(),
|
|
|
|
|
User: request.GetUser(),
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &v1.AuthRegisterResponse{Node: resp.GetNode()}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api headscaleV1APIServer) AuthApprove(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.AuthApproveRequest,
|
|
|
|
|
) (*v1.AuthApproveResponse, error) {
|
|
|
|
|
authID, err := types.AuthIDFromString(request.GetAuthId())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid auth_id: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authReq, ok := api.h.state.GetAuthCacheEntry(authID)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, status.Errorf(codes.NotFound, "no pending auth session for auth_id %s", authID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authReq.FinishAuth(types.AuthVerdict{})
|
|
|
|
|
|
|
|
|
|
return &v1.AuthApproveResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api headscaleV1APIServer) AuthReject(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
request *v1.AuthRejectRequest,
|
|
|
|
|
) (*v1.AuthRejectResponse, error) {
|
|
|
|
|
authID, err := types.AuthIDFromString(request.GetAuthId())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid auth_id: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authReq, ok := api.h.state.GetAuthCacheEntry(authID)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, status.Errorf(codes.NotFound, "no pending auth session for auth_id %s", authID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authReq.FinishAuth(types.AuthVerdict{
|
|
|
|
|
Err: fmt.Errorf("auth request rejected"),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return &v1.AuthRejectResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-26 20:42:20 +00:00
|
|
|
func (api headscaleV1APIServer) mustEmbedUnimplementedHeadscaleServiceServer() {}
|