Headscale — Structure#
Layout pattern#
Custom Layout — close to Standard Go Layout but with notable departures.
The project uses cmd/ for binaries and hscontrol/ as the core control-plane package (project-specific name, not internal/). There is no pkg/ directory. The root module package (package headscale) exists for a single file — a Swagger UI handler that embeds the generated OpenAPI spec. The integration test suite lives at the top level as a peer directory alongside cmd/ and hscontrol/, signaling it is a first-class concern. Generated protobuf code is isolated in gen/. The absence of internal/ is intentional: hscontrol/ is technically public, though it carries an implicit boundary through its name.
Directory map#
headscale/ # Module: github.com/juanfont/headscale
├── cmd/ # Command-line application entry points
│ ├── headscale/ # Primary server binary
│ │ ├── headscale.go # main(): zerolog init, delegates to cli.Execute()
│ │ └── cli/ # Cobra subcommands (serve, users, nodes, keys, policy…)
│ ├── hi/ # "Headscale Integration" Docker test runner (dev only)
│ └── mapresponses/ # Debug tool: reads and compares saved MapResponse files
├── hscontrol/ # Core control-plane logic (~168 .go files)
│ ├── app.go # Server wiring: HTTP+gRPC mux, TLS, lifecycle
│ ├── poll.go # Tailscale MapRequest/MapResponse protocol (hot path)
│ ├── noise.go # Tailscale Noise protocol handshake
│ ├── auth.go # Node authentication (web flow, OIDC, pre-auth key)
│ ├── oidc.go # OpenID Connect integration
│ ├── grpcv1.go # gRPC service implementation (management API)
│ ├── handlers.go # HTTP handlers (registration, key exchange, etc.)
│ ├── metrics.go # Prometheus metrics
│ ├── tailsql.go # TailSQL debug endpoint
│ ├── db/ # Persistence layer (GORM, SQLite + PostgreSQL)
│ │ ├── db.go # DB abstraction, migration runner, connection setup
│ │ ├── node.go # Node lifecycle: registration, expiry, IP assignment
│ │ ├── users.go # User management
│ │ ├── ip.go # IP address allocation
│ │ ├── policy.go # Policy storage/retrieval
│ │ ├── preauth_keys.go # Pre-auth key management
│ │ ├── api_key.go # API token management
│ │ ├── schema.sql # Authoritative schema (verified at startup)
│ │ └── sqliteconfig/ # SQLite WAL/PRAGMA tuning
│ ├── state/ # In-memory state management
│ │ ├── state.go # Central coordinator (DB, policy, DERP, IP allocator)
│ │ ├── node_store.go # Copy-on-write node cache (perf-critical hot path)
│ │ ├── maprequest.go # MapRequest processing helpers
│ │ └── tags.go # Tag ownership validation
│ ├── mapper/ # Translates internal state → Tailscale wire protocol
│ ├── policy/ # ACL policy engine
│ │ ├── policy.go # HuJSON ACL parsing, peer visibility evaluation
│ │ ├── v2/ # Next-generation policy system (in-progress replacement)
│ │ ├── matcher/ # Rule-matching engine
│ │ └── policyutil/ # Shared policy utilities
│ ├── types/ # Core domain types, configuration structs, validation
│ │ └── change/ # Typed change notification events
│ ├── derp/ # DERP (relay) integration
│ │ └── server/ # Embedded DERP server implementation
│ ├── dns/ # MagicDNS record management
│ ├── routes/ # Subnet route management, primary route selection
│ ├── util/ # Shared helper functions (networking, keys, DNS)
│ │ └── zlog/ # Zerolog field extensions
│ ├── capver/ # Tailscale capability version negotiation
│ ├── templates/ # Client config templates (Apple, Windows, etc.)
│ ├── assets/ # Embedded static web assets
│ └── servertest/ # In-process server helpers for unit tests
├── gen/ # Machine-generated code — do not hand-edit
│ ├── go/headscale/v1/ # Go gRPC stubs + grpc-gateway HTTP bridge
│ └── openapiv2/headscale/v1/ # OpenAPI v2 JSON (embedded via go:embed in swagger.go)
├── proto/ # Protocol Buffer definitions (source of truth)
│ └── headscale/v1/ # 8 .proto files: node, user, apikey, policy, device, auth…
├── integration/ # Docker-based end-to-end tests (~29 .go files)
│ ├── scenario.go # Docker environment orchestration and lifecycle
│ ├── tailscale.go # Tailscale client container management
│ ├── control.go # Headscale server container management
│ ├── helpers.go # Shared test helpers
│ ├── tsic/ # Tailscale-in-container (client) helpers
│ ├── hsic/ # Headscale-in-container (server) helpers
│ ├── dsic/ # DERP-server-in-container helpers
│ ├── dockertestutil/ # Docker API utility layer (build, run, inspect)
│ └── integrationutil/ # Shared integration utilities (timing, assertions)
├── swagger.go # Root package (package headscale): SwaggerUI handler
├── tools/capver/ # Standalone capver analysis tool
├── packaging/
│ ├── deb/ # .deb packaging config
│ └── systemd/ # systemd service unit file
├── docs/ # MkDocs-based user documentation
├── nix/ # NixOS module + NixOS integration tests
├── Makefile # Build orchestration
├── .goreleaser.yml # Release automation (binaries, .deb, .rpm)
├── buf.gen.yaml # Protobuf codegen config (buf tool)
├── flake.nix # Nix dev shell (pins entire toolchain)
└── config-example.yaml # Reference configuration with all optionsEntry points#
cmd/headscale/headscale.go → binary: headscale#
The primary production binary. main() detects terminal color support, configures zerolog, then calls cli.Execute(). The Cobra root command is defined in cmd/headscale/cli/root.go; cli/serve.go wires the headscale serve subcommand, which calls newHeadscaleServerWithConfig() → app.Serve(). Additional subcommands cover the admin CLI: user management, node management, pre-auth keys, API keys, policy operations, debug, and configuration validation. The binary serves two roles: daemon (server mode) and CLI client (admin operations via gRPC).
cmd/hi/main.go → binary: hi (developer only, not in release)#
“Headscale Integration” test runner. Uses creachadair/command (not Cobra). Subcommands: run [pattern], doctor, clean {networks,images,containers,cache,all}. Orchestrates Docker containers to run end-to-end integration tests against real Tailscale clients with full network isolation per run.
cmd/mapresponses/main.go → binary: mapresponses (developer only, not in release)#
Debug utility. Reads a directory of captured MapResponse JSON files, reconstructs the expected online node map, and prints it to stderr. Used to reproduce and diagnose protocol-level issues.
Package organization#
Internal packages (hscontrol/): All core control-plane logic. Not in an internal/ directory but treated as the project’s implementation boundary.
| Package | Purpose |
|---|---|
hscontrol | Top-level application wiring, HTTP+gRPC servers, auth flows, polling |
hscontrol/db | Persistence: GORM ORM, migrations, node/user/key CRUD for SQLite + PostgreSQL |
hscontrol/state | In-memory coordinator: NodeStore (CoW cache), state.go (DB+policy+DERP integration) |
hscontrol/mapper | Wire protocol translation: internal Node → tailcfg.MapResponse |
hscontrol/policy | ACL evaluation: HuJSON parsing, peer visibility, route approval |
hscontrol/policy/v2 | Next-generation policy system (actively replacing policy/) |
hscontrol/types | Domain types: Node, User, Config, PAK, APIKey — no upward deps |
hscontrol/types/change | Typed change event notifications (used by state → hscontrol signalling) |
hscontrol/derp | DERP relay server integration and embedded server |
hscontrol/dns | MagicDNS record management |
hscontrol/routes | Subnet route lifecycle and primary route selection |
hscontrol/capver | Tailscale capability version negotiation (protocol versioning) |
hscontrol/util | Shared helpers: IP parsing, key formatting, DNS name manipulation |
hscontrol/templates | Embedded client config templates |
hscontrol/servertest | In-process server fixture for unit tests |
Public packages (root, gen/):
- Root
package headscale— one file only (swagger.go): serves the embedded Swagger UI and OpenAPI JSON viahttp.HandlerFuncs registered inapp.go. gen/go/headscale/v1— generated gRPC stubs and grpc-gateway bridge. Consumed byhscontrol/grpcv1.goand the CLI client.
Layering — dependency hierarchy (roughly clean/layered architecture):
cmd/headscale/cli ← CLI layer
↓
hscontrol (app.go) ← Application layer (server wiring, request routing)
↓
hscontrol/state ← State coordination layer
↓
hscontrol/db ← Persistence layer
hscontrol/policy ← Policy evaluation layer
hscontrol/mapper ← Wire protocol layer
↓
hscontrol/types ← Domain model (no upward deps)Cross-cutting: hscontrol/util, hscontrol/capver, gen/go/headscale/v1
The layering is not strictly enforced by internal/ boundaries but is respected by convention; types has no imports from db or state.
Build system#
- Build tool: GNU Make (
Makefile) for developer workflow; goreleaser (.goreleaser.yml) for release distribution - Key Makefile targets:
build→go build -o headscale ./cmd/headscale(PIE enabled on non-BSD platforms)test→go test -race ./...generate→go generate ./...(triggers buf for protobuf)fmt→ gofumpt + golangci-lint –fix + mdformat + prettier + clang-formatlint→ golangci-lint + buf lintdev→ fmt + lint + test + build (full cycle)
- Proto codegen:
buftool withbuf.gen.yaml→ generates Go gRPC stubs, grpc-gateway, and OpenAPI JSON intogen/ - Release: goreleaser produces cross-platform binaries (linux/amd64, linux/arm64, darwin, freebsd),
.deb,.rpm, and Docker images; vendor directory is included in source releases - Docker: Multiple Dockerfiles serve different purposes:
Dockerfile.integration— multi-stage debug build (with Delve debugger) for integration testsDockerfile.integration-ci— CI-optimised variantDockerfile.derper— standalone DERP relay server image- No production Dockerfile (users build from goreleaser binary or package)
- Dev environment: Nix (
flake.nix) is the blessed dev shell, pinning Go 1.26.1, buf, golangci-lint, gofumpt, prettier, clang-format. Makefile’scheck-depsemits a Nix-specific warning when tools are absent.
Notable structural decisions#
No
internal/enforcement — The entire implementation is inhscontrol/, a publicly importable package despite being purely an implementation detail. The project grew organically and uses naming convention rather than Go’sinternal/mechanism for boundary enforcement. This is pragmatic but means nothing prevents external code from importinghscontroldirectly.Root module as Swagger stub —
swagger.goat the root creates a thinpackage headscalelayer. This unusual choice allows the OpenAPI spec (embedded via//go:embed gen/openapiv2/...) to be served as an HTTP handler registered inapp.go. It makes the module both a library and an executable host, which is unconventional.integration/as first-class peer — The Docker-based integration test suite sits at the same level ascmd/andhscontrol/, not nested under either. It has its own sub-packages (tsic, hsic, dsic, dockertestutil), its own scenario orchestration, and consumeshscontrol/servertestfor in-process testing. This signals that end-to-end testing against real Tailscale clients is architecturally important, not bolted on.Three binaries, one released — Only
headscaleappears in goreleaser targets.hi(Docker test runner) andmapresponses(debug tool) are developer utilities that live incmd/for discoverability but are never shipped to end users. The double-duty ofheadscaleas both daemon and admin CLI (via gRPC) means there is effectively a single shipped binary.Generated code isolated and cleaned — All protobuf output goes to
gen/, which is excluded from source file collections in the Makefile and deleted bymake clean. This clean separation makes hand-written vs. generated code immediately obvious and prevents developers from modifying generated files.Nix as the project’s build contract — The
flake.nixpins the complete toolchain (Go, buf, all formatters and linters). This is significantly more advanced than a typical Go project, ensuring exact reproducibility of the development environment and CI outputs. The Makefile is essentially a thin wrapper around Nix-managed tools.