Files
headscale/hscontrol/servertest/server.go
Kristoffer Dalby 53b8a81d48 servertest: support tagged pre-auth keys in test clients
WithTags was defined but never passed through to CreatePreAuthKey.
Fix NewClient to use CreateTaggedPreAuthKey when tags are specified,
enabling tests that need tagged nodes (e.g. via grant steering).

Updates #2180
2026-04-01 14:10:42 +01:00

205 lines
5.5 KiB
Go

// Package servertest provides an in-process test harness for Headscale's
// control plane. It wires a real Headscale server to real Tailscale
// controlclient.Direct instances, enabling fast, deterministic tests
// of the full control protocol without Docker or separate processes.
package servertest
import (
"net/http/httptest"
"net/netip"
"testing"
"time"
hscontrol "github.com/juanfont/headscale/hscontrol"
"github.com/juanfont/headscale/hscontrol/state"
"github.com/juanfont/headscale/hscontrol/types"
"tailscale.com/tailcfg"
)
// TestServer is an in-process Headscale control server suitable for
// use with Tailscale's controlclient.Direct.
type TestServer struct {
App *hscontrol.Headscale
HTTPServer *httptest.Server
URL string
st *state.State
}
// ServerOption configures a TestServer.
type ServerOption func(*serverConfig)
type serverConfig struct {
batchDelay time.Duration
bufferedChanSize int
ephemeralTimeout time.Duration
batcherWorkers int
}
func defaultServerConfig() *serverConfig {
return &serverConfig{
batchDelay: 50 * time.Millisecond,
bufferedChanSize: 30,
batcherWorkers: 1,
ephemeralTimeout: 30 * time.Second,
}
}
// WithBatchDelay sets the batcher's change coalescing delay.
func WithBatchDelay(d time.Duration) ServerOption {
return func(c *serverConfig) { c.batchDelay = d }
}
// WithBufferedChanSize sets the per-node map session channel buffer.
func WithBufferedChanSize(n int) ServerOption {
return func(c *serverConfig) { c.bufferedChanSize = n }
}
// WithEphemeralTimeout sets the ephemeral node inactivity timeout.
func WithEphemeralTimeout(d time.Duration) ServerOption {
return func(c *serverConfig) { c.ephemeralTimeout = d }
}
// NewServer creates and starts a Headscale test server.
// The server is fully functional and accepts real Tailscale control
// protocol connections over Noise.
func NewServer(tb testing.TB, opts ...ServerOption) *TestServer {
tb.Helper()
sc := defaultServerConfig()
for _, o := range opts {
o(sc)
}
tmpDir := tb.TempDir()
prefixV4 := netip.MustParsePrefix("100.64.0.0/10")
prefixV6 := netip.MustParsePrefix("fd7a:115c:a1e0::/48")
cfg := types.Config{
// Placeholder; updated below once httptest server starts.
ServerURL: "http://localhost:0",
NoisePrivateKeyPath: tmpDir + "/noise_private.key",
EphemeralNodeInactivityTimeout: sc.ephemeralTimeout,
PrefixV4: &prefixV4,
PrefixV6: &prefixV6,
IPAllocation: types.IPAllocationStrategySequential,
Database: types.DatabaseConfig{
Type: "sqlite3",
Sqlite: types.SqliteConfig{
Path: tmpDir + "/headscale_test.db",
},
},
Policy: types.PolicyConfig{
Mode: types.PolicyModeDB,
},
Tuning: types.Tuning{
BatchChangeDelay: sc.batchDelay,
BatcherWorkers: sc.batcherWorkers,
NodeMapSessionBufferedChanSize: sc.bufferedChanSize,
},
}
app, err := hscontrol.NewHeadscale(&cfg)
if err != nil {
tb.Fatalf("servertest: NewHeadscale: %v", err)
}
// Set a minimal DERP map so MapResponse generation works.
app.GetState().SetDERPMap(&tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
900: {
RegionID: 900,
RegionCode: "test",
RegionName: "Test Region",
Nodes: []*tailcfg.DERPNode{{
Name: "test0",
RegionID: 900,
HostName: "127.0.0.1",
IPv4: "127.0.0.1",
DERPPort: -1, // not a real DERP, just needed for MapResponse
}},
},
},
})
// Start subsystems.
app.StartBatcherForTest(tb)
app.StartEphemeralGCForTest(tb)
// Start the HTTP server with Headscale's full handler (including
// /key and /ts2021 Noise upgrade).
ts := httptest.NewServer(app.HTTPHandler())
tb.Cleanup(ts.Close)
// Now update the config to point at the real URL so that
// MapResponse.ControlURL etc. are correct.
app.SetServerURLForTest(tb, ts.URL)
return &TestServer{
App: app,
HTTPServer: ts,
URL: ts.URL,
st: app.GetState(),
}
}
// State returns the server's state manager for creating users,
// nodes, and pre-auth keys.
func (s *TestServer) State() *state.State {
return s.st
}
// CreateUser creates a test user and returns it.
func (s *TestServer) CreateUser(tb testing.TB, name string) *types.User {
tb.Helper()
u, _, err := s.st.CreateUser(types.User{Name: name})
if err != nil {
tb.Fatalf("servertest: CreateUser(%q): %v", name, err)
}
return u
}
// CreatePreAuthKey creates a reusable pre-auth key for the given user.
func (s *TestServer) CreatePreAuthKey(tb testing.TB, userID types.UserID) string {
tb.Helper()
uid := userID
pak, err := s.st.CreatePreAuthKey(&uid, true, false, nil, nil)
if err != nil {
tb.Fatalf("servertest: CreatePreAuthKey: %v", err)
}
return pak.Key
}
// CreateTaggedPreAuthKey creates a reusable pre-auth key with ACL tags.
func (s *TestServer) CreateTaggedPreAuthKey(tb testing.TB, userID types.UserID, tags []string) string {
tb.Helper()
uid := userID
pak, err := s.st.CreatePreAuthKey(&uid, true, false, nil, tags)
if err != nil {
tb.Fatalf("servertest: CreateTaggedPreAuthKey: %v", err)
}
return pak.Key
}
// CreateEphemeralPreAuthKey creates an ephemeral pre-auth key.
func (s *TestServer) CreateEphemeralPreAuthKey(tb testing.TB, userID types.UserID) string {
tb.Helper()
uid := userID
pak, err := s.st.CreatePreAuthKey(&uid, false, true, nil, nil)
if err != nil {
tb.Fatalf("servertest: CreateEphemeralPreAuthKey: %v", err)
}
return pak.Key
}