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 options

Entry 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.

PackagePurpose
hscontrolTop-level application wiring, HTTP+gRPC servers, auth flows, polling
hscontrol/dbPersistence: GORM ORM, migrations, node/user/key CRUD for SQLite + PostgreSQL
hscontrol/stateIn-memory coordinator: NodeStore (CoW cache), state.go (DB+policy+DERP integration)
hscontrol/mapperWire protocol translation: internal Node → tailcfg.MapResponse
hscontrol/policyACL evaluation: HuJSON parsing, peer visibility, route approval
hscontrol/policy/v2Next-generation policy system (actively replacing policy/)
hscontrol/typesDomain types: Node, User, Config, PAK, APIKey — no upward deps
hscontrol/types/changeTyped change event notifications (used by state → hscontrol signalling)
hscontrol/derpDERP relay server integration and embedded server
hscontrol/dnsMagicDNS record management
hscontrol/routesSubnet route lifecycle and primary route selection
hscontrol/capverTailscale capability version negotiation (protocol versioning)
hscontrol/utilShared helpers: IP parsing, key formatting, DNS name manipulation
hscontrol/templatesEmbedded client config templates
hscontrol/servertestIn-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 via http.HandlerFuncs registered in app.go.
  • gen/go/headscale/v1 — generated gRPC stubs and grpc-gateway bridge. Consumed by hscontrol/grpcv1.go and 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:
    • buildgo build -o headscale ./cmd/headscale (PIE enabled on non-BSD platforms)
    • testgo test -race ./...
    • generatego generate ./... (triggers buf for protobuf)
    • fmt → gofumpt + golangci-lint –fix + mdformat + prettier + clang-format
    • lint → golangci-lint + buf lint
    • dev → fmt + lint + test + build (full cycle)
  • Proto codegen: buf tool with buf.gen.yaml → generates Go gRPC stubs, grpc-gateway, and OpenAPI JSON into gen/
  • 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 tests
    • Dockerfile.integration-ci — CI-optimised variant
    • Dockerfile.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’s check-deps emits a Nix-specific warning when tools are absent.

Notable structural decisions#

  1. No internal/ enforcement — The entire implementation is in hscontrol/, a publicly importable package despite being purely an implementation detail. The project grew organically and uses naming convention rather than Go’s internal/ mechanism for boundary enforcement. This is pragmatic but means nothing prevents external code from importing hscontrol directly.

  2. Root module as Swagger stubswagger.go at the root creates a thin package headscale layer. This unusual choice allows the OpenAPI spec (embedded via //go:embed gen/openapiv2/...) to be served as an HTTP handler registered in app.go. It makes the module both a library and an executable host, which is unconventional.

  3. integration/ as first-class peer — The Docker-based integration test suite sits at the same level as cmd/ and hscontrol/, not nested under either. It has its own sub-packages (tsic, hsic, dsic, dockertestutil), its own scenario orchestration, and consumes hscontrol/servertest for in-process testing. This signals that end-to-end testing against real Tailscale clients is architecturally important, not bolted on.

  4. Three binaries, one released — Only headscale appears in goreleaser targets. hi (Docker test runner) and mapresponses (debug tool) are developer utilities that live in cmd/ for discoverability but are never shipped to end users. The double-duty of headscale as both daemon and admin CLI (via gRPC) means there is effectively a single shipped binary.

  5. Generated code isolated and cleaned — All protobuf output goes to gen/, which is excluded from source file collections in the Makefile and deleted by make clean. This clean separation makes hand-written vs. generated code immediately obvious and prevents developers from modifying generated files.

  6. Nix as the project’s build contract — The flake.nix pins 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.