2022-10-13 16:01:23 +02:00
package hsic
import (
2025-06-23 13:43:14 +02:00
"archive/tar"
"bytes"
2025-02-01 09:16:51 +00:00
"cmp"
2022-11-06 20:22:21 +01:00
"crypto/tls"
2022-10-13 16:01:23 +02:00
"encoding/json"
"errors"
"fmt"
2024-04-27 10:47:39 +02:00
"io"
2022-10-13 16:01:23 +02:00
"log"
"net/http"
2025-02-26 07:22:55 -08:00
"net/netip"
2023-04-27 16:57:11 +02:00
"os"
"path"
2025-06-23 13:43:14 +02:00
"path/filepath"
2025-02-01 09:16:51 +00:00
"sort"
2024-04-21 18:28:17 +02:00
"strconv"
2023-04-27 16:57:11 +02:00
"strings"
2022-11-06 20:22:21 +01:00
"time"
2022-10-13 16:01:23 +02:00
2023-01-05 12:44:28 +01:00
"github.com/davecgh/go-spew/spew"
2022-10-13 16:01:23 +02:00
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
2025-09-05 16:32:46 +02:00
"github.com/juanfont/headscale/hscontrol"
2025-05-20 13:57:26 +02:00
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
2025-08-06 08:37:02 +02:00
"github.com/juanfont/headscale/hscontrol/routes"
2024-04-17 07:03:06 +02:00
"github.com/juanfont/headscale/hscontrol/types"
2023-05-11 09:09:18 +02:00
"github.com/juanfont/headscale/hscontrol/util"
2022-10-13 16:01:23 +02:00
"github.com/juanfont/headscale/integration/dockertestutil"
2022-11-06 20:22:21 +01:00
"github.com/juanfont/headscale/integration/integrationutil"
2022-10-13 16:01:23 +02:00
"github.com/ory/dockertest/v3"
2023-04-13 21:10:08 +00:00
"github.com/ory/dockertest/v3/docker"
2024-11-22 20:23:05 +08:00
"gopkg.in/yaml.v3"
2025-07-24 17:44:09 +02:00
"tailscale.com/envknob"
2024-11-22 20:23:05 +08:00
"tailscale.com/tailcfg"
2025-04-30 12:45:08 +03:00
"tailscale.com/util/mak"
2022-10-13 16:01:23 +02:00
)
2022-10-18 12:09:10 +02:00
const (
2024-11-23 22:14:36 +01:00
hsicHashLength = 6
dockerContextPath = "../."
2024-12-10 16:23:55 +01:00
caCertRoot = "/usr/local/share/ca-certificates"
2024-11-23 22:14:36 +01:00
aclPolicyPath = "/etc/headscale/acl.hujson"
tlsCertPath = "/etc/headscale/tls.cert"
tlsKeyPath = "/etc/headscale/tls.key"
headscaleDefaultPort = 8080
IntegrationTestDockerFileName = "Dockerfile.integration"
2022-10-18 12:09:10 +02:00
)
2022-10-13 16:01:23 +02:00
var errHeadscaleStatusCodeNotOk = errors . New ( "headscale status code not ok" )
2023-01-10 13:46:42 +02:00
type fileInContainer struct {
path string
contents [ ] byte
}
2023-02-03 12:24:27 +01:00
// HeadscaleInContainer is an implementation of ControlServer which
// sets up a Headscale instance inside a container.
2022-10-13 16:01:23 +02:00
type HeadscaleInContainer struct {
hostname string
pool * dockertest . Pool
container * dockertest . Resource
2025-03-21 11:49:32 +01:00
networks [ ] * dockertest . Network
2022-11-02 11:08:54 +01:00
2024-02-18 19:31:29 +01:00
pgContainer * dockertest . Resource
2022-11-02 11:08:54 +01:00
// optional config
2023-01-10 13:46:42 +02:00
port int
2023-04-13 21:10:08 +00:00
extraPorts [ ] string
2025-07-24 17:44:09 +02:00
debugPort int
2024-11-22 20:23:05 +08:00
caCerts [ ] [ ] byte
2023-04-13 21:10:08 +00:00
hostPortBindings map [ string ] [ ] string
2025-05-20 13:57:26 +02:00
aclPolicy * policyv2 . Policy
2023-01-10 13:46:42 +02:00
env map [ string ] string
tlsCert [ ] byte
tlsKey [ ] byte
filesInContainer [ ] fileInContainer
2024-02-18 19:31:29 +01:00
postgres bool
2025-03-31 15:55:07 +02:00
policyMode types . PolicyMode
2022-11-02 11:08:54 +01:00
}
2023-02-03 12:24:27 +01:00
// Option represent optional settings that can be given to a
// Headscale instance.
2022-11-02 11:08:54 +01:00
type Option = func ( c * HeadscaleInContainer )
2023-05-10 10:26:28 +02:00
// WithACLPolicy adds a hscontrol.ACLPolicy policy to the
2023-02-03 12:24:27 +01:00
// HeadscaleInContainer instance.
2025-05-20 13:57:26 +02:00
func WithACLPolicy ( acl * policyv2 . Policy ) Option {
2022-11-02 11:08:54 +01:00
return func ( hsic * HeadscaleInContainer ) {
2024-11-26 15:16:06 +01:00
if acl == nil {
return
}
2022-11-06 20:22:21 +01:00
// TODO(kradalby): Move somewhere appropriate
2024-08-19 13:03:01 +02:00
hsic . env [ "HEADSCALE_POLICY_PATH" ] = aclPolicyPath
2022-11-06 20:22:21 +01:00
2022-11-02 11:08:54 +01:00
hsic . aclPolicy = acl
}
}
2024-11-22 20:23:05 +08:00
// WithCACert adds it to the trusted surtificate of the container.
func WithCACert ( cert [ ] byte ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . caCerts = append ( hsic . caCerts , cert )
}
}
2023-02-03 12:24:27 +01:00
// WithTLS creates certificates and enables HTTPS.
2022-11-06 20:22:21 +01:00
func WithTLS ( ) Option {
return func ( hsic * HeadscaleInContainer ) {
2024-11-22 20:23:05 +08:00
cert , key , err := integrationutil . CreateCertificate ( hsic . hostname )
2022-11-06 20:22:21 +01:00
if err != nil {
log . Fatalf ( "failed to create certificates for headscale test: %s" , err )
}
2024-11-22 20:23:05 +08:00
hsic . tlsCert = cert
hsic . tlsKey = key
}
}
2022-11-06 20:22:21 +01:00
2024-11-22 20:23:05 +08:00
// WithCustomTLS uses the given certificates for the Headscale instance.
func WithCustomTLS ( cert , key [ ] byte ) Option {
return func ( hsic * HeadscaleInContainer ) {
2022-11-06 20:22:21 +01:00
hsic . tlsCert = cert
hsic . tlsKey = key
}
}
2023-02-03 12:24:27 +01:00
// WithConfigEnv takes a map of environment variables that
// can be used to override Headscale configuration.
2022-11-02 11:08:54 +01:00
func WithConfigEnv ( configEnv map [ string ] string ) Option {
return func ( hsic * HeadscaleInContainer ) {
for key , value := range configEnv {
2023-01-05 12:44:28 +01:00
hsic . env [ key ] = value
2022-11-02 11:08:54 +01:00
}
}
2022-10-13 16:01:23 +02:00
}
2023-02-03 12:24:27 +01:00
// WithPort sets the port on where to run Headscale.
2022-11-06 20:22:21 +01:00
func WithPort ( port int ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . port = port
}
}
2023-04-13 21:10:47 +00:00
// WithExtraPorts exposes additional ports on the container (e.g. 3478/udp for STUN).
2023-04-13 21:10:08 +00:00
func WithExtraPorts ( ports [ ] string ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . extraPorts = ports
}
}
func WithHostPortBindings ( bindings map [ string ] [ ] string ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . hostPortBindings = bindings
}
}
2023-03-03 18:22:47 +01:00
// WithTestName sets a name for the test, this will be reflected
2023-02-03 12:24:27 +01:00
// in the Docker container name.
2022-11-14 15:01:31 +01:00
func WithTestName ( testName string ) Option {
return func ( hsic * HeadscaleInContainer ) {
2023-05-11 09:09:18 +02:00
hash , _ := util . GenerateRandomStringDNSSafe ( hsicHashLength )
2022-11-14 15:01:31 +01:00
hostname := fmt . Sprintf ( "hs-%s-%s" , testName , hash )
hsic . hostname = hostname
}
}
2024-11-22 20:23:05 +08:00
// WithHostname sets the hostname of the Headscale instance.
func WithHostname ( hostname string ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . hostname = hostname
}
}
2023-02-03 12:24:27 +01:00
// WithFileInContainer adds a file to the container at the given path.
2023-01-10 13:46:42 +02:00
func WithFileInContainer ( path string , contents [ ] byte ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . filesInContainer = append ( hsic . filesInContainer ,
fileInContainer {
path : path ,
contents : contents ,
} )
}
}
2024-02-18 19:31:29 +01:00
// WithPostgres spins up a Postgres container and
// sets it as the main database.
func WithPostgres ( ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . postgres = true
}
}
2025-07-10 23:38:55 +02:00
// WithPolicy sets the policy mode for headscale.
2025-03-31 15:55:07 +02:00
func WithPolicyMode ( mode types . PolicyMode ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . policyMode = mode
hsic . env [ "HEADSCALE_POLICY_MODE" ] = string ( mode )
}
}
2024-04-17 07:03:06 +02:00
// WithIPAllocationStrategy sets the tests IP Allocation strategy.
2024-07-22 08:56:00 +02:00
func WithIPAllocationStrategy ( strategy types . IPAllocationStrategy ) Option {
2024-04-17 07:03:06 +02:00
return func ( hsic * HeadscaleInContainer ) {
2024-07-22 08:56:00 +02:00
hsic . env [ "HEADSCALE_PREFIXES_ALLOCATION" ] = string ( strategy )
2024-04-17 07:03:06 +02:00
}
}
2024-04-16 21:37:25 +02:00
// WithEmbeddedDERPServerOnly configures Headscale to start
// and only use the embedded DERP server.
// It requires WithTLS and WithHostnameAsServerURL to be
// set.
func WithEmbeddedDERPServerOnly ( ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . env [ "HEADSCALE_DERP_URLS" ] = ""
hsic . env [ "HEADSCALE_DERP_SERVER_ENABLED" ] = "true"
hsic . env [ "HEADSCALE_DERP_SERVER_REGION_ID" ] = "999"
hsic . env [ "HEADSCALE_DERP_SERVER_REGION_CODE" ] = "headscale"
hsic . env [ "HEADSCALE_DERP_SERVER_REGION_NAME" ] = "Headscale Embedded DERP"
hsic . env [ "HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR" ] = "0.0.0.0:3478"
hsic . env [ "HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH" ] = "/tmp/derp.key"
// Envknob for enabling DERP debug logs
hsic . env [ "DERP_DEBUG_LOGS" ] = "true"
hsic . env [ "DERP_PROBER_DEBUG_LOGS" ] = "true"
}
}
2024-11-22 20:23:05 +08:00
// WithDERPConfig configures Headscale use a custom
// DERP server only.
func WithDERPConfig ( derpMap tailcfg . DERPMap ) Option {
return func ( hsic * HeadscaleInContainer ) {
contents , err := yaml . Marshal ( derpMap )
if err != nil {
log . Fatalf ( "failed to marshal DERP map: %s" , err )
return
}
hsic . env [ "HEADSCALE_DERP_PATHS" ] = "/etc/headscale/derp.yml"
hsic . filesInContainer = append ( hsic . filesInContainer ,
fileInContainer {
path : "/etc/headscale/derp.yml" ,
contents : contents ,
} )
// Disable global DERP server and embedded DERP server
hsic . env [ "HEADSCALE_DERP_URLS" ] = ""
hsic . env [ "HEADSCALE_DERP_SERVER_ENABLED" ] = "false"
// Envknob for enabling DERP debug logs
hsic . env [ "DERP_DEBUG_LOGS" ] = "true"
hsic . env [ "DERP_PROBER_DEBUG_LOGS" ] = "true"
}
}
2024-04-21 18:28:17 +02:00
// WithTuning allows changing the tuning settings easily.
func WithTuning ( batchTimeout time . Duration , mapSessionChanSize int ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . env [ "HEADSCALE_TUNING_BATCH_CHANGE_DELAY" ] = batchTimeout . String ( )
2025-07-28 11:15:53 +02:00
hsic . env [ "HEADSCALE_TUNING_NODE_MAPSESSION_BUFFERED_CHAN_SIZE" ] = strconv . Itoa (
mapSessionChanSize ,
)
2024-04-21 18:28:17 +02:00
}
}
2024-09-03 00:22:17 -07:00
func WithTimezone ( timezone string ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . env [ "TZ" ] = timezone
}
}
2025-08-06 08:37:02 +02:00
// WithDERPAsIP enables using IP address instead of hostname for DERP server.
// This is useful for integration tests where DNS resolution may be unreliable.
func WithDERPAsIP ( ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . env [ "HEADSCALE_DEBUG_DERP_USE_IP" ] = "1"
}
}
2025-07-24 17:44:09 +02:00
// WithDebugPort sets the debug port for delve debugging.
func WithDebugPort ( port int ) Option {
return func ( hsic * HeadscaleInContainer ) {
hsic . debugPort = port
}
}
// buildEntrypoint builds the container entrypoint command based on configuration.
func ( hsic * HeadscaleInContainer ) buildEntrypoint ( ) [ ] string {
2025-07-28 11:15:53 +02:00
debugCmd := fmt . Sprintf (
"/go/bin/dlv --listen=0.0.0.0:%d --headless=true --api-version=2 --accept-multiclient --allow-non-terminal-interactive=true exec /go/bin/headscale --continue -- serve" ,
hsic . debugPort ,
)
entrypoint := fmt . Sprintf (
"/bin/sleep 3 ; update-ca-certificates ; %s ; /bin/sleep 30" ,
debugCmd ,
)
2025-07-24 17:44:09 +02:00
return [ ] string { "/bin/bash" , "-c" , entrypoint }
}
2023-02-03 12:24:27 +01:00
// New returns a new HeadscaleInContainer instance.
2022-10-13 16:01:23 +02:00
func New (
pool * dockertest . Pool ,
2025-03-21 11:49:32 +01:00
networks [ ] * dockertest . Network ,
2022-11-02 11:08:54 +01:00
opts ... Option ,
2022-10-18 12:09:10 +02:00
) ( * HeadscaleInContainer , error ) {
2023-05-11 09:09:18 +02:00
hash , err := util . GenerateRandomStringDNSSafe ( hsicHashLength )
2022-10-13 16:01:23 +02:00
if err != nil {
return nil , err
}
2025-07-10 23:38:55 +02:00
hostname := "hs-" + hash
2022-11-02 11:08:54 +01:00
2025-07-24 17:44:09 +02:00
// Get debug port from environment or use default
debugPort := 40000
if envDebugPort := envknob . String ( "HEADSCALE_DEBUG_PORT" ) ; envDebugPort != "" {
if port , err := strconv . Atoi ( envDebugPort ) ; err == nil {
debugPort = port
}
}
2022-11-02 11:08:54 +01:00
hsic := & HeadscaleInContainer {
2025-07-24 17:44:09 +02:00
hostname : hostname ,
port : headscaleDefaultPort ,
debugPort : debugPort ,
2022-11-02 11:08:54 +01:00
2025-03-21 11:49:32 +01:00
pool : pool ,
networks : networks ,
2023-01-05 12:44:28 +01:00
2023-01-10 13:46:42 +02:00
env : DefaultConfigEnv ( ) ,
filesInContainer : [ ] fileInContainer { } ,
2025-03-31 15:55:07 +02:00
policyMode : types . PolicyModeFile ,
2022-11-02 11:08:54 +01:00
}
for _ , opt := range opts {
opt ( hsic )
}
2022-11-14 15:01:31 +01:00
log . Println ( "NAME: " , hsic . hostname )
2022-11-06 20:22:21 +01:00
portProto := fmt . Sprintf ( "%d/tcp" , hsic . port )
2025-07-24 17:44:09 +02:00
debugPortProto := fmt . Sprintf ( "%d/tcp" , hsic . debugPort )
2022-11-02 11:08:54 +01:00
2022-10-13 16:01:23 +02:00
headscaleBuildOptions := & dockertest . BuildOptions {
2024-11-23 22:14:36 +01:00
Dockerfile : IntegrationTestDockerFileName ,
2022-10-13 16:01:23 +02:00
ContextDir : dockerContextPath ,
}
2024-02-18 19:31:29 +01:00
if hsic . postgres {
hsic . env [ "HEADSCALE_DATABASE_TYPE" ] = "postgres"
2025-07-10 23:38:55 +02:00
hsic . env [ "HEADSCALE_DATABASE_POSTGRES_HOST" ] = "postgres-" + hash
2024-02-18 19:31:29 +01:00
hsic . env [ "HEADSCALE_DATABASE_POSTGRES_USER" ] = "headscale"
hsic . env [ "HEADSCALE_DATABASE_POSTGRES_PASS" ] = "headscale"
hsic . env [ "HEADSCALE_DATABASE_POSTGRES_NAME" ] = "headscale"
delete ( hsic . env , "HEADSCALE_DATABASE_SQLITE_PATH" )
2025-06-23 13:43:14 +02:00
pgRunOptions := & dockertest . RunOptions {
2025-07-10 23:38:55 +02:00
Name : "postgres-" + hash ,
2025-06-23 13:43:14 +02:00
Repository : "postgres" ,
Tag : "latest" ,
Networks : networks ,
Env : [ ] string {
"POSTGRES_USER=headscale" ,
"POSTGRES_PASSWORD=headscale" ,
"POSTGRES_DB=headscale" ,
} ,
}
// Add integration test labels if running under hi tool
dockertestutil . DockerAddIntegrationLabels ( pgRunOptions , "postgres" )
2025-07-10 23:38:55 +02:00
2025-06-23 13:43:14 +02:00
pg , err := pool . RunWithOptions ( pgRunOptions )
2024-02-18 19:31:29 +01:00
if err != nil {
return nil , fmt . Errorf ( "starting postgres container: %w" , err )
}
hsic . pgContainer = pg
}
2023-04-27 16:57:11 +02:00
env := [ ] string {
2024-05-24 09:15:34 +01:00
"HEADSCALE_DEBUG_PROFILING_ENABLED=1" ,
"HEADSCALE_DEBUG_PROFILING_PATH=/tmp/profile" ,
2023-07-17 11:13:48 +02:00
"HEADSCALE_DEBUG_DUMP_MAPRESPONSE_PATH=/tmp/mapresponses" ,
2024-05-24 09:15:34 +01:00
"HEADSCALE_DEBUG_DEADLOCK=1" ,
"HEADSCALE_DEBUG_DEADLOCK_TIMEOUT=5s" ,
"HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS=1" ,
"HEADSCALE_DEBUG_DUMP_CONFIG=1" ,
2023-04-27 16:57:11 +02:00
}
2024-11-22 20:23:05 +08:00
if hsic . hasTLS ( ) {
hsic . env [ "HEADSCALE_TLS_CERT_PATH" ] = tlsCertPath
hsic . env [ "HEADSCALE_TLS_KEY_PATH" ] = tlsKeyPath
}
2025-01-26 22:20:11 +01:00
// Server URL and Listen Addr should not be overridable outside of
// the configuration passed to docker.
hsic . env [ "HEADSCALE_SERVER_URL" ] = hsic . GetEndpoint ( )
hsic . env [ "HEADSCALE_LISTEN_ADDR" ] = fmt . Sprintf ( "0.0.0.0:%d" , hsic . port )
2023-01-05 12:44:28 +01:00
for key , value := range hsic . env {
env = append ( env , fmt . Sprintf ( "%s=%s" , key , value ) )
}
log . Printf ( "ENV: \n%s" , spew . Sdump ( hsic . env ) )
2022-10-13 16:01:23 +02:00
runOptions := & dockertest . RunOptions {
2022-11-14 15:01:31 +01:00
Name : hsic . hostname ,
2025-07-24 17:44:09 +02:00
ExposedPorts : append ( [ ] string { portProto , debugPortProto , "9090/tcp" } , hsic . extraPorts ... ) ,
2025-03-21 11:49:32 +01:00
Networks : networks ,
2022-11-02 09:55:48 +01:00
// Cmd: []string{"headscale", "serve"},
// TODO(kradalby): Get rid of this hack, we currently need to give us some
// to inject the headscale configuration further down.
2025-07-24 17:44:09 +02:00
Entrypoint : hsic . buildEntrypoint ( ) ,
2023-01-05 12:44:28 +01:00
Env : env ,
2022-10-13 16:01:23 +02:00
}
2025-07-24 17:44:09 +02:00
// Always bind debug port and metrics port to predictable host ports
if runOptions . PortBindings == nil {
2023-04-13 21:10:08 +00:00
runOptions . PortBindings = map [ docker . Port ] [ ] docker . PortBinding { }
2025-07-24 17:44:09 +02:00
}
runOptions . PortBindings [ docker . Port ( debugPortProto ) ] = [ ] docker . PortBinding {
{ HostPort : strconv . Itoa ( hsic . debugPort ) } ,
}
runOptions . PortBindings [ "9090/tcp" ] = [ ] docker . PortBinding {
{ HostPort : "49090" } ,
}
if len ( hsic . hostPortBindings ) > 0 {
2023-04-13 21:10:08 +00:00
for port , hostPorts := range hsic . hostPortBindings {
runOptions . PortBindings [ docker . Port ( port ) ] = [ ] docker . PortBinding { }
for _ , hostPort := range hostPorts {
runOptions . PortBindings [ docker . Port ( port ) ] = append (
runOptions . PortBindings [ docker . Port ( port ) ] ,
docker . PortBinding { HostPort : hostPort } )
}
}
}
2025-02-05 16:10:18 +01:00
// dockertest isn't very good at handling containers that has already
// been created, this is an attempt to make sure this container isn't
2022-10-13 16:01:23 +02:00
// present.
2022-11-14 15:01:31 +01:00
err = pool . RemoveContainerByName ( hsic . hostname )
2022-10-13 16:01:23 +02:00
if err != nil {
return nil , err
}
2025-06-23 13:43:14 +02:00
// Add integration test labels if running under hi tool
dockertestutil . DockerAddIntegrationLabels ( runOptions , "headscale" )
2025-07-10 23:38:55 +02:00
2022-10-13 16:01:23 +02:00
container , err := pool . BuildAndRunWithBuildOptions (
headscaleBuildOptions ,
runOptions ,
dockertestutil . DockerRestartPolicy ,
dockertestutil . DockerAllowLocalIPv6 ,
dockertestutil . DockerAllowNetworkAdministration ,
)
if err != nil {
2025-10-16 12:17:43 +02:00
// Try to get more detailed build output
log . Printf ( "Docker build failed, attempting to get detailed output..." )
2025-11-28 16:59:54 +01:00
buildOutput , buildErr := dockertestutil . RunDockerBuildForDiagnostics ( dockerContextPath , IntegrationTestDockerFileName )
// Show the last 100 lines of build output to avoid overwhelming the logs
lines := strings . Split ( buildOutput , "\n" )
const maxLines = 100
startLine := 0
if len ( lines ) > maxLines {
startLine = len ( lines ) - maxLines
}
relevantOutput := strings . Join ( lines [ startLine : ] , "\n" )
if buildErr != nil {
// The diagnostic build also failed - this is the real error
return nil , fmt . Errorf ( "could not start headscale container: %w\n\nDocker build failed. Last %d lines of output:\n%s" , err , maxLines , relevantOutput )
}
2025-10-16 12:17:43 +02:00
if buildOutput != "" {
2025-11-28 16:59:54 +01:00
// Build succeeded on retry but container creation still failed
return nil , fmt . Errorf ( "could not start headscale container: %w\n\nDocker build succeeded on retry, but container creation failed. Last %d lines of build output:\n%s" , err , maxLines , relevantOutput )
2025-10-16 12:17:43 +02:00
}
2025-11-28 16:59:54 +01:00
// No output at all - diagnostic build command may have failed
return nil , fmt . Errorf ( "could not start headscale container: %w\n\nUnable to get diagnostic build output (command may have failed silently)" , err )
2022-10-13 16:01:23 +02:00
}
2022-11-14 15:01:31 +01:00
log . Printf ( "Created %s container\n" , hsic . hostname )
2022-10-13 16:01:23 +02:00
2022-11-02 11:08:54 +01:00
hsic . container = container
2025-07-28 11:15:53 +02:00
log . Printf (
"Debug ports for %s: delve=%s, metrics/pprof=49090\n" ,
hsic . hostname ,
hsic . GetHostDebugPort ( ) ,
)
2022-11-02 09:55:48 +01:00
2024-11-22 20:23:05 +08:00
// Write the CA certificates to the container
for i , cert := range hsic . caCerts {
err = hsic . WriteFile ( fmt . Sprintf ( "%s/user-%d.crt" , caCertRoot , i ) , cert )
if err != nil {
return nil , fmt . Errorf ( "failed to write TLS certificate to container: %w" , err )
}
}
2023-01-05 12:44:28 +01:00
err = hsic . WriteFile ( "/etc/headscale/config.yaml" , [ ] byte ( MinimumConfigYAML ( ) ) )
2022-11-02 09:55:48 +01:00
if err != nil {
return nil , fmt . Errorf ( "failed to write headscale config to container: %w" , err )
}
2022-11-02 11:08:54 +01:00
if hsic . aclPolicy != nil {
2025-03-31 15:55:07 +02:00
err = hsic . writePolicy ( hsic . aclPolicy )
2022-11-02 11:08:54 +01:00
if err != nil {
2025-03-31 15:55:07 +02:00
return nil , fmt . Errorf ( "writing policy: %w" , err )
2022-11-02 11:08:54 +01:00
}
}
2022-11-06 20:22:21 +01:00
if hsic . hasTLS ( ) {
err = hsic . WriteFile ( tlsCertPath , hsic . tlsCert )
if err != nil {
return nil , fmt . Errorf ( "failed to write TLS certificate to container: %w" , err )
}
err = hsic . WriteFile ( tlsKeyPath , hsic . tlsKey )
if err != nil {
return nil , fmt . Errorf ( "failed to write TLS key to container: %w" , err )
}
}
2023-01-10 13:46:42 +02:00
for _ , f := range hsic . filesInContainer {
if err := hsic . WriteFile ( f . path , f . contents ) ; err != nil {
return nil , fmt . Errorf ( "failed to write %q: %w" , f . path , err )
}
}
2025-03-31 15:55:07 +02:00
// Load the database from policy file on repeat until it succeeds,
// this is done as the container sleeps before starting headscale.
if hsic . aclPolicy != nil && hsic . policyMode == types . PolicyModeDB {
err := pool . Retry ( hsic . reloadDatabasePolicy )
if err != nil {
return nil , fmt . Errorf ( "loading database policy on startup: %w" , err )
}
}
2022-11-02 09:55:48 +01:00
return hsic , nil
2022-10-13 16:01:23 +02:00
}
2023-04-23 11:02:28 +00:00
func ( t * HeadscaleInContainer ) ConnectToNetwork ( network * dockertest . Network ) error {
return t . container . ConnectToNetwork ( network )
}
2022-11-06 20:22:21 +01:00
func ( t * HeadscaleInContainer ) hasTLS ( ) bool {
return len ( t . tlsCert ) != 0 && len ( t . tlsKey ) != 0
}
2023-02-03 12:24:27 +01:00
// Shutdown stops and cleans up the Headscale container.
2024-09-11 12:00:32 +02:00
func ( t * HeadscaleInContainer ) Shutdown ( ) ( string , string , error ) {
stdoutPath , stderrPath , err := t . SaveLog ( "/tmp/control" )
2023-04-27 16:57:11 +02:00
if err != nil {
log . Printf (
"Failed to save log from control: %s" ,
fmt . Errorf ( "failed to save log from control: %w" , err ) ,
)
}
2024-05-24 09:15:34 +01:00
err = t . SaveMetrics ( fmt . Sprintf ( "/tmp/control/%s_metrics.txt" , t . hostname ) )
2024-04-27 10:47:39 +02:00
if err != nil {
log . Printf (
"Failed to metrics from control: %s" ,
err ,
)
}
2023-04-27 16:57:11 +02:00
// Send a interrupt signal to the "headscale" process inside the container
// allowing it to shut down gracefully and flush the profile to disk.
// The container will live for a bit longer due to the sleep at the end.
err = t . SendInterrupt ( )
if err != nil {
log . Printf (
"Failed to send graceful interrupt to control: %s" ,
fmt . Errorf ( "failed to send graceful interrupt to control: %w" , err ) ,
)
}
err = t . SaveProfile ( "/tmp/control" )
if err != nil {
log . Printf (
"Failed to save profile from control: %s" ,
fmt . Errorf ( "failed to save profile from control: %w" , err ) ,
)
}
2023-07-17 11:13:48 +02:00
err = t . SaveMapResponses ( "/tmp/control" )
if err != nil {
log . Printf (
"Failed to save mapresponses from control: %s" ,
fmt . Errorf ( "failed to save mapresponses from control: %w" , err ) ,
)
}
2024-02-18 19:31:29 +01:00
// We dont have a database to save if we use postgres
if ! t . postgres {
err = t . SaveDatabase ( "/tmp/control" )
if err != nil {
log . Printf (
"Failed to save database from control: %s" ,
fmt . Errorf ( "failed to save database from control: %w" , err ) ,
)
}
}
// Cleanup postgres container if enabled.
if t . postgres {
t . pool . Purge ( t . pgContainer )
2023-11-16 17:55:29 +01:00
}
2024-09-11 12:00:32 +02:00
return stdoutPath , stderrPath , t . pool . Purge ( t . container )
2022-10-13 16:01:23 +02:00
}
2024-09-21 12:05:36 +02:00
// WriteLogs writes the current stdout/stderr log of the container to
// the given io.Writers.
func ( t * HeadscaleInContainer ) WriteLogs ( stdout , stderr io . Writer ) error {
return dockertestutil . WriteLog ( t . pool , t . container , stdout , stderr )
}
2023-02-03 12:24:27 +01:00
// SaveLog saves the current stdout log of the container to a path
// on the host system.
2024-09-11 12:00:32 +02:00
func ( t * HeadscaleInContainer ) SaveLog ( path string ) ( string , string , error ) {
2023-01-30 10:20:08 +01:00
return dockertestutil . SaveLog ( t . pool , t . container , path )
}
2024-04-27 10:47:39 +02:00
func ( t * HeadscaleInContainer ) SaveMetrics ( savePath string ) error {
resp , err := http . Get ( fmt . Sprintf ( "http://%s:9090/metrics" , t . hostname ) )
if err != nil {
return fmt . Errorf ( "getting metrics: %w" , err )
}
defer resp . Body . Close ( )
out , err := os . Create ( savePath )
if err != nil {
return fmt . Errorf ( "creating file for metrics: %w" , err )
}
defer out . Close ( )
_ , err = io . Copy ( out , resp . Body )
if err != nil {
return fmt . Errorf ( "copy response to file: %w" , err )
}
return nil
}
2025-06-23 13:43:14 +02:00
// extractTarToDirectory extracts a tar archive to a directory.
func extractTarToDirectory ( tarData [ ] byte , targetDir string ) error {
2025-07-10 23:38:55 +02:00
if err := os . MkdirAll ( targetDir , 0 o755 ) ; err != nil {
2025-06-23 13:43:14 +02:00
return fmt . Errorf ( "failed to create directory %s: %w" , targetDir , err )
}
tarReader := tar . NewReader ( bytes . NewReader ( tarData ) )
2025-08-27 17:09:13 +02:00
// Find the top-level directory to strip
var topLevelDir string
firstPass := tar . NewReader ( bytes . NewReader ( tarData ) )
for {
header , err := firstPass . Next ( )
if err == io . EOF {
break
}
if err != nil {
return fmt . Errorf ( "failed to read tar header: %w" , err )
}
if header . Typeflag == tar . TypeDir && topLevelDir == "" {
topLevelDir = strings . TrimSuffix ( header . Name , "/" )
break
}
}
tarReader = tar . NewReader ( bytes . NewReader ( tarData ) )
2025-06-23 13:43:14 +02:00
for {
header , err := tarReader . Next ( )
if err == io . EOF {
break
}
if err != nil {
return fmt . Errorf ( "failed to read tar header: %w" , err )
}
// Clean the path to prevent directory traversal
cleanName := filepath . Clean ( header . Name )
if strings . Contains ( cleanName , ".." ) {
continue // Skip potentially dangerous paths
}
2025-08-27 17:09:13 +02:00
// Strip the top-level directory
if topLevelDir != "" && strings . HasPrefix ( cleanName , topLevelDir + "/" ) {
cleanName = strings . TrimPrefix ( cleanName , topLevelDir + "/" )
} else if cleanName == topLevelDir {
// Skip the top-level directory itself
continue
}
2025-08-27 16:11:36 +02:00
2025-08-27 17:09:13 +02:00
// Skip empty paths after stripping
if cleanName == "" {
continue
}
targetPath := filepath . Join ( targetDir , cleanName )
2025-06-23 13:43:14 +02:00
switch header . Typeflag {
case tar . TypeDir :
// Create directory
if err := os . MkdirAll ( targetPath , os . FileMode ( header . Mode ) ) ; err != nil {
return fmt . Errorf ( "failed to create directory %s: %w" , targetPath , err )
}
case tar . TypeReg :
2025-08-27 17:09:13 +02:00
// Ensure parent directories exist
if err := os . MkdirAll ( filepath . Dir ( targetPath ) , 0 o755 ) ; err != nil {
return fmt . Errorf ( "failed to create parent directories for %s: %w" , targetPath , err )
}
2025-08-27 16:11:36 +02:00
2025-06-23 13:43:14 +02:00
// Create file
outFile , err := os . Create ( targetPath )
if err != nil {
return fmt . Errorf ( "failed to create file %s: %w" , targetPath , err )
}
if _ , err := io . Copy ( outFile , tarReader ) ; err != nil {
outFile . Close ( )
return fmt . Errorf ( "failed to copy file contents: %w" , err )
}
outFile . Close ( )
// Set file permissions
if err := os . Chmod ( targetPath , os . FileMode ( header . Mode ) ) ; err != nil {
return fmt . Errorf ( "failed to set file permissions: %w" , err )
}
}
}
return nil
}
2023-04-27 16:57:11 +02:00
func ( t * HeadscaleInContainer ) SaveProfile ( savePath string ) error {
tarFile , err := t . FetchPath ( "/tmp/profile" )
if err != nil {
return err
}
2025-08-27 17:09:13 +02:00
targetDir := path . Join ( savePath , "pprof" )
2025-07-10 23:38:55 +02:00
2025-06-23 13:43:14 +02:00
return extractTarToDirectory ( tarFile , targetDir )
}
func ( t * HeadscaleInContainer ) SaveMapResponses ( savePath string ) error {
tarFile , err := t . FetchPath ( "/tmp/mapresponses" )
2023-07-17 11:13:48 +02:00
if err != nil {
return err
}
2025-08-27 17:09:13 +02:00
targetDir := path . Join ( savePath , "mapresponses" )
2025-07-10 23:38:55 +02:00
2025-06-23 13:43:14 +02:00
return extractTarToDirectory ( tarFile , targetDir )
2023-07-17 11:13:48 +02:00
}
2025-06-23 13:43:14 +02:00
func ( t * HeadscaleInContainer ) SaveDatabase ( savePath string ) error {
// If using PostgreSQL, skip database file extraction
if t . postgres {
return nil
}
// Also check for any .sqlite files
sqliteFiles , err := t . Execute ( [ ] string { "find" , "/tmp" , "-name" , "*.sqlite*" , "-type" , "f" } )
2023-04-27 16:57:11 +02:00
if err != nil {
2025-06-23 13:43:14 +02:00
log . Printf ( "Warning: could not find sqlite files: %v" , err )
} else {
log . Printf ( "SQLite files found in %s:\n%s" , t . hostname , sqliteFiles )
2023-04-27 16:57:11 +02:00
}
2025-06-23 13:43:14 +02:00
// Check if the database file exists and has a schema
dbPath := "/tmp/integration_test_db.sqlite3"
fileInfo , err := t . Execute ( [ ] string { "ls" , "-la" , dbPath } )
if err != nil {
return fmt . Errorf ( "database file does not exist at %s: %w" , dbPath , err )
}
log . Printf ( "Database file info: %s" , fileInfo )
2023-04-27 16:57:11 +02:00
2025-06-23 13:43:14 +02:00
// Check if the database has any tables (schema)
schemaCheck , err := t . Execute ( [ ] string { "sqlite3" , dbPath , ".schema" } )
2023-11-16 17:55:29 +01:00
if err != nil {
2025-06-23 13:43:14 +02:00
return fmt . Errorf ( "failed to check database schema (sqlite3 command failed): %w" , err )
2023-11-16 17:55:29 +01:00
}
2025-07-10 23:38:55 +02:00
2025-06-23 13:43:14 +02:00
if strings . TrimSpace ( schemaCheck ) == "" {
2025-07-10 23:38:55 +02:00
return errors . New ( "database file exists but has no schema (empty database)" )
2025-06-23 13:43:14 +02:00
}
2025-07-10 23:38:55 +02:00
2025-06-23 13:43:14 +02:00
tarFile , err := t . FetchPath ( "/tmp/integration_test_db.sqlite3" )
2023-11-16 17:55:29 +01:00
if err != nil {
2025-06-23 13:43:14 +02:00
return fmt . Errorf ( "failed to fetch database file: %w" , err )
2023-11-16 17:55:29 +01:00
}
2025-06-23 13:43:14 +02:00
// For database, extract the first regular file (should be the SQLite file)
tarReader := tar . NewReader ( bytes . NewReader ( tarFile ) )
for {
header , err := tarReader . Next ( )
if err == io . EOF {
break
}
if err != nil {
return fmt . Errorf ( "failed to read tar header: %w" , err )
}
2025-07-28 11:15:53 +02:00
log . Printf (
"Found file in tar: %s (type: %d, size: %d)" ,
header . Name ,
header . Typeflag ,
header . Size ,
)
2025-06-23 13:43:14 +02:00
// Extract the first regular file we find
if header . Typeflag == tar . TypeReg {
dbPath := path . Join ( savePath , t . hostname + ".db" )
outFile , err := os . Create ( dbPath )
if err != nil {
return fmt . Errorf ( "failed to create database file: %w" , err )
}
written , err := io . Copy ( outFile , tarReader )
outFile . Close ( )
if err != nil {
return fmt . Errorf ( "failed to copy database file: %w" , err )
}
2025-07-28 11:15:53 +02:00
log . Printf (
"Extracted database file: %s (%d bytes written, header claimed %d bytes)" ,
dbPath ,
written ,
header . Size ,
)
2025-06-23 13:43:14 +02:00
// Check if we actually wrote something
if written == 0 {
2025-07-28 11:15:53 +02:00
return fmt . Errorf (
"database file is empty (size: %d, header size: %d)" ,
written ,
header . Size ,
)
2025-06-23 13:43:14 +02:00
}
return nil
}
}
2025-07-10 23:38:55 +02:00
return errors . New ( "no regular file found in database tar archive" )
2023-11-16 17:55:29 +01:00
}
2023-02-03 12:24:27 +01:00
// Execute runs a command inside the Headscale container and returns the
// result of stdout as a string.
2022-10-24 16:40:49 +02:00
func ( t * HeadscaleInContainer ) Execute (
command [ ] string ,
) ( string , error ) {
stdout , stderr , err := dockertestutil . ExecuteCommand (
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
2024-12-10 16:23:55 +01:00
log . Printf ( "command: %v" , command )
2022-10-24 16:40:49 +02:00
log . Printf ( "command stderr: %s\n" , stderr )
2022-11-14 09:56:54 +01:00
if stdout != "" {
log . Printf ( "command stdout: %s\n" , stdout )
}
2022-10-24 16:40:49 +02:00
2024-08-30 16:58:29 +02:00
return stdout , fmt . Errorf ( "executing command in docker: %w, stderr: %s" , err , stderr )
2022-10-24 16:40:49 +02:00
}
return stdout , nil
}
2023-02-03 12:24:27 +01:00
// GetPort returns the docker container port as a string.
2022-10-13 16:01:23 +02:00
func ( t * HeadscaleInContainer ) GetPort ( ) string {
2025-07-10 23:38:55 +02:00
return strconv . Itoa ( t . port )
2022-10-13 16:01:23 +02:00
}
2025-07-24 17:44:09 +02:00
// GetDebugPort returns the debug port as a string.
func ( t * HeadscaleInContainer ) GetDebugPort ( ) string {
return strconv . Itoa ( t . debugPort )
}
// GetHostDebugPort returns the host port mapped to the debug port.
func ( t * HeadscaleInContainer ) GetHostDebugPort ( ) string {
return strconv . Itoa ( t . debugPort )
}
2023-02-03 12:24:27 +01:00
// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer
// instance.
2022-10-13 16:01:23 +02:00
func ( t * HeadscaleInContainer ) GetHealthEndpoint ( ) string {
2025-07-10 23:38:55 +02:00
return t . GetEndpoint ( ) + "/health"
2022-10-13 16:01:23 +02:00
}
2023-02-03 12:24:27 +01:00
// GetEndpoint returns the Headscale endpoint for the HeadscaleInContainer.
2022-10-13 16:01:23 +02:00
func ( t * HeadscaleInContainer ) GetEndpoint ( ) string {
2025-08-06 08:37:02 +02:00
return t . getEndpoint ( false )
}
// GetIPEndpoint returns the Headscale endpoint using IP address instead of hostname.
func ( t * HeadscaleInContainer ) GetIPEndpoint ( ) string {
return t . getEndpoint ( true )
}
// getEndpoint returns the Headscale endpoint, optionally using IP address instead of hostname.
func ( t * HeadscaleInContainer ) getEndpoint ( useIP bool ) string {
var host string
if useIP && len ( t . networks ) > 0 {
// Use IP address from the first network
host = t . GetIPInNetwork ( t . networks [ 0 ] )
} else {
host = t . GetHostname ( )
}
hostEndpoint := fmt . Sprintf ( "%s:%d" , host , t . port )
2022-10-13 16:01:23 +02:00
2022-11-06 20:22:21 +01:00
if t . hasTLS ( ) {
2025-07-10 23:38:55 +02:00
return "https://" + hostEndpoint
2022-11-06 20:22:21 +01:00
}
2025-07-10 23:38:55 +02:00
return "http://" + hostEndpoint
2022-10-13 16:01:23 +02:00
}
2023-02-03 12:24:27 +01:00
// GetCert returns the public certificate of the HeadscaleInContainer.
2022-11-06 20:22:21 +01:00
func ( t * HeadscaleInContainer ) GetCert ( ) [ ] byte {
return t . tlsCert
}
2023-02-03 12:24:27 +01:00
// GetHostname returns the hostname of the HeadscaleInContainer.
2022-11-06 20:22:21 +01:00
func ( t * HeadscaleInContainer ) GetHostname ( ) string {
return t . hostname
}
2025-08-06 08:37:02 +02:00
// GetIPInNetwork returns the IP address of the HeadscaleInContainer in the given network.
func ( t * HeadscaleInContainer ) GetIPInNetwork ( network * dockertest . Network ) string {
return t . container . GetIPInNetwork ( network )
}
2023-08-29 08:33:33 +02:00
// WaitForRunning blocks until the Headscale instance is ready to
2023-02-03 12:24:27 +01:00
// serve clients.
2023-08-29 08:33:33 +02:00
func ( t * HeadscaleInContainer ) WaitForRunning ( ) error {
2022-10-13 16:01:23 +02:00
url := t . GetHealthEndpoint ( )
2022-10-18 11:58:15 +02:00
log . Printf ( "waiting for headscale to be ready at %s" , url )
2022-11-06 20:22:21 +01:00
client := & http . Client { }
if t . hasTLS ( ) {
2022-11-10 08:04:47 +00:00
insecureTransport := http . DefaultTransport . ( * http . Transport ) . Clone ( ) //nolint
insecureTransport . TLSClientConfig = & tls . Config { InsecureSkipVerify : true } //nolint
2022-11-06 20:22:21 +01:00
client = & http . Client { Transport : insecureTransport }
}
2022-10-13 16:01:23 +02:00
return t . pool . Retry ( func ( ) error {
2022-11-06 20:22:21 +01:00
resp , err := client . Get ( url ) //nolint
2022-10-13 16:01:23 +02:00
if err != nil {
return fmt . Errorf ( "headscale is not ready: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return errHeadscaleStatusCodeNotOk
}
return nil
} )
}
2023-02-03 12:24:27 +01:00
// CreateUser adds a new user to the Headscale instance.
2023-01-17 17:43:44 +01:00
func ( t * HeadscaleInContainer ) CreateUser (
user string ,
2025-04-30 12:45:08 +03:00
) ( * v1 . User , error ) {
2025-07-28 11:15:53 +02:00
command := [ ] string {
"headscale" ,
"users" ,
"create" ,
user ,
fmt . Sprintf ( "--email=%s@test.no" , user ) ,
"--output" ,
"json" ,
}
2022-10-13 16:01:23 +02:00
2025-04-30 12:45:08 +03:00
result , _ , err := dockertestutil . ExecuteCommand (
2022-10-13 16:01:23 +02:00
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
2025-04-30 12:45:08 +03:00
return nil , err
2022-10-13 16:01:23 +02:00
}
2025-04-30 12:45:08 +03:00
var u v1 . User
err = json . Unmarshal ( [ ] byte ( result ) , & u )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal user: %w" , err )
}
return & u , nil
2022-10-13 16:01:23 +02:00
}
2023-02-03 12:24:27 +01:00
// CreateAuthKey creates a new "authorisation key" for a User that can be used
// to authorise a TailscaleClient with the Headscale instance.
2022-10-13 16:01:23 +02:00
func ( t * HeadscaleInContainer ) CreateAuthKey (
2025-04-30 12:45:08 +03:00
user uint64 ,
2022-12-27 19:05:21 +00:00
reusable bool ,
ephemeral bool ,
2022-10-13 16:01:23 +02:00
) ( * v1 . PreAuthKey , error ) {
command := [ ] string {
"headscale" ,
2023-01-17 17:43:44 +01:00
"--user" ,
2025-04-30 12:45:08 +03:00
strconv . FormatUint ( user , 10 ) ,
2022-10-13 16:01:23 +02:00
"preauthkeys" ,
"create" ,
"--expiration" ,
"24h" ,
"--output" ,
"json" ,
}
2022-12-27 19:05:21 +00:00
if reusable {
command = append ( command , "--reusable" )
}
if ephemeral {
command = append ( command , "--ephemeral" )
}
2022-10-13 16:01:23 +02:00
result , _ , err := dockertestutil . ExecuteCommand (
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
return nil , fmt . Errorf ( "failed to execute create auth key command: %w" , err )
}
var preAuthKey v1 . PreAuthKey
err = json . Unmarshal ( [ ] byte ( result ) , & preAuthKey )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal auth key: %w" , err )
}
return & preAuthKey , nil
}
2025-02-01 09:16:51 +00:00
// ListNodes lists the currently registered Nodes in headscale.
// Optionally a list of usernames can be passed to get users for
// specific users.
func ( t * HeadscaleInContainer ) ListNodes (
users ... string ,
2023-09-24 13:42:05 +02:00
) ( [ ] * v1 . Node , error ) {
2025-02-01 09:16:51 +00:00
var ret [ ] * v1 . Node
execUnmarshal := func ( command [ ] string ) error {
result , _ , err := dockertestutil . ExecuteCommand (
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
return fmt . Errorf ( "failed to execute list node command: %w" , err )
}
var nodes [ ] * v1 . Node
err = json . Unmarshal ( [ ] byte ( result ) , & nodes )
if err != nil {
return fmt . Errorf ( "failed to unmarshal nodes: %w" , err )
}
ret = append ( ret , nodes ... )
2025-07-10 23:38:55 +02:00
2025-02-01 09:16:51 +00:00
return nil
}
if len ( users ) == 0 {
err := execUnmarshal ( [ ] string { "headscale" , "nodes" , "list" , "--output" , "json" } )
if err != nil {
return nil , err
}
} else {
for _ , user := range users {
command := [ ] string { "headscale" , "--user" , user , "nodes" , "list" , "--output" , "json" }
err := execUnmarshal ( command )
if err != nil {
return nil , err
}
}
}
sort . Slice ( ret , func ( i , j int ) bool {
return cmp . Compare ( ret [ i ] . GetId ( ) , ret [ j ] . GetId ( ) ) == - 1
} )
2025-07-10 23:38:55 +02:00
2025-02-01 09:16:51 +00:00
return ret , nil
}
2025-10-23 17:57:41 +02:00
func ( t * HeadscaleInContainer ) DeleteNode ( nodeID uint64 ) error {
command := [ ] string {
"headscale" ,
"nodes" ,
"delete" ,
"--identifier" ,
fmt . Sprintf ( "%d" , nodeID ) ,
"--output" ,
"json" ,
"--force" ,
}
_ , _ , err := dockertestutil . ExecuteCommand (
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
return fmt . Errorf ( "failed to execute delete node command: %w" , err )
}
return nil
}
2025-05-04 22:52:47 +03:00
func ( t * HeadscaleInContainer ) NodesByUser ( ) ( map [ string ] [ ] * v1 . Node , error ) {
nodes , err := t . ListNodes ( )
if err != nil {
return nil , err
}
var userMap map [ string ] [ ] * v1 . Node
for _ , node := range nodes {
2025-07-10 23:38:55 +02:00
if _ , ok := userMap [ node . GetUser ( ) . GetName ( ) ] ; ! ok {
mak . Set ( & userMap , node . GetUser ( ) . GetName ( ) , [ ] * v1 . Node { node } )
2025-05-04 22:52:47 +03:00
} else {
2025-07-10 23:38:55 +02:00
userMap [ node . GetUser ( ) . GetName ( ) ] = append ( userMap [ node . GetUser ( ) . GetName ( ) ] , node )
2025-05-04 22:52:47 +03:00
}
}
return userMap , nil
}
func ( t * HeadscaleInContainer ) NodesByName ( ) ( map [ string ] * v1 . Node , error ) {
nodes , err := t . ListNodes ( )
if err != nil {
return nil , err
}
var nameMap map [ string ] * v1 . Node
for _ , node := range nodes {
mak . Set ( & nameMap , node . GetName ( ) , node )
}
return nameMap , nil
}
2025-02-01 09:16:51 +00:00
// ListUsers returns a list of users from Headscale.
func ( t * HeadscaleInContainer ) ListUsers ( ) ( [ ] * v1 . User , error ) {
command := [ ] string { "headscale" , "users" , "list" , "--output" , "json" }
2022-10-13 16:01:23 +02:00
result , _ , err := dockertestutil . ExecuteCommand (
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
return nil , fmt . Errorf ( "failed to execute list node command: %w" , err )
}
2025-02-01 09:16:51 +00:00
var users [ ] * v1 . User
err = json . Unmarshal ( [ ] byte ( result ) , & users )
2022-10-13 16:01:23 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal nodes: %w" , err )
}
2025-02-01 09:16:51 +00:00
return users , nil
2022-10-13 16:01:23 +02:00
}
2022-11-02 09:55:09 +01:00
2025-04-30 12:45:08 +03:00
// MapUsers returns a map of users from Headscale. It is keyed by the
// user name.
func ( t * HeadscaleInContainer ) MapUsers ( ) ( map [ string ] * v1 . User , error ) {
users , err := t . ListUsers ( )
if err != nil {
return nil , err
}
var userMap map [ string ] * v1 . User
for _ , user := range users {
2025-07-10 23:38:55 +02:00
mak . Set ( & userMap , user . GetName ( ) , user )
2025-04-30 12:45:08 +03:00
}
return userMap , nil
}
2025-05-20 13:57:26 +02:00
func ( h * HeadscaleInContainer ) SetPolicy ( pol * policyv2 . Policy ) error {
2025-03-31 15:55:07 +02:00
err := h . writePolicy ( pol )
if err != nil {
return fmt . Errorf ( "writing policy file: %w" , err )
}
switch h . policyMode {
case types . PolicyModeDB :
err := h . reloadDatabasePolicy ( )
if err != nil {
return fmt . Errorf ( "reloading database policy: %w" , err )
}
case types . PolicyModeFile :
err := h . Reload ( )
if err != nil {
return fmt . Errorf ( "reloading policy file: %w" , err )
}
default :
panic ( "policy mode is not valid: " + h . policyMode )
}
return nil
}
func ( h * HeadscaleInContainer ) reloadDatabasePolicy ( ) error {
_ , err := h . Execute (
[ ] string {
"headscale" ,
"policy" ,
"set" ,
"-f" ,
aclPolicyPath ,
} ,
)
if err != nil {
return fmt . Errorf ( "setting policy with db command: %w" , err )
}
return nil
}
2025-05-20 13:57:26 +02:00
func ( h * HeadscaleInContainer ) writePolicy ( pol * policyv2 . Policy ) error {
2025-03-31 15:55:07 +02:00
pBytes , err := json . Marshal ( pol )
if err != nil {
return fmt . Errorf ( "marshalling pol: %w" , err )
}
err = h . WriteFile ( aclPolicyPath , pBytes )
if err != nil {
return fmt . Errorf ( "writing policy to headscale container: %w" , err )
}
return nil
}
func ( h * HeadscaleInContainer ) PID ( ) ( int , error ) {
2025-10-27 12:08:52 +01:00
// Use pidof to find the headscale process, which is more reliable than grep
// as it only looks for the actual binary name, not processes that contain
// "headscale" in their command line (like the dlv debugger).
output , err := h . Execute ( [ ] string { "pidof" , "headscale" } )
2025-03-31 15:55:07 +02:00
if err != nil {
2025-10-27 12:08:52 +01:00
// pidof returns exit code 1 when no process is found
return 0 , os . ErrNotExist
2025-03-31 15:55:07 +02:00
}
2025-10-27 12:08:52 +01:00
// pidof returns space-separated PIDs on a single line
pidStrs := strings . Fields ( strings . TrimSpace ( output ) )
if len ( pidStrs ) == 0 {
return 0 , os . ErrNotExist
2025-03-31 15:55:07 +02:00
}
2025-10-27 12:08:52 +01:00
pids := make ( [ ] int , 0 , len ( pidStrs ) )
for _ , pidStr := range pidStrs {
pidInt , err := strconv . Atoi ( pidStr )
2025-03-31 15:55:07 +02:00
if err != nil {
2025-10-27 12:08:52 +01:00
return 0 , fmt . Errorf ( "parsing PID %q: %w" , pidStr , err )
2025-03-31 15:55:07 +02:00
}
// We dont care about the root pid for the container
if pidInt == 1 {
continue
}
pids = append ( pids , pidInt )
}
switch len ( pids ) {
case 0 :
return 0 , os . ErrNotExist
case 1 :
return pids [ 0 ] , nil
default :
2025-10-27 12:08:52 +01:00
// If we still have multiple PIDs, return the first one as a fallback
// This can happen in edge cases during startup/shutdown
return pids [ 0 ] , nil
2025-03-31 15:55:07 +02:00
}
}
// Reload sends a SIGHUP to the headscale process to reload internals,
// for example Policy from file.
func ( h * HeadscaleInContainer ) Reload ( ) error {
pid , err := h . PID ( )
if err != nil {
return fmt . Errorf ( "getting headscale PID: %w" , err )
}
_ , err = h . Execute ( [ ] string { "kill" , "-HUP" , strconv . Itoa ( pid ) } )
if err != nil {
return fmt . Errorf ( "reloading headscale with HUP: %w" , err )
}
return nil
}
2025-02-26 07:22:55 -08:00
// ApproveRoutes approves routes for a node.
func ( t * HeadscaleInContainer ) ApproveRoutes ( id uint64 , routes [ ] netip . Prefix ) ( * v1 . Node , error ) {
command := [ ] string {
"headscale" , "nodes" , "approve-routes" ,
"--output" , "json" ,
"--identifier" , strconv . FormatUint ( id , 10 ) ,
2025-07-10 23:38:55 +02:00
"--routes=" + strings . Join ( util . PrefixesToString ( routes ) , "," ) ,
2025-02-26 07:22:55 -08:00
}
result , _ , err := dockertestutil . ExecuteCommand (
t . container ,
command ,
[ ] string { } ,
)
if err != nil {
2025-07-28 11:15:53 +02:00
return nil , fmt . Errorf (
"failed to execute approve routes command (node %d, routes %v): %w" ,
id ,
routes ,
err ,
)
2025-02-26 07:22:55 -08:00
}
var node * v1 . Node
err = json . Unmarshal ( [ ] byte ( result ) , & node )
if err != nil {
2025-07-28 11:15:53 +02:00
return nil , fmt . Errorf ( "failed to unmarshal node response: %q, error: %w" , result , err )
2025-02-26 07:22:55 -08:00
}
return node , nil
}
2023-02-03 12:24:27 +01:00
// WriteFile save file inside the Headscale container.
2022-11-02 09:55:09 +01:00
func ( t * HeadscaleInContainer ) WriteFile ( path string , data [ ] byte ) error {
2022-11-06 20:22:21 +01:00
return integrationutil . WriteFileToContainer ( t . pool , t . container , path , data )
}
2022-11-02 09:55:09 +01:00
2023-04-27 16:57:11 +02:00
// FetchPath gets a path from inside the Headscale container and returns a tar
// file as byte array.
func ( t * HeadscaleInContainer ) FetchPath ( path string ) ( [ ] byte , error ) {
return integrationutil . FetchPathFromContainer ( t . pool , t . container , path )
}
func ( t * HeadscaleInContainer ) SendInterrupt ( ) error {
pid , err := t . Execute ( [ ] string { "pidof" , "headscale" } )
if err != nil {
return err
}
_ , err = t . Execute ( [ ] string { "kill" , "-2" , strings . Trim ( pid , "'\n" ) } )
if err != nil {
return err
}
return nil
}
2025-08-27 17:09:13 +02:00
func ( t * HeadscaleInContainer ) GetAllMapReponses ( ) ( map [ types . NodeID ] [ ] tailcfg . MapResponse , error ) {
// Execute curl inside the container to access the debug endpoint locally
command := [ ] string {
"curl" , "-s" , "-H" , "Accept: application/json" , "http://localhost:9090/debug/mapresponses" ,
}
result , err := t . Execute ( command )
if err != nil {
return nil , fmt . Errorf ( "fetching mapresponses from debug endpoint: %w" , err )
}
var res map [ types . NodeID ] [ ] tailcfg . MapResponse
if err := json . Unmarshal ( [ ] byte ( result ) , & res ) ; err != nil {
return nil , fmt . Errorf ( "decoding routes response: %w" , err )
}
return res , nil
}
2025-08-06 08:37:02 +02:00
// PrimaryRoutes fetches the primary routes from the debug endpoint.
func ( t * HeadscaleInContainer ) PrimaryRoutes ( ) ( * routes . DebugRoutes , error ) {
// Execute curl inside the container to access the debug endpoint locally
command := [ ] string {
"curl" , "-s" , "-H" , "Accept: application/json" , "http://localhost:9090/debug/routes" ,
}
result , err := t . Execute ( command )
if err != nil {
return nil , fmt . Errorf ( "fetching routes from debug endpoint: %w" , err )
}
var debugRoutes routes . DebugRoutes
if err := json . Unmarshal ( [ ] byte ( result ) , & debugRoutes ) ; err != nil {
return nil , fmt . Errorf ( "decoding routes response: %w" , err )
}
return & debugRoutes , nil
}
// DebugBatcher fetches the batcher debug information from the debug endpoint.
func ( t * HeadscaleInContainer ) DebugBatcher ( ) ( * hscontrol . DebugBatcherInfo , error ) {
// Execute curl inside the container to access the debug endpoint locally
command := [ ] string {
"curl" , "-s" , "-H" , "Accept: application/json" , "http://localhost:9090/debug/batcher" ,
}
result , err := t . Execute ( command )
if err != nil {
return nil , fmt . Errorf ( "fetching batcher debug info: %w" , err )
}
var debugInfo hscontrol . DebugBatcherInfo
if err := json . Unmarshal ( [ ] byte ( result ) , & debugInfo ) ; err != nil {
return nil , fmt . Errorf ( "decoding batcher debug response: %w" , err )
}
return & debugInfo , nil
}
// DebugNodeStore fetches the NodeStore data from the debug endpoint.
func ( t * HeadscaleInContainer ) DebugNodeStore ( ) ( map [ types . NodeID ] types . Node , error ) {
// Execute curl inside the container to access the debug endpoint locally
command := [ ] string {
"curl" , "-s" , "-H" , "Accept: application/json" , "http://localhost:9090/debug/nodestore" ,
}
result , err := t . Execute ( command )
if err != nil {
return nil , fmt . Errorf ( "fetching nodestore debug info: %w" , err )
}
var nodeStore map [ types . NodeID ] types . Node
if err := json . Unmarshal ( [ ] byte ( result ) , & nodeStore ) ; err != nil {
return nil , fmt . Errorf ( "decoding nodestore debug response: %w" , err )
}
return nodeStore , nil
}
2025-10-23 17:57:41 +02:00
// DebugFilter fetches the current filter rules from the debug endpoint.
func ( t * HeadscaleInContainer ) DebugFilter ( ) ( [ ] tailcfg . FilterRule , error ) {
// Execute curl inside the container to access the debug endpoint locally
command := [ ] string {
"curl" , "-s" , "-H" , "Accept: application/json" , "http://localhost:9090/debug/filter" ,
}
result , err := t . Execute ( command )
if err != nil {
return nil , fmt . Errorf ( "fetching filter from debug endpoint: %w" , err )
}
var filterRules [ ] tailcfg . FilterRule
if err := json . Unmarshal ( [ ] byte ( result ) , & filterRules ) ; err != nil {
return nil , fmt . Errorf ( "decoding filter response: %w" , err )
}
return filterRules , nil
}
// DebugPolicy fetches the current policy from the debug endpoint.
func ( t * HeadscaleInContainer ) DebugPolicy ( ) ( string , error ) {
// Execute curl inside the container to access the debug endpoint locally
command := [ ] string {
"curl" , "-s" , "http://localhost:9090/debug/policy" ,
}
result , err := t . Execute ( command )
if err != nil {
return "" , fmt . Errorf ( "fetching policy from debug endpoint: %w" , err )
}
return result , nil
}