mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-30 20:09:36 +09:00
200c2c01e8
nixosModules.testkit makes a node a control server every client joins without trust setup; testkit-peer joins it with hs-join.
202 lines
8.1 KiB
Markdown
202 lines
8.1 KiB
Markdown
# Headscale NixOS Module
|
|
|
|
This directory contains the NixOS module for Headscale.
|
|
|
|
## Rationale
|
|
|
|
The module is maintained in this repository to keep the code and module
|
|
synchronized at the same commit. This allows faster iteration and ensures the
|
|
module stays compatible with the latest Headscale changes. All changes should
|
|
aim to be upstreamed to nixpkgs.
|
|
|
|
## Files
|
|
|
|
- **[`module.nix`](./module.nix)** - The NixOS module implementation
|
|
- **[`example-configuration.nix`](./example-configuration.nix)** - Example
|
|
configuration demonstrating all major features
|
|
- **[`testkit.nix`](./testkit.nix)**,
|
|
**[`testkit-peer.nix`](./testkit-peer.nix)** - Test kit: a control node for
|
|
NixOS VM tests of Tailscale clients, and a peer that joins it (see below)
|
|
- **[`tests/`](./tests/)** - NixOS integration tests
|
|
|
|
## Usage
|
|
|
|
Add to your flake inputs:
|
|
|
|
```nix
|
|
inputs.headscale.url = "github:juanfont/headscale";
|
|
```
|
|
|
|
Then import the module:
|
|
|
|
```nix
|
|
imports = [ inputs.headscale.nixosModules.default ];
|
|
```
|
|
|
|
See [`example-configuration.nix`](./example-configuration.nix) for configuration
|
|
options.
|
|
|
|
## Test kit
|
|
|
|
`nixosModules.testkit` turns a node of a NixOS VM test into a headscale control
|
|
server. Projects that implement or embed Tailscale (tailscaled, tsnet,
|
|
tailscale-rs, …) use it to test against a real control server without writing
|
|
one. Go clients join without a CA, a hosts entry or an env var.
|
|
`nixosModules.testkit-peer` is an optional tailscaled peer that joins it in one
|
|
line.
|
|
|
|
```nix
|
|
pkgs.testers.runNixOSTest {
|
|
name = "my-app";
|
|
nodes.headscale.imports = [ inputs.headscale.nixosModules.testkit ];
|
|
nodes.peer.imports = [ inputs.headscale.nixosModules.testkit-peer ];
|
|
testScript = ''
|
|
start_all()
|
|
key = headscale.succeed("hs-authkey alice").strip()
|
|
peer.succeed(f"hs-join {key}")
|
|
'';
|
|
}
|
|
```
|
|
|
|
It works with `nixosTest`, `runNixOSTest` and `runTest`. The headscale package
|
|
must come from the same release or newer: the kit relies on its
|
|
`preauthkeys create --user NAME` and `HEADSCALE_DEBUG_INSECURE_TLS_LISTEN_ADDR`.
|
|
|
|
### Contract
|
|
|
|
Breaking changes to anything below get a "NixOS test kit" BREAKING entry in the
|
|
CHANGELOG.
|
|
|
|
- Control: `http://<node hostname>`, so `http://headscale` for a node named
|
|
`headscale`. Use it for `tailscale up --login-server`,
|
|
`tsnet.Server.ControlURL` and tailscale-rs `TS_CONTROL_URL`.
|
|
- The same router is also served on TLS :443 with a throwaway self-signed
|
|
certificate. DERP is region 999 (`headscale`) on :443, marked
|
|
`InsecureForTests`, with STUN on UDP 3478.
|
|
- `hs-authkey USER [headscale preauthkeys create flags]`, on the control node:
|
|
- waits for headscale for up to `HEADSCALE_CLI_TIMEOUT` (default `60s`);
|
|
- creates `USER` if missing;
|
|
- prints one reusable 24h key.
|
|
|
|
Extra flags pass through, e.g. `--ephemeral` or `--tags tag:ci`. In a policy,
|
|
refer to these users as `USER@`, because they have no email.
|
|
|
|
- `hs-join KEY [tailscale up flags]`, on a `testkit-peer` node, waits for
|
|
tailscaled, then joins the control node named `headscale` with a 60s
|
|
`tailscale up --timeout`, so a broken control plane fails the test instead of
|
|
hanging it. A trailing `--login-server` overrides the URL.
|
|
- Defaults you can override:
|
|
- `policy.mode = "database"`: nothing stored allows all, and
|
|
`headscale policy set -f FILE` applies live. For a fixed policy, set
|
|
`settings.policy = { mode = "file"; path = pkgs.writeText "policy.json" (builtins.toJSON { … }); }`.
|
|
A policy may name users before `hs-authkey` creates them.
|
|
- MagicDNS with `dns.base_domain = "tailnet"`.
|
|
- `dns.override_local_dns = false`.
|
|
- Everything else is plain `services.headscale.*` on that node.
|
|
- Override the package with a plain assignment, e.g. to carry a patch:
|
|
`services.headscale.package = inputs.headscale.packages.${system}.headscale.overrideAttrs (o: { patches = (o.patches or [ ]) ++ [ ./fix.patch ]; });`
|
|
- Don't bind :80 or :443 on that node yourself.
|
|
|
|
### Clients
|
|
|
|
- **tailscaled**: import `testkit-peer` and use `hs-join`, or run
|
|
`tailscale up --login-server http://headscale --auth-key KEY` yourself. Its
|
|
log warns that it "could not establish an encrypted connection": that is the
|
|
untrusted certificate on :443, and it is expected.
|
|
- **tsnet**: set `ControlURL` to `http://headscale` and hand it the key,
|
|
e.g. `TS_AUTHKEY` in an `EnvironmentFile` that the test writes before it
|
|
starts the unit.
|
|
- **tailscale-rs**: build with its `ts_control/insecure-keyfetch` and
|
|
`ts_control/insecure-derp` features, which allow plain-HTTP `/key` and
|
|
honour `InsecureForTests`. See its examples for the control URL, auth key and
|
|
hostname flags.
|
|
- **Proving relay**: set `TS_DEBUG_ALWAYS_USE_DERP=1` on a Go peer.
|
|
- Between Go peers, `tailscale ping --until-direct=false PEER` reports
|
|
`via DERP(headscale)`.
|
|
- A non-Go peer may not answer pings over DERP. Check its home DERP from a Go
|
|
peer with
|
|
`tailscale status --json | jq -e '.Peer[] | select(.HostName == "NAME") | .Relay == "headscale"'`
|
|
and send application traffic.
|
|
- **Logs**: `testkit-peer` turns off tailscaled's log uploads. Inside the Nix
|
|
build sandbox other clients' uploads just fail; set `TS_NO_LOGS_NO_SUPPORT=1`
|
|
for tsnet if interactive runs should not upload either.
|
|
- **Unprivileged tsnet**: without `CAP_NET_ADMIN`, tsnet binds its sockets to
|
|
the default-route interface. In a test VM that is the user-net NIC, not the
|
|
VLAN, so control is unreachable. The same thing happens on multi-homed
|
|
production hosts, so fix it in the service: grant `CAP_NET_ADMIN`, or call
|
|
`netns.SetEnabled(false)`.
|
|
- **Versions**: headscale supports the Tailscale client versions named at the
|
|
top of each CHANGELOG release, with no upper bound. Pin a headscale release
|
|
that accepts your client.
|
|
|
|
### Without Nix
|
|
|
|
A harness that runs the headscale binary itself, such as an Android emulator
|
|
test, can use the same shape:
|
|
|
|
- Serve plain HTTP.
|
|
- Set `HEADSCALE_DEBUG_INSECURE_TLS_LISTEN_ADDR` to serve the router and DERP
|
|
over a throwaway TLS certificate.
|
|
- Mint keys:
|
|
|
|
```sh
|
|
headscale -c CONFIG users create NAME
|
|
headscale -c CONFIG preauthkeys create --user NAME --reusable --expiration 24h
|
|
```
|
|
|
|
A minimal config:
|
|
|
|
```yaml
|
|
# Clients dial this host for DERP and STUN too, so all of them must reach it.
|
|
server_url: http://127.0.0.1:8080
|
|
listen_addr: 127.0.0.1:8080
|
|
noise: { private_key_path: /tmp/hs/noise.key }
|
|
database: { type: sqlite, sqlite: { path: /tmp/hs/db.sqlite } }
|
|
prefixes: { v4: 100.64.0.0/10, v6: "fd7a:115c:a1e0::/48" }
|
|
dns: { magic_dns: false, override_local_dns: false }
|
|
unix_socket: /tmp/hs/headscale.sock
|
|
disable_check_updates: true
|
|
derp:
|
|
urls: []
|
|
server:
|
|
enabled: true
|
|
region_id: 999
|
|
region_code: headscale
|
|
stun_listen_addr: 127.0.0.1:3478
|
|
private_key_path: /tmp/hs/derp.key
|
|
```
|
|
|
|
- Wait for `HEADSCALE_CLI_TIMEOUT=60s headscale -c CONFIG health`.
|
|
- If `server_url` is a hostname rather than an IP, Go clients redial noise only
|
|
on :443 after a recent dial. Put the TLS listener on :443 then.
|
|
- If clients reach headscale at different addresses, e.g. an emulator behind
|
|
NAT, set `derp.server.enabled: false`. Instead, list a DERP map of your own
|
|
in `derp.paths`, as `.yaml`, `.json` or `.hujson`.
|
|
|
|
### Migrating a hand-rolled control node
|
|
|
|
Delete these and import the kit:
|
|
|
|
- the self-signed cert, `security.pki.certificateFiles` and the nginx TLS proxy;
|
|
- the embedded DERP block (`region_id = 999`, `urls = [ ]`) and its firewall
|
|
ports;
|
|
- `ip_prefixes`, which headscale no longer reads;
|
|
- `/etc/hosts` pins for the control node;
|
|
- jq lookups of the user ID before `preauthkeys create`.
|
|
|
|
### Cost
|
|
|
|
- headscale builds from source with its own pinned nixpkgs and Go.
|
|
- Setting `inputs.headscale.inputs.nixpkgs.follows` works only if your nixpkgs
|
|
has the Go version `go.mod` requires. Compare
|
|
`nix eval --raw nixpkgs#go_latest.version` against it.
|
|
- The input also brings headscale's development inputs into your lock file. If
|
|
you already depend on them, `follows` them.
|
|
|
|
## Upstream
|
|
|
|
- [nixpkgs module](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/networking/headscale.nix)
|
|
- [nixpkgs package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/he/headscale/package.nix)
|
|
|
|
The module in this repository may be newer than the nixpkgs version.
|