1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-11-10 01:20:58 +01:00
This commit is contained in:
Juanjo Presa 2025-10-18 11:55:16 +02:00
parent 46477b8021
commit 47cc4d0f9c
6 changed files with 225 additions and 25 deletions

View File

@ -44,6 +44,7 @@ const (
HeadscaleService_GetPolicy_FullMethodName = "/headscale.v1.HeadscaleService/GetPolicy"
HeadscaleService_SetPolicy_FullMethodName = "/headscale.v1.HeadscaleService/SetPolicy"
HeadscaleService_Health_FullMethodName = "/headscale.v1.HeadscaleService/Health"
HeadscaleService_ListPendingRegistrations_FullMethodName = "/headscale.v1.HeadscaleService/ListPendingRegistrations"
)
// HeadscaleServiceClient is the client API for HeadscaleService service.
@ -81,6 +82,8 @@ type HeadscaleServiceClient interface {
SetPolicy(ctx context.Context, in *SetPolicyRequest, opts ...grpc.CallOption) (*SetPolicyResponse, error)
// --- Health start ---
Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error)
// --- Pending registrations ---
ListPendingRegistrations(ctx context.Context, in *ListPendingRegistrationsRequest, opts ...grpc.CallOption) (*ListPendingRegistrationsResponse, error)
}
type headscaleServiceClient struct {
@ -341,6 +344,16 @@ func (c *headscaleServiceClient) Health(ctx context.Context, in *HealthRequest,
return out, nil
}
func (c *headscaleServiceClient) ListPendingRegistrations(ctx context.Context, in *ListPendingRegistrationsRequest, opts ...grpc.CallOption) (*ListPendingRegistrationsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListPendingRegistrationsResponse)
err := c.cc.Invoke(ctx, HeadscaleService_ListPendingRegistrations_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// HeadscaleServiceServer is the server API for HeadscaleService service.
// All implementations must embed UnimplementedHeadscaleServiceServer
// for forward compatibility.
@ -376,6 +389,8 @@ type HeadscaleServiceServer interface {
SetPolicy(context.Context, *SetPolicyRequest) (*SetPolicyResponse, error)
// --- Health start ---
Health(context.Context, *HealthRequest) (*HealthResponse, error)
// --- Pending registrations ---
ListPendingRegistrations(context.Context, *ListPendingRegistrationsRequest) (*ListPendingRegistrationsResponse, error)
mustEmbedUnimplementedHeadscaleServiceServer()
}
@ -461,6 +476,9 @@ func (UnimplementedHeadscaleServiceServer) SetPolicy(context.Context, *SetPolicy
func (UnimplementedHeadscaleServiceServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Health not implemented")
}
func (UnimplementedHeadscaleServiceServer) ListPendingRegistrations(context.Context, *ListPendingRegistrationsRequest) (*ListPendingRegistrationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListPendingRegistrations not implemented")
}
func (UnimplementedHeadscaleServiceServer) mustEmbedUnimplementedHeadscaleServiceServer() {}
func (UnimplementedHeadscaleServiceServer) testEmbeddedByValue() {}
@ -932,6 +950,24 @@ func _HeadscaleService_Health_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _HeadscaleService_ListPendingRegistrations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPendingRegistrationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HeadscaleServiceServer).ListPendingRegistrations(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: HeadscaleService_ListPendingRegistrations_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HeadscaleServiceServer).ListPendingRegistrations(ctx, req.(*ListPendingRegistrationsRequest))
}
return interceptor(ctx, in, info, handler)
}
// HeadscaleService_ServiceDesc is the grpc.ServiceDesc for HeadscaleService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -1039,6 +1075,10 @@ var HeadscaleService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Health",
Handler: _HeadscaleService_Health_Handler,
},
{
MethodName: "ListPendingRegistrations",
Handler: _HeadscaleService_ListPendingRegistrations_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "headscale/v1/headscale.proto",

View File

@ -0,0 +1,26 @@
// Code generated manually to provide types for PendingRegistrations RPC until buf generate is run.
// This file defines protobuf-compatible Go structs without full reflection metadata.
// It is sufficient for compiling server code that references these types.
package v1
import (
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
// PendingRegistration is a lightweight representation of a pending registration.
type PendingRegistration struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"`
MachineKey string `protobuf:"bytes,3,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"`
NodeKey string `protobuf:"bytes,4,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"`
Expiry *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expiry,proto3" json:"expiry,omitempty"`
}
// ListPendingRegistrationsRequest is the empty request message.
type ListPendingRegistrationsRequest struct{}
// ListPendingRegistrationsResponse contains the current pending registrations.
type ListPendingRegistrationsResponse struct {
Registrations []*PendingRegistration `protobuf:"bytes,1,rep,name=registrations,proto3" json:"registrations,omitempty"`
}

View File

@ -793,4 +793,26 @@ func (api headscaleV1APIServer) Health(
return response, healthErr
}
func (api headscaleV1APIServer) ListPendingRegistrations(
ctx context.Context,
_ *v1.ListPendingRegistrationsRequest,
) (*v1.ListPendingRegistrationsResponse, error) {
regs := api.h.state.ListPendingRegistrations()
resp := &v1.ListPendingRegistrationsResponse{}
for _, r := range regs {
var ts *timestamppb.Timestamp
if r.Expiry != nil {
ts = timestamppb.New(*r.Expiry)
}
resp.Registrations = append(resp.Registrations, &v1.PendingRegistration{
Id: r.ID.String(),
Hostname: r.Hostname,
MachineKey: r.MachineKey,
NodeKey: r.NodeKey,
Expiry: ts,
})
}
return resp, nil
}
func (api headscaleV1APIServer) mustEmbedUnimplementedHeadscaleServiceServer() {}

View File

@ -69,6 +69,17 @@ type State struct {
primaryRoutes *routes.PrimaryRoutes
}
// PendingRegistration represents a pending node registration entry in memory.
// It is populated from the registrationCache and used by API/gRPC layers.
// Note: This is an in-memory view only; entries expire automatically from the cache.
type PendingRegistration struct {
ID types.RegistrationID
Hostname string
MachineKey string
NodeKey string
Expiry *time.Time
}
// 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) {
@ -968,6 +979,36 @@ func (s *State) SetRegistrationCacheEntry(id types.RegistrationID, entry types.R
s.registrationCache.Set(id, entry)
}
// ListPendingRegistrations returns a snapshot of current pending registrations.
// It iterates the registrationCache and extracts minimal identifying details.
func (s *State) ListPendingRegistrations() []PendingRegistration {
if s.registrationCache == nil {
return nil
}
var regs []PendingRegistration
// zcache/v2 supports Keys iteration; use Range if available for efficiency.
// We use Keys here and then look up to read values.
for _, id := range s.registrationCache.Keys() {
if rn, ok := s.registrationCache.Get(id); ok {
var exp *time.Time
if rn.Node.Expiry != nil {
exp = rn.Node.Expiry
}
regs = append(regs, PendingRegistration{
ID: id,
Hostname: rn.Node.Hostname,
MachineKey: rn.Node.MachineKey.String(),
NodeKey: rn.Node.NodeKey.String(),
Expiry: exp,
})
}
}
return regs
}
// logHostinfoValidation logs warnings when hostinfo is nil or has empty hostname.
func logHostinfoValidation(machineKey, nodeKey, username, hostname string, hostinfo *tailcfg.Hostinfo) {
if hostinfo == nil {

View File

@ -0,0 +1,48 @@
package types
import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
// PendingRegistrationProto converts a state.PendingRegistration-like struct to protobuf.
// Kept in types to avoid circular imports from hscontrol/state.
// Input is the plain fields to serialize.
type PendingRegistrationProto struct {
ID string
Hostname string
MachineKey string
NodeKey string
// Expiry may be nil
ExpiryUnixMilli int64 // not used; we will pass a pointer time via Timestamppb in helpers if needed
}
// BuildPendingRegistration constructs a v1.PendingRegistration.
func BuildPendingRegistration(id, hostname, machineKey, nodeKey string, expiry *int64) *v1.PendingRegistration {
pr := &v1.PendingRegistration{
Id: id,
Hostname: hostname,
MachineKey: machineKey,
NodeKey: nodeKey,
}
if expiry != nil {
// We cannot convert int64 millis without time. Keep the field for potential future.
// Callers should prefer using BuildPendingRegistrationWithTimestamp.
_ = expiry
}
return pr
}
// BuildPendingRegistrationWithTimestamp sets a proper protobuf timestamp if provided.
func BuildPendingRegistrationWithTimestamp(id, hostname, machineKey, nodeKey string, ts *timestamppb.Timestamp) *v1.PendingRegistration {
pr := &v1.PendingRegistration{
Id: id,
Hostname: hostname,
MachineKey: machineKey,
NodeKey: nodeKey,
}
if ts != nil {
pr.Expiry = ts
}
return pr
}

View File

@ -3,6 +3,7 @@ package headscale.v1;
option go_package = "github.com/juanfont/headscale/gen/go/v1";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "headscale/v1/user.proto";
import "headscale/v1/preauthkey.proto";
@ -190,6 +191,14 @@ service HeadscaleService {
}
// --- Health end ---
// --- Pending registrations ---
rpc ListPendingRegistrations(ListPendingRegistrationsRequest) returns (ListPendingRegistrationsResponse) {
option (google.api.http) = {
get : "/api/v1/pending-registrations"
};
}
// --- Pending registrations end ---
// Implement Tailscale API
// rpc GetDevice(GetDeviceRequest) returns(GetDeviceResponse) {
// option(google.api.http) = {
@ -223,3 +232,17 @@ message HealthRequest {}
message HealthResponse {
bool database_connectivity = 1;
}
message PendingRegistration {
string id = 1;
string hostname = 2;
string machine_key = 3;
string node_key = 4;
google.protobuf.Timestamp expiry = 5;
}
message ListPendingRegistrationsRequest {}
message ListPendingRegistrationsResponse {
repeated PendingRegistration registrations = 1;
}