Compare commits

...

1816 Commits

Author SHA1 Message Date
alaningtrump 048308511c chore: fix function comment to match actual function name
Signed-off-by: alaningtrump <alaningtrump@outlook.com>
2026-07-03 07:27:08 +02:00
Kristoffer Dalby a84945f134 CHANGELOG: shorten 0.29.2 invalid-name entry, set date
Updates #3346
2026-07-01 16:23:24 +02:00
Kristoffer Dalby fef80f3eb0 CHANGELOG: note /ts2021 WebSocket GET fix
Updates #3357
2026-07-01 15:20:01 +02:00
Kristoffer Dalby 01ef350d5f integration: add TS2021 WebSocket tests to CI matrix
Generated by gh-action-integration-generator.

Updates #3357
2026-07-01 15:20:01 +02:00
Kristoffer Dalby f4fba32dc6 integration: test /ts2021 WebSocket GET with a real WASM client
A raw coder/websocket dial and the real tailscale.com js/wasm control client under Node, both against headscale alongside normal Tailscale clients.

Updates #3357
2026-07-01 15:20:01 +02:00
Kristoffer Dalby fc6f216b61 hscontrol: register /ts2021 for WebSocket GET
The chi migration dropped GET; JS/WASM control clients open /ts2021 as a WebSocket GET and were rejected with 405 before reaching NoiseUpgradeHandler.

Fixes #3357
2026-07-01 15:20:01 +02:00
Kristoffer Dalby de9db9c811 CHANGELOG: note 0.29.2 invalid-name map fix
Updates #3346
2026-07-01 15:19:08 +02:00
Kristoffer Dalby 4946d1c88d state: log nodes with map-breaking data at startup
Scan a node-health check registry at boot and log each node whose name
can't form a valid FQDN, with the rename fix. Log-only, no mutation.

Updates #3346
2026-07-01 15:19:08 +02:00
Kristoffer Dalby c497612c99 state: reject renames whose FQDN exceeds the hostname limit
A valid label can still overflow 255 chars under a long base_domain; gate
RenameNode with the new types.ValidateGivenName.

Updates #3346
2026-07-01 15:19:08 +02:00
Kristoffer Dalby 4e4512c4b7 poll: return an HTTP error on long-poll setup failure
A bare return sent an empty 200 the client read as "unexpected EOF" and
retried forever; emit a real error instead.

Updates #3346
2026-07-01 15:19:08 +02:00
Kristoffer Dalby 08956d51a4 mapper: skip peers with invalid names instead of failing the map
A peer whose GivenName fails GetFQDN aborted the whole map for every node
that could see it. Drop and log it; SSH policy errors degrade too.

Fixes #3346
2026-07-01 15:19:08 +02:00
Kristoffer Dalby d528686f14 mapper,policy: add reconnect-storm and lock-concurrency regression tests
TestInitialMapNotStarvedByReconnectStorm reproduces the #3346 stall;
TestPolicyManagerConcurrentReads guards the RLock cache access under -race.

Updates #3346
2026-07-01 14:41:03 +02:00
Kristoffer Dalby 6f317c7576 policy: take RLock for reads so map generation runs concurrently
One exclusive mutex serialized every policy read, so a mass reconnect on
autogroup:self/via/relay policies stalled clients into "unexpected EOF"
retries. Per-node caches become xsync.Maps for lazy population under RLock.

Fixes #3346
2026-07-01 14:41:03 +02:00
Kristoffer Dalby 518b497be9 ci: run TestK8sOperator in the shared sqlite matrix
It needs no special runner (every integration container already runs privileged), so it joins the generated matrix; load br_netfilter only for that test.

Updates #1202
2026-06-26 22:06:26 +02:00
Kristoffer Dalby 8d91e42a9f cmd/hi: share the test-container prefix matcher and check the k3s image
Factor the hs-/ts-/derp-/k3s- prefix set into one helper used by cleanup and docker; add a doctor check for the ghcr k3s image.
2026-06-26 22:06:26 +02:00
Kristoffer Dalby 99fd62477d integration: add a k3s and Tailscale Kubernetes operator test
Run the real operator in a single-container k3s cluster against an in-test Headscale over plain HTTP. k3sic exposes reusable building blocks (InstallOperator, DeployConnector, DeployEchoServer, ExposeServiceToTailnet, DeployProxyGroup); the test covers operator registration, an egress connector, ingress connectivity from a tailnet node, and proxy groups. tls-ca-baking.md records the private-CA TLS variant.

Updates #1202
2026-06-26 22:06:26 +02:00
Kristoffer Dalby c7b162509c integration/hsic: add CreateOAuthClient via the v2 API client
Mints an admin API key and creates a keyType=client OAuth credential through gen/client/v2, exposed on ControlServer. Reusable by tests needing OAuth client credentials.
2026-06-26 22:06:26 +02:00
Kristoffer Dalby a4ec76605e integration/tsic: add WithDERPOverHTTP for the non-TLS embedded DERP
Sets TS_DEBUG_DERP_WS_CLIENT + TS_DEBUG_USE_DERP_HTTP so a client can reach hsic's plain-HTTP embedded DERP over websockets; the counterpart to hsic.WithoutTLS.
2026-06-26 22:06:26 +02:00
Kristoffer Dalby 1867213b69 capver: order TailscaleLatestMajorMinor versions with cmpver
Lexical sort placed v1.100 before v1.98; cmpver compares numerically.
2026-06-26 22:06:26 +02:00
Kristoffer Dalby 622e08f5e6 oidc: harden callback CSRF cookies, state reuse, and issuer config
Defence-in-depth fixes to the OIDC login flow.

Set the state/nonce cookie Secure flag from the configured server_url
scheme rather than req.TLS, so the cookies stay Secure behind a
TLS-terminating reverse proxy where the proxy-to-Headscale hop is plain
HTTP. Deriving it from config avoids trusting a spoofable
X-Forwarded-Proto header.

Make the OIDC state single-use: consume it from the cache on the
callback and clear the state/nonce cookies once validated, so a replayed
callback cannot resolve the same session and the cookies do not linger
until expiry.

Bound OIDC discovery to the caller's context so a slow or unreachable
issuer fails startup within the timeout instead of hanging, and validate
the issuer URL and required client_id/client_secret at config load so an
unworkable setup fails fast.
2026-06-26 09:18:06 +02:00
Kristoffer Dalby 79bf201ccb changelog: note OAuth clients and scopes 2026-06-26 09:18:06 +02:00
Kristoffer Dalby af46fe8867 hscontrol, integration: test OAuth scope enforcement and the oauth-clients CLI
Add scope-enforcement coverage and integration tests for the
oauth-clients CLI across the test matrix.
2026-06-26 09:18:06 +02:00
Kristoffer Dalby b05f8875c3 hscontrol, cli: serve the v2 API on the local socket and add the oauth-clients command
Serve the v2 API over the local unix socket for the CLI and add the
oauth-clients command that drives it.
2026-06-26 09:18:06 +02:00
Kristoffer Dalby 84715e976c api/v2: add OAuth client-credentials auth and scope enforcement 2026-06-26 09:18:06 +02:00
Kristoffer Dalby ee95cf58d9 hscontrol: add the OAuth client and access-token model
Add the OAuth client type, its database storage, the scope grant package,
policy tag-ownership exposure, and the state operations backing the v2
OAuth client-credentials flow.
2026-06-26 09:18:06 +02:00
Florian Preinstorfer 7b1776a87e Add changelog entry for systemd service refresh 2026-06-25 11:07:10 +02:00
Florian Preinstorfer f6865fb40c Filter @resources from the list of allowed syscalls 2026-06-25 11:07:10 +02:00
Florian Preinstorfer 6e0814a845 Add DevicePolicy and MemoryDenyWriteExecute
I've used those for a long time with my setups.
2026-06-25 11:07:10 +02:00
Florian Preinstorfer 6e6ea3758d Remove CAP_CHOWN from systemd service 2026-06-25 11:07:10 +02:00
Florian Preinstorfer 3846c3f148 Remove X-Restart-Triggers from systemd service file
Its not used by systemd.
2026-06-25 11:07:10 +02:00
Florian Preinstorfer fe536dd799 Slightly restructure the example config comments 2026-06-25 11:07:10 +02:00
Fredrik Ekre 5aeeff98d0 oidc: fix NewProvider ignoring caller context timeout
NewAuthProviderOIDC received a context with a 30-second timeout from
its caller, but immediately discarded it by passing context.Background()
to oidc.NewProvider. This caused both `headscale serve` and
`headscale configtest` to hang indefinitely if the OIDC issuer was
unreachable, rather than timing out and returning an error.

Fix by passing the caller's ctx to oidc.NewProvider and remove the
nolint:contextcheck annotation that was suppressing the linter warning
about this bug. The annotation was introduced in ce580f82 as part of a
bulk lint-suppression pass without addressing the underlying issue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 19:03:00 +02:00
Kristoffer Dalby 66937040f2 policy: allow the tailscale.com/cap/secrets capability
It's setec's official cap; allowlist it so it can be granted via policy.
2026-06-24 18:41:33 +02:00
Kristoffer Dalby 036760df02 hscontrol: collapse key and settings asserts into cmp.Diff 2026-06-24 18:41:06 +02:00
Kristoffer Dalby 83d3828ba1 api/v2: simplify endpoint comments 2026-06-24 18:41:06 +02:00
Kristoffer Dalby dd9f0a3f4f servertest: roundtrip v2 users and 4via6 through the clients
Exercises the user data sources via the Go SDK, tscli, and OpenTofu, and
backfills tailscale_4via6 (provider-local compute), each with the
no-drift gate.
2026-06-24 18:41:06 +02:00
Kristoffer Dalby 961b15313f hscontrol: contract-test the v2 users endpoint
Pins the wire shape: honest constants, deviceCount aggregation, the
{"users":[...]} envelope, ignored type/role filters, and 404 on
unknown/non-numeric/pseudo-user ids.
2026-06-24 18:41:06 +02:00
Kristoffer Dalby 475a5ead0c api/v2: add user get and list (Tailscale compat)
Backs the tailscale_user and tailscale_users data sources. getUser maps
gorm/ErrUserNotFound to 404; unmodelled fields are honest constants,
deviceCount/lastSeen/currentlyConnected aggregate the user's nodes.
2026-06-24 18:41:06 +02:00
Baptiste Fontaine ec0cb6f63a docs: fix typos 2026-06-24 11:23:20 +02:00
Florian Preinstorfer a066a06bef Fix invalid ip syntax
The "ip" field needs to be a list.
2026-06-24 08:27:33 +02:00
Kristoffer Dalby 33a65052c9 servertest: roundtrip the v2 API through go-client, tscli, and opentofu
Exercise the full surface against the three real clients on a live
server, cross-checking get-after-set, server state, and no Terraform
drift.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 13ffd958ed hscontrol: add v2 API contract tests
Drive each endpoint in-process with humatest and validate both the
wire shapes and the server-side state: keys, devices, ACL, settings.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby d5555c6b8c servertest: add real-listener option and node/key helpers
Add WithRealListener for tests needing a real loopback port, plus
CreateAPIKey and CreateRegisteredNode for the v2 roundtrip.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 8898c69d0a flake: add opentofu and tscli for v2 API tests
Provide tofu and tscli in the dev shell, and add the Tailscale Go
client the roundtrip tests drive.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby e2e9fe3081 api/v2: add read-only tailnet settings endpoint
Report the settings the k8s operator reads, computed from config;
writes return 501.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 3cddc07026 api/v2: add Tailscale-compatible ACL endpoint
Serve and replace the policy as HuJSON with content-addressed ETags
and If-Match preconditions; writes are rejected in file mode.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby b3472bb8dd api/v2: add Tailscale-compatible devices API
Map the Tailscale device surface — get, list, delete, set name, tags,
key expiry, routes, and authorization — onto Headscale nodes.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 6b413fe237 api/v2: soft-revoke auth keys with a configurable collector
Tailscale's keys API has no separate expire verb: DELETE is the revoke.
Map it to a soft revoke so the key stays retrievable as invalid afterwards
instead of vanishing, matching the SDK and Terraform's expectations.

Add a revoked timestamp to pre-auth keys (migration plus schema), mark a
key invalid once revoked, and have the keys DELETE handler stamp it rather
than destroy the row. A background collector reaps revoked keys after a
configurable retention window (preauth_keys.revoked_retention, default
168h), so the table does not grow without bound.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby ff7dd807fe hscontrol: mount the v2 API at /api/v2
Wire the v2 mux into the router beside v1, threading the state, change
sink, and config through the Backend.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby fe08d43978 api/v2: add Tailscale-compatible keys API
Add the v2 foundation — Basic/Bearer auth, scope scaffolding, the
tailnet check, and the Tailscale error shape — plus the auth-key
endpoints mapped onto Headscale pre-auth keys.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 2ff342764f state: return ErrGivenNameInvalid for invalid node rename
RenameNode wrapped the raw dnsname error, so the v2 API mapped it to a
500. Emit the sentinel so an invalid name surfaces as a 400.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 3d811f29a6 db: add API-key owner and pre-auth-key description for v2
Give API keys an optional owning user and pre-auth keys a free-text
description, plus the state accessors the v2 API needs to act as a
key's owner and to persist descriptions.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 4fa6af0102 policy/v2: write the SSH check period as time arithmetic
Express the maximum SSH check period as 7 * 24 * time.Hour instead of a
bare nanosecond constant, matching how the rest of the code spells out
magic durations.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 96d736ec23 api/v1: build responses from view accessors, not AsStruct
Serialize node, user and pre-auth-key responses through the NodeView,
UserView and PreAuthKeyView accessors instead of AsStruct(), which clones
the whole record on every read. Document the convention in AGENTS.md.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 339cb392a9 types: add Node StringID and UserView accessors
Add StringID() on Node/NodeView and Username/Display/CreatedAt on
UserView, so the HTTP read paths render ids and read users through view
accessors instead of cloning with AsStruct().
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 24dfcf178f ci: run build, test, lint and format as nix flake checks
Adopt kradalby/flake-checks: flake.nix exposes build, gotest,
golangci-lint and formatting checks over one shared, fileset-filtered
source set, and nix-checks.yml gates each with
nix build .#checks.<system>.<name>. Drop test.yml and lint.yml —
gotestsum, golangci-lint and prettier are now those checks. golangci-lint
runs full-tree; Go formatting stays in it while formatting adds prettier.

hscontrol/servertest is too slow and timing-sensitive for the sandboxed
gotest check, so servertest.yml runs it in the devShell instead.
2026-06-20 21:08:01 +02:00
Kristoffer Dalby 8f314797ce all: fix full-tree golangci-lint findings
The new full-tree golangci-lint check reports issues the --new-from-rev
diff lint hid: nine wsl_v5 whitespace gaps, a prealloc, and an unparam
(setCSRFCookie never errored, so drop the return and update callers).
gocyclo on the central UpdateNodeFromMapRequest and an SA1019 NetMap
deprecation in an integration helper are suppressed with reasons.
2026-06-20 21:08:01 +02:00
Kristoffer Dalby 2632006f5f ci: drop the DeterminateSystems flake.lock update cron
Remove update-flake.yml; flake.lock is no longer auto-bumped by CI.
2026-06-20 21:08:01 +02:00
Kristoffer Dalby 07b3a8ebd3 ci: migrate Nix CI to official installer and hestia cache
Swap nix-quick-install-action -> nix-installer-action and
cache-nix-action -> Mic92/hestia/action across every Nix workflow, drop
the now-unneeded cache keys, and run devShell tooling through a
defaults.run.shell. Add gc.yml to trim the hestia cache daily.
2026-06-20 21:08:01 +02:00
Florian Preinstorfer 3c504af2be Fix placeholder in curl example 2026-06-20 10:41:57 +02:00
Florian Preinstorfer 7af39a6798 Add headscale-panel to webui docs 2026-06-20 10:41:57 +02:00
Kristoffer Dalby 49d6949639 cli: stop doubling the scheme on a remote CLI address
newRemoteClient prepended "https://" unconditionally, so
HEADSCALE_CLI_ADDRESS=https://host became https://https://host and dialed
the host "https". Default a bare host to https, keep an explicit scheme.
2026-06-20 09:55:42 +02:00
Florian Preinstorfer d7150755d4 TLS is not strictly required 2026-06-19 18:52:30 +02:00
Florian Preinstorfer 88234d4046 Replace /swagger with /api/v1/docs 2026-06-19 18:52:30 +02:00
Florian Preinstorfer 15781ed6d8 Fixup redirect for remote-control 2026-06-19 18:52:30 +02:00
Kristoffer Dalby 72adc5ff2a docs: add changelog and refresh the API reference for the v1 API 2026-06-19 15:21:00 +02:00
Kristoffer Dalby 560b6d81ad hscontrol: remove the proto, gRPC and grpc-gateway stack
With the v1 API served by Huma and the CLI on the HTTP client, nothing
uses the gRPC service or its generated code. Delete proto/, the generated
gen/go and gen/openapiv2, grpcv1.go, the buf config, the proto .Proto()
helpers and gRPC config, and the proto build tooling from the flake and
CI.
2026-06-19 15:21:00 +02:00
Kristoffer Dalby 8efa5ad1fe cli: migrate the CLI and integration tests to the v1 HTTP API
Replace the gRPC client with the generated HTTP client across every
command: locally over the unix socket without auth (matching the previous
local gRPC socket), remotely over TLS with a Bearer API key. Output
rendering and integration tests move to the HTTP client types; the
transport changes, the assertions do not.
2026-06-19 15:21:00 +02:00
Kristoffer Dalby ba90048cfb gen/client/v1: add the generated HTTP client
Generate a strongly-typed Go client (oapi-codegen) from the emitted spec, committed under gen/client/v1 as package clientv1. Tests exercise it against the in-memory Huma server over both TCP and the unix socket.
2026-06-19 15:21:00 +02:00
Kristoffer Dalby 1572ac551d test: add golden parity tests for the v1 API
Pin every endpoint's behaviour with golden fixtures captured from the
retiring grpc-gateway (timestamps and secrets neutralised). Error cases
assert their HTTP status independently of the golden via assertStatus, so
a blind regenerate cannot re-pin a wrong code; HEADSCALE_UPDATE_GOLDEN
rewrites the fixtures. A unit test exercises the API-key auth middleware
on the real server router. Exclude the emitted spec and goldens from
pre-commit checks.
2026-06-19 15:21:00 +02:00
Kristoffer Dalby c9bbb5bdec api/v1: add code-first Huma implementation of the v1 API
Reimplement the v1 API as a code-first Huma service in hscontrol/api/v1,
a thin adapter over the state layer. Huma emits the OpenAPI 3.1 document
(openapi/v1/headscale.yaml) from the Go operation and type definitions;
cmd/gen-openapi writes it and the 3.0.3 downgrade the client is generated
from. Responses reproduce the protojson wire contract (string-encoded
64-bit IDs, all fields emitted, RFC3339/null timestamps); errors map to
the correct HTTP status (404/400/409, 500 for server faults) via mapError.

Serve it on the chi router at /api/v1 behind the existing API-key
middleware, and point /swagger at the emitted spec.
2026-06-19 15:21:00 +02:00
Kristoffer Dalby 97eff90ebe db: report a missing pre-auth key on expire and destroy
Expiring or deleting a pre-auth key that does not exist updated zero rows
and returned no error, so the caller could not tell a missing key from a
successful no-op. Return ErrPreAuthKeyNotFound when RowsAffected is zero.
2026-06-19 15:21:00 +02:00
Kristoffer Dalby a00de89c85 ci: run nix jobs with --fallback
cache.nixos.org intermittently serves corrupt NARs; build from source on
substitute failure instead of failing the job.
2026-06-18 21:11:14 +02:00
Kristoffer Dalby f5198841bd ci: drop Docker v28 pin from release workflow
Engine 29 build compat is fixed in the client; the downgrade is no longer
needed.
2026-06-18 21:11:14 +02:00
Kristoffer Dalby dfc25f06e1 integration: pin docker client to daemon API version
dockertest's bundled client builds against API v1.25; Docker Engine 29
rejects it (min 1.40), surfacing as a "broken pipe" that fails every local
image build. Pin to the daemon's reported version so builds use a live path.

Closes #2937
2026-06-18 21:11:14 +02:00
Kristoffer Dalby 74928c0241 integration: regenerate test matrix for new CLI tests 2026-06-18 15:21:11 +02:00
Kristoffer Dalby 9aba06ec7d integration: backfill CLI command coverage
Regroup the CLI tests into per-command files and cover every command
except debug/config with CRUD, flag and error permutations, asserted as
JSON.
2026-06-18 15:21:11 +02:00
Kristoffer Dalby 00afce77b1 ci: run nixos module check with --fallback
cache.nixos.org intermittently serves corrupt NARs; build from source
on substitute failure instead of failing the job.
2026-06-18 14:54:44 +02:00
Kristoffer Dalby 9f1886f587 db: preserve user_id on untagged nodes with tags='null'
A nil tags slice marshals to JSON `null`; the clear-tagged migration
read that as tagged and cleared user_id. Exclude it, and recover
already-detached nodes from their pre-auth key.

Fixes #3323
2026-06-18 14:54:44 +02:00
Kristoffer Dalby ea5165e325 util, db: generate key material as hex via tailscale rands 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 368b9e7edd state: add regression test for NodeKey-rotation binding 2026-06-17 16:12:19 +02:00
Kristoffer Dalby abfd5c9236 types: reject port-bearing server_url under base_domain 2026-06-17 16:12:19 +02:00
Kristoffer Dalby e4cd846075 handlers: set verify Content-Type before writing the body 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 4b693a1320 oidc, types: validate pkce.method when OIDC is active 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 4f4e95fc80 all: adopt strings, errors, and os helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 27468f944b all: adopt maps and cmp helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby ac725f27e4 all: adopt slices helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 60f0544b78 dns, change, noise, auth, capver: misc consolidation 2026-06-17 16:12:19 +02:00
Kristoffer Dalby ea5e52f662 hi: consolidate doctor and stats helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 722a2f1e18 integration, hsic, tsic, dockertestutil: consolidate container helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 75b09cb991 servertest: consolidate harness helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 0320b56911 oidc: consolidate cookie helpers and logging 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 62869f01d2 app: simplify TLS and auth setup 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 19d5d9deeb derp: simplify DERP map handling 2026-06-17 16:12:19 +02:00
Kristoffer Dalby c3cfbf1cc0 grpcv1: share list-RPC response helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby fdc7e26c8a util: consolidate parsing and encoding helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 45e6e90d11 types: consolidate DNS, identifier, and proto helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 468f70a9e5 templates: consolidate shared page and component helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 8c10a96921 hscontrol: consolidate debug-endpoint and template helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 3979887d8d mapper: consolidate builder and connection helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 03e2d24c79 policy/v2, policy/matcher: consolidate alias and IP-set helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby e6ef1dda4d policy: consolidate cache-invalidation and evaluation helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 145672f0b6 state: consolidate route-election and node helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 77f289a627 db: consolidate IP allocation helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 55b9e3cfda db, sqliteconfig: consolidate query and config helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby b56326427c cli: consolidate output and table rendering 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 0b7c8aa270 cli: consolidate gRPC command helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby b88a3cb7b2 servertest, hi, integration: remove dead test helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby ed6596f9d7 policy, mapper, capver, hscontrol: remove dead code 2026-06-17 16:12:19 +02:00
Kristoffer Dalby f49f27adf8 db, sqliteconfig: remove dead and unreachable code 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 43ba20bebb util: remove dead helpers and stale comments 2026-06-17 16:12:19 +02:00
Kristoffer Dalby c8bd4a0947 types: remove dead types, constants, and methods 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 84168fb90a templates: remove dead style helpers and deprecated shims 2026-06-17 16:12:19 +02:00
Kristoffer Dalby b0c221f3a1 changelog: set 0.29 date
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-06-17 11:09:20 +02:00
Kristoffer Dalby 68a6d3cf17 db: drop ambiguous machine-key getter, match precisely in test helper
The First()-by-machine-key getter returned an undefined node when a machine
key mapped to several nodes. It was used only by RegisterNodeForTest; match on
(machine_key, user_id) there instead and remove the getter.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby 1689478485 state: return all nodes for a machine key, reject ambiguous ownership
Collapse the single-pick machine-key lookups onto GetNodesByMachineKeyAllUsers
so callers see every node sharing a machine key and reject the ambiguous or
impossible cases (tagged and user-owned coexistence; a tagged key with several
user-owned candidates) instead of mutating an arbitrarily-picked node.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby a1d3e98255 state: allow key expiry to be set on tagged nodes
Tagged nodes disable key expiry by default but can still have one set
explicitly, and changing tags leaves expiry unchanged, matching Tailscale.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby b83bf3f993 state: serialise registration per machine key
Concurrent registrations of one machine key each saw no existing node and
created their own, duplicating nodes and IPs. Hold a per-machine lock across
the find-then-create section.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby 96d2e6ed60 state: roll back node store when re-registration write fails
Re-registration mutated the node store before the database write and did not
revert on failure, so a restart could drop the client's current node key.
Snapshot the node and restore it if the write fails.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby 9b8949727d db: treat unknown pre-auth key as not found
An unknown or deleted key returned a bare error matching neither the
not-found nor pre-auth-key checks, so registration returned a server error
instead of 401. Wrap gorm.ErrRecordNotFound.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby bff216a184 state: update node in place on pre-auth-key re-registration
A reusable key on a converted node, or a tagged key on a user-owned node,
fell through to new-node creation, leaving two nodes per machine. Match by
machine key and update or convert in place.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby fd08b8fa8c state: make any-user machine-key lookup deterministic
The lookup returned the first map match, so re-auth branch choice varied
with map order once a machine key had more than one node. Prefer the tagged
node, else the lowest node ID.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby a73d38bb3f state: reject re-registration claiming another node's key
The pre-auth-key path wrote the client node key without the collision check
the auth path applies, so a re-registration could claim another node's key
and poison the node-key index. Reject keys bound to a different machine.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby e759d9fc90 auth: re-validate key when an expired node re-registers
The re-registration fast-path skipped validation for a matching node key
without checking expiry, so an expired node could re-auth with a spent key.
Gate it on the node not being expired.

Updates #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby 0961e79e16 state: re-register converted tagged nodes with reused key
A node converted to tagged is re-indexed under no user, so re-registration
keyed on the key's owner missed it and rejected the spent one-shot key.
Match the existing tagged node by machine key.

Fixes #3312
2026-06-15 20:29:15 +02:00
Kristoffer Dalby a5ef3aff15 state: patch relogins and gate endpoint broadcasts
Relogin sent as a peer patch with endpoints preserved; endpoint-only
deltas broadcast only on useful (non-STUN) changes.
2026-06-15 12:02:39 +02:00
Kristoffer Dalby 4da06925d0 types/change: add NodeKeyRotated for relogin peer patch
Sends a relogin as an incremental PeerChange, not a whole-node add.
2026-06-15 12:02:39 +02:00
Kristoffer Dalby 21058d1142 flake, go.mod: refresh dependencies
tailscale.com v1.100.0 -> v1.101.0-pre, golang.org/x/*, genproto,
docker-credential-helpers, client/tailscale/v2; vendor SRI and
nixpkgs lock refreshed. modernc.org/libc stays at v1.72.3 per
modernc.org/sqlite v1.52.0's go.mod. Go 1.26.4 staging pin kept:
nixpkgs-unstable still ships 1.26.3.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby c5f3d5c28d auth: clamp logout expiry to now
Tailscale sends the sentinel time.Unix(123, 0) on logout; storing it
verbatim propagates a 1970 KeyExpiry to every peer's netmap. Same
semantics (expired as of now), saner logs and debug dumps.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby 40ed210521 policy: read pm.pol under the mutex
NodeCanHaveTag, TagExists, ViaRoutesForPeer checked pm.pol before
taking pm.mu, racing SetPolicy's write (caught by -race in
TestRaceConcurrentServerMutations). DebugString read pol, filter, and
the derived maps with no lock at all.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby f4eeb94b1c mapper: gate broadcast sends until a connection's initial map is delivered
AddNode registers the connection before sending the initial map, so a
batched delta could land first and become the stream's first frame;
Tailscale clients then kill the poll with "initial MapResponse lacked
Node" and a retry loop under steady change traffic leaves the netmap
empty. Skip connections awaiting their initial map and retry the
change next tick — the in-flight map may predate it, so it cannot be
dropped. Retries are prepended to keep patch order.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby 7918187e7a servertest: add logout/relogin storm repro with poll churn
Mirrors the flaky integration relogin tests with production batcher
tuning. The churn variant restarts map polls around login like newer
tailscaled does; pre-fix it stranded nodes online after logout 2/2.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby f497b4efd7 state, poll: refcount poll sessions, mark offline only on last release
A cancelled map request whose handler ran late could Connect after the
live session, steal the newest SessionEpoch, then exit without
disconnecting (stillConnected path); the live session's final
Disconnect was rejected as stale and the node stayed online forever
(relogin flake). Counted releases are order-independent, so overlapping
sessions cannot strand a node in either direction.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby 759381ad78 types: add ActiveSessions poll session refcount to Node
In-memory only, like SessionEpoch. Generated clone/view regenerated.
2026-06-11 16:28:25 +02:00
kloba 71a4ce3c9f noise: re-delegate SSH check when the auth session is missing (#3306) 2026-06-10 11:48:02 +02:00
Kristoffer Dalby f585f8a94d flake, go.mod: move to Go 1.26.4 and refresh dependencies
Track nixpkgs staging-next-26.05 for prebuilt Go 1.26.4 (security GO-2026-5037/5039;
unstable still ships 1.26.3, lagging the staging pipeline). Bump go.mod, refresh deps
(tailscale.com v1.100.0, modernc sqlite v1.52.0 with libc lockstep), test Dockerfiles, vendor hash.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 88044f43ff hscontrol: satisfy golangci-lint on changed lines
Sentinel ErrNodeKeyInUse (err113); key the visible-peer set by tailcfg.NodeID
to drop an int64->uint64 cast (gosec G115); NewRequestWithContext (noctx); wsl.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 5e05652a78 mapper: derive incremental visibility from one shared filter
filterVisiblePeerPatches and filterVisibleNodes now share one visiblePeerIDs
helper using the live MatchersForNode/ReduceNodes set, so paths cannot drift.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 020560fc5f policy: remove unused top-level BuildPeerMap
Zero non-test callers; production uses policy/v2 PolicyManager.BuildPeerMap.
A stray second copy of the peer-visibility predicate invites drift.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby fad8f2a729 mapper: test incremental visibility matches full map
Pin that NodeOnline patches, NodeAdded peers, and cross-user profiles expose
exactly the full-map ACL-visible set across the four policy shapes.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 0121083b53 test: assert NodeKey hijack is rejected on re-auth
TestReAuthWithDifferentMachineKey asserted that a second machine claiming an existing NodeKey succeeds, but that hijack (now rejected by f8f08cf7) poisoned the NodeKey index and DoS'd the original node — the test only checked the hijacker's node, never the original's survival. Assert the registration is rejected and the original node survives intact.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 9fc88e308f noise: drop write-only nodeKey field to fix data race
noiseServer.nodeKey was assigned on every PollNetMap and Register request but never read anywhere. The inner HTTP/2 mux multiplexes concurrent requests over one Noise session, so those per-request writes to the shared field raced (caught by -race in servertest TestConnectDisconnectRace, reachable by a client issuing concurrent map requests). Remove the dead field and its two writes.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby c483bebba8 mapper: guard nil Hostinfo in addNextDNSMetadata
addNextDNSMetadata dereferenced node.Hostinfo().OS() without the .Valid() guard its siblings (RequestTags, TailNode) apply, so building the DNS config for a node with nil Hostinfo (a legacy NULL host_info row) panicked whenever a NextDNS resolver was configured. Guard the OS() dereference.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 4914f9f2fd state: reject re-auth claiming another machine's NodeKey
applyAuthNodeUpdate rotated the node's NodeKey to the client-supplied value without the 1:1 NodeKey/MachineKey check createAndSaveNewNode (f8f08cf7) and getAndValidateNode enforce. A re-authenticating node could thus rotate its key to a victim's and poison the NodeStore NodeKey index, denying the victim service. Apply the same uniqueness check on the re-auth path.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 8237ac662a mapper: filter incremental UserProfiles by ACL visibility
buildFromChange's PeersChanged path passed an unfiltered changed-node slice to WithUserProfiles, while the full-map path uses the BuildPeerMap-filtered ListPeers. A node thus received the identities (login name, display name, avatar) of users owning peers its ACL forbids accessing. Filter the UserProfiles peer set via the same ReduceNodes visibility check buildTailPeers applies.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby cd1c208980 mapper: filter peer-change patches by ACL visibility
buildFromChange added PeersChangedPatch (online/offline, endpoint, key-expiry) directly, skipping the policy.ReduceNodes visibility filter that buildTailPeers applies to full peers. A node thus received the existence, presence, and addresses of peers its ACL forbids accessing. Restrict patches to the recipient's visible peer set.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 0fdff0c79b oidc: set SameSite=Lax on state/nonce CSRF cookies
setCSRFCookie set no SameSite attribute despite a nolint comment claiming it did, so the OIDC state/nonce cookies relied on the browser default. Set Lax explicitly (Strict would drop the cookie on the cross-site IdP->callback navigation and break login), hardening browsers that do not default to Lax against OIDC login CSRF.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby eb57a3a62b state: reject registration claiming another machine's NodeKey
createAndSaveNewNode trusted the client NodeKey without checking it was already bound to a different machine, so an authenticated party could register a node carrying a victim's public NodeKey, poison the NodeStore NodeKey index, and make the victim's MapRequest resolve to the wrong node (rejected by getAndValidateNode = DoS). Enforce the 1:1 NodeKey/MachineKey binding at registration, as poll time already does.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 56cd3eb24d policy: guard SSHCheckParams autogroup:self against nil User
The autogroup:self SSH-check branch dereferenced node.User().ID() guarded only by !IsTagged(); a non-tagged node with an unhydrated User (UserID set, association nil) crashed the server via the Noise SSH-check path. Gate on User().Valid() like filter.go, same shape as 171fd7a3.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 8f75ee5647 docker: head tailscale latest go
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-06-08 10:04:49 +02:00
Kristoffer Dalby efdd9463e9 mapper: keep one batched bundle per node in flight to preserve order
A saturated worker pool could deliver a later tick before an earlier one.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby a518a5076a state: batch route auto-approval into one policy rebuild
Per-node SetApprovedRoutes made each policy reload O(m*n^2).
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 2c9164b1c4 policy: precompute node routes in the peer-map build
CanAccess recomputed each node's routes per pair, making the scan O(n^2)-heavy.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 0e7b154617 mapper: register reconnecting node atomically against cleanup
A reconnect could be deleted from b.nodes mid-AddNode and orphaned.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby ad2693ff13 mapper: record initial-map peers only after delivery
A timed-out initial map left phantom lastSentPeers, skewing later diffs.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 7d845ef65e db: advance allocator cursor under lock during IP backfill
Reading prev4/prev6 unlocked raced Next; not advancing made backfill O(n^2).
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 9f0c74e73a dns: release lock before extra-records channel send
A blocked send held the write lock and leaked the watcher at shutdown.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby e413919810 derp: clone regions when merging DERP maps
In-place node shuffling aliased regions shared with the served map.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 0921972f96 oidc: avoid slice panic in getCookieName for short values
A malformed short nonce no longer panics; it yields a non-matching name.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 99ad555d64 db: handle degenerate prefixes in random IP allocation
Single-address prefixes panicked; zero-high-byte addresses failed to encode.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 10696fa634 db: look up API keys by explicit primary key, not struct condition
GetAPIKeyByID(0) returned the first key instead of not-found.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 84c99023e5 util: check RNG error before slicing url-safe random string
A crypto/rand failure sliced empty output and panicked.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby ba54349176 util: handle single-address IPv4 prefix in reverse DNS generation
A /32 indexed past the 4-byte address and panicked at startup.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 08f186f22a state: skip database persist for keepalive-only map requests
A lone LastSeen bump no longer triggers a full-row write and policy rescan.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 017162dac1 state: signal NodeStore shutdown without closing writeQueue
Writes racing Stop now drop cleanly instead of panicking on send.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby bb06b90543 types: skip malformed derp.urls entries instead of panicking
A failed url.Parse left a nil pointer that was dereferenced at startup.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 5a70a72988 types: lock tailcfg DNS config access for extra-records updates
The watcher write raced per-client map builds cloning ExtraRecords.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby ec94573258 db: drop superseded ephemeral deletions in the GC drain loop
A node reconnecting after its deletion queued is no longer removed.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 4c165ae5e7 db: reap ephemeral GC watcher goroutine on cancel and reschedule
A stopped timer never fires, so the per-node watcher leaked until close.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 06d6816dc9 state: keep nil expiry for nodes that stay tagged on reauth
The convert-from-tag arm wrongly set an expiry on still-tagged nodes.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby f61753e737 db: bound IP allocation scan so exhausted prefixes error out
Random strategy previously spun forever under the allocator mutex.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 4f67300005 types: clone Hostinfo before applying DERP change
Stops mutating a NetInfo pointer shared with a published snapshot.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 2e2401833b state: persist live NodeStore node in persistNodeToDB
Avoids clobbering concurrent admin writes (tags, routes, rename).
2026-06-08 10:04:49 +02:00
Kristoffer Dalby 29f87e5eaa flakehashes: refresh vendor hash after dropping gorilla/mux
Regenerated by `go run ./cmd/vendorhash update` so the Nix build's
vendorHash matches the new go.mod/go.sum fingerprint.

Updates #3296
2026-06-05 15:00:59 +02:00
Kristoffer Dalby f61d21b4a7 go.mod: drop unused gorilla/mux dependency
ApplePlatformConfig was the last consumer; go mod tidy removes the
require and the two checksum lines.

Updates #3296
2026-06-05 15:00:59 +02:00
Kristoffer Dalby b892b8f254 hscontrol: read Apple platform via chi.URLParam, not mux.Vars
ApplePlatformConfig still used gorilla mux.Vars after the chi
migration in 30338441, so every /apple/{platform} request returned
400 "no platform specified". Read the param via chi and add tests.

Fixes #3296
2026-06-05 15:00:59 +02:00
Florian Preinstorfer 6777a82ee6 Rewrite reverse proxy documentation
- Restructure and cleanup reverse proxy docs
- Update examples for Apache, Caddy, Nginx
- Provide some basic usage examples
2026-06-04 09:59:19 +02:00
Kristoffer Dalby 5228cb1a40 change: drop subnet-router full update, use policy change
Don't send a full update when a subnet router goes up or down; the gated
policy change already recomputes peers and is a smaller payload.

Updates #3293
2026-06-03 19:05:24 +02:00
Kristoffer Dalby cffdb77c8b mapper, change: coalesce duplicate policy recomputes per tick
Deduplicate broadcast policy changes within a tick so peers don't recompute
their netmap more than needed during a reconnect storm.

Updates #3293
2026-06-03 19:05:24 +02:00
Kristoffer Dalby 7706552c99 state: gate reconnect PolicyChange on NodeNeedsPeerRecompute
Connect and Disconnect appended change.PolicyChange() on every reconnect. PolicyChange sets RequiresRuntimePeerComputation, so the batcher rebuilt a full netmap (packet filters, SSH policy, peer serialization) for every connected node — O(N) per reconnect, O(N^2) on a restart storm. On a small VM this saturated CPU after the v0.28 -> v0.29 upgrade.

Emit it only when the node's online state changes what peers compute: subnet routers, relay targets, and via targets. An ordinary reconnect now sends just the lightweight online/offline peer patch. Relay and via targets still recompute, so peers drop a stale PeerRelay allocation when a relay goes offline.

Fixes #3293
2026-06-03 14:51:57 +02:00
Kristoffer Dalby bceac495f9 policy: add NodeNeedsPeerRecompute predicate
Reports whether a node's online/offline transition forces peers to recompute their netmap. True for subnet routers, relay targets (tailscale.com/cap/relay), and via targets; false otherwise.

The relay-target IP set and via-target tag set are precompiled from the grants in updateLocked, alongside the existing filter, so the per-node check is a cheap set lookup. Keyed on the node itself, so an ordinary node in a tailnet that uses relay or via for other nodes is still classified as not needing a recompute.

Updates #3293
2026-06-03 14:51:57 +02:00
Kristoffer Dalby 171fd7a3c5 policy: key autogroup:self invalidation on UserID not User view
invalidateAutogroupSelfCache derived the owning user from
node.User().ID(), dereferencing the User association view. The
NodeStore stores nodes by value with User as a *User pointer, and not
every write path hydrates that association, so a non-tagged node could
reach SetNodes with UserID set but User nil. UserView.ID() then
dereferenced a nil *User and panicked on /machine/map whenever an
autogroup:self policy was active and a node restarted tailscaled.

Group affected nodes by node.TypedUserID(), which reads the UserID
field directly. UserID is the authoritative ownership field for
non-tagged nodes, so this needs no hydrated association and fixes every
.User().ID() site in the function.
2026-05-29 21:42:56 +02:00
Shourya Gautam ea8fc72570 db: backfill zero-time node expiry to NULL
Versions before 0.28 persisted a pointer to a zero time.Time as
'0001-01-01 00:00:00+00:00' in nodes.expiry rather than NULL. 0.29
reports those rows as expired. #3197 and #3199 stopped new writes;
this normalises the existing rows so the column once again means
"no expiry" when unset.

Fixes: #3284

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:49:05 +02:00
Kristoffer Dalby 77ba225cdb db: treat Go module pseudo-versions as dev builds
Untagged main-sha builds inherit a Go module pseudo-version from
runtime/debug.BuildInfo (vX.Y.Z-<timestamp>-<hash>). isDev only
filtered "", "dev", and "(devel)", so the pseudo-version was stored
in database_versions and the next real release tripped the
multi-minor upgrade guard:

    headscale version v0.29.0-beta.1 cannot be used with a database
    last used by v0.0.0-20260520093041-e4e742c776ee, upgrading more
    than one minor version at a time is not supported

Add pseudoVersionTime, a regex + time.Parse predicate covering all
three Go pseudo-version forms (v0.0.0 base, pre-release ancestor,
release ancestor), and delegate from isDev. The dev gate at
db.go:790 already prevents pseudo-versions from being written, so
already-poisoned databases self-heal on the next real-release start.

Fixes #3281
2026-05-26 17:29:13 +02:00
Kristoffer Dalby 4483fd0cad tsic, gh: keep unstable on Docker Hub
ghcr.io/tailscale/tailscale:unstable is stale (last updated 2022,
points to v1.35.25). Only tailscale/tailscale on Docker Hub publishes
current unstable builds, so tsic.go reverts the unstable case and
build-tailscale-released pulls unstable from Docker Hub while keeping
release tags on ghcr.io.

Release tags on ghcr.io match Docker Hub by digest (verified v1.96),
so the rate-limit avoidance for the bulk of pulls is preserved.

Add the conditional docker/login-action step to the new job so the
main repo gets authenticated pulls; fork PRs fall through to anonymous
DH for the single unstable pull per CI run, well under the 100/6h
anonymous limit.
2026-05-22 14:29:24 +02:00
Kristoffer Dalby 66a5f99bfa gh: pre-pull released tailscale images for fork-PR CI
Fork PRs anonymous-pull tailscale/tailscale:vX.Y at test time and hit
Docker Hub rate limits, causing flakes. A new build-tailscale-released
job pulls the MustTestVersions set from ghcr.io once per CI run and
ships them to every test job as a gzipped tarball artifact, matching
the shape of the existing headscale/HEAD/postgres caches.

The version list is driven by 'hi list-versions' so capver is the
single source of truth; adding a version there propagates to CI
without YAML edits.

ghcr.io public reads need no auth, so the new job has no docker login
step.
2026-05-22 14:29:24 +02:00
Kristoffer Dalby 2e49f3dc67 tsic: pull tailscale images from ghcr.io
Anonymous reads on ghcr.io are unmetered. Pulling tailscale/tailscale
from Docker Hub fails on fork PRs without DOCKERHUB_USERNAME because
the unauthenticated rate limit is hit at test time.

ghcr.io/tailscale/tailscale publishes the same tags. The cache-hit
short-circuit in dockertestutil.PullWithAuth still skips the pull when
the image is already loaded locally, so the change is a no-op once CI
pre-pulls the images.
2026-05-22 14:29:24 +02:00
Kristoffer Dalby 79562b9782 hi: add list-versions subcommand
Prints the tailscale versions integration tests use, with optional
filter and format flags. Source of truth stays in hscontrol/capver;
CI shells out to this instead of duplicating the version list in
YAML.

Examples:
  hi list-versions
  hi list-versions --set=must --exclude=head
  hi list-versions --set=all --format=json
2026-05-22 14:29:24 +02:00
Kristoffer Dalby 58a85b68b3 CHANGELOG: bump 0.29.0 minimum tailscale client to v1.80.0
Reflects the capver floor move from 109 (v1.78) to 113 (v1.80) that
ships with this release.
2026-05-22 11:22:01 +02:00
Kristoffer Dalby eb23c125fa capver, types: bump to tailscale v1.98, drop LegacyDERPString
Regenerate capver for tailscale v1.98 — `MinSupportedCapabilityVersion`
slides 109 → 113 (v1.78 → v1.80). v1.80+ clients understand
`Node.HomeDERP`, which superseded `LegacyDERPString` upstream at capver
111. Drops the emit, plumbing, the trip-wire that caught this cleanup,
the golden test fixtures, and the integration check on
`peer.LegacyDERPString()` — the sibling `HomeDERP` check covers intent.

`v1.98` was added by hand to `capver_generated.go` because tailscale
skipped publishing v1.97/v1.98 to ghcr until late in the cycle; a clean
regeneration produces the same content.
2026-05-22 11:22:01 +02:00
Kristoffer Dalby 570735f204 gen: regenerate grpc stubs with protoc-gen-go-grpc v1.6.2 2026-05-22 11:22:01 +02:00
Kristoffer Dalby 78570c754f Dockerfile: bump base images
Bump golang to 1.26.3, alpine to 3.23, rust to 1.95 across the
integration, derper, tailscale-HEAD, and tailscale-rs Dockerfiles.
2026-05-22 11:22:01 +02:00
Kristoffer Dalby 25adfaf341 flake.nix, flake.lock: bump nixpkgs and pinned tools
nixpkgs: bump unstable revision. Pinned tools: grpc-gateway 2.28.0 ->
2.29.0 (matches the Go dep), golangci-lint 2.11.4 -> 2.12.2. Disable
gomodguard in .golangci.yaml since 2.12 deprecated it.
2026-05-22 11:22:01 +02:00
Kristoffer Dalby be90910d33 go.mod, go.sum: bump dependencies for v0.29.0
Refresh direct deps (tailscale v1.98.1, modernc.org/sqlite v1.50.1 +
libc v1.72.3 lockstep, otel cluster v1.43.0, x/* family, pgx v5.9.2,
go-jose v3.0.5, grpc v1.81.1, grpc-gateway v2.29.0) and bulk-update
remaining direct deps. tailscale held at v1.98.1 since v1.98.2 demands
go 1.26.3 which nixpkgs unstable does not yet ship.
2026-05-22 11:22:01 +02:00
Kristoffer Dalby 575d8ecbfd changelog: normalise 0.29.0 BREAKING and Changes sections
Move HA subnet router health probing above BREAKING so the layout
matches every other release. Drop **User deletion**: / **Node Expiry**:
bold prefixes redundant with the #### subgrouping. Fill missing PR
refs: #3202 (hostname rewrite), #3263 (sshTests + SSH rule validation),
#3194 (HA probe), #3251 (randomize_client_port removal), #3268
(trusted_proxies).
2026-05-20 14:17:24 +02:00
Kristoffer Dalby e4e742c776 noise: pin outer RemoteAddr onto tunnel requests
The HTTP/2 server inside the Noise tunnel fills r.RemoteAddr from the
hijacked TCP socket, so /machine/register and /machine/map logged the
reverse proxy's loopback peer (e.g. 127.0.0.1:44388) even with
trusted_proxies set. The outer router's realIPMiddleware had already
resolved the client IP onto req.RemoteAddr; that value never crossed
the hijack.

Replace the inner realIPMiddleware mount — dead inside the encrypted
tunnel — with overrideRemoteAddr(req.RemoteAddr) so requests served
over the tunnel report the outer-resolved client IP.
2026-05-20 11:30:41 +02:00
Kristoffer Dalby 4cca63155d all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.

Pattern applied across the tree:
  In-package         [Foo], [Foo.Bar]
  Cross-package      [pkg.Foo], [pkg.Foo.Bar]
  Stdlib             [netip.Prefix], [errors.Is], [context.Context]
  Tailscale          [tailcfg.MapResponse], [tailcfg.Node.CapMap],
                     [tailcfg.NodeAttrSuggestExitNode]

Skip rules:
  - File:line refs left as plain text
  - HuJSON wire keys inside backtick raw strings untouched
  - ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
    symbols, left as plain text
  - JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
    link tags, decorative dividers — all left as-is
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 17236fd284 all: annotate complex functions with gocyclo rationale
Splitting these functions does not buy clarity — each has been
extracted before and put back. Pin the //nolint:gocyclo on each
with the reason their shape resists clean decomposition.

  policy/v2/policy.go     ViaRoutesForPeer        — three-pass
                                                    via-grant resolution
  policy/v2/filter.go     compileSSHPolicy        — per-rule
                                                    branches with
                                                    intertwined
                                                    autogroup:self
                                                    handling
                                                    (annotated in the
                                                    earlier nil-error
                                                    commit)
  state/state.go          HandleNodeFromPreAuthKey — security-
                                                    sensitive
                                                    sequential
                                                    validation order
  servertest/routes_test  TestRoutes              — table-driven
                                                    test driver with
                                                    many independent
                                                    subtests

Also: //nolint:recvcheck on policy/v2.SSHUser — UnmarshalJSON
requires a pointer receiver; the other methods on this string
newtype use value receivers by convention.
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 3e2aa5814e all: annotate gosec false positives with rationale
Each //nolint:gosec carries the gosec code and one line on why
the finding is a false positive or already mitigated.

  G124 cookies (oidc.go x3, oidc_confirm_test.go)
    Secure is set conditionally on req.TLS != nil; HttpOnly and
    SameSiteStrictMode already on. gosec misses the conditional.
    Test fixture cookie is explicitly a test fixture.

  G705 (debug.go)
    templates.PingPage(...).Render() is a templ component that
    auto-escapes user input.

  G706 (scenario.go)
    Integration log emits trusted scenario state. The pre-built
    image G706 sites in hsic.go / tsic.go ride along with the
    earlier constants commit.

  G710 (app.go, tailsql.go)
    Redirect target is "trusted ServerURL prefix + path". gosec
    cannot see past the prefix.
2026-05-19 09:55:22 +02:00
Kristoffer Dalby f905d58292 all: mechanical lint fixes
hscontrol/debug.go — pre-size nodes []nodeStatus to len(debugInfo)
    so the loop does not grow under append.
  hscontrol/mapper/batcher_test.go — testing.TB parameter on
    setupBatcherWithTestData renamed t → tb so thelper sees the
    expected name.
  hscontrol/db/text_serialiser.go — reflect.Ptr → reflect.Pointer
    (deprecated alias).
2026-05-19 09:55:22 +02:00
Kristoffer Dalby e00c899219 cmd, templates, integration: extract shared production constants
Constants the operator/test reader benefits from centralising.
Tests stay verbatim (the .golangci.yaml goconst tune skips them);
extraction here applies only where the same literal acts as
shared vocabulary across files.

  cmd/headscale/cli/strings.go (new)
    Cobra subcommand verbs and aliases shared by every list / show
    / new / delete / expire command across api_key, nodes, policy,
    preauthkeys, users — plus the Result / Created / Expiration
    column headers used in printOutput maps.

  hscontrol/templates/design.go
    cssBorderHS, cssBreakWord, cssCenter, cssOverflowWrap — shared
    styles applied across design.go, ping.go, register_confirm.go.
    spaceS already existed; switch raw "0.5rem" literals to it.

  integration/hsic/hsic.go
    binHeadscale, flagOutput, acceptJSON — names invoked across
    hsic.go and config.go.

  integration/tsic/tsic.go
    tailscaleBin — used across docker exec call sites.
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 64c398f2c2 metrics, policy/v2: drop unused scaffolding + nil-error returns
Two cleanups in the same package boundary:

policy/v2.Policy.compileFilterRules and compileFilterRulesForNode
returned (rules, error) where the error was always nil. Drop the
error return and the dead error handling at 15 call sites in the
compat tests.

Delete code with no callers:
  hscontrol/metrics.go
    prometheusMiddleware + respWriterProm — custom HTTP timer
    middleware that was never registered. /metrics stays mounted
    via debug.go and noise.go; mapresponse_* counters keep emitting.
    httpDuration and httpCounter — backing metrics for the deleted
    middleware.
  hscontrol/policy/v2/filter.go
    resolvedAddrsToPrincipals — superseded by ipSetToPrincipals.
    ipSetToPrefixStringList — superseded by inline prefix
    formatting at the surviving callers.
  hscontrol/policy/v2/tailscale_{acl,grants}_data_compat_test.go
    setupACLCompatNodes / setupGrantsCompatNodes — the data-driven
    tests switched to per-scenario topology reconstruction.
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 7f02210863 .golangci: ignore tests for goconst, raise occurrence threshold
Test fixtures repeat strings (IPs, tags, hostnames, user/email
names) by their nature; extracting each into a named constant
adds indirection without adding clarity. Keep goconst strict on
production code, off on tests.

  ignore-tests: true       drop test-fixture noise entirely
  min-occurrences: 5       raise from default 3 to filter
                           "happens thrice" non-vocabulary cases
  min-len: 6               skip short literals like "set", "get",
                           "new" that read better at call sites
2026-05-19 09:55:22 +02:00
Florian Preinstorfer e07b39108f Quote autogroup:self in the CHANGELOG 2026-05-18 17:21:58 +02:00
Florian Preinstorfer e285f3c932 The headscale service is enabled by default 2026-05-18 17:21:58 +02:00
Florian Preinstorfer 355733342f Update config-example links 2026-05-18 17:21:58 +02:00
Florian Preinstorfer f3f84a5a63 Add docs for policy-wide options and node attributes 2026-05-18 17:21:58 +02:00
Florian Preinstorfer 4eb5899154 Add taildrive, tests, sshTests as supported features 2026-05-18 17:21:58 +02:00
Kristoffer Dalby e2f2f9211f state, servertest: property-test HA election + invariant catalogue
Expand TestPrimaryRoutesProperty (5 -> 9 ops). New ops mirror the
production shapes the failure cases hit: BatchProbeResults via
UpdateNodes, SimultaneousDisconnect via UpdateNodes, SetApprovedRoutes
that leaves announced RoutableIPs intact, OfflineExpiry that keeps
Unhealthy set. The model now tracks announced and approved separately
and recomputes the intersection.

Strengthen the per-op assertions to cover invariants the model alone
cannot prove: every primary must be online, every primary must
currently advertise its prefix, no flap onto an unhealthy candidate
when a healthy one was available, no flap off a previous primary that
remains a healthy candidate. The check now takes a pre-op snapshot so
the anti-flap rule has a stable reference.

Add TestHAProberProperty in servertest. It drives a real TestServer
with three HA-route-advertising clients through rapid-drawn sequences
of ClientDisconnect / ClientReconnect / ProberTick / WaitForSnapshot
ops and re-checks the same shape invariants after every step.

Document the system in hscontrol/state/HA_INVARIANTS.md: a state
machine over (Healthy+Online, Unhealthy+Online, Offline,
OfflineExpired), fifteen numbered invariants with predicates and
violation paths, and a coverage matrix mapping each invariant to its
unit, servertest, and integration tests. Three rows pin the recent
fixes to the invariants they enforce.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby c7630b505b state: leave prefix unmapped when all primary candidates unhealthy
electPrimaryRoutes' all-unhealthy fallback picked candidates[0] when
the previous primary was no longer a candidate. The Phase-5
simultaneous dual-disconnect path in TestHASubnetRouterFailoverDocker
Disconnect hits this asymmetrically: a batched probe cycle marks both
routers unhealthy with prev=r2 preserved, then the grace-period
Disconnect for r2 drops it from candidates. With prev gone and the
remaining r1 still carrying its Unhealthy bit, the fallback pointed
peers at the cable-pulled r1 — flapping primary to an unreachable
node and tripping requirePrimaryStable.

Leave the prefix unmapped when prev is gone and every candidate is
unhealthy. Peers see no advertiser instead of an unreachable one,
which is honest: the next probe cycle re-evaluates and picks
whichever node responds. The property-test model that mirrored the
old behaviour is updated to match.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby de6be71a86 state: batch HA probe results so dual-disconnect cannot flap primary
requirePrimaryStable in TestHASubnetRouterFailoverDockerDisconnect
Phase 5a (simultaneous cable-pull of both routers) intermittently
caught the primary flipping to the offline r1. Both probe goroutines
mark their target unhealthy back-to-back; SetNodeUnhealthy publishes a
fresh NodeStore snapshot each call, so the intermediate snapshot — r1
unhealthy, r2 still healthy — runs the election with one healthy
candidate left and picks it. The next snapshot then enters the
all-unhealthy preserve-prev path, which preserves the wrong choice.

Collect probe results from the cycle and apply them through a new
NodeStore.UpdateNodes batched op so the election only runs once, with
the cycle's final health state. PolicyChange dispatch moves outside
the wg.Go goroutines and fires once if the primary assignment
actually changed.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby fb8eecae25 state: defer HA failover when probe target reconnected mid-cycle
The HA prober dispatches a PingRequest, waits ProbeTimeout (5s), and
marks the node unhealthy if no callback arrives. A node that bounced
its poll session between probe cycles satisfies two conditions that
conspire to fail TestHASubnetRouterFailover: a probe queued against
the previous session is silently dropped when the worker writes to
the closed connection (timeout always fires), and a probe sent
immediately after reconnect lands while wgengine is still rebuilding
magicsock state from the new netmap. Either path installs a spurious
unhealthy bit, which sends the preserved-primary anti-flap the wrong
way.

Record the session observed at dispatch time and drop the timeout
path if the node reconnected since. Require the session to survive
a full probe cycle before a timeout can drive a failover.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby a345a22a3b mapper, app: ship MagicDNS Routes as empty slices, not nil
policyChangeResponse already includes everything else; carry DNSConfig
too so the client's netmap DNS is anchored on every policy change
rather than relying on the previous snapshot.

Send the MagicDNS root domains as empty non-nil Resolver slices instead
of nil values. tailcfg.DNSConfig.Clone and net/dns.Config.Clone in
tailscale drop map entries whose value is nil (tailcfg_clone.go and
dns_clone.go both contain `if sv == nil { continue }`). On a major
LinkChange the client's wgengine handler clones lastDNSConfig and
re-applies it; with nil values the cloned config has Routes:{}, dns.Set
wipes Nameservers in /etc/resolv.conf, and curl-by-FQDN fails until
the next route-changing netmap, typically about six minutes later.
Empty slice survives Clone and carries the same "resolve locally"
semantics for Routes entries.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby dfcc96d808 integration: harden ACL test ergonomics
tsic.Curl returned ("", nil) when curl exited 0 with a zero-byte body —
the usual signature of a mid-stream reset — so EventuallyWithT could
not retry. Return an error on empty body instead.

Replace the 56 inline curl + assert.Len(13) blocks with
assertCurlDockerHostname so the empty-body fix benefits every callsite
without further touch-ups.

Gate ACL waits on actual filter visibility: snapshotClientFilters +
waitForClientFilterChange ensure the new PacketFilter has reached the
client before assertions fire; SyncOption + WithPreBarrier feeds a
server-side policy-loaded check into WaitForTailscaleSyncPerUser.

Move advertise-routes mutation out of EventuallyWithT in route_test
(cmd/hi/README forbids retrying state-mutating calls). Pace the
TestNodeOnlineStatus outer loop with a Ticker, not a Sleep.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby 78fd6efb38 integration: replace ad-hoc test timeouts with named constants
Categorised timeouts in integrationutil/timeouts.go remove the drift
opportunity between same-purpose budgets repeated across the suite.
The auth, cli, dns, derp, ssh, and tags tests are swept; acl, route,
and general tests follow in later commits alongside their other
ergonomic fixes.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby eec3844f24 integration/dockertestutil: wait for libnetwork settle on reconnect
DisconnectContainerFromNetwork and ReconnectContainerToNetwork
returned as soon as the docker API call completed, but libnetwork
bridge reprogramming continued for several seconds after. The HA
disconnect tests then raced and bounced between healthy and broken
bridges. Poll until the container's endpoint is gone (on
disconnect) or reconciled (on reconnect), and on the
"conflicts with existing route" surface clear the stale subnet
route from the netns and retry. Settle is now baked into the
primitive so every caller benefits.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby 98e9ff4d36 integration: authenticate Docker Hub pulls and retry transient errors
Anonymous Hub pulls trip the 100/6h IP cap on shared CI runners, turning
into singleton FAIL reports whenever the runner egress IP crosses the
quota. Route every pull through Docker Hub credentials when present, and
retry transient errors with backoff. tsic and hi use the same helper so
both surfaces honour ~/.docker/config.json and the GHA secrets.
2026-05-18 17:18:08 +02:00
Kristoffer Dalby 4d3b567149 ci: use overlay2 storage driver instead of pinning docker v28
Docker 29 itself works; the breakage on the GHA runner image was the
overlayfs default. Setting storage-driver=overlay2 restores the
long-standing default and lets the suite run on the current Docker
without the apt downgrade dance.

Fixes #3094
2026-05-18 17:18:08 +02:00
Kristoffer Dalby 963daf8908 docs: document trusted_proxies config option
Cover the option in config-example.yaml, the reverse-proxy
integration guide, and the 0.29.0 CHANGELOG.
2026-05-18 17:17:55 +02:00
Kristoffer Dalby c6c29c05e5 hscontrol: gate proxy header trust on trusted_proxies
chi middleware.RealIP was mounted unconditionally on both the
public router and the noise router, so any client could send
X-Real-IP or X-Forwarded-For and have the spoofed value land in
r.RemoteAddr and the access-log remote= field.

Add a top-level trusted_proxies config option (list of CIDRs) and
replace middleware.RealIP with a gated middleware that:

  - honours True-Client-IP / X-Real-IP / X-Forwarded-For only when
    r.RemoteAddr is inside one of the configured prefixes;
  - strips those three headers from every request whose peer is
    not trusted, so downstream handlers cannot read them.

X-Forwarded-For is parsed via realclientip-go's
RightmostTrustedRangeStrategy so a prepended value cannot win in a
proxy chain. trustedProxies() rejects 0.0.0.0/0 and ::/0 at config
load.

Empty trusted_proxies (the default) skips the mount entirely;
r.RemoteAddr is the directly-connecting TCP peer.
2026-05-18 17:17:55 +02:00
Kristoffer Dalby 1f48ebb376 go.mod: add github.com/realclientip/realclientip-go
Used by the trusted_proxies middleware for safe X-Forwarded-For
parsing with rightmost-trusted-range semantics.
2026-05-18 17:17:55 +02:00
Kristoffer Dalby b5b786f519 servertest: cover broader-dst via grant in filter test
TestGrantViaSubnetFilterRules pins exact-equality dst. Add a sibling
for the broader-dst case so the regression sits at the server level
alongside the policy-engine unit test.

Updates #3267
2026-05-18 14:02:00 +02:00
Kristoffer Dalby 2cb914df59 policy/v2: add SaaS goldens for via-grant prefix containment
Captures from Tailscale SaaS exercising broader, narrower, host
alias, disjoint, and 4via6 grant destinations against advertised
subnet routes. TestGrantsCompat replays them.

Updates #3267
2026-05-18 14:02:00 +02:00
Kristoffer Dalby e5fcd01ee6 policy/v2: match via-grant destinations by prefix overlap
slices.Contains required exact equality between grant dst and the
advertised subnet route. Any non-identical pair was rejected, so a
via grant with broader (or narrower) dst emitted no filter rule and
added no route to the viewer's AllowedIPs. Tailscale SaaS uses
containment in either direction.

Switch to slices.ContainsFunc(routes, dst.Overlaps) for filter rule
emission (keep dst literal in DstPorts), and append overlapping
advertised routes to ViaRoutesForPeer.Include / Exclude. Rewrite the
multi-router HA election and regular-grant overlap detection to key
off the matched routes rather than the dst. Resolve *Host aliases to
*Prefix once in compileOneViaGrant and at the top of ViaRoutesForPeer
so the switch arms reach them.

Fixes #3267
2026-05-18 14:02:00 +02:00
Kristoffer Dalby af7e7a4560 db: remove unused SetApprovedRoutes and SetTags helpers
Both helpers existed to write the literal "[]" when clearing a slice
column — a workaround for GORM's struct-Updates skipping nil slices.
The State path goes exclusively through persistNodeToDB, which is now
correct end-to-end thanks to the named IsZero slice types, so the
helpers are dead in production. The remaining callers were tests.

TestSetTags is dropped — TestSetTags_* in hscontrol/grpcv1_test.go
already covers the State path that production uses. TestAutoApproveRoutes
now writes routes via DB.Save on the loaded node, which is the path
gRPC SetApprovedRoutes drives in production.

Updates #3110
2026-05-15 11:21:58 +02:00
Kristoffer Dalby b1196baf6d state: add regression test for Node slice persistence
Drives the persist path for ApprovedRoutes, Tags and Endpoints —
seed a non-empty value, clear to nil, read the column back from disk,
then close the State and reopen one against the same sqlite file to
simulate a server restart. Pins the contract the named IsZero slice
types enforce so future changes to the persist path cannot silently
drop a cleared slice column.

Updates #3110
2026-05-15 11:21:58 +02:00
Kristoffer Dalby 7a20db9f49 types: persist Node JSON slices via named IsZero types
Endpoints, Tags and ApprovedRoutes serialize as JSON on Node. GORM's
struct Updates path skips fields it considers zero, and reflect treats
a nil slice as zero — clearing any of these columns via the State
persist path would leave the previous value in the database.

Introduce Strings, Prefixes and AddrPorts as named slice types whose
IsZero() always reports false, so GORM keeps the column in the UPDATE
regardless of the slice being nil or empty. JSON marshalling is
unchanged: nil serializes to null, empty to []. List() returns the
underlying unnamed slice for callers (mainly testify assertions over
reflect.DeepEqual) that distinguish the named type from its base.

Regenerated types_clone.go and types_view.go follow the field-type
swap. Test assertions across hscontrol/{db,state,servertest} updated
to call .List() where reflect.DeepEqual previously matched the raw
slice type.

Fixes #3110
2026-05-15 11:21:58 +02:00
Kristoffer Dalby e78a24b892 CHANGELOG: document sshTests evaluation (beta) 2026-05-13 21:10:13 +02:00
Kristoffer Dalby 574a61852a integration: reject failing sshTests at headscale policy set 2026-05-13 21:10:13 +02:00
Kristoffer Dalby 92a9accfcb cmd/headscale/cli: mention sshTests in policy check help 2026-05-13 21:10:13 +02:00
Kristoffer Dalby 26eebcea5a policy/v2: add sshtester compat runner
Replays recorded policy responses for the sshTests block. 200 captures must evaluate; non-200 captures must reject with the recorded body as a substring of the headscale error. Divergences are listed in knownSSHTesterDivergences.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby 013dea4f40 policy/v2: evaluate sshTests at write boundary
SetPolicy and policy check now compile per-dst SSH rules and replay each sshTests entry. The accept assertion treats check-action rules as reachable; the check assertion requires HoldAndDelegate on the matching rule. Boot reload warns and continues.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby 6a0a297c7f policy/v2: validate sshTests at parse
Adds SSHPolicyTest plus parse-time validation: empty src/dst, port/CIDR/autogroup-internet destinations, and tag references missing from tagOwners are rejected. Engine evaluation comes in a follow-up.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby d600090f2c policy/v2: align SSH rule validation with Tailscale
Trim whitespace on action, users, src, dst; reject empty/wildcard users; reject empty acceptEnv; reject negative and over-max checkPeriod; reject hosts-table aliases as SSH dst; reject non-ASCII tag names; tolerate tag-owner cycles; match group-nesting wording.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby 4ad200ab73 hscontrol: preserve nil expiry on tailscaled restart
The guard added for #2862 in handleRegister checked
node.Expiry().Valid() before preserving node state on
Auth=nil + Expiry=zero registration requests. Valid() returns false
when node.Expiry is nil, the default for tagged nodes and for untagged
nodes registered against a preauth key with no default node.expiry
configured. Both fell through to handleLogout, which wrote
&time.Time{} (0001-01-01T00:00:00Z) over the original nil — the
user-visible 0001-01-01 expiry that `headscale nodes list` reports
after restart.

IsExpired() already returns false for both nil and zero-time, so the
Valid() check was redundant. Drop it so all nil-expiry nodes are
covered by the same preservation path.

Fixes #3170
Fixes #3262
2026-05-13 17:06:16 +02:00
Kristoffer Dalby 5d502bfb88 types/node, mapper: strip own IPv4 from emission when node has disable-ipv4 cap
When a node carries the disable-ipv4 nodeAttr documented at
https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts,
SaaS stops sending the node's CGNAT IPv4 prefix in MapResponse. The
allocator keeps assigning IPv4 server-side; only the wire-shape
delivery is filtered. Subnet routes the node advertises -- including
IPv4 prefixes -- survive in AllowedIPs and PrimaryRoutes.

TailNode now drops Is4 prefixes from Addresses and from the node's
own /32 slot in AllowedIPs when selfPolicyCaps carries
disable-ipv4. Mapper.buildTailPeers passes each peer's policy
CapMap so the filter applies in viewer netmaps too; the CapMap
merge that follows is overwritten by PeerCapMap so only the address
filter survives on the peer path.

Two captures land in testdata/nodeattrs_results to anchor the
behaviour:

  - nodeattrs-attr-c15-disable-ipv4         (on tag:client)
  - nodeattrs-attr-c16-disable-ipv4-router  (on tag:router, which
    advertises 10.33.0.0/16, confirming subnet routes survive)
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 64d13f77e8 types/config, types/node: model default-auto-update from auto_update.enabled
Tailscale stamps tailcfg.NodeAttrDefaultAutoUpdate on every node's
CapMap with a JSON bool reflecting the tailnet-wide auto-update
default. Headscale grows an auto_update.enabled config option and
emits the cap accordingly from TailNode -- the cap leaves the
unmodelledTailnetStateCaps strip list and is compared in full by the
nodeAttrs compat suite.

testNodeAttrsSuccess drives cfg.AutoUpdate.Enabled from
tf.Input.Tailnet.Settings.DevicesAutoUpdatesOn so each capture's
expected emission matches the SaaS state it was taken under. Two
captures cover both branches:

  - nodeattrs-tailnet-devices-auto-updates-on  -> [true]
  - nodeattrs-tailnet-devices-auto-updates-off -> [false]

The Tailscale v2 TailnetSettings API does not expose the Send Files
toggle, so the compat suite cannot vary cfg.Taildrop.Enabled per
capture. TestTaildropDisabledWithholdsFileSharingCap covers the off
path directly in servertest.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 408f4022e4 CHANGELOG: document nodeAttrs feature and migrations 2026-05-13 14:22:30 +02:00
Kristoffer Dalby 8ea4cd3faa types/node, policy/v2: drop taildrive caps from baseline emission
Taildrive (drive:share and drive:access) is policy-driven per
Tailscale's documented behaviour
(https://tailscale.com/docs/features/taildrive). The previous
always-on baseline emission diverged from SaaS for every node not
targeted by a drive nodeAttr -- a real semantic divergence that the
compat suite caught once the test moved to comparing TailNode output
against the captured netmaps.

types.Node.TailNode no longer stamps the drive pair. Operators
wanting taildrive add a nodeAttrs entry:

  "nodeAttrs": [
    { "target": ["*"], "attr": ["drive:share", "drive:access"] }
  ]

unmodelledTailnetStateCaps shrinks accordingly. The baseline-divergence
group is gone; every entry left in the list is genuinely unmodelled
(user-role caps, unimplemented features, tailnet metadata, internal
tuning).

servertest's TestNodeAttrsBaselineCapsAlwaysOn expects the smaller
baseline (admin + ssh + file-sharing). Integration TestGrantCapDrive
grants the drive caps explicitly via NodeAttrs to exercise the
policy-driven emission path.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 5ebc53c29e types/node, mapper, policy/v2: assemble self CapMap inside TailNode
types.NodeView.TailNode takes a selfPolicyCaps tailcfg.NodeCapMap
parameter and merges it into the baseline. The mapper's WithSelfNode
hands it the policy result via state.NodeCapMap; peer-path callers
pass nil because peer-side CapMap is set downstream via
policyv2.PeerCapMap.

The nodeAttrs compat test now diffs the full TailNode self-view
output against captured SaaS netmaps. Before this change the test
compared compileNodeAttrs alone -- the policy-only output -- and
needed a strip list to compensate for the missing baseline. With
TailNode on the diff path, baseline emission is exercised end-to-end
by every capture; a regression in TailNode breaks the suite.

unmodelledTailnetStateCaps drops cap/ssh and cap/file-sharing now
that both sides emit them identically. The file header is rewritten
to read as 'caps SaaS emits where headscale has no equivalent yet'
rather than the more confusing 'shape divergence' framing.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby b3f795f0b4 mapper, policy/v2: stamp suggest-exit-node on Peer.CapMap when exit routes approved
The Tailscale client surfaces 'use this peer as your exit node' when
the peer's CapMap carries the tailcfg.NodeAttrSuggestExitNode cap.
SaaS emits it only on peers whose advertised exit routes are
approved -- not every peer that just has the cap in its own
nodeAttrs slot.

policyv2.PeerCapMap encodes that emission rule: it walks the
peer's own self-CapMap (built from compileNodeAttrs) and surfaces
the gated entries (today just suggest-exit-node when the peer
IsExitNode). Mapper.buildTailPeers calls it for each peer instead
of merging the peer's full nodeAttrs CapMap onto its peer view.

allCapMaps snapshots the full per-node CapMap once per peer-list
build so pm.mu is acquired once rather than per peer.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 078b9e308f policy/v2: SaaS-derived compat tests for nodeAttrs
Adds a data-driven test that loads testdata/nodeattrs_results/*.hujson
and diffs the captured SaaS-rendered netmaps against headscale's
compileNodeAttrs output. Each capture is one scenario the SaaS
control plane has rendered against the same policy headscale is asked
to compile -- the test enforces shape parity per node.

tailnet_state_caps.go enumerates the caps SaaS emits where headscale
has no equivalent concept yet (user-role admin/owner, tailnet lock,
services host, app connectors, internal magicsock and SSH tuning,
tailnet-state metadata) plus the always-on baseline (admin, ssh,
file-sharing) and the taildrive pair. stripUnmodelledTailnetStateCaps
filters both sides of cmp.Diff so the comparison focuses on the
policy-driven caps. PeerCapMap encodes which caps the Tailscale
client reads from the peer view (suggest-exit-node when exit routes
are approved, etc.) for use by the mapper.

testcapture switches to typed tailcfg/netmap/filtertype/apitype
values so schema drift between the capture tool and headscale
becomes a compile error rather than a silent test failure. Existing
compat suites (acl, grants, routes, ssh, issue_3212) move to the
typed shape.

The 53 SelfNode netmap captures and the 7 anonymizer-corrupted
suggest-charmander -> suggest-exit-node restorations in
routes_results / issue_3212 ride along.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 3f73ed5404 config, types: move randomize_client_port from server config to policy file
Tailscale models the randomize-client-port toggle as a top-level
field on the ACL policy. Headscale now matches that shape: the
server-config randomize_client_port key is removed, the toggle
lives in the policy file as randomizeClientPort, and per-node
opt-in via nodeAttrs is also supported.

Operators upgrading from a config-set randomize_client_port hit
depr.fatalWithHint at startup, which prints the deprecation message
and points at the new policy field rather than silently dropping
the toggle. The default carries over (false) so operators who never
set it are unaffected. config-example.yaml ships a REMOVED stanza
showing the migration.

types/node.go drops the cfg.RandomizeClientPort read from
TailNode -- the cap is now policy-driven through compileNodeAttrs
and the tail_test.go expectations follow.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 6fcff9e352 mapper, state: deliver nodeAttrs through MapResponse and harden nextdns DoH rewrite
WithSelfNode and buildTailPeers merge each node's policy CapMap
into the tailcfg.Node.CapMap they emit. State.NodeCapMap and
State.NodeCapMaps wrap the policy manager: NodeCapMap returns a
defensive clone per call; NodeCapMaps snapshots the full per-node
map once for batched callers, amortising pm.mu acquisition across
a peer build.

generateDNSConfig grew a per-node CapMap argument so it can apply
nodeAttr-driven DNS overlays. The nextdns DoH rewrite hardens against
policy-controlled inputs:

  - nextDNSDoHHost anchors the prefix match instead of substring,
    so a hostile resolver URL cannot smuggle a nextdns hostname in
    a path or query.
  - nextDNSProfileFromCapMap accepts only profile names matching
    [A-Za-z0-9._-]{1,64} and picks the lexicographically first when
    multiple are granted -- deterministic, no shell metacharacters
    or URL fragments through.
  - addNextDNSMetadata composes the rewritten URL via url.Parse +
    url.Values rather than fmt.Sprintf, so existing query strings
    on the resolver URL survive and metadata cannot inject a new
    component.

WithTaildropEnabled in servertest controls cfg.Taildrop.Enabled per
test so cap/file-sharing emission can be toggled in tests that need
to verify the off path.
2026-05-13 14:22:30 +02:00
Kristoffer Dalby a4f05b0962 policy/v2: parse, validate, and compile nodeAttrs
ACL policies now accept a top-level nodeAttrs block. Each entry hands
a list of tailcfg node capabilities to every node matching target.
Accepted target forms are the same as acls.src and grants.src: users,
groups, tags, hosts, prefixes, autogroup:member, autogroup:tagged,
and *. autogroup:self, autogroup:internet, and autogroup:danger-all
are rejected at validate time because none describes a stable
identity set a node-level attribute can attach to.

NodeAttrGrant carries Targets, Attrs, and IPPool. IPPool is parsed
but rejected at validate time -- the allocator that consumes it is
not yet implemented. nodeAttrUnsupportedCaps lists caps SaaS accepts
that headscale cannot act on (funnel today) and rejects them with a
tracking-issue link in the error.

compileNodeAttrs resolves each entry's targets, then maps every
targeted node to a tailcfg.NodeCapMap of the entry's attrs. Per-node
IPs are cached once per call so the inner attr loop is O(grants)
instead of O(grants * nodes) IP allocations.

PolicyManager grows NodeCapMap (per-node), NodeCapMaps (snapshot for
batched callers), and NodesWithChangedCapMap (drain buffer for the
self-broadcast diff). refreshNodeAttrsLocked appends to the drain
rather than overwriting so a SetUsers/SetNodes between SetPolicy and
the drain cannot lose the policy-reload diff.
2026-05-13 14:22:30 +02:00
Florian Preinstorfer c4ab267c36 Refresh features page 2026-05-12 14:12:29 +02:00
Florian Preinstorfer 109bfc404c Refresh docs for Grants
- Mention policy as generic term that covers ACLs or Grants
- Refresh routes policy examples
- Remove Headscale specific exit node separation. Use via instead.

Fixes: #3087
2026-05-12 14:12:29 +02:00
Florian Preinstorfer 1a64d950fd Document supported autogroups once 2026-05-12 14:12:29 +02:00
Florian Preinstorfer edb7ad0f81 Rewrite ACL docs as policy
- Rename from acl.md to policy.md and setup redirect links
- Mention both ACLs and Grants
- Remove most old ACL docs and replace with links to Tailscale docs
- Add "Getting started" section
- Add section about notable differences
2026-05-12 14:12:29 +02:00
Florian Preinstorfer 892ffffc4a Remove misleading comment 2026-05-12 14:12:29 +02:00
Florian Preinstorfer e13f0458bb Remove redundant prefix 2026-05-12 14:12:29 +02:00
Florian Preinstorfer 68b0014871 Use distroless without quotes 2026-05-12 14:12:29 +02:00
Florian Preinstorfer 484462898b Remove link to sqlite
Other mentions of SQLite don't link either.
2026-05-12 14:12:29 +02:00
Florian Preinstorfer 45b698dbac Shorten container introduction 2026-05-12 14:12:29 +02:00
Florian Preinstorfer 14ce7e9106 Remove link to Arch AUR headscale-git
Its outdated and unmaintained.
2026-05-12 14:12:29 +02:00
Florian Preinstorfer 84c7f0d450 Link to development builds 2026-05-12 14:12:29 +02:00
Florian Preinstorfer c7f221dd0a Fix typo and wording 2026-05-12 14:12:29 +02:00
Florian Preinstorfer 163363a12a Use docs instead of KB 2026-05-12 14:12:29 +02:00
Kristoffer Dalby f03d41ea9a CHANGELOG: document policy tests (beta)
Fixes #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby d5b2837231 policy/v2: match default proto set for tests with no proto
The policy `tests` block lets entries omit `proto`. Tailscale's client
maps that to the default protocol set {TCP, UDP, ICMP, ICMPv6} — the
captured packet_filter_matches show all four IANA numbers explicitly
when no proto is set — and a rule restricted to any one of them
satisfies an empty-proto reachability test.

srcReachesDst was passing the empty Protocol through unchanged, which
landed an empty []int in ruleMatchesProto. The matcher then short-
circuited to "no match" for every rule with a non-empty IPProto
restriction, including TCP-only grants compiled from `ip: ["tcp:80"]`.
The bug surfaced in the captured allpass-acls-and-grants-mixed
scenario: the grant `tag:client → webserver:80` was reachable in the
compiled filter but the empty-proto test could not see it.

Expand the empty Protocol to the default set at the call site so
ruleMatchesProto's intersection check sees the right requested
protocols. Drop the now-dead empty-requestedProtos branch from the
matcher. The last divergence drops out of knownPolicyTesterDivergences
as a result.

Updates #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby e4e209f919 policy/v2: canonicalize Protocol form during unmarshal
Tailscale accepts both named ("tcp") and numeric IANA ("6") protocol
forms wherever a Protocol value is allowed. Headscale stored whichever
form the user wrote, leaving downstream code with two equivalents to
handle separately. validateProtocolPortCompatibility only recognised
the named constants and rejected the numeric form, so a policy with
`proto: "6", dst: ["host:443"]` was rejected at parse time even though
SaaS accepts it.

Resolve the disagreement by normalising to the named form during
Protocol.UnmarshalJSON. Every downstream consumer now sees one form
regardless of what the user wrote, so layered guards like
`|| protocol == "6"` in the validator are unnecessary.

Updates #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby f172dba0e3 policy/v2: validate tests block at parse boundary
A `tests` entry describes one connection attempt to one specific
host on one specific port over a connection-oriented protocol, and
asserts whether it is allowed or denied. Five shape rules follow —
single-port dst, proto in {tcp, udp, sctp, ""}, no
autogroup:internet dst, no CIDR-typed dst (raw `/N` or hosts:-alias
to a multi-host prefix), at least one of accept/deny — and every
one was previously silently accepted by headscale even though
Tailscale SaaS rejects them as "test(s) failed".

Enforce them in one pass over `pol.Tests` from `Policy.validate()`,
reusing the existing parse-time multierr aggregation. The same
shapes remain valid inside ACL or Grant destinations where the rule
does not apply; the validator only walks the tests array.

The compat runner now treats parse-time errors equivalently to
SetPolicy errors so the captured Tailscale body still matches via
substring regardless of which step surfaces the rejection. Nine
divergences resolved by this validation pass drop out of
knownPolicyTesterDivergences.

Updates #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby c0774a739b policy/v2: add policytester captures recorded from Tailscale SaaS
57 captures covering the alias × outcome matrix for the tests block,
recorded against a real Tailscale SaaS tailnet. Replayed by
TestPolicyTesterCompat.

Bump the check-added-large-files pre-commit threshold to 1024 KB —
captures include verbose per-node netmaps and one is 620 KB.

Updates #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby 7bc701179b policy/v2: add policytester compat test runner
Pin headscale's accept/reject decision and error body against
Tailscale SaaS by replaying captures recorded from a real tailnet.
Mirrors the tailscale_grants_compat_test.go pattern: glob over
testdata/policytest_results/, one t.Run per file, parse-or-SetPolicy
error must contain the captured api_response_body.message.

errPolicyTestsFailed is "test(s) failed" — Tailscale's literal body —
so substring match works against captured response bodies. Per-test
detail (src, dst, expected vs got) is preserved below the prefix for
the CLI / config-reload paths that don't have an audit endpoint.

knownPolicyTesterDivergences gates the 12 mismatches the captures
will surface so the suite stays green; engine fixes in follow-up
commits drop the entries as each is resolved.

Updates #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby b29ae25356 policy/v2: evaluate the tests block on user-initiated writes
v2 silently dropped policy.tests, so a policy that contradicted its
own assertions still applied. Resolve src/dst via the existing Alias
machinery, walk the compiled global filter rules (acls and grants
both contribute), and run on every user-write boundary: SetPolicy,
the file watcher, and `headscale policy check`. A failing test
rejects the write before it mutates live state.

Boot-time reload skips evaluation; an already-stored policy that
references a deleted user shouldn't lock the server out.

`headscale policy check` is a thin frontend for the new CheckPolicy
gRPC method. The server-side handler builds a fresh PolicyManager
from the request bytes and the state's live users/nodes, runs
SetPolicy on the sandbox so the tests block executes, and returns
the result through gRPC status. No persistence, no policy_mode
coupling. --bypass-grpc-and-access-database-directly opens the DB
directly when the server is not running.

cmd/headscale/cli/root.go no longer special-cases `policy check` in
init() (the early return from PR #2580 broke --config registration
and viper priming for --bypass).

integration/cli_policy_test.go covers policy_mode={file,database} x
fixture={acl-only, acl+passing-tests, acl+failing-tests} x
bypass={false,true} = 12 rows.

Updates #1803

Co-authored-by: Janis Jansons <janhouse@gmail.com>
2026-05-12 11:54:54 +01:00
Kristoffer Dalby 56146de377 proto: add CheckPolicy RPC
CheckPolicy validates a candidate policy against a running server's
live users and nodes (running its tests block) without persisting
anything. Used by 'headscale policy check' to replace the in-process
validation path the CLI runs today, which would otherwise need its
own database connection.

Updates #1803
2026-05-12 11:54:54 +01:00
Kristoffer Dalby c3df84e354 policy/matcher: include CapGrant.Dsts in match destinations
MatchFromFilterRule only read DstPorts[].IP into the destination
IPSet. Cap-grant-only filter rules (e.g. tailscale.com/cap/relay)
carry their destinations in CapGrant[].Dsts, so the derived matchers
had empty dest sets and BuildPeerMap / ReduceNodes never exposed the
cap target to its source nodes. Without a companion IP-level grant
the relay node stayed invisible, so clients never tried to use it
and connections sat on DERP.

Union CapGrant[].Dsts into the destination IPSet alongside DstPorts.
Restores peer-visibility for any cap-grant-only relationship; the
peer-relay flow is the most visible instance.

Fixes #3256
2026-05-11 14:55:06 +01:00
Kristoffer Dalby 795a1efe9b ci: fetch full history in golangci-lint job
revgrep needs pull_request.base.sha in the local clone to compute
the diff against new code. With fetch-depth: 2, only HEAD and one
parent are fetched, so a stale base SHA (when main moves between
PR syncs) is not reachable and revgrep falls through, surfacing
pre-existing issues outside the PR scope.
2026-05-11 10:34:58 +01:00
Kristoffer Dalby dc733767c4 Dockerfile.tailscale-HEAD,Dockerfile.derper: bump golang to 1.26.3
tailscale upstream go.mod now requires 1.26.3.
2026-05-11 10:34:58 +01:00
Lealem Amedie 542091e82b Add unit test 2026-05-11 09:25:26 +01:00
Lealem Amedie 6cd919d411 mapper: include UserProfiles in policy-change MapResponses 2026-05-11 09:25:26 +01:00
Kristoffer Dalby 2f907edf87 hscontrol/types: regenerate types_clone.go for viewer bump
cmd/viewer in tailscale.com/cmd v1.97.0-pre emits new(*x) instead
of ptr.To(*x). No behaviour change.
2026-05-11 08:46:12 +01:00
Kristoffer Dalby 9621a97ebe ci, pre-commit: validate vendor hash via vendorhash check
Replace the grep/awk hash extraction in build.yml with a structured
vendorhash check step; the PR review comment now reads expected/
actual values directly from $GITHUB_OUTPUT instead of scraping Nix
stderr. Add a prek hook so divergence is caught locally before push.
2026-05-11 08:46:12 +01:00
Kristoffer Dalby e470774f6a cmd/vendorhash: track vendor SRI in flakehashes.json
Move the headscale vendorHash out of flake.nix into a content-
addressed flakehashes.json maintained by a small Go tool. The
schema and goModFingerprint algorithm mirror upstream tailscale's
tool/updateflakes so a future shared library extraction is trivial.

vendorhash check verifies flakehashes.json against the current
go.mod/go.sum. Hot path is a sha256 over those two files, so
re-runs without input change are essentially free; only an actual
fingerprint drift triggers go mod vendor + nardump.SRI.

vendorhash update recomputes both fields and rewrites the JSON.
The nix-vendor-sri devShell shim now wraps it.
2026-05-11 08:46:12 +01:00
Kristoffer Dalby 980622e9a5 flake.nix, go.mod: bump tailscale.com to v1.97.0-pre
Pulls in the cmd/nardump library split (tailscale/tailscale#19551)
so flakehashes.json tooling can import nardump.SRI directly.

Side effects: Go directive bumps to 1.26.2 and the nixpkgs lock
advances to a revision shipping go 1.26.2.
2026-05-11 08:46:12 +01:00
Kristoffer Dalby 4e0c2b8556 cmd/headscale/cli: validate users in policy check
Add --bypass-grpc-and-access-database-directly to policy check so
the new ambiguous-user validator runs against the live user list.
Without the flag, policy check stays a syntax-only check and the
success message says so.

Updates #3160
2026-05-09 11:28:12 +01:00
Kristoffer Dalby bc9fb6d403 hscontrol/policy/v2: reject ambiguous user references at load time
When a user@ token resolved to more than one DB row, ACL and SSH
rules referencing it were silently dropped at compile time, leaving
clients with SSHPolicy={rules: null} and no signal to the admin.

Validate every Username reference in groups, tagOwners,
autoApprovers, ACLs and SSH rules at NewPolicyManager and SetPolicy
and return ErrMultipleUsersFound. Missing-user tokens stay tolerant
per #2863.

Updates #3160
2026-05-09 11:28:12 +01:00
Möhsün Babayev 585d0c01bc docs(config): fix typo in config-example.yaml
Fixes a typo in the description of `metrics_listen_addr` property.
2026-05-09 05:14:08 +02:00
Möhsün Babayev 01eb5402f9 docs(setup): fix typo in requirements.md
Fix the typo in spelling of "Let's Encrypt".
2026-05-09 05:14:08 +02:00
MunMunMiao e597f4c8a0 Add Headscale UI to web UI documentation 2026-05-09 05:02:44 +02:00
SAY-5 01e548e030 state: avoid nil deref in registration handlers when old user is missing
Mirror the guard from HandleNodeFromPreAuthKey in HandleNodeFromAuthPath.
Both functions log the old user's name in the "different user" branch
when an existing NodeStore entry under the same machine key belongs to
another user. UserView.Name dereferences the backing User pointer
unconditionally, so when the cached node was loaded with a non-nil
UserID but a nil User (Preload join missed the row, or upstream code
left the snapshot in that shape), the log call panics with a nil-pointer
dereference at hscontrol/types/types_view.go:97.

The panic is caught by the http2 server's runHandler for the noise
control plane, so the process keeps running but every retry produces a
new panic — production has observed bursts of ~1.9k panics per hour
during a tailscaled reconnect loop. The gRPC/OIDC entry has no equivalent
recover and would surface the panic to the caller.

Guard both call sites with oldUser.Valid() and fall back to an empty
old-user name when the pointer is nil. The "Creating new node for
different user" log line still includes the existing node ID, hostname,
machine key, and new user, so operator visibility is preserved.

Add reproduction tests for both handlers seeding the orphan shape
directly into NodeStore via PutNodeInStoreForTest.

Co-Authored-By: Kristoffer Dalby <kristoffer@dalby.cc>
2026-05-06 07:23:02 +01:00
Kristoffer Dalby 9482cdf590 testdata: drop unused uppercase SSH-*.hujson fixtures
The 39 SSH-*.hujson files in hscontrol/policy/v2/testdata/ssh_results/
were legacy hand-written "expected SSH rules" snippets superseded by
the lowercase tscap captures (ssh-*.hujson). The active loader in
TestSSHDataCompat globs ssh-*.hujson; filepath.Glob is case-sensitive
on Linux so the uppercase set was loaded by no test.

The duplication caused permanent dirty git state on case-insensitive
filesystems (APFS, NTFS) where only one of SSH-A1.hujson and
ssh-a1.hujson can physically exist in the working tree.

Add an assertion to TestSSHDataCompat that the loader picks up every
*.hujson under ssh_results/ so future fixture migrations cannot leave
stranded files behind.

Fixes #3240
2026-05-05 11:59:01 +01:00
primewildy 3d0f597b23 oidc: handle groups claim as string or array (FlexibleStringSlice)
Some OIDC providers (notably JumpCloud) return the `groups` claim as
a plain string when the user belongs to a single group, rather than
a single-element array:

  Single group:    {"groups": "MyGroup"}
  Multiple groups: {"groups": ["Group1", "Group2"]}

This causes `json.Unmarshal` to fail with:

  cannot unmarshal string into Go struct field OIDCClaims.groups of type []string

This is the same class of issue as juanfont#2293 (FlexibleBoolean for
email_verified). The fix follows the same pattern: introduce a
FlexibleStringSlice type with a custom UnmarshalJSON that accepts
both a string and a []string, and use it for the Groups field in
both OIDCClaims and OIDCUserInfo.
2026-05-04 15:26:53 +02:00
Kristoffer Dalby 76ee29352b servertest: cover via-grant exit-node visibility end-to-end
TestGrantViaExitNodeInternetVisibility boots a server, applies a
policy that scopes autogroup:internet to a tag, registers a tagged
exit advertiser and a regular client, and asserts the client's netmap
surfaces the exit node with 0.0.0.0/0 and ::/0 in AllowedIPs — the
substrate the Tailscale client reads to populate
`tailscale exit-node list`.

TestGrantViaExitNodeNoFilterRules retains its assertion (literal /0
absent from the exit node's PacketFilter, matching SaaS PacketFilter
encoding); only its docstring is updated to reflect that the exit
node now does receive a TheInternet-shaped rule, just not the
literal /0 form.

Updates #3233
2026-04-30 19:22:45 +01:00
Kristoffer Dalby 2b7f15abaa policy/v2: surface autogroup:internet via grants on exit nodes
A grant of the form `{src: alice, dst: autogroup:internet, via:
tag:exit1}` was loading without error but stripping every exit node
from alice's view: `tailscale exit-node list` returned "no exit nodes
found".

Two sites skipped autogroup:internet at the compile / steering layer:
compileViaForNode's *AutoGroup arm produced no FilterRule for the
via-tagged exit node, and ViaRoutesForPeer's *AutoGroup arm produced
no Include/Exclude. With pm.needsPerNodeFilter true, the exit node's
matchers were empty, BuildPeerMap could not link source to exit, and
RoutesForPeer's ReduceRoutes stripped 0.0.0.0/0 and ::/0 from
AllowedIPs.

The skip belongs at the wire-format layer (ReduceFilterRules), not at
the compile layer that also feeds internal matchers. Lift
autogroup:internet handling into both *AutoGroup arms with the same
shape used for *Prefix destinations: emit a TheInternet rule on
via-tagged exit advertisers; surface peer.ExitRoutes() in Include
when the peer carries the via tag, Exclude otherwise.
ReduceFilterRules continues to keep the rule on exit-route
advertisers' wire output and strip it elsewhere, preserving SaaS
PacketFilter encoding.

Also drop compileViaForNode's early len(SubnetRoutes)==0 return:
SubnetRoutes excludes exit routes, so the early return pre-empted the
autogroup:internet branch on nodes that only advertise exit routes.

Existing tests pinning the buggy behaviour (TestViaRoutesForPeer
subtests, TestCompileViaGrant case) flipped to the new contract.

Fixes #3233
2026-04-30 19:22:45 +01:00
Kristoffer Dalby ecaf56e0a0 integration: drop Force flag on docker network disconnect
Force-disconnect leaves stale routes in the container's network
namespace: libnetwork removes the host-side veth but the
namespace-internal route survives. The next ConnectNetwork on the
same network then fails with "cannot program address X/16 in sandbox
interface because it conflicts with existing route", and the route
never resolves on its own. Bounded retry around ConnectNetwork
exhausts MaxElapsedTime instead of recovering.

Without Force, libnetwork drains the namespace routes synchronously
during disconnect and ConnectNetwork sees a clean slate. Cable-pull
semantic is preserved: docker still tears down the endpoint at the
namespace level, leaving in-flight TCP half-open inside the
container's view, verified via paired probe-timeout pairs in HA
prober logs while both routers are physically disconnected.

Fixes #3234
2026-04-30 12:52:05 +01:00
Kristoffer Dalby 94ec607bca state: per-goroutine deadline in HA probe cycle
`time.After(ProbeTimeout)` returned a single channel shared by every
probe goroutine in the cycle. Only the first goroutine to receive the
deadline tick drains the channel; any other goroutine still waiting on
its `responseCh` is then stuck forever, `wg.Wait()` never returns, and
the scheduler loop in `app.go` stalls on the next tick. The condition
fires whenever two or more nodes time out in the same cycle — common
under cable-pull where IsOnline lags reality and both routers stay in
the candidate set as half-open TCP.

Move the timer inside each goroutine so every probe has its own
deadline.

Updates #3234
2026-04-30 12:52:05 +01:00
Kristoffer Dalby d1443a431c integration: skip subpackage tests in workflow generator
The generator scans `integration/` recursively for `Test*` functions
and emits one CI job per match. Helper subpackages like
`dockertestutil` and `tsic` host plain unit tests that should run
under `go test`, not as Docker-based integration matrix entries.
Limit the scan to depth 1 so only top-level `integration/*_test.go`
files contribute job names.
2026-04-30 12:52:05 +01:00
Kristoffer Dalby 155e42f892 integration: retry transient docker network ops
Libnetwork endpoint cleanup is eventually consistent. A back-to-back
disconnect+connect on the same network can race teardown and return a
transient error. Wrap the daemon calls in bounded exponential backoff
so TestHASubnetRouterFailoverDockerDisconnect no longer flakes on
phase 4c reconnect.

Fixes #3234
2026-04-30 12:52:05 +01:00
Kristoffer Dalby 3d5c0af4e7 state: preserve previous primary when all HA advertisers unhealthy
electPrimaryRoutes' all-unhealthy fallback picked candidates[0]
(lowest NodeID) regardless of who was prev. Under cable-pull
semantics IsOnline lags reality (long-poll TCP half-open), so
both routers stay in candidates and both go Unhealthy via the
prober — the fallback then churned primary to a node that was
itself unreachable.

Prefer prev when still in candidates; fall through to
candidates[0] only when prev is gone. Anti-blackhole holds.

Update the property test reference model and split the unit
test into existence (KeepsAPrimary) and identity
(PreservesPrevious) cases.

Fixes #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 27c9113af8 integration: regenerate workflow for HA docker disconnect test
Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 7bb86f2c16 integration: HA cable-pull lifecycle test
Add DisconnectFromNetwork/ReconnectToNetwork on TailscaleClient
backed by pool.Client.DisconnectNetwork.

Exercise single-router fail+recover either side, sequential dual
failure, and simultaneous dual failure. The dual-failure legs
assert no flap to a known-bad primary; the single-router-return
legs check traffic only because docker network disconnect
transiently fails probes on sibling routers.

Fails on parent; passes after the fix.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 863fa2f815 servertest, integration: cover HA both-offline recovery
Three regression tests for the user scenario: an in-process
Disconnect/Reconnect, a tailscale-down/up integration test, and
an iptables -j DROP cable-pull integration test.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 9f7c8e9a07 state: clear Unhealthy when node leaves HA candidate set
Restore the legacy auto-clear at write boundaries that drop HA
candidacy: Disconnect, SetApprovedRoutes(empty), and
UpdateNodeFromMapRequest shrinking advertised routes to empty.
Plus a defensive guard in SetNodeUnhealthy.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 66ac785c22 state: delete routes package, port primary route tests
Remove hscontrol/routes/. Port the named scenarios and the rapid
property test to hscontrol/state/.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 437754aeea state: switch consumers to NodeStore primary routes
Replace routes.PrimaryRoutes reads with NodeStore. Connect bumps
SessionEpoch; Disconnect re-checks it inside UpdateNode so the
check and mutation are atomic against a concurrent Connect on
the same node.

The connect_race regression test is carried in its final
SessionEpoch form.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby da927eb018 state: compute primary routes inside NodeStore snapshot
Add primaries and isPrimary maps to Snapshot plus an election
algorithm. No callers yet.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 942313a10a types: move DebugRoutes from routes to types
Unblocks deletion of the routes package.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 1fe682b141 types: add Unhealthy and SessionEpoch fields to Node
Runtime-only (gorm:"-") fields read by the HA primary route refactor.

Updates #3203
2026-04-29 18:08:39 +01:00
Kristoffer Dalby 010a5564c5 all: rephrase prose to fit codebase voice
Reword comments, one doc paragraph, and one test failure message
so the prose reads naturally. No behaviour change.
2026-04-29 16:22:19 +01:00
Akhilesh Arora de60982d83 state: note tagged-path coverage and self-healing behaviour for #3199
- test: comment that the !regReq.Expiry.IsZero() gate also covers
  the tags-only PreAuthKey path
- CHANGELOG: note pre-existing 0001-01-01 rows self-heal on
  re-registration rather than being backfilled
2026-04-29 13:06:38 +01:00
Akhilesh Arora bcfaf6ad68 CHANGELOG: note nil expiry preservation fix 2026-04-29 13:06:38 +01:00
Akhilesh Arora 0e10ca4e9a state: preserve nil expiry on user owned registration when no default is configured
When a user owned node registers or re registers with a PreAuthKey and the
client sends zero client expiry while node.expiry is set to 0, the expiry
column ends up stored as 0001-01-01 00:00:00 instead of NULL. Two sites in
HandleNodeFromPreAuthKey build a non nil pointer to regReq.Expiry even when
the value is zero time, and the needsDefaultExpiry guard only replaces it
when s.cfg.Node.Expiry > 0, so the pointer to zero time survives to the
database.

Convert an unset regReq.Expiry to nil before handing it off so the
needsDefaultExpiry path is the only place that assigns a non nil pointer.

This is a narrower sibling of #3170 on the user owned PreAuthKey path. The
regression was introduced alongside the fix for #3111 in 6337a3db.
2026-04-29 13:06:38 +01:00
Kristoffer Dalby ba251e7b47 integration: cover exit nodes via autogroup:internet ACL (#3212)
TestEnablingExitRoutes runs without an ACL, so tailcfg.FilterAllowAll
hides any policy-path regression. Add a sibling that applies the literal
#3212 policy via hsic.WithACLPolicy after registration and approval,
then asserts each peer carries 0.0.0.0/0 + ::/0 in AllowedIPs and
ExitNodeOption is true — the daemon-derived bool that drives
`tailscale exit-node list`.

Updates #3212
2026-04-29 11:24:33 +01:00
Kristoffer Dalby c7a0ca709f policy: surface exit nodes via autogroup:internet (#3212)
compileFilterRules skipped autogroup:internet destinations to keep them
out of the wire-format PacketFilter, but those same compiled rules are
the source of pm.matchers — and Node.CanAccess relies on a matcher whose
DestsIsTheInternet covers the public internet to surface exit-node peers
to ACL sources. With the skip in place no such matcher existed, exit
nodes silently dropped out of the source's peer list, and the docs'
exit-node walkthrough stopped working: `tailscale exit-node list`
returned "no exit nodes found" and `tailscale set --exit-node=<ip>`
returned "no node found in netmap with IP".

Drop the compile-time skip so autogroup:internet flows through normal
matcher derivation, and teach ReduceFilterRules to keep the resulting
client packet-filter rule on exit-route advertisers — Tailscale SaaS
sends those rules to exit nodes so the kernel filter accepts traffic
forwarded by autogroup:internet sources.

Verified against a live tailnet on 2026-04-28 via tscap; the b17/b18
captures land under testdata/issue_3212/ as a regression guard. The
captures are isolated from testdata/routes_results/ because the broader
TestRoutesCompat machinery assumes a CIDR-prefix wire format that
differs from the IPSet-range form SaaS emits for autogroup:internet —
aligning that wire format is tracked separately.

Fixes #3212
2026-04-29 11:24:33 +01:00
Kristoffer Dalby a7d405a255 ci: regenerate test-integration.yaml for TestTailscaleRustAxum
Pick up the new tailscale-rs integration test in the CI job matrix.
2026-04-29 10:13:43 +01:00
Kristoffer Dalby 775bc3a715 integration: add TestTailscaleRustAxum for tailscale-rs
Add an integration test that runs the tailscale-rs axum example
against headscale end-to-end. The test provisions one headscale
instance, one Go tailscale probe client (tsic), and one
tailscale-rs node (tsric) on the same tailnet, then verifies:

  - the tailscale-rs node registers and is assigned both an IPv4
    and an IPv6 address
  - the Go probe sees the tailscale-rs node as a peer in its status
  - GET /index.html and GET /assets/index.css from the axum server
    return the expected content over the tailnet
  - three sequential POST /count calls return distinct, incrementing
    counter values, proving netstack state is maintained across
    multiple TCP connections

This is the first integration test that exercises a non-Go
Tailscale client against headscale, giving end-to-end coverage of
the control protocol for alternate implementations.
2026-04-29 10:13:43 +01:00
Kristoffer Dalby ea968e2f5d integration/tsric: add TailscaleRustInContainer package
Add TailscaleRustInContainer (tsric), a Rust-client counterpart to
integration/tsic. It runs the axum example from tailscale-rs in a
Docker container and exposes the same lifecycle hooks as tsic
(Shutdown, SaveLog, Execute, WriteFile) so integration tests can
treat it as any other Tailscale node.

Dockerfile.tailscale-rs clones tailscale-rs at build time, so no
local source checkout is required. The repo URL and ref are Docker
build arguments (TAILSCALE_RS_REPO, TAILSCALE_RS_REF) exposed as
tsric.WithRepo / tsric.WithRef options. The HEADSCALE_INTEGRATION_
TAILSCALE_RS_IMAGE environment variable provides an escape hatch
for using a pre-built image instead of building from source.

The Dockerfile patches the cloned root Cargo.toml to expose
ts_control's insecure-keyfetch feature through the tailscale crate
so the axum example can fetch the control key over plain HTTP.
The integration harness serves the control plane without TLS,
which is the only mode tailscale-rs can register against until it
grows a way to inject a custom CA bundle.
2026-04-29 10:13:43 +01:00
Kristoffer Dalby 2e1a716a9a policy/v2: fix empty grants/acls returning FilterAllowAll
compileFilterRules, compileGrants, and updateLocked guarded the
"no rules so allow all" fallback with len(pol.Grants) == 0, which
matches both an absent grants field and an explicit empty array.
JSON {"grants": []} unmarshals to a non-nil empty slice; it should
compile to zero filter rules (deny all) to match Tailscale SaaS,
but the length check sent it down the FilterAllowAll path.

Distinguish absent (nil) from explicit-empty by switching the guard
to pol.Grants == nil, the same asymmetry already used for ACLs.
{} keeps allowing all; {"acls": []} and {"grants": []} now both
deny all.

Fixes #3211
2026-04-29 08:55:07 +01:00
Kristoffer Dalby 174e409da6 github: drop nu flatten in needs-more-info timer
The scheduled job has failed every night since 2026-04-13. Two
prior fixes (race-condition guards and splitting the combined
where predicate in #3200) did not address the actual cause:
`flatten` collapses nested records in a list-of-records pipeline,
so after `gh api ... | from json | flatten` the `label` and `user`
columns no longer exist - their fields are lifted to the top
level (with prefix only on naming collisions). `where label.name
== ...` and `where user.type != "Bot"` then both reference a
column that is not there.

`gh api --paginate` already returns a single concatenated JSON
array, so `from json` produces a list of records directly and no
flattening is needed. Drop both `| flatten` calls.

Verified locally with nu 0.108 against the events stream of issue
#3178: without flatten, `where event == "labeled" | where
label.name == "needs-more-info" | last` returns the labeled event
with its `label` record intact.
2026-04-28 16:36:32 +01:00
QEDeD 3672a2df3a Fix typo in API key creation help text
Correct loose (opposite of tight) to lose (opposite of keep).
2026-04-28 08:50:26 +02:00
Kristoffer Dalby ce5d1ba8f8 github: split nu where in needs-more-info timer
GitHub's /issues/:n/events endpoint returns a mixed-schema table.
Only labeled/unlabeled rows carry a `label` column. Nu's `where`
does not short-circuit on missing columns, so the combined
predicate `event == "labeled" and label.name == ...` dereferenced
`label.name` on every row and crashed on the first non-labeled
event with `nu::shell::column_not_found`.

The scheduled job has failed every night since 2026-04-13 (the
first run with a labeled issue), so no `needs-more-info` issue
has been auto-closed.

Split into two sequential `where` filters so `label.name` is only
accessed on rows that have the column.
2026-04-18 15:39:29 +01:00
Kristoffer Dalby 4e1d83ecef CHANGELOG: document hostname cleanroom rewrite
Summarise the ingest rewrite, the SaaS-matching collision rule, and the
BREAKING change from random-suffix to numeric-suffix collision labels
and from "invalid-<rand>" to the literal "node" fallback.

Updates #3188
2026-04-18 15:12:21 +01:00
Kristoffer Dalby d6dfdc100c hscontrol: route hostname handling through dnsname and NodeStore
Ingest (registration and MapRequest updates) now calls
dnsname.SanitizeHostname directly and lets NodeStore auto-bump on
collision. Admin rename uses dnsname.ValidLabel + SetGivenName so
conflicts are surfaced to the caller instead of silently mutated.

Three duplicate invalidDNSRegex definitions, the old NormaliseHostname
and ValidateHostname helpers, EnsureHostname, InvalidString,
ApplyHostnameFromHostInfo, GivenNameHasBeenChanged, generateGivenName
and EnsureUniqueGivenName are removed along with their tests.
ValidateHostname's username half is retained as ValidateUsername for
users.go.

The SaaS-matching collision rule replaces the random "invalid-xxxxxx"
fallback and the 8-character hash suffix; the empty-input fallback is
the literal "node". TestUpdateHostnameFromClient now exercises the
rewrite end-to-end with awkward macOS/Windows names.

Fixes #3188
Fixes #2926
Fixes #2343
Fixes #2762
Fixes #2449
Updates #2177
Updates #2121
Updates #363
2026-04-18 15:12:21 +01:00
Kristoffer Dalby a2c3ac095e state: auto-bump GivenName on collision and add SetGivenName
NodeStore's writer goroutine now resolves GivenName collisions inside
applyBatch: on PutNode/UpdateNode the landing label gets -N appended
until unique, matching Tailscale SaaS. Empty labels fall back to the
literal "node".

SetGivenName exposes the admin-rename path: validates via
dnsname.ValidLabel and rejects on collision with ErrGivenNameTaken,
so renames do not silently rewrite behind the caller.

Updates #3188
Updates #2926
Updates #2343
Updates #2762
2026-04-18 15:12:21 +01:00
Florian Preinstorfer f1494a32ce Update links to Tailscale documentation 2026-04-18 09:33:41 +02:00
Florian Preinstorfer 7e6c7924ad Document availability of autgroup:internet 2026-04-18 09:33:41 +02:00
Florian Preinstorfer 9ea09ea4b6 Remove changelog section for 0.28.1 2026-04-18 09:33:41 +02:00
Kristoffer Dalby 436d3db28e servertest: add dynamic HA failover tests
Existing HA tests verify server-side primary election; these add
end-to-end assertions from a viewer client's perspective, that marking
the primary unhealthy or revoking its approved route propagates through
the netmap so the viewer sees the flip.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 978f1e3947 state: tie-break ResolveNode by GivenName then lowest NodeID
Resolve by GivenName (unique per tailnet) before Hostname (client-
reported, may collide); within each pass, pick the lowest NodeID so
results are deterministic across NodeStore snapshot iterations.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 2530d86f1b change: document PingRequest merge first-wins foot-gun
change.Merge keeps the first PingRequest seen when merging, which
means a later probe's callback URL is silently dropped if a stale merge
is in-flight. Document the contract at Merge and at doPing so callers
know to serialise probes.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 1a58b77271 cmd/dev: validate --port fits the derived-port range
dev derives additional ports from --port + offsets (metrics, gRPC,
debug). A --port near uint16 max would overflow silently; add up-front
validation that rejects values that would push derived ports over 65535.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 427b2f15ee matcher: clarify DestsIsTheInternet single-family semantics
DestsIsTheInternet now reports the internet when either family's /0
is contained (0.0.0.0/0 or ::/0), matching what operators expect when
they write the /0 directly. Also documents MatchFromStrings fail-open.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 93e8c7285f debug: explain URLIsNoise choice in ping callback
The /debug/ping callback hits /machine/ping-response on the main
TLS router, not the noise chain, so URLIsNoise stays false. Document
this at the emit site to prevent accidental changes.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 842f36225e state: drain pending pings on Close
Blocked callers waiting on a pingTracker response channel would
hang forever if the server Close()d mid-probe. Drain the pending map on
Close so those goroutines unblock and exit cleanly.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 0567cb6da3 app: add security headers middleware
X-Frame-Options: DENY and frame-ancestors 'none' stop clickjacking
of OIDC, register-confirm, and debug HTML pages. nosniff and no-referrer
are cheap defence-in-depth for the same surfaces.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 5a7cafdf85 noise: reject non-HEAD on PingResponseHandler
chi routes only HEAD to the handler, but assert explicitly so a
future router config change cannot silently accept GET/POST and leak
latency bytes or side-effects.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby f3eb9a7bba templates: escape query value in ping page
elem-go does not escape attribute values, so the raw query reaches
the rendered HTML verbatim. Pre-escape with html.EscapeString to prevent
reflected XSS.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 3a4af8cf87 integration: remove --accept-routes from via steering routers
Subnet routers that advertise routes must not accept peer routes.
With co-router visibility the HA primary's subnet appears in co-routers'
AllowedIPs, and --accept-routes installs a system route that conflicts
with local subnet forwarding.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby ec48f34e1c CHANGELOG: document subnet-to-subnet ACL fixes
Fixes #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 164d659dd2 servertest: add TestViaGrantHACompat for via+HA compat tests
Data-driven tests for via grants combined with HA primary routes:
crossed via tags on same prefix, mixed via+regular across HA pairs,
four-way HA, and the kitchen-sink scenario. Each case uses an inline
topology captured from SaaS.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 7d104b8c8d servertest: add via grant map compat tests
End-to-end exercise of via-grant compilation against SaaS captures:
peer visibility, AllowedIPs, PrimaryRoutes, and per-rule src/dst
reachability from each viewer's perspective.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby a7c9721faa policy/v2: overhaul compat test infrastructure
Reworks the ACL/routes/grant/SSH compat harnesses to read
testcapture.Capture typed files, per-scenario topologies, strict error
wording match, and shared helpers. Surfaces policy-engine drift against
Tailscale SaaS.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby f34dec2754 testcapture: add typed capture format package
Typed Capture/Input/Node/Topology structs for golden SaaS captures.
Schema drift between the tscap capture tool and headscale now becomes a
compile error instead of a silent test pass.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby 1059c678c4 hscontrol/types: silence zerolog by default in tests
Tests were dumping megabytes of zerolog output on failure; silence
at init and let individual tests opt in via SetGlobalLevel when they need
log-driven assertions.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby affaa1a31d policy/v2: align SSH check action with SaaS wire format
SSH check rules now emit CheckPeriod in seconds (matching
Tailscale SaaS) instead of nanoseconds. Adds golden compat tests covering
accept/check modes.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby ded51a4d30 policyutil: fix reduceCapGrantRule and add route reduction
reduceCapGrantRule was dropping rules whose CapGrant IPs overlap a
subnet route; treat subnet routes as part of node identity so those rules
survive reduction. ReduceFilterRules now also reduces route-reachable
destinations.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby b051e7b2bc policy/v2: wire PolicyManager through compiledGrant
Threads PolicyManager into compiledGrant so via grants resolve
viewer identity at compile time instead of re-resolving per MapRequest.
Adds a matchersForNodeMap cache invalidated on policy reload and on node
add/remove.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby b01e67e8e5 types: consider subnet routes as source identity in ACL matching
CanAccess now treats a node's advertised subnet routes as part of
its source identity, so an ACL granting the subnet-owner as source lets
traffic from the subnet through. Matches SaaS semantics.

Updates #3157
2026-04-17 16:31:49 +01:00
Kristoffer Dalby f49c42e716 testdata: add SaaS captures for compat tests
Golden captures of SaaS filter-rules and netmaps across the ACL,
grant, routes, and SSH corpora. These back the data-driven compat tests
that verify headscale's policy output against Tailscale SaaS verbatim.

Updates #3157
2026-04-17 16:31:49 +01:00
Florian Preinstorfer 813eb2d733 Update docs for new HA tracking
Also shorten the description in config-example.yaml a bit.
2026-04-17 16:59:57 +02:00
Kristoffer Dalby 1b6ab52f9e ci: regenerate integration test workflow 2026-04-16 15:10:56 +01:00
Kristoffer Dalby af26bab17a integration: add HA ping failover test
Block ping callbacks via iptables while keeping the Noise session alive
to simulate a zombie-connected router. Verify the prober detects it,
fails over, and does not flap on recovery.

Updates #2129
Updates #2902
2026-04-16 15:10:56 +01:00
Kristoffer Dalby 0378e2d2c6 servertest: add HA health probing tests
Five scenarios: healthy probes, failover on unhealthy primary,
no-flap recovery, connect clears unhealthy, no-op without HA routes.

Updates #2129
Updates #2902
2026-04-16 15:10:56 +01:00
Kristoffer Dalby 8a97dd134b app: wire HA health prober into scheduled tasks
Run the prober on a ticker in scheduledTasks. Enabled by default
(10s interval, 5s timeout). No-op when no HA routes exist.

Fixes #2129
Fixes #2902
2026-04-16 15:10:56 +01:00
Kristoffer Dalby 90e65ccd63 state: add HA health prober
Ping HA subnet routers each probe cycle and mark unresponsive nodes
unhealthy. Reconnecting a node clears its unhealthy state since the
fresh Noise session proves basic connectivity.

Updates #2129
Updates #2902
2026-04-16 15:10:56 +01:00
Kristoffer Dalby 786ce2dce8 routes: add health dimension to HA primary route election
Track unhealthy nodes in PrimaryRoutes so primary election skips them.
When all nodes for a prefix are unhealthy, keep the first as a degraded
primary rather than dropping the route entirely.

Anti-flap is built in: a recovered node becomes standby, not primary,
because updatePrimaryLocked keeps the current primary when still
available and healthy.

Updates #2129
Updates #2902
2026-04-16 15:10:56 +01:00
Kristoffer Dalby 99a93c126b ci: add rolling development tag to container builds 2026-04-15 14:53:53 +01:00
Kristoffer Dalby c9dbea5c18 templates: improve ping page spacing and design system usage
- Remove redundant inline button/input styles that duplicate CSS
- Use CSS variables for input (dark mode support)
- Use A(), Ul(), Ol(), P() wrappers from general.go
- Add expandable explanation of what the ping tests
- Fix section spacing rhythm (spaceXL before results, space2XL
  before connected nodes)
- Add flex-wrap for mobile responsiveness
2026-04-15 10:53:35 +01:00
Kristoffer Dalby 0e5569c3fc templates: add detailsBox collapsible component
Add a reusable <details>/<summary> component to the shared design
system. Styled to match the existing card/box component family
(border, radius, CSS variables for dark mode).

Collapsed by default with a clickable summary line.
2026-04-15 10:53:35 +01:00
Kristoffer Dalby 461a0e2bea cmd/dev: add local development server tool
Add a lightweight dev tool that starts a headscale server on localhost
with a pre-created user and pre-auth key, ready for connecting real
tailscale nodes via mts.

The tool builds the headscale binary, writes a minimal dev config
(SQLite, public DERP, debug logging), starts the server as a
subprocess, and prints a banner with the server URL, auth key, and
mts usage instructions.

Usage: go run ./cmd/dev
       make dev-server
2026-04-15 10:53:35 +01:00
Kristoffer Dalby 0cf27eba77 go.mod: add tstest/mts tool dependency 2026-04-15 10:53:35 +01:00
Kristoffer Dalby 97778c9930 all: add tests for PingRequest implementation
Unit tests for Change (IsEmpty, Merge, Type, PingNode constructor),
ping tracker (register/complete/cancel lifecycle, concurrency, latency),
and end-to-end servertests exercising the full round-trip with real
controlclient.Direct instances.

Updates #2902
Updates #2129
2026-04-15 10:53:35 +01:00
Kristoffer Dalby b113655b71 all: implement PingRequest for node connectivity checking
Implement tailcfg.PingRequest support so the control server can verify
whether a connected node is still reachable. This is the foundation for
faster offline detection (currently ~16min due to Go HTTP/2 TCP retransmit
behavior) and future C2N communication.

The server sends a PingRequest via MapResponse with a unique callback
URL. The Tailscale client responds with a HEAD request to that URL,
proving connectivity. Round-trip latency is measured.

Wire PingRequest through the Change → Batcher → MapResponse pipeline,
add a ping tracker on State for correlating requests with responses,
add ResolveNode for looking up nodes by ID/IP/hostname, and expose a
/debug/ping page (elem-go form UI) and /machine/ping-response endpoint.

Updates #2902
Updates #2129
2026-04-15 10:53:35 +01:00
Florian Preinstorfer 32e1d77663 Install config-example.yaml as example for the debian package
The directory /usr/share/doc/headscale/examples may be used to install
arbitrary example files. This is useful to get a matching configuration
for the release which gets also overwritten automatically.
2026-04-13 20:49:39 +02:00
Kristoffer Dalby de5b1eab68 templates: use table layout for registration confirm details
Replace the bullet list of device details with a two-column table
for cleaner visual hierarchy. Labels are bold and left-aligned,
values right-aligned with subtle row separators. The machine key
value uses an inline code style.

Updates juanfont/headscale#3182
2026-04-13 17:23:47 +01:00
Kristoffer Dalby f066d12153 assets: fix logo alignment and error icon centering
Tighten the SVG viewBox to the actual content bounding box and
remove hardcoded width/height attributes so the browser no longer
adds horizontal padding via preserveAspectRatio. The "h" wordmark
now left-aligns with the page content below it.

Replace the error icon SVG path (which had an off-center X) with
a simple circle + two crossed lines drawn from a centered viewBox.
Both icons now use fill="currentColor" for dark mode adaptation.

Updates juanfont/headscale#3182
2026-04-13 17:23:47 +01:00
Kristoffer Dalby 3918020551 templates: use CSS variables in all shared components
Replace hardcoded Go color constants with var(--hs-*) and
var(--md-*) CSS custom properties in externalLink, orDivider,
card, warningBox, downloadButton, and pageFooter. This ensures
all components follow the dark mode theme automatically.

Also switch pageFooter from div to semantic footer element and
simplify externalLink by letting CSS handle link styling.

Updates juanfont/headscale#3182
2026-04-13 17:23:47 +01:00
Kristoffer Dalby 93860a5c06 all: apply formatter changes 2026-04-13 17:23:47 +01:00
Kristoffer Dalby 814226f327 templates: improve accessibility, dark mode, and typography
Bump base font size from 0.8rem to 1rem (16px) to meet mobile
accessibility guidelines and avoid iOS auto-zoom on inputs.

Add CSS custom properties for all theme colors with a
prefers-color-scheme: dark media query so pages adapt to OS dark
mode. Component inline styles reference var(--hs-*) tokens so they
follow the scheme automatically.

Accessibility improvements:
- role="status" + aria-live="polite" on success boxes
- role="alert" + aria-live="assertive" on error boxes
- role="note" on warning boxes
- Visible focus rings via :focus-visible
- Link underlines (don't rely on color alone)
- SVG icons use currentColor for theme adaptation
- prefers-reduced-motion media query
- <main> landmark element wrapping page content
- Button styling with 44px min-height touch target
- List item spacing

Updates juanfont/headscale#3182
2026-04-13 17:23:47 +01:00
Kristoffer Dalby 78990491da oidc: render HTML error pages for browser-facing failures
Add httpUserError() alongside httpError() for browser-facing error
paths. It renders a styled HTML page using the AuthError template
instead of returning plain text. Technical error details stay in
server logs; the HTML page shows actionable messages derived from
the HTTP status code:

  401/403 → "You are not authorized. Please contact your administrator."
  410     → "Your session has expired. Please try again."
  400-499 → "The request could not be processed. Please try again."
  500+    → "Something went wrong. Please try again later."

Convert all httpError calls in oidc.go (OIDC callback, SSH check,
registration confirm) to httpUserError. Machine-facing endpoints
(noise, verify, key, health, debug) are unchanged.

Fixes juanfont/headscale#3182
2026-04-13 17:23:47 +01:00
Kristoffer Dalby c15caff48c templates: add error box component and error page template
Add errorBox() and errorIcon() to the design system, mirroring the
existing successBox()/checkboxIcon() pattern with red error styling.
Extract error color constants from the inline values in statusMessage().

Add AuthError() template that renders a styled HTML error page using
the same HtmlStructure/mdTypesetBody/logo/footer as all other
browser-facing pages.

Updates juanfont/headscale#3182
2026-04-13 17:23:47 +01:00
Florian Preinstorfer 61c9ae81e4 Remove old migrations for the debian package
Those were required to streamline new installs with updates before 0.27.
Since 0.29 will not allow direct upgrades from <0.27 to 0.29 we might as
well remove it.
2026-04-11 20:35:15 +02:00
Kristoffer Dalby 1f9635c2ec ci: restrict test generator to .go files
The integration test generator scanned all files under integration/
with ripgrep, matching func Test* patterns in README.md code examples
(TestMyScenario, TestRouteAdvertisementBasic). Add --type go to limit
the search to Go source files.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby fd1074160e CHANGELOG: document user-facing changes from #3180 2026-04-10 14:09:57 +01:00
Kristoffer Dalby d66d3a4269 oidc: add confirmation page for node registration
Render an interstitial showing device hostname, OS, and machine-key
fingerprint before finalising OIDC registration. The user must POST
to /register/confirm/{auth_id} with a CSRF double-submit cookie.
Removes the TODO at oidc.go:201.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby d5a4e6e36a debug: route statsviz through tsweb.Protected
Build the statsviz Server directly and wrap its Index/Ws handlers in
tsweb.Protected instead of calling statsviz.Register on the raw mux
which bypasses AllowDebugAccess.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 8c6cb05ab4 noise: pass context to sshActionFollowUp
Select on ctx.Done() alongside auth.WaitForAuth() so the goroutine
exits promptly when the client disconnects instead of parking until
cache eviction.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 42b8c779a0 hscontrol: limit /verify request body size
Wrap req.Body with io.LimitReader bounded to 4 KiB before
io.ReadAll. The DERP verify payload is a few hundred bytes.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby a3c4ad2ca3 types: omit secret fields from JSON marshalling
Add json:"-" to PostgresConfig.Pass, OIDCConfig.ClientSecret, and
CLIConfig.APIKey so they are excluded from json.Marshal output
(e.g. the /debug/config endpoint).
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 0641771128 db: guard UsePreAuthKey with WHERE used=false
Add a row-level check so concurrent registrations with the same
single-use key cannot both succeed. Skip the call on
re-registration where the key is already marked used (#2830).
2026-04-10 14:09:57 +01:00
Kristoffer Dalby f7d8bb8b3f app: remove gRPC reflection from remote server
Reflection is a streaming RPC and bypasses the unary auth
interceptor on the remote (TCP) gRPC server. Remove it there;
the unix-socket server retains it for local debugging.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby adb9467f60 oidc: validate state parameter length in callback
getCookieName sliced value[:6] unconditionally; a short state query
parameter caused a panic recovered by chi middleware. Reject states
shorter than cookieNamePrefixLen with 400.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 41d70fe87b auth: check machine key on tailscaled-restart fast path
The #2862 restart path returned nodeToRegisterResponse after a
NodeKey-only lookup without verifying MachineKey. Add the same
check handleLogout already performs.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 99767cf805 hscontrol: validate machine key and bind src/dst in SSH check handler
SSHActionHandler now verifies that the Noise session's machine key
matches the dst node before proceeding. The (src, dst) pair is
captured at hold-and-delegate time via a new SSHCheckBinding on
AuthRequest so sshActionFollowUp can verify the follow-up URL
matches. The OIDC non-registration callback requires the
authenticated user to own the src node before approving.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 0d4f2293ff state: replace zcache with bounded LRU for auth cache
Replace zcache with golang-lru/v2/expirable for both the state auth
cache and the OIDC state cache. Add tuning.register_cache_max_entries
(default 1024) to cap the number of pending registration entries.

Introduce types.RegistrationData to replace caching a full *Node;
only the fields the registration callback path reads are retained.
Remove the dead HSDatabase.regCache field. Drop zgo.at/zcache/v2
from go.mod.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 3587225a88 mapper: fix phantom updateSentPeers on disconnected nodes
When send() is called on a node with zero active connections
(disconnected but kept for rapid reconnection), it returns nil
(success). handleNodeChange then calls updateSentPeers, recording
peers as delivered when no client received the data.

This corrupts lastSentPeers: future computePeerDiff calculations
produce wrong results because they compare against phantom state.
After reconnection, the node's initial map resets lastSentPeers,
but any changes processed during the disconnect window leave
stale entries that cause asymmetric peer visibility.

Return errNoActiveConnections from send() when there are no
connections. handleNodeChange treats this as a no-op (the change
was generated but not deliverable) and skips updateSentPeers,
keeping lastSentPeers consistent with what clients actually
received.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 9371b4ee28 mapper: fix empty Peers list not clearing client peer state
When a FullUpdate produces zero visible peers (e.g., a restrictive
policy isolates a node), the MapResponse has Peers: [] (empty
non-nil). The Tailscale client only processes Peers as a full
replacement when len(Peers) > 0 (controlclient/map.go:462), so an
empty list is silently ignored and stale peers persist.

This triggers when a FullUpdate() replaces a pending PolicyChange()
in the batcher. The PolicyChange would have used computePeerDiff to
send explicit PeersRemoved, but the FullUpdate goes through
buildFromChange which sets Peers: [] that the client ignores.

When a full update produces zero peers, compute the peer diff
against lastSentPeers and add explicit PeersRemoved entries so the
client correctly clears its stale peer state.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby cef5338cfe types/change: panic on Merge with conflicting TargetNode values
Merging two changes targeted at different nodes is not supported
because the result can only carry one TargetNode. The second
target's content would be silently misrouted.

Add a panic guard that catches this at the Merge call site rather
than allowing silent data loss. In production, Merge is only called
with broadcast changes (TargetNode=0) so the guard acts as
insurance against future misuse.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 3529fe0da1 types: fix OIDC identifier path traversal dropping subject
url.JoinPath resolves path-traversal segments like '..' and '.',
which silently drops the OIDC subject from the identifier. For
example, Iss='https://example.com' with Sub='..' produces
'https://example.com' — the subject is lost entirely. This causes
distinct OIDC users to receive colliding identifiers.

Replace url.JoinPath with simple string concatenation using a slash
separator. This preserves the subject literally regardless of its
content. url.PathEscape does not help because dots are valid URL
path characters and are not escaped.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 4064f13bda types: fix nil panics in Owner() and TailscaleUserID() for orphaned nodes
Owner() on a non-tagged node with nil User returns an invalid
UserView that panics when Name() is called. Add a guard to return
an empty UserView{} when the user is not valid.

TailscaleUserID() calls UserID().Get() without checking Valid()
first, which panics on orphaned nodes (no tags, no UserID). Add a
validity check to return 0 for this invalid state.

Callers should check Owner().Valid() before accessing fields.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 3037e5eee0 db: fix slice aliasing in migration tag merge
The migration at db.go:680 appends validated tags to existing tags
using append(existingTags, validatedTags...) where existingTags
aliases node.Tags. When node.Tags has spare capacity, append writes
into the shared backing array, and the subsequent slices.Sort
corrupts the original.

Clone existingTags before appending to prevent aliasing.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 82bb4331f5 state: fix routesChanged mutating input Hostinfo
routesChanged aliases newHI.RoutableIPs into a local variable then
sorts it in place, which mutates the caller's Hostinfo data. The
Hostinfo is subsequently stored on the node, so the mutation
propagates but the input contract is violated.

Clone the slice before sorting to avoid mutating the input.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 2a2d5c869a types/change: fix slice aliasing in Change.Merge
Merge copies the receiver by value, but the slice headers share the
backing array with the original. When append has spare capacity, it
writes through to the original's memory, and uniqueNodeIDs then
sorts that shared data in place.

Replace append with slices.Concat which always allocates a fresh
backing array, preventing mutation of the receiver's slices.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 157e3a30fc AGENTS.md: trim to behavioural guidance, drop deprecated sub-agent
Procedural content moves to cmd/hi/README.md and integration/README.md.
Stale references (poll.go:420, mapper/tail.go, notifier/,
quality-control-enforcer, validateAndNormalizeTags) are corrected or
removed.
2026-04-10 12:30:07 +01:00
Kristoffer Dalby 70b622fc68 docs: expand cmd/hi and integration READMEs
Move integration-test runbook and authoring guide into the component
READMEs so the content sits next to the code it describes.
2026-04-10 12:30:07 +01:00
Kristoffer Dalby 742878d172 all: regenerate generated files for new tool versions
The nix dev shell refresh in 758fef9b pulled in protoc-gen-go-grpc
v1.6.1 and newer tailscale.com/cmd/{viewer,cloner}, so rerunning
`make generate` updates the version header comments in the three
affected generated files. No semantic changes.
2026-04-09 18:42:25 +01:00
Kristoffer Dalby 2109674467 nix: update flake inputs and dev shell tool versions
Refresh flake.lock (nixpkgs 2026-03-08 -> 2026-04-09) and bump the
tool pins that live directly in flake.nix:

  * golangci-lint 2.9.0 -> 2.11.4
  * protoc-gen-grpc-gateway 2.27.7 -> 2.28.0 (keeps the dev-shell
    code-gen tool in sync with the grpc-gateway Go module)
  * protobuf-language-server pinned commit bumped to ab4c128

Also replace nodePackages.prettier with the top-level prettier
attribute. nodePackages was removed from nixpkgs in the update and
the dev shell would otherwise fail to evaluate with:

    error: nodePackages has been removed because it was unmaintainable
           within nixpkgs

`nix flake check --all-systems` and `nix build .#headscale` both
pass, and `golangci-lint 2.11.4` reports no new issues on the tree.
2026-04-09 18:42:25 +01:00
Kristoffer Dalby 36a73f8c22 all: update Go dependencies
Routine bump of direct Go dependencies. Notable updates:

  * tailscale.com v1.94.1 -> v1.96.5 (gvisor bumped in lockstep to
    match upstream tailscale go.mod)
  * modernc.org/sqlite v1.44.3 -> v1.48.2, modernc.org/libc v1.67.6
    -> v1.70.0 (updated together as required by the fragile libc
    dependency noted in #2188)
  * google.golang.org/grpc v1.78.0 -> v1.80.0
  * grpc-ecosystem/grpc-gateway/v2 v2.27.7 -> v2.28.0
  * tailscale/hujson, tailscale/squibble, tailscale/tailsql
  * golang.org/x/{crypto,net,sync,oauth2,exp,sys,text,time,term,mod,tools}
  * rs/zerolog, samber/lo, sasha-s/go-deadlock, coreos/go-oidc/v3,
    creachadair/command, go-json-experiment/json, pterm/pterm

Update the nix vendorHash to match the new go.sum. Regenerating capver
against tailscale v1.96.5 produces no diff: v1.96.0 was already
captured in 442fcdbd and the capability version has not changed in
the patch series.

All unit tests and `golangci-lint run --new-from-rev=main` are clean.
2026-04-09 18:42:25 +01:00
Kristoffer Dalby e40dbe3b28 Dockerfile: bump tailscale DERPer builder to Go 1.26.2
Tailscale main now requires go >= 1.26.2, so building the HEAD derper
image against golang:1.26.1-alpine fails with:

    go: go.mod requires go >= 1.26.2 (running go 1.26.1; GOTOOLCHAIN=local)

Bump Dockerfile.derper to match the earlier fix for Dockerfile.tailscale-HEAD
in 6390fcee so TestDERPVerifyEndpoint can build the derper container
again. This test is the only consumer of Dockerfile.derper, which is why
the failure was scoped to that single integration job.
2026-04-09 18:42:25 +01:00
Jacky 7c756b8201 db: scope DestroyUser to only delete the target user's pre-auth keys
DestroyUser called ListPreAuthKeys(tx) which returns ALL pre-auth keys
across all users, then deleted every one of them. This caused deleting
any single user to wipe out pre-auth keys for every other user.

Extract a ListPreAuthKeysByUser function (consistent with the existing
ListNodesByUser pattern) and use it in DestroyUser to scope key deletion
to the user being destroyed.

Add unit test (table-driven in TestDestroyUserErrors) and integration
test to prevent regression.

Fixes #3154

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2026-04-09 08:30:21 +01:00
Kristoffer Dalby 6ae182696f state: fix policy change race in UpdateNodeFromMapRequest
When UpdateNodeFromMapRequest and SetNodeTags race on persistNodeToDB,
the first caller to run updatePolicyManagerNodes detects the tag change
and returns a PolicyChange. The second caller finds no change and falls
back to NodeAdded.

If UpdateNodeFromMapRequest wins the race, it checked
policyChange.IsFull() which is always false for PolicyChange (only sets
IncludePolicy and RequiresRuntimePeerComputation). This caused the
PolicyChange to be dropped, so affected clients never received
PeersRemoved and the stale peer remained in their NetMap indefinitely.

Fix: check !policyChange.IsEmpty() instead, which correctly detects
any non-trivial policy change including PolicyChange().

This fixes the root cause of TestACLTagPropagation/multiple-tags-partial-
removal flaking at ~20% on CI.

Updates #3125
2026-04-08 14:32:08 +01:00
Kristoffer Dalby ccddeceeec state: fix GORM not persisting user_id=NULL on tagged node conversion
GORM's struct-based Updates() silently skips nil pointer fields.
When SetNodeTags sets node.UserID = nil to transfer ownership to tags,
the in-memory NodeStore is correct but the database retains the old
user_id value. This causes tagged nodes to remain associated with the
original user in the database, preventing user deletion and risking
ON DELETE CASCADE destroying tagged nodes.

Add Select("*") before Omit() on all three node persistence paths
to force GORM to include all fields in the UPDATE statement, including
nil pointers. This is the same pattern already used in db/ip.go for
IPv4/IPv6 nil handling, and is documented GORM behavior:

  db.Select("*").Omit("excluded").Updates(struct)

The three affected paths are:
- persistNodeToDB: used by SetNodeTags and MapRequest updates
- applyAuthNodeUpdate: used by re-authentication with --advertise-tags
- HandleNodeFromPreAuthKey: used by PAK re-registration

Fixes #3161
2026-04-08 14:32:08 +01:00
Kristoffer Dalby 580dcad683 hscontrol: add tests for SetTags user_id database persistence
Add four tests that verify the tags-as-identity ownership transition
correctly persists to the database when converting a user-owned node
to a tagged node via SetTags:

- TestSetTags_ClearsUserIDInDatabase: verifies user_id is NULL in DB
- TestSetTags_NodeDisappearsFromUserListing: verifies ListNodes by user
- TestSetTags_NodeStoreAndDBConsistency: verifies in-memory and DB agree
- TestSetTags_UserDeletionDoesNotCascadeToTaggedNode: verifies user
  deletion does not cascade-delete tagged nodes

Three of these tests currently fail because GORM's struct-based
Updates() silently skips nil pointer fields, so user_id is never
written as NULL to the database after SetNodeTags clears it in memory.

Updates #3161
2026-04-08 14:32:08 +01:00
Kristoffer Dalby 442fcdbd33 capver: regenerate for tailscale v1.96
go generate ./hscontrol/capver/...

Adds v1.96 (capVer 133) to tailscaleToCapVer and capVerToTailscaleVer,
rolls the 10-version support window forward so MinSupportedCapabilityVersion
is now 109 (v1.78), and refreshes the test fixture accordingly.
2026-04-08 13:00:22 +01:00
Kristoffer Dalby 380f531342 state: trigger PolicyChange on every Connect and Disconnect
Connect and Disconnect previously only appended a PolicyChange when
the affected node was a subnet router (routeChange) or the database
persist returned a full change. For every other node the peers just
received a small PeerChangedPatch{Online: ...} and no filter rules
were recomputed. That was too narrow: a node going offline or coming
online can affect policy compilation in ways beyond subnet routes.

TestGrantCapRelay Phase 4 exposed this. When the cap/relay target node
went down with `tailscale down`, headscale only sent an Online=false
patch, peers never got a recomputed netmap, and their cached
PeerRelay allocation stayed populated until the 120s assertion
timeout. With a PolicyChange queued on Disconnect, peers immediately
receive a full netmap on relay loss and clear PeerRelay as expected;
the symmetric change on Connect lets Phase 5 re-publish the policy
when the relay comes back.

Drop the now-unused routeChange return from the Disconnect gate.

Updates #2180
2026-04-08 13:00:22 +01:00
Kristoffer Dalby 51eed414b4 integration: fix ACL tests for address-family-specific resolve
Address-based aliases (Prefix, Host) now resolve to exactly the literal
prefix and do not expand to include the matching node's other IP
addresses. This means an IPv4-only host definition only produces IPv4
filter rules, and an IPv6-only definition only produces IPv6 rules.

Update TestACLDevice1CanAccessDevice2 and TestACLNamedHostsCanReach to
track which addresses each test case covers via test1Addr/test2Addr/
test3Addr fields and only assert connectivity for that family.
Previously the tests assumed all address families would work regardless
of how the policy aliases were defined, which was true only when
address-based aliases auto-expanded to include all of a node's IPs.

The group test case (identity-based) keeps using IPv4 since tags, users,
groups, autogroups and the wildcard still resolve to both IPv4 and IPv6.

Updates #2180
2026-04-08 13:00:22 +01:00
Kristoffer Dalby e638cbc9b9 integration/tsic: accept via peer-relay in non-direct ping check
When WithPingUntilDirect(false) is set, the Ping helper should accept
any indirect path, but the substring check only matched "via DERP" and
"via relay". Tailscale peer relay pings output

    pong from ... via peer-relay(ip:port:vni:N) in Nms

which does not contain the "via relay" substring and was therefore
rejected as errTailscalePingNotDERP. TestGrantCapRelay Phase 4 never
passed because of this: even when the data plane was healthy the
helper returned an error.

Commit abe1a3e7 attempted to fix this by adding "via relay" alongside
"via DERP" but missed the "peer-" prefix used by peer relay output.

Add an explicit "via peer-relay" substring check so peer relay pongs
are accepted alongside DERP and plain relay pongs.

Updates #2180
2026-04-08 13:00:22 +01:00
Kristoffer Dalby 6390fcee79 Dockerfile: bump tailscale HEAD builder to Go 1.26.2
Tailscale main now requires go >= 1.26.2, so building the HEAD image
against golang:1.26.1-alpine fails with:

    go: go.mod requires go >= 1.26.2 (running go 1.26.1; GOTOOLCHAIN=local)

Bump the base image to golang:1.26.2-alpine so `go run ./cmd/hi run`
can build the HEAD container locally again.
2026-04-08 13:00:22 +01:00
Kristoffer Dalby b52f8cb52f CHANGELOG: document node.expiry and oidc.expiry deprecation
Updates #1711
2026-04-08 13:00:22 +01:00
Kristoffer Dalby ff29af63f6 servertest: use memnet networking and add WithNodeExpiry option
Replace httptest (real TCP sockets) with tailscale.com/net/memnet
so all connections stay in-process. Wire the client's tsdial.Dialer
to the server's memnet.Network via SetSystemDialerForTest,
preserving the full Noise protocol path.

Also update servertest to use the new Node.Ephemeral.InactivityTimeout
config path introduced in the types refactor, and add WithNodeExpiry
server option for testing default node key expiry behaviour.

Updates #1711
2026-04-08 13:00:22 +01:00
Kristoffer Dalby 7e8930c507 hscontrol: add tests for default node key expiry
Add tests covering the core expiry scenarios:
- Untagged auth key with zero expiry gets configured default
- Tagged nodes ignore node.expiry
- node.expiry=0 disables default (backwards compatible)
- Client-requested expiry takes precedence
- Re-registration refreshes the default expiry

Updates #1711
2026-04-08 13:00:22 +01:00
Kristoffer Dalby 6337a3dbc4 state: apply default node key expiry on registration
Use the node.expiry config to apply a default expiry to non-tagged
nodes when the client does not request a specific expiry. This covers
all registration paths: new node creation, re-authentication, and
pre-auth key re-registration.

Tagged nodes remain exempt and never expire.

Fixes #1711
2026-04-08 13:00:22 +01:00
Kristoffer Dalby 4d0b273b90 types: add node.expiry config, deprecate oidc.expiry
Introduce a structured NodeConfig that replaces the flat
EphemeralNodeInactivityTimeout field with a nested Node section.

Add node.expiry config (default: no expiry) as the unified default key
expiry for all non-tagged nodes regardless of registration method.

Remove oidc.expiry entirely — node.expiry now applies to OIDC nodes
the same as all other registration methods. Using oidc.expiry in the
config is a hard error. determineNodeExpiry() returns nil (no expiry)
unless use_expiry_from_token is enabled, letting state.go apply the
node.expiry default uniformly.

The old ephemeral_node_inactivity_timeout key is preserved for
backwards compatibility.

Updates #1711
2026-04-08 13:00:22 +01:00
Florian Preinstorfer 23a5f1b628 Use pymdownx.magiclink with its default configuration
The docs contain bare links that are not rendered without it.
2026-04-02 21:24:27 +02:00
Florian Preinstorfer 44600550c6 Fix invisible selected menu item
A light background with white primary font makes the selected menu entry
unreadable.
2026-04-02 21:24:27 +02:00
Kristoffer Dalby 835db974b5 testdata: strip unused fields from all test data files (23MB -> 4MB)
Strip fields not consumed by any test from all 594 HuJSON test data files:

grant_results/ (248 files, 21MB -> 1.8MB):
  - Remove: timestamp, propagation_wait_seconds, input.policy_file,
    input.grants_section, input.api_endpoint, input.api_method,
    topology.nodes.mts_name, topology.nodes.socket, topology.nodes.user_id,
    captures.commands, captures.packet_filter_matches, captures.whois
  - V14-V16, V26-V36: keep stripped netmap (Peers.Name/AllowedIPs/PrimaryRoutes
    + PacketFilterRules) for via_compat_test.go compatibility
  - V17-V25: strip netmap (old topology, incompatible with via_compat harness)

acl_results/ (215 files, 1.4MB -> 1.2MB):
  - Remove: timestamp, propagation_wait_seconds, input.policy_file,
    input.api_endpoint, input.api_response_code, entire topology section
    (parsed by Go struct but completely ignored — nodes are hardcoded)

routes_results/ (92 files, unchanged — topology is actively used):
  - Remove: timestamp, propagation_wait_seconds, input.policy_file,
    input.api_endpoint, input.api_response_code

ssh_results/ (39 files, unchanged — minimal to begin with):
  - Remove: policy_file
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 30dce30a9d testdata: convert .json to .hujson with header comments
Rename all 594 test data files from .json to .hujson and add
descriptive header comments to each file documenting what policy
rules are under test and what outcome is expected.

Update test loaders in all 5 _test.go files to parse HuJSON via
hujson.Parse/Standardize/Pack before json.Unmarshal.

Add cross-dependency warning to via_compat_test.go documenting
that GRANT-V29/V30/V31/V36 are shared with TestGrantsCompat.

Add .gitignore exemption for testdata HuJSON files.
2026-04-01 14:10:42 +01:00
Kristoffer Dalby f693cc0851 CHANGELOG: document grants support for 0.29.0
Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby abd2b15db5 policy/v2: clean up dead error variables, stale TODO, and test skip reasons
Remove unused error variables (ErrGrantViaNotSupported, ErrGrantEmptySources, ErrGrantEmptyDestinations, ErrGrantViaOnlyTag) and the stale TODO for via implementation. Update compat test skip reasons to reflect that user:*@passkey wildcard is a known unsupported feature, not a pending implementation.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby b762e4c350 integration: remove exit node via grant tests
Remove TestGrantViaExitNodeSteering and TestGrantViaMixedSteering.
Exit node traffic forwarding through via grants cannot be validated
with curl/traceroute in Docker containers because Tailscale exit nodes
strip locally-connected subnets from their forwarding filter.

The correctness of via exit steering is validated by:
- Golden MapResponse comparison (TestViaGrantMapCompat with GRANT-V31
  and GRANT-V36) comparing full netmap output against Tailscale SaaS
- Filter rule compatibility (TestGrantsCompat with GRANT-V14 through
  GRANT-V36) comparing per-node PacketFilter rules against Tailscale SaaS
- TestGrantViaSubnetSteering (kept) validates via subnet steering with
  actual curl/traceroute through Docker, which works for subnet routes

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby c36cedc32f policy/v2: fix via grants in BuildPeerMap, MatchersForNode, and ViaRoutesForPeer
Use per-node compilation path for via grants in BuildPeerMap and MatchersForNode to ensure via-granted nodes appear in peer maps. Fix ViaRoutesForPeer golden test route inference to correctly resolve via grant effects.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 6a55f7d731 policy/v2: add via exit steering golden captures and tests
Add golden test data for via exit route steering and fix via exit grant compilation to match Tailscale SaaS behavior. Includes MapResponse golden tests for via grant route steering verification.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby bca6e6334d integration: add custom subnet support and fix exit node tests
Add NetworkSpec struct with optional Subnet field to ScenarioSpec.Networks.
When Subnet is set, the Docker network is created with that specific CIDR
instead of Docker's auto-assigned RFC1918 range.

Fix all exit node integration tests to use curl + traceroute. Tailscale
exit nodes strip locally-connected subnets from their forwarding filter
(shrinkDefaultRoute + localInterfaceRoutes), so exit nodes cannot
forward to IPs on their Docker network via the default route alone.
This is by design: exit nodes provide internet access, not LAN access.
To also get LAN access, the subnet must be explicitly advertised as a
route — matching real-world Tailscale deployment requirements.

- TestSubnetRouterMultiNetworkExitNode: advertise usernet1 subnet
  alongside exit route, upgraded from ping to curl + traceroute
- TestGrantViaExitNodeSteering: usernet1 subnet in via grants and
  auto-approvers alongside autogroup:internet
- TestGrantViaMixedSteering: externet subnet in auto-approvers and
  route advertisement for exit traffic

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 0431039f2a servertest: add regression tests for via grant filter rules
Add three tests that verify control plane behavior for grant policies:

- TestGrantViaSubnetFilterRules: verifies the router's PacketFilter
  contains destination rules for via-steered subnets. Without per-node
  filter compilation for via grants, these rules were missing and the
  router would drop forwarded traffic.

- TestGrantViaExitNodeFilterRules: same verification for exit nodes
  with via grants steering autogroup:internet traffic.

- TestGrantIPv6OnlyPrefixACL: verifies that address-based aliases
  (Prefix, Host) resolve to exactly the literal prefix and do not
  expand to include the matching node's other IP addresses. An
  IPv6-only host definition produces only IPv6 filter rules.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby ccd284c0a5 policy/v2: use per-node filter compilation for via grants
Via grants compile filter rules that depend on the node's route state
(SubnetRoutes, ExitRoutes). Without per-node compilation, these rules
were only included in the global filter path which explicitly skips via
grants (compileFilterRules skips grants with non-empty Via fields).

Add a needsPerNodeFilter flag that is true when the policy uses either
autogroup:self or via grants. filterForNodeLocked now uses this flag
instead of usesAutogroupSelf alone, ensuring via grant rules are
compiled per-node through compileFilterRulesForNode/compileViaGrant.

The filter cache also needs to account for route-dependent compilation:

- nodesHavePolicyAffectingChanges now treats route changes as
  policy-affecting when needsPerNodeFilter is true, so SetNodes
  triggers updateLocked and clears caches through the normal flow.

- invalidateGlobalPolicyCache now clears compiledFilterRulesMap
  (the unreduced per-node cache) alongside filterRulesMap when
  needsPerNodeFilter is true and routes changed.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 9db5fb6393 integration: fix error message assertion for invalid ACL action
Action.UnmarshalJSON produces the format
'action="unknown-action" is not supported: invalid ACL action',
not the reversed format the test expected.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 3ca4ff8f3f state,servertest: add grant control plane tests and fix via route ReduceRoutes filtering
Add servertest grant policy control plane tests covering basic grants, via grants, and cap grants. Fix ReduceRoutes in State to apply route reduction to non-via routes first, then append via-included routes, preventing via grant routes from being incorrectly filtered.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 5cd5e5de69 policy/v2: add unit tests for ViaRoutesForPeer
Test via route computation for viewer-peer pairs: self-steering returns
empty, viewer not in source returns empty, peer without advertised
destination returns empty, peer with/without via tag populates
Include/Exclude respectively, mixed prefix and autogroup:internet
destinations, and exit route steering.

7 subtests covering all code paths in ViaRoutesForPeer.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 08d26e541c policy/v2: add unit tests for grant filter compilation helpers
Test companionCapGrantRules, sourcesHaveWildcard, sourcesHaveDangerAll,
srcIPsWithRoutes, the FilterAllowAll fix for grant-only policies,
compileViaGrant, compileGrantWithAutogroupSelf grant paths, and
destinationsToNetPortRange autogroup:internet skipping.

51 subtests across 8 test functions covering all grant-specific code
paths in filter.go that previously had no test coverage.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby d243adaedd types,mapper,integration: enable Taildrive and add cap/drive grant lifecycle test
Add NodeAttrsTaildriveShare and NodeAttrsTaildriveAccess to the node capability map, enabling Taildrive file sharing when granted via policy. Add integration test verifying the full cap/drive grant lifecycle.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 9b1a6b6c05 integration: add cap/relay grant peer relay lifecycle test
Add ConnectToNetwork to the TailscaleClient interface for multi-network test scenarios and implement peer relay ping support. Use these to test that cap/relay grants correctly enable peer-to-peer relay connections between tagged nodes.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 8573ff9158 policy/v2: fix grant-only policies returning FilterAllowAll
compileFilterRules checked only pol.ACLs == nil to decide whether
to return FilterAllowAll (permit-any). Policies that use only Grants
(no ACLs) had nil ACLs, so the function short-circuited before
compiling any CapGrant rules. This meant cap/relay, cap/drive, and
any other App-based grant capabilities were silently ignored.

Check both ACLs and Grants are empty before returning FilterAllowAll.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby a739862c65 integration: add via grant route steering tests
Add integration tests validating that via grants correctly steer
routes to designated nodes per client group:

- TestGrantViaSubnetSteering: two routers advertise the same
  subnet, via grants steer each client group to a specific router.
  Verifies per-client route visibility, curl reachability, and
  traceroute path.

- TestGrantViaExitNodeSteering: two exit nodes, via grants steer
  each client group to a designated exit node. Verifies exit
  routes are withdrawn from non-designated nodes and the client
  rejects setting a non-designated exit node.

- TestGrantViaMixedSteering: cross-steering where subnet routes
  and exit routes go to different servers per client group.
  Verifies subnet traffic uses the subnet-designated server while
  exit traffic uses the exit-designated server.

Also add autogroupp helper for constructing AutoGroup aliases in
grant policy configurations.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 8358017dcf policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.

Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.

Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.

Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 28be15f8ea policy/v2: handle autogroup:internet in via grant compilation
compileViaGrant only handled *Prefix destinations, skipping
*AutoGroup entirely. This meant via grants with
dst=[autogroup:internet] produced no filter rules even when the
node was an exit node with approved exit routes.

Switch the destination loop from a type assertion to a type switch
that handles both *Prefix (subnet routes) and *AutoGroup (exit
routes via autogroup:internet). Also check ExitRoutes() in
addition to SubnetRoutes() so the function doesn't bail early
when a node only has exit routes.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 687cf0882f policy/v2: implement autogroup:danger-all support
Add autogroup:danger-all as a valid source alias that matches ALL IP
addresses including non-Tailscale addresses. When used as a source,
it resolves to 0.0.0.0/0 + ::/0 internally but produces SrcIPs: ["*"]
in filter rules. When used as a destination, it is rejected with an
error matching Tailscale SaaS behavior.

Key changes:
- Add AutoGroupDangerAll constant and validation
- Add sourcesHaveDangerAll() helper and hasDangerAll parameter to
  srcIPsWithRoutes() across all compilation paths
- Add ErrAutogroupDangerAllDst for destination rejection
- Remove 3 AUTOGROUP_DANGER_ALL skip entries (K6, K7, K8)

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 4f040dead2 policy/v2: implement grant validation rules matching Tailscale SaaS
Implement comprehensive grant validation including: accept empty sources/destinations (they produce no rules), validate grant ip/app field requirements, capability name format, autogroup constraints, via tag existence, and default route CIDR restrictions.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 54db47badc policy/v2: implement via route compilation for grants
Compile grants with "via" field into FilterRules that are placed only
on nodes matching the via tag and actually advertising the destination
subnets. Key behavior:

- Filter rules go exclusively to via-nodes with matching approved routes
- Destination subnets not advertised by the via node are silently dropped
- App-only via grants (no ip field) produce no packet filter rules
- Via grants are skipped in the global compileFilterRules since they
  are node-specific

Reduces grant compat test skips from 41 to 30 (11 newly passing).

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 0e3acdd8ec policy/v2: implement CapGrant compilation with companion capabilities
Compile grant app fields into CapGrant FilterRules matching Tailscale
SaaS behavior. Key changes:

- Generate CapGrant rules in compileFilterRules and
  compileGrantWithAutogroupSelf, with node-specific /32 and /128
  Dsts for autogroup:self grants
- Add reversed companion rules for drive→drive-sharer and
  relay→relay-target capabilities, ordered by original cap name
- Narrow broad CapGrant Dsts to node-specific prefixes in
  ReduceFilterRules via new reduceCapGrantRule helper
- Skip merging CapGrant rules in mergeFilterRules to preserve
  per-capability structure
- Remove ip+app mutual exclusivity validation (Tailscale accepts both)
- Add semantic JSON comparison for RawMessage types and netip.Prefix
  comparators in test infrastructure

Reduces grant compat test skips from 99 to 41 (58 newly passing).

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby ebe0f4078d policy/v2: preserve non-wildcard source IPs alongside wildcard ranges
When an ACL source list contains a wildcard (*) alongside explicit
sources (tags, groups, hosts, etc.), Tailscale preserves the individual
IPs from non-wildcard sources in SrcIPs alongside the merged wildcard
CGNAT ranges. Previously, headscale's IPSetBuilder would merge all
sources into a single set, absorbing the explicit IPs into the wildcard
range.

Track non-wildcard resolved addresses separately during source
resolution, then append their individual IP strings to the output
when a wildcard is also present. This fixes the remaining 5 ACL
compat test failures (K01 and M06 subtests).

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby dda35847b0 policy/v2: reorder ACL self grants to match Tailscale rule ordering
When an ACL has non-autogroup destinations (groups, users, tags, hosts)
alongside autogroup:self, emit non-self grants before self grants to
match Tailscale's filter rule ordering. ACLs with only autogroup
destinations (self + member) preserve the policy-defined order.

This fixes ACL-A17, ACL-SF07, and ACL-SF11 compat test failures.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby f95b254ea9 policy/v2: exclude exit routes from ReduceFilterRules
Add exit route check in ReduceFilterRules to prevent exit nodes from receiving packet filter rules for destinations that only overlap via exit routes. Remove resolved SUBNET_ROUTE_FILTER_RULES grant skip entries and update error message formatting for grant validation.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby e05f45cfb1 policy/v2: use approved node routes in wildcard SrcIPs
Per Tailscale documentation, the wildcard (*) source includes "any
approved subnets" — the actually-advertised-and-approved routes from
nodes, not the autoApprover policy prefixes.

Change Asterix.resolve() to return just the base CGNAT+ULA set, and
add approved subnet routes as separate SrcIPs entries in the filter
compilation path. This preserves individual route prefixes that would
otherwise be merged by IPSet (e.g., 10.0.0.0/8 absorbing 10.33.0.0/16).

Also swap rule ordering in compileGrantWithAutogroupSelf() to emit
non-self destination rules before autogroup:self rules, matching the
Tailscale FilterRule wire format ordering.

Remove the unused AutoApproverPolicy.prefixes() method.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 995ed0187c policy/v2: add advertised routes to compat test topologies
Add routable_ips and approved_routes fields to the node topology
definitions in all golden test files. These represent the subnet
routes actually advertised by nodes on the Tailscale SaaS network
during data capture:

  Routes topology (92 files, 6 router nodes):
    big-router:     10.0.0.0/8
    subnet-router:  10.33.0.0/16
    ha-router1:     192.168.1.0/24
    ha-router2:     192.168.1.0/24
    multi-router:   172.16.0.0/24
    exit-node:      0.0.0.0/0, ::/0

  ACL topology (199 files, 1 router node):
    subnet-router:  10.33.0.0/16

  Grants topology (203 files, 1 router node):
    subnet-router:  10.33.0.0/16

The route assignments were deduced from the golden data by analyzing
which router nodes receive FilterRules for which destination CIDRs
across all test files, and cross-referenced with the MTS setup
script (setup_grant_nodes.sh).

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 927ce418d2 policy/v2: use bare IPs in autogroup:self DstPorts
Use ip.String() instead of netip.PrefixFrom(ip, ip.BitLen()).String()
when building DstPorts for autogroup:self destinations. This produces
bare IPs like "100.90.199.68" instead of CIDR notation like
"100.90.199.68/32", matching the Tailscale FilterRule wire format.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 93d79d8da9 policy: include IPv6 in identity-based alias resolution
AppendToIPSet now adds both IPv4 and IPv6 addresses for nodes, matching Tailscale's FilterRule wire format where identity-based aliases (tags, users, groups, autogroups) resolve to both address families. Update ReduceFilterRules test expectations to include IPv6 entries.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 500442c8f1 policy/v2: convert routes compat tests to data-driven format with Tailscale SaaS captures
Replace 8,286 lines of inline Go test expectations with 92 JSON golden files captured from Tailscale SaaS. The data-driven test driver validates route filtering, auto-approval, HA routing, and exit node behavior against real Tailscale output.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 2fb71690e8 policy/v2: convert ACL compat tests to data-driven format with Tailscale SaaS captures
Replace 9,937 lines of inline Go test expectations with 215 JSON golden files captured from Tailscale SaaS. The new data-driven test driver compares headscale's filter compilation output against real Tailscale behavior for each node in an 8-node topology.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 9f7aa55689 policy/v2: refactor alias resolution to use ResolvedAddresses
Introduce ResolvedAddresses type for structured IP set results. Refactor all Alias.Resolve() methods to return ResolvedAddresses instead of raw IPSets. Restrict identity-based aliases to matching address families, fix nil dereferences in partial resolution paths, and update test expectations for the new IP format (bare IPs, IP ranges instead of CIDR prefixes).

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 0fa9dcaff8 policy/v2: add data-driven grants compatibility test with Tailscale SaaS captures
Rename tailscale_compat_test.go to tailscale_acl_compat_test.go to make room for the grants compat test. Add 237 GRANT-*.json golden test files captured from Tailscale SaaS and a data-driven test driver that compares headscale's grant filter compilation against real Tailscale behavior.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby f74ea5b8ed hscontrol/policy/v2: add Grant policy format support
Add support for the Grant policy format as an alternative to ACL format,
following Tailscale's policy v2 specification. Grants provide a more
structured way to define network access rules with explicit separation
of IP-based and capability-based permissions.

Key changes:

- Add Grant struct with Sources, Destinations, InternetProtocols (ip),
  and App (capabilities) fields
- Add ProtocolPort type for unmarshaling protocol:port strings
- Add Grant validation in Policy.validate() to enforce:
  - Mutual exclusivity of ip and app fields
  - Required ip or app field presence
  - Non-empty sources and destinations
- Refactor compileFilterRules to support both ACLs and Grants
- Convert ACLs to Grants internally via aclToGrants() for unified
  processing
- Extract destinationsToNetPortRange() helper for cleaner code
- Rename parseProtocol() to toIANAProtocolNumbers() for clarity
- Add ProtocolNumberToName mapping for reverse lookups

The Grant format allows policies to be written using either the legacy
ACL format or the new Grant format. ACLs are converted to Grants
internally, ensuring backward compatibility while enabling the new
format's benefits.

Updates #2180
2026-04-01 14:10:42 +01:00
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
Kristoffer Dalby 15c1cfd778 types: include ExitRoutes in HasNetworkChanges
When exit routes are approved, SubnetRoutes remains empty because exit
routes (0.0.0.0/0, ::/0) are classified separately. Without checking
ExitRoutes, the PolicyManager cache is not invalidated on exit route
approval, causing stale filter rules that lack via grant entries for
autogroup:internet destinations.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby a76b4bd46c ci: switch integration tests to ARM runners
Switch all integration test jobs (build, build-postgres, test
template) from ubuntu-latest (x86_64) to ubuntu-24.04-arm (aarch64).

ARM runners on GitHub Actions are free for public repos and tend
to have more consistent performance characteristics than the
shared x86_64 pool. This should reduce flakiness caused by
resource contention on congested runners.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby a9a2001ae7 integration: scale remaining hardcoded timeouts and replace pingAllHelper
Apply CI-aware scaling to all remaining hardcoded timeouts:

- requireAllClientsOfflineStaged: scale the three internal stage
  timeouts (15s/20s/60s) with ScaledTimeout.
- validateReloginComplete: scale requireAllClientsOnline (120s)
  and requireAllClientsNetInfoAndDERP (3min) calls.
- WaitForTailscaleSyncPerUser callers in acl_test.go (3 sites, 60s).
- WaitForRunning callers in tags_test.go (10 sites): switch to
  PeerSyncTimeout() to match convention.
- WaitForRunning/WaitForPeers direct callers in route_test.go.
- requireAllClientsOnline callers in general_test.go and
  auth_key_test.go.

Replace pingAllHelper with assertPingAll/assertPingAllWithCollect:

- Wraps pings in EventuallyWithT so transient docker exec timeouts
  are retried instead of immediately failing the test.
- Timeout scales with the ping matrix size (2s per ping budget for
  2 full sweeps) so large tests get proportionally more time.
- Uses CollectT correctly, fixing the broken EventuallyWithT usage
  in TestEphemeral where the old t.Errorf bypassed CollectT.
- Follows the established assert*/assertWithCollect naming.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby acb8cfc7ee integration: make docker execute and ping timeouts CI-aware
The default docker execute timeout (10s) is the root cause of
"dockertest command timed out" errors across many integration tests
on CI. On congested GitHub Actions runners, docker exec latency
alone can consume 2-5 seconds of this budget before the command
even starts inside the container.

Replace the hardcoded 10s constant with a function that returns
20s on CI, doubling the budget for all container commands
(tailscale status, headscale CLI, curl, etc.).

Similarly, scale the default tailscale ping timeout from 200ms to
400ms on CI. This doubles the per-attempt budget and the docker
exec timeout for pings (from 200ms*5=1s to 400ms*5=2s), giving
more headroom for docker exec overhead.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby f1e5f1346d integration/acl: add tag verification step to TestACLTagPropagationPortSpecific
TestACLTagPropagationPortSpecific failed twice on CI because it jumped
from SetNodeTags directly to checking curl, without first verifying the
tag change was applied on the server. This races against server-side
processing.

Add a tag verification step (matching TestACLTagPropagation's pattern)
and bump the Step 4 timeout from 60s to 90s since port-specific filter
changes require both endpoints to process the new PacketFilter from
the MapResponse while the WireGuard tunnel stays up.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby 210f58f62e integration: use CI-scaled timeouts for all EventuallyWithT assertions
Wrap all 329 hardcoded EventuallyWithT timeouts across 12 test files
with integrationutil.ScaledTimeout(), which applies a 2x multiplier
on CI runners. This addresses the systemic issue where hardcoded
timeouts that work locally are insufficient under CI resource
contention.

Variable-based timeouts (propagationTime, assertTimeout in
route_test.go and totalWaitTime in auth_oidc_test.go) are wrapped
at their definition site so all downstream usages benefit.

The retry intervals (second duration parameter) are intentionally
NOT scaled, as they control polling frequency, not total wait time.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby a147b0cd87 integration/acl: use CurlFailFast for all negative curl assertions
Replace Curl() with CurlFailFast() in all negative curl assertions
(where the test expects the connection to fail). CurlFailFast uses
1 retry and 2s max time instead of 3 retries and 5s max, which
avoids wasting time on unnecessary retries when we expect the
connection to be blocked.

This affects 21 call sites across 7 test functions:

- TestACLAllowUser80Dst
- TestACLDenyAllPort80
- TestACLAllowUserDst
- TestACLAllowStarDst
- TestACLNamedHostsCanReach
- TestACLDevice1CanAccessDevice2
- TestPolicyUpdateWhileRunningWithCLIInDatabase
- TestACLAutogroupSelf
- TestACLPolicyPropagationOverTime

Where possible, the inline Curl+Error pattern is replaced with the
assertCurlFailWithCollect helper introduced in the previous commit.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby a7edcf3b0f integration: add CI-scaled timeouts and curl helpers for flaky ACL tests
Add ScaledTimeout to scale EventuallyWithT timeouts by 2x on CI,
consistent with the existing PeerSyncTimeout (60s/120s) and
dockertestMaxWait (300s/600s) conventions.

Add assertCurlSuccessWithCollect and assertCurlFailWithCollect helpers
following the existing *WithCollect naming convention.
assertCurlFailWithCollect uses CurlFailFast internally for aggressive
timeouts, avoiding wasted retries when expecting blocked connections.

Apply these to the three flakiest ACL tests:

- TestACLTagPropagation: swap NetMap and curl verification order so
  the fast NetMap check (confirms MapResponse arrived) runs before
  the slower curl check. Use curl helpers and scaled timeouts.

- TestACLTagPropagationPortSpecific: use curl helpers and scaled
  timeouts.

- TestACLHostsInNetMapTable: scale the 10s EventuallyWithT timeout.

Updates #3125
2026-03-31 22:06:25 +02:00
Kristoffer Dalby fda72ad1a3 Update main.md
Co-authored-by: nblock <nblock@users.noreply.github.com>
2026-03-31 13:36:31 +02:00
Kristoffer Dalby dfaf120f2a docs: add development builds install page
Move the container image and binary download details from the README
into a dedicated documentation page at setup/install/main. This gives
development builds a proper home in the docs site alongside the other
install methods. The README now links to the docs page instead.
2026-03-31 13:36:31 +02:00
Kristoffer Dalby e171d30179 ci: add build workflow for main branch
Build and push multi-arch container images (linux/amd64, linux/arm64)
to GHCR and Docker Hub on every push to main that changes Go or Nix
files. Images are tagged as main-<short-sha> using ko with the same
distroless base image as release builds.

Cross-compiled binaries for linux and darwin (amd64, arm64) are
uploaded as workflow artifacts. The README links to these via
nightly.link for stable download URLs.
2026-03-31 13:36:31 +02:00
Kristoffer Dalby 0c6b9f5348 goreleaser: remove unused ts2019 build tag
The ts2019 build tag is no longer used. Remove it from the
goreleaser build configuration.
2026-03-31 13:36:31 +02:00
Florian Preinstorfer f3512d50df Switch to mkdocs-materialx
The project mkdocs-material is in maintenance-only mode and their
successor is not ready yet.

Use the modern, refreshed theme and drop the pymdownx.magiclink
extension.
2026-03-25 22:30:03 +01:00
Florian Preinstorfer efd83da14e Explicitly mention that a headscale username should *not* end with @
See: #3149
2026-03-20 19:44:33 +01:00
Tanayk07 568baf3d02 fix: align banner right-side border to consistent 64-char width 2026-03-19 07:08:35 +01:00
Tanayk07 5105033224 feat: add prominent warning banner for non-standard IP prefixes
Add a highly visible ASCII-art warning banner that is printed at
startup when the configured IP prefixes fall outside the standard
Tailscale CGNAT (100.64.0.0/10) or ULA (fd7a:115c:a1e0::/48) ranges.

The warning fires once even if both v4 and v6 are non-standard, and
the warnBanner() function is reusable for other critical configuration
warnings in the future.

Also updates config-example.yaml to clarify that subsets of the
default ranges are fine, but ranges outside CGNAT/ULA are not.

Closes #3055
2026-03-19 07:08:35 +01:00
Kristoffer Dalby 3d53f97c82 hscontrol/servertest: fix test expectations for eventual consistency
Three corrections to issue tests that had wrong assumptions about
when data becomes available:

1. initial_map_should_include_peer_online_status: use WaitForCondition
   instead of checking the initial netmap. Online status is set by
   Connect() which sends a PeerChange patch after the initial
   RegisterResponse, so it may not be present immediately.

2. disco_key_should_propagate_to_peers: use WaitForCondition. The
   DiscoKey is sent in the first MapRequest (not RegisterRequest),
   so peers may not see it until a subsequent map update.

3. approved_route_without_announcement: invert the test expectation.
   Tailscale uses a strict advertise-then-approve model -- routes are
   only distributed when the node advertises them (Hostinfo.RoutableIPs)
   AND they are approved. An approval without advertisement is a dormant
   pre-approval. The test now asserts the route does NOT appear in
   AllowedIPs, matching upstream Tailscale semantics.

Also fix TestClient.Reconnect to clear the cached netmap and drain
pending updates before re-registering. Without this, WaitForPeers
returned immediately based on the old session's stale data.
2026-03-19 07:05:58 +01:00
Kristoffer Dalby 1053fbb16b hscontrol/state: fix online status reset during re-registration
Two fixes to how online status is handled during registration:

1. Re-registration (applyAuthNodeUpdate, HandleNodeFromPreAuthKey) no
   longer resets IsOnline to false. Online status is managed exclusively
   by Connect()/Disconnect() in the poll session lifecycle. The reset
   caused a false offline blip: the auth handler's change notification
   triggered a map regeneration showing the node as offline to peers,
   even though Connect() would set it back to true moments later.

2. New node creation (createAndSaveNewNode) now explicitly sets
   IsOnline=false instead of leaving it nil. This ensures peers always
   receive a known online status rather than an ambiguous nil/unknown.
2026-03-19 07:05:58 +01:00
Kristoffer Dalby b09af3846b hscontrol/poll,state: fix grace period disconnect TOCTOU race
When a node disconnects, serveLongPoll defers a cleanup that starts a
grace period goroutine. This goroutine polls batcher.IsConnected() and,
if the node has not reconnected within ~10 seconds, calls
state.Disconnect() to mark it offline. A TOCTOU race exists: the node
can reconnect (calling Connect()) between the IsConnected check and
the Disconnect() call, causing the stale Disconnect() to overwrite
the new session's online status.

Fix with a monotonic per-node generation counter:

- State.Connect() increments the counter and returns the current
  generation alongside the change list.
- State.Disconnect() accepts the generation from the caller and
  rejects the call if a newer generation exists, making stale
  disconnects from old sessions a no-op.
- serveLongPoll captures the generation at Connect() time and passes
  it to Disconnect() in the deferred cleanup.
- RemoveNode's return value is now checked: if another session already
  owns the batcher slot (reconnect happened), the old session skips
  the grace period entirely.

Update batcher_test.go to track per-node connect generations and
pass them through to Disconnect(), matching production behavior.

Fixes the following test failures:
- server_state_online_after_reconnect_within_grace
- update_history_no_false_offline
- nodestore_correct_after_rapid_reconnect
- rapid_reconnect_peer_never_sees_offline
2026-03-19 07:05:58 +01:00
Kristoffer Dalby 00c41b6422 hscontrol/servertest: add race, stress, and poll race tests
Add three test files designed to stress the control plane under
concurrent and adversarial conditions:

- race_test.go: 14 tests exercising concurrent mutations, session
  replacement, batcher contention, NodeStore access, and map response
  delivery during disconnect. All pass the Go race detector.

- poll_race_test.go: 8 tests targeting the poll.go grace period
  interleaving. These confirm a logical TOCTOU race: when a node
  disconnects and reconnects within the grace period, the old
  session's deferred Disconnect() can overwrite the new session's
  Connect(), leaving IsOnline=false despite an active poll session.

- stress_test.go: sustained churn, rapid mutations, rolling
  replacement, data integrity checks under load, and verification
  that rapid reconnects do not leak false-offline notifications.

Known failing tests (grace period TOCTOU race):
- server_state_online_after_reconnect_within_grace
- update_history_no_false_offline
- rapid_reconnect_peer_never_sees_offline
2026-03-19 07:05:58 +01:00
Kristoffer Dalby ab4e205ce7 hscontrol/servertest: expand issue tests to 24 scenarios, surface 4 issues
Split TestIssues into 7 focused test functions to stay under cyclomatic
complexity limits while testing more aggressively.

Issues surfaced (4 failing tests):

1. initial_map_should_include_peer_online_status: Initial MapResponse
   has Online=nil for peers. Online status only arrives later via
   PeersChangedPatch.

2. disco_key_should_propagate_to_peers: DiscoPublicKey set by client
   is not visible to peers. Peers see zero disco key.

3. approved_route_without_announcement_is_visible: Server-side route
   approval without client-side announcement silently produces empty
   SubnetRoutes (intersection of empty announced + approved = empty).

4. nodestore_correct_after_rapid_reconnect: After 5 rapid reconnect
   cycles, NodeStore reports node as offline despite having an active
   poll session. The connect/disconnect grace period interleaving
   leaves IsOnline in an incorrect state.

Passing tests (20) verify:
- IP uniqueness across 10 nodes
- IP stability across reconnect
- New peers have addresses immediately
- Node rename propagates to peers
- Node delete removes from all peer lists
- Hostinfo changes (OS field) propagate
- NodeStore/DB consistency after route mutations
- Grace period timing (8-20s window)
- Ephemeral node deletion (not just offline)
- 10-node simultaneous connect convergence
- Rapid sequential node additions
- Reconnect produces complete map
- Cross-user visibility with default policy
- Same-user multiple nodes get distinct IDs
- Same-hostname nodes get unique GivenNames
- Policy change during connect still converges
- DERP region references are valid
- User profiles present for self and peers
- Self-update arrives after route approval
- Route advertisement stored as AnnouncedRoutes
2026-03-19 07:05:58 +01:00
Kristoffer Dalby f87b08676d hscontrol/servertest: add policy, route, ephemeral, and content tests
Extend the servertest harness with:
- TestClient.Direct() accessor for advanced operations
- TestClient.WaitForPeerCount and WaitForCondition helpers
- TestHarness.ChangePolicy for ACL policy testing
- AssertDERPMapPresent and AssertSelfHasAddresses

New test suites:
- content_test.go: self node, DERP map, peer properties, user profiles,
  update history monotonicity, and endpoint update propagation
- policy_test.go: default allow-all, explicit policy, policy triggers
  updates on all nodes, multiple policy changes, multi-user mesh
- ephemeral_test.go: ephemeral connect, cleanup after disconnect,
  mixed ephemeral/regular, reconnect prevents cleanup
- routes_test.go: addresses in AllowedIPs, route advertise and approve,
  advertised routes via hostinfo, CGNAT range validation

Also fix node_departs test to use WaitForCondition instead of
assert.Eventually, and convert concurrent_join_and_leave to
interleaved_join_and_leave with grace-period-tolerant assertions.
2026-03-19 07:05:58 +01:00
Kristoffer Dalby ca7362e9aa hscontrol/servertest: add control plane lifecycle and consistency tests
Add three test files exercising the servertest harness:

- lifecycle_test.go: connection, disconnection, reconnection, session
  replacement, and mesh formation at various sizes.
- consistency_test.go: symmetric visibility, consistent peer state,
  address presence, concurrent join/leave convergence.
- weather_test.go: rapid reconnects, flapping stability, reconnect
  with various delays, concurrent reconnects, and scale tests.

All tests use table-driven patterns with subtests.
2026-03-19 07:05:58 +01:00
Kristoffer Dalby 0288614bdf hscontrol: add servertest harness for in-process control plane testing
Add a new hscontrol/servertest package that provides a test harness
for exercising the full Headscale control protocol in-process, using
Tailscale's controlclient.Direct as the client.

The harness consists of:
- TestServer: wraps a Headscale instance with an httptest.Server
- TestClient: wraps controlclient.Direct with NetworkMap tracking
- TestHarness: orchestrates N clients against a single server
- Assertion helpers for mesh completeness, visibility, and consistency

Export minimal accessor methods on Headscale (HTTPHandler, NoisePublicKey,
GetState, SetServerURL, StartBatcher, StartEphemeralGC) so the servertest
package can construct a working server from outside the hscontrol package.

This enables fast, deterministic tests of connection lifecycle, update
propagation, and network weather scenarios without Docker.
2026-03-19 07:05:58 +01:00
Kristoffer Dalby 82c7efccf8 mapper/batcher: serialize per-node work to prevent out-of-order delivery
processBatchedChanges queued each pending change for a node as a
separate work item. Since multiple workers pull from the same channel,
two changes for the same node could be processed concurrently by
different workers. This caused two problems:

1. MapResponses delivered out of order — a later change could finish
   generating before an earlier one, so the client sees stale state.
2. updateSentPeers and computePeerDiff race against each other —
   updateSentPeers does Clear() + Store() which is not atomic relative
   to a concurrent Range() in computePeerDiff.

Bundle all pending changes for a node into a single work item so one
worker processes them sequentially. Add a per-node workMu that
serializes processing across consecutive batch ticks, preventing a
second worker from starting tick N+1 while tick N is still in progress.

Fixes #3140
2026-03-19 07:05:58 +01:00
Kristoffer Dalby 81b871c9b5 integration/acl: replace custom entrypoints with WithPackages
Replace inline WithDockerEntrypoint shell scripts in
TestACLTagPropagation and TestACLTagPropagationPortSpecific with
the standard WithPackages and WithWebserver options.

The custom entrypoints used fragile fixed sleeps and lacked the
robust network/cert readiness waits that buildEntrypoint provides.

Updates #3139
2026-03-16 03:57:05 -07:00
Kristoffer Dalby e5ebe3205a integration: standardize test infrastructure options
Make embedded DERP server and TLS the default configuration for all
integration tests, replacing the per-test opt-in model that led to
inconsistent and flaky test behavior.

Infrastructure changes:
- DefaultConfigEnv() includes embedded DERP server settings
- New() auto-generates a proper CA + server TLS certificate pair
- CA cert is installed into container trust stores and returned by
  GetCert() so clients and internal tools (curl) trust the server
- CreateCertificate() now returns (caCert, cert, key) instead of
  discarding the CA certificate
- Add WithPublicDERP() and WithoutTLS() opt-out options
- Remove WithTLS(), WithEmbeddedDERPServerOnly(), and WithDERPAsIP()
  since all their behavior is now the default or unnecessary

Test cleanup:
- Remove all redundant WithTLS/WithEmbeddedDERPServerOnly/WithDERPAsIP
  calls from test files
- Give every test a unique WithTestName by parameterizing aclScenario,
  sshScenario, and derpServerScenario helpers
- Add WithTestName to tests that were missing it
- Document all non-standard options with inline comments explaining
  why each is needed

Updates #3139
2026-03-16 03:57:05 -07:00
Kristoffer Dalby 87b8507ac9 mapper/batcher: replace connected map with per-node disconnectedAt
The Batcher's connected field (*xsync.Map[types.NodeID, *time.Time])
encoded three states via pointer semantics:

  - nil value:    node is connected
  - non-nil time: node disconnected at that timestamp
  - key missing:  node was never seen

This was error-prone (nil meaning 'connected' inverts Go idioms),
redundant with b.nodes + hasActiveConnections(), and required keeping
two parallel maps in sync. It also contained a bug in RemoveNode where
new(time.Now()) was used instead of &now, producing a zero time.

Replace the separate connected map with a disconnectedAt field on
multiChannelNodeConn (atomic.Pointer[time.Time]), tracked directly
on the object that already manages the node's connections.

Changes:
  - Add disconnectedAt field and helpers (markConnected, markDisconnected,
    isConnected, offlineDuration) to multiChannelNodeConn
  - Remove the connected field from Batcher
  - Simplify IsConnected from two map lookups to one
  - Simplify ConnectedMap and Debug from two-map iteration to one
  - Rewrite cleanupOfflineNodes to scan b.nodes directly
  - Remove the markDisconnectedIfNoConns helper
  - Update all tests and benchmarks

Fixes #3141
2026-03-16 02:22:56 -07:00
Kristoffer Dalby 60317064fd mapper/batcher: serialize per-node work to prevent out-of-order delivery
processBatchedChanges queued each pending change for a node as a
separate work item. Since multiple workers pull from the same channel,
two changes for the same node could be processed concurrently by
different workers. This caused two problems:

1. MapResponses delivered out of order — a later change could finish
   generating before an earlier one, so the client sees stale state.
2. updateSentPeers and computePeerDiff race against each other —
   updateSentPeers does Clear() + Store() which is not atomic relative
   to a concurrent Range() in computePeerDiff.

Bundle all pending changes for a node into a single work item so one
worker processes them sequentially. Add a per-node workMu that
serializes processing across consecutive batch ticks, preventing a
second worker from starting tick N+1 while tick N is still in progress.

Fixes #3140
2026-03-16 02:22:46 -07:00
Juan Font 4d427cfe2a noise: limit request body size to prevent unauthenticated OOM
The Noise handshake accepts any machine key without checking
registration, so all endpoints behind the Noise router are reachable
without credentials. Three handlers used io.ReadAll without size
limits, allowing an attacker to OOM-kill the server.

Fix:
- Add http.MaxBytesReader middleware (1 MiB) on the Noise router.
- Replace io.ReadAll + json.Unmarshal with json.NewDecoder in
  PollNetMapHandler and RegistrationHandler.
- Stop reading the body in NotImplementedHandler entirely.
2026-03-16 09:28:31 +01:00
Kristoffer Dalby afd3a6acbc mapper/batcher: remove disabled X-prefixed test functions
Remove XTestBatcherChannelClosingRace (~95 lines) and
XTestBatcherScalability (~515 lines). These were disabled by
prefixing with X (making them invisible to go test) and served
as dead code. The functionality they covered is exercised by the
active test suite.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby feaf85bfbc mapper/batcher: clean up test constants and output
L8: Rename SCREAMING_SNAKE_CASE test constants to idiomatic Go
camelCase. Remove highLoad* and extremeLoad* constants that were
only referenced by disabled (X-prefixed) tests.

L10: Fix misleading assert message that said "1337" while checking
for region ID 999.

L12: Remove emoji from test log output to avoid encoding issues
in CI environments.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 86e279869e mapper/batcher: minor production code cleanup
L1: Replace crypto/rand with an atomic counter for generating
connection IDs. These identifiers are process-local and do not need
cryptographic randomness; a monotonic counter is cheaper and
produces shorter, sortable IDs.

L5: Use getActiveConnectionCount() in Debug() instead of directly
locking the mutex and reading the connections slice. This avoids
bypassing the accessor that already exists for this purpose.

L6: Extract the hardcoded 15*time.Minute cleanup threshold into
the named constant offlineNodeCleanupThreshold.

L7: Inline the trivial addWork wrapper; AddWork now calls addToBatch
directly.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 7881f65358 mapper: extract node connection types to node_conn.go
Move connectionEntry, multiChannelNodeConn, generateConnectionID, and
all their methods from batcher.go into a dedicated file. This reduces
batcher.go from ~1170 lines to ~800 and separates per-node connection
management from batcher orchestration.

Pure move — no logic changes.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 2d549e579f mapper/batcher: add regression tests for M1, M3, M7 fixes
- TestBatcher_CloseBeforeStart_DoesNotHang: verifies Close() before
  Start() returns promptly now that done is initialized in NewBatcher.

- TestBatcher_QueueWorkAfterClose_DoesNotHang: verifies queueWork
  returns via the done channel after Close(), even without Start().

- TestIsConnected_FalseAfterAddNodeFailure: verifies IsConnected
  returns false after AddNode fails and removes the last connection.

- TestRemoveConnectionAtIndex_NilsTrailingSlot: verifies the backing
  array slot is nil-ed after removal to avoid retaining pointers.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 50e8b21471 mapper/batcher: fix pointer retention, done-channel init, and connected-map races
M7: Nil out trailing *connectionEntry pointers in the backing array
after slice removal in removeConnectionAtIndexLocked and send().
Without this, the GC cannot collect removed entries until the slice
is reallocated.

M1: Initialize the done channel in NewBatcher instead of Start().
Previously, calling Close() or queueWork before Start() would select
on a nil channel, blocking forever. Moving the make() to the
constructor ensures the channel is always usable.

M2: Move b.connected.Delete and b.totalNodes decrement inside the
Compute callback in cleanupOfflineNodes. Previously these ran after
the Compute returned, allowing a concurrent AddNode to reconnect
between the delete and the bookkeeping update, which would wipe the
fresh connected state.

M3: Call markDisconnectedIfNoConns on AddNode error paths. Previously,
when initial map generation or send timed out, the connection was
removed but b.connected retained its old nil (= connected) value,
making IsConnected return true for a node with zero connections.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 8e26651f2c mapper/batcher: add regression tests for timer leak and Close lifecycle
Add four unit tests guarding fixes introduced in recent commits:

- TestConnectionEntry_SendFastPath_TimerStopped: verifies the
  time.NewTimer fix (H1) does not leak goroutines after many
  fast-path sends on a buffered channel.

- TestBatcher_CloseWaitsForWorkers: verifies Close() blocks until all
  worker goroutines exit (H3), preventing sends on torn-down channels.

- TestBatcher_CloseThenStartIsNoop: verifies the one-shot lifecycle
  contract; Start() after Close() must not spawn new goroutines.

- TestBatcher_CloseStopsTicker: verifies Close() stops the internal
  ticker to prevent resource leaks.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 57a38b5678 mapper/batcher: reduce hot-path log verbosity
Remove Caller(), channel pointer formatting (fmt.Sprintf("%p",...)),
and mutex timing from send(), addConnection(), and
removeConnectionByChannel(). Move per-broadcast summary and
no-connection logs from Debug to Trace. Remove per-connection
"attempting"/"succeeded" logs entirely; keep Warn for failures.

These methods run on every MapResponse delivery, so the savings
compound quickly under load.

Updates #2545
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 051a38a4c4 mapper/batcher: track worker goroutines and stop ticker on Close
Close() previously closed the done channel and returned immediately,
without waiting for worker goroutines to exit. This caused goroutine
leaks in tests and allowed workers to race with connection teardown.
The ticker was also never stopped, leaking its internal goroutine.

Add a sync.WaitGroup to track the doWork goroutine and every worker
it spawns. Close() now calls wg.Wait() after signalling shutdown,
ensuring all goroutines have exited before tearing down connections.
Also stop the ticker to prevent resource leaks.

Document that a Batcher must not be reused after Close().
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 3276bda0c0 mapper/batcher: replace time.After with NewTimer to avoid timer leak
connectionEntry.send() is on the hot path: called once per connection
per broadcast tick. time.After allocates a timer that sits in the
runtime timer heap until it fires (50 ms), even when the channel send
succeeds immediately. At 1000 connected nodes, every tick leaks 1000
timers into the heap, creating continuous GC pressure.

Replace with time.NewTimer + defer timer.Stop() so the timer is
removed from the heap as soon as the fast-path send completes.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby ebc57d9a38 integration/acl: fix TestACLPolicyPropagationOverTime infrastructure
Add embedded DERP server, TLS, and netfilter=off to match the
infrastructure configuration used by all other ACL integration tests.

Without these options, the test fails intermittently because traffic
routes through external DERP relays and iptables initialization fails
in Docker containers.

Updates #3139
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 2058343ad6 mapper: remove Batcher interface, rename to Batcher struct
Remove the Batcher interface since there is only one implementation.
Rename LockFreeBatcher to Batcher and merge batcher_lockfree.go into
batcher.go.

Drop type assertions in debug.go now that mapBatcher is a concrete
*mapper.Batcher pointer.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 9b24a39943 mapper/batcher: add scale benchmarks
Add benchmarks that systematically test node counts from 100 to
50,000 to identify scaling limits and validate performance under
load.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 3ebe4d99c1 mapper/batcher: reduce lock contention with two-phase send
Rewrite multiChannelNodeConn.send() to use a two-phase approach:
1. RLock: snapshot connections slice (cheap pointer copy)
2. Unlock: send to all connections (50ms timeouts happen here)
3. Lock: remove failed connections by pointer identity

Previously, send() held the write lock for the entire duration of
sending to all connections. With N stale connections each timing out
at 50ms, this blocked addConnection/removeConnection for N*50ms.
The two-phase approach holds the lock only for O(N) pointer
operations, not for N*50ms I/O waits.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby da33795e79 mapper/batcher: fix race conditions in cleanup and lookups
Replace the two-phase Load-check-Delete in cleanupOfflineNodes with
xsync.Map.Compute() for atomic check-and-delete. This prevents the
TOCTOU race where a node reconnects between the hasActiveConnections
check and the Delete call.

Add nil guards on all b.nodes.Load() and b.nodes.Range() call sites
to prevent nil pointer panics from concurrent cleanup races.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 57070680a5 mapper/batcher: restructure internals for correctness
Move per-node pending changes from a shared xsync.Map on the batcher
into multiChannelNodeConn, protected by a dedicated mutex. The new
appendPending/drainPending methods provide atomic append and drain
operations, eliminating data races in addToBatch and
processBatchedChanges.

Add sync.Once to multiChannelNodeConn.close() to make it idempotent,
preventing panics from concurrent close calls on the same channel.

Add started atomic.Bool to guard Start() against being called
multiple times, preventing orphaned goroutines.

Add comprehensive concurrency tests validating these changes.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 21e02e5d1f mapper/batcher: add unit tests and benchmarks
Add comprehensive unit tests for the LockFreeBatcher covering
AddNode/RemoveNode lifecycle, addToBatch routing (broadcast, targeted,
full update), processBatchedChanges deduplication, cleanup of offline
nodes, close/shutdown behavior, IsConnected state tracking, and
connected map consistency.

Add benchmarks for connection entry send, multi-channel send and
broadcast, peer diff computation, sentPeers updates, addToBatch at
various scales (10/100/1000 nodes), processBatchedChanges, broadcast
delivery, IsConnected lookups, connected map enumeration, connection
churn, and concurrent send+churn scenarios.

Widen setupBatcherWithTestData to accept testing.TB so benchmarks can
reuse the same database-backed test setup as unit tests.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 2f94b80e70 go.mod: add stress tool dependency
Add golang.org/x/tools/cmd/stress as a tool dependency for running
tests under repeated stress to surface flaky failures.

Update flake vendorHash for the new go.mod dependencies.
2026-03-14 02:52:28 -07:00
Kristoffer Dalby 3e0a96ec3a all: fix test flakiness and improve test infrastructure
Buffer the AuthRequest verdict channel to prevent a race where the
sender blocks indefinitely if the receiver has already timed out, and
increase the auth followup test timeout from 100ms to 5s to prevent
spurious failures under load.

Skip postgres-backed tests when the postgres server is unavailable
instead of calling t.Fatal, which was preventing the rest of the test
suite from running.

Add TestMain to db, types, and policy/v2 packages to chdir to the
source directory before running tests. This ensures relative testdata/
paths resolve correctly when the test binary is executed from an
arbitrary working directory (e.g., via "go tool stress").
2026-03-14 02:52:28 -07:00
DM fffc58b5d0 poll: fix poll test linter violations 2026-03-12 01:27:34 -07:00
DM 4aca9d6568 poll: stop stale map sessions through an explicit teardown hook
When stale-send cleanup prunes a connection from the batcher, the old serveLongPoll session needs an explicit stop signal. Pass a stop hook into AddNode and trigger it when that connection is removed, so the session exits through its normal cancel path instead of relying on channel closure from the batcher side.
2026-03-12 01:27:34 -07:00
DM 3daf45e88a mapper: close stale map channels after send timeouts
When the batcher timed out sending to a node, it removed the channel from multiChannelNodeConn but left the old serveLongPoll goroutine running on that channel. That left a live stale session behind: it no longer received new updates, but it could still keep the stream open and block shutdown.

Close the pruned channel when stale-send cleanup removes it so the old map session exits after draining any buffered update.
2026-03-12 01:27:34 -07:00
DM b81d6c734d mapper: handle RemoveNode after channel cleanup
A connection can already be removed from multiChannelNodeConn by the stale-send cleanup path before serveLongPoll reaches its deferred RemoveNode call. In that case RemoveNode used to return early on "channel not found" and never updated the node's connected state.

Drop that early return so RemoveNode still checks whether any active connections remain and marks the node disconnected when the last one is gone.
2026-03-12 01:27:34 -07:00
Kristoffer Dalby c5ef1d3bb9 nix: upgrade dev shell to Python 3.14
Update mdformat and related packages from python313Packages to
python314Packages. All four packages (mdformat, mdformat-footnote,
mdformat-frontmatter, mdformat-mkdocs) are available in the updated
nixpkgs.

Updates #1261
2026-03-11 03:18:14 -07:00
Kristoffer Dalby 542cdb2cb2 all: update Go to 1.26.1
Bump Go version from 1.26.0 to 1.26.1 across go.mod, Dockerfiles,
and the integration test runner fallback defaults.

Updates #1261
2026-03-11 03:18:14 -07:00
Kristoffer Dalby 5e33259550 nix: update flake inputs
Update nixpkgs from 2026-02-15 (ac055f38) to 2026-03-08 (608d0cad).

Updates #1261
2026-03-11 03:18:14 -07:00
Kristoffer Dalby 65880ecb58 nix: disable external DERP URL fetch in VM test
Explicitly set derp.urls to an empty list in the NixOS VM test,
matching the upstream nixpkgs test. The VMs have no internet
access, so fetching the default Tailscale DERP map would silently
fail and add unnecessary timeout delay to the test run.
2026-03-06 05:18:44 -08:00
Kristoffer Dalby 37c6a9e3a6 nix: sync module options and descriptions with upstream nixpkgs
Add missing typed options from the upstream nixpkgs module:
- configFile: read-only option exposing the generated config path
  for composability with other NixOS modules
- dns.split: split DNS configuration with proper type checking
- dns.extra_records: typed submodule with name/type/value validation

Sync descriptions and assertions with upstream:
- Use Tailscale doc link for override_local_dns description
- Remove redundant requirement note from nameservers.global
- Match upstream assertion message wording and expression style

Update systemd script to reference cfg.configFile instead of a
local let-binding, matching the upstream pattern.
2026-03-06 05:18:44 -08:00
DM 8423af2732 Swap favicon for updated version
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been skipped
Needs More Info - Timer / close-stale (push) Has been skipped
Close inactive issues / close-issues (push) Has been skipped
2026-03-03 05:59:40 +01:00
Florian Preinstorfer 9baa795ddb Update docs for auth-id changes
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
- Replace "headscale nodes register" with "headscale auth register"
- Update from registration key to Auth ID
- Fix API example to register a node
2026-03-01 13:38:22 +01:00
Florian Preinstorfer acddd73183 Reformat docs with mdformat 2026-03-01 09:24:52 +01:00
Florian Preinstorfer 47307d19cf Switch to mdformat to format docs
- Use mdformat and mdformat-mkdocs to format docs
- Add mdformat to Makefile and pre-commit-config
- Prettier ignores docs/
2026-03-01 09:24:52 +01:00
Kristoffer Dalby 5c449db125 ci: regenerate test-integration.yaml for TestSSHLocalpart
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Updates #3049
2026-02-28 05:14:11 -08:00
Kristoffer Dalby 2be94ce19a integration: add TestSSHLocalpart integration test
Add end-to-end integration test that validates localpart:*@domain
SSH user mapping with real Tailscale clients. The test sets up an
SSH policy with localpart entries and verifies that users can SSH
into tagged servers using their email local-part as the username.

Updates #3049
2026-02-28 05:14:11 -08:00
Kristoffer Dalby 6c59d3e601 policy/v2: add SSH compatibility testdata from Tailscale SaaS
Add 39 test fixtures captured from Tailscale SaaS API responses
to validate SSH policy compilation parity. Each JSON file contains
the SSH policy section and expected compiled SSHRule arrays for 5
test nodes (3 user-owned, 2 tagged).

Test series: SSH-A (basic), SSH-B (specific sources), SSH-C
(destination combos), SSH-D (localpart), SSH-E (edge cases),
SSH-F (multi-rule), SSH-G (acceptEnv).

The data-driven TestSSHDataCompat harness uses cmp.Diff with
principal order tolerance but strict rule ordering (first-match-wins
semantics require exact order).

Updates #3049
2026-02-28 05:14:11 -08:00
Kristoffer Dalby 0acf09bdd2 policy/v2: add localpart:*@domain SSH user compilation
Add support for localpart:*@<domain> entries in SSH policy users.
When a user SSHes into a target, their email local-part becomes the
OS username (e.g. alice@example.com → OS user alice).

Type system (types.go):
- SSHUser.IsLocalpart() and ParseLocalpart() for validation
- SSHUsers.LocalpartEntries(), NormalUsers(), ContainsLocalpart()
- Enforces format: localpart:*@<domain> (wildcard-only)
- UserWildcard.Resolve for user:*@domain SSH source aliases
- acceptEnv passthrough for SSH rules

Compilation (filter.go):
- resolveLocalparts: pure function mapping users to local-parts
  by email domain. No node walking, easy to test.
- groupSourcesByUser: single walk producing per-user principals
  with sorted user IDs, and tagged principals separately.
- ipSetToPrincipals: shared helper replacing 6 inline copies.
- selfPrincipalsForNode: self-access using pre-computed byUser.

The approach separates data gathering from rule assembly. Localpart
rules are interleaved per source user to match Tailscale SaaS
first-match-wins ordering.

Updates #3049
2026-02-28 05:14:11 -08:00
QEDeD 414d3bbbd8 Fix typo in comment about fsnotify behavior
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Correct loose (opposite of tight) to lose (opposite of keep).
2026-02-27 15:23:06 +01:00
Stefan Bethke 0f12e414a6 Explain one approach to update OIDC provider info
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
See #3112
2026-02-27 10:06:50 +01:00
Stefan Bethke df339cd290 Add a link to Authentik's integration guide 2026-02-27 09:56:18 +01:00
DM 610c1daa4d types: avoid NodeView clone in CanAccess
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
NodeView.CanAccess called node2.AsStruct() on every check. In peer-map construction we run CanAccess in O(n^2) pair scans (often twice per pair), so that per-call clone multiplied into large heap churn
2026-02-26 19:15:07 -08:00
Kristoffer Dalby 84adda226b doc: add CHANGELOG entries for SSH check and auth commands
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 0f97294665 ci: regenerate integration test workflow
Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 3db0a483ed integration: add SSH check mode tests
Add ReadLog method to headscale integration container for log
inspection. Split SSH check mode tests into CLI and OIDC variants
and add comprehensive test coverage:

- TestSSHOneUserToOneCheckModeCLI: basic check mode with CLI approval
- TestSSHOneUserToOneCheckModeOIDC: check mode with OIDC approval
- TestSSHCheckModeUnapprovedTimeout: rejection on cache expiry
- TestSSHCheckModeCheckPeriodCLI: session expiry and re-auth
- TestSSHCheckModeAutoApprove: auto-approval within check period
- TestSSHCheckModeNegativeCLI: explicit rejection via CLI

Update existing integration tests to use headscale auth register.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 7bab8da366 state, policy, noise: implement SSH check period auto-approval
Add SSH check period tracking so that recently authenticated users
are auto-approved without requiring manual intervention each time.

Introduce SSHCheckPeriod type with validation (min 1m, max 168h,
"always" for every request) and encode the compiled check period
as URL query parameters in the HoldAndDelegate URL.

The SSHActionHandler checks recorded auth times before creating a
new HoldAndDelegate flow. Auth timestamps are stored in-memory:
- Default period (no explicit checkPeriod): auth covers any
  destination, keyed by source node with Dst=0 sentinel
- Explicit period: auth covers only that specific destination,
  keyed by (source, destination) pair

Auth times are cleared on policy changes.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 48cc98b787 hscontrol, cli: add auth register and approve commands
Implement AuthRegister and AuthApprove gRPC handlers and add
corresponding CLI commands (headscale auth register, approve, reject)
for managing pending auth requests including SSH check approvals.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 61a14bb0e4 gen: regenerate from auth proto changes
Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby dc0e52a960 proto: add AuthRegister and AuthApprove RPCs
Add gRPC service definitions for managing auth requests:
AuthRegister to register interactive auth sessions and
AuthApprove/AuthReject to approve or deny pending requests
(used for SSH check mode).

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 107c2f2f70 policy, noise: implement SSH check action
Implement the SSH "check" action which requires additional
verification before allowing SSH access. The policy compiler generates
a HoldAndDelegate URL that the Tailscale client calls back to
headscale. The SSHActionHandler creates an auth session and waits for
approval via the generalised auth flow.

Sort check (HoldAndDelegate) rules before accept rules to match
Tailscale's first-match-wins evaluation order.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 4a7e1475c0 templates: generalise auth templates for web and OIDC
Extract shared HTML/CSS design into a common template and create
generalised auth success and web auth templates that work for both
node registration and SSH check authentication flows.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby cb3b6949ea auth: generalise auth flow and introduce AuthVerdict
Generalise the registration pipeline to a more general auth pipeline
supporting both node registrations and SSH check auth requests.
Rename RegistrationID to AuthID, unexport AuthRequest fields, and
introduce AuthVerdict to unify the auth finish API.

Add the urlParam generic helper for extracting typed URL parameters
from chi routes, used by the new auth request handler.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 30338441c1 app: switch from gorilla to chi mux
Replace gorilla/mux with go-chi/chi as the HTTP router and add a
custom zerolog-based request logger to replace chi's default
stdlib-based middleware.Logger, consistent with the rest of the
application.

Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 25ccb5a161 build: update golangci-lint and gopls in flake
Updates #1850
2026-02-25 21:28:05 +01:00
Kristoffer Dalby 8048f10d13 hscontrol/state: extract findExistingNodeForPAK to reduce complexity
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Extract the existing-node lookup logic from HandleNodeFromPreAuthKey
into a separate method. This reduces the cyclomatic complexity from
32 to 28, below the gocyclo limit of 30.

Updates #3077
2026-02-20 21:51:00 +01:00
Kristoffer Dalby be4fd9ff2d integration: fix tag tests for tagged nodes with nil user_id
Tagged nodes no longer have user_id set, so ListNodes(user) cannot
find them. Update integration tests to use ListNodes() (all nodes)
when looking up tagged nodes.

Add a findNode helper to locate nodes by predicate from an
unfiltered list, used in ACL tests that have multiple nodes per
scenario.

Updates #3077
2026-02-20 21:51:00 +01:00
Kristoffer Dalby 1e4fc3f179 hscontrol: add tests for deleting users with tagged nodes
Test the tagged-node-survives-user-deletion scenario at two layers:

DB layer (users_test.go):
- success_user_only_has_tagged_nodes: tagged nodes with nil
  user_id do not block user deletion and survive it
- error_user_has_tagged_and_owned_nodes: user-owned nodes
  still block deletion even when tagged nodes coexist

App layer (grpcv1_test.go):
- TestDeleteUser_TaggedNodeSurvives: full registration flow
  with tagged PreAuthKey verifies nil UserID after registration,
  absence from nodesByUser index, user deletion succeeds, and
  tagged node remains in global node list

Also update auth_tags_test.go assertions to expect nil UserID
on tagged nodes, consistent with the new invariant.

Updates #3077
2026-02-20 21:51:00 +01:00
Kristoffer Dalby 894e6946dc hscontrol/types: regenerate types_view.go
make generate

Updates #3077
2026-02-20 21:51:00 +01:00
Kristoffer Dalby 75e56df9e4 hscontrol: enforce that tagged nodes never have user_id
Tagged nodes are owned by their tags, not a user. Enforce this
invariant at every write path:

- createAndSaveNewNode: do not set UserID for tagged PreAuthKey
  registration; clear UserID when advertise-tags are applied
  during OIDC/CLI registration
- SetNodeTags: clear UserID/User when tags are assigned
- processReauthTags: clear UserID/User when tags are applied
  during re-authentication
- validateNodeOwnership: reject tagged nodes with non-nil UserID
- NodeStore: skip nodesByUser indexing for tagged nodes since
  they have no owning user
- HandleNodeFromPreAuthKey: add fallback lookup for tagged PAK
  re-registration (tagged nodes indexed under UserID(0)); guard
  against nil User deref for tagged nodes in different-user check

Since tagged nodes now have user_id = NULL, ListNodesByUser
will not return them and DestroyUser naturally allows deleting
users whose nodes have all been tagged. The ON DELETE CASCADE
FK cannot reach tagged nodes through a NULL foreign key.

Also tone down shouty comments throughout state.go.

Fixes #3077
2026-02-20 21:51:00 +01:00
Kristoffer Dalby 52d454d0c8 hscontrol/db: add migration to clear user_id on tagged nodes
Tagged nodes are owned by their tags, not a user. Previously
user_id was kept as "created by" tracking, but this prevents
deleting users whose nodes have all been tagged, and the
ON DELETE CASCADE FK would destroy the tagged nodes.

Add a migration that sets user_id = NULL on all existing tagged
nodes. Subsequent commits enforce this invariant at write time.

Updates #3077
2026-02-20 21:51:00 +01:00
Kristoffer Dalby f20bd0cf08 node: implement disable key expiry via CLI and API
Add --disable flag to "headscale nodes expire" CLI command and
disable_expiry field handling in the gRPC API to allow disabling
key expiry for nodes. When disabled, the node's expiry is set to
NULL and IsExpired() returns false.

The CLI follows the new grpcRunE/RunE/printOutput patterns
introduced in the recent CLI refactor.

Also fix NodeSetExpiry to persist directly to the database instead
of going through persistNodeToDB which omits the expiry field.

Fixes #2681

Co-authored-by: Marco Santos <me@marcopsantos.com>
2026-02-20 21:49:55 +01:00
Kristoffer Dalby a8f7fedced proto: add disable_expiry field to ExpireNodeRequest
Add bool disable_expiry field (field 3) to ExpireNodeRequest proto
and regenerate all protobuf, gRPC gateway, and OpenAPI files.

Fixes #2681

Co-authored-by: Marco Santos <me@marcopsantos.com>
2026-02-20 21:49:55 +01:00
Kristoffer Dalby b668c7a596 policy/v2: add policy unmarshal tests for bracketed IPv6
Add end-to-end test cases to TestUnmarshalPolicy that verify bracketed
IPv6 addresses are correctly parsed through the full policy pipeline
(JSON unmarshal -> splitDestinationAndPort -> parseAlias -> parsePortRange)
and survive JSON round-trips.

Cover single port, multiple ports, wildcard port, CIDR prefix, port
range, bracketed IPv4, and hostname rejection.

Updates #2754
2026-02-20 21:49:21 +01:00
Kristoffer Dalby 49744cd467 policy/v2: accept RFC 3986 bracketed IPv6 in ACL destinations
Headscale rejects IPv6 addresses with square brackets in ACL policy
destinations (e.g. "[fd7a:115c:a1e0::87e1]:80,443"), while Tailscale
SaaS accepts them. The root cause is that splitDestinationAndPort uses
strings.LastIndex(":") which leaves brackets on the destination string,
and netip.ParseAddr does not accept brackets.

Add a bracket-handling branch at the top of splitDestinationAndPort that
uses net.SplitHostPort for RFC 3986 parsing when input starts with "[".
The extracted host is validated with netip.ParseAddr/ParsePrefix to
ensure brackets are only accepted around IP addresses and CIDR prefixes,
not hostnames or other alias types like tags and groups.

Fixes #2754
2026-02-20 21:49:21 +01:00
Brandon Sprague a0d6802d5b Fix minor formatting issue in FAQ
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This just fixes a small issue I noticed reading the docs: the two 'scenarios' listed in the scaling section end up showing up as a numbered list of five items, instead of the desired two items + their descriptions.
2026-02-20 16:59:14 +01:00
Kristoffer Dalby 13ebea192c cmd/headscale/cli: remove nil resp guards and unexport HasMachineOutputFlag
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Remove dead if-resp-nil checks in tagCmd and approveRoutesCmd; gRPC
returns either an error or a valid response, never (nil, nil).

Rename HasMachineOutputFlag to hasMachineOutputFlag since it has a
single internal caller in root.go.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby af777f44f4 cmd/headscale/cli: extract bypassDatabase helper and simplify policy file reads
Add bypassDatabase() to consolidate the repeated LoadServerConfig +
NewHeadscaleDatabase pattern in getPolicy and setPolicy.

Replace os.Open + io.ReadAll with os.ReadFile in setPolicy and
checkPolicy, removing the manual file-handle management.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 7460bec767 cmd/headscale/cli: move errMissingParameter and Error type to their users
Move errMissingParameter from users.go to utils.go alongside the
other shared sentinel errors; the variable is referenced by
api_key.go and preauthkeys.go.

Move the Error constant-error type from debug.go to mockoidc.go,
its only consumer.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby ca321d3c13 cmd/headscale/cli: use HeadscaleDateTimeFormat and util.Base10 consistently
Replace five hardcoded "2006-01-02 15:04:05" strings with the
HeadscaleDateTimeFormat constant already defined in utils.go.
Replace two literal 10 base arguments to strconv.FormatUint with
util.Base10 to match the convention in api_key.go and nodes.go.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 2765fd397f cmd/headscale/cli: drop dead flag-read error checks
Flags registered on a cobra.Command cannot fail to read at runtime;
GetString/GetUint64/GetStringSlice only error when the flag name is
unknown. The error-handling blocks for these calls are unreachable
dead code.

Adopt the value, _ := pattern already used in api_key.go,
preauthkeys.go and users.go, removing ~40 lines of dead code from
nodes.go and debug.go.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby d72a06c6c6 cmd/headscale/cli: remove legacy namespace and machine aliases
The --namespace flag on nodes list/register and debug create-node was
never wired to the --user flag, so its value was silently ignored.
Remove it along with the deprecateNamespaceMessage constant.

Also remove the namespace/ns command aliases on users and
machine/machines aliases on nodes, which have been deprecated since
the naming changes in 0.23.0.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby e816397d54 cmd/headscale/cli: remove no-op Args functions from serveCmd and dumpConfigCmd
These functions unconditionally return nil, which is the default cobra
behavior when Args is not set.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 22fccae125 cmd/headscale/cli: deduplicate expiration parsing and api-key flag validation
Add expirationFromFlag helper that parses the --expiration flag into a
timestamppb.Timestamp, replacing identical duration-parsing blocks in
api_key.go and preauthkeys.go.

Add apiKeyIDOrPrefix helper to validate the mutually-exclusive --id and
--prefix flags, replacing the duplicated switch block in expireAPIKeyCmd
and deleteAPIKeyCmd.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 6c08b49d63 cmd/headscale/cli: add confirmAction helper for force/prompt patterns
Centralise the repeated force-flag-check + YesNo-prompt logic into a
single confirmAction(cmd, prompt) helper.  Callers still decide what
to return on decline (error, message, or nil).
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 7b7b270126 cmd/headscale/cli: add mustMarkRequired helper for init-time flag validation
Replace three inconsistent MarkFlagRequired error-handling styles
(stdlib log.Fatal, zerolog log.Fatal, silently discarded) with a
single mustMarkRequired helper that panics on programmer error.

Also fixes a bug where renameNodeCmd.MarkFlagRequired("new-name")
targeted the wrong command (should be renameUserCmd), making the
--new-name flag effectively never required on "headscale users rename".
2026-02-20 11:42:07 +01:00
Kristoffer Dalby d6c39e65a5 cmd/headscale/cli: add printListOutput to centralise table-vs-JSON branching
Add a helper that checks the --output flag and either serialises as
JSON/YAML or invokes a table-rendering callback. This removes the
repeated format,_ := cmd.Flags().GetString("output") + if-branch from
the five list commands.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 8891ec9835 cmd/headscale/cli: remove deprecated output, SuccessOutput, ErrorOutput
All callers now use formatOutput/printOutput (non-exiting) with
RunE error returns, so the old os.Exit-based helpers are dead code.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby 095106f498 cmd/headscale/cli: convert remaining commands to RunE
Convert the 10 commands that were still using Run with
ErrorOutput/SuccessOutput or log.Fatal/os.Exit:

- backfillNodeIPsCmd: use grpcRunE-style manual connection with
  error returns; simplify the confirm/force logic
- getPolicy, setPolicy, checkPolicy: replace ErrorOutput with
  fmt.Errorf returns in both the bypass-gRPC and gRPC paths
- serveCmd, configTestCmd: replace log.Fatal with error returns
- mockOidcCmd: replace log.Error+os.Exit with error return
- versionCmd, generatePrivateKeyCmd: replace SuccessOutput with
  printOutput
- dumpConfigCmd: return the error instead of swallowing it
2026-02-20 11:42:07 +01:00
Kristoffer Dalby e4fe216e45 cmd/headscale/cli: switch to RunE with grpcRunE and error returns
Rename grpcRun to grpcRunE: the inner closure now returns error
and the wrapper returns a cobra RunE-compatible function.

Change newHeadscaleCLIWithConfig to return an error instead of
calling log.Fatal/os.Exit, making connection failures propagate
through the normal error path.

Add formatOutput (returns error) and printOutput (writes to stdout)
as non-exiting replacements for the old output/SuccessOutput pair.
Extract output format string literals into package-level constants.
Mark the old ErrorOutput, SuccessOutput and output helpers as
deprecated; they remain temporarily for the unconverted commands.

Convert all 22 grpcRunE commands from Run+ErrorOutput+SuccessOutput
to RunE+fmt.Errorf+printOutput. Change usernameAndIDFromFlag to
return an error instead of calling ErrorOutput directly.

Update backfillNodeIPsCmd and policy.go callers of
newHeadscaleCLIWithConfig for the new 5-return signature while
keeping their Run-based pattern for now.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby e6546b2cea cmd/headscale/cli: silence cobra error/usage output and centralise error formatting
Set SilenceErrors and SilenceUsage on the root command so that
cobra never prints usage text for runtime errors. A SetFlagErrorFunc
callback re-enables usage output specifically for flag-parsing
errors (the kubectl pattern).

Add printError to utils.go and switch Execute() to ExecuteC() so
the returned error is formatted as JSON/YAML when --output requests
machine-readable output.
2026-02-20 11:42:07 +01:00
Kristoffer Dalby aae2f7de71 cmd/headscale/cli: add grpcRun wrapper for gRPC client lifecycle
Add a grpcRun helper that wraps cobra RunFuncs, injecting a ready
gRPC client and context. The connection lifecycle (cancel, close)
is managed by the wrapper, eliminating the duplicated 3-line
boilerplate (newHeadscaleCLIWithConfig + defer cancel + defer
conn.Close) from 22 command handlers across 7 files.

Three call sites are intentionally left unconverted:
- backfillNodeIPsCmd: creates the client only after user confirmation
- getPolicy/setPolicy: conditionally use gRPC vs direct DB access
2026-02-20 11:42:07 +01:00
Florian Preinstorfer cfb308b4a7 Add FAQ entry to migrate back to default IP prefixes
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2026-02-19 17:16:40 +01:00
Florian Preinstorfer 4bb0241257 Require to update from one version to the next 2026-02-19 17:16:40 +01:00
Florian Preinstorfer 513544cc11 Simplify upgrade snippet with a link to the upgrade guide
Remove some duplicated text.
2026-02-19 17:16:40 +01:00
Florian Preinstorfer d556df1c36 Extend upgrade guide with backup instructions 2026-02-19 17:16:40 +01:00
Kristoffer Dalby d15ec28799 ci: pin Docker to v28 to avoid v29 breaking changes
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Docker 29 (shipped with runner-images 20260209.23.1) breaks docker
build via Go client libraries (broken pipe writing build context)
and docker load/save with certain tarball formats. Add Docker's
official apt repository and install docker-ce 28.5.x in all CI
jobs that interact with Docker.

See https://github.com/actions/runner-images/issues/13474

Updates #3058
2026-02-19 08:21:23 +01:00
Kristoffer Dalby eccf64eb58 all: fix staticcheck SA4006 in types_test.go
Use new(users["name"]) instead of extracting to intermediate
variables that staticcheck does not recognise as used with
Go 1.26 new(value) syntax.

Updates #3058
2026-02-19 08:21:23 +01:00
Kristoffer Dalby 43afeedde2 all: apply golangci-lint 2.9.0 fixes
Fix issues found by the upgraded golangci-lint:
- wsl_v5: add required whitespace in CLI files
- staticcheck SA4006: replace new(var.Field) with &localVar
  pattern since staticcheck does not recognize Go 1.26
  new(value) as a use of the variable
- staticcheck SA5011: use t.Fatal instead of t.Error for
  nil guard checks so execution stops
- unused: remove dead ptrTo helper function
2026-02-19 08:21:23 +01:00
Kristoffer Dalby 73613d7f53 db: fix database_versions table creation for PostgreSQL
Use GORM AutoMigrate instead of raw SQL to create the
database_versions table, since PostgreSQL does not support the
datetime type used in the raw SQL (it requires timestamp).
2026-02-19 08:21:23 +01:00
Kristoffer Dalby 30d18575be CHANGELOG: document strict version upgrade path 2026-02-19 08:21:23 +01:00
Kristoffer Dalby 70f8141abd all: upgrade from Go 1.26rc2 to Go 1.26.0 2026-02-19 08:21:23 +01:00
Kristoffer Dalby 82958835ce db: enforce strict version upgrade path
Add a version check that runs before database migrations to ensure
users do not skip minor versions or downgrade. This protects database
migrations and allows future cleanup of old migration code.

Rules enforced:
- Same minor version: always allowed (patch changes either way)
- Single minor upgrade (e.g. 0.27 -> 0.28): allowed
- Multi-minor upgrade (e.g. 0.25 -> 0.28): blocked with guidance
- Any minor downgrade: blocked
- Major version change: blocked
- Dev builds: warn but allow, preserve stored version

The version is stored in a purpose-built database_versions table
after migrations succeed. The table is created with raw SQL before
gormigrate runs to avoid circular dependencies.

Updates #3058
2026-02-19 08:21:23 +01:00
Kristoffer Dalby 9c3a3c5837 flake: upgrade golangci-lint to 2.9.0 and update nixpkgs 2026-02-19 08:21:23 +01:00
Florian Preinstorfer faf55f5e8f Document how to use the provider identifier in the policy
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2026-02-18 10:24:05 +01:00
Florian Preinstorfer e3323b65e5 Describe how to set username instead of SPN for Kanidm 2026-02-18 10:24:05 +01:00
Florian Preinstorfer 8f60b819ec Refresh update path
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / remove-label-on-response (push) Has been cancelled
Needs More Info - Timer / close-stale (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2026-02-16 15:22:46 +01:00
Florian Preinstorfer c29bcd2eaf Release planning happens in milestones 2026-02-16 15:22:46 +01:00
Florian Preinstorfer 890a044ef6 Add more UIs 2026-02-16 15:22:46 +01:00
Florian Preinstorfer 8028fa5483 No longer consider autogroup:self experimental 2026-02-16 15:22:46 +01:00
Kristoffer Dalby a7f981e30e github: fix needs-more-info label race condition
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Replace tiangolo/issue-manager with custom logic that distinguishes
bot comments from human responses. The issue-manager action treated
all comments equally, so the bot's own instruction comment would
trigger label removal on the next scheduled run.

Split into two jobs:
- remove-label-on-response: triggers on issue_comment from non-bot
  users, removes the needs-more-info label immediately
- close-stale: runs on daily schedule, uses nushell to iterate open
  needs-more-info issues, checks for human comments after the label
  was added, and closes after 3 days with no response
2026-02-15 19:42:47 +01:00
Kristoffer Dalby e0d8c3c877 github: fix needs-more-info label race condition
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Needs More Info - Timer / manage-needs-more-info (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Remove the `issues: labeled` trigger from the timer workflow.

When both workflows triggered on label addition, the comment workflow
would post the bot comment, and by the time the timer workflow ran,
issue-manager would see "a comment was added after the label" and
immediately remove the label due to `remove_label_on_comment: true`.

The timer workflow now only runs on:
- Daily cron (to close stale issues)
- issue_comment (to remove label when humans respond)
- workflow_dispatch (for manual testing)
2026-02-09 10:03:12 +01:00
Kristoffer Dalby c1b468f9f4 github: update issue template contact links
Reorder contact links to show Discord first, then documentation.
Update Discord invite link and docs URL to current values.
2026-02-09 09:51:28 +01:00
Kristoffer Dalby 900f4b7b75 github: add support-request automation workflow
Add workflow that automatically closes issues labeled as
support-request with a message directing users to Discord
for configuration and support questions.

The workflow:
- Triggers when support-request label is added
- Posts a comment explaining this tracker is for bugs/features
- Links to documentation and Discord
- Closes the issue as "not planned"
2026-02-09 09:51:28 +01:00
Kristoffer Dalby 64f23136a2 github: add needs-more-info automation workflow
Add GitHub Actions automation that helps manage issues requiring
additional information from reporters:

- Post an instruction comment when 'needs-more-info' label is added,
  requesting environment details, debug logs from multiple nodes,
  configuration files, and proper formatting
- Automatically remove the label when anyone comments
- Close the issue after 3 days if no response is provided
- Exempt needs-more-info labeled issues from the stale bot

The instruction comment includes guidance on:
- Required environment and debug information
- Collecting logs from both connecting and connected-to nodes
- Proper redaction rules (replace consistently, never remove IPs)
- Formatting requirements for attachments and Markdown
- Encouragement to discuss on Discord before filing issues
2026-02-09 09:51:28 +01:00
Kristoffer Dalby 0f6d312ada all: upgrade to Go 1.26rc2 and modernize codebase
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This commit upgrades the codebase from Go 1.25.5 to Go 1.26rc2 and
adopts new language features.

Toolchain updates:
- go.mod: go 1.25.5 → go 1.26rc2
- flake.nix: buildGo125Module → buildGo126Module, go_1_25 → go_1_26
- flake.nix: build golangci-lint from source with Go 1.26
- Dockerfile.integration: golang:1.25-trixie → golang:1.26rc2-trixie
- Dockerfile.tailscale-HEAD: golang:1.25-alpine → golang:1.26rc2-alpine
- Dockerfile.derper: golang:alpine → golang:1.26rc2-alpine
- .goreleaser.yml: go mod tidy -compat=1.25 → -compat=1.26
- cmd/hi/run.go: fallback Go version 1.25 → 1.26rc2
- .pre-commit-config.yaml: simplify golangci-lint hook entry

Code modernization using Go 1.26 features:
- Replace tsaddr.SortPrefixes with slices.SortFunc + netip.Prefix.Compare
- Replace ptr.To(x) with new(x) syntax
- Replace errors.As with errors.AsType[T]

Lint rule updates:
- Add forbidigo rules to prevent regression to old patterns
2026-02-08 12:35:23 +01:00
Kristoffer Dalby 20dff82f95 CHANGELOG: add minimum Tailscale version for 0.29.0
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Update the 0.29.0 changelog entry to document the minimum
supported Tailscale client version (v1.76.0), which corresponds
to capability version 106 based on the 10-version support window.
2026-02-07 08:23:51 +01:00
Kristoffer Dalby 31c4331a91 capver: regenerate from docker tags
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2026-02-07 08:23:51 +01:00
Kristoffer Dalby ce580f8245 all: fix golangci-lint issues (#3064)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-02-06 21:45:32 +01:00
Kristoffer Dalby bfb6fd80df integration: fixup test
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 3acce2da87 errors: rewrite errors to follow go best practices
Errors should not start capitalised and they should not contain the word error
or state that they "failed" as we already know it is an error

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 4a9a329339 all: use lowercase log messages
Go style recommends that log messages and error strings should not be
capitalized (unless beginning with proper nouns or acronyms) and should
not end with punctuation.

This change normalizes all zerolog .Msg() and .Msgf() calls to start
with lowercase letters, following Go conventions and making logs more
consistent across the codebase.
2026-02-06 07:40:29 +01:00
Kristoffer Dalby dd16567c52 hscontrol/state,db: use zf constants for logging
Replace raw string field names with zf constants in state.go and
db/node.go for consistent, type-safe logging.

state.go changes:
- User creation, hostinfo validation, node registration
- Tag processing during reauth (processReauthTags)
- Auth path and PreAuthKey handling
- Route auto-approval and MapRequest processing

db/node.go changes:
- RegisterNodeForTest logging
- Invalid hostname replacement logging
2026-02-06 07:40:29 +01:00
Kristoffer Dalby e0a436cefc hscontrol/util/zlog/zf: add tag, authkey, and route constants
Add new zerolog field constants for improved logging consistency:

- Tag fields: CurrentTags, RemovedTags, RejectedTags, NewTags, OldTags,
  IsTagged, WasAuthKeyTagged
- Node fields: ExistingNodeID
- AuthKey fields: AuthKeyID, AuthKeyUsed, AuthKeyExpired, AuthKeyReusable,
  NodeKeyRotation
- Route fields: RoutesApprovedOld, RoutesApprovedNew, OldAnnouncedRoutes,
  NewAnnouncedRoutes, ApprovedRoutes, OldApprovedRoutes, NewApprovedRoutes,
  AutoApprovedRoutes, AllApprovedRoutes, RouteChanged
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 53cdeff129 hscontrol/mapper: use sub-loggers and zf constants
Add sub-logger patterns to worker(), AddNode(), RemoveNode() and
multiChannelNodeConn to eliminate repeated field calls. Use zf.*
constants for consistent field naming.

Changes in batcher_lockfree.go:
- Add wlog sub-logger in worker() with worker.id context
- Add log field to multiChannelNodeConn struct
- Initialize mc.log with node.id in newMultiChannelNodeConn()
- Add nlog sub-loggers in AddNode() and RemoveNode()
- Update all connection methods to use mc.log

Changes in batcher.go:
- Use zf.NodeID and zf.Reason in handleNodeChange()
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 7148a690d0 hscontrol/grpcv1: use EmbedObject and zf constants
Replace manual field extraction with EmbedObject for node logging
in gRPC handlers. Use zf.* constants for consistent field naming.

Changes:
- RegisterNode: use EmbedObject(node), zf.RegistrationKey, etc.
- SetTags: use EmbedObject(node)
- ExpireNode: use EmbedObject(node), zf.ExpiresAt
- RenameNode: use EmbedObject(node), zf.NewName
- SetApprovedRoutes: use zf.NodeID
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 4e73133b9f hscontrol/routes: use sub-logger and zf constants
Add sub-logger pattern to SetRoutes() to eliminate repeated node.id
field calls. Replace raw strings with zf.* constants throughout
the primary routes code for consistent field naming.

Changes:
- Add nlog sub-logger in SetRoutes() with node.id context
- Replace "prefix" with zf.Prefix
- Replace "changed" with zf.Changes
- Replace "newState" with zf.NewState
- Replace "finalState" with zf.FinalState
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 4f8724151e hscontrol/poll: use sub-logger pattern for mapSession
Replace the helper functions (logf, infof, tracef, errf) with a
zerolog sub-logger initialized in newMapSession(). The sub-logger
is pre-populated with session context (component, node, omitPeers,
stream) eliminating repeated field calls throughout the code.

Changes:
- Add log field to mapSession struct
- Initialize sub-logger with EmbedObject(node) and request context
- Remove logf/infof/tracef/errf helper functions
- Update all callers to use m.log.Level().Caller()... pattern
- Update noise.go to use sess.log instead of sess.tracef

This reduces code by ~20 lines and eliminates ~15 repeated field
calls per log statement.
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 91730e2a1d hscontrol: use EmbedObject for node logging
Replace manual Uint64("node.id")/Str("node.name") field patterns with
EmbedObject(node) which automatically includes all standard node fields
(id, name, machine key, node key, online status, tags, user).

This reduces code repetition and ensures consistent logging across:
- state.go: Connect/Disconnect, persistNodeToDB, AutoApproveRoutes
- auth.go: handleLogout, handleRegisterWithAuthKey
2026-02-06 07:40:29 +01:00
Kristoffer Dalby b5090a01ec cmd: use zf constants for zerolog field names
Update CLI logging to use zf.* constants instead of inline strings
for consistency with the rest of the codebase.
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 27f5641341 golangci: add forbidigo rule for zerolog field constants
Add a lint rule to enforce use of zf.* constants for zerolog field
names instead of inline string literals. This catches at lint time
any new code that doesn't follow the convention.

The rule matches common zerolog field methods (Str, Int, Bool, etc.)
and flags any usage with a string literal first argument.
2026-02-06 07:40:29 +01:00
Kristoffer Dalby cf3d30b6f6 types: add MarshalZerologObject to domain types
Implement zerolog.LogObjectMarshaler interface on domain types
for structured logging:

- Node: logs node.id, node.name, machine.key (short), node.key (short),
  node.is_tagged, node.expired, node.online, node.tags, user.name
- User: logs user.id, user.name, user.display, user.provider
- PreAuthKey: logs pak.id, pak.prefix (masked), pak.reusable,
  pak.ephemeral, pak.used, pak.is_tagged, pak.tags
- APIKey: logs api_key.id, api_key.prefix (masked), api_key.expiration

Security: PreAuthKey and APIKey only log masked prefixes, never full
keys or hashes. Uses zf.* constants for consistent field naming.
2026-02-06 07:40:29 +01:00
Kristoffer Dalby 58020696fe zlog: add utility package for safe and consistent logging
Add hscontrol/util/zlog package with:

- zf subpackage: field name constants for compile-time safety
- SafeHostinfo: wrapper that redacts device fingerprinting data
- SafeMapRequest: wrapper that redacts client endpoints

The zf (zerolog fields) subpackage provides short constant names
(e.g., zf.NodeID instead of inline "node.id" strings) ensuring
consistent field naming across all log statements.

Security considerations:
- SafeHostinfo never logs: OSVersion, DeviceModel, DistroName
- SafeMapRequest only logs endpoint counts, not actual IPs
2026-02-06 07:40:29 +01:00
Kristoffer Dalby e44b402fe4 integration: update TestSubnetRouteACL for filter merging and IPProto
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Update integration test expectations to match current policy behavior:

1. IPProto defaults include all four protocols (TCP, UDP, ICMPv4,
   ICMPv6) for port-range ACL rules, not just TCP and UDP.

2. Filter rules with identical SrcIPs and IPProto are now merged
   into a single rule with combined DstPorts, so the subnet router
   receives one filter rule instead of two.

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 835b7eb960 policy: autogroup:internet does not generate packet filters
According to Tailscale SaaS behavior, autogroup:internet is handled
by exit node routing via AllowedIPs, not by packet filtering. ACL
rules with autogroup:internet as destination should produce no
filter rules for any node.

Previously, Headscale expanded autogroup:internet to public CIDR
ranges and distributed filters to exit nodes (because 0.0.0.0/0
"covers" internet destinations). This was incorrect.

Add detection for AutoGroupInternet in filter compilation to skip
filter generation for this autogroup. Update test expectations
accordingly.
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 95b1fd636e policy: fix wildcard DstPorts format and proto:icmp handling
Fix two compatibility issues discovered in Tailscale SaaS testing:

1. Wildcard DstPorts format: Headscale was expanding wildcard
   destinations to CGNAT ranges (100.64.0.0/10, fd7a:115c:a1e0::/48)
   while Tailscale uses {IP: "*"} directly. Add detection for
   wildcard (Asterix) alias type in filter compilation to use the
   correct format.

2. proto:icmp handling: The "icmp" protocol name was returning both
   ICMPv4 (1) and ICMPv6 (58), but Tailscale only returns ICMPv4.
   Users should use "ipv6-icmp" or protocol number 58 explicitly
   for IPv6 ICMP.

Update all test expectations accordingly. This significantly reduces
test file line count by replacing duplicated CGNAT range patterns
with single wildcard entries.
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 834ac27779 policy/v2: add subnet routes and exit node compatibility tests
Add comprehensive test file for validating Headscale's ACL engine
behavior for subnet routes and exit nodes against documented
Tailscale SaaS behavior.

Tests cover:
- Category A: Subnet route basics (wildcard includes routes, tag-based
  ACL excludes routes)
- Category B: Exit node behavior (exit routes not in SrcIPs)
- Category F: Filter placement rules (filters on destination nodes)
- Category G: Protocol and port restrictions
- Category R: Route coverage rules
- Category O: Overlapping routes
- Category H: Edge cases (wildcard formats, CGNAT handling)
- Category T: Tag resolution (tags resolve to node IPs only)
- Category I: IPv6 specific behavior

The tests document expected Tailscale SaaS behavior with TODOs marking
areas where Headscale currently differs. This provides a baseline for
compatibility improvements.
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 4a4032a4b0 changelog: document filter rule merging
Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 29aa08df0e policy: update test expectations for merged filter rules
Update test expectations across policy tests to expect merged
FilterRule entries instead of separate ones. Tests now expect:
- Single FilterRule with combined DstPorts for same source
- Reduced matcher counts for exit node tests

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 0b1727c337 policy: merge filter rules with identical SrcIPs and IPProto
Tailscale merges multiple ACL rules into fewer FilterRule entries
when they have identical SrcIPs and IPProto, combining their DstPorts
arrays. This change implements the same behavior in Headscale.

Add mergeFilterRules() which uses O(n) hash map lookup to merge rules
with identical keys. DstPorts are NOT deduplicated to match Tailscale
behavior.

Also fix DestsIsTheInternet() to handle merged filter rules where
TheInternet is combined with other destinations - now uses superset
check instead of equality check.

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 08fe2e4d6c policy: use CIDR format for autogroup:self destinations
Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby cb29cade46 docs: add compatibility test documentation
Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby f27298c759 changelog: document wildcard CGNAT range change
Add breaking change entry for the wildcard resolution change to use
CGNAT/ULA ranges instead of all IPs.
Updates #3036

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 8baa14ef4a policy: use CGNAT/ULA ranges for wildcard resolution
Change Asterix.Resolve() to use Tailscale's CGNAT range (100.64.0.0/10)
and ULA range (fd7a:115c:a1e0::/48) instead of all IPs (0.0.0.0/0 and
::/0).
This better matches Tailscale's security model where wildcard (*) means
"any node in the tailnet" rather than literally "any IP address on the
internet".
Updates #3036

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby ebdbe03639 policy: validate autogroup:self sources in ACL rules
Tailscale validates that autogroup:self destinations in ACL rules can
only be used when ALL sources are users, groups, autogroup:member, or
wildcard (*). Previously, Headscale only performed this validation for
SSH rules.
Add validateACLSrcDstCombination() to enforce that tags, autogroup:tagged,
hosts, and raw IPs cannot be used as sources with autogroup:self
destinations. Invalid policies like `tag:client → autogroup:self:*` are
now rejected at validation time, matching Tailscale behavior.
Wildcard (*) is allowed because autogroup:self evaluation narrows it
per-node to only the node's own IPs.

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby f735502eae policy: add ICMP protocols to default and export constants
When ACL rules don't specify a protocol, Headscale now defaults to
[TCP, UDP, ICMP, ICMPv6] instead of just [TCP, UDP], matching
Tailscale's behavior.
Also export protocol number constants (ProtocolTCP, ProtocolUDP, etc.)
for use in external test packages, renaming the string protocol
constants to ProtoNameTCP, ProtoNameUDP, etc. to avoid conflicts.
This resolves 78 ICMP-related TODOs in the Tailscale compatibility
tests, reducing the total from 165 to 87.

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 53d17aa321 policy: add comprehensive Tailscale ACL compatibility tests
Add extensive test coverage verifying Headscale's ACL policy behavior
matches Tailscale's coordination server. Tests cover:
- Source/destination resolution for users, groups, tags, hosts, IPs
- autogroup:member, autogroup:tagged, autogroup:self behavior
- Filter rule deduplication and merging semantics
- Multi-rule interaction patterns
- Error case validation
Key behavioral differences documented:
- Headscale creates separate filter entries per ACL rule; Tailscale
  merges rules with identical sources
- Headscale deduplicates Dsts within a rule; Tailscale does not
- Headscale does not validate autogroup:self source restrictions for
  ACL rules (only SSH rules); Tailscale rejects invalid sources
Tests are based on real Tailscale coordination server responses
captured from a test environment with 5 nodes (1 user-owned, 4 tagged).

Updates #3036
2026-02-05 19:29:16 +01:00
Kristoffer Dalby 14f833bdb9 policy: fix autogroup:self handling for tagged nodes
Skip autogroup:self destination processing for tagged nodes since they
can never match autogroup:self (which only applies to user-owned nodes).
Also reorder the IsTagged() check to short-circuit before accessing
User() to avoid potential nil pointer access on tagged nodes.

Updates #3036
2026-02-05 19:29:16 +01:00
Florian Preinstorfer 9e50071df9 Link Fosdem 2026 talk
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
2026-02-05 08:01:02 +01:00
Florian Preinstorfer c907b0d323 Fix version in mkdocs 2026-02-05 08:01:02 +01:00
Kristoffer Dalby 97fa117c48 changelog: set 0.28 date
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-02-04 21:26:22 +01:00
Kristoffer Dalby b5329ff0f3 flake.lock: update nixpkgs to 2026-02-03
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2026-02-04 20:18:46 +01:00
Kristoffer Dalby eac8a57bce flake.nix: update hashes for dependency changes
Update vendorHash for headscale after Go module dependency updates.
Update grpc-gateway from v2.27.4 to v2.27.7 with new source and
vendor hashes.
2026-02-04 20:18:46 +01:00
Kristoffer Dalby 44af046196 all: update Go module dependencies
Update all direct and indirect Go module dependencies to their latest
compatible versions.

Notable direct dependency updates:
- tailscale.com v1.94.0 → v1.94.1
- github.com/coreos/go-oidc/v3 v3.16.0 → v3.17.0
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 → v2.27.7
- github.com/puzpuzpuz/xsync/v4 v4.3.0 → v4.4.0
- golang.org/x/crypto v0.46.0 → v0.47.0
- golang.org/x/net v0.48.0 → v0.49.0
- google.golang.org/genproto updated to 2025-02-03

Notable indirect dependency updates:
- AWS SDK v2 group updated
- OpenTelemetry v1.39.0 → v1.40.0
- github.com/jackc/pgx/v5 v5.7.6 → v5.8.0
- github.com/gaissmai/bart v0.18.0 → v0.26.1

Add lockstep comment for gvisor.dev/gvisor noting it must be updated
together with tailscale.com, similar to the existing modernc.org/sqlite
comment.
2026-02-04 20:18:46 +01:00
Kristoffer Dalby 4a744f423b changelog: change api key format
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-02-04 20:18:46 +01:00
Kristoffer Dalby ca75e096e6 integration: add test for tagged→user-owned conversion panic
Add TestTagsAuthKeyConvertToUserViaCLIRegister that reproduces the
exact panic from #3038: register a node with a tags-only PreAuthKey
(no user), force reauth with empty tags, then register via CLI with
a user. The mapper panics on node.Owner().Model().ID when User is nil.

The critical detail is using a tags-only PreAuthKey (User: nil). When
the key is created under a user, the node inherits the User pointer
from createAndSaveNewNode and the bug is masked.

Also add Owner() validity assertions to the existing unit test
TestTaggedNodeWithoutUserToDifferentUser to catch the nil pointer
at the unit test level.

Updates #3038
2026-02-04 15:44:55 +01:00
Kristoffer Dalby ce7c256d1e state: set User pointer during tagged→user-owned conversion
processReauthTags sets UserID when converting a tagged node to
user-owned, but does not set the User pointer. When the node was
registered with a tags-only PreAuthKey (User: nil), the in-memory
NodeStore cache holds a node with User=nil. The mapper's
generateUserProfiles then calls node.Owner().Model().ID, which
dereferences the nil pointer and panics.

Set node.User alongside node.UserID in processReauthTags. Also add
defensive nil checks in generateUserProfiles to gracefully handle
nodes with invalid owners rather than panicking.

Fixes #3038
2026-02-04 15:44:55 +01:00
Kristoffer Dalby 4912ceaaf5 state: inline reauthExistingNode and convertTaggedNodeToUser
These were thin wrappers around applyAuthNodeUpdate that only added
logging. Move the logging into applyAuthNodeUpdate and call it directly
from HandleNodeFromAuthPath.

This simplifies the code structure without changing behavior.

Updates #3038
2026-02-04 15:44:55 +01:00
Kristoffer Dalby d7f7f2c85e state: validate tags before UpdateNode to ensure consistency
Move tag validation before the UpdateNode callback in applyAuthNodeUpdate.
Previously, tag validation happened inside the callback, and the error
check occurred after UpdateNode had already committed changes to the
NodeStore. This left the NodeStore in an inconsistent state when tags
were rejected.

Now validation happens first, and UpdateNode is only called when we know
the operation will succeed. This follows the principle that UpdateNode
should only be called when we have all information and are ready to commit.

Also extract validateRequestTags as a reusable function and use it in
createAndSaveNewNode to deduplicate the tag validation logic.

Updates #3038
Updates #3048
2026-02-04 15:44:55 +01:00
Kristoffer Dalby df184e5276 state: fix expiry handling during node tag conversion
Previously, expiry handling ran BEFORE processReauthTags(), using the
old tagged status to determine whether to set/clear expiry. This caused:

- Personal → Tagged: Expiry remained set (should be cleared to nil)
- Tagged → Personal: Expiry remained nil (should be set from client)

Move expiry handling after tag processing and handle all four transition
cases based on the new tagged status:

- Tagged → Personal: Set expiry from client request
- Personal → Tagged: Clear expiry (tagged nodes don't expire)
- Personal → Personal: Update expiry from client
- Tagged → Tagged: Keep existing nil expiry

Fixes #3048
2026-02-04 15:44:55 +01:00
Kristoffer Dalby 0630fd32e5 state: refactor HandleNodeFromAuthPath for clarity
Reorganize HandleNodeFromAuthPath (~300 lines) into a cleaner structure
with named conditions and extracted helper functions.

Changes:
- Add authNodeUpdateParams struct for shared update logic
- Extract applyAuthNodeUpdate for common reauth/convert operations
- Extract reauthExistingNode and convertTaggedNodeToUser handlers
- Extract createNewNodeFromAuth for new node creation
- Use named boolean conditions (nodeExistsForSameUser, existingNodeIsTagged,
  existingNodeOwnedByOtherUser) instead of compound if conditions
- Create logger with common fields (registration_id, user.name, machine.key,
  method) to reduce log statement verbosity

Updates #3038
2026-02-04 15:44:55 +01:00
Kristoffer Dalby 306aabbbce state: fix nil pointer panic when re-registering tagged node without user
When a node was registered with a tags-only PreAuthKey (no user
associated), the node had User=nil and UserID=nil. When attempting to
re-register this node to a different user via HandleNodeFromAuthPath,
two issues occurred:

1. The code called oldUser.Name() without checking if oldUser was valid,
   causing a nil pointer dereference panic.

2. The existing node lookup logic didn't find the tagged node because it
   searched by (machineKey, userID), but tagged nodes have no userID.
   This caused a new node to be created instead of updating the existing
   tagged node.

Fix this by restructuring HandleNodeFromAuthPath to:
1. First check if a node exists for the same user (existing behavior)
2. If not found, check if an existing TAGGED node exists with the same
   machine key (regardless of userID)
3. If a tagged node exists, UPDATE it to convert from tagged to
   user-owned (preserving the node ID)
4. Only create a new node if the existing node is user-owned by a
   different user

This ensures consistent behavior between:
- personal → tagged → personal (same node, same owner)
- tagged (no user) → personal (same node, new owner)

Add a test that reproduces the panic and conversion scenario by:
1. Creating a tags-only PreAuthKey (no user)
2. Registering a node with that key
3. Re-registering the same machine to a different user
4. Verifying the node ID stays the same (conversion, not creation)

Fixes #3038
2026-02-04 15:44:55 +01:00
Kristoffer Dalby a09b0d1d69 policy/v2: add Caller() to log statements in compileACLWithAutogroupSelf
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Both compileFilterRules and compileSSHPolicy include .Caller() on
their resolution error log statements, but compileACLWithAutogroupSelf
does not. Add .Caller() to the three log sites (source resolution
error, destination resolution error, nil destination) for consistent
debuggability across all compilation paths.

Updates #2990
2026-02-03 16:53:15 +01:00
Kristoffer Dalby 362696a5ef policy/v2: keep partial IPSet on SSH destination resolution errors
In compileSSHPolicy, when resolving other (non-autogroup:self)
destinations, the code discards the entire result on error via
`continue`. If a destination alias (e.g., a tag owned by a group
with a non-existent user) returns a partial IPSet alongside an
error, valid IPs are lost.

Both ACL compilation paths (compileFilterRules and
compileACLWithAutogroupSelf) already handle this correctly by
logging the error and using the IPSet if non-nil.

Remove the `continue` so the SSH path is consistent with the
ACL paths.

Fixes #2990
2026-02-03 16:53:15 +01:00
Kristoffer Dalby 1f32c8bf61 policy/v2: add IsTagged() guards to prevent panics on tagged nodes
Three related issues where User().ID() is called on potentially tagged
nodes without first checking IsTagged():

1. compileACLWithAutogroupSelf: the autogroup:self block at line 166
   lacks the !node.IsTagged() guard that compileSSHPolicy already has.
   If a tagged node is the compilation target, node.User().ID() may
   panic. Tagged nodes should never participate in autogroup:self.

2. compileSSHPolicy: the IsTagged() check is on the right side of &&,
   so n.User().ID() evaluates first and may panic before short-circuit
   can prevent it. Swap to !n.IsTagged() && n.User().ID() == ... to
   match the already-correct order in compileACLWithAutogroupSelf.

3. invalidateAutogroupSelfCache: calls User().ID() at ~10 sites
   without IsTagged() guards. Tagged nodes don't participate in
   autogroup:self, so they should be skipped when collecting affected
   users and during cache lookup. Tag status transitions are handled
   by using the non-tagged version's user ID.

Fixes #2990
2026-02-03 16:53:15 +01:00
Kristoffer Dalby fb137a8fe3 policy/v2: use partial IPSet on group resolution errors in autogroup:self path
In compileACLWithAutogroupSelf, when a group contains a non-existent
user, Group.Resolve() returns a partial IPSet (with IPs from valid
users) alongside an error. The code was discarding the entire result
via `continue`, losing valid IPs. The non-autogroup-self path
(compileFilterRules) already handles this correctly by logging the
error and using the IPSet if non-empty.

Remove the `continue` on error for both source and destination
resolution, matching the existing behavior in compileFilterRules.
Also reorder the IsTagged check before User().ID() comparison
in the same-user node filter to prevent nil dereference on tagged
nodes that have no User set.

Fixes #2990
2026-02-03 16:53:15 +01:00
Kristoffer Dalby c2f28efbd7 policy/v2: add test for issue #2990 same-user tagged device
Add test reproducing the exact scenario from issue #2990 where:
- One user (user1) in group:admin
- node1: user device (not tagged)
- node2: tagged with tag:admin, same user

The test verifies that peer visibility and packet filters are correct.

Updates #2990
2026-02-03 16:53:15 +01:00
Kristoffer Dalby 11f0d4cfdd policy/v2: include nodes with empty filters in BuildPeerMap
Previously, nodes with empty filter rules (e.g., tagged servers that are
only destinations, never sources) were skipped entirely in BuildPeerMap.
This could cause visibility issues when using autogroup:self with
multiple user groups.

Remove the len(filter) == 0 skip condition so all nodes are included in
nodeMatchers. Empty filters result in empty matchers where CanAccess()
returns false, but the node still needs to be in the map so symmetric
visibility works correctly: if node A can access node B, both should see
each other regardless of B's filter rules.

Add comprehensive tests for:
- Multi-group scenarios where autogroup:self is used by privileged users
- Nodes with empty filters remaining visible to authorized peers
- Combined access rules (autogroup:self + tags in same rule)

Updates #2990
2026-02-03 16:53:15 +01:00
Florian Preinstorfer 5d300273dc Add a tags page and describe a few common operations
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2026-01-28 15:52:57 +01:00
Florian Preinstorfer 7f003ecaff Add a page to describe supported registration methods 2026-01-28 15:52:57 +01:00
Florian Preinstorfer 2695d1527e Use registration key instead of machine key 2026-01-28 15:52:57 +01:00
Florian Preinstorfer d32f6707f7 Add missing words 2026-01-28 15:52:57 +01:00
Florian Preinstorfer 89e436f0e6 Bump year/version for mkdocs 2026-01-28 15:52:57 +01:00
Kristoffer Dalby 46daa659e2 state: omit AuthKeyID/AuthKey in node Updates to prevent FK errors
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
When a PreAuthKey is deleted, the database correctly sets auth_key_id
to NULL on referencing nodes via ON DELETE SET NULL. However, the
NodeStore (in-memory cache) retains the old AuthKeyID value.

When nodes send MapRequests (e.g., after tailscaled restart), GORM's
Updates() tries to persist the stale AuthKeyID, causing a foreign key
constraint error when trying to reference a deleted PreAuthKey.

Fix this by adding AuthKeyID and AuthKey to the Omit() call in all
three places where nodes are updated via GORM's Updates():
- persistNodeToDB (MapRequest processing)
- HandleNodeFromAuthPath (re-auth via web/OIDC)
- HandleNodeFromPreAuthKey (re-registration with preauth key)

This tells GORM to never touch the auth_key_id column or AuthKey
association during node updates, letting the database handle the
foreign key relationship correctly.

Added TestDeletedPreAuthKeyNotRecreatedOnNodeUpdate to verify that
deleted PreAuthKeys are not recreated when nodes send MapRequests.
2026-01-26 12:12:11 +00:00
Florian Preinstorfer 49b70db7f2 Conversion from personal to tagged node is reversible
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2026-01-24 17:18:59 +01:00
Florian Preinstorfer 04b4071888 Fix node expiration success message
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
A node is expired when the requested expiration is either now or in the
past.
2026-01-24 15:18:12 +01:00
Florian Preinstorfer ee127edbf7 Remove trace log for preauthkeys create
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This always prints a TRC message on `preauthkeys create`. Since we don't
print anything for `apikeys create` either we might as well remove it.
2026-01-23 08:40:09 +01:00
Kristoffer Dalby 606e5f68a0 changelog: fixups for 0.28.0-beta.2
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2026-01-22 08:33:41 +00:00
Kristoffer Dalby a04b21abc6 gen: regenerate protobuf and type views
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Regenerated with updated grpc-gateway and tailscale dependencies.
2026-01-21 19:17:10 +00:00
Kristoffer Dalby 92caadcee6 nix: update vendor hash for Go dependencies 2026-01-21 19:17:10 +00:00
Kristoffer Dalby aa29fd95a3 derp: migrate to derpserver package API
tailscale.com v1.94.0 moved derp.Server to the derpserver subpackage.
Update imports and type references accordingly.
2026-01-21 19:17:10 +00:00
Kristoffer Dalby 0565e01c2f go.mod: update dependencies
Notable updates:
- tailscale.com v1.86.5 -> v1.94.0
- modernc.org/sqlite v1.39.1 -> v1.44.3
- modernc.org/libc v1.66.10 -> v1.67.6
- google.golang.org/grpc v1.75.1 -> v1.78.0
2026-01-21 19:17:10 +00:00
Kristoffer Dalby aee1d2a640 nix: fix deprecated attributes and update dev tools
- Fix deprecated flake output attributes (overlay -> overlays.default,
  devShell -> devShells.default, defaultPackage -> packages.default)
- Use stdenv.hostPlatform.system instead of deprecated prev.system
- Update grpc-gateway 2.24.0 -> 2.27.4
- Update protobuf-language-server
- Update nixpkgs
2026-01-21 19:17:10 +00:00
Kristoffer Dalby ee303186b3 docs: add changelog for SSH policy changes
Document breaking changes:
- Wildcard (*) no longer supported as SSH destination
- SSH source/destination validation enforces Tailscale's security model

Fixes #3009
Fixes #3010
2026-01-21 17:01:30 +00:00
Kristoffer Dalby e9a94f00a9 integration: update SSH tests for validation rules
Update integration tests to use valid SSH patterns:

- TestSSHOneUserToAll: use autogroup:member and autogroup:tagged
  instead of wildcard destination
- TestSSHMultipleUsersAllToAll: use autogroup:self instead of
  username destinations for group-to-user SSH access

Updates #3009
Updates #3010
2026-01-21 17:01:30 +00:00
Kristoffer Dalby d40203e153 policy: update tests for SSH validation rules
Update unit tests to use valid SSH patterns that conform to Tailscale's
security model:

- Change group->user destinations to group->tag
- Change tag->user destinations to tag->tag
- Update expected error messages for new validation format
- Add proper tagged/untagged node setup in filter tests

Updates #3009
Updates #3010
2026-01-21 17:01:30 +00:00
Kristoffer Dalby 5688c201e9 policy/v2: validate SSH source/destination combinations
Add validation for SSH source/destination combinations that enforces
Tailscale's security model:

- Tags/autogroup:tagged cannot SSH to user-owned devices
- autogroup:self destination requires source to contain only users/groups
- Username destinations require source to be that same single user only
- Wildcard (*) is no longer supported as SSH destination; use
  autogroup:member or autogroup:tagged instead

The validateSSHSrcDstCombination() function is called during policy
validation to reject invalid configurations at load time.

Fixes #3009
Fixes #3010
2026-01-21 17:01:30 +00:00
Shourya Gautam 4e1834adaf db: use PolicyManager for RequestTags migration
Refactor the RequestTags migration (202601121700-migrate-hostinfo-request-tags)
to use PolicyManager.NodeCanHaveTag() instead of reimplementing tag validation.

Changes:
- NewHeadscaleDatabase now accepts *types.Config to allow migrations
  access to policy configuration
- Add loadPolicyBytes helper to load policy from file or DB based on config
- Add standalone GetPolicy(tx *gorm.DB) for use during migrations
- Replace custom tag validation logic with PolicyManager

Benefits:
- Full HuJSON parsing support (not just JSON)
- Proper group expansion via PolicyManager
- Support for nested tags and autogroups
- Works with both file and database policy modes
- Single source of truth for tag validation


Co-Authored-By: Shourya Gautam <shouryamgautam@gmail.com>
2026-01-21 15:10:29 +01:00
Kristoffer Dalby 22afb2c61b policy: fix asymmetric peer visibility with autogroup:self
When autogroup:self was combined with other ACL rules (e.g., group:admin
-> *:*), tagged nodes became invisible to users who should have access.

The BuildPeerMap function had two code paths:
- Global filter path: used symmetric OR logic (if either can access, both
  see each other)
- Autogroup:self path: used asymmetric logic (only add peer if that
  specific direction has access)

This caused problems with one-way rules like admin -> tagged-server. The
admin could access the server, but since the server couldn't access the
admin, neither was added to the other's peer list.

Fix by using symmetric visibility in the autogroup:self path, matching
the global filter path behavior: if either node can access the other,
both should see each other as peers.

Credit: vdovhanych <vdovhanych@users.noreply.github.com>

Fixes #2990
2026-01-21 14:35:16 +01:00
Kristoffer Dalby b3c4d0ec81 integration: add tests for API key expire/delete by ID
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Extend TestApiKeyCommand to test the new --id flag for expire and
delete commands, verifying that API keys can be managed by their
database ID in addition to the existing --prefix method.

Updates #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby b82c9c9c0e docs: add changelog entry for API key expire/delete by ID
Fixes #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby e0bae9b769 cli: add --id flag to API key expire/delete commands
Add --id flag as an alternative to --prefix for expiring and
deleting API keys. This allows users to use the ID shown in
'headscale apikeys list' output, which is more convenient than
the prefix.

Either --id or --prefix must be provided; both flags are optional
but at least one is required.

Updates #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby a194712c34 grpc: support expire/delete API keys by ID
Update ExpireApiKey and DeleteApiKey handlers to accept either ID or
prefix for identifying the API key. Returns InvalidArgument error if
neither or both are provided.

Add tests for:
- Expire by ID
- Expire by prefix (backwards compatibility)
- Delete by ID
- Delete by prefix (backwards compatibility)
- Error when neither ID nor prefix provided
- Error when both ID and prefix provided

Updates #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby 8776745428 gen: regenerate protobuf code
Updates #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby b01eda721c proto: add id field to API key expire/delete requests
Add id field to ExpireApiKeyRequest and DeleteApiKeyRequest messages.
This allows API keys to be expired or deleted by their database ID
in addition to the existing prefix-based lookup.

Updates #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby 42bd9cd058 state: add GetAPIKeyByID method
Add GetAPIKeyByID method to the state layer, delegating to the existing
database layer function. This enables API key lookup by ID in addition
to the existing prefix-based lookup.

Updates #2986
2026-01-20 17:13:38 +01:00
Kristoffer Dalby 515a22e696 go.mod: remove gopkg.in/check.v1 dependency
Remove gopkg.in/check.v1 as a direct dependency now that all tests
have been migrated to testify.
2026-01-20 15:41:33 +01:00
Kristoffer Dalby 6654142fbe cmd/headscale: migrate tests from check.v1 to testify
Convert config loading tests from gopkg.in/check.v1 Suite-based testing
to standard Go tests with testify assert/require.

Changes:
- Remove Suite boilerplate (Test, Suite type, SetUpSuite, TearDownSuite)
- Convert TestConfigFileLoading and TestConfigLoading to standalone tests
- Replace check assertions with testify equivalents
2026-01-20 15:41:33 +01:00
Kristoffer Dalby 424e26d636 db: migrate tests from check.v1 to testify
Migrate all database tests from gopkg.in/check.v1 Suite-based testing
to standard Go tests with testify assert/require.

Changes:
- Remove empty Suite files (hscontrol/suite_test.go, hscontrol/mapper/suite_test.go)
- Convert hscontrol/db/suite_test.go to modern helpers only
- Convert 6 Suite test methods in node_test.go to standalone tests
- Convert 5 Suite test methods in api_key_test.go to standalone tests
- Fix stale global variable reference in db_test.go

The legacy TestListPeers Suite method was renamed to TestListPeersManyNodes
to avoid conflict with the existing modern TestListPeers function, as they
test different aspects (basic peer listing vs ID filtering).
2026-01-20 15:41:33 +01:00
Kristoffer Dalby d9cbb96603 state: add unit test for DeleteUser change signal
Updates #2967
2026-01-20 15:41:19 +01:00
Kristoffer Dalby c1cfb59b91 ci: add ACL unknown user tests to integration workflow
Updates #2967
2026-01-20 15:41:19 +01:00
Kristoffer Dalby 4be13baf3f state: update policy manager when deleting users
Make DeleteUser call updatePolicyManagerUsers() to refresh the policy
manager's cached user list after user deletion. This ensures consistency
with CreateUser, UpdateUser, and RenameUser which all update the policy
manager.

Previously, DeleteUser only removed the user from the database without
updating the policy manager. This could leave stale user references in
the cached user list, potentially causing issues when policy is
re-evaluated.

The gRPC handler now uses the change returned from DeleteUser instead of
manually constructing change.UserRemoved().

Fixes #2967
2026-01-20 15:41:19 +01:00
Kristoffer Dalby 98c0817b95 integration: add tests for ACL group with deleted/unknown users
Add DeleteUser method to ControlServer interface and implement it in
HeadscaleInContainer to enable testing user deletion scenarios.

Add two integration tests for issue #2967:
- TestACLGroupWithUnknownUser: tests that valid users can communicate
  when a group references a non-existent user
- TestACLGroupAfterUserDeletion: tests connectivity after deleting a
  user that was referenced in an ACL group

These tests currently pass but don't fully reproduce the reported issue
where deleted users break connectivity for the entire group.

Updates #2967
2026-01-20 15:41:19 +01:00
Kristoffer Dalby 951fd5a8e7 cli: show Owner column in preauthkeys list
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Replace the Tags column with an Owner column that displays:
- Tags (newline-separated) if the key has ACL tags
- User name if the key is associated with a user
- Dash (-) if neither is present

This aligns the CLI output with the tags-as-identity model where
preauthkeys can be created with either tags or user ownership.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby b8f3e09046 integration: fix tags-only auth key tests
- Rename TestTagsAuthKeyWithoutUserIgnoresAdvertisedTags to
  TestTagsAuthKeyWithoutUserRejectsAdvertisedTags to reflect actual
  behavior (PreAuthKey registrations reject advertised tags)
- Fix TestTagsAuthKeyWithoutUserInheritsTags to use ListNodes() without
  user filter since tags-only nodes don't have a user association

Updates #2977
2026-01-20 12:53:20 +01:00
Kristoffer Dalby 4ab06930a2 hscontrol: handle tags-only PreAuthKeys in registration
HandleNodeFromPreAuthKey assumed pak.User was always set, but
tags-only PreAuthKeys have nil User. This caused nil pointer
dereference when registering nodes with tags-only keys.

Also updates integration tests to use GetTags() instead of the
removed GetValidTags() method.

Updates #2977
2026-01-20 12:53:20 +01:00
Kristoffer Dalby 165c5f0491 cli: fix preauthkeys expire/delete argument validation
The Args function incorrectly required positional arguments but
the commands use --id flag. Move validation into Run function.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby c8c3c9d4a0 hscontrol: allow CreatePreAuthKey without user when tags provided
Handle case where user is 0 in gRPC layer to support tags-only
auth keys.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby 4dd1b49a35 integration: update CLI tests for ID-based preauthkey commands
Remove --user flag from list commands.
Change expire command to use --id flag instead of --user and key.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby db6882b5f5 integration: update DeleteAuthKey to use ID 2026-01-20 12:53:20 +01:00
Kristoffer Dalby 1325fd8b27 cli,hscontrol: use ID-based preauthkey operations 2026-01-20 12:53:20 +01:00
Kristoffer Dalby 8631581852 gen: regenerate proto code 2026-01-20 12:53:20 +01:00
Kristoffer Dalby 1398d01bd8 proto: change preauthkey API to ID-based operations
Remove user parameter from ListPreAuthKeys.
Change ExpirePreAuthKey and DeletePreAuthKey to use key ID.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby 00da5361b3 integration: test tags-only auth key behavior
Add tests for auth keys without user ownership to verify tags from
key are used regardless of --advertise-tags flag.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby 740d2b5a2c integration: support auth keys without user
Add AuthKeyOptions to create auth keys owned by tags only.
2026-01-20 12:53:20 +01:00
Kristoffer Dalby 3b4b9a4436 hscontrol: fix tag updates not propagating to node self view
When SetNodeTags changed a node's tags, the node's self view wasn't
updated. The bug manifested as: the first SetNodeTags call updates
the server but the client's self view doesn't update until a second
call with the same tag.

Root cause: Three issues combined to prevent self-updates:

1. SetNodeTags returned PolicyChange which doesn't set OriginNode,
   so the mapper's self-update check failed.

2. The Change.Merge function didn't preserve OriginNode, so when
   changes were batched together, OriginNode was lost.

3. generateMapResponse checked OriginNode only in buildFromChange(),
   but PolicyChange uses RequiresRuntimePeerComputation which
   bypasses that code path entirely and calls policyChangeResponse()
   instead.

The fix addresses all three:
- state.go: Set OriginNode on the returned change
- change.go: Preserve OriginNode (and TargetNode) during merge
- batcher.go: Pass isSelfUpdate to policyChangeResponse so the
  origin node gets both self info AND packet filters
- mapper.go: Add includeSelf parameter to policyChangeResponse

Fixes #2978
2026-01-20 10:13:47 +01:00
Kristoffer Dalby 1b6db34b93 integration/tags: add self-tag validation to existing tests
Update 8 tests that involve admin tag assignment via SetNodeTags()
to verify both server-side state and node self view updates:

- TestTagsAuthKeyWithTagAdminOverrideReauthPreserves
- TestTagsAuthKeyWithTagCLICannotModifyAdminTags
- TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset
- TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise
- TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag
- TestTagsUserLoginCLINoOpAfterAdminAssignment
- TestTagsUserLoginCLICannotRemoveAdminTags
- TestTagsAdminAPICanSetUnownedTag

Each test now validates that tag updates propagate to the node's
own self view using assertNodeSelfHasTagsWithCollect, addressing
the issue #2978 scenario where tag changes were observed to
propagate to peers but not to the node itself.

Updates #2978
2026-01-20 10:13:47 +01:00
Kristoffer Dalby 07a4b1b1fd integration/tags: add dedicated issue #2978 reproduction test
Add TestTagsIssue2978ReproTagReplacement that specifically tests the
scenario from issue #2978:
- Register node with tag:foo via web auth with --advertise-tags
- Admin changes tag to tag:bar via SetNodeTags
- Verify client's self view updates (not just server-side)

The test performs multiple tag replacements with timing checks to
verify whether tag updates propagate to the node's self view after
the first call (fixed behavior) or only after a redundant second
call (bug behavior).

Add helper functions for test validation:
- assertNodeSelfHasTagsWithCollect: validates client's status.Self.Tags
- assertNetmapSelfHasTagsWithCollect: validates client's netmap.SelfNode.Tags

Updates #2978
2026-01-20 10:13:47 +01:00
Kristoffer Dalby 2e180d2587 integration: add test for reauth tag removal
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Add TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags to validate that
nodes can be untagged via `tailscale up --advertise-tags= --force-reauth`.

The test verifies:
- Node starts with tags and is owned by tagged-devices
- After reauth with empty tags, all tags are removed
- Node ownership returns to the authenticating user

Updates #2979
2026-01-17 10:13:24 +01:00
Kristoffer Dalby 0451dd4718 state: allow untagging nodes via reauth with empty RequestTags
When a node re-authenticates via OIDC/web auth with empty RequestTags
(from `tailscale up --advertise-tags= --force-reauth`), remove all tags
and return ownership to the authenticating user.

This allows nodes to transition from any tagged state (including nodes
originally registered with a tagged pre-auth key) back to user-owned.

Fixes #2979
2026-01-17 10:13:24 +01:00
Kristoffer Dalby a6696582a4 util/dns: fix variable redeclaration in ValidateDNSName 2026-01-17 10:13:24 +01:00
Kristoffer Dalby 00f22a8443 state: disable key expiry for nodes with approved advertise-tags
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Extends #2971 fix to also cover nodes that authenticate as users but
become tagged immediately via --advertise-tags. When RequestTags are
approved by policy, the node's expiry is now disabled, consistent with
nodes registered via tagged PreAuthKeys.
2026-01-16 17:05:59 +01:00
Kristoffer Dalby 1d9900273e state: disable key expiry for tagged nodes
Nodes registered with tagged PreAuthKeys now have key expiry disabled,
matching Tailscale's behavior. User-owned nodes continue to use the
client-requested expiry.

On re-authentication, tagged nodes preserve their disabled expiry while
user-owned nodes can update their expiry from the client request.

Fixes #2971
2026-01-16 17:05:59 +01:00
Florian Preinstorfer 18e13f6ffa Link to headscale.net for docs 2026-01-16 14:54:04 +01:00
Florian Preinstorfer a445278f76 Mention tags on the features page 2026-01-16 14:54:04 +01:00
Florian Preinstorfer 8387c9cd82 Fix ownership description for auto approved routers/exits
Just the tags tag:router and tag:exit are owned by alice. Upon join,
those nodes will have their ownership transferred from alice to the
system user "tagged-devices".
2026-01-16 14:54:04 +01:00
Florian Preinstorfer 25a7434830 Bump version in mkdocs 2026-01-16 14:54:04 +01:00
Florian Preinstorfer 183a38715c Fix list-routes examples 2026-01-16 14:54:04 +01:00
Florian Preinstorfer 99d35fbbbc Document oidc.email_verification_required 2026-01-16 14:54:04 +01:00
Florian Preinstorfer d50108c722 Changelog: mark oidc.email_verified_required as breaking
Headscale is now stricter and this is a breaking change if authorization
filters are used and at least one user has an unverified email address.
2026-01-16 14:54:04 +01:00
Florian Preinstorfer 6d21a4a3fe Document /version in the API docs 2026-01-16 14:54:04 +01:00
Florian Preinstorfer 7d81dca9aa Document how to disable the metrics interfaces 2026-01-16 14:54:04 +01:00
Kristoffer Dalby 3689f05407 types: use Username() in User.Proto() when Name is empty
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
User.Proto() was returning u.Name directly, which is empty for OIDC
users who have their identifier in the Email field instead. This caused
"headscale nodes list" to show empty user names for OIDC-authenticated
nodes.

Only fall back to Username() when Name is empty, which provides a
display-friendly identifier (Email > ProviderIdentifier > ID). This
ensures OIDC users display their email while CLI users retain their
original Name.

Fixes #2972
2026-01-14 13:16:51 +01:00
chen bb30208f97 Add headscale-piying web UI to docs 2026-01-14 12:56:05 +01:00
Florian Preinstorfer c3e2e57f8e Clarify autogroup:member
Fixes: #3007
2026-01-14 12:44:18 +01:00
Kristoffer Dalby e43f19df79 CHANGELOG: add breaking change for Node API simplification
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-01-14 09:32:46 +01:00
Kristoffer Dalby 0516c0ec37 gen: regenerate protobuf code 2026-01-14 09:32:46 +01:00
Kristoffer Dalby eec54cbbf3 api/v1: replace ForcedTags/InvalidTags/ValidTags with Tags
Simplifies the API by exposing a single tags field instead of three
separate fields for forced, invalid, and valid tags. The distinction
between these was an internal implementation detail that should not
be exposed in the public API.

Marks fields 18-20 as reserved to prevent field number reuse.
2026-01-14 09:32:46 +01:00
Kristoffer Dalby 72fcb93ef3 cli: ensure tagged-devices is included in profile list (#2991)
Close inactive issues / close-issues (push) Has been cancelled
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2026-01-09 16:31:23 +01:00
Kristoffer Dalby f5c779626a nix: use testers.nixosTest instead of nixosTest
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
nixosTest was renamed to testers.nixosTest in nixpkgs.
2026-01-09 12:57:32 +01:00
Kristoffer Dalby d227b3a135 docs: update integration testing docs for concurrent execution
Update documentation to reflect the new concurrent test execution
capabilities and add guidance on run ID isolation.

AGENTS.md:
- Add examples for running multiple tests concurrently
- Document run ID format and container naming conventions
- Update "Critical Notes" to explain isolation mechanisms

.claude/agents/headscale-integration-tester.md:
- Add "Concurrent Execution and Run ID Isolation" section
- Document forbidden and safe operations for cleanup
- Add "Agent Session Isolation Rules" for multi-agent environments
- Add 6th core responsibility about concurrent execution awareness
- Add ISOLATION PRINCIPLE to critical principles
- Update pre-test cleanup documentation
2026-01-09 12:34:16 +01:00
Kristoffer Dalby 0bcfdc29ad cmd/hi: enable concurrent test execution
Remove the concurrent test prevention logic and update cleanup to use
run ID-based isolation, allowing multiple tests to run simultaneously.

Changes:
- cleanup: Add killTestContainersByRunID() to clean only containers
  belonging to a specific run, add cleanupStaleTestContainers() to
  remove only stopped/exited containers without affecting running tests
- docker: Remove RunningTestInfo, checkForRunningTests(), and related
  error types, update cleanupAfterTest() to use run ID-based cleanup
- run: Remove Force flag and concurrent test prevention check

The test runner now:
- Allows multiple concurrent test runs on the same Docker daemon
- Cleans only stale containers before tests (not running ones)
- Cleans only containers with matching run ID after tests
- Prints run ID and monitoring info for operator visibility
2026-01-09 12:34:16 +01:00
Kristoffer Dalby 87c230d251 integration: add run ID isolation for concurrent test execution
Add run ID-based isolation to container naming and network setup to
enable multiple integration tests to run concurrently on the same
Docker daemon without conflicts.

Changes:
- hsic: Add run ID prefix to headscale container names and use dynamic
  port allocation for metrics endpoint (port 0 lets kernel assign)
- tsic: Add run ID prefix to tailscale container names
- dsic: Add run ID prefix to DERP container names
- scenario: Use run ID-aware test suite container name for network setup

Container naming now follows: {type}-{runIDShort}-{identifier}-{hash}
Example: ts-mdjtzx-1-74-fgdyls, hs-mdjtzx-pingallbyip-abc123

The run ID is obtained from HEADSCALE_INTEGRATION_RUN_ID environment
variable via dockertestutil.GetIntegrationRunID().
2026-01-09 12:34:16 +01:00
github-actions[bot] 84c092a9f9 flake.lock: Update
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/35f5903' (2025-10-15)
  → 'github:NixOS/nixpkgs/3edc4a3' (2025-12-27)
2025-12-28 08:25:28 +01:00
Florian Preinstorfer 9146140217 Add headscale-operator
Ref: #1523
2025-12-23 20:22:57 +01:00
Kristoffer Dalby 5103b35f3c sqliteconfig: add config opt for tx locking
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-22 14:01:40 +01:00
Justin Angel 7be20912f5 oidc: make email verification configurable
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-12-18 11:42:32 +00:00
Kristoffer Dalby e8753619de capver: generate
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-12-18 10:02:23 +01:00
Kristoffer Dalby 251e16d772 tools/capver: regenerate from docker tags
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-12-18 10:02:23 +01:00
Kristoffer Dalby 3f0bfe28cc changelog: prepare for 0.28.0 beta
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-12-17 15:15:43 +01:00
Kristoffer Dalby 82d4275c3b mapper: correct some variable names missed from change
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 13:19:26 +01:00
Kristoffer Dalby f3767dddf8 batcher: ensure removal from batcher
Fixes #2924

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 13:19:26 +01:00
Florian Preinstorfer 5c6cd62df1 Legacy preauthkeys must be used as-is 2025-12-17 13:05:08 +01:00
Shourya Gautam 56bec66a44 app: only wire up debug server if set
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Fixes #2871

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 12:32:04 +01:00
Kristoffer Dalby f0e464dc36 policy: add test to confirm group cant approve tag
Confirms #2891 is implemented correctly.

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 09:32:05 +01:00
Kristoffer Dalby 2c3c943acf .github/workflows: split long TestAutoApproveMultiNetwork into multiple jobs
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 09:32:05 +01:00
Kristoffer Dalby a50bd13930 integration: prepare AutoApprove test for new tags
Validates #2891

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 09:32:05 +01:00
Kristoffer Dalby 5655ef86d7 AGENTS: golangci-lint from main, no "full matrix"
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-17 09:32:05 +01:00
Kristoffer Dalby 21ba197d06 integration: make entrypoint override more robust
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby 9d77207ed8 policy: clarify usernam resolve comment
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby cf1ad47b42 flake: remove hi from shell
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby a288f04a1a Dockerfile: align packages
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby 5767ca5085 change: smarter change notifications
This commit replaces the ChangeSet with a simpler bool based
change model that can be directly used in the map builder to
build the appropriate map response based on the change that
has occured. Previously, we fell back to sending full maps
for a lot of changes as that was consider "the safe" thing to
do to ensure no updates were missed.

This was slightly problematic as a node that already has a list
of peers will only do full replacement of the peers if the list
is non-empty, meaning that it was not possible to remove all
nodes (if for example policy changed).

Now we will keep track of last seen nodes, so we can send remove
ids, but also we are much smarter on how we send smaller, partial
maps when needed.

Fixes #2389

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby f67ed36fe2 integration: replicate tag propagation issue
This commit adds tests to validate that there are
issues with how we propagate tag changes in the system.

This replicates #2389

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby 506bd8c8eb policy: more accurate node change
This commit changes so that node changes to the policy is
calculated if any of the nodes has changed in a way that might
affect the policy.

Previously we just checked if the number of nodes had changed,
which meant that if a node was added and removed, we would be
in a bad state.

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby daf9f36c78 editorconfig: add basic editor config
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-12-16 10:12:36 +01:00
Kristoffer Dalby 616c0e895d batcher: fix closed panic
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-12-15 16:28:27 +01:00
Kristoffer Dalby c4600346f9 .github/workflows: prebuilt integration test artifacts (#2954)
Close inactive issues / close-issues (push) Has been cancelled
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
This PR restructures the integration tests and prebuilds all common assets used in all tests:

Headscale and Tailscale HEAD image
hi binary that is used to run tests
go cache is warmed up for compilation of the test
This essentially means we spend 6-10 minutes building assets before any tests starts, when that is done, all tests can just sprint through.

It looks like we are saving 3-9 minutes per test, and since we are limited to running max 20 concurrent tests across the repo, that means we had a lot of double work.

There is currently 113 checks, so we have to do five runs of 20, and the saving should be quite noticeable! I think the "worst case" saving would be 20+min and "best case" probably towards an hour.
2025-12-12 23:01:52 +01:00
Kristoffer Dalby 642073f4b8 types: add option to disable taildrop, improve tests (#2955)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-12-12 11:35:16 +01:00
Kristoffer Dalby 87bd67318b golangci-lint: use forbidigo to block time.Sleep (#2946)
Close inactive issues / close-issues (push) Has been cancelled
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-12-10 16:45:59 +00:00
Kristoffer Dalby 0e1673041c all: remove deadcode (#2952)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-12-10 15:55:15 +01:00
Kristoffer Dalby f3f2d30004 cli: better formatting of lists (#2951) 2025-12-10 12:33:21 +01:00
Kristoffer Dalby c8376e44a2 mapper: move tail node conversion to node type (#2950) 2025-12-10 09:16:22 +01:00
Rogan Lynch 5d0a6ab0e9 fix: list-routes command now respects identifier filter with JSON output
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Fixes #2927

In v0.27.0, the list-routes command with -i flag and -o json output
was returning all nodes instead of just the specified node.

The issue was that JSON output was happening before the identifier
filtering logic. This change moves the JSON output to after both
the identifier filter and route existence filter are applied,
ensuring the correct filtered results are returned.

This restores the v0.26.1 behavior where:
  headscale nodes list-routes -i 12 -o json
correctly returns only node 12's route information.
2025-12-10 06:19:17 +01:00
Kristoffer Dalby 22ee2bfc9c tags: process tags on registration, simplify policy (#2931)
Close inactive issues / close-issues (push) Has been cancelled
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
This PR investigates, adds tests and aims to correctly implement Tailscale's model for how Tags should be accepted, assigned and used to identify nodes in the Tailscale access and ownership model.

When evaluating in Headscale's policy, Tags are now only checked against a nodes "tags" list, which defines the source of truth for all tags for a given node. This simplifies the code for dealing with tags greatly, and should help us have less access bugs related to nodes belonging to tags or users.

A node can either be owned by a user, or a tag.

Next, to ensure the tags list on the node is correctly implemented, we first add tests for every registration scenario and combination of user, pre auth key and pre auth key with tags with the same registration expectation as observed by trying them all with the Tailscale control server. This should ensure that we implement the correct behaviour and that it does not change or break over time.

Lastly, the missing parts of the auth has been added, or changed in the cases where it was wrong. This has in large parts allowed us to delete and simplify a lot of code.
Now, tags can only be changed when a node authenticates or if set via the CLI/API. Tags can only be fully overwritten/replaced and any use of either auth or CLI will replace the current set if different.

A user owned device can be converted to a tagged device, but it cannot be changed back. A tagged device can never remove the last tag either, it has to have a minimum of one.
2025-12-08 18:51:07 +01:00
Dusty Mabe 1f5df017a1 hscontrol: log acme/autocert errors (#2933) 2025-12-08 16:39:30 +00:00
Florian Preinstorfer bba91a89be Use lists for integration docs
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Refactor the tables in "Tools" and "WebUI" integration pages to lists.
Lists are easier to extend and contributions are easier to review.
2025-12-08 12:50:19 +01:00
Florian Preinstorfer 6359511a62 Use debian13 distroless images
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-12-07 20:58:29 +01:00
adinhodovic d2fcd5b95b docs(tools): Add tailscale-exporter
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
A Prometheus exporter for Tailscale and Headscale that provides tailnet-level metrics using the Tailscale/Headscale API.
2025-12-07 14:39:08 +01:00
Kristoffer Dalby 15c84b34e0 policy: allow tags to own tags (#2930)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-12-06 10:23:35 +01:00
Kristoffer Dalby eb788cd007 make tags first class node owner (#2885)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This PR changes tags to be something that exists on nodes in addition to users, to being its own thing. It is part of moving our tags support towards the correct tailscale compatible implementation.

There are probably rough edges in this PR, but the intention is to get it in, and then start fixing bugs from 0.28.0 milestone (long standing tags issue) to discover what works and what doesnt.

Updates #2417
Closes #2619
2025-12-02 12:01:25 +01:00
Kristoffer Dalby 705b239677 changelog: prep for 0.27.2 rc
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-12-02 12:01:02 +01:00
Kristoffer Dalby cb4d5b1906 hscontrol/oidc: fix ACL policy not applied to new OIDC nodes (#2890)
Fixes #2888
Fixes #2896
2025-12-02 12:01:02 +01:00
Vitalij Dovhanyc 0078eb7790 chore: fix filterHash to work with autogroup:self in the acls (#2882) 2025-12-02 12:01:02 +01:00
Kristoffer Dalby 3cf2d7195a auth: ensure machines are allowed in when pak change (#2917) 2025-12-02 12:01:02 +01:00
Kristoffer Dalby 16d811b306 cli: remove node move command (#2922)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-12-01 21:43:31 +01:00
Kristoffer Dalby eec196d200 modernize: run gopls modernize to bring up to 1.25 (#2920)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-12-01 19:40:25 +01:00
Kristoffer Dalby bfcd9d261d cmd/hi: reject if we are already running (#2919) 2025-12-01 19:40:08 +01:00
Florian Preinstorfer f00c412cde Move static doc assets into docs/assets
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-11-28 21:27:54 +01:00
Florian Preinstorfer 2010805712 Provide Headscale's favicon at its expected place
Assets need to reside within the docs/ directory for mkdocs to pick them
up.
2025-11-28 21:27:54 +01:00
Florian Preinstorfer c5133ee5d3 Fix trailing whitespace 2025-11-28 21:27:54 +01:00
Florian Preinstorfer 9c33cbfdc8 Exclude docs/ only for prettier pre-commit hook
Applying the the built-in hooks to docs/ seems to be fine.
2025-11-28 21:27:54 +01:00
Florian Preinstorfer 9b327f6b56 Update pre-commit-hooks 2025-11-28 21:27:54 +01:00
Kristoffer Dalby 9368fee1c5 generate: add new patches (#2921)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-11-28 17:00:52 +01:00
Kristoffer Dalby ed78bf4b98 cmd/hi: improve test cleanup to reduce CI disk usage (#2881) 2025-11-28 16:59:54 +01:00
Kristoffer Dalby db293e0698 hscontrol/state: make NodeStore batch configuration tunable (#2886) 2025-11-28 16:38:29 +01:00
pwuersch 9c4c017eac docs: Enable automatic theme switching
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: pwuersch <49908921+pwuersch@users.noreply.github.com>
2025-11-24 06:43:22 +01:00
János Benjamin Antal 14af9b3ab1 Add docs to manage headscale from another local user 2025-11-24 06:37:35 +01:00
Florian Preinstorfer 72d5fd04a7 Remove duplicated documentation and link to getting started instead
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-11-18 11:07:49 +01:00
Florian Preinstorfer e86d063056 Mention /health instead of /windows 2025-11-18 11:07:49 +01:00
Acha e0c9e18e22 Update OIDC documentation for allowed groups filter
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Clarify configuration for allowed groups filter with Microsoft Entra ID.
2025-11-15 17:44:00 +01:00
Florian Preinstorfer 21af106f68 Containers should be read-only
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This improves security and explicitly fails on startup when a user picks
the wrong directory to store its data.

- Run in read-only mode
- Make /var/run/headscale a read-write tmpfs
- Mount the config volume read-only
- Use the /health endpoint to check if Headscale is up
2025-11-14 14:51:27 +01:00
Kristoffer Dalby 7fb0f9a501 batcher: send endpoint and derp only updates. (#2856)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-11-13 20:38:49 +01:00
Kristoffer Dalby 4b25976288 db: add comment to always check errors in migration
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-13 09:46:40 -06:00
Kristoffer Dalby 1c146f70e9 db: remove _schema from migration tests
Previously we tested migrations on schemas and dumps
of old databases.

The problems with testing migrations against the schemas
is that the migration table is empty, so we try to run
migrations that are already ran on that schema, which might
blow up.

This commit removes the schema approach and just leaves all
the dumps, which include the migration table.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-13 09:46:40 -06:00
Florian Preinstorfer 249630bed8 Add API documentation
Document the API endpoint and the built-in swagger docs at /swagger. The
remote control docs are just a use case for gRPC - move it in the API
docs and update links to it.
2025-11-13 15:22:55 +01:00
Kristoffer Dalby 75247f82b8 hscontrol/db: add init schema, drop pre-0.25 support (#2883) 2025-11-13 04:44:10 -06:00
Tianon Gravi 665cc44094 Explicitly drop apt-get clean and use dist-clean
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
The former is a no-op in the base images (https://github.com/debuerreotype/debuerreotype/blob/45491f2c5c8ac76630e1e2d27503528ca29e1f6f/scripts/debuerreotype-minimizing-config#L87-L109), and `apt-get dist-clean` is a safer/better version of the `rm -rf /var/lib/apt/lists/*` that keeps the cryptographic bits that help prevent downgrade attacks.
2025-11-13 07:15:22 +01:00
Kristoffer Dalby 8394e7094a capver: update latest (#2774)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-11-12 20:26:54 +01:00
Kristoffer Dalby da9018a0eb types: make pre auth key use bcrypt (#2853)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-11-12 16:36:36 +01:00
Kristoffer Dalby e3ced80278 hscontrol: consolidate assets into single package
Move favicon.png, style.css, and headscale.svg to hscontrol/assets/
and create a single assets.go file with all embed directives.

Update hscontrol/handlers.go and hscontrol/templates/general.go to
use the centralized assets package.
2025-11-12 08:28:12 -06:00
Kristoffer Dalby 09c9762fe0 hscontrol: convert BlankHandler to use elem-go 2025-11-12 08:28:12 -06:00
Kristoffer Dalby 75e24de7bd flake: disable CGO in dev shell
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-12 08:28:12 -06:00
Kristoffer Dalby 2aa5b8b68d changelog: add entry for templates redesign 2025-11-12 08:28:12 -06:00
Kristoffer Dalby 4e77e910c5 hscontrol: use octal literal syntax in test 2025-11-12 08:28:12 -06:00
Kristoffer Dalby a496864762 hscontrol: add template HTML consistency test
Add test to validate HTML template output consistency across all
templates (OIDC callback, registration, Windows, Apple).

Verifies all templates produce valid HTML5 with:
- Proper DOCTYPE declaration
- HTML5 lang attribute
- UTF-8 charset
- Viewport meta tag
- Semantic HTML structure

Ensures template refactoring maintains standards compliance.
2025-11-12 08:28:12 -06:00
Kristoffer Dalby 3ed1067a95 hscontrol/templates: refactor to use CSS classes and embedded files
Refactor template system to use go:embed for external assets and
CSS classes for styling instead of inline styles:

- general.go: Add go:embed directives for style.css and headscale.svg,
  replace inline styles with CSS classes (H1, H2, H3, P, etc.),
  add mdTypesetBody wrapper with Material for MkDocs styling

- apple.go, oidc_callback.go, register_web.go, windows.go:
  Update to use new CSS-based helper functions (H1, H2, P, etc.)
  and mdTypesetBody for consistent layout

This separates content from presentation, making templates easier
to maintain and update. All styling is now centralized in style.css
with Material for MkDocs design system.
2025-11-12 08:28:12 -06:00
Kristoffer Dalby 285c4e46a9 hscontrol/templates: add Material for MkDocs design assets
Add design system assets for HTML templates:
- headscale.svg: Logo with optimized viewBox for proper alignment
- style.css: Material for MkDocs CSS variables and typography
- design.go: Design system constants for consistent styling

The logo viewBox is adjusted to 32.92 0 1247.08 640 to eliminate
whitespace from the original export and ensure left alignment with
text content.
2025-11-12 08:28:12 -06:00
Kristoffer Dalby 89285c317b templates: migrate OIDC callback to elem-go
Replace html/template with type-safe elem-go templating for OIDC
callback page. Improves consistency with other templates and provides
compile-time safety. All UI elements and styling preserved.
2025-11-12 08:28:12 -06:00
Kristoffer Dalby d14be8d43b nix: add NixOS module and tests (#2857) 2025-11-12 13:11:38 +00:00
Kristoffer Dalby 000d5c3b0c prettier: use standard config for all files including changelog (#2879) 2025-11-12 13:59:43 +01:00
Teej 218a8db1b9 add favicon to webpages (#2858)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Co-authored-by: TeejMcSteez <tjhall047@gmail.com>
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-11-12 03:46:57 +00:00
Kristoffer Dalby 1dcb04ce9b changelog: add changelog entry
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-11 17:27:00 -06:00
Andrey Bobelev 299cef4e99 fix: free ips from usedIps ipset on DeleteNode 2025-11-11 17:27:00 -06:00
Kristoffer Dalby 6d24afba1c add pre-commit hooks, move claude to agents. (#2877) 2025-11-11 20:35:23 +01:00
Kristoffer Dalby f658a8eacd mkdocs: 0.27.1
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-11 13:17:02 -06:00
Kristoffer Dalby 785168a7b8 changelog: prepare for 0.27.1
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-11 13:17:02 -06:00
Kristoffer Dalby 3bd4ecd9cd fix: preserve node expiry when tailscaled restarts
When tailscaled restarts, it sends RegisterRequest with Auth=nil and
Expiry=zero. Previously this was treated as a logout because
time.Time{}.Before(time.Now()) returns true.

Add early return in handleRegister() to detect this case and preserve
the existing node state without modification.

Fixes #2862
2025-11-11 12:47:48 -06:00
Kristoffer Dalby 3455d1cb59 hscontrol/db: fix RenameUser to use Updates()
RenameUser only modifies Name field, should use Updates() not Save().
2025-11-11 12:47:48 -06:00
Kristoffer Dalby ddd31ba774 hscontrol: use Updates() instead of Save() for partial updates
Changed UpdateUser and re-registration flows to use Updates() which only
writes modified fields, preventing unintended overwrites of unchanged fields.

Also updated UsePreAuthKey to use Model().Update() for single field updates
and removed unused NodeSave wrapper.
2025-11-11 12:47:48 -06:00
Kristoffer Dalby 4a8dc2d445 hscontrol/state,db: preserve node expiry on MapRequest updates
Fixes a regression introduced in v0.27.0 where node expiry times were
being reset to zero when tailscaled restarts and sends a MapRequest.

The issue was caused by using GORM's Save() method in persistNodeToDB(),
which overwrites ALL fields including zero values. When a MapRequest
updates a node (without including expiry information), Save() would
overwrite the database expiry field with a zero value.

Changed to use Updates() which only updates non-zero values, preserving
existing database values when struct pointer fields are nil.

In BackfillNodeIPs, we need to explicitly update IPv4/IPv6 fields even
when nil (to remove IPs), so we use Select() to specify those fields.

Added regression test that validates expiry is preserved after MapRequest.

Fixes #2862
2025-11-11 12:47:48 -06:00
Kristoffer Dalby 773a46a968 integration: add test to replicate #2862
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-11 12:47:48 -06:00
Kristoffer Dalby 4728a2ba9e hscontrol/state: allow expired auth keys for node re-registration
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Skip auth key validation for existing nodes re-registering with the same
NodeKey. Pre-auth keys are only required for initial authentication.

NodeKey rotation still requires a valid auth key as it is a security-sensitive
operation that changes the node's cryptographic identity.

Fixes #2830
2025-11-11 05:12:59 -06:00
Florian Preinstorfer abed534628 Document how to restrict access to exit nodes per user/group
Updates: #2855
Ref: #2784
2025-11-11 11:51:35 +01:00
Kristoffer Dalby 21e3f2598d policy: fix issue where non existent user results in empty ssh pol
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
When we encounter a source we cannot resolve, we skipped the whole rule,
even if some of the srcs could be resolved. In this case, if we had one user
that exists and one that does not.

In the regular policy, we log this, and still let a rule be created from what
does exist, while in the SSH policy we did not.

This commit fixes it so the behaviour is the same.

Fixes #2863

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-10 20:34:12 +01:00
Kristoffer Dalby a28d9bed6d policy: reproduce 2863 in test
reproduce that if a user does not exist, the ssh policy ends up empty

Updates #2863

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-10 20:34:12 +01:00
Kristoffer Dalby 28faf8cd71 db: add defensive removal of old indicies
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-10 20:07:29 +01:00
Kristoffer Dalby 5a2ee0c391 db: add comment about removing migrations
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-10 17:32:39 +01:00
Andrey Bobelev 5cd15c3656 fix: make state cookies valid when client uses multiple login URLs
On Windows, if the user clicks the Tailscale icon in the system tray,
it opens a login URL in the browser.

When the login URL is opened, `state/nonce` cookies are set for that particular URL.

If the user clicks the icon again, a new login URL is opened in the browser,
and new cookies are set.

If the user proceeds with auth in the first tab,
the redirect results in a "state did not match" error.

This patch ensures that each opened login URL sets an individual cookie
that remains valid on the `/oidc/callback` page.

`TestOIDCMultipleOpenedLoginUrls` illustrates and tests this behavior.
2025-11-10 16:27:46 +01:00
Kristoffer Dalby 2024219bd1 types: Distinguish subnet and exit node access
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
When we fixed the issue of node visibility of nodes
that only had access to eachother because of a subnet
route, we gave all nodes access to all exit routes by
accident.

This commit splits exit nodes and subnet routes in the
access.

If a matcher indicates that the node should have access to
any part of the subnet routes, we do not remove it from the
node list.

If a matcher destination is equal to the internet, and the
target node is an exit node, we also do not remove the access.

Fixes #2784
Fixes #2788

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-02 13:19:59 +01:00
Kristoffer Dalby d9c3eaf8c8 matcher: Add func for comparing Dests and TheInternet
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-02 13:19:59 +01:00
Kristoffer Dalby bd9cf42b96 types: NodeView CanAccess uses internal
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-02 13:19:59 +01:00
Kristoffer Dalby d7a43a7cf1 state: use AllApprovedRoutes instead of SubnetRoutes
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-02 13:19:59 +01:00
Kristoffer Dalby 1c0bb0338d types: split SubnetRoutes and ExitRoutes
There are situations where the subnet routes and exit nodes
must be treated differently. This splits it so SubnetRoutes
only returns routes that are not exit routes.

It adds `IsExitRoutes` and `AllApprovedRoutes` for convenience.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-02 13:19:59 +01:00
Kristoffer Dalby c649c89e00 policy: Reproduce exit node visibility issues
Reproduces #2784 and #2788

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-11-02 13:19:59 +01:00
Vitalij Dovhanyc af2de35b6c chore: fix autogroup:self with other acl rules (#2842) 2025-11-02 10:48:27 +00:00
Kristoffer Dalby 02c7c1a0e7 cli: only validate bypass-grpc set policy (#2854) 2025-11-02 09:42:59 +00:00
Copilot d23fa26395 Fix flaky TestShuffleDERPMapDeterministic by ensuring deterministic map iteration (#2848)
Co-authored-by: kradalby <98431+kradalby@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-11-02 10:05:23 +01:00
Andrey f9bb88ad24 expire nodes with a custom timestamp (#2828)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-11-01 08:09:13 +01:00
Kristoffer Dalby 456a5d5cce db: ignore _litestream tables when validating (#2843) 2025-11-01 07:08:22 +00:00
Kristoffer Dalby ddbd3e14ba db: remove all old, unused tables (#2844) 2025-11-01 08:03:37 +01:00
Florian Preinstorfer 0a43aab8f5 Use Debian 12 as minimum version for the deb package
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-28 05:55:26 +01:00
Florian Preinstorfer 4bd614a559 Use current stable base images for Debian and Alpine 2025-10-28 05:55:26 +01:00
Kristoffer Dalby 19a33394f6 changelog: set 0.27 date (#2823)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-27 12:14:02 +01:00
Kristoffer Dalby 84fe3de251 integration: reduce TestAutoApproveMultiNetwork matrix to 3 tests (#2815) 2025-10-27 11:08:52 +00:00
Paarth Shah 450a7b15ec #2796: Add creation_time and ko_data_creation_time to goreleaser.yml kos 2025-10-27 11:18:57 +01:00
Kristoffer Dalby 64b7142e22 .goreleaser: add upgrade section (#2820)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-10-27 10:41:52 +01:00
Kristoffer Dalby 52d27d58f0 hscontrol: add /version HTTP endpoint (#2821) 2025-10-27 10:41:34 +01:00
Kristoffer Dalby e68e2288f7 gen: test-integration (#2814)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-10-24 17:22:53 +02:00
Kristoffer Dalby c808587de0 cli: do not show new pre-releases on stable (#2813) 2025-10-24 13:15:53 +02:00
Kristoffer Dalby 2bf1200483 policy: fix autogroup:self propagation and optimize cache invalidation (#2807)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-23 17:57:41 +02:00
Kristoffer Dalby 66826232ff integration: add tests for api bypass (#2811)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-22 16:30:25 +02:00
Kristoffer Dalby 1cdea7ed9b stricter hostname validation and replace (#2383) 2025-10-22 13:50:39 +02:00
Elyas Asmad 2c9e98d3f5 fix: guard every error statement with early return (#2810) 2025-10-22 13:48:07 +02:00
Florian Preinstorfer 8becb7e54a Mention explicitly that @ is only required in policy
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-21 14:28:03 +02:00
Florian Preinstorfer ed38d00aaa Fix autogroup:self alternative example
Also indent and split the comment into two lines to avoid horizontal
scrolling.
2025-10-21 14:28:03 +02:00
Florian Preinstorfer 8010cc574e Remove outdated hint about an empty config file
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-19 17:14:15 +02:00
Juanjo Presa c97d0ff23d Fix fatal error on missing config file by handling viper.ConfigFileNotFoundError
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Correctly identify Viper's ConfigFileNotFoundError in LoadConfig to log a warning and use defaults, unifying behavior with empty config files. Fixes fatal error when no config file is present for CLI commands relying on environment variables.
2025-10-19 15:29:47 +02:00
Florian Preinstorfer 047dbda136 Add FAQ on how to disable log submission
Fixes: #2793
2025-10-19 08:24:23 +02:00
Florian Preinstorfer 2a1392fb5b Add healthcheck to container docs
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-10-19 08:22:30 +02:00
Florian Preinstorfer 46477b8021 Downgrade completed broadcast message to debug
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-10-18 07:56:59 +02:00
Kristoffer Dalby c87471136b integration: eventually fixups (#2799)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-17 08:28:30 +02:00
Kristoffer Dalby e7a28a14af changelog: prepare for 0.27.0 (#2797)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-10-16 19:04:07 +02:00
Kristoffer Dalby 4912769ab3 update dependencies (#2798) 2025-10-16 19:03:30 +02:00
Stavros Kois c07cc491bf add health command (#2659)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
* add health command
* update health check implementation to allow for more checks to added over time
* add change changelog entry
2025-10-16 12:00:11 +00:00
Vitalij Dovhanyc c2a58a304d feat: add autogroup:self (#2789) 2025-10-16 12:59:52 +02:00
Kristoffer Dalby fddc7117e4 stability and race conditions in auth and node store (#2781)
This PR addresses some consistency issues that was introduced or discovered with the nodestore.

nodestore:
Now returns the node that is being put or updated when it is finished. This closes a race condition where when we read it back, we do not necessarily get the node with the given change and it ensures we get all the other updates from that batch write.

auth:
Authentication paths have been unified and simplified. It removes a lot of bad branches and ensures we only do the minimal work.
A comprehensive auth test set has been created so we do not have to run integration tests to validate auth and it has allowed us to generate test cases for all the branches we currently know of.

integration:
added a lot more tooling and checks to validate that nodes reach the expected state when they come up and down. Standardised between the different auth models. A lot of this is to support or detect issues in the changes to nodestore (races) and auth (inconsistencies after login and reaching correct state)

This PR was assisted, particularly tests, by claude code.
2025-10-16 12:17:43 +02:00
Florian Preinstorfer 881a6b9227 The sequential prefix allocation uses a best-effort approach
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Fixes: #2682
2025-10-15 17:07:13 +02:00
yckwan 3fbde7a1b6 Update official.md
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
in the step 5 file default value is [line11] ExecStart=/usr/bin/headscale serve
2025-10-13 17:06:56 +02:00
Andrey Bobelev c4a8c038cd fix: return valid AuthUrl in followup request on expired reg id
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
- tailscale client gets a new AuthUrl and sets entry in the regcache
- regcache entry expires
- client doesn't know about that
- client always polls followup request а gets error

When user clicks "Login" in the app (after cache expiry), they visit
invalid URL and get "node not found in registration cache". Some clients
on Windows for e.g. can't get a new AuthUrl without restart the app.

To fix that we can issue a new reg id and return user a new valid
AuthUrl.

RegisterNode is refactored to be created with NewRegisterNode() to
autocreate channel and other stuff.
2025-10-11 05:57:39 +02:00
Andrey Bobelev 022098fe4e chore: make reg cache expiry tunable
Mostly for the tests, opts:

- tuning.register_cache_expiration
- tuning.register_cache_cleanup
2025-10-11 05:57:39 +02:00
Florian Preinstorfer bd35fcf338 Add FAQ entry about policy migration in the database
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-09-17 16:32:29 +02:00
Florian Preinstorfer 2d680b5ebb Misc typos and spelling 2025-09-17 16:32:29 +02:00
Kristoffer Dalby ed3a9c8d6d mapper: send change instead of full update (#2775) 2025-09-17 14:23:21 +02:00
Kristoffer Dalby 4de56c40d8 flake: goreleaser doesnt follow go nix convention (#2779)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-09-17 09:41:05 +02:00
github-actions[bot] 40b3d54c1f flake.lock: Update (#2755)
Close inactive issues / close-issues (push) Has been cancelled
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-09-14 16:15:51 +00:00
Florian Preinstorfer 30d12dafed Add FAQ entry about the recommended upgrade path
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-09-13 08:15:01 +02:00
Kristoffer Dalby 2b30a15a68 cmd: add option to get and set policy directly from database (#2765)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-09-12 16:55:15 +02:00
Kristoffer Dalby 2938d03878 policy: reject unsupported fields (#2764)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-09-12 14:47:56 +02:00
Kristoffer Dalby 1b1c989268 {policy, node}: allow return paths in route reduction (#2767) 2025-09-12 11:47:51 +02:00
Kristoffer Dalby 3950f8f171 cli: use gobuild version handling (#2770) 2025-09-12 11:47:31 +02:00
Kristoffer Dalby ee0ef396a2 policy: fix ssh usermap, fixing autogroup:nonroot (#2768) 2025-09-12 09:12:30 +02:00
Kristoffer Dalby 7056fbb63b derp: fix flaky shuffle test (#2772)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-09-11 13:49:02 +00:00
Kristoffer Dalby c91b9fc761 poll: add missing godoc (#2763)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-09-11 14:15:19 +02:00
Kristoffer Dalby d41fb4d540 app: fix sigint hanging
When the node notifier was replaced with batcher, we removed
its closing, but forgot to add the batchers so it was never
stopping node connections and waiting forever.

Fixes #2751

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-11 11:53:26 +02:00
Kristoffer Dalby 01c1f6f82a policy: validate error message for asterix in ssh (#2766)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-09-10 18:41:43 +02:00
Oleksii Samoliuk 3f6657ae57 fix: documentation
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-09-09 20:54:47 +02:00
Kristoffer Dalby 0512f7c57e .github/ISSUE_TEMPLATE: add node number to environment
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 19:04:23 +02:00
Florian Preinstorfer c6427aa296 Use group id instead of group name for Entra ID
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-09-09 12:23:34 +02:00
Florian Preinstorfer 4e6d42d5bd Keycloak's group format is configurable 2025-09-09 12:23:34 +02:00
Florian Preinstorfer 8ff5baadbe Refresh OIDC docs
The UserInfo endpoint is always queried since 5d8a2c2.

This allows to use all OIDC related features without any extra
configuration on Authelia.

For Keycloak, its sufficient to add the groups mapper to the userinfo
endpoint.
2025-09-09 12:23:34 +02:00
Florian Preinstorfer 2f3c365b68 Describe how to remove a DERP region
Add documentation for d29feaef.

Fixes: #2450
2025-09-09 11:05:30 +02:00
Kristoffer Dalby 4893cdac74 integration: make timestamp const
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 476f30ab20 state: ensure netinfo is preserved and not removed
the client will send a lot of fields as `nil` if they have
not changed. NetInfo, which is inside Hostinfo, is one of those
fields and we often would override the whole hostinfo meaning that
we would remove netinfo if it hadnt changed.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 233dffc186 lint and leftover
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 39443184d6 gen: new proto version
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 0303b76e1f postgres uses more memory
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 684239e015 cmd/mapresponses: add mini tool to inspect mapresp state from integration
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 81b3e8f743 util: harden parsing of traceroute
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 50ed24847b debug: add json and improve
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 9b962956b5 integration: Eventually, debug output, lint and format
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 3b16b75fe6 integration: rework retry for waiting for node sync
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 9d236571f4 state/nodestore: in memory representation of nodes
Initial work on a nodestore which stores all of the nodes
and their relations in memory with relationship for peers
precalculated.

It is a copy-on-write structure, replacing the "snapshot"
when a change to the structure occurs. It is optimised for reads,
and while batches are not fast, they are grouped together
to do less of the expensive peer calculation if there are many
changes rapidly.

Writes will block until commited, while reads are never
blocked.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 38be30b6d4 derp: allow override to ip for debug
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 7f8b14f6f3 .github/workflows: remove integration retry
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 3326c5b7ec cmd/hi: lint and format
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby b6d5788231 mapper: produce map before poll
Before this patch, we would send a message to each "node stream"
that there is an update that needs to be turned into a mapresponse
and sent to a node.

Producing the mapresponse is a "costly" afair which means that while
a node was producing one, it might start blocking and creating full
queues from the poller and all the way up to where updates where sent.

This could cause updates to time out and being dropped as a bad node
going away or spending too time processing would cause all the other
nodes to not get any updates.

In addition, it contributed to "uncontrolled parallel processing" by
potentially doing too many expensive operations at the same time:

Each node stream is essentially a channel, meaning that if you have 30
nodes, we will try to process 30 map requests at the same time. If you
have 8 cpu cores, that will saturate all the cores immediately and cause
a lot of wasted switching between the processing.

Now, all the maps are processed by workers in the mapper, and the number
of workers are controlable. These would now be recommended to be a bit
less than number of CPU cores, allowing us to process them as fast as we
can, and then send them to the poll.

When the poll recieved the map, it is only responsible for taking it and
sending it to the node.

This might not directly improve the performance of Headscale, but it will
likely make the performance a lot more consistent. And I would argue the
design is a lot easier to reason about.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 33e9e7a71f CLAUDE: split into agents
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby ccd79ed8d4 mcp: add some standard mcp server
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby f6c4b338fd .github/workflows: add generate check
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 306d8e1bd4 integration: validate expected online status in ping
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 4927e9d590 fix: improve mapresponses and profiles extraction in hi tool
- Fix directory hierarchy flattening by using full paths instead of filepath.Base()
- Remove redundant container hostname prefixes from directory names
- Strip top-level directory from tar extraction to avoid nested structure
- Ensure parent directories exist before creating files
- Results in clean structure: control_logs/mapresponses/1-ts-client/file.json
2025-09-09 09:40:00 +02:00
Kristoffer Dalby 8e25f7f9dd bunch of qol (#2748)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-08-27 17:09:13 +02:00
github-actions[bot] 1a7a2f4196 flake.lock: Update (#2699)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-08-24 12:07:32 +00:00
Dylan Blanqué 860a8a597f Update tools.md
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
Share/Contribute Headscale Zabbix Monitoring scripts and templates.

Thank you for the awesome application to everyone involved in Headscale's development!
2025-08-24 06:05:21 +02:00
cuiweixie a2a6d20218 Refactor to use reflect.TypeFor
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-08-23 20:43:49 +02:00
Andrey Bobelev d29feaef79 chore(derp): allow nil regions in DERPMaps
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
Previously, nil regions were not properly handled. This change allows users to disable regions in DERPMaps.

Particularly useful to disable some official regions.
2025-08-23 06:54:14 +02:00
Andrey Bobelev 630bfd265a chore(derp): prioritize loading DERP maps from URLs
This allows users to override default entries provided via URL
2025-08-23 06:54:14 +02:00
Florian Preinstorfer e949859d33 Add DERP docs
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2025-08-22 12:09:31 +02:00
Florian Preinstorfer 4d61da30d0 Use an IPv4 address range suitable for documentation 2025-08-22 12:09:31 +02:00
Kristoffer Dalby b87567628a derp: increase update frequency and harden on failures (#2741) 2025-08-22 10:40:38 +02:00
dotlambda 51c6367bb1 Correctly document the default for dns.override_local_dns
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-08-19 15:02:49 +02:00
Florian Preinstorfer be337c6a33 Enable derp.server.verify_clients by default
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
This setting is already enabled in example-config.yaml but would default
to false if no key is set.
2025-08-19 11:30:44 +02:00
Shourya Gautam 086fcad7d9 Fix Internal server error on /verify (#2735)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
* converted the returned error to an httpError
2025-08-18 14:39:42 +00:00
afranco 3e3c72ea6f docs(acls): Add example for allow/deny all acl policy 2025-08-18 16:13:14 +02:00
afranco 43f90d205e fix: allow all traffic if acls field is omited from the policy 2025-08-18 16:13:14 +02:00
Florian Preinstorfer 7b8b796a71 docs: connect Android using a preauthkey
Fixes: #2616
2025-08-18 16:06:17 +02:00
nblock fa619ea9f3 Fix CHANGELOG for autogroup:member and autogroup:tagged (#2733)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Tests / test (push) Waiting to run
2025-08-18 08:59:03 +02:00
Florian Preinstorfer 30a1f7e68e Log registrationID to simplify interactive node registration
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Some clients such as Android make it hard to transfer the registrationID
to the server, its easier to get it from the server logs.
2025-08-15 17:11:38 +02:00
Florian Preinstorfer 30cec3aa2b Document ports in use
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Ref: #1767
2025-08-14 09:24:09 +02:00
Fredrik Ekre 5d8a2c25ea OIDC: Query userinfo endpoint before verifying user
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This patch includes some changes to the OIDC integration in particular:
 - Make sure that userinfo claims are queried *before* comparing the
   user with the configured allowed groups, email and email domain.
 - Update user with group claim from the userinfo endpoint which is
   required for allowed groups to work correctly. This is essentially a
   continuation of #2545.
 - Let userinfo claims take precedence over id token claims.

With these changes I have verified that Headscale works as expected
together with Authelia without the documented escape hatch [0], i.e.
everything works even if the id token only contain the iss and sub
claims.

[0]: https://www.authelia.com/integration/openid-connect/headscale/#configuration-escape-hatch
2025-08-11 17:51:16 +02:00
Jeff Emershaw b4f7782fd8 support force flag for nodes backfillips
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-08-10 13:31:24 +02:00
eyjhb d77874373d feat: add robots.txt 2025-08-10 10:57:45 +02:00
Kristoffer Dalby a058bf3cd3 mapper: produce map before poll (#2628)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-07-28 11:15:53 +02:00
Luke Watts b2a18830ed docs: fix typos
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2025-07-28 10:28:49 +02:00
Kristoffer Dalby 9779adc0b7 integration: run headscale with delve and debug symbols (#2689)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-07-24 17:44:09 +02:00
nblock e7fe645be5 Fix invocation of golangci-lint (#2703)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-07-24 08:41:20 +02:00
Florian Preinstorfer bcd80ee773 Add debugging and troubleshooting guide
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-07-22 14:56:45 +02:00
Florian Preinstorfer c04e17d82e Document valid log levels
Also change the order as the level seems more important than the format.
2025-07-22 14:56:45 +02:00
Florian Preinstorfer 98fc0563ac Bump version in docs 2025-07-22 14:56:45 +02:00
Kian-Meng Ang 3123d5286b Fix typos
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
Found via `codespell -L shs,hastable,userr`
2025-07-21 12:06:07 +02:00
Kristoffer Dalby 7fce5065c4 all: remove 32 bit support (#2692)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-07-16 13:32:59 +02:00
Florian Preinstorfer a98d9bd05f The preauthkeys commands expect a user id instead of a username
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2025-07-16 09:53:05 +02:00
Florian Preinstorfer 46c59a3fff Fix command in bug report template
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-07-15 21:12:32 +02:00
Kristoffer Dalby 044193bf34 integration: Use Eventually around external calls (#2685)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-07-13 17:37:11 +02:00
Mohammad Javad Naderi a8f2eebf66 Fix config param name in TLS doc 2025-07-13 12:56:25 +02:00
github-actions[bot] 6220e64978 flake.lock: Update (#2669)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-07-13 06:36:04 +00:00
Kristoffer Dalby c6d7b512bd integration: replace time.Sleep with assert.EventuallyWithT (#2680)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-07-10 23:38:55 +02:00
Kristoffer Dalby b904276f2b poll: use nodeview everywhere
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
There was a bug in HA subnet router handover where we used stale node data
from the longpoll session that we handed to Connect. This meant that we got
some odd behaviour where routes would not be deactivated correctly.

This commit changes to the nodeview is used through out, and we load the
current node to be updated in the write path and then handle it all there
to be consistent.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-08 21:05:15 +02:00
Kristoffer Dalby 4a8d2d9ed3 .github/workflows: reduce integration retry to 3
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-08 07:28:35 +01:00
Kristoffer Dalby 22e6094a90 golangci: disable varnamelen
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 21:28:59 +01:00
Kristoffer Dalby 73023c2ec3 all: use immutable node view in read path
This commit changes most of our (*)types.Node to
types.NodeView, which is a readonly version of the
underlying node ensuring that there is no mutations
happening in the read path.

Based on the migration, there didnt seem to be any, but the
idea here is to prevent it in the future and simplify other
new implementations.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 21:28:59 +01:00
Kristoffer Dalby 5ba7120418 .github/workflows: prettier
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 15:48:38 +01:00
Kristoffer Dalby d311d2e206 flake: dont override gopls
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 15:48:38 +01:00
Kristoffer Dalby 05996a5048 .github/workflow: only run a few selected postgres tests
We are already being punished by github actions, there seem to be
little value in running all the tests for both databases, so only
run a few key tests to check postgres isnt broken.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 15:48:38 +01:00
Kristoffer Dalby 4668e5dd96 changelog: add entry for db
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 15:48:38 +01:00
Kristoffer Dalby c6736dd6d6 db: add sqlite "source of truth" schema
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-07-07 15:48:38 +01:00
Stavros Kois 855c48aec2 remove unneeded check (#2658)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-07-04 15:47:01 +00:00
Stavros Kois ded049b905 don't crash if config file is missing (#2656)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2025-07-04 12:58:17 +00:00
github-actions[bot] 3bad5d5590 flake.lock: Update (#2585)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-04 12:00:59 +00:00
Florian Preinstorfer d461db3abd Refactor OpenID Connect documentation
Restructure and rewrite the OpenID Connect documentation. Start from the
most minimal configuration and describe what needs to be done both in
Headscale and the identity provider. Describe additional features such
as PKCE and authorization filters in a generic manner with examples.

Document how Headscale populates its user profile and how it relates to
OIDC claims. This is a revised version from the table in the changelog.
Document the validation rules for fields and extend known limitations.

Sort the provider specific section alphabetically and add a section for
Authelia, Authentik, Kanidm and Keycloak. Also simplify and rename Azure
to Entra ID.

Update the description for the oidc section in the example
configuration. Give a short explanation of each configuration setting.

All documentend features were tested with Headscale 0.26 (using a fresh
database each time) using the following identity providers:

* Authelia
* Authentik
* Kanidm
* Keycloak

Fixes: #2295
2025-07-04 10:51:37 +02:00
eyJhb efc6974017 fix typo in parseCapabilityVersion, and removed unused error (#2644) (#2644) 2025-07-04 09:40:29 +02:00
Fredrik Ekre 3f72ee9de8 Clarify SIGHUP log message (#2661) 2025-07-04 09:30:51 +02:00
nblock e73b2a9fb9 Ensure that a username starts with a letter (#2635)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-06-24 14:45:44 +02:00
Kristoffer Dalby 081af2674b ci: fix golangci-lint flag for v2 compatibility (#2654)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2025-06-24 08:14:50 +02:00
Kristoffer Dalby 1553f0ab53 state: introduce state
this commit moves all of the read and write logic, and all different parts
of headscale that manages some sort of persistent and in memory state into
a separate package.

The goal of this is to clearly define the boundry between parts of the app
which accesses and modifies data, and where it happens. Previously, different
state (routes, policy, db and so on) was used directly, and sometime passed to
functions as pointers.

Now all access has to go through state. In the initial implementation,
most of the same functions exists and have just been moved. In the future
centralising this will allow us to optimise bottle necks with the database
(in memory state) and make the different parts talking to eachother do so
in the same way across headscale components.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-06-24 07:58:54 +02:00
Kristoffer Dalby a975b6a8b1 hscontrol: remove go-grpc-middleware v1 dependency (#2653)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Co-authored-by: Claude <noreply@anthropic.com>
2025-06-23 16:57:20 +02:00
Kristoffer Dalby afc11e1f0c cmd/hi: fixes and qol (#2649) 2025-06-23 13:43:14 +02:00
Kristoffer Dalby ea7376f522 cmd/hi: add integration test runner CLI tool (#2648)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* cmd/hi: add integration test runner CLI tool

Add a new CLI tool 'hi' for running headscale integration tests
with Docker automation. The tool replaces manual Docker command
composition with an automated solution.

Features:
- Run integration tests in golang:1.24 containers
- Docker context detection (supports colima and other contexts)
- Test isolation with unique run IDs and isolated control_logs
- Automatic Docker image pulling and container management
- Comprehensive cleanup operations for containers, networks, images
- Docker volume caching for Go modules
- Verbose logging and detailed test artifact reporting
- Support for PostgreSQL/SQLite selection and various test flags

Usage: go run ./cmd/hi run TestPingAllByIP --verbose

The tool uses creachadair/command and flax for CLI parsing and
provides cleanup subcommands for Docker resource management.

Updates flake.nix vendorHash for new Go dependencies.

* ci: update integration tests to use hi CLI tool

Replace manual Docker command composition in GitHub Actions
workflow with the new hi CLI tool for running integration tests.

Changes:
- Replace complex docker run command with simple 'go run ./cmd/hi run'
- Remove manual environment variable setup (handled by hi tool)
- Update artifact paths for new timestamped log directory structure
- Simplify command from 15+ lines to 3 lines
- Maintain all existing functionality (postgres/sqlite, timeout, test patterns)

The hi tool automatically handles Docker context detection, container
management, volume mounting, and environment variable setup that was
previously done manually in the workflow.

* makefile: remove test integration

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-06-18 11:22:15 +02:00
seiuneko d325211617 feat: add verify client config for embedded DERP (#2260)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* feat: add verify client config for embedded DERP

* refactor: embedded DERP no longer verify clients via HTTP

- register the `headscale://` protocol in `http.DefaultTransport` to intercept network requests
- update configuration to use a single boolean option `verify_clients`

* refactor: use `http.HandlerFunc` for type definition

* refactor: some renaming and restructuring

* chore: some renaming and fix lint

* test: fix TestDERPVerifyEndpoint

- `tailscale debug derp` use random node private key

* test: add verify clients integration test for embedded DERP server

* fix: apply code review suggestions

* chore: merge upstream changes

* fix: apply code review suggestions

---------

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2025-06-18 09:24:53 +02:00
Mustafa Enes Batur bad783321e Fix /machine/map endpoint vulnerability (#2642)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* Improve map auth logic

* Bugfix

* Add comment, improve error message

* noise: make func, get by node

this commit splits the additional validation into a
separate function so it can be reused if we add more
endpoints in the future.

It swaps the check, so we still look up by NodeKey, but before
accepting the connection, we validate the known machinekey from
the db against the noise connection.

The reason for this is that when a node logs in or out, the node key
is replaced and it will no longer be possible to look it up, breaking
reauthentication.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* noise: add comment to remind future use of getAndVal

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog: add entry

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-06-06 12:14:11 +02:00
Hannes b8044c29dd Replace magic-nix-cache-action (#2575)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-05-26 23:05:08 +02:00
Shubham Hibare df69840f92 feat(tools): Add Go client implementation
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-05-23 17:52:31 +02:00
lucarickli 76ca7a2b50 Add headscale-console
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-05-22 06:52:02 +02:00
Florian Preinstorfer cd704570be Drop support for Ubuntu 20.04
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
Its old and our service file logs warning about unsupported options.
2025-05-21 15:40:32 +02:00
Florian Preinstorfer 43c9c50af4 Drop syslog.target and systemd-managed /var/run
The systemd target "syslog.target" and not required because syslog is
socket activated.

The directory /var/run is usually a symlink to /run and its created by
systemd via the RuntimeDirectory=headscale option. System creates and
handles permissions, no need to manually mark it as a read-write path.
2025-05-21 15:40:32 +02:00
Florian Preinstorfer 4a941a2cb4 Refactor Debian/Ubuntu package
Move files for packaging outside the docs directory into its own
packaging directory. Replace the existing postinstall and postremove
scripts with Debian maintainerscripts to behave more like a typical
Debian package:

* Start and enable the headscale systemd service by default
* Does not print informational messages
* No longer stop and disable the service on updates

This package also performs migrations for all changes done in previous
package versions on upgrade:

* Set login shell to /usr/sbin/nologin
* Set home directory to /var/lib/headscale
* Migrate to system UID/GID

The package is lintian-clean with a few exceptions that are documented
as excludes and it passes puipars (both tested on Debian 12).

The following scenarious were tested on Ubuntu 22.04, Ubuntu 24.04,
Debian 11, Debian 12:

* Install
* Install same version again
* Install -> Remove -> Install
* Install -> Purge -> Install
* Purge
* Update from 0.22.0
* Update from 0.26.0

See: #2278
See: #2133
Fixes: #2311
2025-05-21 15:40:32 +02:00
Greg Dietsche d2879b2b36 web: change node registration parameter order (#2607)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
This change makes editing the generated command easier.
For example, after pasting into a terminal, the cursor position will be
near the username portion which requires editing.
2025-05-21 11:18:53 +02:00
Kristoffer Dalby a52f1df180 policy: remove v1 code (#2600)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* policy: remove v1 code

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* db: update test with v1 removal

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: start moving to v2 policy

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: add ssh unmarshal tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog: add entry

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: remove v1 comment

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: remove comment out case

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* cleanup skipv1

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: remove v1 prefix workaround

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: add all node ips if prefix/host is ts ip

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-20 13:57:26 +02:00
azrikahar 1605e2a7a9 fix typo in TailSQL's log
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-05-18 07:15:41 +02:00
Vitalij Dovhanyc 6750414db1 feat: add autogroup:member, autogroup:tagged (#2572)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-05-17 11:07:34 +02:00
Florian Preinstorfer b50e10a1be Document breaking change for dns.override_local_dns
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
See: #2438
2025-05-16 19:33:00 +02:00
Florian Preinstorfer c15aa541bb Document HEADSCALE_CONFIG 2025-05-16 19:33:00 +02:00
Florian Preinstorfer 49b3468845 Do not ignore config-example.yml
Various tools (e.g ripgrep) skip files ignored by Git. Do not ignore
config-example.yml to include it in searches.
2025-05-16 19:33:00 +02:00
Kristoffer Dalby bd6ed80936 policy/v2: error on missing or zero port (#2606)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* policy/v2: error on missing or zero port

Fixes #2605

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog: add entry

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-16 17:30:47 +02:00
Kristoffer Dalby 30525cee0e goreleaser: always do draft (#2595)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-16 10:23:22 +02:00
Kristoffer Dalby 2dc2f3b3f0 users: harden, test, and add cleaner of identifier (#2593)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
* users: harden, test, and add cleaner of identifier

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* db: migrate badly joined provider identifiers

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-14 16:45:14 +02:00
Kristoffer Dalby d7a503a34e changelog: entry for 0.26 (#2594)
* changelog: entry for 0.26

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* docs: bump version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-14 16:32:56 +02:00
jasonrepos 62b489dc68 fix: change FormatUint base from 64 to 10 in preauthkeys list command (#2588)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-05-13 18:40:17 +00:00
nblock 8c7e650616 Remove map_legacy_users from example configuration (#2590) 2025-05-13 21:38:52 +03:00
Kristoffer Dalby 43943aeee9 bring back last_seen in database (#2579)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* db: add back last_seen to the database

Fixes #2574

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: ensure last_seen is set

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-10 09:49:08 +02:00
nblock d81b0053e5 Simplify policy migration (#2582)
These steps are easier to accomplish and require only Headscale 0.26.
They also work when a user has already upgraded the database.

See: #2567
2025-05-10 08:04:42 +02:00
nblock dd0cbdf40c Add migration steps when policy is stored in the database (#2581)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Fixes: #2567
2025-05-09 23:30:39 +02:00
Kristoffer Dalby 37dc0dad35 policy/v2: separate exit node and 0.0.0.0/0 routes (#2578)
* policy: add tests for route auto approval

Reproduce #2568

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: separate exit node and 0.0.0.0/0 routes

Fixes #2568

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-09 23:20:04 +02:00
Kristoffer Dalby 377b854dd8 cli: policy check, dont require config or log (#2580)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-09 23:19:47 +02:00
Kristoffer Dalby 56db4ed0f1 policy/v2: validate that no undefined group or tag is used (#2576)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* policy/v2: allow Username as ssh source

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: validate that no undefined group or tag is used

Fixes #2570

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: fixup tests which violated tag constraing

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-09 11:51:30 +02:00
nblock 833e0f66f1 Remove subnet router visibility workaround from docs (#2569)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Previous Headscale versions required a dedicated rule to make a subnet
router visible to clients. This workaround is no longer required.
2025-05-05 15:24:59 +02:00
Kristoffer Dalby 1dddd3e93b app: throw away not found body (#2566)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-04 22:06:44 +02:00
nblock 9a86ffc102 Misc doc fixes (#2562)
* Link to stable and development docs in the README

* Add Tailscale SSH and autogroup:nonroot to features page

* Use @ when referencing users in policy

* Remove unmaintained headscale-webui

The project seems to be unmaintained (last commit: 2023-05-08) and it
only supports Headscale 0.22 or earlier.

* Use full image URL in container docs

This makes it easy to switch the container runtime from docker <->
podman.

* Remove version from docker-compose.yml example

This is now deprecated and yields a warning.
2025-05-04 21:55:08 +02:00
Kristoffer Dalby 45e38cb080 policy: reduce routes sent to peers based on packetfilter (#2561)
* notifier: use convenience funcs

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: reduce routes based on policy

Fixes #2365

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* hsic: more helper methods

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: more test cases

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: add route with filter acl integration test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: correct route reduce test, now failing

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* mapper: compare peer routes against node

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* hs: more output to debug strings

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* types/node: slice.ContainsFunc

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: more reduce route test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog: add entry for route filter

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-04 21:52:47 +02:00
Kristoffer Dalby b9868f6516 Make more granular SSH tests for both Policies (#2555)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* policy/v1: dont consider empty if ssh has rules

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: replace time.Duration with model.Duration

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: add autogroup and ssh validation

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: replace time.Duration with model.Duration

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: replace old ssh tests with more granular test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: skip v1 tests expected to fail (missing error handling)

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: skip v1 group tests, old bugs wont be fixed

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: user valid policy for ssh

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Changelog, add ssh section

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* nix update

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-04 12:05:41 +00:00
Kristoffer Dalby f317a85ab4 go.mod: update rest of deps (#2559)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* flake: update go hash

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* go.mod: update more deps

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-03 16:36:08 +02:00
Alexey Tarasov 53d9c95160 Update container.md 2025-05-03 12:51:46 +02:00
Jacob Yundt 03a91693ac feat: Create headscale user and group as system user/groups (#2322)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
When creating the headscale user and group, create both as system groups
rather than creating them as 'user' groups.

FIXES #2278
2025-05-03 09:13:54 +00:00
nblock cb7c0173ec Fix deprecation warnings (#2558)
See https://goreleaser.com/deprecations/#archivesformat and
https://goreleaser.com/deprecations/#nfpmsbuilds
2025-05-03 10:18:49 +02:00
nblock 18d21d3585 Add documentation for routes (#2496)
* Add documentation for routes

* Rename exit-node to routes and add redirects
* Add a new section on subnet routers
* Extend the existing exit-node documentation
* Describe auto approvers for subnet routers and exit nodes
* Provide ACL examples for subnet routers and exit nodes
* Describe HA and its current limitations
* Add a troubleshooting section with IP forwarding

* Update features page for 0.26

Add auto approvers and link to our documentation if available.

* Prefer the console lexer when commandline and output mixed
2025-05-03 10:16:45 +02:00
Kristoffer Dalby e7d2d79134 update capmap and deps for release (#2522)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* generate new capver map

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* replace old sort func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* nix: flake update

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* capgen: update

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* capgen: update

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* go.mod: update tailscale

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* go.mod: update other deps

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-02 22:12:29 +02:00
Kristoffer Dalby d810597414 policy/matcher: fix bug using contains instead of overlap (#2556)
* policy/matcher: slices.ContainsFunc

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/matcher: slices.ContainsFunc, correct contains vs overlap

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: add tests to validate fix for 2181

Fixes #2181

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-02 22:08:56 +02:00
Kristoffer Dalby 93afb03f67 cmd: add policy check command (#2553)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-05-02 13:58:30 +03:00
Kristoffer Dalby e4d10ad964 policy/v2: validate autogroup:interet only in dst (#2552) 2025-05-02 13:58:12 +03:00
Janne Johansson 7dc86366b4 Update source.md
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
If we assume someone doesn't already have the required go package, they might also not have the required git package installed either, so pkg_add both of them.
2025-05-02 10:43:56 +02:00
Kristoffer Dalby c923f461ab error on undefined host in policy (#2490)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* add testcases

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: add validate to do post marshal validation

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-01 14:30:52 +02:00
Kristoffer Dalby a4a203b9a3 cli/nodes: filter nodes without any routes (#2551) 2025-05-01 13:27:54 +03:00
aergus-tng 4651d06fa8 Make matchers part of the Policy interface (#2514)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* Make matchers part of the Policy interface

* Prevent race condition between rules and matchers

* Test also matchers in tests for Policy.Filter

* Compute `filterChanged` in v2 policy correctly

* Fix nil vs. empty list issue in v2 policy test

* policy/v2: always clear ssh map

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: Aras Ergus <aras.ergus@tngtech.com>
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-01 07:06:30 +02:00
Kristoffer Dalby eb1ecefd9e auth: ensure that routes are autoapproved when the node is stored (#2550)
* integration: ensure route is set before node joins, reproduce

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* auth: ensure that routes are autoapproved when the node is stored

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-05-01 07:05:42 +02:00
Kristoffer Dalby 6b6509eeeb notify nodes after owner change (#2543)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* proto: user id as identifier for move node

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* gen: regenr

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* grpc: move, use userid, one tx, send update

Updates #2467

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: update move cli tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-30 18:33:38 +02:00
Kristoffer Dalby cfe9bbf829 oidc: try to get username from userinfo (#2545)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* oidc: try to get username from userinfo

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-30 11:54:13 +02:00
Kristoffer Dalby 8f9fbf16f1 types/authkey: include user object in response (#2542)
* types/authkey: include user object, not string

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* make preauthkeys use id

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: wire up user id for auth keys

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-30 11:45:08 +02:00
Kristoffer Dalby f1206328dc fix webauth + autoapprove routes (#2528)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* types/node: add helper funcs for node tags

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* types/node: add DebugString method for node

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: add String func to AutoApprover interface

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: simplify, use slices.Contains

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: debug, use nodes.DebugString

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v1: fix potential nil pointer in NodeCanApproveRoute

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v1: slices.Contains

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration/tsic: fix diff in login commands

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: fix webauth running with wrong scenario

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: move common oidc opts to func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: require node count, more verbose

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* auth: remove uneffective route approve

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* .github/workflows: fmt

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration/tsic: add id func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: remove call that might be nil

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: test autoapprovers against web/authkey x group/tag/user

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: unique network id per scenario

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Revert "integration: move common oidc opts to func"

This reverts commit 7e9d165d4a900c304f1083b665f1a24a26e06e55.

* remove cmd

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: clean docker images between runs in ci

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: run autoapprove test against differnt policy modes

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration/tsic: append, not overrwrite extra login args

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* .github/workflows: remove polv2

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-30 07:54:04 +02:00
Kristoffer Dalby 57861507ab integration: remove failing resolvconf tests (#2549)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-30 07:52:23 +02:00
Kristoffer Dalby 2b38f7bef7 policy/v2: make default (#2546)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* policy/v2: make default

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* integration: do not run v1 tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy/v2: fix potential nil pointers

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* mapper: fix test failures in v2

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-29 16:27:41 +02:00
github-actions[bot] 9a4d0e1a99 flake.lock: Update (#2518)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/18dd725c29603f582cf1900e0d25f9f1063dbf11?narHash=sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38%3D' (2025-04-13)
  → 'github:NixOS/nixpkgs/ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c?narHash=sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs%3D' (2025-04-17)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-04-24 11:02:09 +00:00
Kristoffer Dalby 30539b2e26 config: disallow same server url and base_domain (#2544)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* config: disallow same server url and base_domain

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-23 16:24:38 +02:00
Kristoffer Dalby 098ab0357c add casbin user test (#2474)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
* add casbin user test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Delete double slash

* types/users: use join url on iss that are ursl

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2025-04-23 13:21:51 +02:00
Relihan Myburgh 56d085bd08 Fix panic on fast reconnection of node (#2536)
* Fix panic on fast reconnection of node

* Use parameter captured in closure as per review request
2025-04-23 11:52:24 +02:00
Relihan Myburgh 92e587a82c Fix goroutine leak in EphemeralGC on node cancel (#2538)
* Fix goroutine leak in EphemeralGC on node cancel

* Deal with timer firing whilst the GC is shutting down. Fix typos.
2025-04-23 11:44:24 +02:00
Pamplemousse f3a1e693f2 Mention "Network flow logs" as a missing feature 2025-04-22 11:28:41 +02:00
Kristoffer Dalby f783555469 integration: clean up unreferenced hs- networks (#2534) 2025-04-18 12:06:28 +02:00
Kristoffer Dalby 710d75367e policy/v2: fix host validation, consistent pattern (#2533) 2025-04-18 11:35:04 +02:00
Kristoffer Dalby c30e3a4762 flake: add golang-lint lsp (#2507)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-18 11:15:02 +02:00
alteriks 3287aa8bba Update oidc.md
Authelia docs
2025-04-18 10:16:08 +02:00
Kristoffer Dalby 8e7e52cf3a some clarifications for tags (#2531)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-04-18 09:33:02 +02:00
nblock 1e0516b99d Restore support for "Override local DNS" (#2438)
Tailscale allows to override the local DNS settings of a node via
"Override local DNS" [1]. Restore this flag with the same config setting
name `dns.override_local_dns` but disable it by default to align it with
Tailscale's default behaviour.

Tested with Tailscale 1.80.2 and systemd-resolved on Debian 12.

With `dns.override_local_dns: false`:

```
Link 12 (tailscale0)
Current Scopes: DNS
     Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
   DNS Servers: 100.100.100.100
    DNS Domain: tn.example.com ~0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa [snip]
```

With `dns.override_local_dns: true`:

```
Link 12 (tailscale0)
Current Scopes: DNS
     Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
   DNS Servers: 100.100.100.100
    DNS Domain: tn.example.com ~.
```

[1] https://tailscale.com/kb/1054/dns#override-local-dns

Fixes: #2256
2025-04-17 17:16:59 +02:00
Kristoffer Dalby 0fbe392499 more wait, more retry (#2532) 2025-04-16 12:42:26 +02:00
Nick 109989005d ensure final dot on node name (#2503)
* ensure final dot on node name

This ensures that nodes which have a base domain set, will have a dot appended to their FQDN.

Resolves: https://github.com/juanfont/headscale/issues/2501

* improve OIDC TTL expire test

Waiting a bit more than the TTL of the OIDC token seems to remove some flakiness of this test. This furthermore makes use of a go func safe buffer which should avoid race conditions.
2025-04-11 12:39:08 +02:00
Enkelmann 0d3134720b Only read relevant nodes from database in PeerChangedResponse (#2509)
* Only read relevant nodes from database in PeerChangedResponse

* Rework to ensure transactional consistency in PeerChangedResponse again

* An empty nodeIDs list should return an empty nodes list

* Add test to ListNodesSubset

* Link PR in CHANGELOG.md

* combine ListNodes and ListNodesSubset into one function

* query for all nodes in ListNodes if no parameter is given

* also add optional filtering for relevant nodes to ListPeers
2025-04-08 14:56:44 +02:00
Christoph d2a6356d89 Add unraid-headscale-admin web UI to docs (#2515)
* Add unraid-headscale-admin link
2025-04-02 20:54:32 +02:00
Kristoffer Dalby 5a18e91317 fix auto approver on register and new policy (#2506)
* fix issue auto approve route on register bug

This commit fixes an issue where routes where not approved
on a node during registration. This cause the auto approval
to require the node to readvertise the routes.

Fixes #2497
Fixes #2485

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* hsic: only set db policy if exist

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* policy: calculate changed based on policy and filter

v1 is a bit simpler than v2, it does not pre calculate the auto approver map
and we cannot tell if it is changed.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-31 15:55:07 +02:00
Kristoffer Dalby e3521be705 allow users to be defined with @ in v1 (#2495)
* allow users to be defined with @ in v1

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove integration test rewrite hack

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove test rewrite hack

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add @ to integration tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* a bit to agressive removeals

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix last test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-30 11:19:05 +00:00
github-actions[bot] f52f15ff08 flake.lock: Update (#2510) 2025-03-30 06:18:37 +00:00
Kristoffer Dalby cbc99010f0 populate serving from primary routes (#2489)
* populate serving from primary routes

Depends on #2464
Fixes #2480

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* also exit

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix route update outside of connection

there was a bug where routes would not be updated if
they changed while a node was connected and it was not part of an
autoapprove.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update expected test output, cli only shows service node

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-28 13:22:15 +01:00
Benjamin Staffin b5953d689c OIDC: Fetch UserInfo to get EmailVerified if necessary (#2493) 2025-03-27 10:39:29 +01:00
github-actions[bot] badbb68217 flake.lock: Update (#2468) 2025-03-23 08:34:03 +00:00
Kristoffer Dalby 603f3ad490 Multi network integration tests (#2464) 2025-03-21 11:49:32 +01:00
Florian Preinstorfer 707438f25e Mention that private keys generated if needed 2025-03-19 06:12:14 +01:00
Florian Preinstorfer 24ad235917 Explicitly handle /headscale/{config,lib,run} in container docs
Fixes: #2304
2025-03-19 06:12:14 +01:00
Oleksii Samoliuk 00d5d647ed add third-party tool headscale-pf 2025-03-17 13:20:24 +01:00
Florian Preinstorfer cbce8f6011 Remove coderabbit
It is no longer available for free and didn't do anything in the past
months.
2025-03-16 14:02:33 +01:00
Florian Preinstorfer 05202099f7 Set content-type to JSON for some debug endpoints
Some endpoints in /debug send JSON data as string. Set the Content-Type
header to "application/json" which renders nicely in Firefox.

Mention the /debug route in the example configuration.
2025-03-16 14:02:12 +01:00
Kristoffer Dalby 800456018a update bug template with debug (#2481)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-16 13:43:13 +01:00
Florian Preinstorfer 586a20fbff Add a FAQ entry about two nodes seeing each other
See: #2431
See: #2174
2025-03-14 18:48:44 +01:00
Kristoffer Dalby 818046f240 add faq section on scaling/performance (#2476)
* add faq section on scaling/performance

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Apply suggestions from code review

Co-authored-by: nblock <nblock@users.noreply.github.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: nblock <nblock@users.noreply.github.com>
2025-03-14 17:09:30 +00:00
nblock fe06a00d45 Container images are also available on GHCR (#2470)
Fixes: #2456
2025-03-11 06:54:05 +01:00
Kristoffer Dalby 0b5c29e875 remove policy handling for old capver (#2429)
* remove policy handling for old capver

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-10 18:19:25 +00:00
Florian Preinstorfer 0a243b4162 Remove leftover printf 2025-03-10 16:52:29 +01:00
Florian Preinstorfer 29ba29478b Add usage example to routes flag 2025-03-10 16:52:29 +01:00
Florian Preinstorfer e52f1e87ce Drop routes table 2025-03-10 16:52:29 +01:00
Kristoffer Dalby 87326f5c4f Experimental implementation of Policy v2 (#2214)
* utility iterator for ipset

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* split policy -> policy and v1

This commit split out the common policy logic and policy implementation
into separate packages.

policy contains functions that are independent of the policy implementation,
this typically means logic that works on tailcfg types and generic formats.
In addition, it defines the PolicyManager interface which the v1 implements.

v1 is a subpackage which implements the PolicyManager using the "original"
policy implementation.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use polivyv1 definitions in integration tests

These can be marshalled back into JSON, which the
new format might not be able to.

Also, just dont change it all to JSON strings for now.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* formatter: breaks lines

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove compareprefix, use tsaddr version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove getacl test, add back autoapprover

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use policy manager tag handling

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* rename display helper for user

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* introduce policy v2 package

policy v2 is built from the ground up to be stricter
and follow the same pattern for all types of resolvers.

TODO introduce
aliass
resolver

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* wire up policyv2 in integration testing

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* split policy v2 tests into seperate workflow to work around github limit

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add policy manager output to /debug

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-10 16:20:29 +01:00
github-actions[bot] b6fbd37539 flake.lock: Update (#2454)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/dad564433178067be1fbdfcce23b546254b6d641?narHash=sha256-vn285HxnnlHLWnv59Og7muqECNMS33mWLM14soFIv2g%3D' (2025-02-20)
  → 'github:NixOS/nixpkgs/199169a2135e6b864a888e89a2ace345703c025d?narHash=sha256-igS2Z4tVw5W/x3lCZeeadt0vcU9fxtetZ/RyrqsCRQ0%3D' (2025-03-01)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-02 19:59:44 +00:00
Kristoffer Dalby 7891378f57 Redo route code (#2422)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-26 16:22:55 +01:00
Kristoffer Dalby 16868190c8 fix double login URL with OIDC (#2445)
* factor out login url parser

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* move to not trigger test gen checker

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* return regresp or err after waiting for registration

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-25 18:16:07 +01:00
Kristoffer Dalby da2ca054b1 fix routes not being saved when new nodes registers (#2444)
* add test to validate exitnode propagation

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* save routes on register

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* no nil

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add missing integration tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-23 23:10:25 +01:00
Kristoffer Dalby bcff0eaae7 handle register auth errors (#2435)
* handle register auth errors

This commit handles register auth errors as the
Tailscale clients expect. It returns the error as
part of a tailcfg.RegisterResponse and not as a
http error.

In addition it fixes a nil pointer panic triggered
by not handling the errors as part of this chain.

Fixes #2434

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-23 17:02:46 +01:00
github-actions[bot] b220fb7d51 flake.lock: Update (#2440) 2025-02-23 01:23:16 +00:00
github-actions[bot] 2cce3a99eb flake.lock: Update (#2430) 2025-02-16 20:40:20 +00:00
Kristoffer Dalby bbe57f6cd4 use tailscale version in all unsupported errs (#2426) 2025-02-14 13:43:33 +01:00
Kristoffer Dalby 604f7f6282 update to go 1.24 (#2427) 2025-02-14 10:56:03 +01:00
badsmoke c61fbe9c5f activate json logs (#2424)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Co-authored-by: jan.sulimma <jan.sulimma@zigpos.com>
2025-02-12 15:31:24 +01:00
Kristoffer Dalby b943cce868 set 0.25.0 changelog date (#2423)
* date in changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update docs version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-11 16:25:53 +01:00
Kristoffer Dalby 6403c8d5d2 use tsweb debugger (#2420)
This PR switches the homegrown debug endpoint to using tsweb.Debugger, a neat toolkit with batteries included for pprof and friends, and making it easy to add additional debug info:

I've started out by adding a bunch of "introspect" endpoints
image

So users can see the acl, filter, config, derpmap and connected nodes as headscale sees them.
2025-02-11 11:18:59 +01:00
github-actions[bot] b3fa16fbda flake.lock: Update (#2419)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-02-09 08:45:06 +00:00
Kristoffer Dalby 1f0110fe06 use helper function for constructing state updates (#2410)
This helps preventing messages being sent with the wrong update type
and payload combination, and it is shorter/neater.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-07 13:49:59 +01:00
Kristoffer Dalby b92bd3d27e remove oidc migration (#2411)
* remove oidc migration

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-07 13:49:45 +01:00
Kristoffer Dalby 3bf7d5a9c9 add git hash to binary, print on startup (#2415)
* add git hash to binary, print on startup

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-07 13:49:34 +01:00
Kristoffer Dalby 1d65865425 make version info in bug template more explicit (#2413)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-07 11:36:00 +01:00
Kristoffer Dalby c53ff2ce00 update changelog (#2414)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-07 11:32:01 +01:00
Kristoffer Dalby b4ac8cd9a3 hscontrol/db: add migration setting non existing pak on nodes to null (#2412)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-07 10:22:23 +01:00
Nbelles 22277d1fc7 Spell check
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-02-05 17:29:30 +01:00
Kristoffer Dalby 9ae3570154 drop versions older than 1.62 (#2405) 2025-02-05 12:02:32 +01:00
Florian Preinstorfer f12cb2e048 Headscale now updates the user profile 2025-02-05 11:05:03 +01:00
Florian Preinstorfer 8c09afe20c Headscale implements a single tailnet
Separate the term "tailnet" from user and be more explicit about
providing a single tailnet.

Also be more explicit about users. Refer to "headscale users" when
mentioning commandline invocations and use the term "local users" when
discussing unix accounts.

Fixes: #2335
2025-02-05 11:05:03 +01:00
Kristoffer Dalby 8b92c017ec add 1.80 to capver and update deps (#2394) 2025-02-05 07:17:51 +01:00
github-actions[bot] 9a7890d56b flake.lock: Update (#2402)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-02-02 07:58:12 +00:00
Kristoffer Dalby 45752db0f6 Return better web errors to the user (#2398)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* add dedicated http error to propagate to user

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* classify user errors in http handlers

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* move validation of pre auth key out of db

This move separates the logic a bit and allow us to
write specific errors for the caller, in this case the web
layer so we can present the user with the correct error
codes without bleeding web stuff into a generic validate.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-01 15:25:18 +01:00
Kristoffer Dalby 1c7f3bc440 no edit of oidc users, minimum hostname length (#2393)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* return an error when renaming users from OIDC
* set minimum hostname length of 2
2025-02-01 09:40:37 +00:00
Kristoffer Dalby 9bd143852f do not allow preauth keys to be deleted if assigned to node (#2396)
* do not allow preauth keys to be deleted if assigned to node

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-02-01 10:31:13 +01:00
Kristoffer Dalby d57a55c024 Rewrite authentication flow (#2374) 2025-02-01 09:16:51 +00:00
Kristoffer Dalby e172c29360 initial capver packet tracking version (#2391)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
* initial capver packet tracking version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Log the minimum version as client version, not only capver

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove old versions

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use capver for integration tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* patch through m and n key

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-30 21:49:09 +00:00
Kristoffer Dalby cd3b8e68ff clean up handler methods, common logging (#2384)
* clean up handler methods, common logging

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* streamline http.Error calls

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-30 22:40:29 +01:00
nblock f44b1d37c4 Remove routes without a node_id (#2386)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
The routes table has a NOT NULL constraint on node_id.

Fixes: #2376
2025-01-30 14:57:06 +01:00
Kristoffer Dalby 7ba6ad3489 simplify findUserByToken in ACL, add missing testcases (#2388)
* update users doc on unique constraints

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* simplify finduser func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add initial tests for findUserFromToken

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-30 11:35:49 +01:00
Kristoffer Dalby 2c279e0a7b create and rename usernames validated by new func (#2381)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-27 22:58:10 +01:00
Kristoffer Dalby 4c8e847f47 use dedicated registration ID for auth flow (#2337)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-01-26 22:20:11 +01:00
github-actions[bot] 97e5d95399 flake.lock: Update (#2378)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-01-26 15:33:51 +00:00
Kristoffer Dalby d1dbe4ece9 fix panic if derp update is 0 (#2368)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* fix panic if derp update is 0

Fixes #2362

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-23 16:16:12 +01:00
Kristoffer Dalby 9e3f945eda fix postgres migration issue with 0.24 (#2367)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* fix postgres migration issue with 0.24

Fixes #2351

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add postgres migration test for 2351

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-23 14:58:42 +01:00
Kristoffer Dalby 615ee5df75 make it harder to insert invalid routes (#2371)
* make it harder to insert invalid routes

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* dont panic if node is not available for route

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-23 13:40:23 +01:00
Kristoffer Dalby c1f42cdf4b relax user validation to allow emails, add tests from various oidc providers (#2364)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* relax user validation to allow emails, add tests from various oidc providers

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-22 18:10:15 +01:00
github-actions[bot] aa76980b43 flake.lock: Update (#2353)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-01-19 09:59:29 +00:00
Kristoffer Dalby 5b986ed0a7 set oidc.map_legacy_users false (#2350)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-01-17 14:44:04 +00:00
Kristoffer Dalby 8076c94444 Release docs 0.24 (#2349)
* correct changelog date

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update docs version and copyright

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix deprecated goreleaser key and DRY

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-17 12:57:13 +00:00
Kristoffer Dalby e88406e837 set changelog date (#2347)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-01-17 12:01:06 +01:00
Kristoffer Dalby e4a3dcc3b8 use headscale server url as domain instead of base_domain (#2338)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-01-16 18:05:20 +01:00
Kristoffer Dalby caad5c613d fix nil pointer deref (#2339) 2025-01-16 18:05:05 +01:00
Kristoffer Dalby 38aef77e54 allow @ and Log if OIDC username is not consider valid (#2340) 2025-01-16 18:04:54 +01:00
Dmitry Gordin 1ab7b315a2 Update apple.md for latest version of iOS (#2321)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
The official iOS app now has a simpler login process for custom instances, directly within the app.
2025-01-13 12:09:53 +00:00
github-actions[bot] 610597bfb7 flake.lock: Update (#2342)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2025-01-12 18:54:59 +00:00
Stefan Majer ede4f97a16 Fix typos
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2025-01-09 10:38:25 +01:00
Kristoffer Dalby fa641e38b8 Set CSRF cookies for OIDC (#2328)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* set state and nounce in oidc to prevent csrf

Fixes #2276

* try to fix new postgres issue

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-01-08 16:29:37 +01:00
github-actions[bot] 41bad2b9fd flake.lock: Update (#2324)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
2025-01-05 07:35:18 +00:00
github-actions[bot] f9bbfa5eab flake.lock: Update (#2320)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2024-12-29 11:41:52 +00:00
Rorical b81420bef1 feat: Add PKCE Verifier for OIDC (#2314)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* feat: add PKCE verifier for OIDC

* Update CHANGELOG.md
2024-12-22 16:46:36 +00:00
github-actions[bot] 9313e5b058 flake.lock: Update (#2313)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2024-12-22 07:07:26 +00:00
Kristoffer Dalby 770f3dcb93 fix tags not resolving to username if email is present (#2309)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* ensure valid tags is populated on user gets too

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* ensure forced tags are added

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove unused envvar in test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* debug log auth/unauth tags in policy man

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* defer shutdown in tags test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add tag test with groups

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add email, display name, picture to create user

Updates #2166

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add ability to set display and email to cli

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add email to test users in integration

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix issue where tags were only assigned to email, not username

Fixes #2300
Fixes #2307

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* expand principles to correct login name

and if fix an issue where nodeip principles might not expand to all
relevant IPs instead of taking the first in a prefix.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix ssh unit test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update cli and oauth tests for users with email

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* index by test email

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix last test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-19 13:10:10 +01:00
Kristoffer Dalby af4508b9dc bump deps (#2308)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
* Bump go crypto

Closes #2281

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* upgrade tailscale

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* upgrade rest

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* nix: flake update

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-17 15:35:42 +01:00
Florian Preinstorfer bbc93a90a2 Set title for code listings
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2024-12-17 14:08:34 +01:00
Florian Preinstorfer 0acb2b5647 Misc doc updates 2024-12-17 14:08:34 +01:00
Florian Preinstorfer 3269cfdca0 Mention reload and SIGHUP when editing the ACL policy file
Fixes: #2284
2024-12-17 14:08:34 +01:00
Florian Preinstorfer 319ce67c87 Update DNS documentation for dns.extra_records_path
* Describe both ways to add extra DNS records
* Use "extra" instead of "custom" to align with the configuration file
* Include dns.extra_records_path in the configuration file
2024-12-17 14:08:34 +01:00
Florian Preinstorfer 47b405d6c6 Changelog: support client verify for DERP
and fix some links

Ref: #2304
2024-12-17 14:08:34 +01:00
Florian Preinstorfer 65304a0ce7 Remove sealos documentation
The referenced version is outdated (0.23.0-beta1) and seems
unmaintained.
2024-12-17 14:08:34 +01:00
Dongjun Na e270169c13 Add -race Flag to GitHub Action and Fix Data Race in CreateTailscaleNodesInUser (#2038)
* Add -race flag to Makefile and integration tests; fix data race in CreateTailscaleNodesInUser

* Fix data race in ExecuteCommand by using local buffers and mutex

Signed-off-by: Dongjun Na <kmu5544616@gmail.com>

* lint

Signed-off-by: Dongjun Na <kmu5544616@gmail.com>

---------

Signed-off-by: Dongjun Na <kmu5544616@gmail.com>
2024-12-17 14:06:57 +01:00
Shaw Drastin 7d937c6bd0 Correct macOS GUI connect guide because there's no ALT key on a mac (#2306)
* Correct macOS GUI connect guide because there's no ALT key on a mac
* also correct macOS GUI connect in hscontrol text
2024-12-17 12:11:27 +00:00
Kristoffer Dalby ccc895b4c6 fixes to extra-record file watcher (#2298)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* Fix excess error message during writes

Fixes #2290

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* retry filewatcher on removed files

This should handled if files are deleted and added again, and for rename
scenarios.

Fixes #2289

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* test more write and remove in filewatcher

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-16 11:26:56 +01:00
Kristoffer Dalby 5345f19693 fix issue where some oidc claim bools are sent as string (#2297)
Jumpcloud send invalid json, so we need to handle it.

Fixes #2293

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-16 11:26:32 +01:00
Kristoffer Dalby ec8729b772 fix sighup issue with empty acl (#2296)
Fixes #2291

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-16 07:48:19 +01:00
github-actions[bot] e00b9d9a91 flake.lock: Update (#2294)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
2024-12-15 06:46:14 +00:00
Kristoffer Dalby 58d089ce0a fix deletion of exit routes without nodes (#2286)
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Fixes #2259

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-13 20:15:24 +01:00
Kristoffer Dalby 76d26a7eec update oidc part of changelog for 0.24.0 (#2285) 2024-12-13 12:35:24 +00:00
Kristoffer Dalby 380fcdba17 Add worker reading extra_records_path from file (#2271)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* consolidate scheduled tasks into one goroutine

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* rename Tailcfg dns struct

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add dns.extra_records_path option

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* prettier lint

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* go-fmt

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-13 07:52:40 +00:00
Florian Preinstorfer 89a648c7dd Remove use_username_in_magic_dns option
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=386 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Has been cancelled
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Tests / test (push) Has been cancelled
Upgrade the use of dns.use_username_in_magic_dns or
dns_config.use_username_in_magic_dns to a fatal error and remove the
option from the example configuration and integration tests.

Fixes: #2219
2024-12-11 18:39:35 +01:00
Vitalij Dovhanyc 697d80d5a8 chore: configure some actions to be skipped for forks (#2005)
* chore: configure some actions to be skipped for forks

* chore: build docs only when it changes
2024-12-11 16:44:37 +01:00
Kristoffer Dalby 757defa2f2 run cross compile of headscale as part of build (#2270)
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
Deploy docs / deploy (push) Has been cancelled
2024-12-10 16:26:53 +01:00
Kristoffer Dalby 64fd1f9483 restructure command/api to use stable IDs (#2261) 2024-12-10 16:23:55 +01:00
Kristoffer Dalby 08bd4b9bc5 fix docker network caps (#2273)
Build / build (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
Docker releases a patch release which changed the required permissions to be able to do tun devices in containers, this caused all containers to fail in tests causing us to fail all tests. This fixes it, and adds some tools for debugging in the future.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-12-09 16:15:38 +00:00
github-actions[bot] 26d91ae513 flake.lock: Update (#2266)
Build / build (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Tests / test (push) Waiting to run
2024-12-08 09:10:42 +00:00
Florian Preinstorfer 75e74117db Add FAQ entry on which database to use
Build / build (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Fixes: #2257
2024-12-04 06:08:44 +01:00
Florian Preinstorfer d2a86b1ef2 Fix broken indent 2024-12-04 06:08:44 +01:00
Florian Preinstorfer 0d3cf74098 Fix README links to point to the stable version 2024-12-04 06:08:44 +01:00
Florian Preinstorfer 44456497b0 Add versioned documentation
Build / build (push) Has been cancelled
Deploy docs / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Setup mike to provide versioned builds of the documentation.

The goal is to have versioned docs for stable releases (0.23.0, 0.24.0)
and development docs that can progress along with the code. This allows
us to tailor docs to the next upcoming version as we no longer need to
care about diversion between rendered docs and the latest release.

Versions:
* development (alias: unstable) on each push to the main branch
* MAJOR.MINOR.PATCH (alias: stable, latest for the newest version)
  * for each "final" release tag
  * for each push to doc maintenance branches: doc/MAJOR.MINOR.PATCH

The default version should the current stable version. The doc
maintenance branches may be used to update the version specific
documentation when issues arise after a release.
2024-12-02 16:51:50 +01:00
Florian Preinstorfer 7512e236d6 Bump deprecated github actions
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
2024-11-27 09:52:08 +01:00
Kristoffer Dalby f7b0cbbbea wrap policy in policy manager interface (#2255)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-26 15:16:06 +01:00
github-actions[bot] 2c1ad6d11a flake.lock: Update (#2254)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
2024-11-24 09:42:22 +00:00
Kristoffer Dalby fffd23602b Resolve user to stable unique ID in policy (#2205)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
2024-11-24 00:13:27 +01:00
Kristoffer Dalby 3a2589f1a9 rename dockerfile to integration to avoid confusion (#2225)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:36 +00:00
Kristoffer Dalby f6276ab9d2 fix postgres constraints, add postgres testing
This commit fixes the constraint syntax so it is both valid for
sqlite and postgres.

To validate this, I've added a new postgres testing library and a
helper that will spin up local postgres, setup a db and use it in
the constraints tests. This should also help testing db stuff in
the future.

postgres has been added to the nix dev shell and is now required
for running the unit tests.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 7d9b430ec2 fix constraints
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 3780c9fd69 fix nil in test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 281025bb16 fix constraints
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 5e7c3153b9 nits
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 7ba0c3d515 use userID instead of username everywhere
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 4b58dc6eb4 make preauthkey tags test stable
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 4dd12a2f97 fix oidc test, add tests for migration
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 2fe65624c0 restore strip_email_domain for migration
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 35b669fe59 add iss to identifier, only set email if verified
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby dc07779143 add @ to end of username if not present
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby d72663a4d0 remove log print
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 0a82d3f17a update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 78214699ad Harden OIDC migration and make optional
This commit hardens the migration part of the OIDC from
the old username based approach to the new sub based approach
and makes it possible for the operator to opt out entirely.

Fixes #1990

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-23 21:14:55 +01:00
Kristoffer Dalby 64bb56352f make configurable wal auto checkpoint (#2242) 2024-11-23 21:03:48 +01:00
nblock dc17b4d378 Documentation dependencies (#2252)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* Use a trailing slash
recommended by mkdocs-material

* Update doc requirements
Let mkdocs-material resolve its imaging dependencies (cairosvg and
pillow) and fix a dependabot warning along the way.

Reference compatible versions by major.minor.
2024-11-22 16:52:36 +00:00
Kristoffer Dalby a6b19e85db more linter fixups (#2212)
* linter fixes

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* conf

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update nix hash

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-22 15:54:58 +00:00
ArcticLampyrid edf9e25001 feat: support client verify for derp (add integration tests) (#2046)
* feat: support client verify for derp

* docs: fix doc for integration test

* tests: add integration test for DERP verify endpoint

* tests: use `tailcfg.DERPMap` instead of `[]byte`

* refactor: introduce func `ContainsNodeKey`

* tests(dsic): use string builder for cmd args

* ci: fix tests order

* tests: fix derper failure

* chore: cleanup

* tests(verify-client): perfer to use `CreateHeadscaleEnv`

* refactor(verify-client): simplify error handling

* tests: fix `TestDERPVerifyEndpoint`

* refactor: make `doVerify` a seperated func

---------

Co-authored-by: 117503445 <t117503445@gmail.com>
2024-11-22 13:23:05 +01:00
Motiejus Jakštys c6336adb01 config: loosen up BaseDomain and ServerURL checks (#2248)
* config: loosen up BaseDomain and ServerURL checks

Requirements [here][1]:

> OK:
> server_url: headscale.com, base: clients.headscale.com
> server_url: headscale.com, base: headscale.net
>
> Not OK:
> server_url: server.headscale.com, base: headscale.com
>
> Essentially we have to prevent the possibility where the headscale
> server has a URL which can also be assigned to a node.
>
> So for the Not OK scenario:
>
> if the server is: server.headscale.com, and a node joins with the name
> server, it will be assigned server.headscale.com and that will break
> the connection for nodes which will now try to connect to that node
> instead of the headscale server.

Fixes #2210

[1]: https://github.com/juanfont/headscale/issues/2210#issuecomment-2488165187

* server_url and base_domain: re-word error message, fix a one-off bug and add a test case for the bug.

* lint

* lint again
2024-11-22 13:21:44 +01:00
enoperm 5fbf3f8327 Websocket derp test fixes (#2247)
* integration testing: add and validate build-time options for tailscale head

* fixup! integration testing: add and validate build-time options for tailscale head

integration testing: comply with linter

* fixup! fixup! integration testing: add and validate build-time options for tailscale head

integration testing: tsic.New must never return nil

* fixup! fixup! fixup! integration testing: add and validate build-time options for tailscale head

* minor fixes
2024-11-22 11:57:01 +01:00
Nathan Sweet 6275399327 Update tls.md to mention using the full cert chain (#2243)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
2024-11-18 06:12:12 +00:00
nblock 29119bb7f4 Misc doc fixes (#2240)
* Link back to node registration docs
* adjust wording in apple docs
* Mention client specific page to check if headscale works

Ref: #2238
2024-11-18 05:46:58 +01:00
github-actions[bot] 93ba21ede5 flake.lock: Update (#2239)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
2024-11-17 19:38:50 +00:00
nblock a7874af3d0 Use discord server invite link (#2235)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Replace channel links with links to discord invite link and remove
channel list.

Fixes: #1521
2024-11-16 07:06:15 +01:00
nblock e7245856c5 Refresh remote CLI documentation (#2216)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
Close inactive issues / close-issues (push) Successful in 4s
* Document to either use a minimal configuration file or environment
  variables to connect with a remote headscale instance.
* Document a workaround specific for headscale 0.23.0.
* Remove reference to ancient headscale version.
* Use `cli.insecure: true` or `HEADSCALE_CLI_INSECURE=1` to skip
  certificate verification.
* Style and typo fixes

Ref: #2193
2024-11-13 18:35:42 +01:00
nblock 2345c38e1e Add a page for third-party tools (#2217)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
* Remove status from web-ui docs

Rename the title to indicate that there multiple web interfaces
available. Do not track the status of each web interface here as their
status is subject to change over time.

* Add page for third-party tools and scripts
2024-11-12 16:53:30 +01:00
github-actions[bot] 8cfaa6bdac flake.lock: Update (#2222) 2024-11-12 13:27:49 +00:00
docgalaxyblock 4e44d57bf7 fix: missing stable-debug tag (#2232)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
Fixes #2171
2024-11-11 06:06:44 +00:00
Philip Henning 0089ceaf1d Feature tvos documentation (#2226)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
* Add usage documentation for tvOS

* lint and format

* Change admonition to mkdocs flavoured style

* fix typos

* Update hscontrol/templates/apple.go

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>

* change outer quoting for where quoting in-text is used

---------

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2024-11-07 14:56:18 +00:00
nblock 9a46c5763c Handle /derp/latency-check (#2227)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
According to https://github.com/tailscale/tailscale/commit/15fc6cd96637e8a0e697ff2157c1608ada8e4a39
the routes `/derp/probe` and `/derp/latency-check` are the same and
different versions of the tailscale client use one or the other
endpoint.

Also handle /derp/latency-check

Fixes: #2211
2024-11-06 15:59:38 +01:00
Kristoffer Dalby a71a933705 add nblock to doc owners (#2207)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-04 10:12:50 -06:00
github-actions[bot] 0c98d09783 Update flake.lock (#2195)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da?narHash=sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M%3D' (2024-10-05)
  → 'github:NixOS/nixpkgs/41dea55321e5a999b17033296ac05fe8a8b5a257?narHash=sha256-WvLXzNNnnw%2BqpFOmgaM3JUlNEH%2BT4s22b5i2oyyCpXE%3D' (2024-10-25)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 14:16:10 +00:00
Kristoffer Dalby e2d5ee0927 cleanup linter warnings (#2206)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-23 10:45:59 -05:00
Kristoffer Dalby 028d9aab73 add new user fields to grpc and list command (#2202)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Updates #2166

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-18 14:20:03 +00:00
hopleus b6dc6eb36c #2140 Fixed reflection of hostname change (#2199)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
* #2140 Fixed updating of hostname and givenName when it is updated in HostInfo

* #2140 Added integration tests

* #2140 Fix unit tests

* Changed IsAutomaticNameMode to GivenNameHasBeenChanged. Fixed errors in files according to golangci-lint rules
2024-10-17 09:45:33 -06:00
Goran Draganić 45c9585b52 feat: derpmap field in config (#1823)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
2024-10-17 05:34:20 -06:00
hopleus cc42fc394a #2177 Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied (#2198)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
2024-10-15 18:33:03 +02:00
hopleus 52a3b54ba2 Fixed loginUrl with "WithTLS()" used. Added "WithTLS()" to scenario integration tests (#2187) 2024-10-15 14:38:43 +02:00
Pepper 0602304cea Add headplane
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
update-flake-lock / lockfile (push) Has been cancelled
GitHub Actions Version Updater / build (push) Has been cancelled
Add headplane to the list of UI's
2024-10-11 16:32:33 +02:00
nblock 8c7d8ee34f Restructure headscale documentation (#2163)
Build / build (push) Waiting to run
Build documentation / build (push) Waiting to run
Build documentation / deploy (push) Blocked by required conditions
Tests / test (push) Waiting to run
* Setup mkdocs-redirects

* Restructure existing documentation

* Move client OS support into the documentation

* Move existing Client OS support table into its own documentation page
* Link from README.md to the rendered documentation
* Document minimum Tailscale client version

* Reuse CONTRIBUTING.md" in the documentation

* Include "CONTRIBUTING.md" from the repository root
* Update FAQ and index page and link to the contributing docs

* Add configuration reference

* Add a getting started page and explain the first steps with headscale

* Use the existing "Using headscale" sections and combine them into a
  single getting started guide with a little bit more explanation.
* Explain how to get help from the command line client.
* Remove duplicated sections from existing installation guides

* Document requirements and assumptions

* Document packages provided by the community

* Move deb install guide to official releases

* Move manual install guide to official releases

* Move container documentation to setup section

* Move sealos documentation to cloud install page

* Move OpenBSD docs to build from source

* Simplify DNS documentation

* Add sponsor page

* Add releases page

* Add features page

* Add help page

* Add upgrading page

* Adjust mkdocs nav

* Update wording

Use the term headscale for the project, Headscale on the beginning of a
sentence and `headscale` when refering to the CLI.

* Welcome to headscale

* Link to existing documentation in the FAQ

* Remove the goal header and use the text as opener

* Indent code block in OIDC

* Make a few pages linter compatible

Also update ignored files for prettier

* Recommend HTTPS on port 443

Fixes: #2164

* Use hosts in acl documentation

thx @efficacy38 for noticing this

Ref: #1863

* Use mkdocs-macros to set headscale version once
2024-10-10 15:24:04 +02:00
hopleus b3cda08af6 #2178 Fixed processing of fields in post request in MoveNode rpc (#2179)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
* #2178 Fixed processing of fields in post request in MoveNode rpc

* #2178 Updated CHANGELOG.md
2024-10-09 09:36:47 +02:00
github-actions[bot] 101ca7f4a2 Update flake.lock (#2173)
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Tests / test (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/b5b2fecd0cadd82ef107c9583018f381ae70f222?narHash=sha256-k6YxGj08voz9NvuKExojiGXAVd69M8COtqWSKr6sQS4%3D' (2024-09-28)
  → 'github:NixOS/nixpkgs/e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da?narHash=sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M%3D' (2024-10-05)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-06 12:00:59 +00:00
Amha Mersha 24e7851a40 Changed all the html into go using go-elem (#2161)
* Changed all the HTML into go using go-elem

            Created templates package in ./hscontrol/templates.
            Moved the registerWebAPITemplate into the templates package as a function to be called.

            Replaced the apple and windows html files with go-elem.

* update flake

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-04 11:39:24 +00:00
Kristoffer Dalby 9515040161 make reauth test compat with tailscale head (#2167)
* make reauth test compat with tailscale head

tailscale/tailscale@1eaad7d broke our reauth test as it makes the client
retry with https/443 if it reconnects within 2 minutes.

This commit fixes this by running the test as a two part,
- with https, to confirm instant reconnect works
- with http, and a 3 min wait, to check that it work without.

The change is not a general consern as headscale in prod is ran
with https.

Updates #2164

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* sort test for stable order

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-03 12:01:48 +02:00
Kristoffer Dalby e16ea2ee69 set hostinfo,ipv* columns explicitly (#2165)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 18:12:25 +02:00
Kristoffer Dalby 218138afee Redo OIDC configuration (#2020)
expand user, add claims to user

This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.

This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.

remove usernames in magic dns, normalisation of emails

this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.

In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.

Email are no longer normalised as part of the policy processing.

untagle oidc and regcache, use typed cache

This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.

try to make reauth/register branches clearer in oidc

Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.

This commit tries to split this into what to do if the node
exists, if it needs to register etc.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
Kristoffer Dalby bc9e83b52e use gorm serialiser instead of custom hooks (#2156)
* add sqlite to debug/test image

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* test using gorm serialiser instead of custom hooks

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 11:41:58 +02:00
Kristoffer Dalby 3964dec1c6 use tsaddr library and cleanups (#2150)
* resuse tsaddr code instead of handrolled

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* ensure we dont give out internal tailscale IPs

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use prefix instead of string for routes

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove old custom compare func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* trim unused util code

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 09:06:09 +02:00
Jacob Yundt 63035cdb5a Update headscale user creation settings in .deb (#2134)
* Update headscale user creation settings in .deb

Update the headscale user settings to:
- shell = /usr/sbin/nologin
- home-dir = /var/lib/headscale

This syncs the .deb installation behavior with the current Linux docs:
https://github.com/juanfont/headscale/blob/fe68f503289db6cb1c2a568b8ae02a45ac632dd6/docs/running-headscale-linux-manual.md?plain=1#L39-L45

Fixes juanfont/headscale#2133

* slight refactor to use existing variables.

* Fixup for HOME_DIR var
2024-09-29 11:00:52 +00:00
Kristoffer Dalby 5eda9c8d2d denormalise PreAuthKey tags (#2155)
this commit denormalises the Tags related to a Pre auth key
back onto the preauthkey table and struct as a string list.

There was not really any real normalisation here as we just added
a bunch of duplicate tags with new IDs and preauthkeyIDs, lots of
GORM cermony but no actual advantage.

This work is the start to fixup tags which currently are not working
as they should.

Updates #1369

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-29 13:00:27 +02:00
github-actions[bot] 49ce5734fc flake.lock: Update (#2158) 2024-09-29 08:24:08 +00:00
Hazel Atkinson 204a102389 Add ouroboros to web ui list (#2154) 2024-09-27 09:16:18 +02:00
Robert Schütz 2c974dd72d MagicDNS no longer requires nameservers (#1681)
According to https://tailscale.com/kb/1081/magicdns#accessing-devices-over-magicdns,

> MagicDNS does not require a DNS nameserver if running Tailscale v1.20 or later.
2024-09-26 19:09:19 +00:00
nblock e367454745 Add -it to docker exec (#2148)
Some commands such as `nodes delete` require user interaction and they
fail if `-it` is no supplied to `docker exec`. Use `docker exec -it` in
documentation examples to also make them work in interactive commands.
2024-09-25 09:52:28 +02:00
Kristoffer Dalby 4f2fb65929 remove versions older than 1.56 (#2149)
* remove versions older than 1.56

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove code no longer needed for new clients

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-24 18:34:20 +02:00
David Mell 07b596d3cc Allow nodes to use SSH agent forwarding (#2145) 2024-09-23 11:59:16 +02:00
github-actions[bot] f3fca8302a flake.lock: Update (#2143) 2024-09-22 09:46:04 +00:00
enoperm 1e61084898 Add compatibility with only websocket-capable clients (#2132)
* handle control protocol through websocket

The necessary behaviour is already in place,
but the wasm build only issued GETs, and the handler was not invoked.

* get DERP-over-websocket working for wasm clients

* Prepare for testing builtin websocket-over-DERP

Still needs some way to assert that clients are connected through websockets,
rather than the TCP hijacking version of DERP.

* integration tests: properly differentiate between DERP transports

* do not touch unrelated code

* linter fixes

* integration testing: unexport common implementation of derp server scenario

* fixup! integration testing: unexport common implementation of derp server scenario

* dockertestutil/logs: remove unhelpful comment

* update changelog

---------

Co-authored-by: Csaba Sarkadi <sarkadicsa@tutanota.de>
2024-09-21 12:05:36 +02:00
Kristoffer Dalby 10a72e8d54 update changelog for 0.23 release (#2138)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-18 09:43:08 +01:00
Kristoffer Dalby ed78ecda12 add shutdown that asserts if headscale had panics (#2126)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-17 11:44:55 +02:00
github-actions[bot] 6cbbcd859c flake.lock: Update (#2135) 2024-09-16 06:15:45 +00:00
nblock e9d9c0773c Exclude irrelevant files from mkdocs rendering (#2136) 2024-09-16 06:13:45 +00:00
nblock fe68f50328 Use headscale.example.com (#2122) 2024-09-11 16:46:06 +00:00
nblock c3ef90a7f7 Update documentation for Apple (#2117)
* Rename docs/ios-client.md to docs/apple-client.md. Add instructions
  for macOS; those are copied from the /apple endpoint and slightly
  modified. Fix doc links in the README.
* Move infoboxes for /apple and /windows under the "Goal" section to the
  top. Those should be seen by users first as they contain *their*
  specific headscale URL.
* Swap order of macOS and iOS to move "Profiles" further down.
* Remove apple configuration profiles
* Remove Tailscale versions hints
* Mention /apple and /windows in the README along with their docs

See: #2096
2024-09-11 18:43:59 +02:00
Kristoffer Dalby 064c46f2a5 move logic for validating node names (#2127)
* move logic for validating node names

this commits moves the generation of "given names" of nodes
into the registration function, and adds validation of renames
to RenameNode using the same logic.

Fixes #2121

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix double arg

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-11 18:27:49 +02:00
Kristoffer Dalby 64319f79ff make stream shutdown if self-node has been removed (#2125)
* add shutdown that asserts if headscale had panics

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add test case producing 2118 panic

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* make stream shutdown if self-node has been removed

Currently we will read the node from database, and since it is
deleted, the id might be set to nil. Keep the node around and
just shutdown, so it is cleanly removed from notifier.

Fixes #2118

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-11 12:00:32 +02:00
Kristoffer Dalby 4b02dc9565 make cli mode respect log.level (#2124)
Fixes #2119

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-11 10:43:22 +02:00
Kristoffer Dalby 7be8796d87 dont override golangci go (#2116)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-09 14:29:09 +02:00
curlwget 99f18f9cd9 chore: fix some comments (#2069) 2024-09-09 14:17:25 +02:00
github-actions[bot] c3b260a6f7 flake.lock: Update (#2111) 2024-09-09 14:16:35 +02:00
Kristoffer Dalby 60b94b0467 Fix slow shutdown (#2113)
* rearrange shutdown

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* http closed is fine

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* logging while shutting

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-09 14:10:22 +02:00
nblock bac7ea67f4 Simplify windows setup instructions (#2114)
* Simplify /windows to the bare minimum. Also remove the
  /windows/tailscale.reg endpoint as its generated file is no longer
  valid for current Tailscale versions.
* Update and simplify the windows documentation accordingly.
* Add a "Unattended mode" section to the troubleshooting section
  explaining how to enable "Unattended mode" in the via the Tailscale
  tray icon.
* Add infobox about /windows to the docs

Tested on Windows 10, 22H2 with Tailscale 1.72.0

Replaces: #1995
See: #2096
2024-09-09 13:18:16 +02:00
nblock 5597edac1e Remove version and update setup instructions for Android (#2112) 2024-09-09 06:57:50 +00:00
Kristoffer Dalby 8a3a0fee3c Only load needed part of configuration (#2109) 2024-09-07 09:23:58 +02:00
Kristoffer Dalby f368ed01ed 2068 AutoApprovers tests (#2105)
* replace old suite approved routes test with table driven

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add test to reproduce issue

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add integration test for 2068

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-05 16:46:20 +02:00
Kristoffer Dalby adc084f20f add no stalebot exception (#2107)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-05 14:00:36 +02:00
Kristoffer Dalby 42d2c27853 fix goreleaser warnings (#2106)
* add version to goreleaser config

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* rename deprected setting

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-05 14:00:19 +02:00
nblock 1c34101e72 Remove outdated proposals (#2104)
Fixes: #2101
2024-09-05 11:50:10 +00:00
greizgh 6609f60938 actually lint file on CI (#2018)
* replace deprecated golangci-lint output format

CI was producing this kind of messages:
> [config_reader] The output format `github-actions` is deprecated, please use `colored-line-number`

* Actually lint files on CI
2024-09-05 13:37:05 +02:00
nblock 35bfe7ced0 Add support for service reload and sync service file (#2102)
* Add support for service reload and sync service file

* Copy the systemd.service file to the manual linux docs and adjust the
  path to the headscale binary to match with the previous documentation
  blocks. Unfortunately, there seems to be no easy way to include a
  file in mkdocs.
* Remove a redundant "deprecation" block. The beginning of the
  documentation already states that.
* Add `ExecReload` to the systemd.service file.

Fixes: #2016

* Its called systemd

* Fix link to systemd homepage
2024-09-05 10:08:50 +00:00
nblock e43d6a0361 Move flags after the command (#2100)
The built-in help also shows flags to given after the command. Align
documentation examples accordingly.
2024-09-04 12:38:38 +00:00
Kristoffer Dalby f039caf134 update godeps (#2098) 2024-09-04 07:55:16 +02:00
nblock d66c5e144f Update documentation for 0.23 (#2096)
* docs/acl: fix path to policy file

* docs/exit-node: fixup for 0.23

* Add newlines between commands to improve readability
* Use nodes instead on name
* Remove query parameter from link to Tailscale docs

* docs/remote-cli: fix formatting

* Indent blocks below line numbers to restore numbering
* Fix minor typos

* docs/reverse-proxy: remove version information

* Websocket support is always required now
* s/see detail/see details

* docs/exit-node: add warning to manual documentation

* Replace the warning section with a warning admonition
* Fix TODO link back to the regular linux documentation

* docs/openbsd: fix typos

* the database is created on-the-fly

* docs/sealos: fix typos

* docs/container: various fixes

* Remove a stray sentence
* Remove "headscale" before serve
* Indent line continuation
* Replace hardcoded 0.22 with <VERSION>
* Fix path in debug image to /ko-app/headscale

Fixes: #1822

aa
2024-09-03 11:04:20 +00:00
Mike Poindexter 3101f895a7 Fix 764 (#2093)
* Fix KeyExpiration when a zero time value has a timezone

When a zero time value is loaded from JSON or a DB in a way that
assigns it the local timezone, it does not roudtrip in JSON as a
value for which IsZero returns true. This causes KeyExpiry to be
treated as a far past value instead of a nilish value.

See https://github.com/golang/go/issues/57040

* Fix whitespace

* Ensure that postgresql is used for all tests when env var is set

* Pass through value of HEADSCALE_INTEGRATION_POSTGRES env var

* Add option to set timezone on headscale container

* Add test for registration with auth key in alternate timezone
2024-09-03 09:22:17 +02:00
nblock aa0f3d43cc Fix typo in example config (#2095) 2024-09-02 06:18:16 +00:00
nblock ed71d230eb Remove references to tests/acls from the documentation (#2088) 2024-09-01 13:09:47 +00:00
github-actions[bot] 976cbfa630 flake.lock: Update (#2078) 2024-09-01 13:08:57 +00:00
nblock a9a1a07e37 Use dns: as config key (#2092) 2024-09-01 13:08:06 +00:00
Kristoffer Dalby 1193a50e9e oldest client supported, not latest (#2086) 2024-08-30 16:59:37 +02:00
Kristoffer Dalby cb0e2e4476 various doc updates in prep for 0.23 (#2091)
* various doc updates in prep for 0.23

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add note discouraging postgresql

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Update docs/faq.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* remove entry for glossary in doc

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix typo

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-08-30 16:59:24 +02:00
Kristoffer Dalby 2b5e52b08b validate policy against nodes, error if not valid (#2089)
* validate policy against nodes, error if not valid

this commit aims to improve the feedback of "runtime" policy
errors which would only manifest when the rules are compiled to
filter rules with nodes.

this change will in;

file-based mode load the nodes from the db and try to compile the rules on
start up and return an error if they would not work as intended.

database-based mode prevent a new ACL being written to the database if
it does not compile with the current set of node.

Fixes #2073
Fixes #2044

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* ensure stderr can be used in err checks

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* test policy set validation

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add new integration test to ghaction

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add back defer for cli tst

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-08-30 16:58:29 +02:00
Roman Zabaluev fffd9d7ee9 Update ACLs file format docs (#2066) 2024-08-30 07:20:07 +00:00
Mike Poindexter 76515d12d6 Fix self notification on expiry update via oidc relogin (#2080) 2024-08-30 08:20:29 +02:00
Mike Poindexter 34361c6f82 Fix FKs on sqlite migrations (#2083) 2024-08-30 08:08:54 +02:00
Thomas Gautvedt f4427dd29e fix: correct a small spelling mistake (#2081) 2024-08-28 07:50:09 +00:00
Kristoffer Dalby cf6a606d74 fix route table migration wiping routes 0.22 -> 0.23 (#2076) 2024-08-27 18:54:28 +02:00
dragon2611 827e3e83ae Issue 2045, Feature Request (#2071)
Requiring someone to write a design doc/contribute to the feature shouldn't be a requirement for raising a feature request as users may lack the skills required to do this.
2024-08-27 11:03:51 +02:00
Kristoffer Dalby 9c4c286696 fix warning errs from beta2 (#2075)
* remove default false for use usernames causing warning

Fixes #2065

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Ensure DoH warnings are only emitted if err

Fixes #2064

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-08-23 17:17:37 +02:00
Kristoffer Dalby a68854ac33 upgrade go (1.23) and tailscale (1.72.1) (#2077) 2024-08-23 15:28:54 +02:00
github-actions[bot] 9bed76d481 flake.lock: Update (#2059) 2024-08-19 11:05:43 +00:00
Kristoffer Dalby 84cb5d0aed make acl_policy_path fatal if policy.path is not set (#2041) 2024-08-19 13:03:01 +02:00
Kristoffer Dalby f99497340b add coderabbit config (#2060)
Code Rabbit is one of these new fancy LLM code review tools. I am skeptical
but we can try it for free and it might provide us with some value to let
people get feedback while waiting for other people.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-08-19 10:06:55 +00:00
nadongjun fdc034e8ae Integrate GORM Logger with Zerolog and Add Configuration Options for Logging and Performance (#2040)
* Integrate GORM logger with zerolog and add custom GORM configuration options

* Add GormConfig struct to group GORM-related settings

* Update debug mode instruction in config-example.yaml

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>

---------

Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2024-08-19 11:47:52 +02:00
Kristoffer Dalby ac8491efec Redo DNS configuration (#2034)
this commit changes and streamlines the dns_config into a new
key, dns. It removes a combination of outdates and incompatible
configuration options that made it easy to confuse what headscale
could and could not do, or what to expect from ones configuration.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-08-19 11:41:05 +02:00
Chuangbo Li 022fb24cd9 Fix command get policy works with relative policy path (#2051) 2024-08-12 12:11:59 +02:00
github-actions[bot] fcd1183805 flake.lock: Update (#2052) 2024-08-11 05:48:15 +00:00
Kristoffer Dalby ece907d878 test embedded derp with derp updater, check client health (#2030) 2024-08-11 07:44:59 +02:00
github-actions[bot] 948d53f934 flake.lock: Update (#2042) 2024-08-04 06:35:46 +00:00
github-actions[bot] 06f07053eb flake.lock: Update (#2035) 2024-07-28 08:42:38 +00:00
nadongjun 4ad3f3c484 Fix data race issues in EphemeralGarbageCollector tests (#2023)
* Fix data race issues in EphemeralGarbageCollector tests

* Add defer for mutex unlock in TestEphemeralGarbageCollectorOrder

* Fix mutex unlock order in closure by updating defer placement
2024-07-22 15:11:57 +00:00
Steven Honson db7a4358e9 config-example.yaml: Remove reference to yaml for policy files (#2022) 2024-07-22 13:38:42 +00:00
github-actions[bot] b799245f1e flake.lock: Update (#2021) 2024-07-22 08:56:21 +02:00
greizgh 8571513e3c reformat code (#2019)
* reformat code

This is mostly an automated change with `make lint`.
I had to manually please golangci-lint in routes_test because of a short
variable name.

* fix start -> strategy which was wrongly corrected by linter
2024-07-22 08:56:00 +02:00
Kristoffer Dalby ca47d6f353 small cleanups (#2017) 2024-07-19 09:21:14 +02:00
Kristoffer Dalby 11fde62b8c remove custom contains funcs for slices.Contains (#2015)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-07-19 09:04:04 +02:00
Kristoffer Dalby 9e523d4687 move userprofiles into method on user struct (#2014)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-07-19 09:03:18 +02:00
Kristoffer Dalby 7e62031444 replace ephemeral deletion logic (#2008)
* replace ephemeral deletion logic

this commit replaces the way we remove ephemeral nodes,
currently they are deleted in a loop and we look at last seen
time. This time is now only set when a node disconnects and
there was a bug (#2006) where nodes that had never disconnected
was deleted since they did not have a last seen.

The new logic will start an expiry timer when the node disconnects
and delete the node from the database when the timer is up.

If the node reconnects within the expiry, the timer is cancelled.

Fixes #2006

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use uint64 as authekyid and ptr helper in tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add test db helper

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add list ephemeral node func

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* schedule ephemeral nodes for removal on startup

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix gorm query for postgres

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add godoc

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-07-18 10:01:59 +02:00
Pallab Pain 58bd38a609 feat: implements apis for managing headscale policy (#1792) 2024-07-18 07:38:25 +02:00
Rubens Peculis 00ff288f0c fix(1996): Implement register method enum converter (#2013)
Added a new function `RegisterMethodToV1Enum()` to Node, converting the internal register method string to the corresponding V1 Enum value. Included corresponding unit test in `node_test.go` to ensure correct conversion for various register methods.
2024-07-17 13:12:16 +02:00
greizgh 8823778d05 Add gofumpt to dev dependencies (#2010) 2024-07-17 13:12:02 +02:00
greizgh 74d27ee5fa Remove deprecated linters from golangci-lint (#2009)
When running lints, golangci-lint complained about removed
linters (which were already disabled).
This removes the relevant warnings.
2024-07-17 10:08:41 +02:00
github-actions[bot] 3f60ab23a6 Update flake.lock (#2011)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb?narHash=sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8%3D' (2024-07-05)
  → 'github:NixOS/nixpkgs/8b5a3d5a1d951344d683b442c0739010b80039db?narHash=sha256-po3TZO9kcZwzvkyMJKb0WCzzDtiHWD34XeRaX1lWXp0%3D' (2024-07-12)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-14 06:20:22 +00:00
github-actions[bot] eb1591df35 flake.lock: Update (#2000) 2024-07-07 06:16:36 +00:00
github-actions[bot] 89ada557bc flake.lock: Update (#1991) 2024-07-01 05:46:33 +00:00
Kristoffer Dalby 14a3f94f0c fix search domains and remove username from magicdns (#1987) 2024-06-26 13:44:40 +02:00
Kristoffer Dalby 4a34cfc4a6 Make write-ahead-log default and configurable for sqlite (#1985)
* correctly enable WAL log for sqlite

this commit makes headscale correctly enable write-ahead-log for
sqlite and adds an option to turn it on and off.

WAL is enabled by default and should make sqlite perform a lot better,
even further eliminating the need to use postgres.

It also adds a couple of other useful defaults.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-06-23 22:06:59 +02:00
Kristoffer Dalby 8f8f469c0a Remove allocations of lists before use (#1989)
* policy: remove allocs before appends in acls

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* notifier: make batcher tests stable/non-flaky

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* {db,derp,mapper}: dont allocate until append

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-06-23 22:06:50 +02:00
Lars Kiesow 69c33658f6 Fix android docs (#1976)
The current Tailscale app for Android looks and behaves differently.
This patch updates the documentation for that.
2024-06-23 00:52:23 +02:00
Kyhwana Pardus 99e91a9d8a Update reverse-proxy.md (#1986)
Add blurb about how cloudflare proxy/tunnels is not supported/will not work
2024-06-23 00:47:26 +02:00
github-actions[bot] dfc089ed6a flake.lock: Update (#1979) 2024-06-16 20:24:08 +00:00
Lars Kiesow 51676c668b Make registration screen easier to use (#1975) 2024-06-15 09:40:49 +02:00
github-actions[bot] 1f4b59566a flake.lock: Update (#1958) 2024-06-09 07:23:16 +00:00
Dongjun Na 5f9c26930c fixed typo and path (#1960) 2024-05-28 16:11:39 +00:00
Kristoffer Dalby 5a4e52b727 remove last_successful_update error check (#1959)
most of the time we dont even check this error and checking
the string for particular errors is very flake as different
databases (sqlite and psql) use different error messages, and
some users might have it in other languages.

Fixes #1956

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-27 11:53:37 +01:00
github-actions[bot] 51b56ba447 Update flake.lock (#1952)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/b3fcfcfabd01b947a1e4f36622bbffa3985bdac6?narHash=sha256-iJYnKMtLi5u6hZhJm94cRNSDG5Rz6ZzIkGbhPFtDRm0%3D' (2024-05-15)
  → 'github:NixOS/nixpkgs/02923630b89aa1ab36ef8e422501a6f4fd4b2016?narHash=sha256-OhysviwHQz4p2HZL4g7XGMLoUbWMjkMr/ogaR3VUYNA%3D' (2024-05-18)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-24 10:30:11 +01:00
Kristoffer Dalby c8ebbede54 Simplify map session management (#1931)
This PR removes the complicated session management introduced in https://github.com/juanfont/headscale/pull/1791 which kept track of the sessions in a map, in addition to the channel already kept track of in the notifier.

Instead of trying to close the mapsession, it will now be replaced by the new one and closed after so all new updates goes to the right place.

The map session serve function is also split into a streaming and a non-streaming version for better readability.

RemoveNode in the notifier will not remove a node if the channel is not matching the one that has been passed (e.g. it has been replaced with a new one).

A new tuning parameter has been added to added to set timeout before the notifier gives up to send an update to a node.

Add a keep alive resetter so we wait with sending keep alives if a node has just received an update.

In addition it adds a bunch of env debug flags that can be set:

- `HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS`: make certain metrics include per node.id, not recommended to use in prod. 
- `HEADSCALE_DEBUG_PROFILING_ENABLED`: activate tracing 
- `HEADSCALE_DEBUG_PROFILING_PATH`: where to store traces 
- `HEADSCALE_DEBUG_DUMP_CONFIG`: calls `spew.Dump` on the config object startup
- `HEADSCALE_DEBUG_DEADLOCK`: enable go-deadlock to dump goroutines if it looks like a deadlock has occured, enabled in integration tests.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-24 10:15:34 +02:00
Stefan Majer 8185a70dc7 Fix typos (#1860)
* Fix typos

* trigger GitHub actions

* remove kdiff3 orig files

* fix unicode

* remove unnecessary function call

* remove unnecessary comment

* remove unnecessary comment

---------

Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-05-19 23:49:27 +02:00
ohdearaugustin 2dc62e981e move debug image to distroless (#1950) 2024-05-19 11:17:37 +02:00
Kristoffer Dalby 5ad0aa44cb update tailscale go dep (#1948)
* update tailscale go dep

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update gorm go dep

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update grpc go dep

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update golang.org go dep

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update rest of go dep

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-17 08:58:33 -04:00
github-actions[bot] 723a0408a3 flake.lock: Update (#1897) 2024-05-16 14:01:34 +00:00
dependabot[bot] 30986c29cd Bump github.com/docker/docker (#1947) 2024-05-16 04:42:53 +00:00
dependabot[bot] faa57ddc28 Bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.3 (#1945) 2024-05-16 03:02:12 +00:00
dependabot[bot] fff229f4f6 Bump github.com/jackc/pgx/v5 from 5.5.3 to 5.5.4 (#1946) 2024-05-16 03:01:18 +00:00
dependabot[bot] fd4f921281 Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#1944) 2024-05-16 02:15:41 +00:00
dependabot[bot] 151f224a98 Bump golang.org/x/net from 0.22.0 to 0.23.0 (#1943)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-16 01:22:02 +00:00
Sandro a9763c9692 Initialize log config earlier to prevent trace messages being printed early on (#1939)
like TRC DNS configuration loaded dns_config={....}
2024-05-15 20:40:30 -04:00
MichaelKo 7fd2485000 Restore foreign keys and add constraints (#1562)
* fix #1482, restore foregin keys, add constraints

* #1562, fix tests, fix formatting

* #1562: fix tests

* #1562: fix local run of test_integration
2024-05-15 20:40:14 -04:00
Dan Pastusek 2bac80cfbf [DOCS] Make linux installation instructions more clear (#1927)
* Make linux installation instructions more clear

* Update running-headscale-linux.md
2024-05-06 20:06:30 +02:00
Michael Savage 93a915c096 Update OpenBSD installation docs for 2024 (#1915) 2024-05-06 20:03:21 +02:00
Kristoffer Dalby 622aa82da2 ensure expire routines are cleaned up (#1924)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-02 15:57:53 +00:00
Kristoffer Dalby a9c568c801 trace log and notifier shutdown (#1922)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-02 13:39:19 +02:00
Kristoffer Dalby 1c6bfc503c fix preauth key logging in as previous user (#1920)
* add test case to reproduce #1885

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix preauth key issue logging in as wrong user

Fixes #1885

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add test to gh

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-02 11:53:16 +02:00
Kristoffer Dalby 55b35f4160 fix issue preveting get node when disco is missing (#1919)
Fixed #1816

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-05-01 17:06:42 +02:00
Kristoffer Dalby d5ed8bc074 error if no prefix is configured (#1918)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-04-30 11:11:29 +02:00
Kristoffer Dalby 87e2ae4d52 add autogroup:internet, fix reduce filter rules (#1917) 2024-04-30 07:23:16 +02:00
Juan Font ff427ccb78 Apply suggestions from code review
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-29 23:04:02 +02:00
Juan Font 39277844dd Apply suggestions from code review
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-29 23:04:02 +02:00
Juan Font 50a7d15769 Update CONTRIBUTING.md
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-29 23:04:02 +02:00
Juan Font d740ee489e Update CONTRIBUTING.md
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-29 23:04:02 +02:00
Juan Font 10e37ec28d Add contributing document 2024-04-29 23:04:02 +02:00
Kristoffer Dalby cb0b495ea9 batch updates in notifier (#1905) 2024-04-27 10:47:39 +02:00
Juan Font fef8261339 Do not access node ID when node is not found (#1912) 2024-04-24 07:44:35 +02:00
Kristoffer Dalby c62d5570f2 remove multistep build, build go last, allowing cached build layers (#1903) 2024-04-24 07:44:07 +02:00
Kristoffer Dalby 318d5d2b21 replace issue templates with github issue forms
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-04-22 23:10:34 +02:00
Kristoffer Dalby 9229d17bbe remove examples/, and kustomize (#1906)
this directory is unmaintained and not verified, if it should be restored, it should end up
under the community docs effort.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-04-22 21:21:38 +02:00
Juan Font aba4b36030 Clarify relation with Tailscale (#1908) 2024-04-22 20:37:59 +02:00
Juan Font bd047928f7 Move pprof to metrics router (#1902) 2024-04-21 22:08:59 +02:00
ChengenH 9375b09206 chore: use errors.New to replace fmt.Errorf with no parameters will much better
Signed-off-by: ChengenH <hce19970702@gmail.com>
2024-04-21 20:23:25 +02:00
Kristoffer Dalby ba614a5e6c metrics, tuning in tests, db cleanups, fix concurrency issue (#1895) 2024-04-21 18:28:17 +02:00
oftenoccur 7d8178406d chore: fix function names in comment (#1866)
* chore: fix function names in comment

Signed-off-by: oftenoccur <ezc5@sina.com>

---------

Signed-off-by: oftenoccur <ezc5@sina.com>
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-21 18:19:38 +02:00
ohdearaugustin 8394208856 fix prettier 2024-04-21 17:32:41 +02:00
Arnaud Dezandee 803269a64c docs(readme): change contributors section (#1889) 2024-04-21 16:48:33 +02:00
Carson Yang d6ec31c4e0 docs: Add docs for running headscale on sealos (#1666)
* docs: Add docs for running headscale on sealos

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>

* run prettier

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-21 16:43:31 +02:00
Juan Font 68503581a0 Add test stage to docs (#1893)
* Add test stage to docs

Add new file with docs tets

Run only in pulls

* set explicit python version

* Revert "set explicit python version"

This reverts commit 4dd7b81f26.

* docs/requirements: update mkdocs-material

---------

Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-21 16:33:22 +02:00
Juan Font e2afd30b1c Add the latest UI to the website 2024-04-18 14:55:59 +02:00
Juan Font c906aaf927 Allow to remove forced tags of a node
Set as empty StringList
2024-04-18 09:55:55 +02:00
Juan Font 580f96ce83 Remove unused node check interval 2024-04-17 20:20:44 +02:00
Juan Font c4c8cfe5ea Fix crash when a prefix family was empty 2024-04-17 15:28:06 +02:00
Kristoffer Dalby 40953727cf fix ip migration
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-04-17 12:36:12 +02:00
Cas de Reuver d4af0c386c Log available update as warning (#1877) 2024-04-17 11:22:53 +02:00
Kristoffer Dalby 2ce23df45a Migrate IP fields in database to dedicated columns (#1869) 2024-04-17 07:03:06 +02:00
Sergey Aksenov 85cef84e17 use newer fork of termcolor (#1842)
jagottsicher's fork fixed a bug in Windows implementation. While Windows may be not intended as a target platform,
some contributors may prefer it for development.

Also ran go mod tidy, thus two more unnecessary packages are removed from go.sum
2024-04-16 23:48:51 +02:00
Kristoffer Dalby 7d62e9fce5 move "embedded derp" settings into With options for integration tests (#1872) 2024-04-16 21:37:25 +02:00
Kristoffer Dalby 60f0cf908c more log.Error -> fmt.Errorf cleanup 2024-04-15 12:31:53 +02:00
Kristoffer Dalby 1704977e76 improve testing of route failover logic
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-04-15 12:31:53 +02:00
Kristoffer Dalby bf4fd078fc clean up use of log.Error where errors could be wrapped
Replace a lot of occurences of log.Error with fmt.Errorf,
bubbling the error up the chain instead.
2024-04-15 12:31:53 +02:00
Kristoffer Dalby 58c94d2bd3 Rework map session
This commit restructures the map session in to a struct
holding the state of what is needed during its lifetime.

For streaming sessions, the event loop is structured a
bit differently not hammering the clients with updates
but rather batching them over a short, configurable time
which should significantly improve cpu usage, and potentially
flakyness.

The use of Patch updates has been dialed back a little as
it does not look like its a 100% ready for prime time. Nodes
are now updated with full changes, except for a few things
like online status.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-04-15 12:31:53 +02:00
github-actions[bot] dd693c444c flake.lock: Update (#1848) 2024-04-14 06:46:44 +00:00
Brandon Schneider 2858ab402a Add new shasum for flake.nix 2024-04-11 08:01:26 +02:00
Brandon Schneider 7bea885b8c Updated dependencies, fixing segfault for OpenBSD
Fixes #1857
2024-04-11 08:01:26 +02:00
Riezebos 84de1854f8 Run prettier 2024-04-10 22:19:47 +02:00
Riezebos 6efc50789d Update docs/faq.md
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2024-04-10 22:19:47 +02:00
Riezebos 0fcfd643fa More concise 2024-04-10 22:19:47 +02:00
Riezebos bdf54e802e Update answer based on comment 2024-04-10 22:19:47 +02:00
Riezebos dbe32829a1 Update docs/faq.md
Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-04-10 22:19:47 +02:00
Riezebos 2fb7428ba9 Add FAQ question about using the same machine as server and client 2024-04-10 22:19:47 +02:00
Daniel Lo Nigro 8a8e25a8d1 [docs] Use modern Apt command to install package (#1420) 2024-03-22 20:32:01 +01:00
Maja Bojarska 4d9021047f Fix/improve documentation formatting (#1575)
* Fix docs formatting

* .prettierignore add docu

---------

Co-authored-by: ohdearaugustin <ohdearaugustin@users.noreply.github.com>
2024-03-22 19:55:20 +01:00
ohdearaugustin 74ff14eb30 update docs workflow (#1832) 2024-03-22 19:50:35 +01:00
Sandro c1d4fef194 Downgrade update sent to debug (#1843) 2024-03-22 09:28:10 +00:00
George Pchelkin 785b150467 Fix typo in docs - DB file path (#1546) 2024-03-19 22:43:43 +01:00
github-actions[bot] 20bf3777d3 docs(README): update contributors (#1834)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-19 22:39:43 +01:00
github-actions[bot] c29eddded3 flake.lock: Update (#1833) 2024-03-17 09:01:57 +01:00
zx b477e5f366 Update docker related doc (#1421) 2024-03-14 23:01:33 +01:00
Sandro 95004de5e8 docs/reverse-proxy: use standard map, correct X-Forwarded-Proto variable for nginx (#1790) 2024-03-14 22:56:55 +01:00
Vitalij Dovhanyc ef26f58085 update gh workflow actions (#1809) 2024-03-13 13:43:06 +01:00
github-actions[bot] 1d3eae8861 Update flake.lock (#1657)
Flake lock file updates:

• Updated input 'flake-utils':
    'github:numtide/flake-utils/1ef2e671c3b0c19053962c07dbda38332dcebf26' (2024-01-15)
  → 'github:numtide/flake-utils/d465f4819400de7c8d874d50b982301f28a84605' (2024-02-28)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/442d407992384ed9c0e6d352de75b69079904e4e' (2024-02-09)
  → 'github:NixOS/nixpkgs/0e7f98a5f30166cbed344569426850b21e4091d4' (2024-03-09)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-10 12:40:31 +01:00
Juan Font a244eabd03 Ephemeral keys can now be reusable and non-reusable
Fixes the issue reported in #1712. In Tailscale SaaS, ephemeral keys can be single-user or reusable. Until now, our ephemerals were only reusable. This PR makes us adhere to the .com behaviour.
2024-03-03 12:37:37 +01:00
Vitalij Dovhanyc e15a08326c simplify integration testing with matrix jobs (#1799)
* simplify integration testing with matrix jobs

* check if all of the integration tests are in the test-integration workflow
2024-03-02 22:36:38 +01:00
Kristoffer Dalby c9966ba6c2 fix postgres migration (#1802) 2024-03-01 19:11:46 +01:00
Kristoffer Dalby 7a920ee701 move debug inside if in docker goreleaser tag (#1783)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-19 14:03:31 +01:00
Kristoffer Dalby 8b2c31aabc fix ifs in goreleaser gotemplate (#1781)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-19 10:50:52 +01:00
Kristoffer Dalby 5dbd59ca55 Get integration test netmap from watch-ipn command (#1729) 2024-02-18 23:22:18 +01:00
Kristoffer Dalby 3f162c212c drop unused last_successful_update field from node table (#1754) 2024-02-18 23:22:07 +01:00
Kristoffer Dalby 384ca03208 new IP allocator and add postgres to integration tests. (#1756) 2024-02-18 19:31:29 +01:00
Kristoffer Dalby f581d4d9c0 replace linter actions with nix to ensure consistent version (#1773) 2024-02-18 13:33:40 +01:00
Kristoffer Dalby b60ee9db54 improve errors for missing directories (#1765)
* improve errors for missing directories

Fixes #1761
Updates #1760

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update container docs

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog with /var changes

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-17 13:36:19 +01:00
Kristoffer Dalby c73e8476b9 make database configuration change breaking (#1766)
A lot of things are breaking in 0.23 so instead of having this
be a long process, just rip of the plaster.

Updates #1758

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-17 13:18:15 +01:00
Kristoffer Dalby 6055d0b397 rollback gorm, broke migration #1755 (#1762)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-17 13:17:16 +01:00
Kristoffer Dalby 1904d79e90 rework docker tags (#1763)
* rework docker tags

This commit tries to align the new docker tags with the old schema

A prerelease will end up with the following tags:

- unstable
- v0.23.0-alpha3
- 0.23.0.alpha3
- sha-1234adsfg

A release will end up with:

- latest
- stable
- v0.23.0
- v0.23
- v0
- 0.23.0
- 0.23
- 0
- sha-1234adsfg

All of the builds will also have a `-debug` version.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-17 13:15:31 +01:00
Sandro 1b01b9e14f Reduce poll logging to debug (#1746)
Before this logged ~10 messages every few seconds and quite spamming the
log.
2024-02-16 10:32:23 +01:00
Calvin Figuereo-Supraner 5717c8255a Use result of fmt.Errorf call (#1668) 2024-02-15 11:02:54 +01:00
Kristoffer Dalby c42f25bd72 fix ko dockerhub builds (#1751)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-15 10:23:01 +01:00
Stepan 82c64f682c Docs: fix path to nologin shell (#1610) 2024-02-15 08:43:50 +01:00
Linus 7afc2fd180 TLS documentation updates (#1733)
* TLS documentation updates

Move "Bring your own certificates" to the top
  since the letsencrypt section is now much longer, it seems wrong to
  keep such a short section way down at the bottom.

Restructure "Challenge types" into separate sections

Add technical description of letsencrypt renewals
  this aims to answer:
  - what can be expected in terms of renewals
  - what logs can be expected (none)
  - how to validate that renewal happened successfully
  - the reason for some of the 'acme/autocert' logs, or at least
    some best-effort assumptions

* +prettier
2024-02-15 08:43:01 +01:00
Kristoffer Dalby 5109af94a3 login to docker registries (#1744)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-12 16:01:56 +01:00
Kristoffer Dalby 905fdaa409 remove quotes from command (#1742)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-12 15:09:19 +01:00
Kristoffer Dalby 0333e97630 Build docker images with ko (goreleaser) (#1716)
* make dockerfiles testing only note

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* setup ko image builder for goreleaser

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove release-docker

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove non-debug Dockerfile

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* Comments and lint

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

* build debug images based on debian

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

* remove debug flag for goreleaser

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2024-02-12 14:53:07 +01:00
DeveloperDragon e3553aae50 Allow when user has only a subnet route (#1734)
* Add test because of issue 1604

* Add peer for routes

* Revert previous change to try different way to add peer

* Add traces

* Remove traces

* Make sure tests have IPPrefix comparator

* Get allowedIps before loop

* Remove comment

* Add composite literals :)
2024-02-12 11:44:37 +01:00
Pallab Pain 47405931c6 feat(apikey): adds command to delete api keys (#1702)
We currently do not have a way to clean up api keys. There may be cases
where users of headscale may generate a lot of api keys and these may
end up accumulating in the database. This commit adds the command to
delete an api key given a prefix.
2024-02-12 11:31:21 +01:00
foosinn c4beb0b8af document setting oidc client secret cia env (#1649) 2024-02-12 09:20:31 +01:00
Kristoffer Dalby 3f2b238a46 Upgrade to Go 1.22 and update deps (#1728)
* go 1.22

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* upgrade tailscale

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

* add script for getting nix sri for go

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update deps and only build headscale

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

* add script to update all deps

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

* get golines and golangci from upstream nix

Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2024-02-12 09:11:33 +01:00
Kristoffer Dalby 68a8ecee7a Prepare notify channel before sending first update (#1730)
* create channel before sending first update

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* do not notify on register, wait for connect

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-12 09:11:17 +01:00
lööps c3257e2146 docs(windows-client): add Windows registry command (#1658)
Add Windows registry command to create the `Tailscale IPN` path before setting properties.
2024-02-09 19:16:17 +01:00
Pallab Pain 9047c09871 feat: add pqsql configs for open and idle connections (#1583)
When Postgres is used as the backing database for headscale,
it does not set a limit on maximum open and idle connections
which leads to hundreds of open connections to the Postgres
server.

This commit introduces the configuration variables to set those
values and also sets default while opening a new postgres connection.
2024-02-09 17:34:28 +01:00
Kristoffer Dalby 91bb85e7d2 Update bug_report.md (#1672) 2024-02-09 07:27:13 +01:00
Kristoffer Dalby 94b30abf56 Restructure database config (#1700) 2024-02-09 07:27:00 +01:00
Kristoffer Dalby 00e7550e76 Add assert func for verifying status, netmap and netcheck (#1723) 2024-02-09 07:26:41 +01:00
Kristoffer Dalby 83769ba715 Replace database locks with transactions (#1701)
This commits removes the locks used to guard data integrity for the
database and replaces them with Transactions, turns out that SQL had
a way to deal with this all along.

This reduces the complexity we had with multiple locks that might stack
or recurse (database, nofitifer, mapper). All notifications and state
updates are now triggered _after_ a database change.


Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-02-08 17:28:19 +01:00
DeveloperDragon cbf57e27a7 Login with OIDC after having been logged out (#1719) 2024-02-05 10:45:35 +01:00
derelm 4ea12f472a Fix failover to disabled route #1706 (#1707)
* fix #1706 - failover should disregard disabled routes during failover

* fixe tests for failover; all current tests assume routes to be enabled

* add testcase for #1706 - failover to disabled route
2024-02-03 15:30:15 +01:00
danielalvsaaker b4210e2c90 Trim client secret after reading from file (#1697)
Reading from file will include a line break, which results in a mismatching client secret
compared to reading directly from the config.
2024-01-25 09:53:34 +01:00
dyz a369d57a17 fix node expire error due to type in gorm model Update (#1692)
Fixes #1674

Signed-off-by: fortitude.zhang <fortitude.zhang@gmail.com>
2024-01-21 17:38:24 +01:00
Kristoffer Dalby 1e22f17f36 node selfupdate and fix subnet router when ACL is enabled (#1673)
Fixes #1604

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-01-18 17:30:25 +01:00
Kristoffer Dalby 65376e2842 ensure renabled auto-approve routes works (#1670) 2024-01-18 16:36:47 +01:00
Alexander Halbarth 7e8bf4bfe5 Add Customization Options to DERP Map entry of integrated DERP server (#1565)
Co-authored-by: Alexander Halbarth <alexander.halbarth@alite.at>
Co-authored-by: Bela Lemle <bela.lemle@alite.at>
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2024-01-16 16:04:03 +01:00
Kristoffer Dalby 3b103280ef implement selfupdate and pass expiry (#1647) 2024-01-05 10:41:56 +01:00
Kristoffer Dalby a592ae56b4 fix issue where advertise tags causes hang (#1669)
Fixes #1665

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-01-04 21:26:49 +01:00
Kristoffer Dalby 054b06d45d add 1.54 and 1.56 to integration tests (#1652)
* add 1.54 and 1.56 to integration tests

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix bug where we tested random versions, now sorted

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-01-02 10:41:40 +01:00
Kristoffer Dalby 55ca078f22 embed (hidden) tailsql for debugging (#1663)
Signed-off-by: Kristoffer Dalby <kristoffer@dalby.cc>
2023-12-20 21:47:48 +01:00
Kristoffer Dalby 6049ec758c add versioned migrations (#1644) 2023-12-10 15:46:14 +01:00
Kristoffer Dalby ac910fd44c make stale shorter (#1646) 2023-12-10 15:30:30 +01:00
Kristoffer Dalby 9982ae5f09 add breaking entry of derp priv key (#1641) 2023-12-10 15:23:23 +01:00
Kristoffer Dalby cf8ffea154 turn off grpc communication logging (#1640) 2023-12-10 15:22:59 +01:00
Kristoffer Dalby 790bbe5e8d fix hostinfo db column spelling (#1642) 2023-12-10 15:22:26 +01:00
github-actions[bot] 2c8fc9b061 Update flake.lock (#1632)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-10 09:50:39 +01:00
github-actions[bot] b359939812 docs(README): update contributors (#1639)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-10 08:56:01 +01:00
Kristoffer Dalby f65f4eca35 ensure online status and route changes are propagated (#1564) 2023-12-09 18:09:24 +01:00
Kristoffer Dalby 0153e26392 upgrade go dependencies (#1628) 2023-11-30 14:41:31 +01:00
Andrei Pechkurov 6c9c55774b Update xsync to v3.0.2 (#1597)
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-11-29 15:47:14 +01:00
github-actions[bot] 2f558bee80 Update flake.lock (#1598)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-29 15:11:28 +01:00
Azamat H. Hackimov 4c608a4b58 Fix Github Actions docs pipeline (#1622) 2023-11-29 15:11:00 +01:00
JesseBot f13cf64578 Docs: Update running-headscale-container.md - fix link to example config (#1618) 2023-11-29 15:10:21 +01:00
MichaelKo 85e92db505 Enhance pipeline stability and automatically retry unstable tests (#1566)
* add test retry to action

* add test retry to action
2023-11-27 18:32:52 +01:00
Kristoffer Dalby a59aab2081 Remove support for non-noise clients (pre-1.32) (#1611) 2023-11-23 08:31:33 +01:00
Kristoffer Dalby b918aa03fc move to use tailscfg types over strings/custom types (#1612)
* rename database only fields

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use correct endpoint type over string list

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove HostInfo wrapper

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* wrap errors in database hooks

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-11-21 18:20:06 +01:00
Kristoffer Dalby ed4e19996b Use tailscale key types instead of strings (#1609)
* upgrade tailscale

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* make Node object use actualy tailscale key types

This commit changes the Node struct to have both a field for strings
to store the keys in the database and a dedicated Key for each type
of key.

The keys are populated and stored with Gorm hooks to ensure the data
is stored in the db.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use key types throughout the code

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* make sure machinekey is concistently used

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use machine key in auth url

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix web register

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use key type in notifier

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* fix relogin with webauth

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-11-19 22:37:04 +01:00
Kristoffer Dalby c0fd06e3f5 remove the use key stripping and store the proper keys (#1603) 2023-11-16 17:55:29 +01:00
github-actions[bot] 2af71c9e31 docs(README): update contributors (#1592)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-06 14:07:35 +01:00
Lucalux 42b7f8f65a redundant line removed from systemd.service (#1587) 2023-11-06 13:45:34 +01:00
github-actions[bot] 48c7d763d5 Update flake.lock (#1589)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/e12483116b3b51a185a33a272bf351e357ba9a99' (2023-09-21)
  → 'github:NixOS/nixpkgs/a0b3b06b7a82c965ae0bb1d59f6e386fe755001d' (2023-11-05)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-06 12:57:06 +01:00
Kristoffer Dalby d0d6438337 Add workflow to autoupdate flake.lock deps (#1588)
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-11-06 12:48:52 +01:00
Kristoffer Dalby fb4ed95ff6 Upgrade Go 1.21, Tailscale 1.50 and add Capability version support (#1563) 2023-09-28 12:33:53 -07:00
Kristoffer Dalby 01b85e5232 remove readonly case for mapresponse, dont think it is used (#1556) 2023-09-25 14:27:24 -07:00
Kristoffer Dalby 64c0a6523f Set online status in lite requests (#1555) 2023-09-25 14:27:14 -07:00
Kristoffer Dalby 84fbca97f7 add note about db backup to changelog (#1560) 2023-09-25 14:27:03 -07:00
Kristoffer Dalby 56cf4b082e Add github stale action (#1559) 2023-09-25 09:33:31 -05:00
github-actions[bot] 6cd0f77511 docs(README): update contributors (#1558)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-24 14:34:53 -07:00
Kristoffer Dalby b27e8ab5a1 add 0.23.0 changelog entry (#1557) 2023-09-24 14:18:19 -07:00
Juan Font 0030af3fa4 Rename Machine to Node (#1553) 2023-09-24 06:42:05 -05:00
Kristoffer Dalby 096ac31bb3 handle route updates correctly
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby c957f893bd Return simple responses immediatly
This commit rearranges the poll handler to immediatly accept
updates and notify its peers and return, not travel down the
function for a bit. This reduces the DB calls and other
holdups that isnt necessary to send a "lite response", a
map response without peers, or accepting an endpoint update.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 217ccd6540 improve debug logging, rw lock for notifier
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 3bef63bb80 Remove LastSuccessfulUpdate from Machine
This field is no longer used, it was used in our old state
"algorithm" to determine if we should send an update.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 591ff8d347 add pprof endpoint
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 14f8c1ba34 order path
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby ca4a48afbb gitignore infolder tailscale
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 9ccf87c566 add lock around saving ts clients
Closes #1544

Co-Authored-By: Patrick Huang <huangxiaoman@gmail.com>
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 4c12c02e71 Upgrade go and debian in headscale docker
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 2434d76ade give ci more tollerance for timeouts
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 432e975a7f move MapResponse peer logic into function and reuse
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 387aa03adb Remove database from Mapper
This commit changes the internals of the mapper to
track all the changes to peers over its lifetime.

This means that it no longer depends on the database
and this should hopefully help with locks and timing issues.
When the mapper is created, it needs the current list of peers,
the world view, when the polling session was started. Then as
update changes are called, it tracks the changes and generates
responses based on its internal list.

As a side, the types.Machines and types.MachinesP, as well as
types.Machine being passed as a full struct and pointer has been
changed to always be pointers, everywhere.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 3b0749a320 Update packetfilter when peers change
Previously we did not update the packet filter
when nodes changed, which would cause new nodes
to be missing from packet filters of old nodes.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby a8079a2096 rearrange poll, lock, notify
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 593b3ad981 filter out peers without endpoints
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby e90a669951 remove retries for pings in tsic
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 9c5301ee2e add maprequest to all mapper calls
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 13a7285658 fix lint
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby e55fe0671a only send lite map responses when omitpeers
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby e0ba325b3b additional debug logging, use mapper pointer
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby eff529f2c5 introduce rw lock for db, ish...
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby a1a3ff4ba8 disable online map by default for now
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 78268d78a0 add debug option to save all map responses
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby f73172fb21 add less/jq to hs debug container
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby b7c6e0ec88 add annoying linter to golangci
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 2d87085cbc rearrange channel closing defers
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 13fe4ec91b add script to run integration tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 53a9e28faf Add missing return in shutdown
Co-Authored-By: Jason <armooo@armooo.net>
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 4b65cf48d0 Split up MapResponse
This commits extends the mapper with functions for creating "delta"
MapResponses for different purposes (peer changed, peer removed, derp).

This wires up the new state management with a new StateUpdate struct
letting the poll worker know what kind of update to send to the
connected nodes.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 66ff1fcd40 Replace the timestamp based state system
This commit replaces the timestamp based state system with a new
one that has update channels directly to the connected nodes. It
will send an update to all listening clients via the polling
mechanism.

It introduces a new package notifier, which has a concurrency safe
manager for all our channels to the connected nodes.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 056d3a81c5 format with prettier 3.0
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-19 10:20:21 -05:00
Kristoffer Dalby 7edc953d35 Update tsic.go 2023-09-05 08:47:43 +02:00
Kristoffer Dalby 12a04f9459 fix relogin test, pass accept route flag
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-05 08:47:43 +02:00
Kristoffer Dalby 1766e6b5df General fixups discovered by checking errors
There was a lot of tests that actually threw a lot of errors and that did
not pass all the way because we didnt check everything. This commit should
fix all of these cases.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-05 08:47:43 +02:00
Kristoffer Dalby f8a58aa15b introduce a version subset we must test against
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-05 08:47:43 +02:00
Kristoffer Dalby b4a4d0f760 Handle errors in integration test setups
Thanks @kev-the-dev

Closes #1460

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-05 08:47:43 +02:00
Kristoffer Dalby 63caf9a222 update flake, fix prettier lint
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-05 08:47:43 +02:00
Kristoffer Dalby 47255d267e add script to run integration tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-05 08:47:43 +02:00
Juan Font e3acc95859 Send logs to stderr, rather than stdout 2023-08-11 20:59:38 +02:00
qzydustin fb203a2e45 Format code 2023-07-23 23:46:02 +02:00
qzydustin 6567af7730 Fix IP Address Order Bug 2023-07-23 23:46:02 +02:00
Kristoffer Dalby 23a3adf8d2 use cmp.Diff instead of reflect.DeepEqual
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-07-01 21:55:47 +02:00
Kristoffer Dalby 665a3cc666 add generic logerr func to shorten code
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-07-01 08:35:04 +01:00
Kristoffer Dalby fe75b71620 use nix caching and docker caching in CI
Also bumps tailscale version to trigger build and fixes a CLI test
that had the wrong capitalisation

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-29 12:49:36 +01:00
Kristoffer Dalby 19dc0ac702 rename acl "get" funcs to "expand" for consistency
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 155cc072f7 migrate last acl tests away from database
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby e2c08db3b5 reduce filter rules at the end, so we filter nodes correctly
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby fcdc7a6f7d remove redundant tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 88ca2501d1 only send relevant filterrules to nodes
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 2675ff4b94 make parse destination string into a func
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 717abe89c1 remove "stripEmailDomain" argument
This commit makes a wrapper function round the normalisation requiring
"stripEmailDomain" which has to be passed in almost all functions of
headscale by loading it from Viper instead.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 161243c787 make generateFilterRules take machine and peers
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 9c425a1c08 Finish SSH
This commit allows SSH rules to be assigned to each relevant not and
by doing that allow SSH to be rejected, completing the initial SSH
support.

This commit enables SSH by default and removes the experimental flag.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby db6cf4ac0a make GenerateFilterRules take machine and peers
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Kristoffer Dalby 35770278f7 upgrade tailscale
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00
Viacheslav Sychov 36c9b5ce74 Adjust the template for the OIDC callback login page 2023-06-14 18:08:49 +02:00
Kristoffer Dalby 0562260fe0 rename handler files
This commit renames a bunch of files to try to make it a bit less confusing;

protocol_ is now auth as they contained registration, auth and login/out flow
protocol_.*_poll is now poll.go
api.go and other generic handlers are now handlers.go

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby c1218ad3c2 move reminder of dns funcs to util
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby d36336a572 fix lint
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby 80ea87c032 move derp_server to derp server module
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby 8c4c4c8633 move derp.go to derp module
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby 2289a2acbf move Config definitions into types
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Viacheslav Sychov c72401a99b Fix IPv6 in ACLs 2023-06-08 16:34:15 +02:00
Kristoffer Dalby 725bbd7408 Remove variables and leftovers of pregenerated ACL content
Prior to the code reorg, we would generate rules from the Policy and
store it on the global object. Now we generate it on the fly for each node
and this commit cleans up the old variables to make sure we have no
unexpected side effects.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby 084d1d5d6e Add initial test for mapresponse
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby f9f6e1557a Remove complicated testcase obsoleated by tailNode test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby 5bad48a24e remove DB dependency of tailNode conversion, add test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby bce8427423 Map route into machine
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
Kristoffer Dalby f7f472ae07 introduce mapper package
The mapper package contains functions related to creating and marshalling
reponses to machines.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-08 16:34:15 +02:00
github-actions[bot] 699655a93f docs(README): update contributors 2023-05-27 16:21:42 +02:00
Kristoffer Dalby feb15365b5 Split code into modules
This is a massive commit that restructures the code into modules:

db/
    All functions related to modifying the Database

types/
    All type definitions and methods that can be exclusivly used on
    these types without dependencies

policy/
    All Policy related code, now without dependencies on the Database.

policy/matcher/
    Dedicated code to match machines in a list of FilterRules

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-26 12:24:50 +02:00
Kristoffer Dalby 14e29a7bee create DB struct
This is step one in detaching the Database layer from Headscale (h). The
ultimate goal is to have all function that does database operations in
its own package, and keep the business logic and writing separate.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-26 12:24:50 +02:00
Juan Font b01f1f1867 Clean apt 2023-05-12 10:09:36 +02:00
Juan Font c027ef0f6c Added changelog for 0.22.3 2023-05-12 10:09:36 +02:00
Six db97a7ab10 Add ca-certificates to Dockerfile 2023-05-12 09:24:55 +02:00
Kristoffer Dalby 252342a0a5 update nix hash
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby cdf3c47d63 changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 61a2915f17 port reminder of integrationv1 test to v2
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby a16f0c9f60 clean up unused legacy stuff
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 52ad138c32 update dependency path for integration
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby d2413d0a2f move swagger to root for now
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 51dc0d5784 update dependency path for cmd
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 2d365c8c9c inline old acl hujson tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby f2c1d1b8f9 regenerate gen
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 2d6356fa13 move templates
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 3bfc598ccc move generated files
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 3683d3e82f rename package name to hscontrol
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Kristoffer Dalby 4a7921ead5 move all go files from root to hscontrol
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-10 20:47:51 +02:00
Juan Font 22e397e0b6 Use common path in unix_socket default setting 2023-05-10 18:18:04 +02:00
Juan Font c7db99d6ca Update changelog + prepare for 0.22.2 2023-05-10 18:18:04 +02:00
Juan Font f73354b4f4 Create default sock path in Docker 2023-05-10 18:18:04 +02:00
Juan Font 4c8f8c6a1c Ditch distroless for Docker image
distroless has proven a mantenance burden for us, and it has caused headaches for user when trying to debug issues in the container.

And in 2023, 20MB of extra disk space are neglectible.
2023-05-10 18:18:04 +02:00
Juan Font 997e93455d Added web ui section
Added discord
2023-05-10 16:16:12 +02:00
Juan Font 9f381256c4 Update config.go 2023-05-10 14:25:13 +02:00
Juan Font f60c5a1398 Fix socket location in config.go 2023-05-10 14:25:13 +02:00
Juan Font 5706f84cb0 Revert "Revert unix_socket to default value"
This reverts commit ca54fb9f56.
2023-05-10 14:25:13 +02:00
Juan Font 9478c288f6 Added missing file 2023-05-10 10:26:21 +02:00
Juan Font 6043ec87cf Update mkdocs.yml 2023-05-10 09:49:13 +02:00
Juan Font dcf2439c61 Improved website
More docs
2023-05-10 09:49:13 +02:00
Kristoffer Dalby ba45d7dbd3 update readme and templates to clarify scope (#1437)
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-05-10 08:03:13 +01:00
Juan Font bab4e14828 Further clarification on unsupported ranges in config example 2023-05-08 12:47:08 +02:00
Juan Font 526e568e1e Update changelog 2023-05-07 15:27:30 +02:00
Juan Font 02ab0df2de Disable and Delete route must affect both exit routes (IPv4 and IPv6)
Fixed linting
2023-05-07 15:27:30 +02:00
Juan Font 7338775de7 Give a warning when users have set an unsupported prefix
Fix minor log issue

Removed debug meessage
2023-05-07 13:14:32 +02:00
Sebastian Muszytowski 00c514608e Add IP forwarding requirement to documentation
I propose to add the information, that IP forwarding needs to be enabled in order to use a node as an exit-node.
2023-05-06 21:48:59 +02:00
Maja Bojarska 6c5723a463 Update CHANGELOG.md
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-05-04 22:54:32 +02:00
Maja Bojarska 57fd5cf310 Update CHANGELOG.md 2023-05-04 22:54:32 +02:00
Maja Bojarska f113cc7846 Add missing GH releases page link 2023-05-04 22:54:32 +02:00
ohdearaugustin ca54fb9f56 Revert unix_socket to default value 2023-05-03 20:16:04 +02:00
Kristoffer Dalby 735b185e7f use IPSet in acls instead of string slice
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 1a7ae11697 Add basic testcases for Machine.canAccess
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 644be822d5 move matcher to separate file
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 56b63c6e10 use netipx.IPSet for matcher
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby ccedf276ab add a filter case with really large destination set #1372
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 10320a5f1f lint and nolint tailscale borrowed func
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby ecd62fb785 remove terrible filter code
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 0d24e878d0 update flake hash
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 889d5a1b29 testing without that horrible filtercode
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 1700a747f6 outline tests for full filter generate
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 200e3b88cc make generateFilterRule a pol struct func
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 5bbbe437df clear up the acl function naming
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby 6de53e2f8d simplify expandAlias function, move seperate logic out
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-05-03 18:43:57 +02:00
Kristoffer Dalby b23a9153df trim dockerfiles, script to rebuild test images (#1403) 2023-05-02 10:51:30 +01:00
Juan Font 80772033ee Improvements on Noise implementation (#1379) 2023-05-02 08:15:33 +02:00
Juan Font a2b760834f Fix extra space 2023-04-30 23:28:16 +02:00
loprima-l 493bcfcf18 Update mkdocs.yml
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-04-30 23:28:16 +02:00
loprima-l df72508089 Fix : Change master branch to main
This fix should change the edit branch to main in the documentation
2023-04-30 23:28:16 +02:00
loprima-l 0f8d8fc2d8 Fix : Updating the doc path
Updating the doc path to be the doc website url as it's a better documentation tool
2023-04-30 22:56:38 +02:00
Jonathan Wright 744e5a11b6 Update CHANGELOG.md
Co-authored-by: Juan Font <juanfontalonso@gmail.com>
2023-04-30 18:25:43 +02:00
Jonathan Wright 3ea1750ea0 Update CHANGELOG.md 2023-04-30 18:25:43 +02:00
Jonathan Wright a45777d22e Put systemd service file in proper location 2023-04-30 18:25:43 +02:00
Kristoffer Dalby 56dd734300 Add go profiling flag, and enable on integration tests (#1382) 2023-04-27 16:57:11 +02:00
Philipp Krivanec d0113732fe optimize generateACLPeerCacheMap (#1377) 2023-04-26 06:02:54 +02:00
Kristoffer Dalby 6215eb6471 update flake hash (#1376) 2023-04-24 15:52:15 +02:00
Juan Font 1d2b4bca8a Remove legacy DERP tests 2023-04-24 12:35:29 +02:00
Juan Font 96f9680afd Reuse Ping function for DERP ping 2023-04-24 12:17:24 +02:00
Juan Font b465592c07 Do not use host networking in embedded DERP tests
fixed linting
2023-04-24 12:17:24 +02:00
Juan Font 991ff25362 Added workflow for embedded derp 2023-04-24 12:17:24 +02:00
Juan Font eacd687dbf Added DERP integration tests
Linting fixes

Set listen addr to :8443
2023-04-24 12:17:24 +02:00
Juan Font 549f5a164d Expand surface of hsic for better TLS support 2023-04-24 12:17:24 +02:00
Juan Font bb07aec82c Expand tsic to offer PingViaDerp 2023-04-24 12:17:24 +02:00
Kristoffer Dalby a5afe4bd06 Add more capabilities for systemd
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 15:53:19 +02:00
Kristoffer Dalby a71cc81fe7 fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 12:05:57 +02:00
Kristoffer Dalby 679305c3e4 Add version to binary release
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 12:05:57 +02:00
Kristoffer Dalby c0680f34f1 fix issue where binaries are not released
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 11:10:27 +02:00
Kristoffer Dalby 64ebe6b0c8 change date in changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 08:13:38 +02:00
Kristoffer Dalby e6b26499f7 release source code with vendored dependencies
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-20 08:13:38 +02:00
Kristoffer Dalby 977eb1dee3 Update flakes, add some quality of life improvements (#1346) 2023-04-20 07:56:53 +02:00
Kristoffer Dalby b2e2b02210 set release date
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:47:31 +02:00
Kristoffer Dalby 2abff4bb08 update changelog for #1339
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:45:27 +02:00
Kristoffer Dalby 54c00645d1 update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby cad5ce0ebd lint fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby b12a167fa2 remove rpm, might add back later
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby 667295e15e add new documentation on how to install on debian/ubuntu
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby bea52678e3 move current linux documentation into "manual"
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby 307cfc3304 add systemd enable to postinstall script
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-19 20:04:58 +02:00
Kristoffer Dalby 5e74ca9414 Fix IPv6 in ACLs (#1339) 2023-04-16 12:26:35 +02:00
Juan Font 9836b097a4 Make sure all clients of a user are ready (#1335) 2023-04-12 09:25:51 +02:00
Juan Font d0b3b1bfc4 Fix binary releases 2023-04-08 09:21:27 +02:00
Juan Font 6eea96eabc Added 1.38.4 in the new tests 2023-04-07 19:45:46 +02:00
github-actions[bot] d08fee78c3 docs(README): update contributors (#1325) 2023-04-07 17:31:29 +02:00
Andriy Kushnir (Orhideous) bb5f0d456c Change primary color for light mode to white 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) c186c49e25 Removed custom accents, going with defaults 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) 4ec6894773 Build with strict mode 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) dd9b4b1cb7 Move examples out of docs/ directory 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) a43bb9c958 Replace placeholder link with actual one 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) ba905ff6fc Add GHA CI to build and deploy docs 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) 99bd09f688 Add new index page 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) a6bc792a61 Move admonitions to relevant sections 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) 6381d3660a Add admonitions marking community-provided docs 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) 66c5f74d78 Add admonitions marking community-provided docs 2023-04-07 15:24:13 +02:00
Andriy Kushnir (Orhideous) 1723a6bf40 Configure MkDocs Material scaffold 2023-04-07 15:24:13 +02:00
Juan Font 353f191e4f Update changelog 2023-04-07 13:25:34 +02:00
Juan Font 8d865bb61b Target Go 1.20 in flake.nix 2023-04-07 13:25:34 +02:00
Juan Font c6815c5334 Target Go 1.20 and Tailscale 1.38 2023-04-07 13:25:34 +02:00
Kristoffer Dalby b684ac0668 Simplify goreleaser, package deb and rpm
This commit simplifies the goreleaser configuration and then adds nfpm
support which allows us to build .deb and .rpm for each of the ARCH we
support.

The deb and rpm packages adds systemd services and users, creates
directories etc and should in general give the user a working
environment. We should be able to remove a lot of the complicated,
PEBCAK inducing documentation after this.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-04-07 11:06:42 +02:00
Juan Font dfc5d861c7 Fix CIDR calculation in expandACLPeerAddr 2023-04-05 09:44:46 +02:00
Juan Font 50b706eeed Remove deprecated linters + one casuing issues with imports 2023-04-04 22:37:27 +02:00
Sean Reifschneider 036ff1cbb9 Adding Powershell commands to Windows instructions (#1299)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2023-04-04 08:58:32 +02:00
Kristoffer Dalby ceeef40cdf Add tests to verify "Hosts" aliases in ACL (#1304) 2023-04-03 10:08:48 +02:00
Julien Zweverink 681c86cc95 ACL Doc's (#1288) 2023-03-28 18:41:23 +02:00
Kristoffer Dalby c7b459b615 Fix issue where ACL * would filter out returning connections (#1279) 2023-03-27 19:19:32 +02:00
Gabe Cook 56a7b1e349 Add SVG logos (#1286) 2023-03-27 15:33:25 +02:00
Antonio Fernandez f1eee841cb updated to ACL doc (#1278) 2023-03-27 11:25:55 +02:00
Stefan Majer 45fbd34480 Do not use yaml.v2 and yaml.v3 as direct dependency (#1281) 2023-03-27 10:48:39 +02:00
Juan Font 248abcf353 Add missing entry to changelog and prepare for 0.22
Add missing entry to changelog
2023-03-20 13:48:56 +01:00
Kurnia D Win 2560c32378 adding some sleep on re-registration after machine expired (#1256) 2023-03-20 11:14:34 +01:00
Kristoffer Dalby e38efd3cfa Add ACL test for limiting a single port. (#1258) 2023-03-20 08:52:52 +01:00
Moritz Poldrack d12f247490 document running exit nodes
Currently the only kind-of documentation is #210 which is outdated. To remedy
this, add a document describing the process.
2023-03-19 11:02:01 +01:00
nicholas-yap 003036a779 Update iOS compatibility and added iOS docs (#1264) 2023-03-17 15:56:15 +01:00
github-actions[bot] ed79f977a7 docs(README): update contributors (#1262) 2023-03-17 10:07:39 +01:00
Kristoffer Dalby 8012e1cbd2 Add instructions on how to login to iOS (#1261) 2023-03-15 11:31:38 +00:00
Kristoffer Dalby a5562850a7 MapResponse optimalisations, peer list integration tests (#1254)
Co-authored-by: Allen <979347228@qq.com>
2023-03-06 17:50:26 +01:00
Stefan Majer bb786ac8e4 github.com/gofrs/uuid/v5 is now go modules compatible, use it (#1224) 2023-03-06 09:54:24 +01:00
Juan Font ea82035222 Allow to delete routes (#1244) 2023-03-06 09:05:40 +01:00
Albert Copeland c9ecdd6ef1 Add Graphical Control Panels section to README (#1226) 2023-03-03 19:05:12 +01:00
Juan Font 54f5c249f1 Fix various linting issues + golang-lint upgrade (#1245) 2023-03-03 18:22:47 +01:00
dnaq a82a603db6 Return 404 on unmatched routes (#1201) 2023-03-03 17:14:30 +01:00
Sean Reifschneider f49930c514 Add "configtest" CLI command. (#1230)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
Fixes https://github.com/juanfont/headscale/issues/1229
2023-03-03 14:55:29 +01:00
Kristoffer Dalby 2baeb79aa0 changelog: prep for 0.21 (#1246) 2023-03-03 13:42:45 +01:00
Kristoffer Dalby b3f78a209a Post PR comment when nix vendor sum breaks
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-03-02 18:17:59 +01:00
Josh Taylor 5e6868a858 Run prettier 2023-02-27 10:28:49 +01:00
Josh Taylor 5caf848f94 Add steps for Google OAuth for OIDC 2023-02-27 10:28:49 +01:00
Juan Font 3e097123bf Target ts 1.36 in integration tests 2023-02-26 15:35:27 +01:00
Juan Font 74447b02e8 Target Tailscale 1.36 when building 2023-02-26 15:35:27 +01:00
Juan Font 20e96de963 Update dependencies 20230226 2023-02-26 14:39:37 +01:00
Juan Font 7c765fb3dc Update prettier action
Update prettier action
2023-02-26 13:54:30 +01:00
Michael Savage dcc246c869 Fix OpenBSD build docs
- OpenBSD 7.2 installs go 1.19 by default
- Doubt you can run nix so skip the makefile/nix build and just go build directly
2023-02-26 12:54:47 +01:00
Àlex Torregrosa cf7767d8f9 Add css to the /windows and /apple templates (#1211)
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2023-02-09 08:49:05 -05:00
Maxim Gajdaj 61c578f82b Update running-headscale-linux.md
Option WorkingDirectory for headscale.service added
2023-02-09 08:17:28 -05:00
Kristoffer Dalby 6950ff7841 Add list of talks to the readme
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-07 17:04:14 +01:00
Kristoffer Dalby e65ce17f7b Add documentation to integration test framework
so tsic, hsic and scenario

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 16:25:58 +01:00
Kristoffer Dalby b190ec8edc Add section about running locally
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 16:25:58 +01:00
Kristoffer Dalby c39085911f Add node expiry test
This commits adds a test to verify that nodes get updated if a node in
their network expires.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 3c20d2a178 Update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 9187e4287c Remove unused components from old integration tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 2b7bcb77a5 Stop using deprecated string function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 97a909866d Use pingAll helper for all integration pinging
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby feeb5d334b Populate the tags field on node
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby a840a2e6ee Sort tailcfg.Node creation as upstream
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 4183345020 Do not collect services, we dont support it
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 50fb7ad6ce Add TODOs for only sending patch updates
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 88a9f4b44c Send control time in map response
This gives all the nodes the same constant time to work from

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-03 09:26:22 +01:00
Kristoffer Dalby 00fbd8dd93 Remove all tests before generating new ones
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-02 17:55:19 +01:00
Kristoffer Dalby ce587d2421 Update test workflows
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-01 10:58:37 +01:00
Kristoffer Dalby e1eb30084d Remove new line at start of test template
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-01 10:58:37 +01:00
Kristoffer Dalby 673638afe7 Use ripgrep to find list of tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-02-01 10:58:37 +01:00
Kristoffer Dalby da48cf64b3 Set OpenID Connect Expiry
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes #1176.

Co-authored-by: Even Holthe <even.holthe@bekk.no>
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-31 18:55:16 +01:00
Dominic Bevacqua 385fd93e73 Update changelog 2023-01-31 00:15:48 +01:00
Dominic Bevacqua 26edf24477 Allow split DNS configuration without requiring global nameservers
Align behaviour of dns_config.restricted_nameservers to tailscale.

Tailscale allows split DNS configuration without requiring global nameservers.

In addition, as per [the docs](https://tailscale.com/kb/1054/dns/#using-dns-settings-in-the-admin-console):

> These nameservers also configure search domains for your devices

This commit aligns headscale to tailscale by:

 * honouring dns_config.restricted_nameservers regardless of whether any global resolvers are configured
 * adding a search domain for each restricted_nameserver
2023-01-31 00:15:48 +01:00
Kristoffer Dalby 83a538cc95 Rename IP specific function, add missing test case
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-30 15:56:38 +01:00
Kristoffer Dalby cffa040474 Cancel old builds if new commits appear
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-30 14:57:10 +01:00
Kristoffer Dalby 727d95b477 Improve generated integration tests
- Save logs from control(headscale) on every run to tmp
- Upgrade nix-actions
- Cancel builds if new commit is pushed
- Fix a sorting bug in user command test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-30 14:43:03 +01:00
Juan Font 640bb94119 Do not show IsPrimary field as false in exit nodes 2023-01-29 14:54:09 +01:00
Juan Font 0f65918a25 Update tests
Fixed linting
2023-01-29 12:25:37 +01:00
Juan Font 3ac2e0b253 Enable both exit node routes (IPv4 and IPv6) at the same time.
As indicated by bradfitz in https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002,
both routes for the exit node must be enabled at the same time. If a user tries to enable one of the exit node routes,
the other gets activated too.

This commit also reduces the API surface, making private a method that didnt need to be exposed.
2023-01-29 12:25:37 +01:00
Juan Font b322cdf251 Updated changelog for v0.20.0 2023-01-29 11:46:37 +01:00
Johan Siebens e128796b59 use smallzstd and sync pool 2023-01-27 12:03:24 +01:00
Kristoffer Dalby 6d669c6b9c Migrate namespace_id to user_id column in machine and pak
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-26 11:07:26 +01:00
Kristoffer Dalby 8dadb045cf Mark -n and --namespace as deprecated
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-26 10:22:38 +01:00
Christian Heusel 9f6e546522 modify the test to reflect the changes on the webinterface
related to 2d44a1c99c17

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-26 08:33:44 +01:00
Juan Font 9714900db9 Target Tailscale 1.36.0 2023-01-26 07:50:03 +01:00
Jan Hartkopf cb25f0d650 Add hint for reverse proxying with Apache 2023-01-23 15:51:20 +01:00
caelansar 9c2e580ab5 put Where before Find 2023-01-20 10:50:29 +01:00
Christian Heusel 0ffff2c994 Update the node join instruction to reference "username"
related to https://github.com/juanfont/headscale/pull/1144

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-20 09:50:49 +01:00
Christian Heusel c720af66d6 permalink in the limitations section to tailscale
The relative link was broken after one commit to the file

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-20 09:19:26 +01:00
Kristoffer Dalby 86a7129027 Update changelog, more explicit backup note
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-19 12:54:34 +01:00
Kristoffer Dalby 9eaa8dd049 Migrate DB: rename table is plural, order matters
The calls to AutoMigrate to other classes that refer to users will
create the table and it will break, it needs to be done before
everything else.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-19 12:54:34 +01:00
Kristoffer Dalby 81441afe70 update changelog
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby f19e8aa7f0 Fix failing tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby 90287a6735 gofumpt
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby fb3e2dcf10 Rename namespace to user in docs
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby bf0b85f382 Rename acl test file
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby 5da0963aac Migrate DB: rename namespace, automigrate user
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby da5c051d73 Lint fix
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby b98bf199dd Regenerate go from proto
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby 428d7c86ce Rename namespace in protobuf files
While this truly breaks the point of the backwards compatible stuff with
protobuf, it does not seem worth it to attempt to glue together a
compatible API.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby af1ec5a593 Rename .go namespace files
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Kristoffer Dalby e3a2593344 Rename [Nn]amespace -> [Uu]ser in go code
Use gopls, ag and perl to rename all occurances of Namespace

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-18 15:40:04 +01:00
Motiejus Jakštys bafb6791d3 oidc: allow reading the client secret from a file
Currently the most "secret" way to specify the oidc client secret is via
an environment variable `OIDC_CLIENT_SECRET`, which is problematic[1].
Lets allow reading oidc client secret from a file. For extra convenience
the path to the secret will resolve the environment variables.

[1]: https://systemd.io/CREDENTIALS/
2023-01-14 17:03:57 +01:00
Motiejus Jakštys 6edac4863a Makefile: remove a missing target
test_integration_oidc was removed in 0525bea593
2023-01-14 13:42:48 +01:00
Even Holthe e27e01c09f nodes list: expose expiration time 2023-01-12 13:43:21 +01:00
Even Holthe dd173ecc1f Refresh machines with correct new expiry 2023-01-12 13:43:21 +01:00
Kristoffer Dalby 8ca0fb7ed0 update ip_prefixes docs
we cant actually have arbitrary ip ranges, add a note about that.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-12 11:39:39 +01:00
Juan Font 6c714e88ee Added entry for performance improvements in ACLs 2023-01-11 08:58:03 +01:00
Allen a6c8718a97 ToStringSlice will lead to high CPU usage, early conversion can reduce cpu usage 2023-01-11 08:45:54 +01:00
Even Holthe 26282b7a54 Fix SIGSEGV crash related to map of state changes
See https://github.com/juanfont/headscale/issues/1114#issuecomment-1373698441
2023-01-10 22:26:21 +01:00
Kristoffer Dalby 93aca81c1c Read integration test config from Env
This commit sets the Headscale config from env instead of file for
integration tests, the main point is to make sure that when we add per
test config, it properly replaces the config key and not append it or
something similar.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 23:06:43 +01:00
Kristoffer Dalby 81254cdf7a Limit run regex for generated workflows
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 18:36:31 +01:00
Kristoffer Dalby b3a0c4a63b Add integration readme
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 12:32:24 +01:00
Kristoffer Dalby 376235c9de make prettier ignore generated test flows
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 12:32:24 +01:00
Kristoffer Dalby 7274fdacc6 Generate github action jobs for integration tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 12:32:24 +01:00
Kristoffer Dalby 91c1f54b49 Remove "run all v2 job"
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 12:32:24 +01:00
Kristoffer Dalby efd0f79fbc Add script to generate integration test gitjobs
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-01-06 12:32:24 +01:00
Juan Font 2084464225 Update CHANGELOG.md
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2023-01-05 14:59:02 +01:00
Juan Font 66ebbf3ecb Preload AuthKey in machine getters 2023-01-05 14:59:02 +01:00
Juan Font 55a3885614 Added integration tests for ephemeral nodes
Fetch the machines from headscale
2023-01-05 14:59:02 +01:00
Juan Font afae1ff7b6 Delete ephemeral machines on logout
Update changelog

Use dedicated method to delete
2023-01-05 14:59:02 +01:00
Juan Font 4de49f5f49 Add isEphemeral() method to Machine 2023-01-05 14:59:02 +01:00
Even Holthe 6db9656008 oidc: update changelog 2023-01-04 09:23:52 +01:00
Even Holthe fecb13b24b oidc: add basic docs 2023-01-04 09:23:52 +01:00
Even Holthe 23a595c26f oidc: add test for expiring nodes after token expiration 2023-01-04 09:23:52 +01:00
Even Holthe 085912cfb4 expire machines after db expiry 2023-01-04 09:23:52 +01:00
Even Holthe 7157e14aff add expiration from OIDC token to machine 2023-01-04 09:23:52 +01:00
Allen 4e2c4f92d3 reflect.DeepEqual is a value copy that causes golang to continuously allocate memory 2023-01-03 18:09:18 +01:00
Juan Font 893b0de8fa Added tests on allowedip field for routing 2023-01-03 13:34:55 +01:00
Juan Font 9b98c3b79f Send in AllowedIPs both primary routes AND enabled exit routes 2023-01-03 13:34:55 +01:00
Even Holthe 6de26b1d7c Remove Tailscale v1.18.2 from test matrix 2023-01-02 16:06:12 +01:00
Christian Heusel 1f1931fb00 fix spelling mistakes 2023-01-01 22:45:16 +01:00
Christian Heusel 1f4efbcd3b add changelog entry 2023-01-01 22:45:16 +01:00
Christian Heusel 711fe1d806 enumerate the config 2023-01-01 22:45:16 +01:00
Christian Heusel e2c62a7b0c document how to add new DNS records via extra_records 2023-01-01 22:45:16 +01:00
Christian Heusel ab6565723e add the possibility for custom DNS records
related to https://github.com/juanfont/headscale/issues/762

Co-Authored-By: Jamie Greeff <jamie@greeff.me>
Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-01 22:45:16 +01:00
John Axel Eriksson 7bb6f1a7eb domains/restricted_nameservers: check dnsConfig.Resolvers instead of dnsConfig.Nameservers 2022-12-31 19:06:32 +01:00
Avirut Mehta 549b82df11 Add Caddy instructions to reverse_proxy.md 2022-12-27 23:08:34 +01:00
Marc 036cdf922f templates: fix typo "custm" -> "custom" 2022-12-27 12:02:33 +01:00
jimyag b4ff22935c update macos check 2022-12-25 15:45:45 +01:00
ma6174 5feadbf3fc fix goroutine leak 2022-12-25 14:11:16 +01:00
Juan Font 3e9ee816f9 Add integration tests for logout with authkey 2022-12-22 20:02:18 +01:00
Juan Font 2494e27a73 Make WaitForTailscaleLogout a Scenario method 2022-12-22 20:02:18 +01:00
Juan Font 8e8b65bb84 Add ko-fi sponsor button 2022-12-22 17:25:49 +01:00
Juan Font b7d7fc57c4 Add logout method to tsic 2022-12-22 00:09:21 +01:00
Juan Font b54c0e3d22 Add integration tests that check logout and relogin 2022-12-21 20:52:08 +01:00
Juan Font 593040b73d Run the Noise handlers under a new struct so we can access the noiseConn from the handlers
In TS2021 the MachineKey can be obtained from noiseConn.Peer() - contrary to what I thought before,
where I assumed MachineKey was dropped in TS2021.

By having a ts2021App and hanging from there the TS2021 handlers, we can fetch again the MachineKey.
2022-12-21 20:52:08 +01:00
Juan Font 6e890afc5f Minor linting fixes 2022-12-21 08:28:53 +01:00
Fatih Acar 2afba0233b fix(routes): ensure routes are correctly propagated
When using Tailscale v1.34.1, enabling or disabling a route does not
effectively add or remove the route from the node's routing table.
We must restart tailscale on the node to have a netmap update.

Fix this by refreshing last state change so that a netmap diff is sent.

Also do not include secondary routes in allowedIPs, otherwise secondary
routes might be used by nodes instead of the primary route.

Signed-off-by: Fatih Acar <facar@scaleway.com>
2022-12-20 15:39:59 +01:00
Anoop Sundaresh 91900b7310 Update remote-cli.md
Fixing the local binary path
2022-12-19 19:16:48 +01:00
Juan Font 55b198a16a Clients are offline when expired 2022-12-19 15:56:12 +01:00
Juan Font ca37dc6268 Update changelog 2022-12-15 00:13:53 -08:00
Juan Font 000c02dad9 Show online in CLI & API when isOnline() reports so 2022-12-15 00:13:53 -08:00
Juan Font 4532915be1 Refresh autogenerated grpc stuff 2022-12-15 00:13:53 -08:00
Juan Font 4b8d6e7c64 Include online field in proto for machine 2022-12-15 00:13:53 -08:00
Kristoffer Dalby 579c5827b3 regenerate proto with new plugin
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-14 00:05:29 -08:00
Kristoffer Dalby 01628f76ff upgrade grpc-gateway plugin
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-14 00:05:29 -08:00
Kristoffer Dalby 53858a32f1 dont fail docker if nothing to delete
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-13 23:13:23 -08:00
Juan Font Alonso 2bf576ea8a Disable Tailscale 1.16 in integration tests 2022-12-09 19:11:24 +01:00
github-actions[bot] 1faac0b3d7 docs(README): update contributors 2022-12-07 15:18:37 +01:00
Kristoffer Dalby 134c72f4fb Set db_ssl to false by default, fixes #1043
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-07 14:58:47 +01:00
Zachary Newell 70f2f5d750 Added an OIDC AllowGroups option for authorization. 2022-12-07 08:53:16 +01:00
Kristoffer Dalby 4453728614 Murder docker container and network before run
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-06 08:52:21 +01:00
Juan Font 34107f9a0f Updated changelog 2022-12-06 08:17:14 +01:00
Juan Font 52862b8a22 Port integration tests routes CLI to v2
Fix options signature
2022-12-06 08:17:14 +01:00
Juan Font 946d38e5d7 Minor linting fixes
Remove magic number (base10...)
2022-12-06 08:17:14 +01:00
Juan Font 78819be03c Use the new routes API from the CLI 2022-12-06 08:17:14 +01:00
Juan Font 34631dfcf5 Refactored route grpc glue code 2022-12-06 08:17:14 +01:00
Juan Font 8fa9755b55 Updated generated pb code
Update swagger
2022-12-06 08:17:14 +01:00
Juan Font 1b557ac1ea Update protobuf definitions + support methods for the API
Add more logging

Updated protos with new routes API
2022-12-06 08:17:14 +01:00
Juan Font 8170f5e693 Removed unused code and linting fixes
Another bunch of gosec/golint related fixes

Remove method no longer used
2022-12-06 08:17:14 +01:00
Juan Font a506d0fcc8 Run handlePrimarySubnetFailover() with a ticker when Serve 2022-12-06 08:17:14 +01:00
Juan Font 6718ff71d3 Added helper methods for subnet failover + unit tests
Added method to perform subnet failover

Added tests for subnet failover
2022-12-06 08:17:14 +01:00
Juan Font b62acff2e3 Refactor machine.go, and move functionality to routes.go + unit tests
Port routes tests to new model

Mark as primary the first instance of subnet + tests

In preparation for subnet failover, mark the initial occurrence of a subnet as the primary one.
2022-12-06 08:17:14 +01:00
Juan Font ac8bff716d Call processMachineRoutes when a new Map is received 2022-12-06 08:17:14 +01:00
Juan Font fba77de4eb Add Route DB model and migration from existing field
Add migration from Machine.EnabledRoutes to the new Route table

Cleanup route.go and add helper methods to process HostInfo
2022-12-06 08:17:14 +01:00
github-actions[bot] d1bca105ef docs(README): update contributors 2022-12-05 22:05:25 +01:00
Juan Font 6c2d6fa302 Do not explicitly set the protocols when ommited in ACL 2022-12-05 21:45:18 +01:00
Kristoffer Dalby 1015bc3e02 Upgrade to Tailscale 1.34.0
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby 68c72d03b5 Prep changelog for new release
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby bd4b2da06e Add changelog entry to correct version
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby 7b8cf5ef1a Add 1.34.0 to integration tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby 638a3d48ec fix nix run
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby 4de676c64e Add instructions for macOS GUI
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby a58a552f0e Update macos/windows doc
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 20:41:15 +01:00
Kristoffer Dalby 0db16c7bbe Update nix deps, get go 1.19.3 in
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 10:40:17 +01:00
Kristoffer Dalby 06f7e7cfd8 Tag dockerfiles to minor version so we dont have to care about patch
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 10:40:17 +01:00
Kristoffer Dalby 19f12f94c0 Make goreleaser use Nix
Eliminate one more place to make sure we use the same go version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 10:40:17 +01:00
Kristoffer Dalby 95d3062c21 Add github action updater
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 10:40:17 +01:00
Kristoffer Dalby 86fa136a63 Upgrade go dependencies
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-05 10:40:17 +01:00
Juan Font 89c12072ba added changelog for 0.17.1 2022-12-03 16:34:23 +01:00
Juan Font 54f701ff92 generateACLPolicy() no longer a Headscale method 2022-12-03 15:43:40 +01:00
Juan Font 5a70ea7326 Correct typo on standalone (fixes #1021) 2022-12-01 17:01:20 +01:00
Kristoffer Dalby 63cd3122e6 Add breaking change about noise private path
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-01 14:47:19 +01:00
Kristoffer Dalby 6f4c6c1876 Ignore tparallel where it doesnt make sense
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-12-01 14:45:11 +01:00
Kristoffer Dalby eb072a1a74 mark some changes as more important
Signed-off-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-11-26 12:01:12 +01:00
Kristoffer Dalby 36b8862e7c Add notes about current ssh status
Signed-off-by: Kristoffer Dalby <kradalby@kradalby.no>
2022-11-26 11:53:31 +01:00
Even Holthe d4e3bf184b Add experimental flag to unit test 2022-11-26 11:53:31 +01:00
Even Holthe c28ca27133 Add SSH ACL to changelog 2022-11-26 11:53:31 +01:00
Kristoffer Dalby c02e105065 Mark the flag properly experimental
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby 22da5bfc1d Enable SSH for tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby c6d31747f7 Add feature flag for SSH, and warning
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby 91ed6e2197 Allow WithEnv to be passed multiple times
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby d71aef3b98 Mark all tests with Parallel
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby 8a79c2e7ed Do not retry on permission denied in ssh
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby f34e7c341b Strip newline from hostname
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby e28d308796 Add negative tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Even Holthe f610be632e SSH: add test between namespaces 2022-11-26 11:53:31 +01:00
Even Holthe fd6d25b5c1 SSH: Lint and typos 2022-11-26 11:53:31 +01:00
Kristoffer Dalby 3695284286 Make simple initial test case
This commit makes the initial SSH test a bit simpler:

- Use the same pattern/functions for all clients as other tests
- Only test within _one_ namespace/user to confirm the base case
- Use retry function, same as taildrop, there is some funky going on
  there...

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby cfaa36e51a Add method to expose container id
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Kristoffer Dalby d207c30949 Ensure we have ssh in container
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-26 11:53:31 +01:00
Even Holthe 519f22f9bf SSH integration test setup 2022-11-26 11:53:31 +01:00
Even Holthe 52a323b90d Add SSH capability advertisement
Advertises the SSH capability, and parses the SSH ACLs to pass to the
tailscale client. Doesn’t support ‘autogroup’ ACL functionality.

Co-authored-by: Daniel Brooks <db48x@headline.com>
2022-11-26 11:53:31 +01:00
github-actions[bot] 91559d0558 docs(README): update contributors 2022-11-25 08:58:13 +01:00
Orville Q. Song 25195b8d73 Update CHANGELOG.md 2022-11-24 16:13:47 +01:00
Orville Q. Song e69176e200 Tweak 2022-11-24 16:13:47 +01:00
Orville Q. Song d29d0222af Add a note about the db_ssl field in the example config file 2022-11-24 16:13:47 +01:00
Orville Q. Song 72b9803a08 Change DBssl to string 2022-11-24 16:13:47 +01:00
Kristoffer Dalby 99e33181b2 Make displayName include basedomain if set
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-22 19:16:58 +01:00
Kristoffer Dalby e7f322b9b6 Mark all tests to run in parallel
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-22 13:18:58 +01:00
Juan Font 1d36e1775f Remove OIDC action 2022-11-21 22:07:27 +01:00
Juan Font 0525bea593 Remove legacy OIDC tests 2022-11-21 22:07:27 +01:00
Juan Font 2770c7cc07 Initial proposal for better routing 2022-11-21 21:58:22 +01:00
Juan Font 1b0e80bb10 Add OIDC integration tests
* Port OIDC integration tests to v2

* Move Tailscale old versions to TS2019 list

* Remove Alpine Linux container

* Updated changelog

* Releases: use flavor to set the tag suffix

* Added more debug messages in OIDC registration

* Added more logging

* Do not strip nodekey prefix on handle expired

* Updated changelog

* Add WithHostnameAsServerURL option func

* Reduce the number of namespaces and use hsic.WithHostnameAsServerURL

* Linting fix

* Fix linting issues

* Wait for ready outside the up goroutine

* Minor change in log message

* Add prefix to env var

* Remove unused env var

Co-authored-by: Juan Font <juan.font@esa.int>
Co-authored-by: Steven Honson <steven@honson.id.au>
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2022-11-21 21:51:54 +01:00
Kristoffer Dalby 4ccc528d96 Remove some very verbose error outputs
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-21 14:37:50 +01:00
Juan Font 6a311f4ab6 Remove broken renovatebot 2022-11-20 17:19:50 +01:00
manju-rn a49a405413 Correction in the sample config file
Added the db_type in the sample config.yaml  Without this entry, the container throws Unsupported DB error
`db_type: sqlite3`
2022-11-20 17:12:13 +01:00
Juan Font 24f946e2e9 Fix completion issues (fixes #839) 2022-11-20 13:57:38 +01:00
Juan Font c3cdb340de Increase integration tests timeout to 120m 2022-11-20 12:56:07 +01:00
Juan Font 935319a218 Remove mTLS from doc and config example 2022-11-19 19:50:34 +01:00
Juan Font 4c7e15a7ce Remove mTLS config from integration config 2022-11-19 19:50:34 +01:00
Juan Font d461097247 Remove mTLS stuff from code 2022-11-19 19:50:34 +01:00
Juan Font f90a3c196c Move TS WaitForReady outside up goroutine 2022-11-19 17:16:08 +01:00
Juan Font Alonso 751cc173d4 Fix issue when CLI is configured in config file 2022-11-18 19:19:56 +01:00
Juan Font Alonso ff134f2b8e Fix remote CLI when there is no config file present 2022-11-18 19:19:56 +01:00
Arnar Gauti Ingason 6d3ede1367 Add support for NextDNS resolver 2022-11-18 09:38:46 +01:00
Steven Honson c0884f94b8 Release: tag every release with develop 2022-11-17 16:52:12 +01:00
Kristoffer Dalby 3d8dd68b14 default to localhost, not listen on all
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-16 17:37:35 +01:00
Kristoffer Dalby b02e88364e Fix test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-16 17:37:35 +01:00
Kristoffer Dalby 9790831afb Make config example "local dev first"
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-16 17:37:35 +01:00
Juan Font Alonso 2d79179141 Updated changelog 2022-11-15 21:28:26 +01:00
Juan Font Alonso 275cc28193 Do not strip nodekey prefix on handle expired 2022-11-15 21:28:26 +01:00
Juan Font c5ba7552c5 Added more logging 2022-11-15 21:28:26 +01:00
Juan Font 8909f801bb Added more debug messages in OIDC registration 2022-11-15 21:28:26 +01:00
Steven Honson 3d4af52b3a Releases: use flavor to set the tag suffix 2022-11-15 11:36:38 +01:00
Juan Font 6391555dab Updated changelog 2022-11-15 08:42:29 +01:00
Juan Font 8cc5b2174b Remove Alpine Linux container 2022-11-15 08:42:29 +01:00
Juan Font 9269dd01f5 Move Tailscale old versions to TS2019 list 2022-11-14 23:06:30 +01:00
Juan Font ef68f17a96 Return the correct error on cache miss 2022-11-14 18:34:27 +01:00
Juan Font f74266f8f8 OIDC code cleanup and harmonize with regular web auth 2022-11-14 18:34:27 +01:00
Kristoffer Dalby 46df219ed3 Add testname identifier to hs container
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 835288d864 Remove unused variable
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 93d56362af Lock and unify headscale start/get method
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 4799859be0 Fix renamed method
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 8e44596171 less verbose command output
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby d479234058 Split ts versions into 2019/2021 for dedicated tests later
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 3fc5866de0 Remove duplicate function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby f3c40086ac Make TLS setup work automatically
This commit injects the per-test-generated tls certs into the tailscale
container and makes sure all can ping all. It does not test any of the
DERP isolation yet.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 09ed21edd8 Remove duplicate function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 456479eaa1 Rename and move wait for headscale
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby cb87852825 Add nolint to gosec stuff that doesnt matter because test
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 69440058bb Clean up cert function
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
Kristoffer Dalby 9bc6ac0f35 Make TLS setup work automatically
This commit injects the per-test-generated tls certs into the tailscale
container and makes sure all can ping all. It does not test any of the
DERP isolation yet.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-14 16:50:28 +01:00
1612 changed files with 12186048 additions and 36992 deletions
+4
View File
@@ -17,3 +17,7 @@ LICENSE
.vscode
*.sock
node_modules/
package-lock.json
package.json
+16
View File
@@ -0,0 +1,16 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120
[*.go]
indent_style = tab
[Makefile]
indent_style = tab
+8 -8
View File
@@ -1,10 +1,10 @@
* @juanfont @kradalby
*.md @ohdearaugustin
*.yml @ohdearaugustin
*.yaml @ohdearaugustin
Dockerfile* @ohdearaugustin
.goreleaser.yaml @ohdearaugustin
/docs/ @ohdearaugustin
/.github/workflows/ @ohdearaugustin
/.github/renovate.json @ohdearaugustin
*.md @ohdearaugustin @nblock
*.yml @ohdearaugustin @nblock
*.yaml @ohdearaugustin @nblock
Dockerfile* @ohdearaugustin @nblock
.goreleaser.yaml @ohdearaugustin @nblock
/docs/ @ohdearaugustin @nblock
/.github/workflows/ @ohdearaugustin @nblock
/.github/renovate.json @ohdearaugustin @nblock
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
ko_fi: headscale
-30
View File
@@ -1,30 +0,0 @@
---
name: "Bug report"
about: "Create a bug report to help us improve"
title: ""
labels: ["bug"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. -->
**Bug description**
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
and how it is currently different. If you are unsure if it is a bug, consider discussing
it on our Discord server first. -->
**To Reproduce**
<!-- Steps to reproduce the behavior. -->
**Context info**
<!-- Please add relevant information about your system. For example:
- Version of headscale used
- Version of tailscale client
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
- Kernel version
- The relevant config parameters you used
- Log output
-->
+106
View File
@@ -0,0 +1,106 @@
name: 🐞 Bug
description: File a bug/issue
title: "[Bug] <title>"
labels: ["bug", "needs triage"]
body:
- type: checkboxes
attributes:
label: Is this a support request?
description: This issue tracker is for bugs and feature requests only. If you need
help, please use ask in our Discord community
options:
- label: This is not a support request
required: true
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you
encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
1. With this config...
1. Run '...'
1. See error...
validations:
required: true
- type: textarea
attributes:
label: Environment
description: |
Please provide information about your environment.
If you are using a container, always provide the headscale version and not only the Docker image version.
Please do not put "latest".
Describe your "headscale network". Is there a lot of nodes, are the nodes all interconnected, are some subnet routers?
If you are experiencing a problem during an upgrade, please provide the versions of the old and new versions of Headscale and Tailscale.
examples:
- **OS**: Ubuntu 24.04
- **Headscale version**: 0.24.3
- **Tailscale version**: 1.80.0
- **Number of nodes**: 20
value: |
- OS:
- Headscale version:
- Tailscale version:
render: markdown
validations:
required: true
- type: checkboxes
attributes:
label: Runtime environment
options:
- label: Headscale is behind a (reverse) proxy
required: false
- label: Headscale runs in a container
required: false
- type: textarea
attributes:
label: Debug information
description: |
Please have a look at our [Debugging and troubleshooting
guide](https://headscale.net/development/ref/debug/) to learn about
common debugging techniques.
Links? References? Anything that will give us more context about the issue you are encountering.
If **any** of these are omitted we will likely close your issue, do **not** ignore them.
- Client netmap dump (see below)
- Policy configuration
- Headscale configuration
- Headscale log (with `trace` enabled)
Dump the netmap of tailscale clients:
`tailscale debug netmap > DESCRIPTIVE_NAME.json`
Dump the status of tailscale clients:
`tailscale status --json > DESCRIPTIVE_NAME.json`
Get the logs of a Tailscale client that is not working as expected.
`tailscale debug daemon-logs`
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
**Ensure** you use formatting for files you attach.
Do **not** paste in long files.
validations:
required: true
+4 -4
View File
@@ -3,9 +3,9 @@ blank_issues_enabled: false
# Contact links
contact_links:
- name: "headscale usage documentation"
url: "https://github.com/juanfont/headscale/blob/main/docs"
about: "Find documentation about how to configure and run headscale."
- name: "headscale Discord community"
url: "https://discord.gg/xGj2TuqyxY"
url: "https://discord.gg/c84AZQhmpx"
about: "Please ask and answer questions about usage of headscale here."
- name: "headscale usage documentation"
url: "https://headscale.net/"
about: "Find documentation about how to configure and run headscale."
-17
View File
@@ -1,17 +0,0 @@
---
name: "Feature request"
about: "Suggest an idea for headscale"
title: ""
labels: ["enhancement"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the feature request in this language. -->
**Feature request**
<!-- A clear and precise description of what new or changed feature you want. -->
<!-- Please include the reason, why you would need the feature. E.g. what problem
does it solve? Or which workflow is currently frustrating and will be improved by
this? -->
@@ -0,0 +1,36 @@
name: 🚀 Feature Request
description: Suggest an idea for Headscale
title: "[Feature] <title>"
labels: [enhancement]
body:
- type: textarea
attributes:
label: Use case
description: Please describe the use case for this feature.
placeholder: |
<!-- Include the reason, why you would need the feature. E.g. what problem
does it solve? Or which workflow is currently frustrating and will be improved by
this? -->
validations:
required: true
- type: textarea
attributes:
label: Description
description: A clear and precise description of what new or changed feature you want.
validations:
required: true
- type: checkboxes
attributes:
label: Contribution
description: Are you willing to contribute to the implementation of this feature?
options:
- label: I can write the design doc for this feature
required: false
- label: I can contribute this feature
required: false
- type: textarea
attributes:
label: How can it be implemented?
description: Free text for your ideas on how this feature could be implemented.
validations:
required: false
-30
View File
@@ -1,30 +0,0 @@
---
name: "Other issue"
about: "Report a different issue"
title: ""
labels: ["bug"]
assignees: ""
---
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the issue in this language. -->
<!-- If you have a question, please consider using our Discord for asking questions -->
**Issue description**
<!-- Please add your issue description. -->
**To Reproduce**
<!-- Steps to reproduce the behavior. -->
**Context info**
<!-- Please add relevant information about your system. For example:
- Version of headscale used
- Version of tailscale client
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
- Kernel version
- The relevant config parameters you used
- Log output
-->
+80
View File
@@ -0,0 +1,80 @@
Thank you for taking the time to report this issue.
To help us investigate and resolve this, we need more information. Please provide the following:
> [!TIP]
> Most issues turn out to be configuration errors rather than bugs. We encourage you to discuss your problem in our [Discord community](https://discord.gg/c84AZQhmpx) **before** opening an issue. The community can often help identify misconfigurations quickly, saving everyone time.
## Required Information
### Environment Details
- **Headscale version**: (run `headscale version`)
- **Tailscale client version**: (run `tailscale version`)
- **Operating System**: (e.g., Ubuntu 24.04, macOS 14, Windows 11)
- **Deployment method**: (binary, Docker, Kubernetes, etc.)
- **Reverse proxy**: (if applicable: nginx, Traefik, Caddy, etc. - include configuration)
### Debug Information
Please follow our [Debugging and Troubleshooting Guide](https://headscale.net/stable/ref/debug/) and provide:
1. **Client netmap dump** (from affected Tailscale client):
```bash
tailscale debug netmap > netmap.json
```
2. **Client status dump** (from affected Tailscale client):
```bash
tailscale status --json > status.json
```
3. **Tailscale client logs** (if experiencing client issues):
```bash
tailscale debug daemon-logs
```
> [!IMPORTANT]
> We need logs from **multiple nodes** to understand the full picture:
>
> - The node(s) initiating connections
> - The node(s) being connected to
>
> Without logs from both sides, we cannot diagnose connectivity issues.
4. **Headscale server logs** with `log.level: trace` enabled
5. **Headscale configuration** (with sensitive values redacted - see rules below)
6. **ACL/Policy configuration** (if using ACLs)
7. **Proxy/Docker configuration** (if applicable - nginx.conf, docker-compose.yml, Traefik config, etc.)
## Formatting Requirements
- **Attach long files** - Do not paste large logs or configurations inline. Use GitHub file attachments or GitHub Gists.
- **Use proper Markdown** - Format code blocks, logs, and configurations with appropriate syntax highlighting.
- **Structure your response** - Use the headings above to organize your information clearly.
## Redaction Rules
> [!CAUTION]
> **Replace, do not remove.** Removing information makes debugging impossible.
When redacting sensitive information:
- ✅ **Replace consistently** - If you change `alice@company.com` to `user1@example.com`, use `user1@example.com` everywhere (logs, config, policy, etc.)
- ✅ **Use meaningful placeholders** - `user1@example.com`, `bob@example.com`, `my-secret-key` are acceptable
- ❌ **Never remove information** - Gaps in data prevent us from correlating events across logs
- ❌ **Never redact IP addresses** - We need the actual IPs to trace network paths and identify issues
**If redaction rules are not followed, we will be unable to debug the issue and will have to close it.**
---
**Note:** This issue will be automatically closed in 3 days if no additional information is provided. Once you reply with the requested information, the `needs-more-info` label will be removed automatically.
If you need help gathering this information, please visit our [Discord community](https://discord.gg/c84AZQhmpx).
+15
View File
@@ -0,0 +1,15 @@
Thank you for reaching out.
This issue tracker is used for **bug reports and feature requests** only. Your question appears to be a support or configuration question rather than a bug report.
For help with setup, configuration, or general questions, please visit our [Discord community](https://discord.gg/c84AZQhmpx) where the community and maintainers can assist you in real-time.
**Before posting in Discord, please check:**
- [Documentation](https://headscale.net/)
- [FAQ](https://headscale.net/stable/faq/)
- [Debugging and Troubleshooting Guide](https://headscale.net/stable/ref/debug/)
If after troubleshooting you determine this is actually a bug, please open a new issue with the required debug information from the troubleshooting guide.
This issue has been automatically closed.
+13 -1
View File
@@ -1,6 +1,18 @@
<!--
Headscale is "Open Source, acknowledged contribution", this means that any
contribution will have to be discussed with the Maintainers before being submitted.
This model has been chosen to reduce the risk of burnout by limiting the
maintenance overhead of reviewing and validating third-party code.
Headscale is open to code contributions for bug fixes without discussion.
If you find mistakes in the documentation, please submit a fix to the documentation.
-->
<!-- Please tick if the following things apply. You… -->
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing)
- [ ] have read the [CONTRIBUTING.md](./CONTRIBUTING.md) file
- [ ] raised a GitHub issue or discussed it on the projects chat beforehand
- [ ] added unit tests
- [ ] added integration tests
+11 -15
View File
@@ -6,31 +6,27 @@
"onboarding": false,
"extends": ["config:base", ":rebaseStalePrs"],
"ignorePresets": [":prHourlyLimit2"],
"enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ],
"enabledManagers": ["dockerfile", "gomod", "github-actions", "regex"],
"includeForks": true,
"repositories": ["juanfont/headscale"],
"platform": "github",
"packageRules": [
{
"matchDatasources": ["go"],
"groupName": "Go modules",
"groupSlug": "gomod",
"separateMajorMinor": false
"matchDatasources": ["go"],
"groupName": "Go modules",
"groupSlug": "gomod",
"separateMajorMinor": false
},
{
"matchDatasources": ["docker"],
"groupName": "Dockerfiles",
"groupSlug": "dockerfiles"
}
"matchDatasources": ["docker"],
"groupName": "Dockerfiles",
"groupSlug": "dockerfiles"
}
],
"regexManagers": [
{
"fileMatch": [
".github/workflows/.*.yml$"
],
"matchStrings": [
"\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"
],
"fileMatch": [".github/workflows/.*.yml$"],
"matchStrings": ["\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"],
"datasourceTemplate": "golang-version",
"depNameTemplate": "actions/go-version"
}
+73 -20
View File
@@ -5,38 +5,91 @@ on:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
jobs:
build:
build-nix:
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
filters: |
files:
- '*.nix'
- 'go.*'
- '**/*.go'
- 'integration_test/'
- 'config-example.yaml'
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
if: steps.changed-files.outputs.files == 'true'
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
if: steps.changed-files.outputs.files == 'true'
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Check vendor hash
id: vendorhash
if: steps.changed-files.outputs.files == 'true'
run: |
go run ./cmd/vendorhash check | tee check-result
{
grep '^expected_sri=' check-result || true
grep '^actual_sri=' check-result || true
} >> "$GITHUB_OUTPUT"
- name: Run build
if: steps.changed-files.outputs.any_changed == 'true'
run: nix build
- name: Vendor hash diverging
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: failure() && steps.vendorhash.outcome == 'failure'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.pulls.createReviewComment({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Vendor hash in `flakehashes.json` is stale (was `${{ steps.vendorhash.outputs.expected_sri }}`, should be `${{ steps.vendorhash.outputs.actual_sri }}`). Run `go run ./cmd/vendorhash update` and commit the result.'
})
- uses: actions/upload-artifact@v2
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run nix build
if: steps.changed-files.outputs.files == 'true'
run: nix build --fallback
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: steps.changed-files.outputs.files == 'true'
with:
name: headscale-linux
path: result/bin/headscale
build-cross:
runs-on: ubuntu-latest
strategy:
matrix:
env:
- "GOARCH=arm64 GOOS=linux"
- "GOARCH=amd64 GOOS=linux"
- "GOARCH=arm64 GOOS=darwin"
- "GOARCH=amd64 GOOS=darwin"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: Run go cross compile
env:
CGO_ENABLED: 0
run: env ${{ matrix.env }} go build -o "headscale"
./cmd/headscale
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: "headscale-${{ matrix.env }}"
path: "headscale"
+54
View File
@@ -0,0 +1,54 @@
name: Check Generated Files
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
jobs:
check-generated:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
files:
- '*.nix'
- 'go.*'
- '**/*.go'
- 'tools/**'
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
if: steps.changed-files.outputs.files == 'true'
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
if: steps.changed-files.outputs.files == 'true'
- name: Run make generate
if: steps.changed-files.outputs.files == 'true'
run: make generate
- name: Check for uncommitted changes
if: steps.changed-files.outputs.files == 'true'
run: |
if ! git diff --exit-code; then
echo "❌ Generated files are not up to date!"
echo "Please run 'make generate' and commit the changes."
exit 1
else
echo "✅ All generated files are up to date."
fi
+45
View File
@@ -0,0 +1,45 @@
name: Check integration tests workflow
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
jobs:
check-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
files:
- '*.nix'
- 'go.*'
- '**/*.go'
- 'integration_test/'
- 'config-example.yaml'
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
if: steps.changed-files.outputs.files == 'true'
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
if: steps.changed-files.outputs.files == 'true'
- name: Generate and check integration tests
if: steps.changed-files.outputs.files == 'true'
run: |
(cd .github/workflows && go generate)
git diff --exit-code .github/workflows/test-integration.yaml
- name: Show missing tests
if: failure()
run: |
git diff .github/workflows/test-integration.yaml
+108
View File
@@ -0,0 +1,108 @@
---
name: Build (main)
on:
push:
branches:
- main
paths:
- "*.nix"
- "go.*"
- "**/*.go"
- ".github/workflows/container-main.yml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.sha }}
cancel-in-progress: true
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
jobs:
container:
if: github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: Set commit timestamp
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> $GITHUB_ENV
- name: Build and push to GHCR
env:
KO_DOCKER_REPO: ghcr.io/juanfont/headscale
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
CGO_ENABLED: "0"
run: |
ko build \
--bare \
--platform=linux/amd64,linux/arm64 \
--tags=main-${GITHUB_SHA::7},development \
./cmd/headscale
- name: Push to Docker Hub
env:
KO_DOCKER_REPO: headscale/headscale
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
CGO_ENABLED: "0"
run: |
ko build \
--bare \
--platform=linux/amd64,linux/arm64 \
--tags=main-${GITHUB_SHA::7},development \
./cmd/headscale
binaries:
if: github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: Build binary
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: go build -o headscale ./cmd/headscale
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: headscale-${{ matrix.goos }}-${{ matrix.goarch }}
path: headscale
-35
View File
@@ -1,35 +0,0 @@
name: Contributors
on:
push:
branches:
- main
workflow_dispatch:
jobs:
add-contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Delete upstream contributor branch
# Allow continue on failure to account for when the
# upstream branch is deleted or does not exist.
continue-on-error: true
run: git push origin --delete update-contributors
- name: Create up-to-date contributors branch
run: git checkout -B update-contributors
- name: Push empty contributors branch
run: git push origin update-contributors
- name: Switch back to main
run: git checkout main
- uses: BobAnkh/add-contributors@v0.2.2
with:
CONTRIBUTOR: "## Contributors"
COLUMN_PER_ROW: "6"
ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
IMG_WIDTH: "100"
FONT_SIZE: "14"
PATH: "/README.md"
COMMIT_MESSAGE: "docs(README): update contributors"
AVATAR_SHAPE: "round"
BRANCH: "update-contributors"
PULL_REQUEST: "main"
+51
View File
@@ -0,0 +1,51 @@
name: Deploy docs
on:
push:
branches:
# Main branch for development docs
- main
# Doc maintenance branches
- doc/[0-9]+.[0-9]+.[0-9]+
tags:
# Stable release tags
- v[0-9]+.[0-9]+.[0-9]+
paths:
- "docs/**"
- "mkdocs.yml"
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: Install python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: 3.x
- name: Setup cache
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
key: ${{ github.ref }}
path: .cache
- name: Setup dependencies
run: pip install -r docs/requirements.txt
- name: Configure git
run: |
git config user.name github-actions
git config user.email github-actions@github.com
- name: Deploy development docs
if: github.ref == 'refs/heads/main'
run: mike deploy --push development unstable
- name: Deploy stable docs from doc branches
if: startsWith(github.ref, 'refs/heads/doc/')
run: mike deploy --push ${GITHUB_REF_NAME##*/}
- name: Deploy stable docs from tag
if: startsWith(github.ref, 'refs/tags/v')
# This assumes that only newer tags are pushed
run: mike deploy --push --update-aliases ${GITHUB_REF_NAME#v} stable latest
+27
View File
@@ -0,0 +1,27 @@
name: Test documentation build
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: 3.x
- name: Setup cache
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
key: ${{ github.ref }}
path: .cache
- name: Setup dependencies
run: pip install -r docs/requirements.txt
- name: Build docs
run: mkdocs build --strict
+39
View File
@@ -0,0 +1,39 @@
name: Cache GC
# Garbage collection for the hestia binary cache. Must run on the default
# branch: a PR job's cache scope is read-only towards the default branch and
# dies with the PR, but the default-branch scope grows forever without GC.
concurrency:
group: hestia-gc
cancel-in-progress: false
on:
schedule:
# Daily, off-peak (UTC).
- cron: "23 3 * * *"
workflow_dispatch:
inputs:
dry-run:
description: Plan only; do not repack, touch, or delete anything.
type: boolean
default: false
permissions:
contents: read
jobs:
gc:
runs-on: ubuntu-latest
permissions:
# REST cache deletes need actions:write.
actions: write
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: Run garbage collection
run: '"${HESTIA_BIN}" gc ${{ inputs.dry-run && ''--dry-run'' || '''' }}'
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -0,0 +1,145 @@
package main
//go:generate go run ./gh-action-integration-generator.go
import (
"bytes"
"fmt"
"log"
"os/exec"
"strings"
)
// testsToSplit defines tests that should be split into multiple CI jobs.
// Key is the test function name, value is a list of subtest prefixes.
// Each prefix becomes a separate CI job as "TestName/prefix".
//
// Example: [TestAutoApproveMultiNetwork] has subtests like:
// - TestAutoApproveMultiNetwork/authkey-tag-advertiseduringup-false-pol-database
// - TestAutoApproveMultiNetwork/webauth-user-advertiseduringup-true-pol-file
//
// Splitting by approver type (tag, user, group) creates 6 CI jobs with 4 tests each:
// - TestAutoApproveMultiNetwork/authkey-tag.* (4 tests)
// - TestAutoApproveMultiNetwork/authkey-user.* (4 tests)
// - TestAutoApproveMultiNetwork/authkey-group.* (4 tests)
// - TestAutoApproveMultiNetwork/webauth-tag.* (4 tests)
// - TestAutoApproveMultiNetwork/webauth-user.* (4 tests)
// - TestAutoApproveMultiNetwork/webauth-group.* (4 tests)
//
// This reduces load per CI job (4 tests instead of 12) to avoid infrastructure
// flakiness when running many sequential Docker-based integration tests.
var testsToSplit = map[string][]string{
"TestAutoApproveMultiNetwork": {
"authkey-tag",
"authkey-user",
"authkey-group",
"webauth-tag",
"webauth-user",
"webauth-group",
},
}
// expandTests takes a list of test names and expands any that need splitting
// into multiple subtest patterns.
func expandTests(tests []string) []string {
var expanded []string
for _, test := range tests {
if prefixes, ok := testsToSplit[test]; ok {
// This test should be split into multiple jobs.
// We append ".*" to each prefix because the CI runner wraps patterns
// with ^...$ anchors. Without ".*", a pattern like "authkey$" wouldn't
// match "authkey-tag-advertiseduringup-false-pol-database".
for _, prefix := range prefixes {
expanded = append(expanded, fmt.Sprintf("%s/%s.*", test, prefix))
}
} else {
expanded = append(expanded, test)
}
}
return expanded
}
func findTests() []string {
rgBin, err := exec.LookPath("rg")
if err != nil {
log.Fatalf("failed to find rg (ripgrep) binary")
}
args := []string{
"--type", "go",
"--regexp", "func (Test.+)\\(.*",
"--max-depth", "1",
"../../integration/",
"--replace", "$1",
"--sort", "path",
"--no-line-number",
"--no-filename",
"--no-heading",
}
cmd := exec.Command(rgBin, args...)
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
log.Fatalf("failed to run command: %s", err)
}
tests := strings.Split(strings.TrimSpace(out.String()), "\n")
return tests
}
func updateYAML(tests []string, jobName string, testPath string) {
testsForYq := fmt.Sprintf("[%s]", strings.Join(tests, ", "))
yqCommand := fmt.Sprintf(
"yq eval '.jobs.%s.strategy.matrix.test = %s' %s -i",
jobName,
testsForYq,
testPath,
)
cmd := exec.Command("bash", "-c", yqCommand)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("stdout: %s", stdout.String())
log.Printf("stderr: %s", stderr.String())
log.Fatalf("failed to run yq command: %s", err)
}
fmt.Printf("YAML file (%s) job %s updated successfully\n", testPath, jobName)
}
func main() {
tests := findTests()
// Expand tests that should be split into multiple jobs
expandedTests := expandTests(tests)
quotedTests := make([]string, len(expandedTests))
for i, test := range expandedTests {
quotedTests[i] = fmt.Sprintf("\"%s\"", test)
}
// Define selected tests for PostgreSQL
postgresTestNames := []string{
"TestACLAllowUserDst",
"TestPingAllByIP",
"TestEphemeral2006DeletedTooQuickly",
"TestPingAllByIPManyUpDown",
"TestSubnetRouterMultiNetwork",
}
quotedPostgresTests := make([]string, len(postgresTestNames))
for i, test := range postgresTestNames {
quotedPostgresTests[i] = fmt.Sprintf("\"%s\"", test)
}
// Update both SQLite and PostgreSQL job matrices
updateYAML(quotedTests, "sqlite", "./test-integration.yaml")
updateYAML(quotedPostgresTests, "postgres", "./test-integration.yaml")
}
+23
View File
@@ -0,0 +1,23 @@
name: GitHub Actions Version Updater
on:
schedule:
# Automatically run on every Sunday
- cron: "0 0 * * 0"
jobs:
build:
if: github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# [Required] Access token with `workflow` scope.
token: ${{ secrets.WORKFLOW_SECRET }}
- name: Run GitHub Actions Version Updater
uses: saadmk11/github-actions-version-updater@d8781caf11d11168579c8e5e94f62b068038f442 # v0.9.0
with:
# [Required] Access token with `workflow` scope.
token: ${{ secrets.WORKFLOW_SECRET }}
@@ -0,0 +1,146 @@
name: Integration Test Template
on:
workflow_call:
inputs:
test:
required: true
type: string
postgres_flag:
required: false
type: string
default: ""
database_name:
required: true
type: string
jobs:
test:
runs-on: ubuntu-24.04-arm
env:
# Github does not allow us to access secrets in pull requests,
# so this env var is used to check if we have the secret or not.
# If we have the secrets, meaning we are running on push in a fork,
# there might be secrets available for more debugging.
# If TS_OAUTH_CLIENT_ID and TS_OAUTH_SECRET is set, then the job
# will join a debug tailscale network, set up SSH and a tmux session.
# The SSH will be configured to use the SSH key of the Github user
# that triggered the build.
HAS_TAILSCALE_SECRET: ${{ secrets.TS_OAUTH_CLIENT_ID }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
- name: Tailscale
if: ${{ env.HAS_TAILSCALE_SECRET }}
uses: tailscale/github-action@a392da0a182bba0e9613b6243ebd69529b1878aa # v4.1.0
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:gh
- name: Setup SSH server for Actor
if: ${{ env.HAS_TAILSCALE_SECRET }}
uses: alexellis/setup-sshd-actor@master
- name: Download headscale image
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: headscale-image
path: /tmp/artifacts
- name: Download tailscale HEAD image
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: tailscale-head-image
path: /tmp/artifacts
- name: Download tailscale released images
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: tailscale-released-images
path: /tmp/artifacts
- name: Download hi binary
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: hi-binary
path: /tmp/artifacts
- name: Download Go cache
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: go-cache
path: /tmp/artifacts
- name: Download postgres image
if: ${{ inputs.postgres_flag == '--postgres=1' }}
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: postgres-image
path: /tmp/artifacts
- name: Force overlay2 storage driver
run: |
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version
- name: Load br_netfilter for in-cluster service routing
if: inputs.test == 'TestK8sOperator'
# TestK8sOperator runs k3s in a container; without br_netfilter on the
# host, bridged pod-to-pod traffic skips kube-proxy's ClusterIP DNAT and
# in-cluster DNS (kube-dns) is unreachable. The module cannot be loaded
# from inside the unprivileged-module rancher/k3s image, so load it here.
run: sudo modprobe br_netfilter
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Load Docker images, Go cache, and prepare binary
run: |
gunzip -c /tmp/artifacts/headscale-image.tar.gz | docker load
gunzip -c /tmp/artifacts/tailscale-head-image.tar.gz | docker load
gunzip -c /tmp/artifacts/tailscale-released-images.tar.gz | docker load
if [ -f /tmp/artifacts/postgres-image.tar.gz ]; then
gunzip -c /tmp/artifacts/postgres-image.tar.gz | docker load
fi
chmod +x /tmp/artifacts/hi
docker images
# Extract Go cache to host directories for bind mounting
mkdir -p /tmp/go-cache
tar -xzf /tmp/artifacts/go-cache.tar.gz -C /tmp/go-cache
ls -la /tmp/go-cache/ /tmp/go-cache/.cache/
- name: Run Integration Test
env:
HEADSCALE_INTEGRATION_HEADSCALE_IMAGE: headscale:${{ github.sha }}
HEADSCALE_INTEGRATION_TAILSCALE_IMAGE: tailscale-head:${{ github.sha }}
HEADSCALE_INTEGRATION_POSTGRES_IMAGE: ${{ inputs.postgres_flag == '--postgres=1' && format('postgres:{0}', github.sha) || '' }}
HEADSCALE_INTEGRATION_GO_CACHE: /tmp/go-cache/go
HEADSCALE_INTEGRATION_GO_BUILD_CACHE: /tmp/go-cache/.cache/go-build
# Mirror the docker/login-action secrets into env so the
# dockertestutil.Credentials resolver picks them up directly
# (otherwise it falls back to parsing ~/.docker/config.json,
# which works but is one step further from the source).
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
run: /tmp/artifacts/hi run --stats --ts-memory-limit=300 --hs-memory-limit=1500 "^${{ inputs.test }}$" \
--timeout=120m \
${{ inputs.postgres_flag }}
# Sanitize test name for artifact upload (replace invalid characters: " : < > | * ? \ / with -)
- name: Sanitize test name for artifacts
if: always()
id: sanitize
run: echo "name=${TEST_NAME//[\":<>|*?\\\/]/-}" >> $GITHUB_OUTPUT
env:
TEST_NAME: ${{ inputs.test }}
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: always()
with:
name: ${{ inputs.database_name }}-${{ steps.sanitize.outputs.name }}-logs
path: "control_logs/*/*.log"
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: always()
with:
name: ${{ inputs.database_name }}-${{ steps.sanitize.outputs.name }}-artifacts
path: control_logs/
- name: Setup a blocking tmux session
if: ${{ env.HAS_TAILSCALE_SECRET }}
uses: alexellis/block-with-tmux-action@master
-76
View File
@@ -1,76 +0,0 @@
---
name: Lint
on: [push, pull_request]
jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- name: golangci-lint
if: steps.changed-files.outputs.any_changed == 'true'
uses: golangci/golangci-lint-action@v2
with:
version: v1.49.0
# Only block PRs on new problems.
# If this is not enabled, we will end up having PRs
# blocked because new linters has appared and other
# parts of the code is affected.
only-new-issues: true
prettier-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
**/*.md
**/*.yml
**/*.yaml
**/*.ts
**/*.js
**/*.sass
**/*.css
**/*.scss
**/*.html
- name: Prettify code
if: steps.changed-files.outputs.any_changed == 'true'
uses: creyD/prettier_action@v4.0
with:
prettier_options: >-
--check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
only_changed: false
dry: true
proto-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: bufbuild/buf-setup-action@v1.7.0
- uses: bufbuild/buf-lint-action@v1
with:
input: "proto"
@@ -0,0 +1,28 @@
name: Needs More Info - Post Comment
on:
issues:
types: [labeled]
jobs:
post-comment:
if: >-
github.event.label.name == 'needs-more-info' &&
github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
sparse-checkout: .github/label-response/needs-more-info.md
sparse-checkout-cone-mode: false
- name: Post instruction comment
run: gh issue comment "$NUMBER" --body-file .github/label-response/needs-more-info.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
@@ -0,0 +1,99 @@
name: Needs More Info - Timer
on:
schedule:
- cron: "0 0 * * *" # Daily at midnight UTC
issue_comment:
types: [created]
workflow_dispatch:
jobs:
# When a non-bot user comments on a needs-more-info issue, remove the label.
remove-label-on-response:
if: >-
github.repository == 'juanfont/headscale' &&
github.event_name == 'issue_comment' &&
github.event.comment.user.type != 'Bot' &&
contains(github.event.issue.labels.*.name, 'needs-more-info')
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Remove needs-more-info label
run: gh issue edit "$NUMBER" --remove-label needs-more-info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
# On schedule, close issues that have had no human response for 3 days.
close-stale:
if: >-
github.repository == 'juanfont/headscale' &&
github.event_name != 'issue_comment'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: hustcer/setup-nu@920172d92eb04671776f3ba69d605d3b09351c30 # v3.22
with:
version: "*"
- name: Close stale needs-more-info issues
shell: nu {0}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
let issues = (gh issue list
--repo $env.GH_REPO
--label "needs-more-info"
--state open
--json number
| from json)
for issue in $issues {
let number = $issue.number
print $"Checking issue #($number)"
# Find when needs-more-info was last added
let events = (gh api $"repos/($env.GH_REPO)/issues/($number)/events"
--paginate | from json)
let label_event = ($events
| where event == "labeled"
| where label.name == "needs-more-info"
| last)
let label_added_at = ($label_event.created_at | into datetime)
# Check for non-bot comments after the label was added
let comments = (gh api $"repos/($env.GH_REPO)/issues/($number)/comments"
--paginate | from json)
let human_responses = ($comments
| where user.type != "Bot"
| where { ($in.created_at | into datetime) > $label_added_at })
if ($human_responses | length) > 0 {
print $" Human responded, removing label"
gh issue edit $number --repo $env.GH_REPO --remove-label needs-more-info
continue
}
# Check if 3 days have passed
let elapsed = (date now) - $label_added_at
if $elapsed < 3day {
print $" Only ($elapsed | format duration day) elapsed, skipping"
continue
}
print $" No response for ($elapsed | format duration day), closing"
let message = [
"This issue has been automatically closed because no additional information was provided within 3 days."
""
"If you have the requested information, please open a new issue and include the debug information requested above."
""
"Thank you for your understanding."
] | str join "\n"
gh issue comment $number --repo $env.GH_REPO --body $message
gh issue close $number --repo $env.GH_REPO --reason "not planned"
gh issue edit $number --repo $env.GH_REPO --remove-label needs-more-info
}
+56
View File
@@ -0,0 +1,56 @@
name: Nix Flake Checks
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
# Each job only runs `nix build .#checks.<system>.<name>`; the check logic lives
# in flake.nix via the flake-checks library. The fileset-filtered checks hit the
# hestia cache when their inputs are unchanged, so no changed-files gating.
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: build
run: nix build -L .#checks.x86_64-linux.build
gotest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: gotest
run: nix build -L .#checks.x86_64-linux.gotest
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: golangci-lint
run: nix build -L .#checks.x86_64-linux.golangci-lint
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: formatting
run: nix build -L .#checks.x86_64-linux.formatting
+51
View File
@@ -0,0 +1,51 @@
name: NixOS Module Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
nix-module-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
nix:
- 'nix/**'
- 'flake.nix'
- 'flake.lock'
go:
- 'go.*'
- '**/*.go'
- 'cmd/**'
- 'hscontrol/**'
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
- name: Run NixOS module tests
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
run: |
echo "Running NixOS module integration test..."
nix build .#checks.x86_64-linux.headscale -L --fallback
+24 -210
View File
@@ -7,223 +7,37 @@ on:
- "*" # triggers only if push new tag version
workflow_dispatch:
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
jobs:
goreleaser:
runs-on: ubuntu-18.04 # due to CGO we need to user an older version
if: github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19.0
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
distribution: goreleaser
version: latest
args: release --rm-dist
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: Run goreleaser
run: goreleaser release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest
type=sha
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
build-args: |
VERSION=${{ steps.meta.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
docker-debug-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-debug
key: ${{ runner.os }}-buildx-debug-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-debug-
- name: Docker meta
id: meta-debug
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}-debug
type=semver,pattern={{major}}.{{minor}}-debug
type=semver,pattern={{major}}-debug
type=raw,value=latest-debug
type=sha,suffix=-debug
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.debug
tags: ${{ steps.meta-debug.outputs.tags }}
labels: ${{ steps.meta-debug.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-debug
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
build-args: |
VERSION=${{ steps.meta-debug.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-debug
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug
docker-alpine-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Set up QEMU for multiple platforms
uses: docker/setup-qemu-action@master
with:
platforms: arm64,amd64
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache-alpine
key: ${{ runner.os }}-buildx-alpine-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-alpine-
- name: Docker meta
id: meta-alpine
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
type=raw,value=latest-alpine
type=sha,suffix=-alpine
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
file: Dockerfile.alpine
tags: ${{ steps.meta-alpine.outputs.tags }}
labels: ${{ steps.meta-alpine.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache-alpine
cache-to: type=local,dest=/tmp/.buildx-cache-alpine-new
build-args: |
VERSION=${{ steps.meta-alpine.outputs.version }}
- name: Prepare cache for next build
run: |
rm -rf /tmp/.buildx-cache-alpine
mv /tmp/.buildx-cache-alpine-new /tmp/.buildx-cache-alpine
-27
View File
@@ -1,27 +0,0 @@
---
name: Renovate
on:
schedule:
- cron: "* * 5,20 * *" # Every 5th and 20th of the month
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Get token
id: get_token
uses: machine-learning-apps/actions-app-token@master
with:
APP_PEM: ${{ secrets.RENOVATEBOT_SECRET }}
APP_ID: ${{ secrets.RENOVATEBOT_APP_ID }}
- name: Checkout
uses: actions/checkout@v3
- name: Self-hosted Renovate
uses: renovatebot/github-action@v31.81.3
with:
configurationFile: .github/renovate.json
token: "x-access-token:${{ steps.get_token.outputs.app_token }}"
# env:
# LOG_LEVEL: "debug"
+36
View File
@@ -0,0 +1,36 @@
name: Server Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
# hscontrol/servertest is excluded from the sandboxed gotest flake check: it is
# slow (10s+ convergence cases) and timing-sensitive (race/stress/HA property
# tests), so it runs here in the devShell with a generous timeout instead.
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
permissions:
contents: read
jobs:
servertest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: go test ./hscontrol/servertest
env:
CGO_ENABLED: "0"
run: go test -timeout=20m ./hscontrol/servertest/...
+27
View File
@@ -0,0 +1,27 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
if: github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
days-before-issue-stale: 90
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 90 days with no
activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days
since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "no-stale-bot,needs-more-info"
repo-token: ${{ secrets.GITHUB_TOKEN }}
+30
View File
@@ -0,0 +1,30 @@
name: Support Request - Close Issue
on:
issues:
types: [labeled]
jobs:
close-support-request:
if: >-
github.event.label.name == 'support-request' &&
github.repository == 'juanfont/headscale'
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
sparse-checkout: .github/label-response/support-request.md
sparse-checkout-cone-mode: false
- name: Post comment and close issue
run: |
gh issue comment "$NUMBER" --body-file .github/label-response/support-request.md
gh issue close "$NUMBER" --reason "not planned"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
@@ -1,35 +0,0 @@
name: Integration Test CLI
on: [pull_request]
jobs:
integration-test-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run CLI integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_cli
@@ -1,35 +0,0 @@
name: Integration Test DERP
on: [pull_request]
jobs:
integration-test-derp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run Embedded DERP server integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_derp
@@ -1,35 +0,0 @@
name: Integration Test OIDC
on: [pull_request]
jobs:
integration-test-oidc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run OIDC integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_oidc
@@ -1,27 +0,0 @@
name: Integration Test v2 - kradalby
on: [pull_request]
jobs:
integration-test-v2-kradalby:
runs-on: [self-hosted, linux, x64, nixos, docker]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_v2_general
+408
View File
@@ -0,0 +1,408 @@
name: integration
# To debug locally on a branch, and when needing secrets
# change this to include `push` so the build is ran on
# the main repository.
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
defaults:
run:
shell: nix develop --fallback --command bash -e {0}
jobs:
# build: Builds binaries and Docker images once, uploads as artifacts for reuse.
# build-postgres: Pulls postgres image separately to avoid Docker Hub rate limits.
# build-tailscale-released: Pre-pulls released Tailscale images from ghcr.io
# so fork PRs (no DOCKERHUB_USERNAME secret) don't hit Docker Hub rate
# limits at test time.
# sqlite: Runs all integration tests with SQLite backend.
# postgres: Runs a subset of tests with PostgreSQL to verify database compatibility.
build:
runs-on: ubuntu-24.04-arm
outputs:
files-changed: ${{ steps.changed-files.outputs.files }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
files:
- '*.nix'
- 'go.*'
- '**/*.go'
- 'integration/**'
- 'config-example.yaml'
- '.github/workflows/test-integration.yaml'
- '.github/workflows/integration-test-template.yml'
- 'Dockerfile.*'
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
if: steps.changed-files.outputs.files == 'true'
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
if: steps.changed-files.outputs.files == 'true'
- name: Build binaries and warm Go cache
if: steps.changed-files.outputs.files == 'true'
run: |
# Build all Go binaries in one nix shell to maximize cache reuse
go build -o hi ./cmd/hi
CGO_ENABLED=0 GOOS=linux go build -o headscale ./cmd/headscale
# Build integration test binary to warm the cache with all dependencies
go test -c ./integration -o /dev/null 2>/dev/null || true
- name: Upload hi binary
if: steps.changed-files.outputs.files == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: hi-binary
path: hi
retention-days: 10
- name: Package Go cache
if: steps.changed-files.outputs.files == 'true'
run: |
# Package Go module cache and build cache
tar -czf go-cache.tar.gz -C ~ go .cache/go-build
- name: Upload Go cache
if: steps.changed-files.outputs.files == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: go-cache
path: go-cache.tar.gz
retention-days: 10
- name: Force overlay2 storage driver
if: steps.changed-files.outputs.files == 'true'
run: |
# Docker 29 runner images default to overlayfs, which breaks
# docker build via Go SDK libraries and docker save/load
# tarball formats. overlay2 is the long-standing default.
# https://github.com/actions/runner-images/issues/13474
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Build headscale image
if: steps.changed-files.outputs.files == 'true'
run: |
docker build \
--file Dockerfile.integration-ci \
--tag headscale:${{ github.sha }} \
.
docker save headscale:${{ github.sha }} | gzip > headscale-image.tar.gz
- name: Build tailscale HEAD image
if: steps.changed-files.outputs.files == 'true'
run: |
docker build \
--file Dockerfile.tailscale-HEAD \
--tag tailscale-head:${{ github.sha }} \
.
docker save tailscale-head:${{ github.sha }} | gzip > tailscale-head-image.tar.gz
- name: Upload headscale image
if: steps.changed-files.outputs.files == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: headscale-image
path: headscale-image.tar.gz
retention-days: 10
- name: Upload tailscale HEAD image
if: steps.changed-files.outputs.files == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: tailscale-head-image
path: tailscale-head-image.tar.gz
retention-days: 10
build-postgres:
runs-on: ubuntu-24.04-arm
needs: build
if: needs.build.outputs.files-changed == 'true'
steps:
- name: Force overlay2 storage driver
shell: bash
run: |
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Pull and save postgres image
shell: bash
run: |
docker pull postgres:latest
docker tag postgres:latest postgres:${{ github.sha }}
docker save postgres:${{ github.sha }} | gzip > postgres-image.tar.gz
- name: Upload postgres image
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: postgres-image
path: postgres-image.tar.gz
retention-days: 10
build-tailscale-released:
runs-on: ubuntu-24.04-arm
needs: build
if: needs.build.outputs.files-changed == 'true'
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
- name: Force overlay2 storage driver
run: |
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: List Tailscale versions to pre-pull
id: versions
run: |
versions=$(go run ./cmd/hi list-versions --set=must --exclude=head)
echo "versions=${versions}" >> "$GITHUB_OUTPUT"
echo "Pre-pulling: ${versions}"
- name: Pull Tailscale images
run: |
# Releases come from ghcr.io (anonymous, unmetered). The
# "unstable" floating tag on ghcr.io has been stale since 2022,
# so it still needs to come from Docker Hub. xargs -P 0 fans
# out one process per tag and returns non-zero if any pull
# fails.
refs=""
for v in ${{ steps.versions.outputs.versions }}; do
if [ "${v}" = "unstable" ]; then
refs="${refs} tailscale/tailscale:${v}"
else
refs="${refs} ghcr.io/tailscale/tailscale:${v}"
fi
done
echo "${refs}" | tr ' ' '\n' | grep -v '^$' \
| xargs -P 0 -I{} docker pull "{}"
echo "REFS=${refs}" >> "$GITHUB_ENV"
- name: Save Tailscale images to tarball
run: |
# Single docker save with all refs: one consistent snapshot, no
# parallel-daemon race.
docker save ${REFS} | gzip > tailscale-released-images.tar.gz
ls -lh tailscale-released-images.tar.gz
- name: Upload Tailscale released images
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: tailscale-released-images
path: tailscale-released-images.tar.gz
retention-days: 10
sqlite:
needs: [build, build-tailscale-released]
if: needs.build.outputs.files-changed == 'true'
strategy:
fail-fast: false
matrix:
test:
- TestACLHostsInNetMapTable
- TestACLAllowUser80Dst
- TestACLDenyAllPort80
- TestACLAllowUserDst
- TestACLAllowStarDst
- TestACLNamedHostsCanReachBySubnet
- TestACLNamedHostsCanReach
- TestACLDevice1CanAccessDevice2
- TestPolicyUpdateWhileRunningWithCLIInDatabase
- TestACLAutogroupMember
- TestACLAutogroupTagged
- TestACLAutogroupSelf
- TestACLPolicyPropagationOverTime
- TestACLTagPropagation
- TestACLTagPropagationPortSpecific
- TestACLGroupWithUnknownUser
- TestACLGroupAfterUserDeletion
- TestACLGroupDeletionExactReproduction
- TestACLDynamicUnknownUserAddition
- TestACLDynamicUnknownUserRemoval
- TestAPIAuthenticationBypass
- TestAPIAuthenticationBypassCurl
- TestRemoteCLIAuthenticationBypass
- TestCLIWithConfigAuthenticationBypass
- TestAuthKeyLogoutAndReloginSameUser
- TestAuthKeyLogoutAndReloginNewUser
- TestAuthKeyLogoutAndReloginSameUserExpiredKey
- TestAuthKeyDeleteKey
- TestAuthKeyLogoutAndReloginRoutesPreserved
- TestOIDCAuthenticationPingAll
- TestOIDCExpireNodesBasedOnTokenExpiry
- TestOIDC024UserCreation
- TestOIDCAuthenticationWithPKCE
- TestOIDCReloginSameNodeNewUser
- TestOIDCFollowUpUrl
- TestOIDCMultipleOpenedLoginUrls
- TestOIDCReloginSameNodeSameUser
- TestOIDCExpiryAfterRestart
- TestOIDCACLPolicyOnJoin
- TestOIDCReloginSameUserRoutesPreserved
- TestAuthWebFlowAuthenticationPingAll
- TestAuthWebFlowLogoutAndReloginSameUser
- TestAuthWebFlowLogoutAndReloginNewUser
- TestApiKeyCommand
- TestApiKeyCommandValidation
- TestAuthCommandValidation
- TestNodeCommand
- TestNodeExpireCommand
- TestNodeRenameCommand
- TestPreAuthKeyCorrectUserLoggedInCommand
- TestTaggedNodesCLIOutput
- TestNodeExpireFlagsCommand
- TestNodeCommandValidation
- TestNodeTagCommand
- TestNodeRouteCommands
- TestNodeBackfillIPsCommand
- TestOAuthClientCommand
- TestOAuthClientCommandValidation
- TestPolicyCheckCommand
- TestSSHTestsRejectFailingPolicy
- TestPolicyCommand
- TestPolicyBrokenConfigCommand
- TestPreAuthKeyCommand
- TestPreAuthKeyCommandWithoutExpiry
- TestPreAuthKeyCommandReusableEphemeral
- TestPreAuthKeyDeleteCommand
- TestPreAuthKeyCommandValidation
- TestServerInfoCommands
- TestUserCommand
- TestUserCreateCommand
- TestUserCommandValidation
- TestDERPVerifyEndpoint
- TestResolveMagicDNS
- TestResolveMagicDNSExtraRecordsPath
- TestDERPServerScenario
- TestDERPServerWebsocketScenario
- TestPingAllByIP
- TestPingAllByIPPublicDERP
- TestEphemeral
- TestEphemeralInAlternateTimezone
- TestEphemeral2006DeletedTooQuickly
- TestPingAllByHostname
- TestTaildrop
- TestUpdateHostnameFromClient
- TestExpireNode
- TestSetNodeExpiryInFuture
- TestDisableNodeExpiry
- TestNodeOnlineStatus
- TestPingAllByIPManyUpDown
- Test2118DeletingOnlineNodePanics
- TestGrantCapRelay
- TestGrantCapDrive
- TestK8sOperator
- TestEnablingRoutes
- TestHASubnetRouterFailover
- TestSubnetRouteACL
- TestEnablingExitRoutes
- TestExitRoutesWithAutogroupInternetACL
- TestSubnetRouterMultiNetwork
- TestSubnetRouterMultiNetworkExitNode
- TestAutoApproveMultiNetwork/authkey-tag.*
- TestAutoApproveMultiNetwork/authkey-user.*
- TestAutoApproveMultiNetwork/authkey-group.*
- TestAutoApproveMultiNetwork/webauth-tag.*
- TestAutoApproveMultiNetwork/webauth-user.*
- TestAutoApproveMultiNetwork/webauth-group.*
- TestSubnetRouteACLFiltering
- TestGrantViaSubnetSteering
- TestHASubnetRouterPingFailover
- TestHASubnetRouterFailoverBothOffline
- TestHASubnetRouterFailoverBothOfflineCablePull
- TestHASubnetRouterFailoverDockerDisconnect
- TestHeadscale
- TestTailscaleNodesJoiningHeadcale
- TestSSHOneUserToAll
- TestSSHMultipleUsersAllToAll
- TestSSHNoSSHConfigured
- TestSSHIsBlockedInACL
- TestSSHUserOnlyIsolation
- TestSSHAutogroupSelf
- TestSSHOneUserToOneCheckModeCLI
- TestSSHOneUserToOneCheckModeOIDC
- TestSSHCheckModeUnapprovedTimeout
- TestSSHCheckModeCheckPeriodCLI
- TestSSHCheckModeAutoApprove
- TestSSHCheckModeSessionLossReDelegates
- TestSSHCheckModeNegativeCLI
- TestSSHLocalpart
- TestTagsAuthKeyWithTagRequestDifferentTag
- TestTagsAuthKeyWithTagNoAdvertiseFlag
- TestTagsAuthKeyWithTagCannotAddViaCLI
- TestTagsAuthKeyWithTagCannotChangeViaCLI
- TestTagsAuthKeyWithTagAdminOverrideReauthPreserves
- TestTagsAuthKeyWithTagCLICannotModifyAdminTags
- TestTagsAuthKeyWithoutTagCannotRequestTags
- TestTagsAuthKeyWithoutTagRegisterNoTags
- TestTagsAuthKeyWithoutTagCannotAddViaCLI
- TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset
- TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise
- TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag
- TestTagsUserLoginOwnedTagAtRegistration
- TestTagsUserLoginNonExistentTagAtRegistration
- TestTagsUserLoginUnownedTagAtRegistration
- TestTagsUserLoginAddTagViaCLIReauth
- TestTagsUserLoginRemoveTagViaCLIReauth
- TestTagsUserLoginCLINoOpAfterAdminAssignment
- TestTagsUserLoginCLICannotRemoveAdminTags
- TestTagsAuthKeyWithTagRequestNonExistentTag
- TestTagsAuthKeyWithTagRequestUnownedTag
- TestTagsAuthKeyWithoutTagRequestNonExistentTag
- TestTagsAuthKeyWithoutTagRequestUnownedTag
- TestTagsAdminAPICannotSetNonExistentTag
- TestTagsAdminAPICanSetUnownedTag
- TestTagsAdminAPICannotRemoveAllTags
- TestTagsIssue2978ReproTagReplacement
- TestTagsAdminAPICannotSetInvalidFormat
- TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags
- TestTagsAuthKeyWithoutUserInheritsTags
- TestTagsAuthKeyWithoutUserRejectsAdvertisedTags
- TestTagsAuthKeyConvertToUserViaCLIRegister
- TestTS2021WebSocketGET
- TestTS2021WASMClientUnderNode
- TestTailscaleRustAxum
uses: ./.github/workflows/integration-test-template.yml
secrets: inherit
with:
test: ${{ matrix.test }}
postgres_flag: "--postgres=0"
database_name: "sqlite"
postgres:
needs: [build, build-postgres, build-tailscale-released]
if: needs.build.outputs.files-changed == 'true'
strategy:
fail-fast: false
matrix:
test:
- TestACLAllowUserDst
- TestPingAllByIP
- TestEphemeral2006DeletedTooQuickly
- TestPingAllByIPManyUpDown
- TestSubnetRouterMultiNetwork
uses: ./.github/workflows/integration-test-template.yml
secrets: inherit
with:
test: ${{ matrix.test }}
postgres_flag: "--postgres=1"
database_name: "postgres"
-30
View File
@@ -1,30 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v34
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --check
+28 -3
View File
@@ -1,3 +1,11 @@
ignored/
tailscale/
.vscode/
.claude/
logs/
*.prof
# Binaries for programs and plugins
*.exe
*.exe~
@@ -12,13 +20,16 @@
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
vendor/
dist/
/headscale
config.json
config.yaml
config*.yaml
!config-example.yaml
derp.yaml
*.hujson
!hscontrol/policy/v2/testdata/*/*.hujson
*.key
/db.sqlite
*.sqlite3
@@ -26,10 +37,24 @@ derp.yaml
# Exclude Jetbrains Editors
.idea
test_output/
test_output/
control_logs/
# Nix build output
result
.direnv/
integration_test/etc/config.dump.yaml
# OpenAPI spec is served live from the code and emitted on demand, not committed
/openapi/v1/headscale.yaml
# MkDocs
.cache
/site
__debug_bin
node_modules/
package-lock.json
package.json
+109 -55
View File
@@ -1,66 +1,120 @@
---
run:
timeout: 10m
build-tags:
- ts2019
issues:
skip-dirs:
- gen
version: "2"
linters:
enable-all: true
default: all
disable:
- exhaustivestruct
- revive
- lll
- interfacer
- scopelint
- maligned
- golint
- gofmt
- cyclop
- depguard
- dupl
- exhaustruct
- funcorder
- funlen
- gochecknoglobals
- gochecknoinits
- gocognit
- funlen
- exhaustivestruct
- tagliatelle
- godox
- ireturn
- execinquery
- exhaustruct
- nolintlint
# We should strive to enable these:
- wrapcheck
- dupl
- makezero
- maintidx
# Limits the methods of an interface to 10. We have more in integration tests
- gomodguard
- interfacebloat
# We might want to enable this, but it might be a lot of work
- cyclop
- ireturn
- lll
- maintidx
- makezero
- mnd
- musttag
- nestif
- wsl # might be incompatible with gofumpt
- testpackage
- nolintlint
- paralleltest
- revive
- tagliatelle
- testpackage
- varnamelen
- wrapcheck
- wsl
settings:
goconst:
# Test fixtures repeat strings (IPs, tags, hostnames) by their
# nature; extracting them obscures the test rather than helping.
# Production code stays strict.
ignore-tests: true
# Default is 3. Bump so "happens thrice" cases that are not part
# of a shared vocabulary do not get extracted.
min-occurrences: 5
# Default is 3. Short literals ("set", "get", "new") read better
# at call sites than behind a named constant.
min-len: 6
forbidigo:
forbid:
# Forbid time.Sleep everywhere with context-appropriate alternatives
- pattern: 'time\.Sleep'
msg: >-
time.Sleep is forbidden.
In tests: use assert.EventuallyWithT for polling/waiting patterns.
In production code: use a backoff strategy (e.g., cenkalti/backoff) or proper synchronization primitives.
# Forbid inline string literals in zerolog field methods - use zf.* constants
- pattern: '\.(Str|Int|Int8|Int16|Int32|Int64|Uint|Uint8|Uint16|Uint32|Uint64|Float32|Float64|Bool|Dur|Time|TimeDiff|Strs|Ints|Uints|Floats|Bools|Any|Interface)\("[^"]+"'
msg: >-
Use zf.* constants for zerolog field names instead of string literals.
Import "github.com/juanfont/headscale/hscontrol/util/zlog/zf" and use
constants like zf.NodeID, zf.UserName, etc. Add new constants to
hscontrol/util/zlog/zf/fields.go if needed.
# Forbid ptr.To - use Go 1.26 new(expr) instead
- pattern: 'ptr\.To\('
msg: >-
ptr.To is forbidden. Use Go 1.26's new(expr) syntax instead.
Example: ptr.To(value) → new(value)
# Forbid tsaddr.SortPrefixes - use slices.SortFunc with netip.Prefix.Compare
- pattern: 'tsaddr\.SortPrefixes'
msg: >-
tsaddr.SortPrefixes is forbidden. Use Go 1.26's netip.Prefix.Compare instead.
Example: slices.SortFunc(prefixes, netip.Prefix.Compare)
analyze-types: true
gocritic:
disabled-checks:
- appendAssign
- ifElseChain
nlreturn:
block-size: 4
varnamelen:
ignore-names:
- err
- db
- id
- ip
- ok
- c
- tt
- tx
- rx
- sb
- wg
- pr
- p
- p2
ignore-type-assert-ok: true
ignore-map-index-ok: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
- gen
linters-settings:
varnamelen:
ignore-type-assert-ok: true
ignore-map-index-ok: true
ignore-names:
- err
- db
- id
- ip
- ok
- c
- tt
gocritic:
disabled-checks:
- appendAssign
# TODO(kradalby): Remove this
- ifElseChain
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- gen
+137 -63
View File
@@ -1,86 +1,160 @@
---
version: 2
before:
hooks:
- go mod tidy -compat=1.19
- go mod tidy -compat=1.26
- go mod vendor
release:
prerelease: auto
draft: true
header: |
## Upgrade
Please follow the steps outlined in the [upgrade guide](https://headscale.net/stable/setup/upgrade/) to update your existing Headscale installation.
builds:
- id: darwin-amd64
main: ./cmd/headscale/headscale.go
- id: headscale
main: ./cmd/headscale
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- darwin
goarch:
- amd64
targets:
- darwin_amd64
- darwin_arm64
- freebsd_amd64
- linux_amd64
- linux_arm64
flags:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: darwin-arm64
main: ./cmd/headscale/headscale.go
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-amd64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
- id: linux-arm64
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- arm64
main: ./cmd/headscale/headscale.go
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}
tags:
- ts2019
archives:
- id: golang-cross
builds:
- darwin-amd64
- darwin-arm64
- linux-amd64
- linux-arm64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: binary
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
formats:
- binary
source:
enabled: true
name_template: "{{ .ProjectName }}_{{ .Version }}"
format: tar.gz
files:
- "vendor/"
nfpms:
# Configure nFPM for .deb releases
#
# See https://goreleaser.com/customization/package/nfpm/
#
# Useful tools for debugging .debs:
# List file contents: dpkg -c dist/headscale...deb
# Package metadata: dpkg --info dist/headscale....deb
#
- ids:
- headscale
package_name: headscale
priority: optional
vendor: headscale
maintainer: Kristoffer Dalby <kristoffer@dalby.cc>
homepage: https://github.com/juanfont/headscale
description: |-
Open source implementation of the Tailscale control server.
Headscale aims to implement a self-hosted, open source alternative to the
Tailscale control server. Headscale's goal is to provide self-hosters and
hobbyists with an open-source server they can use for their projects and
labs. It implements a narrow scope, a single Tailscale network (tailnet),
suitable for a personal use, or a small open-source organisation.
bindir: /usr/bin
section: net
formats:
- deb
contents:
- src: ./config-example.yaml
dst: /etc/headscale/config.yaml
type: config|noreplace
file_info:
mode: 0644
- src: ./packaging/systemd/headscale.service
dst: /usr/lib/systemd/system/headscale.service
- dst: /var/lib/headscale
type: dir
- src: ./config-example.yaml
dst: /usr/share/doc/headscale/examples/config-example.yaml
- src: LICENSE
dst: /usr/share/doc/headscale/copyright
scripts:
postinstall: ./packaging/deb/postinst
postremove: ./packaging/deb/postrm
preremove: ./packaging/deb/prerm
deb:
lintian_overrides:
- no-changelog # Our CHANGELOG.md uses a different formatting
- no-manual-page
- statically-linked-binary
kos:
- id: ghcr
repositories:
- ghcr.io/juanfont/headscale
- headscale/headscale
# bare tells KO to only use the repository
# for tagging and naming the container.
bare: true
base_image: gcr.io/distroless/base-debian13
build: headscale
main: ./cmd/headscale
env:
- CGO_ENABLED=0
platforms:
- linux/amd64
- linux/arm64
tags:
- "{{ if not .Prerelease }}latest{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}{{ end }}"
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}"
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}{{ end }}"
- "{{ if not .Prerelease }}v{{ .Major }}{{ end }}"
- "{{ if not .Prerelease }}stable{{ else }}unstable{{ end }}"
- "{{ .Tag }}"
- '{{ trimprefix .Tag "v" }}'
- "sha-{{ .ShortCommit }}"
creation_time: "{{.CommitTimestamp}}"
ko_data_creation_time: "{{.CommitTimestamp}}"
- id: ghcr-debug
repositories:
- ghcr.io/juanfont/headscale
- headscale/headscale
bare: true
base_image: gcr.io/distroless/base-debian13:debug
build: headscale
main: ./cmd/headscale
env:
- CGO_ENABLED=0
platforms:
- linux/amd64
- linux/arm64
tags:
- "{{ if not .Prerelease }}latest-debug{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}-debug{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}-debug{{ end }}"
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
- "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
- "{{ if not .Prerelease }}stable-debug{{ else }}unstable-debug{{ end }}"
- "{{ .Tag }}-debug"
- '{{ trimprefix .Tag "v" }}-debug'
- "sha-{{ .ShortCommit }}-debug"
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Tag }}-next"
version_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
+34
View File
@@ -0,0 +1,34 @@
{
"mcpServers": {
"claude-code-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@steipete/claude-code-mcp@latest"],
"env": {}
},
"sequential-thinking": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
"env": {}
},
"nixos": {
"type": "stdio",
"command": "uvx",
"args": ["mcp-nixos"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
},
"git": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/git-mcp-server"],
"env": {}
}
}
}
+2
View File
@@ -0,0 +1,2 @@
[plugin.mkdocs]
align_semantic_breaks_in_lists = true
+72
View File
@@ -0,0 +1,72 @@
# prek/pre-commit configuration for headscale
# See: https://prek.j178.dev/quickstart/
# See: https://prek.j178.dev/builtin/
# Global exclusions - ignore generated code (proto output and emitted OpenAPI)
# and recorded golden fixtures.
exclude: ^(gen|openapi)/|^hscontrol/testdata/apiv1_golden/
repos:
# Built-in hooks from pre-commit/pre-commit-hooks
# prek will use fast-path optimized versions automatically
# See: https://prek.j178.dev/builtin/
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
args: [--maxkb=1024]
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-xml
- id: check-yaml
- id: detect-private-key
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: mixed-line-ending
- id: trailing-whitespace
# Local hooks for project-specific tooling
- repo: local
hooks:
# nixpkgs-fmt for Nix files
- id: nixpkgs-fmt
name: nixpkgs-fmt
entry: nixpkgs-fmt
language: system
files: \.nix$
# Prettier for formatting
- id: prettier
name: prettier
entry: prettier --write --list-different
language: system
exclude: ^docs/
types_or: [javascript, jsx, ts, tsx, yaml, json, toml, html, css, scss, sass, markdown]
# mdformat for docs
- id: mdformat
name: mdformat
entry: mdformat
language: system
types_or: [markdown]
files: ^docs/
# golangci-lint for Go code quality
- id: golangci-lint
name: golangci-lint
entry: nix develop --command -- golangci-lint run --new-from-rev=HEAD~1 --timeout=5m --fix
language: system
types: [go]
pass_filenames: false
# vendor-hash keeps flakehashes.json in sync with go.mod/go.sum.
- id: vendor-hash
name: vendor-hash
entry: nix develop --command -- go run ./cmd/vendorhash check
language: system
files: ^(go\.mod|go\.sum|flakehashes\.json)$
pass_filenames: false
+2
View File
@@ -0,0 +1,2 @@
.github/workflows/test-integration-v2*
docs/
+296
View File
@@ -0,0 +1,296 @@
# AGENTS.md
Behavioural guidance for AI agents working in this repository. Reference
material for complex procedures lives next to the code — integration
testing is documented in [`cmd/hi/README.md`](cmd/hi/README.md) and
[`integration/README.md`](integration/README.md). Read those files
before running tests or writing new ones.
Headscale is an open-source implementation of the Tailscale control server
written in Go. It manages node registration, IP allocation, policy
enforcement, and DERP routing for self-hosted tailnets.
## Interaction Rules
These rules govern how you work in this repo. They are listed first
because they shape every other decision.
### Ask with comprehensive multiple-choice options
When you need to clarify intent, scope, or approach, use the
`AskUserQuestion` tool (or a numbered list fallback) and present the user
with a comprehensive set of options. Cover the likely branches explicitly
and include an "other — please describe" escape.
- Bad: _"How should I handle expired nodes?"_
- Good: _"How should expired nodes be handled? (a) Remain visible to peers
but marked expired (current behaviour); (b) Hidden from peers entirely;
(c) Hidden from peers but visible in admin API; (d) Other."_
This matters more than you think — open-ended questions waste a round
trip and often produce a misaligned answer.
### Read the documented procedure before running complex commands
Before invoking any `hi` command, integration test, generator, or
migration tool, read the referenced README in full —
`cmd/hi/README.md` for running tests, `integration/README.md` for
writing them. Never guess flags. If the procedure is not documented
anywhere, ask the user rather than inventing one.
### Map once, then act
Use `Glob` / `Grep` to understand file structure, then execute. Do not
re-explore the same area to "double-check" once you have a plan. Do not
re-read files you edited in this session — the harness tracks state for
you.
### Fail fast, report up
If a command fails twice with the same error, stop and report the exact
error to the user with context. Do not loop through variants or
"try one more thing". A repeated failure means your model of the problem
is wrong.
### Confirm scope for multi-file changes
Before touching more than three files, show the user which files will
change and why. Use plan mode (`ExitPlanMode`) for non-trivial work.
### Prefer editing existing files
Do not create new files unless strictly necessary. Do not generate helper
abstractions, wrapper utilities, or "just in case" configuration. Three
similar lines of code is better than a premature abstraction.
## Quick Start
```bash
# Enter the nix dev shell (Go 1.26.1, buf, golangci-lint, prek)
nix develop
# Full development workflow: fmt + lint + test + build
make dev
# Individual targets
make build # build the headscale binary
make test # go test ./...
make fmt # format Go, docs, proto
make lint # lint Go, proto
make generate # regenerate protobuf code (after changes to proto/)
make clean # remove build artefacts
# Direct go test invocations
go test ./...
go test -race ./...
# Integration tests — read cmd/hi/README.md first
go run ./cmd/hi doctor
go run ./cmd/hi run "TestName"
```
Go 1.26.1 minimum (per `go.mod:3`). `nix develop` pins the exact toolchain
used in CI.
## Pre-Commit with prek
`prek` installs git hooks that run the same checks as CI.
```bash
nix develop
prek install # one-time setup
prek run # run hooks on staged files
prek run --all-files # run hooks on the full tree
```
Hooks cover: file hygiene (trailing whitespace, line endings, BOM),
syntax validation (JSON/YAML/TOML/XML), merge-conflict markers, private
key detection, nixpkgs-fmt, prettier, and `golangci-lint` via
`--new-from-rev=HEAD~1` (see `.pre-commit-config.yaml:59`). A manual
invocation with an `upstream/main` remote is equivalent:
```bash
golangci-lint run --new-from-rev=upstream/main --timeout=5m --fix
```
`git commit --no-verify` is acceptable only for WIP commits on feature
branches — never on `main`.
## Project Layout
```
headscale/
├── cmd/
│ ├── headscale/ # Main headscale server binary
│ └── hi/ # Integration test runner (see cmd/hi/README.md)
├── hscontrol/ # Core control plane
├── integration/ # End-to-end Docker-based tests (see integration/README.md)
├── proto/ # Protocol buffer definitions
├── gen/ # Generated code (buf output — do not edit)
├── docs/ # User and ACL reference documentation
└── packaging/ # Distribution packaging
```
### `hscontrol/` packages
- `app.go`, `handlers.go`, `grpcv1.go`, `noise.go`, `auth.go`, `oidc.go`,
`poll.go`, `metrics.go`, `debug.go`, `tailsql.go`, `platform_config.go`
— top-level server files
- `state/` — central coordinator (`state.go`) and the copy-on-write
`NodeStore` (`node_store.go`). All cross-subsystem operations go
through `State`.
- `db/` — GORM layer, migrations, schema. `node.go`, `users.go`,
`api_key.go`, `preauth_keys.go`, `ip.go`, `policy.go`.
- `mapper/` — streaming batcher that distributes MapResponses to
clients: `batcher.go`, `node_conn.go`, `builder.go`, `mapper.go`.
Performance-critical.
- `policy/``policy/v2/` is **the** policy implementation. The
top-level `policy.go` is thin wrappers. There is no v1 directory.
- `routes/`, `dns/`, `derp/`, `types/`, `util/`, `templates/`, `capver/`
— routing, MagicDNS, relay, core types, helpers, client templates,
capability versioning.
- `servertest/` — in-memory test harness for server-level tests that
don't need Docker. Prefer this over `integration/` when possible.
- `assets/` — embedded UI assets.
### `cmd/hi/` files
`main.go`, `run.go`, `doctor.go`, `docker.go`, `cleanup.go`, `stats.go`,
`README.md`. **Read `cmd/hi/README.md` before running any `hi` command.**
## Architecture Essentials
- **`hscontrol/state/state.go`** is the central coordinator. Cross-cutting
operations (node updates, policy evaluation, IP allocation) go through
the `State` type, not directly to the database.
- **`NodeStore`** in `hscontrol/state/node_store.go` is a copy-on-write
in-memory cache backed by `atomic.Pointer[Snapshot]`. Every read is a
pointer load; writes rebuild a new snapshot and atomically swap. It is
the hot path for `MapRequest` processing and peer visibility.
- **The map-request sync point** is
`State.UpdateNodeFromMapRequest()` in
`hscontrol/state/state.go:2351`. This is where Hostinfo changes,
endpoint updates, and route advertisements land in the NodeStore.
- **Mapper subsystem** streams MapResponses via `batcher.go` and
`node_conn.go`. Changes here affect all connected clients.
- **Node registration flow**: noise handshake (`noise.go`) → auth
(`auth.go`) → state/DB persistence (`state/`, `db/`) → initial map
(`mapper/`).
## Database Migration Rules
These rules are load-bearing — violating them corrupts production
databases. The `migrationsRequiringFKDisabled` map in
`hscontrol/db/db.go:962` is frozen as of 2025-07-02 (see the comment at
`db.go:989`). All new migrations must:
1. **Never reorder existing migrations.** Migration order is immutable
once committed.
2. **Only add new migrations to the end** of the migrations array.
3. **Never disable foreign keys.** No new entries in
`migrationsRequiringFKDisabled`.
4. **Use the migration ID format** `YYYYMMDDHHMM-short-description`
(timestamp + descriptive suffix). Example: `202602201200-clear-tagged-node-user-id`.
5. **Never rename columns** that later migrations reference. Let
`AutoMigrate` create a new column if needed.
## Tags-as-Identity
Headscale enforces **tags XOR user ownership**: every node is either
tagged (owned by tags) or user-owned (owned by a user namespace), never
both. This is a load-bearing architectural rule.
- **Use `node.IsTagged()`** (`hscontrol/types/node.go:221`) to determine
ownership, not `node.UserID().Valid()`. A tagged node may still have
`UserID` set for "created by" tracking — `IsTagged()` is authoritative.
- `IsUserOwned()` (`node.go:227`) returns `!IsTagged()`.
- Tagged nodes are presented to Tailscale as the special
`TaggedDevices` user (`hscontrol/types/users.go`, ID `2147455555`).
- `SetTags` validation is enforced by `validateNodeOwnership()` in
`hscontrol/state/tags.go`.
- Examples and edge cases live in `hscontrol/types/node_tags_test.go`
and `hscontrol/grpcv1_test.go` (`TestSetTags_*`).
**Don't do this**:
```go
if node.UserID().Valid() { /* assume user-owned */ } // WRONG
if node.UserID().Valid() && !node.IsTagged() { /* ok */ } // correct
```
## Policy Engine
`hscontrol/policy/v2/policy.go` is the policy implementation. The
top-level `hscontrol/policy/policy.go` contains only wrapper functions
around v2. There is no v1 directory.
Key concepts an agent will encounter:
- **Autogroups**: `autogroup:self`, `autogroup:member`, `autogroup:internet`
- **Tag owners**: IP-based authorization for who can claim a tag
- **Route approvals**: auto-approval of subnet routes by policy
- **SSH policies**: SSH access control via grants
- **HuJSON** parsing for policy files
For usage examples, read `hscontrol/policy/v2/policy_test.go`. For ACL
reference documentation, see `docs/`.
## Integration Testing
**Before running any `hi` command, read `cmd/hi/README.md` in full.**
Guessing at `hi` flags leads to broken runs and stale containers.
Test-authoring patterns (`EventuallyWithT`, `IntegrationSkip`, helper
variants, scenario setup) are documented in `integration/README.md`.
Key reminders:
- Integration test functions **must** start with `IntegrationSkip(t)`.
- External calls (`client.Status`, `headscale.ListNodes`, etc.) belong
inside `EventuallyWithT`; state-mutating commands (`tailscale set`)
must not.
- Tests generate ~100 MB of logs per run under `control_logs/{runID}/`.
Prune old runs if disk is tight.
- Flakes are almost always code, not infrastructure. Read `hs-*.stderr.log`
before blaming Docker.
## Code Conventions
- **Commit messages** follow Go-style `package: imperative description`.
Recent examples from `git log`:
- `db: scope DestroyUser to only delete the target user's pre-auth keys`
- `state: fix policy change race in UpdateNodeFromMapRequest`
- `integration: fix ACL tests for address-family-specific resolve`
Not Conventional Commits. No `feat:`/`chore:`/`docs:` prefixes.
- **Protobuf regeneration**: changes under `proto/` require
`make generate` (which runs `buf generate`) and should land in a
**separate commit** from the callers that use the regenerated types.
- **Formatting** is enforced by `golangci-lint` with `golines` (width 88)
and `gofumpt`. Run `make fmt` or rely on the pre-commit hook.
- **Logging** uses `zerolog`. Prefer single-line chains
(`log.Info().Str(...).Msg(...)`). For 4+ fields or conditional fields,
build incrementally and **reassign** the event variable:
`e = e.Str("k", v)`. Forgetting to reassign silently drops the field.
- **Tests**: prefer `hscontrol/servertest/` for server-level tests that
don't need Docker — faster than full integration tests.
- **View types in read paths**: response serializers must read through
`NodeView`/`UserView`/`PreAuthKeyView` accessors. `AsStruct()` clones the
whole record on every read — it is only for DB-write/merge clones and mutable
working copies, never to build an API response. `grep AsStruct hscontrol/api`
must come back empty.
## Gotchas
- **Database**: SQLite for local dev, PostgreSQL for integration-heavy
tests (`go run ./cmd/hi run "..." --postgres`). Some race conditions
only surface on one backend.
- **NodeStore writes** rebuild a full snapshot. Measure before changing
hot-path code.
- **`.claude/agents/` is deprecated.** Do not create new agent files
there. Put behavioural guidance in this file and procedural guidance
in the nearest README.
- **Do not edit `gen/`** — it is regenerated from `proto/` by
`make generate`.
- **Proto changes + code changes should be two commits**, not one.
+1380 -43
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+1 -1
View File
@@ -62,7 +62,7 @@ event.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
at our Discord channel. All complaints
on our [Discord server](https://discord.gg/c84AZQhmpx). All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and
+34
View File
@@ -0,0 +1,34 @@
# Contributing
Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the maintainers before being added to the project.
This model has been chosen to reduce the risk of burnout by limiting the maintenance overhead of reviewing and validating third-party code.
## Why do we have this model?
Headscale has a small maintainer team that tries to balance working on the project, fixing bugs and reviewing contributions.
When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops.
Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly.
The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added.
This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature.
## What do we require?
A general description is provided here and an explicit list is provided in our pull request template.
All new features have to start out with a design document, which should be discussed on the issue tracker (not discord). It should include a use case for the feature, how it can be implemented, who will implement it and a plan for maintaining it.
All features have to be end-to-end tested (integration tests) and have good unit test coverage to ensure that they work as expected. This will also ensure that the feature continues to work as expected over time. If a change cannot be tested, a strong case for why this is not possible needs to be presented.
The contributor should help to maintain the feature over time. In case the feature is not maintained probably, the maintainers reserve themselves the right to remove features they redeem as unmaintainable. This should help to improve the quality of the software and keep it in a maintainable state.
## Bug fixes
Headscale is open to code contributions for bug fixes without discussion.
## Documentation
If you find mistakes in the documentation, please submit a fix to the documentation.
-23
View File
@@ -1,23 +0,0 @@
# Builder image
FROM docker.io/golang:1.19.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale
# Production image
FROM gcr.io/distroless/base-debian11
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]
-24
View File
@@ -1,24 +0,0 @@
# Builder image
FROM docker.io/golang:1.19.0-alpine AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN apk add gcc musl-dev
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN strip /go/bin/headscale
RUN test -e /go/bin/headscale
# Production image
FROM docker.io/alpine:latest
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
EXPOSE 8080/tcp
CMD ["headscale"]
-24
View File
@@ -1,24 +0,0 @@
# Builder image
FROM docker.io/golang:1.19.0-bullseye AS build
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
COPY go.mod go.sum /go/src/headscale/
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go install -tags ts2019 -ldflags="-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$VERSION" -a ./cmd/headscale
RUN test -e /go/bin/headscale
# Debug image
FROM docker.io/golang:1.19.0-bullseye
COPY --from=build /go/bin/headscale /bin/headscale
ENV TZ UTC
# Need to reset the entrypoint or everything will run as a busybox script
ENTRYPOINT []
EXPOSE 8080/tcp
CMD ["headscale"]
+19
View File
@@ -0,0 +1,19 @@
# For testing purposes only
FROM golang:1.26.4-alpine AS build-env
WORKDIR /go/src
RUN apk add --no-cache git
ARG VERSION_BRANCH=main
RUN git clone https://github.com/tailscale/tailscale.git --branch=$VERSION_BRANCH --depth=1
WORKDIR /go/src/tailscale
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
FROM alpine:3.23
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
COPY --from=build-env /go/bin/* /usr/local/bin/
ENTRYPOINT [ "/usr/local/bin/derper" ]
+44
View File
@@ -0,0 +1,44 @@
# This Dockerfile and the images produced are for testing headscale,
# and are in no way endorsed by Headscale's maintainers as an
# official nor supported release or distribution.
FROM docker.io/golang:1.26.4-trixie AS builder
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
# Install delve debugger first - rarely changes, good cache candidate
RUN go install github.com/go-delve/delve/cmd/dlv@latest
# Download dependencies - only invalidated when go.mod/go.sum change
COPY go.mod go.sum /go/src/headscale/
RUN go mod download
# Copy source and build - invalidated on any source change
COPY . .
# Build debug binary with debug symbols for delve
RUN CGO_ENABLED=0 GOOS=linux go build -gcflags="all=-N -l" -o /go/bin/headscale ./cmd/headscale
# Runtime stage
FROM debian:trixie-slim
RUN apt-get --update install --no-install-recommends --yes \
bash ca-certificates curl dnsutils findutils iproute2 jq less procps python3 sqlite3 \
&& apt-get dist-clean
RUN mkdir -p /var/run/headscale
# Copy binaries from builder
COPY --from=builder /go/bin/headscale /usr/local/bin/headscale
COPY --from=builder /go/bin/dlv /usr/local/bin/dlv
# Copy source code for delve source-level debugging
COPY --from=builder /go/src/headscale /go/src/headscale
WORKDIR /go/src/headscale
# Need to reset the entrypoint or everything will run as a busybox script
ENTRYPOINT []
EXPOSE 8080/tcp 40000/tcp
CMD ["dlv", "--listen=0.0.0.0:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/usr/local/bin/headscale", "--"]
+17
View File
@@ -0,0 +1,17 @@
# Minimal CI image - expects pre-built headscale binary in build context
# For local development with delve debugging, use Dockerfile.integration instead
FROM debian:trixie-slim
RUN apt-get --update install --no-install-recommends --yes \
bash ca-certificates curl dnsutils findutils iproute2 jq less procps python3 sqlite3 \
&& apt-get dist-clean
RUN mkdir -p /var/run/headscale
# Copy pre-built headscale binary from build context
COPY headscale /usr/local/bin/headscale
ENTRYPOINT []
EXPOSE 8080/tcp
CMD ["/usr/local/bin/headscale"]
-17
View File
@@ -1,17 +0,0 @@
FROM ubuntu:latest
ARG TAILSCALE_VERSION=*
ARG TAILSCALE_CHANNEL=stable
RUN apt-get update \
&& apt-get install -y gnupg curl \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
&& rm -rf /var/lib/apt/lists/*
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates
+37 -13
View File
@@ -1,23 +1,47 @@
FROM golang:latest
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
RUN apt-get update \
&& apt-get install -y ca-certificates dnsutils git iptables \
&& rm -rf /var/lib/apt/lists/*
# This Dockerfile is more or less lifted from tailscale/tailscale
# to ensure a similar build process when testing the HEAD of tailscale.
FROM golang:1.26.4-alpine AS build-env
WORKDIR /go/src
RUN apk add --no-cache git
# Replace `RUN git...` with `COPY` and a local checked out version of Tailscale in `./tailscale`
# to test specific commits of the Tailscale client. This is useful when trying to find out why
# something specific broke between two versions of Tailscale with for example `git bisect`.
# COPY ./tailscale .
RUN git clone https://github.com/tailscale/tailscale.git
WORKDIR /go/tailscale
WORKDIR /go/src/tailscale
RUN git checkout main
RUN sh build_dist.sh tailscale.com/cmd/tailscale
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
# see build_docker.sh
ARG VERSION_LONG=""
ENV VERSION_LONG=$VERSION_LONG
ARG VERSION_SHORT=""
ENV VERSION_SHORT=$VERSION_SHORT
ARG VERSION_GIT_HASH=""
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
ARG TARGETARCH
RUN cp tailscale /usr/local/bin/
RUN cp tailscaled /usr/local/bin/
ARG BUILD_TAGS=""
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
RUN GOARCH=$TARGETARCH go install -tags="${BUILD_TAGS}" -ldflags="\
-X tailscale.com/version.longStamp=$VERSION_LONG \
-X tailscale.com/version.shortStamp=$VERSION_SHORT \
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
RUN update-ca-certificates
FROM alpine:3.23
# Upstream: ca-certificates ip6tables iptables iproute2
# Tests: curl python3 (traceroute via BusyBox)
RUN apk add --no-cache ca-certificates curl ip6tables iptables iproute2 python3
COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be
# using build_docker.sh which sets an entrypoint for the image.
RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh
+26
View File
@@ -0,0 +1,26 @@
FROM rust:1.95-trixie AS builder
ARG TAILSCALE_RS_REPO=https://github.com/tailscale/tailscale-rs.git
ARG TAILSCALE_RS_REF=main
WORKDIR /app
RUN git clone --depth 1 --branch "$TAILSCALE_RS_REF" "$TAILSCALE_RS_REPO" .
# Re-export ts_control's insecure-keyfetch feature through the tailscale
# crate so the axum example can fetch the headscale control key over
# plain HTTP. The integration harness serves the control plane without
# TLS, and upstream only allows plain-HTTP key fetches when this Cargo
# feature is compiled in.
RUN sed -i '/^axum = \["dep:axum"\]/a insecure-keyfetch = ["ts_control/insecure-keyfetch"]' Cargo.toml
RUN cargo build --release --features axum,insecure-keyfetch --example axum
FROM debian:trixie-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
iproute2 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/examples/axum /usr/local/bin/axum
+30
View File
@@ -0,0 +1,30 @@
# For integration testing only.
#
# Builds the Tailscale control client (integration/wasmic/wasmclient) for
# GOOS=js/GOARCH=wasm and packages it with Go's wasm_exec Node runner. The
# container idles; the integration test execs
# node /app/wasm_exec_node.js /app/client.wasm <control-url>
# to drive a real browser-style WebSocket GET against headscale's /ts2021,
# guarding the regression in issue #3357.
FROM golang:1.26.4-alpine AS build
WORKDIR /src
# Only the module metadata and the wasm client package are needed to build the
# js/wasm binary; its imports (tailscale.com/control/controlhttp, ...) resolve
# from the module proxy.
COPY go.mod go.sum ./
COPY integration/wasmic/wasmclient ./integration/wasmic/wasmclient
RUN GOOS=js GOARCH=wasm go build -o /out/client.wasm ./integration/wasmic/wasmclient \
&& cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" /out/wasm_exec.js \
&& cp "$(go env GOROOT)/lib/wasm/wasm_exec_node.js" /out/wasm_exec_node.js
FROM node:24-alpine
WORKDIR /app
COPY --from=build /out/ /app/
# Idle; the test execs the client on demand with the headscale control URL.
ENTRYPOINT ["tail", "-f", "/dev/null"]
+129 -80
View File
@@ -1,100 +1,149 @@
# Calculate version
version ?= $(shell git describe --always --tags --dirty)
# Headscale Makefile
# Modern Makefile following best practices
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
# Version calculation
VERSION ?= $(shell git describe --always --tags --dirty)
# Determine if OS supports pie
# Build configuration
GOOS ?= $(shell uname | tr '[:upper:]' '[:lower:]')
ifeq ($(filter $(GOOS), openbsd netbsd soloaris plan9), )
pieflags = -buildmode=pie
else
ifeq ($(filter $(GOOS), openbsd netbsd solaris plan9), )
PIE_FLAGS = -buildmode=pie
endif
TAGS = -tags ts2019
# Tool availability check with nix warning
define check_tool
@command -v $(1) >/dev/null 2>&1 || { \
echo "Warning: $(1) not found. Run 'nix develop' to ensure all dependencies are available."; \
exit 1; \
}
endef
# GO_SOURCES = $(wildcard *.go)
# PROTO_SOURCES = $(wildcard **/*.proto)
GO_SOURCES = $(call rwildcard,,*.go)
PROTO_SOURCES = $(call rwildcard,,*.proto)
# Source file collections using shell find for better performance
GO_SOURCES := $(shell find . -name '*.go' -not -path './gen/*' -not -path './vendor/*')
PRETTIER_SOURCES := $(shell find . \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.ts' -o -name '*.js' -o -name '*.html' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -not -path './gen/*' -not -path './vendor/*' -not -path './node_modules/*')
# Default target
.PHONY: all
all: lint test build
# Dependency checking
.PHONY: check-deps
check-deps:
$(call check_tool,go)
$(call check_tool,golangci-lint)
$(call check_tool,gofumpt)
$(call check_tool,mdformat)
$(call check_tool,prettier)
# Build targets
.PHONY: build
build: check-deps $(GO_SOURCES) go.mod go.sum
@echo "Building headscale..."
go build $(PIE_FLAGS) -ldflags "-X main.version=$(VERSION)" -o headscale ./cmd/headscale
# Test targets
.PHONY: test
test: check-deps $(GO_SOURCES) go.mod go.sum
@echo "Running Go tests..."
go test -race ./...
build:
nix build
# Formatting targets
.PHONY: fmt
fmt: fmt-go fmt-mdformat fmt-prettier
dev: lint test build
.PHONY: fmt-go
fmt-go: check-deps $(GO_SOURCES)
@echo "Formatting Go code..."
gofumpt -l -w .
golangci-lint run --fix
test:
@go test $(TAGS) -short -coverprofile=coverage.out ./...
.PHONY: fmt-mdformat
fmt-mdformat: check-deps
@echo "Formatting documentation..."
mdformat docs/
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_v2_general
.PHONY: fmt-prettier
fmt-prettier: check-deps $(PRETTIER_SOURCES)
@echo "Formatting markup and config files..."
prettier --write '**/*.{ts,js,md,yaml,yml,sass,css,scss,html}'
test_integration_cli:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
docker network create headscale-test || true
docker run -t --rm \
--network headscale-test \
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./...
# Linting targets
.PHONY: lint
lint: lint-go
test_integration_derp:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
docker network create headscale-test || true
docker run -t --rm \
--network headscale-test \
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationDERP ./...
.PHONY: lint-go
lint-go: check-deps $(GO_SOURCES) go.mod go.sum
@echo "Linting Go code..."
golangci-lint run --timeout 10m
test_integration_oidc:
docker network rm $$(docker network ls --filter name=headscale --quiet) || true
docker network create headscale-test || true
docker run -t --rm \
--network headscale-test \
-v ~/.cache/hs-integration-go:/go \
-v $$PWD:$$PWD -w $$PWD \
-v /var/run/docker.sock:/var/run/docker.sock golang:1 \
go test $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationOIDC ./...
# Code generation
.PHONY: generate
generate: check-deps
@echo "Generating code..."
go generate ./...
$(MAKE) client
test_integration_v2_general:
docker run \
-t --rm \
-v ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test $(TAGS) -failfast ./... -timeout 60m -parallel 6
# Emit the OpenAPI spec on demand. The server serves it live at /openapi.yaml;
# this is for external consumers or inspection and is not committed.
.PHONY: openapi
openapi:
@echo "Emitting OpenAPI spec from code..."
go run ./cmd/gen-openapi
coverprofile_func:
go tool cover -func=coverage.out
# Generate the strongly-typed Go HTTP clients (v1 and v2). The served specs are
# OpenAPI 3.1, but oapi-codegen v2 does not yet read 3.1, so each client is
# generated from a transient 3.0.3 downgrade of its document. Pinned so the
# committed clients are reproducible.
.PHONY: client
client:
@echo "Generating API clients..."
@tmp=$$(mktemp -t headscale-openapi-3.0.XXXXXX.yaml); \
go run ./cmd/gen-openapi -downgrade "$$tmp" && \
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.1 \
-generate types,client -package clientv1 -o gen/client/v1/client.gen.go "$$tmp" && \
go run ./cmd/gen-openapi -api v2 -downgrade "$$tmp" && \
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.1 \
-generate types,client -package clientv2 -o gen/client/v2/client.gen.go "$$tmp"; \
status=$$?; rm -f "$$tmp"; exit $$status
coverprofile_html:
go tool cover -html=coverage.out
# Clean targets
.PHONY: clean
clean:
rm -rf headscale gen/client
lint:
golangci-lint run --fix --timeout 10m
# Development workflow
.PHONY: dev
dev: fmt lint test build
fmt:
prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
golines --max-len=88 --base-formatter=gofumpt -w $(GO_SOURCES)
clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
# Start a local headscale dev server (use mts to add nodes)
.PHONY: dev-server
dev-server:
go run ./cmd/dev
proto-lint:
cd proto/ && go run github.com/bufbuild/buf/cmd/buf lint
compress: build
upx --brute headscale
generate:
rm -rf gen
go run github.com/bufbuild/buf/cmd/buf generate proto
install-protobuf-plugins:
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
# Help target
.PHONY: help
help:
@echo "Headscale Development Makefile"
@echo ""
@echo "Main targets:"
@echo " all - Run lint, test, and build (default)"
@echo " build - Build headscale binary"
@echo " test - Run Go tests"
@echo " fmt - Format all code (Go, docs, markup)"
@echo " lint - Lint all code (Go)"
@echo " generate - Generate code (go generate + client)"
@echo " dev - Full development workflow (fmt + lint + test + build)"
@echo " clean - Clean build artifacts"
@echo ""
@echo "Specific targets:"
@echo " fmt-go - Format Go code only"
@echo " fmt-mdformat - Format documentation only"
@echo " fmt-prettier - Format markup and config files only"
@echo " lint-go - Lint Go code only"
@echo ""
@echo "Dependencies:"
@echo " check-deps - Verify required tools are available"
@echo ""
@echo "Note: If not running in a nix shell, ensure dependencies are available:"
@echo " nix develop"
+77 -664
View File
@@ -1,14 +1,18 @@
![headscale logo](./docs/logo/headscale3_header_stacked_left.png)
![headscale logo](./docs/assets/logo/headscale3_header_stacked_left.png)
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
An open source, self-hosted implementation of the Tailscale control server.
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat.
Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat.
**Note:** Always select the same GitHub tag as the released version you use
to ensure you have the correct example configuration and documentation.
The `main` branch might contain unreleased changes.
to ensure you have the correct example configuration. The `main` branch might
contain unreleased changes. The documentation is available for stable and
development versions:
- [Documentation for the stable version](https://headscale.net/stable/)
- [Documentation for the development version](https://headscale.net/development/)
## What is Tailscale
@@ -26,77 +30,75 @@ nodes in the Tailscale network. It assigns the IP addresses of the clients,
creates the boundaries between each user, enables sharing machines between users,
and exposes the advertised routes of your nodes.
A [Tailscale network (tailnet)](https://tailscale.com/kb/1136/tailnet/) is private
network which Tailscale assigns to a user in terms of private users or an
A [Tailscale network (tailnet)](https://tailscale.com/docs/concepts/tailnet) is
private network which Tailscale assigns to a user in terms of private users or an
organisation.
## Design goal
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale
control server. `headscale` has a narrower scope and an instance of `headscale`
implements a _single_ Tailnet, which is typically what a single organisation, or
home/personal setup would use.
Headscale aims to implement a self-hosted, open source alternative to the
[Tailscale](https://tailscale.com/) control server. Headscale's goal is to
provide self-hosters and hobbyists with an open-source server they can use for
their projects and labs. It implements a narrow scope, a _single_ Tailscale
network (tailnet), suitable for a personal use, or a small open-source
organisation.
`headscale` uses terms that maps to Tailscale's control server, consult the
[glossary](./docs/glossary.md) for explainations.
## Support
## Supporting Headscale
If you like `headscale` and find it useful, there is a sponsorship and donation
buttons available in the repo.
If you would like to sponsor features, bugs or prioritisation, reach out to
one of the maintainers.
## Features
- Full "base" support of Tailscale's features
- Configurable DNS
- [Split DNS](https://tailscale.com/kb/1054/dns/#using-dns-settings-in-the-admin-console)
- Node registration
- Single-Sign-On (via Open ID Connect)
- Pre authenticated key
- Taildrop (File Sharing)
- [Access control lists](https://tailscale.com/kb/1018/acls/)
- [MagicDNS](https://tailscale.com/kb/1081/magicdns)
- Support for multiple IP ranges in the tailnet
- Dual stack (IPv4 and IPv6)
- Routing advertising (including exit nodes)
- Ephemeral nodes
- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
Please see ["Features" in the documentation](https://headscale.net/stable/about/features/).
## Client OS support
| OS | Supports headscale |
| ------- | --------------------------------------------------------- |
| Linux | Yes |
| OpenBSD | Yes |
| FreeBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes [docs](./docs/windows-client.md) |
| Android | Yes [docs](./docs/android-client.md) |
| iOS | Not yet |
Please see ["Client and operating system support" in the documentation](https://headscale.net/stable/about/clients/).
## Running headscale
Please have a look at the documentation under [`docs/`](docs/).
**Please note that we do not support nor encourage the use of reverse proxies
and container to run Headscale.**
Please have a look at the [`documentation`](https://headscale.net/stable/).
For NixOS users, a module is available in [`nix/`](./nix/).
## Builds from `main`
Development builds from the `main` branch are available as container images and
binaries. See the [development builds](https://headscale.net/stable/setup/install/main/)
documentation for details.
## Talks
- Fosdem 2026 (video): [Headscale & Tailscale: The complementary open source clone](https://fosdem.org/2026/schedule/event/KYQ3LL-headscale-the-complementary-open-source-clone/)
- presented by Kristoffer Dalby
- Fosdem 2023 (video): [Headscale: How we are using integration testing to reimplement Tailscale](https://fosdem.org/2023/schedule/event/goheadscale/)
- presented by Juan Font Alonso and Kristoffer Dalby
## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc.
2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
This project is not associated with Tailscale Inc.
However, one of the active maintainers for Headscale [is employed by Tailscale](https://tailscale.com/blog/opensource) and he is allowed to spend work hours contributing to the project. Contributions from this maintainer are reviewed by other maintainers.
The maintainers work together on setting the direction for the project. The underlying principle is to serve the community of self-hosters, enthusiasts and hobbyists - while having a sustainable project.
## Contributing
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
and [Buf](https://buf.build)(Protobuf generator).
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
### Requirements
To contribute to headscale you would need the latest version of [Go](https://golang.org)
and [Buf](https://buf.build) (Protobuf generator).
We recommend using [Nix](https://nixos.org/) to setup a development environment. This can
be done with `nix develop`, which will install the tools and give you a shell.
This guarantees that you will have the same dev env as `headscale` maintainers.
PRs and suggestions are welcome.
### Code style
To ensure we have some consistency with a growing number of contributions,
@@ -111,6 +113,8 @@ run `make lint` and `make fmt` before committing any code.
The **Proto** code is linted with [`buf`](https://docs.buf.build/lint/overview) and
formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
The **docs** are formatted with [`mdformat`](https://mdformat.readthedocs.io).
The **rest** (Markdown, YAML, etc) is formatted with [`prettier`](https://prettier.io).
Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
@@ -146,626 +150,35 @@ make test
To build the program:
```shell
nix build
```
or
```shell
make build
```
### Development workflow
We recommend using Nix for dependency management to ensure you have all required tools. If you prefer to manage dependencies yourself, you can use Make directly:
**With Nix (recommended):**
```shell
nix develop
make test
make build
```
**With your own dependencies:**
```shell
make test
make build
```
The Makefile will warn you if any required tools are missing and suggest running `nix develop`. Run `make help` to see all available targets.
## Contributors
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kradalby>
<img src=https://avatars.githubusercontent.com/u/98431?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kristoffer Dalby/>
<br />
<sub style="font-size:14px"><b>Kristoffer Dalby</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/juanfont>
<img src=https://avatars.githubusercontent.com/u/181059?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Font/>
<br />
<sub style="font-size:14px"><b>Juan Font</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/restanrm>
<img src=https://avatars.githubusercontent.com/u/4344371?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adrien Raffin-Caboisse/>
<br />
<sub style="font-size:14px"><b>Adrien Raffin-Caboisse</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cure>
<img src=https://avatars.githubusercontent.com/u/149135?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ward Vandewege/>
<br />
<sub style="font-size:14px"><b>Ward Vandewege</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/huskyii>
<img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
<br />
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/tsujamin>
<img src=https://avatars.githubusercontent.com/u/2435619?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Benjamin Roberts/>
<br />
<sub style="font-size:14px"><b>Benjamin Roberts</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/reynico>
<img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
<br />
<sub style="font-size:14px"><b>Nico</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
<br />
<sub style="font-size:14px"><b>e-zk</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/arch4ngel>
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
<br />
<sub style="font-size:14px"><b>Justin Angel</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ItalyPaleAle>
<img src=https://avatars.githubusercontent.com/u/43508?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Alessandro (Ale) Segala/>
<br />
<sub style="font-size:14px"><b>Alessandro (Ale) Segala</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/unreality>
<img src=https://avatars.githubusercontent.com/u/352522?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=unreality/>
<br />
<sub style="font-size:14px"><b>unreality</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ohdearaugustin>
<img src=https://avatars.githubusercontent.com/u/14001491?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ohdearaugustin/>
<br />
<sub style="font-size:14px"><b>ohdearaugustin</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mpldr>
<img src=https://avatars.githubusercontent.com/u/33086936?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Moritz Poldrack/>
<br />
<sub style="font-size:14px"><b>Moritz Poldrack</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/GrigoriyMikhalkin>
<img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
<br />
<sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mike-lloyd03>
<img src=https://avatars.githubusercontent.com/u/49411532?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mike Lloyd/>
<br />
<sub style="font-size:14px"><b>Mike Lloyd</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Niek>
<img src=https://avatars.githubusercontent.com/u/213140?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Niek van der Maas/>
<br />
<sub style="font-size:14px"><b>Niek van der Maas</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/negbie>
<img src=https://avatars.githubusercontent.com/u/20154956?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Eugen Biegler/>
<br />
<sub style="font-size:14px"><b>Eugen Biegler</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/617a7a>
<img src=https://avatars.githubusercontent.com/u/67651251?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Azz/>
<br />
<sub style="font-size:14px"><b>Azz</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/iSchluff>
<img src=https://avatars.githubusercontent.com/u/1429641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anton Schubert/>
<br />
<sub style="font-size:14px"><b>Anton Schubert</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/qbit>
<img src=https://avatars.githubusercontent.com/u/68368?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aaron Bieber/>
<br />
<sub style="font-size:14px"><b>Aaron Bieber</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kazauwa>
<img src=https://avatars.githubusercontent.com/u/12330159?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Igor Perepilitsyn/>
<br />
<sub style="font-size:14px"><b>Igor Perepilitsyn</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Aluxima>
<img src=https://avatars.githubusercontent.com/u/16262531?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Laurent Marchaud/>
<br />
<sub style="font-size:14px"><b>Laurent Marchaud</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fdelucchijr>
<img src=https://avatars.githubusercontent.com/u/69133647?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Fernando De Lucchi/>
<br />
<sub style="font-size:14px"><b>Fernando De Lucchi</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/hdhoang>
<img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=hdhoang/>
<br />
<sub style="font-size:14px"><b>hdhoang</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/bravechamp>
<img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
<br />
<sub style="font-size:14px"><b>bravechamp</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/deonthomasgy>
<img src=https://avatars.githubusercontent.com/u/150036?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Deon Thomas/>
<br />
<sub style="font-size:14px"><b>Deon Thomas</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/madjam002>
<img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
<br />
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ChibangLW>
<img src=https://avatars.githubusercontent.com/u/22293464?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ChibangLW/>
<br />
<sub style="font-size:14px"><b>ChibangLW</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/mevansam>
<img src=https://avatars.githubusercontent.com/u/403630?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mevan Samaratunga/>
<br />
<sub style="font-size:14px"><b>Mevan Samaratunga</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/dragetd>
<img src=https://avatars.githubusercontent.com/u/3639577?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Michael G./>
<br />
<sub style="font-size:14px"><b>Michael G.</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ptman>
<img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
<br />
<sub style="font-size:14px"><b>Paul Tötterman</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/samson4649>
<img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
<br />
<sub style="font-size:14px"><b>Samuel Lock</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/majst01>
<img src=https://avatars.githubusercontent.com/u/410110?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan Majer/>
<br />
<sub style="font-size:14px"><b>Stefan Majer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kevin1sMe>
<img src=https://avatars.githubusercontent.com/u/6886076?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kevinlin/>
<br />
<sub style="font-size:14px"><b>kevinlin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/artemklevtsov>
<img src=https://avatars.githubusercontent.com/u/603798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Artem Klevtsov/>
<br />
<sub style="font-size:14px"><b>Artem Klevtsov</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/cmars>
<img src=https://avatars.githubusercontent.com/u/23741?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Casey Marshall/>
<br />
<sub style="font-size:14px"><b>Casey Marshall</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/CNLHC>
<img src=https://avatars.githubusercontent.com/u/21005146?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=LiuHanCheng/>
<br />
<sub style="font-size:14px"><b>LiuHanCheng</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pvinis>
<img src=https://avatars.githubusercontent.com/u/100233?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pavlos Vinieratos/>
<br />
<sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/SilverBut>
<img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
<br />
<sub style="font-size:14px"><b>Silver Bullet</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ratsclub>
<img src=https://avatars.githubusercontent.com/u/25647735?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Victor Freire/>
<br />
<sub style="font-size:14px"><b>Victor Freire</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lachy2849>
<img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
<br />
<sub style="font-size:14px"><b>lachy2849</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/t56k>
<img src=https://avatars.githubusercontent.com/u/12165422?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=thomas/>
<br />
<sub style="font-size:14px"><b>thomas</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aberoham>
<img src=https://avatars.githubusercontent.com/u/586805?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Abraham Ingersoll/>
<br />
<sub style="font-size:14px"><b>Abraham Ingersoll</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/puzpuzpuz>
<img src=https://avatars.githubusercontent.com/u/37772591?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrei Pechkurov/>
<br />
<sub style="font-size:14px"><b>Andrei Pechkurov</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/apognu>
<img src=https://avatars.githubusercontent.com/u/3017182?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Antoine POPINEAU/>
<br />
<sub style="font-size:14px"><b>Antoine POPINEAU</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/aofei>
<img src=https://avatars.githubusercontent.com/u/5037285?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Aofei Sheng/>
<br />
<sub style="font-size:14px"><b>Aofei Sheng</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/awoimbee>
<img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
<br />
<sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stensonb>
<img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
<br />
<sub style="font-size:14px"><b>Bryan Stenson</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/yangchuansheng>
<img src=https://avatars.githubusercontent.com/u/15308462?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt= Carson Yang/>
<br />
<sub style="font-size:14px"><b> Carson Yang</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kundel>
<img src=https://avatars.githubusercontent.com/u/10158899?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kundel/>
<br />
<sub style="font-size:14px"><b>kundel</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/fkr>
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
<br />
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/felixonmars>
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
<br />
<sub style="font-size:14px"><b>Felix Yan</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/JJGadgets>
<img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
<br />
<sub style="font-size:14px"><b>JJGadgets</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
<br />
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ShadowJonathan>
<img src=https://avatars.githubusercontent.com/u/22740616?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jonathan de Jong/>
<br />
<sub style="font-size:14px"><b>Jonathan de Jong</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/piec>
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
<br />
<sub style="font-size:14px"><b>Pierre Carru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Donran>
<img src=https://avatars.githubusercontent.com/u/4838348?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pontus N/>
<br />
<sub style="font-size:14px"><b>Pontus N</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nnsee>
<img src=https://avatars.githubusercontent.com/u/36747857?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Rasmus Moorats/>
<br />
<sub style="font-size:14px"><b>Rasmus Moorats</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/rcursaru>
<img src=https://avatars.githubusercontent.com/u/16259641?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=rcursaru/>
<br />
<sub style="font-size:14px"><b>rcursaru</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/renovate-bot>
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Mend Renovate/>
<br />
<sub style="font-size:14px"><b>Mend Renovate</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ryanfowler>
<img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
<br />
<sub style="font-size:14px"><b>Ryan Fowler</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/shaananc>
<img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
<br />
<sub style="font-size:14px"><b>Shaanan Cohney</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/stefanvanburen>
<img src=https://avatars.githubusercontent.com/u/622527?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Stefan VanBuren/>
<br />
<sub style="font-size:14px"><b>Stefan VanBuren</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/sophware>
<img src=https://avatars.githubusercontent.com/u/41669?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sophware/>
<br />
<sub style="font-size:14px"><b>sophware</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/m-tanner-dev0>
<img src=https://avatars.githubusercontent.com/u/97977342?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tanner/>
<br />
<sub style="font-size:14px"><b>Tanner</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Teteros>
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
<br />
<sub style="font-size:14px"><b>Teteros</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gitter-badger>
<img src=https://avatars.githubusercontent.com/u/8518239?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=The Gitter Badger/>
<br />
<sub style="font-size:14px"><b>The Gitter Badger</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/tianon>
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
<br />
<sub style="font-size:14px"><b>Tianon Gravi</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/thetillhoff>
<img src=https://avatars.githubusercontent.com/u/25052289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Till Hoffmann/>
<br />
<sub style="font-size:14px"><b>Till Hoffmann</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/woudsma>
<img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
<br />
<sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/y0ngb1n>
<img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
<br />
<sub style="font-size:14px"><b>Yang Bin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gozssky>
<img src=https://avatars.githubusercontent.com/u/17199941?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yujie Xia/>
<br />
<sub style="font-size:14px"><b>Yujie Xia</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zekker6>
<img src=https://avatars.githubusercontent.com/u/1367798?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zakhar Bessarab/>
<br />
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/zhzy0077>
<img src=https://avatars.githubusercontent.com/u/8717471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Zhiyuan Zheng/>
<br />
<sub style="font-size:14px"><b>Zhiyuan Zheng</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Bpazy>
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
<br />
<sub style="font-size:14px"><b>Ziyuan Han</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/derelm>
<img src=https://avatars.githubusercontent.com/u/465155?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=derelm/>
<br />
<sub style="font-size:14px"><b>derelm</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/nning>
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
<br />
<sub style="font-size:14px"><b>henning mueller</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/ignoramous>
<img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
<br />
<sub style="font-size:14px"><b>ignoramous</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/lion24>
<img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=sharkonet/>
<br />
<sub style="font-size:14px"><b>sharkonet</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/pernila>
<img src=https://avatars.githubusercontent.com/u/12460060?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=pernila/>
<br />
<sub style="font-size:14px"><b>pernila</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/phpmalik>
<img src=https://avatars.githubusercontent.com/u/26834645?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=phpmalik/>
<br />
<sub style="font-size:14px"><b>phpmalik</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Wakeful-Cloud>
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
<br />
<sub style="font-size:14px"><b>Wakeful-Cloud</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/xpzouying>
<img src=https://avatars.githubusercontent.com/u/3946563?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=zy/>
<br />
<sub style="font-size:14px"><b>zy</b></sub>
</a>
</td>
</tr>
</table>
<a href="https://github.com/juanfont/headscale/graphs/contributors">
<img src="https://contrib.rocks/image?repo=juanfont/headscale" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
-578
View File
@@ -1,578 +0,0 @@
package headscale
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/netip"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"tailscale.com/tailcfg"
)
const (
errEmptyPolicy = Error("empty policy")
errInvalidAction = Error("invalid action")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
)
const (
Base8 = 8
Base10 = 10
BitSize16 = 16
BitSize32 = 32
BitSize64 = 64
portRangeBegin = 0
portRangeEnd = 65535
expectedTokenItems = 2
)
// For some reason golang.org/x/net/internal/iana is an internal package.
const (
protocolICMP = 1 // Internet Control Message
protocolIGMP = 2 // Internet Group Management
protocolIPv4 = 4 // IPv4 encapsulation
protocolTCP = 6 // Transmission Control
protocolEGP = 8 // Exterior Gateway Protocol
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
protocolUDP = 17 // User Datagram
protocolGRE = 47 // Generic Routing Encapsulation
protocolESP = 50 // Encap Security Payload
protocolAH = 51 // Authentication Header
protocolIPv6ICMP = 58 // ICMP for IPv6
protocolSCTP = 132 // Stream Control Transmission Protocol
ProtocolFC = 133 // Fibre Channel
)
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
func (h *Headscale) LoadACLPolicy(path string) error {
log.Debug().
Str("func", "LoadACLPolicy").
Str("path", path).
Msg("Loading ACL policy from path")
policyFile, err := os.Open(path)
if err != nil {
return err
}
defer policyFile.Close()
var policy ACLPolicy
policyBytes, err := io.ReadAll(policyFile)
if err != nil {
return err
}
switch filepath.Ext(path) {
case ".yml", ".yaml":
log.Debug().
Str("path", path).
Bytes("file", policyBytes).
Msg("Loading ACLs from YAML")
err := yaml.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
log.Trace().
Interface("policy", policy).
Msg("Loaded policy from YAML")
default:
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
}
if policy.IsZero() {
return errEmptyPolicy
}
h.aclPolicy = &policy
return h.UpdateACLRules()
}
func (h *Headscale) UpdateACLRules() error {
rules, err := h.generateACLRules()
if err != nil {
return err
}
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
h.aclRules = rules
return nil
}
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{}
if h.aclPolicy == nil {
return nil, errEmptyPolicy
}
machines, err := h.ListMachines()
if err != nil {
return nil, err
}
for index, acl := range h.aclPolicy.ACLs {
if acl.Action != "accept" {
return nil, errInvalidAction
}
srcIPs := []string{}
for innerIndex, src := range acl.Sources {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
return nil, err
}
srcIPs = append(srcIPs, srcs...)
}
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
return nil, err
}
destPorts := []tailcfg.NetPortRange{}
for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDest(
machines,
*h.aclPolicy,
dest,
needsWildcard,
)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
return nil, err
}
destPorts = append(destPorts, dests...)
}
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPs,
DstPorts: destPorts,
IPProto: protocols,
})
}
return rules, nil
}
func (h *Headscale) generateACLPolicySrcIP(
machines []Machine,
aclPolicy ACLPolicy,
src string,
) ([]string, error) {
return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
}
func (h *Headscale) generateACLPolicyDest(
machines []Machine,
aclPolicy ACLPolicy,
dest string,
needsWildcard bool,
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
return nil, errInvalidPortFormat
}
var alias string
// We can have here stuff like:
// git-server:*
// 192.168.1.0/24:22
// tag:montreal-webserver:80,443
// tag:api-server:443
// example-host-1:*
if len(tokens) == expectedTokenItems {
alias = tokens[0]
} else {
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
expanded, err := expandAlias(
machines,
aclPolicy,
alias,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
return nil, err
}
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
if err != nil {
return nil, err
}
dests := []tailcfg.NetPortRange{}
for _, d := range expanded {
for _, p := range *ports {
pr := tailcfg.NetPortRange{
IP: d,
Ports: p,
}
dests = append(dests, pr)
}
}
return dests, nil
}
// parseProtocol reads the proto field of the ACL and generates a list of
// protocols that will be allowed, following the IANA IP protocol number
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
//
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
// as per Tailscale behaviour (see tailcfg.FilterRule).
//
// Also returns a boolean indicating if the protocol
// requires all the destinations to use wildcard as port number (only TCP,
// UDP and SCTP support specifying ports).
func parseProtocol(protocol string) ([]int, bool, error) {
switch protocol {
case "":
return []int{
protocolICMP,
protocolIPv6ICMP,
protocolTCP,
protocolUDP,
}, false, nil
case "igmp":
return []int{protocolIGMP}, true, nil
case "ipv4", "ip-in-ip":
return []int{protocolIPv4}, true, nil
case "tcp":
return []int{protocolTCP}, false, nil
case "egp":
return []int{protocolEGP}, true, nil
case "igp":
return []int{protocolIGP}, true, nil
case "udp":
return []int{protocolUDP}, false, nil
case "gre":
return []int{protocolGRE}, true, nil
case "esp":
return []int{protocolESP}, true, nil
case "ah":
return []int{protocolAH}, true, nil
case "sctp":
return []int{protocolSCTP}, false, nil
case "icmp":
return []int{protocolICMP, protocolIPv6ICMP}, true, nil
default:
protocolNumber, err := strconv.Atoi(protocol)
if err != nil {
return nil, false, err
}
needsWildcard := protocolNumber != protocolTCP &&
protocolNumber != protocolUDP &&
protocolNumber != protocolSCTP
return []int{protocolNumber}, needsWildcard, nil
}
}
// expandalias has an input of either
// - a namespace
// - a group
// - a tag
// and transform these in IPAddresses.
func expandAlias(
machines []Machine,
aclPolicy ACLPolicy,
alias string,
stripEmailDomain bool,
) ([]string, error) {
ips := []string{}
if alias == "*" {
return []string{"*"}, nil
}
log.Debug().
Str("alias", alias).
Msg("Expanding")
if strings.HasPrefix(alias, "group:") {
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
if err != nil {
return ips, err
}
for _, n := range namespaces {
nodes := filterMachinesByNamespace(machines, n)
for _, node := range nodes {
ips = append(ips, node.IPAddresses.ToStringSlice()...)
}
}
return ips, nil
}
if strings.HasPrefix(alias, "tag:") {
// check for forced tags
for _, machine := range machines {
if contains(machine.ForcedTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
// find tag owners
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
if err != nil {
if errors.Is(err, errInvalidTag) {
if len(ips) == 0 {
return ips, fmt.Errorf(
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
errInvalidTag,
alias,
)
}
return ips, nil
} else {
return ips, err
}
}
// filter out machines per tag owner
for _, namespace := range owners {
machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines {
hi := machine.GetHostInfo()
if contains(hi.RequestTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
}
return ips, nil
}
// if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias)
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
if len(ips) > 0 {
return ips, nil
}
// if alias is an host
if h, ok := aclPolicy.Hosts[alias]; ok {
return []string{h.String()}, nil
}
// if alias is an IP
ip, err := netip.ParseAddr(alias)
if err == nil {
return []string{ip.String()}, nil
}
// if alias is an CIDR
cidr, err := netip.ParsePrefix(alias)
if err == nil {
return []string{cidr.String()}, nil
}
log.Warn().Msgf("No IPs found with the alias %v", alias)
return ips, nil
}
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
// that are correctly tagged since they should not be listed as being in the namespace
// we assume in this function that we only have nodes from 1 namespace.
func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy,
nodes []Machine,
namespace string,
stripEmailDomain bool,
) []Machine {
out := []Machine{}
tags := []string{}
for tag := range aclPolicy.TagOwners {
owners, _ := expandTagOwners(aclPolicy, namespace, stripEmailDomain)
ns := append(owners, namespace)
if contains(ns, namespace) {
tags = append(tags, tag)
}
}
// for each machine if tag is in tags list, don't append it.
for _, machine := range nodes {
hi := machine.GetHostInfo()
found := false
for _, t := range hi.RequestTags {
if contains(tags, t) {
found = true
break
}
}
if len(machine.ForcedTags) > 0 {
found = true
}
if !found {
out = append(out, machine)
}
}
return out
}
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
if portsStr == "*" {
return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
}, nil
}
if needsWildcard {
return nil, errWildcardIsNeeded
}
ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") {
rang := strings.Split(portStr, "-")
switch len(rang) {
case 1:
port, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
First: uint16(port),
Last: uint16(port),
})
case expectedTokenItems:
start, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil {
return nil, err
}
last, err := strconv.ParseUint(rang[1], Base10, BitSize16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
First: uint16(start),
Last: uint16(last),
})
default:
return nil, errInvalidPortFormat
}
}
return &ports, nil
}
func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
out := []Machine{}
for _, machine := range machines {
if machine.Namespace.Name == namespace {
out = append(out, machine)
}
}
return out
}
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups.
func expandTagOwners(
aclPolicy ACLPolicy,
tag string,
stripEmailDomain bool,
) ([]string, error) {
var owners []string
ows, ok := aclPolicy.TagOwners[tag]
if !ok {
return []string{}, fmt.Errorf(
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
errInvalidTag,
tag,
)
}
for _, owner := range ows {
if strings.HasPrefix(owner, "group:") {
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
if err != nil {
return []string{}, err
}
owners = append(owners, gs...)
} else {
owners = append(owners, owner)
}
}
return owners, nil
}
// expandGroup will return the list of namespace inside the group
// after some validation.
func expandGroup(
aclPolicy ACLPolicy,
group string,
stripEmailDomain bool,
) ([]string, error) {
outGroups := []string{}
aclGroups, ok := aclPolicy.Groups[group]
if !ok {
return []string{}, fmt.Errorf(
"group %v isn't registered. %w",
group,
errInvalidGroup,
)
}
for _, group := range aclGroups {
if strings.HasPrefix(group, "group:") {
return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup,
)
}
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
if err != nil {
return []string{}, fmt.Errorf(
"failed to normalize group %q, err: %w",
group,
errInvalidGroup,
)
}
outGroups = append(outGroups, grp)
}
return outGroups, nil
}
-1461
View File
File diff suppressed because it is too large Load Diff
-135
View File
@@ -1,135 +0,0 @@
package headscale
import (
"encoding/json"
"net/netip"
"strings"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
)
// ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct {
Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"tests" yaml:"tests"`
AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"`
}
// ACL is a basic rule for the ACL Policy.
type ACL struct {
Action string `json:"action" yaml:"action"`
Protocol string `json:"proto" yaml:"proto"`
Sources []string `json:"src" yaml:"src"`
Destinations []string `json:"dst" yaml:"dst"`
}
// Groups references a series of alias in the ACL rules.
type Groups map[string][]string
// Hosts are alias for IP addresses or subnets.
type Hosts map[string]netip.Prefix
// TagOwners specify what users (namespaces?) are allow to use certain tags.
type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
type ACLTest struct {
Source string `json:"src" yaml:"src"`
Accept []string `json:"accept" yaml:"accept"`
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
}
// AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes
// or exit node status automatically enabled.
type AutoApprovers struct {
Routes map[string][]string `json:"routes" yaml:"routes"`
ExitNode []string `json:"exitNode" yaml:"exitNode"`
}
// UnmarshalJSON allows to parse the Hosts directly into netip objects.
func (hosts *Hosts) UnmarshalJSON(data []byte) error {
newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string)
ast, err := hujson.Parse(data)
if err != nil {
return err
}
ast.Standardize()
data = ast.Pack()
err = json.Unmarshal(data, &hostIPPrefixMap)
if err != nil {
return err
}
for host, prefixStr := range hostIPPrefixMap {
if !strings.Contains(prefixStr, "/") {
prefixStr += "/32"
}
prefix, err := netip.ParsePrefix(prefixStr)
if err != nil {
return err
}
newHosts[host] = prefix
}
*hosts = newHosts
return nil
}
// UnmarshalYAML allows to parse the Hosts directly into netip objects.
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string)
err := yaml.Unmarshal(data, &hostIPPrefixMap)
if err != nil {
return err
}
for host, prefixStr := range hostIPPrefixMap {
prefix, err := netip.ParsePrefix(prefixStr)
if err != nil {
return err
}
newHosts[host] = prefix
}
*hosts = newHosts
return nil
}
// IsZero is perhaps a bit naive here.
func (policy ACLPolicy) IsZero() bool {
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
return true
}
return false
}
// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix.
func (autoApprovers *AutoApprovers) GetRouteApprovers(
prefix netip.Prefix,
) ([]string, error) {
if prefix.Bits() == 0 {
return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent
}
approverAliases := []string{}
for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes {
autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix)
if err != nil {
return nil, err
}
if prefix.Bits() >= autoApprovedPrefix.Bits() &&
autoApprovedPrefix.Contains(prefix.Masked().Addr()) {
approverAliases = append(approverAliases, autoApproverAliases...)
}
}
return approverAliases, nil
}
-168
View File
@@ -1,168 +0,0 @@
package headscale
import (
"bytes"
"encoding/json"
"html/template"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"tailscale.com/types/key"
)
const (
// TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed.
registrationHoldoff = time.Second * 5
reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authkey"
RegisterMethodOIDC = "oidc"
RegisterMethodCLI = "cli"
ErrRegisterMethodCLIDoesNotSupportExpire = Error(
"machines registered with CLI does not support expire",
)
)
func (h *Headscale) HealthHandler(
writer http.ResponseWriter,
req *http.Request,
) {
respond := func(err error) {
writer.Header().Set("Content-Type", "application/health+json; charset=utf-8")
res := struct {
Status string `json:"status"`
}{
Status: "pass",
}
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
log.Error().Caller().Err(err).Msg("health check failed")
res.Status = "fail"
}
buf, err := json.Marshal(res)
if err != nil {
log.Error().Caller().Err(err).Msg("marshal failed")
}
_, err = writer.Write(buf)
if err != nil {
log.Error().Caller().Err(err).Msg("write failed")
}
}
if err := h.pingDB(req.Context()); err != nil {
respond(err)
return
}
respond(nil)
}
type registerWebAPITemplateConfig struct {
Key string
}
var registerWebAPITemplate = template.Must(
template.New("registerweb").Parse(`
<html>
<head>
<title>Registration - Headscale</title>
</head>
<body>
<h1>headscale</h1>
<h2>Machine registration</h2>
<p>
Run the command below in the headscale server to add this machine to your network:
</p>
<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
</body>
</html>
`))
// RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register/:nkey.
//
// This is not part of the Tailscale control API, as we could send whatever URL
// in the RegisterResponse.AuthURL field.
func (h *Headscale) RegisterWebAPI(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
nodeKeyStr, ok := vars["nkey"]
if !NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
// We need to make sure we dont open for XSS style injections, if the parameter that
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
// the template and log an error.
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText(
[]byte(NodePublicKeyEnsurePrefix(nodeKeyStr)),
)
if !ok || nodeKeyStr == "" || err != nil {
log.Warn().Err(err).Msg("Failed to parse incoming nodekey")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Wrong params"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
var content bytes.Buffer
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
Key: nodeKeyStr,
}); err != nil {
log.Error().
Str("func", "RegisterWebAPI").
Err(err).
Msg("Could not render register web API template")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err = writer.Write([]byte("Could not render register web API template"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
-80
View File
@@ -1,80 +0,0 @@
package headscale
import (
"github.com/rs/zerolog/log"
"tailscale.com/tailcfg"
)
func (h *Headscale) generateMapResponse(
mapRequest tailcfg.MapRequest,
machine *Machine,
) (*tailcfg.MapResponse, error) {
log.Trace().
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Cannot convert to node")
return nil, err
}
peers, err := h.getValidPeers(machine)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Cannot fetch peers")
return nil, err
}
profiles := getMapResponseUserProfiles(*machine, peers)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil {
log.Error().
Caller().
Str("func", "generateMapResponse").
Err(err).
Msg("Failed to convert peers to Tailscale nodes")
return nil, err
}
dnsConfig := getMapResponseDNSConfig(
h.cfg.DNSConfig,
h.cfg.BaseDomain,
*machine,
peers,
)
resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: h.aclRules,
DERPMap: h.DERPMap,
UserProfiles: profiles,
Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
},
}
log.Trace().
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
// Interface("payload", resp).
Msgf("Generated map response: %s", tailMapResponseToString(resp))
return &resp, nil
}
-157
View File
@@ -1,157 +0,0 @@
package headscale
import (
"fmt"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
apiPrefixLength = 7
apiKeyLength = 32
ErrAPIKeyFailedToParse = Error("Failed to parse ApiKey")
)
// APIKey describes the datamodel for API keys used to remotely authenticate with
// headscale.
type APIKey struct {
ID uint64 `gorm:"primary_key"`
Prefix string `gorm:"uniqueIndex"`
Hash []byte
CreatedAt *time.Time
Expiration *time.Time
LastSeen *time.Time
}
// CreateAPIKey creates a new ApiKey in a namespace, and returns it.
func (h *Headscale) CreateAPIKey(
expiration *time.Time,
) (string, *APIKey, error) {
prefix, err := GenerateRandomStringURLSafe(apiPrefixLength)
if err != nil {
return "", nil, err
}
toBeHashed, err := GenerateRandomStringURLSafe(apiKeyLength)
if err != nil {
return "", nil, err
}
// Key to return to user, this will only be visible _once_
keyStr := prefix + "." + toBeHashed
hash, err := bcrypt.GenerateFromPassword([]byte(toBeHashed), bcrypt.DefaultCost)
if err != nil {
return "", nil, err
}
key := APIKey{
Prefix: prefix,
Hash: hash,
Expiration: expiration,
}
if err := h.db.Save(&key).Error; err != nil {
return "", nil, fmt.Errorf("failed to save API key to database: %w", err)
}
return keyStr, &key, nil
}
// ListAPIKeys returns the list of ApiKeys for a namespace.
func (h *Headscale) ListAPIKeys() ([]APIKey, error) {
keys := []APIKey{}
if err := h.db.Find(&keys).Error; err != nil {
return nil, err
}
return keys, nil
}
// GetAPIKey returns a ApiKey for a given key.
func (h *Headscale) GetAPIKey(prefix string) (*APIKey, error) {
key := APIKey{}
if result := h.db.First(&key, "prefix = ?", prefix); result.Error != nil {
return nil, result.Error
}
return &key, nil
}
// GetAPIKeyByID returns a ApiKey for a given id.
func (h *Headscale) GetAPIKeyByID(id uint64) (*APIKey, error) {
key := APIKey{}
if result := h.db.Find(&APIKey{ID: id}).First(&key); result.Error != nil {
return nil, result.Error
}
return &key, nil
}
// DestroyAPIKey destroys a ApiKey. Returns error if the ApiKey
// does not exist.
func (h *Headscale) DestroyAPIKey(key APIKey) error {
if result := h.db.Unscoped().Delete(key); result.Error != nil {
return result.Error
}
return nil
}
// ExpireAPIKey marks a ApiKey as expired.
func (h *Headscale) ExpireAPIKey(key *APIKey) error {
if err := h.db.Model(&key).Update("Expiration", time.Now()).Error; err != nil {
return err
}
return nil
}
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
prefix, hash, found := strings.Cut(keyStr, ".")
if !found {
return false, ErrAPIKeyFailedToParse
}
key, err := h.GetAPIKey(prefix)
if err != nil {
return false, fmt.Errorf("failed to validate api key: %w", err)
}
if key.Expiration.Before(time.Now()) {
return false, nil
}
if err := bcrypt.CompareHashAndPassword(key.Hash, []byte(hash)); err != nil {
return false, err
}
return true, nil
}
func (key *APIKey) toProto() *v1.ApiKey {
protoKey := v1.ApiKey{
Id: key.ID,
Prefix: key.Prefix,
}
if key.Expiration != nil {
protoKey.Expiration = timestamppb.New(*key.Expiration)
}
if key.CreatedAt != nil {
protoKey.CreatedAt = timestamppb.New(*key.CreatedAt)
}
if key.LastSeen != nil {
protoKey.LastSeen = timestamppb.New(*key.LastSeen)
}
return &protoKey
}
-89
View File
@@ -1,89 +0,0 @@
package headscale
import (
"time"
"gopkg.in/check.v1"
)
func (*Suite) TestCreateAPIKey(c *check.C) {
apiKeyStr, apiKey, err := app.CreateAPIKey(nil)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)
// Did we get a valid key?
c.Assert(apiKey.Prefix, check.NotNil)
c.Assert(apiKey.Hash, check.NotNil)
c.Assert(apiKeyStr, check.Not(check.Equals), "")
_, err = app.ListAPIKeys()
c.Assert(err, check.IsNil)
keys, err := app.ListAPIKeys()
c.Assert(err, check.IsNil)
c.Assert(len(keys), check.Equals, 1)
}
func (*Suite) TestAPIKeyDoesNotExist(c *check.C) {
key, err := app.GetAPIKey("does-not-exist")
c.Assert(err, check.NotNil)
c.Assert(key, check.IsNil)
}
func (*Suite) TestValidateAPIKeyOk(c *check.C) {
nowPlus2 := time.Now().Add(2 * time.Hour)
apiKeyStr, apiKey, err := app.CreateAPIKey(&nowPlus2)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)
valid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(valid, check.Equals, true)
}
func (*Suite) TestValidateAPIKeyNotOk(c *check.C) {
nowMinus2 := time.Now().Add(time.Duration(-2) * time.Hour)
apiKeyStr, apiKey, err := app.CreateAPIKey(&nowMinus2)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)
valid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(valid, check.Equals, false)
now := time.Now()
apiKeyStrNow, apiKey, err := app.CreateAPIKey(&now)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)
validNow, err := app.ValidateAPIKey(apiKeyStrNow)
c.Assert(err, check.IsNil)
c.Assert(validNow, check.Equals, false)
validSilly, err := app.ValidateAPIKey("nota.validkey")
c.Assert(err, check.NotNil)
c.Assert(validSilly, check.Equals, false)
validWithErr, err := app.ValidateAPIKey("produceerrorkey")
c.Assert(err, check.NotNil)
c.Assert(validWithErr, check.Equals, false)
}
func (*Suite) TestExpireAPIKey(c *check.C) {
nowPlus2 := time.Now().Add(2 * time.Hour)
apiKeyStr, apiKey, err := app.CreateAPIKey(&nowPlus2)
c.Assert(err, check.IsNil)
c.Assert(apiKey, check.NotNil)
valid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(valid, check.Equals, true)
err = app.ExpireAPIKey(apiKey)
c.Assert(err, check.IsNil)
c.Assert(apiKey.Expiration, check.NotNil)
notValid, err := app.ValidateAPIKey(apiKeyStr)
c.Assert(err, check.IsNil)
c.Assert(notValid, check.Equals, false)
}
-985
View File
@@ -1,985 +0,0 @@
package headscale
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"sort"
"strings"
"sync"
"syscall"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/mux"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/puzpuzpuz/xsync/v2"
zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
)
const (
errSTUNAddressNotSet = Error("STUN address not set")
errUnsupportedDatabase = Error("unsupported DB")
errUnsupportedLetsEncryptChallengeType = Error(
"unknown value for Lets Encrypt challenge type",
)
)
const (
AuthPrefix = "Bearer "
Postgres = "postgres"
Sqlite = "sqlite3"
updateInterval = 5000
HTTPReadTimeout = 30 * time.Second
HTTPShutdownTimeout = 3 * time.Second
privateKeyFileMode = 0o600
registerCacheExpiration = time.Minute * 15
registerCacheCleanup = time.Minute * 20
DisabledClientAuth = "disabled"
RelaxedClientAuth = "relaxed"
EnforcedClientAuth = "enforced"
)
// Headscale represents the base app of the service.
type Headscale struct {
cfg *Config
db *gorm.DB
dbString string
dbType string
dbDebug bool
privateKey *key.MachinePrivate
noisePrivateKey *key.MachinePrivate
noiseMux *mux.Router
DERPMap *tailcfg.DERPMap
DERPServer *DERPServer
aclPolicy *ACLPolicy
aclRules []tailcfg.FilterRule
lastStateChange *xsync.MapOf[string, time.Time]
oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
registrationCache *cache.Cache
ipAllocationMutex sync.Mutex
shutdownChan chan struct{}
pollNetMapStreamWG sync.WaitGroup
}
// Look up the TLS constant relative to user-supplied TLS client
// authentication mode. If an unknown mode is supplied, the default
// value, tls.RequireAnyClientCert, is returned. The returned boolean
// indicates if the supplied mode was valid.
func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
switch mode {
case DisabledClientAuth:
// Client cert is _not_ required.
return tls.NoClientCert, true
case RelaxedClientAuth:
// Client cert required, but _not verified_.
return tls.RequireAnyClientCert, true
case EnforcedClientAuth:
// Client cert is _required and verified_.
return tls.RequireAndVerifyClientCert, true
default:
// Return the default when an unknown value is supplied.
return tls.RequireAnyClientCert, false
}
}
func NewHeadscale(cfg *Config) (*Headscale, error) {
privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create private key: %w", err)
}
// TS2021 requires to have a different key from the legacy protocol.
noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
}
if privateKey.Equal(*noisePrivateKey) {
return nil, fmt.Errorf("private key and noise private key are the same: %w", err)
}
var dbString string
switch cfg.DBtype {
case Postgres:
dbString = fmt.Sprintf(
"host=%s dbname=%s user=%s",
cfg.DBhost,
cfg.DBname,
cfg.DBuser,
)
if !cfg.DBssl {
dbString += " sslmode=disable"
}
if cfg.DBport != 0 {
dbString += fmt.Sprintf(" port=%d", cfg.DBport)
}
if cfg.DBpass != "" {
dbString += fmt.Sprintf(" password=%s", cfg.DBpass)
}
case Sqlite:
dbString = cfg.DBpath
default:
return nil, errUnsupportedDatabase
}
registrationCache := cache.New(
registerCacheExpiration,
registerCacheCleanup,
)
app := Headscale{
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
privateKey: privateKey,
noisePrivateKey: noisePrivateKey,
aclRules: tailcfg.FilterAllowAll, // default allowall
registrationCache: registrationCache,
pollNetMapStreamWG: sync.WaitGroup{},
}
err = app.initDB()
if err != nil {
return nil, err
}
if cfg.OIDC.Issuer != "" {
err = app.initOIDC()
if err != nil {
if cfg.OIDC.OnlyStartIfOIDCIsAvailable {
return nil, err
} else {
log.Warn().Err(err).Msg("failed to set up OIDC provider, falling back to CLI based authentication")
}
}
}
if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes)
// we might have routes already from Split DNS
if app.cfg.DNSConfig.Routes == nil {
app.cfg.DNSConfig.Routes = make(map[string][]*dnstype.Resolver)
}
for _, d := range magicDNSDomains {
app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
}
}
if cfg.DERP.ServerEnabled {
embeddedDERPServer, err := app.NewDERPServer()
if err != nil {
return nil, err
}
app.DERPServer = embeddedDERPServer
}
return &app, nil
}
// Redirect to our TLS url.
func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
target := h.cfg.ServerURL + req.URL.RequestURI()
http.Redirect(w, req, target, http.StatusFound)
}
// expireEphemeralNodes deletes ephemeral machine records that have not been
// seen for longer than h.cfg.EphemeralNodeInactivityTimeout.
func (h *Headscale) expireEphemeralNodes(milliSeconds int64) {
ticker := time.NewTicker(time.Duration(milliSeconds) * time.Millisecond)
for range ticker.C {
h.expireEphemeralNodesWorker()
}
}
func (h *Headscale) expireEphemeralNodesWorker() {
namespaces, err := h.ListNamespaces()
if err != nil {
log.Error().Err(err).Msg("Error listing namespaces")
return
}
for _, namespace := range namespaces {
machines, err := h.ListMachinesInNamespace(namespace.Name)
if err != nil {
log.Error().
Err(err).
Str("namespace", namespace.Name).
Msg("Error listing machines in namespace")
return
}
expiredFound := false
for _, machine := range machines {
if machine.AuthKey != nil && machine.LastSeen != nil &&
machine.AuthKey.Ephemeral &&
time.Now().
After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
expiredFound = true
log.Info().
Str("machine", machine.Hostname).
Msg("Ephemeral client removed from database")
err = h.db.Unscoped().Delete(machine).Error
if err != nil {
log.Error().
Err(err).
Str("machine", machine.Hostname).
Msg("🤮 Cannot delete ephemeral machine from the database")
}
}
}
if expiredFound {
h.setLastStateChangeToNow()
}
}
}
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Check if the request is coming from the on-server client.
// This is not secure, but it is to maintain maintainability
// with the "legacy" database-based client
// It is also neede for grpc-gateway to be able to connect to
// the server
client, _ := peer.FromContext(ctx)
log.Trace().
Caller().
Str("client_address", client.Addr.String()).
Msg("Client is trying to authenticate")
meta, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg("Retrieving metadata is failed")
return ctx, status.Errorf(
codes.InvalidArgument,
"Retrieving metadata is failed",
)
}
authHeader, ok := meta["authorization"]
if !ok {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg("Authorization token is not supplied")
return ctx, status.Errorf(
codes.Unauthenticated,
"Authorization token is not supplied",
)
}
token := authHeader[0]
if !strings.HasPrefix(token, AuthPrefix) {
log.Error().
Caller().
Str("client_address", client.Addr.String()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
return ctx, status.Error(
codes.Unauthenticated,
`missing "Bearer " prefix in "Authorization" header`,
)
}
valid, err := h.ValidateAPIKey(strings.TrimPrefix(token, AuthPrefix))
if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", client.Addr.String()).
Msg("failed to validate token")
return ctx, status.Error(codes.Internal, "failed to validate token")
}
if !valid {
log.Info().
Str("client_address", client.Addr.String()).
Msg("invalid token")
return ctx, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().
Caller().
Str("client_address", req.RemoteAddr).
Msg("HTTP authentication invoked")
authHeader := req.Header.Get("authorization")
if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
Str("client_address", req.RemoteAddr).
Msg(`missing "Bearer " prefix in "Authorization" header`)
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
valid, err := h.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
if err != nil {
log.Error().
Caller().
Err(err).
Str("client_address", req.RemoteAddr).
Msg("failed to validate token")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
if !valid {
log.Info().
Str("client_address", req.RemoteAddr).
Msg("invalid token")
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
next.ServeHTTP(writer, req)
})
}
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
// and will remove it if it is not.
func (h *Headscale) ensureUnixSocketIsAbsent() error {
// File does not exist, all fine
if _, err := os.Stat(h.cfg.UnixSocket); errors.Is(err, os.ErrNotExist) {
return nil
}
return os.Remove(h.cfg.UnixSocket)
}
func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
router := mux.NewRouter()
router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost)
router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet)
h.addLegacyHandlers(router)
router.HandleFunc("/oidc/register/{nkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).
Methods(http.MethodGet)
router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig).
Methods(http.MethodGet)
router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1).
Methods(http.MethodGet)
if h.cfg.DERP.ServerEnabled {
router.HandleFunc("/derp", h.DERPHandler)
router.HandleFunc("/derp/probe", h.DERPProbeHandler)
router.HandleFunc("/bootstrap-dns", h.DERPBootstrapDNSHandler)
}
apiRouter := router.PathPrefix("/api").Subrouter()
apiRouter.Use(h.httpAuthenticationMiddleware)
apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
router.PathPrefix("/").HandlerFunc(stdoutHandler)
return router
}
func (h *Headscale) createNoiseMux() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/map", h.NoisePollNetMapHandler)
return router
}
// Serve launches a GIN server with the Headscale API.
func (h *Headscale) Serve() error {
var err error
// Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP)
if h.cfg.DERP.ServerEnabled {
// When embedded DERP is enabled we always need a STUN server
if h.cfg.DERP.STUNAddr == "" {
return errSTUNAddressNotSet
}
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
go h.ServeSTUN()
}
if h.cfg.DERP.AutoUpdate {
derpMapCancelChannel := make(chan struct{})
defer func() { derpMapCancelChannel <- struct{}{} }()
go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
}
go h.expireEphemeralNodes(updateInterval)
if zl.GlobalLevel() == zl.TraceLevel {
zerolog.RespLog = true
} else {
zerolog.RespLog = false
}
// Prepare group for running listeners
errorGroup := new(errgroup.Group)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
//
//
// Set up LOCAL listeners
//
err = h.ensureUnixSocketIsAbsent()
if err != nil {
return fmt.Errorf("unable to remove old socket file: %w", err)
}
socketListener, err := net.Listen("unix", h.cfg.UnixSocket)
if err != nil {
return fmt.Errorf("failed to set up gRPC socket: %w", err)
}
// Change socket permissions
if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil {
return fmt.Errorf("failed change permission of gRPC socket: %w", err)
}
grpcGatewayMux := runtime.NewServeMux()
// Make the grpc-gateway connect to grpc over socket
grpcGatewayConn, err := grpc.Dial(
h.cfg.UnixSocket,
[]grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(GrpcSocketDialer),
}...,
)
if err != nil {
return err
}
// Connect to the gRPC server over localhost to skip
// the authentication.
err = v1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn)
if err != nil {
return err
}
// Start the local gRPC server without TLS and without authentication
grpcSocket := grpc.NewServer(zerolog.UnaryInterceptor())
v1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
reflection.Register(grpcSocket)
errorGroup.Go(func() error { return grpcSocket.Serve(socketListener) })
//
//
// Set up REMOTE listeners
//
tlsConfig, err := h.getTLSSettings()
if err != nil {
log.Error().Err(err).Msg("Failed to set up TLS configuration")
return err
}
//
//
// gRPC setup
//
// We are sadly not able to run gRPC and HTTPS (2.0) on the same
// port because the connection mux does not support matching them
// since they are so similar. There is multiple issues open and we
// can revisit this if changes:
// https://github.com/soheilhy/cmux/issues/68
// https://github.com/soheilhy/cmux/issues/91
var grpcServer *grpc.Server
var grpcListener net.Listener
if tlsConfig != nil || h.cfg.GRPCAllowInsecure {
log.Info().Msgf("Enabling remote gRPC at %s", h.cfg.GRPCAddr)
grpcOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(
grpcMiddleware.ChainUnaryServer(
h.grpcAuthenticationInterceptor,
zerolog.NewUnaryServerInterceptor(),
),
),
}
if tlsConfig != nil {
grpcOptions = append(grpcOptions,
grpc.Creds(credentials.NewTLS(tlsConfig)),
)
} else {
log.Warn().Msg("gRPC is running without security")
}
grpcServer = grpc.NewServer(grpcOptions...)
v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
reflection.Register(grpcServer)
grpcListener, err = net.Listen("tcp", h.cfg.GRPCAddr)
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
errorGroup.Go(func() error { return grpcServer.Serve(grpcListener) })
log.Info().
Msgf("listening and serving gRPC on: %s", h.cfg.GRPCAddr)
}
//
//
// HTTP setup
//
// This is the regular router that we expose
// over our main Addr. It also serves the legacy Tailcale API
router := h.createRouter(grpcGatewayMux)
// This router is served only over the Noise connection, and exposes only the new API.
//
// The HTTP2 server that exposes this router is created for
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
h.noiseMux = h.createNoiseMux()
httpServer := &http.Server{
Addr: h.cfg.Addr,
Handler: router,
ReadTimeout: HTTPReadTimeout,
// Go does not handle timeouts in HTTP very well, and there is
// no good way to handle streaming timeouts, therefore we need to
// keep this at unlimited and be careful to clean up connections
// https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#aboutstreaming
WriteTimeout: 0,
}
var httpListener net.Listener
if tlsConfig != nil {
httpServer.TLSConfig = tlsConfig
httpListener, err = tls.Listen("tcp", h.cfg.Addr, tlsConfig)
} else {
httpListener, err = net.Listen("tcp", h.cfg.Addr)
}
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
errorGroup.Go(func() error { return httpServer.Serve(httpListener) })
log.Info().
Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
promMux := http.NewServeMux()
promMux.Handle("/metrics", promhttp.Handler())
promHTTPServer := &http.Server{
Addr: h.cfg.MetricsAddr,
Handler: promMux,
ReadTimeout: HTTPReadTimeout,
WriteTimeout: 0,
}
var promHTTPListener net.Listener
promHTTPListener, err = net.Listen("tcp", h.cfg.MetricsAddr)
if err != nil {
return fmt.Errorf("failed to bind to TCP address: %w", err)
}
errorGroup.Go(func() error { return promHTTPServer.Serve(promHTTPListener) })
log.Info().
Msgf("listening and serving metrics on: %s", h.cfg.MetricsAddr)
// Handle common process-killing signals so we can gracefully shut down:
h.shutdownChan = make(chan struct{})
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
syscall.SIGHUP)
sigFunc := func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
for {
sig := <-c
switch sig {
case syscall.SIGHUP:
log.Info().
Str("signal", sig.String()).
Msg("Received SIGHUP, reloading ACL and Config")
// TODO(kradalby): Reload config on SIGHUP
if h.cfg.ACL.PolicyPath != "" {
aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath)
err := h.LoadACLPolicy(aclPath)
if err != nil {
log.Error().Err(err).Msg("Failed to reload ACL policy")
}
log.Info().
Str("path", aclPath).
Msg("ACL policy successfully reloaded, notifying nodes of change")
h.setLastStateChangeToNow()
}
default:
log.Info().
Str("signal", sig.String()).
Msg("Received signal to stop, shutting down gracefully")
close(h.shutdownChan)
h.pollNetMapStreamWG.Wait()
// Gracefully shut down servers
ctx, cancel := context.WithTimeout(
context.Background(),
HTTPShutdownTimeout,
)
if err := promHTTPServer.Shutdown(ctx); err != nil {
log.Error().Err(err).Msg("Failed to shutdown prometheus http")
}
if err := httpServer.Shutdown(ctx); err != nil {
log.Error().Err(err).Msg("Failed to shutdown http")
}
grpcSocket.GracefulStop()
if grpcServer != nil {
grpcServer.GracefulStop()
grpcListener.Close()
}
// Close network listeners
promHTTPListener.Close()
httpListener.Close()
grpcGatewayConn.Close()
// Stop listening (and unlink the socket if unix type):
socketListener.Close()
// Close db connections
db, err := h.db.DB()
if err != nil {
log.Error().Err(err).Msg("Failed to get db handle")
}
err = db.Close()
if err != nil {
log.Error().Err(err).Msg("Failed to close db")
}
log.Info().
Msg("Headscale stopped")
// And we're done:
cancel()
os.Exit(0)
}
}
}
errorGroup.Go(func() error {
sigFunc(sigc)
return nil
})
return errorGroup.Wait()
}
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
var err error
if h.cfg.TLS.LetsEncrypt.Hostname != "" {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().
Msg("Listening with TLS but ServerURL does not start with https://")
}
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLS.LetsEncrypt.Hostname),
Cache: autocert.DirCache(h.cfg.TLS.LetsEncrypt.CacheDir),
Client: &acme.Client{
DirectoryURL: h.cfg.ACMEURL,
},
Email: h.cfg.ACMEEmail,
}
switch h.cfg.TLS.LetsEncrypt.ChallengeType {
case tlsALPN01ChallengeType:
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
// The RFC requires that the validation is done on port 443; in other words, headscale
// must be reachable on port 443.
return certManager.TLSConfig(), nil
case http01ChallengeType:
// Configuration via autocert with HTTP-01. This requires listening on
// port 80 for the certificate validation in addition to the headscale
// service, which can be configured to run on any other port.
server := &http.Server{
Addr: h.cfg.TLS.LetsEncrypt.Listen,
Handler: certManager.HTTPHandler(http.HandlerFunc(h.redirect)),
ReadTimeout: HTTPReadTimeout,
}
go func() {
err := server.ListenAndServe()
log.Fatal().
Caller().
Err(err).
Msg("failed to set up a HTTP server")
}()
return certManager.TLSConfig(), nil
default:
return nil, errUnsupportedLetsEncryptChallengeType
}
} else if h.cfg.TLS.CertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
}
return nil, err
} else {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
}
log.Info().Msg(fmt.Sprintf(
"Client authentication (mTLS) is \"%s\". See the docs to learn about configuring this setting.",
h.cfg.TLS.ClientAuthMode))
tlsConfig := &tls.Config{
ClientAuth: h.cfg.TLS.ClientAuthMode,
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
return tlsConfig, err
}
}
func (h *Headscale) setLastStateChangeToNow() {
var err error
now := time.Now().UTC()
namespaces, err := h.ListNamespaces()
if err != nil {
log.Error().
Caller().
Err(err).
Msg("failed to fetch all namespaces, failing to update last changed state.")
}
for _, namespace := range namespaces {
lastStateUpdate.WithLabelValues(namespace.Name, "headscale").Set(float64(now.Unix()))
if h.lastStateChange == nil {
h.lastStateChange = xsync.NewMapOf[time.Time]()
}
h.lastStateChange.Store(namespace.Name, now)
}
}
func (h *Headscale) getLastStateChange(namespaces ...Namespace) time.Time {
times := []time.Time{}
// getLastStateChange takes a list of namespaces as a "filter", if no namespaces
// are past, then use the entier list of namespaces and look for the last update
if len(namespaces) > 0 {
for _, namespace := range namespaces {
if lastChange, ok := h.lastStateChange.Load(namespace.Name); ok {
times = append(times, lastChange)
}
}
} else {
h.lastStateChange.Range(func(key string, value time.Time) bool {
times = append(times, value)
return true
})
}
sort.Slice(times, func(i, j int) bool {
return times[i].After(times[j])
})
log.Trace().Msgf("Latest times %#v", times)
if len(times) == 0 {
return time.Now().UTC()
} else {
return times[0]
}
}
func stdoutHandler(
writer http.ResponseWriter,
req *http.Request,
) {
body, _ := io.ReadAll(req.Body)
log.Trace().
Interface("header", req.Header).
Interface("proto", req.Proto).
Interface("url", req.URL).
Bytes("body", body).
Msg("Request did not match")
}
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
privateKey, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
log.Info().Str("path", path).Msg("No private key file at path, creating...")
machineKey := key.NewMachine()
machineKeyStr, err := machineKey.MarshalText()
if err != nil {
return nil, fmt.Errorf(
"failed to convert private key to string for saving: %w",
err,
)
}
err = os.WriteFile(path, machineKeyStr, privateKeyFileMode)
if err != nil {
return nil, fmt.Errorf(
"failed to save private key to disk: %w",
err,
)
}
return &machineKey, nil
} else if err != nil {
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
trimmedPrivateKey := strings.TrimSpace(string(privateKey))
privateKeyEnsurePrefix := PrivateKeyEnsurePrefix(trimmedPrivateKey)
var machineKey key.MachinePrivate
if err = machineKey.UnmarshalText([]byte(privateKeyEnsurePrefix)); err != nil {
log.Info().
Str("path", path).
Msg("This might be due to a legacy (headscale pre-0.12) private key. " +
"If the key is in WireGuard format, delete the key and restart headscale. " +
"A new key will automatically be generated. All Tailscale clients will have to be restarted")
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
return &machineKey, nil
}
-78
View File
@@ -1,78 +0,0 @@
package headscale
import (
"net/netip"
"os"
"testing"
"gopkg.in/check.v1"
)
func Test(t *testing.T) {
check.TestingT(t)
}
var _ = check.Suite(&Suite{})
type Suite struct{}
var (
tmpDir string
app Headscale
)
func (s *Suite) SetUpTest(c *check.C) {
s.ResetDB(c)
}
func (s *Suite) TearDownTest(c *check.C) {
os.RemoveAll(tmpDir)
}
func (s *Suite) ResetDB(c *check.C) {
if len(tmpDir) != 0 {
os.RemoveAll(tmpDir)
}
var err error
tmpDir, err = os.MkdirTemp("", "autoygg-client-test")
if err != nil {
c.Fatal(err)
}
cfg := Config{
IPPrefixes: []netip.Prefix{
netip.MustParsePrefix("10.27.0.0/23"),
},
}
app = Headscale{
cfg: &cfg,
dbType: "sqlite3",
dbString: tmpDir + "/headscale_test.db",
}
err = app.initDB()
if err != nil {
c.Fatal(err)
}
db, err := app.openDB()
if err != nil {
c.Fatal(err)
}
app.db = db
}
// Enusre an error is returned when an invalid auth mode
// is supplied.
func (s *Suite) TestInvalidClientAuthMode(c *check.C) {
_, isValid := LookupTLSClientAuthMode("invalid")
c.Assert(isValid, check.Equals, false)
}
// Ensure that all client auth modes return a nil error.
func (s *Suite) TestAuthModes(c *check.C) {
modes := []string{"disabled", "relaxed", "enforced"}
for _, v := range modes {
_, isValid := LookupTLSClientAuthMode(v)
c.Assert(isValid, check.Equals, true)
}
}
-21
View File
@@ -1,21 +0,0 @@
version: v1
plugins:
- name: go
out: gen/go
opt:
- paths=source_relative
- name: go-grpc
out: gen/go
opt:
- paths=source_relative
- name: grpc-gateway
out: gen/go
opt:
- paths=source_relative
- generate_unbound_methods=true
# - name: gorm
# out: gen/go
# opt:
# - paths=source_relative,enums=string,gateway=true
- name: openapiv2
out: gen/openapiv2
+95
View File
@@ -0,0 +1,95 @@
# cmd/dev -- Local Development Environment
Starts a headscale server on localhost with a pre-created user and
pre-auth key. Pair with `mts` to add real tailscale nodes.
## Quick start
```bash
# Terminal 1: start headscale
go run ./cmd/dev
# Terminal 2: start mts server
go tool mts server run
# Terminal 3: add and connect nodes
go tool mts server add node1
go tool mts server add node2
# Disable logtail (avoids startup delays, see "Known issues" below)
for n in node1 node2; do
cat > ~/.config/multi-tailscale-dev/$n/env.txt << 'EOF'
TS_NO_LOGS_NO_SUPPORT=true
EOF
done
# Restart nodes so env.txt takes effect
go tool mts server stop node1 && go tool mts server start node1
go tool mts server stop node2 && go tool mts server start node2
# Connect to headscale (use the auth key printed by cmd/dev)
go tool mts node1 up --login-server=http://127.0.0.1:8080 --authkey=<KEY> --reset
go tool mts node2 up --login-server=http://127.0.0.1:8080 --authkey=<KEY> --reset
# Verify
go tool mts node1 status
```
## Flags
| Flag | Default | Description |
| -------- | ------- | ---------------------------- |
| `--port` | 8080 | Headscale listen port |
| `--keep` | false | Keep state directory on exit |
The metrics/debug port is `port + 1010` (default 9090).
## What it does
1. Builds the headscale binary into a temp directory
2. Writes a minimal dev config (SQLite, public DERP, debug logging)
3. Starts `headscale serve` as a subprocess
4. Creates a "dev" user and a reusable 24h pre-auth key via the CLI
5. Prints a banner with server URL, auth key, and usage instructions
6. Blocks until Ctrl+C, then kills headscale
State lives in `/tmp/headscale-dev-*/`. Pass `--keep` to preserve it
across restarts (useful for inspecting the database or reusing keys).
## Useful endpoints
- `http://127.0.0.1:8080/health` -- health check
- `http://127.0.0.1:9090/debug/ping` -- interactive ping UI
- `http://127.0.0.1:9090/debug/ping?node=1` -- quick-ping a node
- `POST http://127.0.0.1:9090/debug/ping` with `node=<id>` -- trigger ping
## Managing headscale
The banner prints the full path to the built binary and config. Use it
for any headscale CLI command:
```bash
/tmp/headscale-dev-*/headscale -c /tmp/headscale-dev-*/config.yaml nodes list
/tmp/headscale-dev-*/headscale -c /tmp/headscale-dev-*/config.yaml users list
```
## Known issues
### Logtail delays on mts nodes
Freshly created `mts` instances may take 30+ seconds to start if
`~/.local/share/tailscale/` contains stale logtail cache from previous
tailscaled runs. The daemon blocks trying to upload old logs before
creating its socket.
Fix: write `TS_NO_LOGS_NO_SUPPORT=true` to each instance's `env.txt`
before starting (or restart after writing). See the quick start above.
### mts node cleanup
`mts` stores state in `~/.config/multi-tailscale-dev/`. Old instances
accumulate over time. Clean them with:
```bash
go tool mts server rm <name>
```
+312
View File
@@ -0,0 +1,312 @@
// cmd/dev starts a local headscale development server with a pre-created
// user and pre-auth key, ready for connecting tailscale nodes via mts.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
)
var (
port = flag.Int("port", 8080, "headscale listen port")
keep = flag.Bool("keep", false, "keep state directory on exit")
)
var errHealthTimeout = errors.New("health check timed out")
var errEmptyAuthKey = errors.New("empty auth key in response")
// maxDevPort is the highest --port value that keeps the derived metrics
// port (port+1010) inside the valid 1..65535 TCP range.
const maxDevPort = 64525
const devConfig = `---
server_url: http://127.0.0.1:%d
listen_addr: 127.0.0.1:%d
metrics_listen_addr: 127.0.0.1:%d
noise:
private_key_path: %s/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
database:
type: sqlite
sqlite:
path: %s/db.sqlite
write_ahead_log: true
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: false
dns:
magic_dns: true
base_domain: headscale.dev
override_local_dns: false
log:
level: debug
format: text
policy:
mode: database
unix_socket: %s/headscale.sock
unix_socket_permission: "0770"
`
func main() {
flag.Parse()
log.SetFlags(0)
if *port < 1 || *port > maxDevPort {
log.Fatalf(
"--port must be in 1..%d (higher values overflow the derived metrics port); got %d",
maxDevPort, *port,
)
}
http.DefaultClient.Timeout = 2 * time.Second
http.DefaultClient.CheckRedirect = func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
err := run()
if err != nil {
log.Fatal(err)
}
}
func run() error {
metricsPort := *port + 1010 // default 9090
tmpDir, err := os.MkdirTemp("", "headscale-dev-")
if err != nil {
return fmt.Errorf("creating temp dir: %w", err)
}
if !*keep {
defer os.RemoveAll(tmpDir)
}
// Write config.
configPath := filepath.Join(tmpDir, "config.yaml")
configContent := fmt.Sprintf(
devConfig,
*port, *port, metricsPort,
tmpDir, tmpDir, tmpDir,
)
err = os.WriteFile(configPath, []byte(configContent), 0o600)
if err != nil {
return fmt.Errorf("writing config: %w", err)
}
// Build headscale.
fmt.Println("Building headscale...")
hsBin := filepath.Join(tmpDir, "headscale")
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
build := exec.CommandContext(ctx, "go", "build", "-o", hsBin, "./cmd/headscale")
build.Stdout = os.Stdout
build.Stderr = os.Stderr
err = build.Run()
if err != nil {
return fmt.Errorf("building headscale: %w", err)
}
// Start headscale serve.
fmt.Println("Starting headscale server...")
serve := exec.CommandContext(ctx, hsBin, "serve", "-c", configPath)
serve.Stdout = os.Stdout
serve.Stderr = os.Stderr
err = serve.Start()
if err != nil {
return fmt.Errorf("starting headscale: %w", err)
}
// Wait for server to be ready.
healthURL := fmt.Sprintf("http://127.0.0.1:%d/health", *port)
err = waitForHealth(ctx, healthURL, 30*time.Second)
if err != nil {
return fmt.Errorf("waiting for headscale: %w", err)
}
// Create user.
fmt.Println("Creating user and pre-auth key...")
userJSON, err := runHS(ctx, hsBin, configPath, "users", "create", "dev", "-o", "json")
if err != nil {
return fmt.Errorf("creating user: %w", err)
}
userID, err := extractUserID(userJSON)
if err != nil {
return fmt.Errorf("parsing user: %w", err)
}
// Create pre-auth key.
keyJSON, err := runHS(
ctx, hsBin, configPath,
"preauthkeys", "create",
"-u", strconv.FormatUint(userID, 10),
"--reusable",
"-e", "24h",
"-o", "json",
)
if err != nil {
return fmt.Errorf("creating pre-auth key: %w", err)
}
authKey, err := extractAuthKey(keyJSON)
if err != nil {
return fmt.Errorf("parsing pre-auth key: %w", err)
}
// Print banner.
fmt.Printf(
`
=== Headscale Dev Environment ===
Server: http://127.0.0.1:%d
Metrics: http://127.0.0.1:%d
Debug: http://127.0.0.1:%d/debug/ping
Config: %s
State: %s
Pre-auth key: %s
Connect nodes with mts:
go tool mts server run # start mts (once, another terminal)
go tool mts server add node1 # create a node
go tool mts node1 up --login-server=http://127.0.0.1:%d --authkey=%s
go tool mts node1 status # check connection
Manage headscale:
%s -c %s nodes list
%s -c %s users list
Press Ctrl+C to stop.
`,
*port, metricsPort, metricsPort,
configPath, tmpDir,
authKey,
*port, authKey,
hsBin, configPath,
hsBin, configPath,
)
// Wait for headscale to exit.
err = serve.Wait()
if err != nil {
// Context cancellation is expected on Ctrl+C.
if ctx.Err() != nil {
fmt.Println("\nShutting down...")
return nil
}
return fmt.Errorf("headscale exited: %w", err)
}
return nil
}
// waitForHealth polls the health endpoint until it returns 200 or the
// timeout expires.
func waitForHealth(ctx context.Context, url string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if ctx.Err() != nil {
return ctx.Err()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err == nil {
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return nil
}
}
// Busy-wait is acceptable for a dev tool polling a local server.
time.Sleep(200 * time.Millisecond) //nolint:forbidigo
}
return errHealthTimeout
}
// runHS executes a headscale CLI command and returns its stdout.
func runHS(ctx context.Context, bin, config string, args ...string) ([]byte, error) {
fullArgs := append([]string{"-c", config}, args...)
cmd := exec.CommandContext(ctx, bin, fullArgs...)
cmd.Stderr = os.Stderr
return cmd.Output()
}
// extractUserID parses the JSON output of "users create" and returns the
// user ID.
func extractUserID(data []byte) (uint64, error) {
var user struct {
ID uint64 `json:"id"`
}
err := json.Unmarshal(data, &user)
if err != nil {
return 0, fmt.Errorf("unmarshalling user JSON: %w (raw: %s)", err, data)
}
return user.ID, nil
}
// extractAuthKey parses the JSON output of "preauthkeys create" and
// returns the key string.
func extractAuthKey(data []byte) (string, error) {
var key struct {
Key string `json:"key"`
}
err := json.Unmarshal(data, &key)
if err != nil {
return "", fmt.Errorf("unmarshalling key JSON: %w (raw: %s)", err, data)
}
if key.Key == "" {
return "", errEmptyAuthKey
}
return key.Key, nil
}
+75
View File
@@ -0,0 +1,75 @@
// Command gen-openapi emits a Headscale OpenAPI document from the authoritative
// Huma definitions in hscontrol/api/v1 and hscontrol/api/v2. The server also
// serves each spec live (at /openapi.yaml and /api/v2/openapi); this tool emits
// them on demand, and with -downgrade the 3.0.3 form used to generate the typed
// client. The output is not committed.
//
// Usage:
//
// go run ./cmd/gen-openapi # write the v1 3.1 spec to its default path
// go run ./cmd/gen-openapi -api v2 # write the v2 3.1 spec to its default path
// go run ./cmd/gen-openapi -downgrade <path> # write the v1 3.0.3 downgrade (for client gen)
// go run ./cmd/gen-openapi -api v2 -downgrade <path> # the v2 3.0.3 downgrade
package main
import (
"flag"
"log"
"os"
"path/filepath"
apiv1 "github.com/juanfont/headscale/hscontrol/api/v1"
apiv2 "github.com/juanfont/headscale/hscontrol/api/v2"
)
// spec bundles a version's full (3.1) and downgraded (3.0.3) generators with the
// committed output path. outPath is relative to the repository root.
type spec struct {
full func() ([]byte, error)
down func() ([]byte, error)
outPath string
}
// specs maps the -api value to its generators.
var specs = map[string]spec{
"v1": {apiv1.Spec, apiv1.Spec30, "openapi/v1/headscale.yaml"},
"v2": {apiv2.Spec, apiv2.Spec30, "openapi/v2/headscale.yaml"},
}
func main() {
api := flag.String("api", "v1", "which API spec to emit: v1 or v2")
downgrade := flag.String("downgrade", "", "write the OpenAPI 3.0.3 downgrade to this path instead of the committed 3.1 spec")
flag.Parse()
s, ok := specs[*api]
if !ok {
log.Fatalf("unknown -api %q (want v1 or v2)", *api)
}
if *downgrade != "" {
writeSpec(*downgrade, s.down)
return
}
writeSpec(s.outPath, s.full)
}
func writeSpec(path string, gen func() ([]byte, error)) {
spec, err := gen()
if err != nil {
log.Fatalf("generating OpenAPI spec: %v", err)
}
err = os.MkdirAll(filepath.Dir(path), 0o755)
if err != nil {
log.Fatalf("creating output directory: %v", err)
}
err = os.WriteFile(path, spec, 0o600)
if err != nil {
log.Fatalf("writing %s: %v", path, err)
}
log.Printf("wrote %s", path)
}
+153 -131
View File
@@ -1,21 +1,18 @@
package cli
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/prometheus/common/model"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// 90 days.
// DefaultAPIKeyExpiry is 90 days.
DefaultAPIKeyExpiry = "90d"
)
@@ -29,11 +26,12 @@ func init() {
apiKeysCmd.AddCommand(createAPIKeyCmd)
expireAPIKeyCmd.Flags().StringP("prefix", "p", "", "ApiKey prefix")
err := expireAPIKeyCmd.MarkFlagRequired("prefix")
if err != nil {
log.Fatal().Err(err).Msg("")
}
expireAPIKeyCmd.Flags().Uint64P("id", "i", 0, "ApiKey ID")
apiKeysCmd.AddCommand(expireAPIKeyCmd)
deleteAPIKeyCmd.Flags().StringP("prefix", "p", "", "ApiKey prefix")
deleteAPIKeyCmd.Flags().Uint64P("id", "i", 0, "ApiKey ID")
apiKeysCmd.AddCommand(deleteAPIKeyCmd)
}
var apiKeysCmd = &cobra.Command{
@@ -43,64 +41,45 @@ var apiKeysCmd = &cobra.Command{
}
var listAPIKeys = &cobra.Command{
Use: "list",
Use: cmdList,
Short: "List the Api keys for headscale",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ListApiKeysRequest{}
response, err := client.ListApiKeys(ctx, request)
Aliases: []string{"ls", cmdShow},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
resp, err := client.ListApiKeysWithResponse(ctx)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting the list of keys: %s", err),
output,
)
return
return fmt.Errorf("listing api keys: %w", err)
}
if output != "" {
SuccessOutput(response.ApiKeys, "", output)
return
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
tableData := pterm.TableData{
{"ID", "Prefix", "Expiration", "Created"},
}
for _, key := range response.ApiKeys {
expiration := "-"
apiKeys := resp.JSON200.ApiKeys
if key.GetExpiration() != nil {
expiration = ColourTime(key.Expiration.AsTime())
return printListOutput(cmd, apiKeys, func() error {
rows := make([][]string, 0, len(apiKeys))
for _, key := range apiKeys {
expiration := "-"
if key.Expiration != nil {
expiration = ColourTime(*key.Expiration)
}
var created string
if key.CreatedAt != nil {
created = key.CreatedAt.Format(HeadscaleDateTimeFormat)
}
rows = append(rows, []string{
key.Id,
key.Prefix,
expiration,
created,
})
}
tableData = append(tableData, []string{
strconv.FormatUint(key.GetId(), headscale.Base10),
key.GetPrefix(),
expiration,
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
})
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
}
},
return renderTable([]string{"ID", "Prefix", colExpiration, colCreated}, rows)
})
}),
}
var createAPIKeyCmd = &cobra.Command{
@@ -109,93 +88,136 @@ var createAPIKeyCmd = &cobra.Command{
Long: `
Creates a new Api key, the Api key is only visible on creation
and cannot be retrieved again.
If you loose a key, create a new one and revoke (expire) the old one.`,
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
log.Trace().
Msg("Preparing to create ApiKey")
request := &v1.CreateApiKeyRequest{}
durationStr, _ := cmd.Flags().GetString("expiration")
duration, err := model.ParseDuration(durationStr)
If you lose a key, create a new one and revoke (expire) the old one.`,
Aliases: []string{"c", cmdNew},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
expiryTime, err := expirationFromFlag(cmd)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Could not parse duration: %s\n", err),
output,
)
return
return err
}
expiration := time.Now().UTC().Add(time.Duration(duration))
log.Trace().
Dur("expiration", time.Duration(duration)).
Msg("expiration has been set")
request.Expiration = timestamppb.New(expiration)
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.CreateApiKey(ctx, request)
resp, err := client.CreateApiKeyWithResponse(ctx, clientv1.CreateApiKeyJSONRequestBody{
Expiration: &expiryTime,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot create Api Key: %s\n", err),
output,
)
return
return fmt.Errorf("creating api key: %w", err)
}
SuccessOutput(response.ApiKey, response.ApiKey, output)
},
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.ApiKey, resp.JSON200.ApiKey)
}),
}
// apiKeyIDOrPrefix reads --id and --prefix from cmd and validates that
// exactly one is provided.
func apiKeyIDOrPrefix(cmd *cobra.Command) (uint64, string, error) {
id, _ := cmd.Flags().GetUint64("id")
prefix, _ := cmd.Flags().GetString("prefix")
switch {
case id == 0 && prefix == "":
return 0, "", fmt.Errorf("either --id or --prefix must be provided: %w", errMissingParameter)
case id != 0 && prefix != "":
return 0, "", fmt.Errorf("only one of --id or --prefix can be provided: %w", errMissingParameter)
}
return id, prefix, nil
}
var expireAPIKeyCmd = &cobra.Command{
Use: "expire",
Use: cmdExpire,
Short: "Expire an ApiKey",
Aliases: []string{"revoke", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
prefix, err := cmd.Flags().GetString("prefix")
Aliases: []string{"revoke", aliasExp, "e"},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting prefix from CLI flag: %s", err),
output,
)
return
return err
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
body := clientv1.ExpireApiKeyJSONRequestBody{}
request := &v1.ExpireApiKeyRequest{
Prefix: prefix,
if id != 0 {
idStr := strconv.FormatUint(id, util.Base10)
body.Id = &idStr
}
response, err := client.ExpireApiKey(ctx, request)
if prefix != "" {
body.Prefix = &prefix
}
resp, err := client.ExpireApiKeyWithResponse(ctx, body)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot expire Api Key: %s\n", err),
output,
)
return
return fmt.Errorf("expiring api key: %w", err)
}
SuccessOutput(response, "Key expired", output)
},
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key expired")
}),
}
var deleteAPIKeyCmd = &cobra.Command{
Use: cmdDelete,
Short: "Delete an ApiKey",
Aliases: []string{"remove", aliasDel},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil {
return err
}
// The DELETE route addresses the key by its prefix in the path. When the
// user deletes by --id we resolve the id to its (masked) prefix first,
// since the path segment is required and a query-only id cannot be routed.
if prefix == "" {
prefix, err = apiKeyPrefixForID(ctx, client, id)
if err != nil {
return err
}
}
resp, err := client.DeleteApiKeyWithResponse(ctx, prefix, &clientv1.DeleteApiKeyParams{})
if err != nil {
return fmt.Errorf("deleting api key: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key deleted")
}),
}
// apiKeyPrefixForID resolves an API key id to its display prefix by listing the
// keys. The DELETE endpoint addresses keys by prefix in the URL path, so a
// delete by --id needs the prefix; the returned masked prefix is accepted by
// the server's lookup.
func apiKeyPrefixForID(
ctx context.Context,
client *clientv1.ClientWithResponses,
id uint64,
) (string, error) {
resp, err := client.ListApiKeysWithResponse(ctx)
if err != nil {
return "", fmt.Errorf("listing api keys: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return "", apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
idStr := strconv.FormatUint(id, util.Base10)
for _, key := range resp.JSON200.ApiKeys {
if key.Id == idStr {
return key.Prefix, nil
}
}
return "", fmt.Errorf("%w: api key %d not found", errMissingParameter, id)
}
+99
View File
@@ -0,0 +1,99 @@
package cli
import (
"context"
"fmt"
"net/http"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(authCmd)
authRegisterCmd.Flags().StringP("user", "u", "", "User")
authRegisterCmd.Flags().String("auth-id", "", "Auth ID")
mustMarkRequired(authRegisterCmd, "user", "auth-id")
authCmd.AddCommand(authRegisterCmd)
authApproveCmd.Flags().String("auth-id", "", "Auth ID")
mustMarkRequired(authApproveCmd, "auth-id")
authCmd.AddCommand(authApproveCmd)
authRejectCmd.Flags().String("auth-id", "", "Auth ID")
mustMarkRequired(authRejectCmd, "auth-id")
authCmd.AddCommand(authRejectCmd)
}
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage node authentication and approval",
}
var authRegisterCmd = &cobra.Command{
Use: "register",
Short: "Register a node to your network",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
authID, _ := cmd.Flags().GetString("auth-id")
resp, err := client.AuthRegisterWithResponse(ctx, clientv1.AuthRegisterJSONRequestBody{
AuthId: &authID,
User: &user,
})
if err != nil {
return fmt.Errorf("registering node: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
node := resp.JSON200.Node
return printOutput(
cmd,
node,
fmt.Sprintf("Node %s registered", node.GivenName),
)
}),
}
var authApproveCmd = &cobra.Command{
Use: "approve",
Short: "Approve a pending authentication request",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
resp, err := client.AuthApproveWithResponse(ctx, clientv1.AuthApproveJSONRequestBody{AuthId: &authID})
if err != nil {
return fmt.Errorf("approving auth request: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Auth request approved")
}),
}
var authRejectCmd = &cobra.Command{
Use: "reject",
Short: "Reject a pending authentication request",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
resp, err := client.AuthRejectWithResponse(ctx, clientv1.AuthRejectJSONRequestBody{AuthId: &authID})
if err != nil {
return fmt.Errorf("rejecting auth request: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Auth request rejected")
}),
}
+25
View File
@@ -0,0 +1,25 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(configTestCmd)
}
var configTestCmd = &cobra.Command{
Use: "configtest",
Short: "Test the configuration.",
Long: "Run a test of the configuration and exit.",
RunE: func(cmd *cobra.Command, args []string) error {
_, err := newHeadscaleServerWithConfig()
if err != nil {
return fmt.Errorf("configuration error: %w", err)
}
return nil
},
}
+26 -94
View File
@@ -1,42 +1,23 @@
package cli
import (
"context"
"fmt"
"net/http"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
)
const (
errPreAuthKeyMalformed = Error("key is malformed. expected 64 hex characters with `nodekey` prefix")
)
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
type Error string
func (e Error) Error() string { return string(e) }
func init() {
rootCmd.AddCommand(debugCmd)
createNodeCmd.Flags().StringP("name", "", "", "Name")
err := createNodeCmd.MarkFlagRequired("name")
if err != nil {
log.Fatal().Err(err).Msg("")
}
createNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = createNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatal().Err(err).Msg("")
}
createNodeCmd.Flags().StringP("user", "u", "", "User")
createNodeCmd.Flags().StringP("key", "k", "", "Key")
err = createNodeCmd.MarkFlagRequired("key")
if err != nil {
log.Fatal().Err(err).Msg("")
}
mustMarkRequired(createNodeCmd, "name", "user", "key")
createNodeCmd.Flags().
StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to advertise")
@@ -51,82 +32,33 @@ var debugCmd = &cobra.Command{
var createNodeCmd = &cobra.Command{
Use: "create-node",
Short: "Create a node (machine) that can be registered with `nodes register <>` command",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
Short: "Create a node that can be registered with `auth register <>` command",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
name, _ := cmd.Flags().GetString("name")
registrationID, _ := cmd.Flags().GetString("key")
namespace, err := cmd.Flags().GetString("namespace")
_, err := types.AuthIDFromString(registrationID)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
return fmt.Errorf("parsing machine key: %w", err)
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
routes, _ := cmd.Flags().GetStringSlice("route")
name, err := cmd.Flags().GetString("name")
resp, err := client.DebugCreateNodeWithResponse(ctx, clientv1.DebugCreateNodeJSONRequestBody{
Key: &registrationID,
Name: &name,
User: &user,
Routes: &routes,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node from flag: %s", err),
output,
)
return
return fmt.Errorf("creating node: %w", err)
}
machineKey, err := cmd.Flags().GetString("key")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting key from flag: %s", err),
output,
)
return
}
if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) {
err = errPreAuthKeyMalformed
ErrorOutput(
err,
fmt.Sprintf("Error: %s", err),
output,
)
return
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
routes, err := cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
request := &v1.DebugCreateMachineRequest{
Key: machineKey,
Name: name,
Namespace: namespace,
Routes: routes,
}
response, err := client.DebugCreateMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot create machine: %s", status.Convert(err).Message()),
output,
)
return
}
SuccessOutput(response.Machine, "Machine created", output)
},
return printOutput(cmd, resp.JSON200.Node, "Node created")
}),
}
+4 -6
View File
@@ -15,14 +15,12 @@ var dumpConfigCmd = &cobra.Command{
Use: "dumpConfig",
Short: "dump current config to /etc/headscale/config.dump.yaml, integration test only",
Hidden: true,
Args: func(cmd *cobra.Command, args []string) error {
return nil
},
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
err := viper.WriteConfigAs("/etc/headscale/config.dump.yaml")
if err != nil {
//nolint
fmt.Println("Failed to dump config")
return fmt.Errorf("dumping config: %w", err)
}
return nil
},
}
+4 -9
View File
@@ -21,22 +21,17 @@ var generateCmd = &cobra.Command{
var generatePrivateKeyCmd = &cobra.Command{
Use: "private-key",
Short: "Generate a private key for the headscale server",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
RunE: func(cmd *cobra.Command, args []string) error {
machineKey := key.NewMachine()
machineKeyStr, err := machineKey.MarshalText()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine key from flag: %s", err),
output,
)
return fmt.Errorf("marshalling machine key: %w", err)
}
SuccessOutput(map[string]string{
return printOutput(cmd, map[string]string{
"private_key": string(machineKeyStr),
},
string(machineKeyStr), output)
string(machineKeyStr))
},
}
+32
View File
@@ -0,0 +1,32 @@
package cli
import (
"context"
"fmt"
"net/http"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(healthCmd)
}
var healthCmd = &cobra.Command{
Use: "health",
Short: "Check the health of the Headscale server",
Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
resp, err := client.HealthWithResponse(ctx)
if err != nil {
return fmt.Errorf("checking health: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "")
}),
}
+72 -15
View File
@@ -1,25 +1,33 @@
package cli
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/oauth2-proxy/mockoidc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
const (
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
accessTTL = 10 * time.Minute
refreshTTL = 60 * time.Minute
var (
errMockOidcClientIDNotDefined = errors.New("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = errors.New("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = errors.New("MOCKOIDC_PORT not defined")
errMockOidcUsersNotDefined = errors.New("MOCKOIDC_USERS not defined")
)
const refreshTTL = 60 * time.Minute
var accessTTL = 2 * time.Minute
func init() {
rootCmd.AddCommand(mockOidcCmd)
}
@@ -28,12 +36,13 @@ var mockOidcCmd = &cobra.Command{
Use: "mockoidc",
Short: "Runs a mock OIDC server for testing",
Long: "This internal command runs a OpenID Connect for testing purposes",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
err := mockOIDC()
if err != nil {
log.Error().Err(err).Msgf("Error running mock OIDC server")
os.Exit(1)
return fmt.Errorf("running mock OIDC server: %w", err)
}
return nil
},
}
@@ -42,30 +51,59 @@ func mockOIDC() error {
if clientID == "" {
return errMockOidcClientIDNotDefined
}
clientSecret := os.Getenv("MOCKOIDC_CLIENT_SECRET")
if clientSecret == "" {
return errMockOidcClientSecretNotDefined
}
addrStr := os.Getenv("MOCKOIDC_ADDR")
if addrStr == "" {
return errMockOidcPortNotDefined
}
portStr := os.Getenv("MOCKOIDC_PORT")
if portStr == "" {
return errMockOidcPortNotDefined
}
accessTTLOverride := os.Getenv("MOCKOIDC_ACCESS_TTL")
if accessTTLOverride != "" {
newTTL, err := time.ParseDuration(accessTTLOverride)
if err != nil {
return err
}
accessTTL = newTTL
}
userStr := os.Getenv("MOCKOIDC_USERS")
if userStr == "" {
return errMockOidcUsersNotDefined
}
var users []mockoidc.MockUser
err := json.Unmarshal([]byte(userStr), &users)
if err != nil {
return fmt.Errorf("unmarshalling users: %w", err)
}
log.Info().Interface(zf.Users, users).Msg("loading users from JSON")
log.Info().Msgf("access token TTL: %s", accessTTL)
port, err := strconv.Atoi(portStr)
if err != nil {
return err
}
mock, err := getMockOIDC(clientID, clientSecret)
mock, err := getMockOIDC(clientID, clientSecret, users)
if err != nil {
return err
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addrStr, port))
listener, err := new(net.ListenConfig).Listen(context.Background(), "tcp", fmt.Sprintf("%s:%d", addrStr, port))
if err != nil {
return err
}
@@ -74,20 +112,28 @@ func mockOIDC() error {
if err != nil {
return err
}
log.Info().Msgf("Mock OIDC server listening on %s", listener.Addr().String())
log.Info().Msgf("Issuer: %s", mock.Issuer())
log.Info().Msgf("mock OIDC server listening on %s", listener.Addr().String())
log.Info().Msgf("issuer: %s", mock.Issuer())
c := make(chan struct{})
<-c
return nil
}
func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) {
func getMockOIDC(clientID string, clientSecret string, users []mockoidc.MockUser) (*mockoidc.MockOIDC, error) {
keypair, err := mockoidc.NewKeypair(nil)
if err != nil {
return nil, err
}
userQueue := mockoidc.UserQueue{}
for _, user := range users {
userQueue.Push(&user)
}
mock := mockoidc.MockOIDC{
ClientID: clientID,
ClientSecret: clientSecret,
@@ -96,9 +142,20 @@ func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, erro
CodeChallengeMethodsSupported: []string{"plain", "S256"},
Keypair: keypair,
SessionStore: mockoidc.NewSessionStore(),
UserQueue: &mockoidc.UserQueue{},
UserQueue: &userQueue,
ErrorQueue: &mockoidc.ErrorQueue{},
}
_ = mock.AddMiddleware(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Info().Msgf("request: %+v", r)
h.ServeHTTP(w, r)
if r.Response != nil {
log.Info().Msgf("response: %+v", r.Response)
}
})
})
return &mock, nil
}
-243
View File
@@ -1,243 +0,0 @@
package cli
import (
"fmt"
survey "github.com/AlecAivazis/survey/v2"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
)
func init() {
rootCmd.AddCommand(namespaceCmd)
namespaceCmd.AddCommand(createNamespaceCmd)
namespaceCmd.AddCommand(listNamespacesCmd)
namespaceCmd.AddCommand(destroyNamespaceCmd)
namespaceCmd.AddCommand(renameNamespaceCmd)
}
const (
errMissingParameter = headscale.Error("missing parameters")
)
var namespaceCmd = &cobra.Command{
Use: "namespaces",
Short: "Manage the namespaces of Headscale",
Aliases: []string{"namespace", "ns", "user", "users"},
}
var createNamespaceCmd = &cobra.Command{
Use: "create NAME",
Short: "Creates a new namespace",
Aliases: []string{"c", "new"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespaceName := args[0]
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
log.Trace().Interface("client", client).Msg("Obtained gRPC client")
request := &v1.CreateNamespaceRequest{Name: namespaceName}
log.Trace().Interface("request", request).Msg("Sending CreateNamespace request")
response, err := client.CreateNamespace(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot create namespace: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Namespace, "Namespace created", output)
},
}
var destroyNamespaceCmd = &cobra.Command{
Use: "destroy NAME",
Short: "Destroys a namespace",
Aliases: []string{"delete"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespaceName := args[0]
request := &v1.GetNamespaceRequest{
Name: namespaceName,
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
_, err := client.GetNamespace(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error: %s", status.Convert(err).Message()),
output,
)
return
}
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
Message: fmt.Sprintf(
"Do you want to remove the namespace '%s' and any associated preauthkeys?",
namespaceName,
),
}
err := survey.AskOne(prompt, &confirm)
if err != nil {
return
}
}
if confirm || force {
request := &v1.DeleteNamespaceRequest{Name: namespaceName}
response, err := client.DeleteNamespace(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot destroy namespace: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response, "Namespace destroyed", output)
} else {
SuccessOutput(map[string]string{"Result": "Namespace not destroyed"}, "Namespace not destroyed", output)
}
},
}
var listNamespacesCmd = &cobra.Command{
Use: "list",
Short: "List all the namespaces",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ListNamespacesRequest{}
response, err := client.ListNamespaces(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get namespaces: %s", status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Namespaces, "", output)
return
}
tableData := pterm.TableData{{"ID", "Name", "Created"}}
for _, namespace := range response.GetNamespaces() {
tableData = append(
tableData,
[]string{
namespace.GetId(),
namespace.GetName(),
namespace.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
},
)
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
}
},
}
var renameNamespaceCmd = &cobra.Command{
Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a namespace",
Aliases: []string{"mv"},
Args: func(cmd *cobra.Command, args []string) error {
expectedArguments := 2
if len(args) < expectedArguments {
return errMissingParameter
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.RenameNamespaceRequest{
OldName: args[0],
NewName: args[1],
}
response, err := client.RenameNamespace(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot rename namespace: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Namespace, "Namespace renamed", output)
},
}
+345 -481
View File
@@ -1,586 +1,433 @@
package cli
import (
"context"
"fmt"
"log"
"net/http"
"net/netip"
"strconv"
"strings"
"time"
survey "github.com/AlecAivazis/survey/v2"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/pterm/pterm"
"github.com/samber/lo"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"tailscale.com/types/key"
)
func init() {
rootCmd.AddCommand(nodeCmd)
listNodesCmd.Flags().StringP("namespace", "n", "", "Filter by namespace")
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
listNodesCmd.Flags().StringP("user", "u", "", "Filter by user")
nodeCmd.AddCommand(listNodesCmd)
registerNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err := registerNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
listNodeRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
nodeCmd.AddCommand(listNodeRoutesCmd)
registerNodeCmd.Flags().StringP("user", "u", "", "User")
registerNodeCmd.Flags().StringP("key", "k", "", "Key")
err = registerNodeCmd.MarkFlagRequired("key")
if err != nil {
log.Fatalf(err.Error())
}
mustMarkRequired(registerNodeCmd, "user", "key")
nodeCmd.AddCommand(registerNodeCmd)
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = expireNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
expireNodeCmd.Flags().StringP("expiry", "e", "", "Set expire to (RFC3339 format, e.g. 2025-08-27T10:00:00Z), or leave empty to expire immediately.")
expireNodeCmd.Flags().BoolP("disable", "d", false, "Disable key expiry (node will never expire)")
mustMarkRequired(expireNodeCmd, "identifier")
nodeCmd.AddCommand(expireNodeCmd)
renameNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = renameNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
mustMarkRequired(renameNodeCmd, "identifier")
nodeCmd.AddCommand(renameNodeCmd)
deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = deleteNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
mustMarkRequired(deleteNodeCmd, "identifier")
nodeCmd.AddCommand(deleteNodeCmd)
moveNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = moveNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
moveNodeCmd.Flags().StringP("namespace", "n", "", "New namespace")
err = moveNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(moveNodeCmd)
tagCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = tagCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
tagCmd.Flags().
StringSliceP("tags", "t", []string{}, "List of tags to add to the node")
mustMarkRequired(tagCmd, "identifier")
tagCmd.Flags().StringSliceP("tags", "t", []string{}, "List of tags to add to the node")
nodeCmd.AddCommand(tagCmd)
approveRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
mustMarkRequired(approveRoutesCmd, "identifier")
approveRoutesCmd.Flags().StringSliceP("routes", "r", []string{}, `List of routes that will be approved (comma-separated, e.g. "10.0.0.0/8,192.168.0.0/24" or empty string to remove all approved routes)`)
nodeCmd.AddCommand(approveRoutesCmd)
nodeCmd.AddCommand(backfillNodeIPsCmd)
}
var nodeCmd = &cobra.Command{
Use: "nodes",
Short: "Manage the nodes of Headscale",
Aliases: []string{"node", "machine", "machines"},
Aliases: []string{"node"},
}
var registerNodeCmd = &cobra.Command{
Use: "register",
Short: "Registers a machine to your network",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
Use: "register",
Short: "Registers a node to your network",
Deprecated: "use 'headscale auth register --auth-id <id> --user <user>' instead",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
registrationID, _ := cmd.Flags().GetString("key")
params := &clientv1.RegisterNodeParams{
User: &user,
Key: &registrationID,
}
resp, err := client.RegisterNodeWithResponse(ctx, params)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
return fmt.Errorf("registering node: %w", err)
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
machineKey, err := cmd.Flags().GetString("key")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting node key from flag: %s", err),
output,
)
return
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
request := &v1.RegisterMachineRequest{
Key: machineKey,
Namespace: namespace,
}
node := resp.JSON200.Node
response, err := client.RegisterMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(
response.Machine,
fmt.Sprintf("Machine %s registered", response.Machine.GivenName), output)
},
return printOutput(
cmd,
node,
fmt.Sprintf("Node %s registered", node.GivenName),
)
}),
}
var listNodesCmd = &cobra.Command{
Use: "list",
Use: cmdList,
Short: "List nodes",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
Aliases: []string{"ls", cmdShow},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
params := &clientv1.ListNodesParams{}
if user != "" {
params.User = &user
}
resp, err := client.ListNodesWithResponse(ctx, params)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
return fmt.Errorf("listing nodes: %w", err)
}
showTags, err := cmd.Flags().GetBool("tags")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
nodes := resp.JSON200.Nodes
return printListOutput(cmd, nodes, func() error {
tableData, err := nodesToPtables(nodes)
if err != nil {
return fmt.Errorf("converting to table: %w", err)
}
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
})
}),
}
var listNodeRoutesCmd = &cobra.Command{
Use: "list-routes",
Short: "List routes available on nodes",
Aliases: []string{"lsr", "routes"},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
resp, err := client.ListNodesWithResponse(ctx, &clientv1.ListNodesParams{})
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting tags flag: %s", err), output)
return
return fmt.Errorf("listing nodes: %w", err)
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ListMachinesRequest{
Namespace: namespace,
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
response, err := client.ListMachines(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
nodes := resp.JSON200.Nodes
return
if identifier != 0 {
idStr := strconv.FormatUint(identifier, util.Base10)
for _, node := range nodes {
if node.Id == idStr {
nodes = []clientv1.Node{node}
break
}
}
}
if output != "" {
SuccessOutput(response.Machines, "", output)
nodes = lo.Filter(nodes, func(n clientv1.Node, _ int) bool {
return len(n.SubnetRoutes) > 0 || len(n.ApprovedRoutes) > 0 || len(n.AvailableRoutes) > 0
})
return
}
tableData, err := nodesToPtables(namespace, showTags, response.Machines)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
}
},
return printListOutput(cmd, nodes, func() error {
return pterm.DefaultTable.WithHasHeader().WithData(nodeRoutesToPtables(nodes)).Render()
})
}),
}
var expireNodeCmd = &cobra.Command{
Use: "expire",
Short: "Expire (log out) a machine in your network",
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
Aliases: []string{"logout", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
Use: cmdExpire,
Short: "Expire (log out) a node in your network",
Long: `Expiring a node will keep the node in the database and force it to reauthenticate.
identifier, err := cmd.Flags().GetUint64("identifier")
Use --disable to disable key expiry (node will never expire).`,
Aliases: []string{"logout", aliasExp, "e"},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
disableExpiry, _ := cmd.Flags().GetBool("disable")
nodeID := strconv.FormatUint(identifier, util.Base10)
// Handle disable expiry - node will never expire.
if disableExpiry {
disable := true
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
DisableExpiry: &disable,
})
if err != nil {
return fmt.Errorf("disabling node expiry: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node expiry disabled")
}
expiry, _ := cmd.Flags().GetString("expiry")
now := time.Now()
expiryTime := now
if expiry != "" {
var err error
expiryTime, err = time.Parse(time.RFC3339, expiry)
if err != nil {
return fmt.Errorf("parsing expiry time: %w", err)
}
}
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
Expiry: &expiryTime,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
return fmt.Errorf("expiring node: %w", err)
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ExpireMachineRequest{
MachineId: identifier,
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
response, err := client.ExpireMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot expire machine: %s\n",
status.Convert(err).Message(),
),
output,
)
node := resp.JSON200.Node
return
if now.Equal(expiryTime) || now.After(expiryTime) {
return printOutput(cmd, node, "Node expired")
}
SuccessOutput(response.Machine, "Machine expired", output)
},
return printOutput(cmd, node, "Node expiration updated")
}),
}
var renameNodeCmd = &cobra.Command{
Use: "rename NEW_NAME",
Short: "Renames a machine in your network",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
Short: "Renames a node in your network",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
newName := ""
if len(args) > 0 {
newName = args[0]
}
request := &v1.RenameMachineRequest{
MachineId: identifier,
NewName: newName,
}
response, err := client.RenameMachine(ctx, request)
resp, err := client.RenameNodeWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), newName)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot rename machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
return fmt.Errorf("renaming node: %w", err)
}
SuccessOutput(response.Machine, "Machine renamed", output)
},
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node renamed")
}),
}
var deleteNodeCmd = &cobra.Command{
Use: "delete",
Use: cmdDelete,
Short: "Delete a node",
Aliases: []string{"del"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
Aliases: []string{aliasDel},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
nodeID := strconv.FormatUint(identifier, util.Base10)
identifier, err := cmd.Flags().GetUint64("identifier")
getResponse, err := client.GetNodeWithResponse(ctx, nodeID)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
return fmt.Errorf("getting node: %w", err)
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
getRequest := &v1.GetMachineRequest{
MachineId: identifier,
if getResponse.StatusCode() != http.StatusOK {
return apiError(getResponse.StatusCode(), getResponse.ApplicationproblemJSONDefault)
}
getResponse, err := client.GetMachine(ctx, getRequest)
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.JSON200.Node.Name,
)) {
return printOutput(cmd, map[string]string{colResult: "Node not deleted"}, "Node not deleted")
}
deleteResponse, err := client.DeleteNodeWithResponse(ctx, nodeID)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error getting node node: %s",
status.Convert(err).Message(),
),
output,
)
return
return fmt.Errorf("deleting node: %w", err)
}
deleteRequest := &v1.DeleteMachineRequest{
MachineId: identifier,
if deleteResponse.StatusCode() != http.StatusOK {
return apiError(deleteResponse.StatusCode(), deleteResponse.ApplicationproblemJSONDefault)
}
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
Message: fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetMachine().Name,
),
}
err = survey.AskOne(prompt, &confirm)
return printOutput(
cmd,
map[string]string{colResult: "Node deleted"},
"Node deleted",
)
}),
}
var backfillNodeIPsCmd = &cobra.Command{
Use: "backfillips",
Short: "Backfill IPs missing from nodes",
Long: `
Backfill IPs can be used to add/remove IPs from nodes
based on the current configuration of Headscale.
If there are nodes that does not have IPv4 or IPv6
even if prefixes for both are configured in the config,
this command can be used to assign IPs of the sort to
all nodes that are missing.
If you remove IPv4 or IPv6 prefixes from the config,
it can be run to remove the IPs that should no longer
be assigned to nodes.`,
RunE: func(cmd *cobra.Command, args []string) error {
if !confirmAction(cmd, "Are you sure that you want to assign/remove IPs to/from nodes?") {
return nil
}
return withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
confirmed := true
resp, err := client.BackfillNodeIPsWithResponse(ctx, &clientv1.BackfillNodeIPsParams{
Confirmed: &confirmed,
})
if err != nil {
return
return fmt.Errorf("backfilling IPs: %w", err)
}
}
if confirm || force {
response, err := client.DeleteMachine(ctx, deleteRequest)
if output != "" {
SuccessOutput(response, "", output)
return
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error deleting node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(
map[string]string{"Result": "Node deleted"},
"Node deleted",
output,
)
} else {
SuccessOutput(map[string]string{"Result": "Node not deleted"}, "Node not deleted", output)
}
return printOutput(cmd, resp.JSON200, "Node IPs backfilled successfully")
})
},
}
var moveNodeCmd = &cobra.Command{
Use: "move",
Short: "Move node to another namespace",
Aliases: []string{"mv"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting namespace: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
getRequest := &v1.GetMachineRequest{
MachineId: identifier,
}
_, err = client.GetMachine(ctx, getRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error getting node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
moveRequest := &v1.MoveMachineRequest{
MachineId: identifier,
Namespace: namespace,
}
moveResponse, err := client.MoveMachine(ctx, moveRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error moving node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(moveResponse.Machine, "Node moved to another namespace", output)
},
}
func nodesToPtables(
currentNamespace string,
showTags bool,
machines []*v1.Machine,
) (pterm.TableData, error) {
func nodesToPtables(nodes []clientv1.Node) (pterm.TableData, error) {
tableHeader := []string{
"ID",
"Hostname",
"Name",
"MachineKey",
"NodeKey",
"Namespace",
"User",
"Tags",
"IP addresses",
"Ephemeral",
"Last seen",
"Online",
colExpiration,
"Connected",
"Expired",
}
if showTags {
tableHeader = append(tableHeader, []string{
"ForcedTags",
"InvalidTags",
"ValidTags",
}...)
}
tableData := pterm.TableData{tableHeader}
tableData := make(pterm.TableData, 1, 1+len(nodes))
tableData[0] = tableHeader
for _, machine := range machines {
var ephemeral bool
if machine.PreAuthKey != nil && machine.PreAuthKey.Ephemeral {
ephemeral = true
}
for _, node := range nodes {
// An absent pre-auth key decodes into a zero NodePreAuthKey, so guard
// on Id before reading its flags.
ephemeral := node.PreAuthKey.Id != "" && node.PreAuthKey.Ephemeral
var lastSeen time.Time
var lastSeenTime string
if machine.LastSeen != nil {
lastSeen = machine.LastSeen.AsTime()
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
if node.LastSeen != nil {
lastSeenTime = node.LastSeen.Format(HeadscaleDateTimeFormat)
}
var expiry time.Time
if machine.Expiry != nil {
expiry = machine.Expiry.AsTime()
expiryTime := "N/A"
if node.Expiry != nil {
expiryTime = node.Expiry.Format(HeadscaleDateTimeFormat)
}
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(node.MachineKey))
if err != nil {
machineKey = key.MachinePublic{}
}
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText(
[]byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)),
)
err = nodeKey.UnmarshalText([]byte(node.NodeKey))
if err != nil {
return nil, err
}
var online string
if lastSeen.After(
time.Now().Add(-5 * time.Minute),
) { // TODO: Find a better way to reliably show if online
online := pterm.LightRed("offline")
if node.Online {
online = pterm.LightGreen("online")
} else {
online = pterm.LightRed("offline")
}
var expired string
if expiry.IsZero() || expiry.After(time.Now()) {
expired = pterm.LightGreen("no")
} else {
expired := pterm.LightGreen("no")
if node.Expiry != nil && node.Expiry.Before(time.Now()) {
expired = pterm.LightRed("yes")
}
var forcedTags string
for _, tag := range machine.ForcedTags {
forcedTags += "," + tag
}
forcedTags = strings.TrimLeft(forcedTags, ",")
var invalidTags string
for _, tag := range machine.InvalidTags {
if !contains(machine.ForcedTags, tag) {
invalidTags += "," + pterm.LightRed(tag)
}
}
invalidTags = strings.TrimLeft(invalidTags, ",")
var validTags string
for _, tag := range machine.ValidTags {
if !contains(machine.ForcedTags, tag) {
validTags += "," + pterm.LightGreen(tag)
}
}
validTags = strings.TrimLeft(validTags, ",")
tags := strings.Join(node.Tags, "\n")
var namespace string
if currentNamespace == "" || (currentNamespace == machine.Namespace.Name) {
namespace = pterm.LightMagenta(machine.Namespace.Name)
} else {
// Shared into this namespace
namespace = pterm.LightYellow(machine.Namespace.Name)
var ipBuilder strings.Builder
for _, addr := range node.IpAddresses {
ip, err := netip.ParseAddr(addr)
if err == nil {
if ipBuilder.Len() > 0 {
ipBuilder.WriteString("\n")
}
ipBuilder.WriteString(ip.String())
}
}
var IPV4Address string
var IPV6Address string
for _, addr := range machine.IpAddresses {
if netip.MustParseAddr(addr).Is4() {
IPV4Address = addr
} else {
IPV6Address = addr
}
}
ipAddresses := ipBuilder.String()
nodeData := []string{
strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
machine.GetGivenName(),
node.Id,
node.Name,
node.GivenName,
machineKey.ShortString(),
nodeKey.ShortString(),
namespace,
strings.Join([]string{IPV4Address, IPV6Address}, ", "),
node.User.Name,
tags,
ipAddresses,
strconv.FormatBool(ephemeral),
lastSeenTime,
expiryTime,
online,
expired,
}
if showTags {
nodeData = append(nodeData, []string{forcedTags, invalidTags, validTags}...)
}
tableData = append(
tableData,
nodeData,
@@ -590,60 +437,77 @@ func nodesToPtables(
return tableData, nil
}
func nodeRoutesToPtables(
nodes []clientv1.Node,
) pterm.TableData {
tableHeader := []string{
"ID",
"Hostname",
"Approved",
"Available",
"Serving (Primary)",
}
tableData := make(pterm.TableData, 1, 1+len(nodes))
tableData[0] = tableHeader
for _, node := range nodes {
nodeData := []string{
node.Id,
node.GivenName,
strings.Join(node.ApprovedRoutes, "\n"),
strings.Join(node.AvailableRoutes, "\n"),
strings.Join(node.SubnetRoutes, "\n"),
}
tableData = append(
tableData,
nodeData,
)
}
return tableData
}
var tagCmd = &cobra.Command{
Use: "tag",
Short: "Manage the tags of a node",
Aliases: []string{"tags", "t"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
tagsToSet, _ := cmd.Flags().GetStringSlice("tags")
// retrieve flags from CLI
identifier, err := cmd.Flags().GetUint64("identifier")
resp, err := client.SetTagsWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetTagsJSONRequestBody{
Tags: &tagsToSet,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
tagsToSet, err := cmd.Flags().GetStringSlice("tags")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error retrieving list of tags to add to machine, %v", err),
output,
)
return
return fmt.Errorf("setting tags: %w", err)
}
// Sending tags to machine
request := &v1.SetTagsRequest{
MachineId: identifier,
Tags: tagsToSet,
}
resp, err := client.SetTags(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error while sending tags to headscale: %s", err),
output,
)
return
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
if resp != nil {
SuccessOutput(
resp.GetMachine(),
"Machine updated",
output,
)
}
},
return printOutput(cmd, resp.JSON200.Node, "Node updated")
}),
}
var approveRoutesCmd = &cobra.Command{
Use: "approve-routes",
Short: "Manage the approved routes of a node",
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
routes, _ := cmd.Flags().GetStringSlice("routes")
resp, err := client.SetApprovedRoutesWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetApprovedRoutesJSONRequestBody{
Routes: &routes,
})
if err != nil {
return fmt.Errorf("setting approved routes: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node updated")
}),
}
+271
View File
@@ -0,0 +1,271 @@
package cli
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
clientv2 "github.com/juanfont/headscale/gen/client/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/cobra"
)
// oauthTailnet is the single Headscale tailnet the v2 API addresses as "-".
const oauthTailnet = "-"
func init() {
rootCmd.AddCommand(oauthClientsCmd)
oauthClientsCmd.AddCommand(listOAuthClientsCmd)
createOAuthClientCmd.Flags().
StringArrayP("scope", "s", nil, "Scope the client's tokens are granted (repeatable): auth_keys, oauth_keys, devices:core, devices:routes, policy_file, feature_settings (each with a :read variant), or all/all:read")
createOAuthClientCmd.Flags().
StringArrayP("tag", "t", nil, "Tag the client's tokens may assign to devices (repeatable), e.g. tag:k8s-operator")
createOAuthClientCmd.Flags().StringP("description", "d", "", "Human-readable description")
oauthClientsCmd.AddCommand(createOAuthClientCmd)
deleteOAuthClientCmd.Flags().StringP("id", "i", "", "OAuth client id")
oauthClientsCmd.AddCommand(deleteOAuthClientCmd)
}
var oauthClientsCmd = &cobra.Command{
Use: "oauth-clients",
Short: "Manage OAuth clients",
Aliases: []string{"oauth-client", "oauthclients", "oauthclient", "oauth"},
}
var createOAuthClientCmd = &cobra.Command{
Use: "create",
Short: "Create an OAuth client",
Long: `Create a general-purpose OAuth client. It authenticates with the OAuth 2.0
client-credentials grant and mints short-lived, scope-limited access tokens.
The wire format is compatible with Tailscale tooling (the Terraform provider,
the Kubernetes operator, tscli, ...), so those can drive Headscale unchanged.
The client secret is shown ONCE on creation and cannot be retrieved again; if
you lose it, delete the client and create a new one.
Scopes gate what the client's tokens may do; tags are the device tags those
tokens may assign and are required when the scopes include devices:core or
auth_keys.`,
Aliases: []string{"c", cmdNew},
RunE: func(cmd *cobra.Command, _ []string) error {
scopes, _ := cmd.Flags().GetStringArray("scope")
tags, _ := cmd.Flags().GetStringArray("tag")
description, _ := cmd.Flags().GetString("description")
if len(scopes) == 0 {
return fmt.Errorf("at least one --scope is required: %w", errMissingParameter)
}
ctx, client, cancel, err := newV2Client()
if err != nil {
return err
}
defer cancel()
keyType := "client"
resp, err := client.CreateKeyWithResponse(ctx, oauthTailnet, clientv2.CreateKeyRequest{
KeyType: &keyType,
Scopes: &scopes,
Tags: &tags,
Description: &description,
})
if err != nil {
return fmt.Errorf("creating oauth client: %w", err)
}
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
if err != nil {
return err
}
key := resp.JSON200
return printOutput(cmd, key,
fmt.Sprintf("OAuth client %s created.\nSecret (shown once, store it now): %s", key.Id, ptrStr(key.Key)))
},
}
var listOAuthClientsCmd = &cobra.Command{
Use: cmdList,
Short: "List OAuth clients",
Aliases: []string{"ls", cmdShow},
RunE: func(cmd *cobra.Command, _ []string) error {
ctx, client, cancel, err := newV2Client()
if err != nil {
return err
}
defer cancel()
resp, err := client.ListKeysWithResponse(ctx, oauthTailnet, nil)
if err != nil {
return fmt.Errorf("listing oauth clients: %w", err)
}
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
if err != nil {
return err
}
// The keys endpoint is multiplexed; keep only OAuth clients.
clients := make([]clientv2.Key, 0, len(resp.JSON200.Keys))
for _, k := range resp.JSON200.Keys {
if k.KeyType == "client" {
clients = append(clients, k)
}
}
return printListOutput(cmd, clients, func() error {
rows := make([][]string, 0, len(clients))
for _, c := range clients {
rows = append(rows, []string{
c.Id,
strings.Join(ptrStrs(c.Scopes), ","),
strings.Join(ptrStrs(c.Tags), ","),
ptrStr(c.Description),
c.Created.Format(HeadscaleDateTimeFormat),
})
}
return renderTable([]string{"ID", "Scopes", "Tags", "Description", colCreated}, rows)
})
},
}
var deleteOAuthClientCmd = &cobra.Command{
Use: cmdDelete,
Short: "Delete an OAuth client",
Aliases: []string{"remove", aliasDel},
RunE: func(cmd *cobra.Command, _ []string) error {
id, _ := cmd.Flags().GetString("id")
if id == "" {
return fmt.Errorf("--id is required: %w", errMissingParameter)
}
ctx, client, cancel, err := newV2Client()
if err != nil {
return err
}
defer cancel()
resp, err := client.DeleteKeyWithResponse(ctx, oauthTailnet, id)
if err != nil {
return fmt.Errorf("deleting oauth client: %w", err)
}
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
if err != nil {
return err
}
return printOutput(cmd, map[string]string{"id": id}, "OAuth client "+id+" deleted")
},
}
// newV2Client builds a generated v2 API client, selecting the transport the same
// way the v1 client does: over the local unix socket it is unauthenticated
// (local trust); a remote address injects the configured API key as a bearer
// token.
func newV2Client() (context.Context, *clientv2.ClientWithResponses, context.CancelFunc, error) {
cfg, err := types.LoadCLIConfig()
if err != nil {
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
if cfg.CLI.Address == "" {
socketPath := cfg.UnixSocket
httpClient := &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialHeadscaleSocket(ctx, socketPath)
},
}}
client, err := clientv2.NewClientWithResponses("http://local", clientv2.WithHTTPClient(httpClient))
if err != nil {
cancel()
return nil, nil, nil, err
}
return ctx, client, cancel, nil
}
if cfg.CLI.APIKey == "" {
cancel()
return nil, nil, nil, errAPIKeyNotSet
}
transport := &http.Transport{}
if cfg.CLI.Insecure {
//nolint:gosec // intentionally honouring the insecure flag
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
apiKey := cfg.CLI.APIKey
client, err := clientv2.NewClientWithResponses(
clientBaseURL(cfg.CLI.Address),
clientv2.WithHTTPClient(&http.Client{Transport: transport}),
clientv2.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
req.Header.Set("Authorization", "Bearer "+apiKey)
return nil
}),
)
if err != nil {
cancel()
return nil, nil, nil, err
}
return ctx, client, cancel, nil
}
// v2Error turns a non-2xx v2 response into an error. The v2 API emits the
// Tailscale error body ({"message":...}) rather than RFC 7807, so it reads the
// "message" field instead of the generated problem+json types.
func v2Error(status int, body []byte) error {
if status >= http.StatusOK && status < http.StatusMultipleChoices {
return nil
}
var e struct {
Message string `json:"message"`
}
if json.Unmarshal(body, &e) == nil && e.Message != "" {
//nolint:err113 // surfacing the server's message
return fmt.Errorf("api error (%d): %s", status, e.Message)
}
//nolint:err113 // surfacing the server's body
return fmt.Errorf("api error (%d): %s", status, strings.TrimSpace(string(body)))
}
func ptrStr(s *string) string {
if s == nil {
return ""
}
return *s
}
func ptrStrs(s *[]string) []string {
if s == nil {
return nil
}
return *s
}
+260
View File
@@ -0,0 +1,260 @@
package cli
import (
"context"
"errors"
"fmt"
"net/http"
"os"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/cobra"
"tailscale.com/types/views"
)
const (
bypassFlag = "bypass-server-and-access-database-directly" //nolint:gosec // not a credential
)
var errAborted = errors.New("command aborted by user")
// bypassDatabase opens the database directly, bypassing the running server.
// The caller must close the returned handle.
func bypassDatabase() (*db.HSDatabase, error) {
cfg, err := types.LoadServerConfig()
if err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
d, err := db.NewHeadscaleDatabase(cfg)
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
return d, nil
}
// openBypassDB confirms the destructive bypass action and opens the database
// directly. The caller is responsible for closing the returned handle.
func openBypassDB(cmd *cobra.Command) (*db.HSDatabase, error) {
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
return nil, errAborted
}
return bypassDatabase()
}
func init() {
rootCmd.AddCommand(policyCmd)
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
policyCmd.AddCommand(getPolicy)
setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
mustMarkRequired(setPolicy, "file")
policyCmd.AddCommand(setPolicy)
checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no running server required) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
mustMarkRequired(checkPolicy, "file")
policyCmd.AddCommand(checkPolicy)
}
var policyCmd = &cobra.Command{
Use: "policy",
Short: "Manage the Headscale ACL Policy",
}
var getPolicy = &cobra.Command{
Use: "get",
Short: "Print the current ACL Policy",
Aliases: []string{cmdShow, "view", "fetch"},
RunE: func(cmd *cobra.Command, args []string) error {
var policyData string
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
d, err := openBypassDB(cmd)
if err != nil {
return err
}
defer d.Close()
pol, err := d.GetPolicy()
if err != nil {
return fmt.Errorf("loading policy from database: %w", err)
}
policyData = pol.Data
} else {
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
resp, err := client.GetPolicyWithResponse(ctx)
if err != nil {
return fmt.Errorf("loading ACL policy: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
policyData = resp.JSON200.Policy
return nil
})
if err != nil {
return err
}
}
// This does not pass output format as we don't support yaml, json or
// json-line output for this command. It is HuJSON already.
fmt.Println(policyData)
return nil
},
}
var setPolicy = &cobra.Command{
Use: "set",
Short: "Updates the ACL Policy",
Long: `
Updates the existing ACL Policy with the provided policy. The policy must be a valid HuJSON object.
This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`,
Aliases: []string{"put", "update"},
RunE: func(cmd *cobra.Command, args []string) error {
policyPath, _ := cmd.Flags().GetString("file")
policyBytes, err := os.ReadFile(policyPath)
if err != nil {
return fmt.Errorf("reading policy file: %w", err)
}
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
d, err := openBypassDB(cmd)
if err != nil {
return err
}
defer d.Close()
users, err := d.ListUsers(nil)
if err != nil {
return fmt.Errorf("loading users for policy validation: %w", err)
}
_, err = policy.NewPolicyManager(policyBytes, users, views.Slice[types.NodeView]{})
if err != nil {
return fmt.Errorf("parsing policy file: %w", err)
}
_, err = d.SetPolicy(string(policyBytes))
if err != nil {
return fmt.Errorf("setting ACL policy: %w", err)
}
} else {
policyStr := string(policyBytes)
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
resp, err := client.SetPolicyWithResponse(ctx, clientv1.SetPolicyJSONRequestBody{
Policy: &policyStr,
})
if err != nil {
return fmt.Errorf("setting ACL policy: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return nil
})
if err != nil {
return err
}
}
fmt.Println("Policy updated.")
return nil
},
}
var checkPolicy = &cobra.Command{
Use: "check",
Short: "Check the Policy file for errors",
Long: `
Check validates the policy against the server's live users and nodes,
running any "tests" or "sshTests" block. By default the command calls a
running headscale over its API; pass --` + bypassFlag + ` to
open the database directly when headscale is not running.`,
RunE: func(cmd *cobra.Command, args []string) error {
policyPath, _ := cmd.Flags().GetString("file")
policyBytes, err := os.ReadFile(policyPath)
if err != nil {
return fmt.Errorf("reading policy file: %w", err)
}
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
d, err := openBypassDB(cmd)
if err != nil {
return err
}
defer d.Close()
users, err := d.ListUsers(nil)
if err != nil {
return fmt.Errorf("loading users: %w", err)
}
nodes, err := d.ListNodes()
if err != nil {
return fmt.Errorf("loading nodes: %w", err)
}
// [policy.NewPolicyManager] validates structure and user references
// but intentionally skips test evaluation (boot path).
// [policy.PolicyManager.SetPolicy] is the user-write boundary and is what runs the
// tests and sshTests blocks.
pm, err := policy.NewPolicyManager(policyBytes, users, nodes.ViewSlice())
if err != nil {
return fmt.Errorf("parsing policy file: %w", err)
}
_, err = pm.SetPolicy(policyBytes)
if err != nil {
return err
}
fmt.Println("Policy is valid")
return nil
}
policyStr := string(policyBytes)
err = withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
resp, err := client.CheckPolicyWithResponse(ctx, clientv1.CheckPolicyJSONRequestBody{
Policy: &policyStr,
})
if err != nil {
return err
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return nil
})
if err != nil {
return err
}
fmt.Println("Policy is valid")
return nil
},
}
+125 -180
View File
@@ -1,17 +1,15 @@
package cli
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/prometheus/common/model"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
@@ -20,14 +18,10 @@ const (
func init() {
rootCmd.AddCommand(preauthkeysCmd)
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
err := preauthkeysCmd.MarkPersistentFlagRequired("namespace")
if err != nil {
log.Fatal().Err(err).Msg("")
}
preauthkeysCmd.AddCommand(listPreAuthKeys)
preauthkeysCmd.AddCommand(createPreAuthKeyCmd)
preauthkeysCmd.AddCommand(expirePreAuthKeyCmd)
preauthkeysCmd.AddCommand(deletePreAuthKeyCmd)
createPreAuthKeyCmd.PersistentFlags().
Bool("reusable", false, "Make the preauthkey reusable")
createPreAuthKeyCmd.PersistentFlags().
@@ -36,6 +30,9 @@ func init() {
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
createPreAuthKeyCmd.Flags().
StringSlice("tags", []string{}, "Tags to automatically assign to node")
createPreAuthKeyCmd.PersistentFlags().Uint64P("user", "u", 0, "User identifier (ID)")
expirePreAuthKeyCmd.PersistentFlags().Uint64P("id", "i", 0, "Authkey ID")
deletePreAuthKeyCmd.PersistentFlags().Uint64P("id", "i", 0, "Authkey ID")
}
var preauthkeysCmd = &cobra.Command{
@@ -45,213 +42,161 @@ var preauthkeysCmd = &cobra.Command{
}
var listPreAuthKeys = &cobra.Command{
Use: "list",
Short: "List the preauthkeys for this namespace",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
Use: cmdList,
Short: "List all preauthkeys",
Aliases: []string{"ls", cmdShow},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
resp, err := client.ListPreAuthKeysWithResponse(ctx)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
return fmt.Errorf("listing preauthkeys: %w", err)
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ListPreAuthKeysRequest{
Namespace: namespace,
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
response, err := client.ListPreAuthKeys(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting the list of keys: %s", err),
output,
)
preAuthKeys := resp.JSON200.PreAuthKeys
return
}
return printListOutput(cmd, preAuthKeys, func() error {
rows := make([][]string, 0, len(preAuthKeys))
for _, key := range preAuthKeys {
expiration := ColourTime(key.Expiration)
if output != "" {
SuccessOutput(response.PreAuthKeys, "", output)
owner := "-"
return
}
switch {
case len(key.AclTags) > 0:
owner = strings.Join(key.AclTags, "\n")
case key.User.Id != "":
owner = key.User.Name
}
tableData := pterm.TableData{
{
rows = append(rows, []string{
key.Id,
key.Key,
strconv.FormatBool(key.Reusable),
strconv.FormatBool(key.Ephemeral),
strconv.FormatBool(key.Used),
expiration,
key.CreatedAt.Format(HeadscaleDateTimeFormat),
owner,
})
}
return renderTable([]string{
"ID",
"Key",
"Key/Prefix",
"Reusable",
"Ephemeral",
"Used",
"Expiration",
"Created",
"Tags",
},
}
for _, key := range response.PreAuthKeys {
expiration := "-"
if key.GetExpiration() != nil {
expiration = ColourTime(key.Expiration.AsTime())
}
var reusable string
if key.GetEphemeral() {
reusable = "N/A"
} else {
reusable = fmt.Sprintf("%v", key.GetReusable())
}
aclTags := ""
for _, tag := range key.AclTags {
aclTags += "," + tag
}
aclTags = strings.TrimLeft(aclTags, ",")
tableData = append(tableData, []string{
key.GetId(),
key.GetKey(),
reusable,
strconv.FormatBool(key.GetEphemeral()),
strconv.FormatBool(key.GetUsed()),
expiration,
key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
aclTags,
})
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
}
},
colExpiration,
colCreated,
"Owner",
}, rows)
})
}),
}
var createPreAuthKeyCmd = &cobra.Command{
Use: "create",
Short: "Creates a new preauthkey in the specified namespace",
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
}
Short: "Creates a new preauthkey",
Aliases: []string{"c", cmdNew},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetUint64("user")
reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
tags, _ := cmd.Flags().GetStringSlice("tags")
log.Trace().
Bool("reusable", reusable).
Bool("ephemeral", ephemeral).
Str("namespace", namespace).
Msg("Preparing to create preauthkey")
request := &v1.CreatePreAuthKeyRequest{
Namespace: namespace,
Reusable: reusable,
Ephemeral: ephemeral,
AclTags: tags,
}
durationStr, _ := cmd.Flags().GetString("expiration")
duration, err := model.ParseDuration(durationStr)
expiryTime, err := expirationFromFlag(cmd)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Could not parse duration: %s\n", err),
output,
)
return
return err
}
expiration := time.Now().UTC().Add(time.Duration(duration))
userStr := strconv.FormatUint(user, util.Base10)
log.Trace().
Dur("expiration", time.Duration(duration)).
Msg("expiration has been set")
request := clientv1.CreatePreAuthKeyJSONRequestBody{
User: &userStr,
Reusable: &reusable,
Ephemeral: &ephemeral,
AclTags: &tags,
Expiration: &expiryTime,
}
request.Expiration = timestamppb.New(expiration)
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
response, err := client.CreatePreAuthKey(ctx, request)
resp, err := client.CreatePreAuthKeyWithResponse(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err),
output,
)
return
return fmt.Errorf("creating preauthkey: %w", err)
}
SuccessOutput(response.PreAuthKey, response.PreAuthKey.Key, output)
},
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
preAuthKey := resp.JSON200.PreAuthKey
return printOutput(cmd, preAuthKey, preAuthKey.Key)
}),
}
// preAuthKeyID reads the required --id flag for preauthkey commands.
func preAuthKeyID(cmd *cobra.Command) (uint64, error) {
id, _ := cmd.Flags().GetUint64("id")
if id == 0 {
return 0, fmt.Errorf("missing --id parameter: %w", errMissingParameter)
}
return id, nil
}
var expirePreAuthKeyCmd = &cobra.Command{
Use: "expire KEY",
Use: cmdExpire,
Short: "Expire a preauthkey",
Aliases: []string{"revoke", "exp", "e"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
Aliases: []string{"revoke", aliasExp, "e"},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, err := preAuthKeyID(cmd)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
return
return err
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
idStr := strconv.FormatUint(id, util.Base10)
request := &v1.ExpirePreAuthKeyRequest{
Namespace: namespace,
Key: args[0],
}
response, err := client.ExpirePreAuthKey(ctx, request)
resp, err := client.ExpirePreAuthKeyWithResponse(ctx, clientv1.ExpirePreAuthKeyJSONRequestBody{
Id: &idStr,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err),
output,
)
return
return fmt.Errorf("expiring preauthkey: %w", err)
}
SuccessOutput(response, "Key expired", output)
},
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key expired")
}),
}
var deletePreAuthKeyCmd = &cobra.Command{
Use: cmdDelete,
Short: "Delete a preauthkey",
Aliases: []string{aliasDel, "rm", "d"},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, err := preAuthKeyID(cmd)
if err != nil {
return err
}
idStr := strconv.FormatUint(id, util.Base10)
resp, err := client.DeletePreAuthKeyWithResponse(ctx, &clientv1.DeletePreAuthKeyParams{
Id: &idStr,
})
if err != nil {
return fmt.Errorf("deleting preauthkey: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key deleted")
}),
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
func ColourTime(date time.Time) string {
dateStr := date.Format("2006-01-02 15:04:05")
dateStr := date.Format(HeadscaleDateTimeFormat)
if date.After(time.Now()) {
dateStr = pterm.LightGreen(dateStr)
+68 -26
View File
@@ -1,21 +1,24 @@
package cli
import (
"fmt"
"os"
"runtime"
"slices"
"strings"
"github.com/juanfont/headscale"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tcnksm/go-latest"
)
var cfgFile string = ""
func init() {
if len(os.Args) > 1 && (os.Args[1] == "version" || os.Args[1] == "mockoidc") {
if len(os.Args) > 1 &&
(os.Args[1] == "version" || os.Args[1] == "mockoidc" || os.Args[1] == "completion") {
return
}
@@ -26,63 +29,98 @@ func init() {
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
rootCmd.PersistentFlags().
Bool("force", false, "Disable prompts and forces the execution")
// Re-enable usage output only for flag-parsing errors; runtime errors
// from [cobra.Command.RunE] should never dump usage text.
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
cmd.SilenceUsage = false
return err
})
}
func initConfig() {
if cfgFile == "" {
cfgFile = os.Getenv("HEADSCALE_CONFIG")
}
if cfgFile != "" {
err := headscale.LoadConfig(cfgFile, true)
err := types.LoadConfig(cfgFile, true)
if err != nil {
log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile)
log.Fatal().Caller().Err(err).Msgf("error loading config file %s", cfgFile)
}
} else {
err := headscale.LoadConfig("", false)
err := types.LoadConfig("", false)
if err != nil {
log.Fatal().Caller().Err(err).Msgf("Error loading config")
log.Fatal().Caller().Err(err).Msgf("error loading config")
}
}
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
log.Fatal().Caller().Err(err)
}
machineOutput := hasMachineOutputFlag()
machineOutput := HasMachineOutputFlag()
zerolog.SetGlobalLevel(cfg.Log.Level)
// If the user has requested a "machine" readable format,
// If the user has requested a "node" readable format,
// then disable login so the output remains valid.
if machineOutput {
zerolog.SetGlobalLevel(zerolog.Disabled)
}
if cfg.Log.Format == headscale.JSONLogFormat {
logFormat := viper.GetString("log.format")
if logFormat == types.JSONLogFormat {
log.Logger = log.Output(os.Stdout)
}
if !cfg.DisableUpdateCheck && !machineOutput {
disableUpdateCheck := viper.GetBool("disable_check_updates")
if !disableUpdateCheck && !machineOutput {
versionInfo := types.GetVersionInfo()
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
Version != "dev" {
!versionInfo.Dirty {
githubTag := &latest.GithubTag{
Owner: "juanfont",
Repository: "headscale",
Owner: "juanfont",
Repository: "headscale",
TagFilterFunc: filterPreReleasesIfStable(func() string { return versionInfo.Version }),
}
res, err := latest.Check(githubTag, Version)
res, err := latest.Check(githubTag, versionInfo.Version)
if err == nil && res.Outdated {
//nolint
fmt.Printf(
log.Warn().Msgf(
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
res.Current,
Version,
versionInfo.Version,
)
}
}
}
}
var prereleases = []string{"alpha", "beta", "rc", "dev"}
func isPreReleaseVersion(version string) bool {
return slices.ContainsFunc(prereleases, func(unstable string) bool {
return strings.Contains(version, unstable)
})
}
// filterPreReleasesIfStable returns a function that filters out
// pre-release tags if the current version is stable.
// If the current version is a pre-release, it does not filter anything.
// versionFunc is a function that returns the current version string, it is
// a func for testability.
func filterPreReleasesIfStable(versionFunc func() string) func(string) bool {
return func(tag string) bool {
version := versionFunc()
// If we are on a pre-release version, then we do not filter anything
// as we want to recommend the user the latest pre-release.
if isPreReleaseVersion(version) {
return false
}
// If we are on a stable release, filter out pre-releases.
return isPreReleaseVersion(tag)
}
}
var rootCmd = &cobra.Command{
Use: "headscale",
Short: "headscale - a Tailscale control server",
@@ -90,11 +128,15 @@ var rootCmd = &cobra.Command{
headscale is an open source implementation of the Tailscale control server
https://github.com/juanfont/headscale`,
SilenceErrors: true,
SilenceUsage: true,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
cmd, err := rootCmd.ExecuteC()
if err != nil {
outputFormat, _ := cmd.Flags().GetString("output")
printError(err, outputFormat)
os.Exit(1)
}
}
+308
View File
@@ -0,0 +1,308 @@
package cli
import (
"testing"
)
const (
v23 = "0.23.0"
v23Alpha1 = "0.23.0-alpha.1"
v23Beta1 = "0.23.0-beta.1"
v23RC1 = "0.23.0-rc.1"
v23Dev = "0.23.0-dev"
v231 = "0.23.1"
v24Alpha1Tag = "v0.24.0-alpha.1"
v24RCTag = "v0.24.0-rc.1"
v24Tag = "v0.24.0"
)
func TestFilterPreReleasesIfStable(t *testing.T) {
tests := []struct {
name string
currentVersion string
tag string
expectedFilter bool
description string
}{
{
name: "stable version filters alpha tag",
currentVersion: v23,
tag: v24Alpha1Tag,
expectedFilter: true,
description: "When on stable release, alpha tags should be filtered",
},
{
name: "stable version filters beta tag",
currentVersion: v23,
tag: "v0.24.0-beta.2",
expectedFilter: true,
description: "When on stable release, beta tags should be filtered",
},
{
name: "stable version filters rc tag",
currentVersion: v23,
tag: v24RCTag,
expectedFilter: true,
description: "When on stable release, rc tags should be filtered",
},
{
name: "stable version allows stable tag",
currentVersion: v23,
tag: v24Tag,
expectedFilter: false,
description: "When on stable release, stable tags should not be filtered",
},
{
name: "alpha version allows alpha tag",
currentVersion: v23Alpha1,
tag: "v0.24.0-alpha.2",
expectedFilter: false,
description: "When on alpha release, alpha tags should not be filtered",
},
{
name: "alpha version allows beta tag",
currentVersion: v23Alpha1,
tag: "v0.24.0-beta.1",
expectedFilter: false,
description: "When on alpha release, beta tags should not be filtered",
},
{
name: "alpha version allows rc tag",
currentVersion: v23Alpha1,
tag: v24RCTag,
expectedFilter: false,
description: "When on alpha release, rc tags should not be filtered",
},
{
name: "alpha version allows stable tag",
currentVersion: v23Alpha1,
tag: v24Tag,
expectedFilter: false,
description: "When on alpha release, stable tags should not be filtered",
},
{
name: "beta version allows alpha tag",
currentVersion: v23Beta1,
tag: v24Alpha1Tag,
expectedFilter: false,
description: "When on beta release, alpha tags should not be filtered",
},
{
name: "beta version allows beta tag",
currentVersion: "0.23.0-beta.2",
tag: "v0.24.0-beta.3",
expectedFilter: false,
description: "When on beta release, beta tags should not be filtered",
},
{
name: "beta version allows rc tag",
currentVersion: v23Beta1,
tag: v24RCTag,
expectedFilter: false,
description: "When on beta release, rc tags should not be filtered",
},
{
name: "beta version allows stable tag",
currentVersion: v23Beta1,
tag: v24Tag,
expectedFilter: false,
description: "When on beta release, stable tags should not be filtered",
},
{
name: "rc version allows alpha tag",
currentVersion: v23RC1,
tag: v24Alpha1Tag,
expectedFilter: false,
description: "When on rc release, alpha tags should not be filtered",
},
{
name: "rc version allows beta tag",
currentVersion: v23RC1,
tag: "v0.24.0-beta.1",
expectedFilter: false,
description: "When on rc release, beta tags should not be filtered",
},
{
name: "rc version allows rc tag",
currentVersion: "0.23.0-rc.2",
tag: "v0.24.0-rc.3",
expectedFilter: false,
description: "When on rc release, rc tags should not be filtered",
},
{
name: "rc version allows stable tag",
currentVersion: v23RC1,
tag: v24Tag,
expectedFilter: false,
description: "When on rc release, stable tags should not be filtered",
},
{
name: "stable version with patch filters alpha",
currentVersion: v231,
tag: v24Alpha1Tag,
expectedFilter: true,
description: "Stable version with patch number should filter alpha tags",
},
{
name: "stable version with patch allows stable",
currentVersion: v231,
tag: v24Tag,
expectedFilter: false,
description: "Stable version with patch number should allow stable tags",
},
{
name: "tag with alpha substring in version number",
currentVersion: v23,
tag: "v1.0.0-alpha.1",
expectedFilter: true,
description: "Tags with alpha in version string should be filtered on stable",
},
{
name: "tag with beta substring in version number",
currentVersion: v23,
tag: "v1.0.0-beta.1",
expectedFilter: true,
description: "Tags with beta in version string should be filtered on stable",
},
{
name: "tag with rc substring in version number",
currentVersion: v23,
tag: "v1.0.0-rc.1",
expectedFilter: true,
description: "Tags with rc in version string should be filtered on stable",
},
{
name: "empty tag on stable version",
currentVersion: v23,
tag: "",
expectedFilter: false,
description: "Empty tags should not be filtered",
},
{
name: "dev version allows all tags",
currentVersion: v23Dev,
tag: v24Alpha1Tag,
expectedFilter: false,
description: "Dev versions should not filter any tags (pre-release allows all)",
},
{
name: "stable version filters dev tag",
currentVersion: v23,
tag: "v0.24.0-dev",
expectedFilter: true,
description: "When on stable release, dev tags should be filtered",
},
{
name: "dev version allows dev tag",
currentVersion: v23Dev,
tag: "v0.24.0-dev.1",
expectedFilter: false,
description: "When on dev release, dev tags should not be filtered",
},
{
name: "dev version allows stable tag",
currentVersion: v23Dev,
tag: v24Tag,
expectedFilter: false,
description: "When on dev release, stable tags should not be filtered",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filterPreReleasesIfStable(func() string { return tt.currentVersion })(tt.tag)
if result != tt.expectedFilter {
t.Errorf(
"%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
tt.name,
result,
tt.expectedFilter,
tt.description,
tt.currentVersion,
tt.tag,
)
}
})
}
}
func TestIsPreReleaseVersion(t *testing.T) {
tests := []struct {
name string
version string
expected bool
description string
}{
{
name: "stable version",
version: v23,
expected: false,
description: "Stable version should not be pre-release",
},
{
name: "alpha version",
version: v23Alpha1,
expected: true,
description: "Alpha version should be pre-release",
},
{
name: "beta version",
version: v23Beta1,
expected: true,
description: "Beta version should be pre-release",
},
{
name: "rc version",
version: v23RC1,
expected: true,
description: "RC version should be pre-release",
},
{
name: "version with alpha substring",
version: "0.23.0-alphabetical",
expected: true,
description: "Version containing 'alpha' should be pre-release",
},
{
name: "version with beta substring",
version: "0.23.0-betamax",
expected: true,
description: "Version containing 'beta' should be pre-release",
},
{
name: "dev version",
version: v23Dev,
expected: true,
description: "Dev version should be pre-release",
},
{
name: "empty version",
version: "",
expected: false,
description: "Empty version should not be pre-release",
},
{
name: "version with patch number",
version: v231,
expected: false,
description: "Stable version with patch should not be pre-release",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isPreReleaseVersion(tt.version)
if result != tt.expected {
t.Errorf(
"%s: got %v, want %v\nDescription: %s\nVersion: %s",
tt.name,
result,
tt.expected,
tt.description,
tt.version,
)
}
})
}
}
-233
View File
@@ -1,233 +0,0 @@
package cli
import (
"fmt"
"log"
"strconv"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
)
func init() {
rootCmd.AddCommand(routesCmd)
listRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err := listRoutesCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(listRoutesCmd)
enableRouteCmd.Flags().
StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
enableRouteCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
enableRouteCmd.Flags().BoolP("all", "a", false, "All routes from host")
err = enableRouteCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
routesCmd.AddCommand(enableRouteCmd)
nodeCmd.AddCommand(routesCmd)
}
var routesCmd = &cobra.Command{
Use: "routes",
Short: "Manage the routes of Headscale",
Aliases: []string{"r", "route"},
}
var listRoutesCmd = &cobra.Command{
Use: "list",
Short: "List routes advertised and enabled by a given node",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine id from flag: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.GetMachineRouteRequest{
MachineId: machineID,
}
response, err := client.GetMachineRoute(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
tableData := routesToPtables(response.Routes)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
}
},
}
var enableRouteCmd = &cobra.Command{
Use: "enable",
Short: "Set the enabled routes for a given node",
Long: `This command will take a list of routes that will _replace_
the current set of routes on a given node.
If you would like to disable a route, simply run the command again, but
omit the route you do not want to enable.
`,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting machine id from flag: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
var routes []string
isAll, _ := cmd.Flags().GetBool("all")
if isAll {
response, err := client.GetMachineRoute(ctx, &v1.GetMachineRouteRequest{
MachineId: machineID,
})
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot get machine routes: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
routes = response.GetRoutes().GetAdvertisedRoutes()
} else {
routes, err = cmd.Flags().GetStringSlice("route")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting routes from flag: %s", err),
output,
)
return
}
}
request := &v1.EnableMachineRoutesRequest{
MachineId: machineID,
Routes: routes,
}
response, err := client.EnableMachineRoutes(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
return
}
tableData := routesToPtables(response.Routes)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
return
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
return
}
},
}
// routesToPtables converts the list of routes to a nice table.
func routesToPtables(routes *v1.Routes) pterm.TableData {
tableData := pterm.TableData{{"Route", "Enabled"}}
for _, route := range routes.GetAdvertisedRoutes() {
enabled := isStringInSlice(routes.EnabledRoutes, route)
tableData = append(tableData, []string{route, strconv.FormatBool(enabled)})
}
return tableData
}
func isStringInSlice(strs []string, s string) bool {
for _, s2 := range strs {
if s == s2 {
return true
}
}
return false
}
+37
View File
@@ -0,0 +1,37 @@
package cli
import (
"errors"
"fmt"
"net/http"
"github.com/spf13/cobra"
"github.com/tailscale/squibble"
)
func init() {
rootCmd.AddCommand(serveCmd)
}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Launches the headscale server",
RunE: func(cmd *cobra.Command, args []string) error {
app, err := newHeadscaleServerWithConfig()
if err != nil {
if squibbleErr, ok := errors.AsType[squibble.ValidationError](err); ok {
fmt.Printf("SQLite schema failed to validate:\n")
fmt.Println(squibbleErr.Diff)
}
return fmt.Errorf("initializing: %w", err)
}
err = app.Serve()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("headscale ran into an error and had to shut down: %w", err)
}
return nil
},
}
-29
View File
@@ -1,29 +0,0 @@
package cli
import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(serveCmd)
}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Launches the headscale server",
Args: func(cmd *cobra.Command, args []string) error {
return nil
},
Run: func(cmd *cobra.Command, args []string) {
app, err := getHeadscaleApp()
if err != nil {
log.Fatal().Caller().Err(err).Msg("Error initializing")
}
err = app.Serve()
if err != nil {
log.Fatal().Caller().Err(err).Msg("Error starting server")
}
},
}
+23
View File
@@ -0,0 +1,23 @@
package cli
// Shared CLI vocabulary used across multiple command definitions in this
// package. Centralising the strings prevents goconst drift and ensures a
// typo in a subcommand name fails to compile rather than silently
// breaking the binding.
const (
// Subcommand verbs (cobra Use field).
cmdList = "list"
cmdShow = "show"
cmdNew = "new"
cmdDelete = "delete"
cmdExpire = "expire"
// Subcommand aliases.
aliasDel = "del"
aliasExp = "exp"
// Output table column headers and printOutput map keys.
colResult = "Result"
colCreated = "Created"
colExpiration = "Expiration"
)
+262
View File
@@ -0,0 +1,262 @@
package cli
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// CLI user errors.
var (
errFlagRequired = errors.New("--name or --identifier flag is required")
errMultipleUsersMatch = errors.New("multiple users match query, specify an ID")
)
func usernameAndIDFlag(cmd *cobra.Command) {
cmd.Flags().Int64P("identifier", "i", -1, "User identifier (ID)")
cmd.Flags().StringP("name", "n", "", "Username")
}
// usernameAndIDFromFlag returns the username and ID from the flags of the command.
func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
username, _ := cmd.Flags().GetString("name")
identifier, _ := cmd.Flags().GetInt64("identifier")
if username == "" && identifier < 0 {
return 0, "", errFlagRequired
}
// Normalise unset/negative identifiers to 0 so the uint64
// conversion does not produce a bogus large value.
identifier = max(identifier, 0)
return uint64(identifier), username, nil //nolint:gosec // identifier is clamped to >= 0 above
}
// resolveSingleUser resolves exactly one user from the --name/--id flags,
// returning the raw flag id and the matched user.
func resolveSingleUser(
ctx context.Context,
client *clientv1.ClientWithResponses,
cmd *cobra.Command,
) (uint64, *clientv1.User, error) {
id, username, err := usernameAndIDFromFlag(cmd)
if err != nil {
return 0, nil, err
}
params := &clientv1.ListUsersParams{}
if username != "" {
params.Name = &username
}
if id != 0 {
idStr := strconv.FormatUint(id, util.Base10)
params.Id = &idStr
}
resp, err := client.ListUsersWithResponse(ctx, params)
if err != nil {
return 0, nil, fmt.Errorf("listing users: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return 0, nil, apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
users := resp.JSON200.Users
if len(users) != 1 {
return 0, nil, errMultipleUsersMatch
}
return id, &users[0], nil
}
func init() {
rootCmd.AddCommand(userCmd)
userCmd.AddCommand(createUserCmd)
createUserCmd.Flags().StringP("display-name", "d", "", "Display name")
createUserCmd.Flags().StringP("email", "e", "", "Email")
createUserCmd.Flags().StringP("picture-url", "p", "", "Profile picture URL")
userCmd.AddCommand(listUsersCmd)
usernameAndIDFlag(listUsersCmd)
listUsersCmd.Flags().StringP("email", "e", "", "Email")
userCmd.AddCommand(destroyUserCmd)
usernameAndIDFlag(destroyUserCmd)
userCmd.AddCommand(renameUserCmd)
usernameAndIDFlag(renameUserCmd)
renameUserCmd.Flags().StringP("new-name", "r", "", "New username")
mustMarkRequired(renameUserCmd, "new-name")
}
var userCmd = &cobra.Command{
Use: "users",
Short: "Manage the users of Headscale",
Aliases: []string{"user"},
}
var createUserCmd = &cobra.Command{
Use: "create NAME",
Short: "Creates a new user",
Aliases: []string{"c", cmdNew},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
}
return nil
},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
userName := args[0]
log.Trace().Interface(zf.Client, client).Msg("obtained API client")
request := clientv1.CreateUserJSONRequestBody{Name: &userName}
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
request.DisplayName = &displayName
}
if email, _ := cmd.Flags().GetString("email"); email != "" {
request.Email = &email
}
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
if _, err := url.Parse(pictureURL); err != nil { //nolint:noinlineerr
return fmt.Errorf("invalid picture URL: %w", err)
}
request.PictureUrl = &pictureURL
}
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
resp, err := client.CreateUserWithResponse(ctx, request)
if err != nil {
return fmt.Errorf("creating user: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.User, "User created")
}),
}
var destroyUserCmd = &cobra.Command{
Use: "destroy --identifier ID or --name NAME",
Short: "Destroys a user",
Aliases: []string{cmdDelete},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
_, user, err := resolveSingleUser(ctx, client, cmd)
if err != nil {
return err
}
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the user %q (%s) and any associated preauthkeys?",
user.Name, user.Id,
)) {
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
}
resp, err := client.DeleteUserWithResponse(ctx, user.Id)
if err != nil {
return fmt.Errorf("destroying user: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "User destroyed")
}),
}
var listUsersCmd = &cobra.Command{
Use: cmdList,
Short: "List all the users",
Aliases: []string{"ls", cmdShow},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
params := &clientv1.ListUsersParams{}
id, _ := cmd.Flags().GetInt64("identifier")
username, _ := cmd.Flags().GetString("name")
email, _ := cmd.Flags().GetString("email")
// filter by one param at most
switch {
case id > 0:
idStr := strconv.FormatInt(id, util.Base10)
params.Id = &idStr
case username != "":
params.Name = &username
case email != "":
params.Email = &email
}
resp, err := client.ListUsersWithResponse(ctx, params)
if err != nil {
return fmt.Errorf("listing users: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
users := resp.JSON200.Users
return printListOutput(cmd, users, func() error {
rows := make([][]string, 0, len(users))
for _, user := range users {
rows = append(
rows,
[]string{
user.Id,
user.DisplayName,
user.Name,
user.Email,
user.CreatedAt.Format(HeadscaleDateTimeFormat),
},
)
}
return renderTable([]string{"ID", "Name", "Username", "Email", colCreated}, rows)
})
}),
}
var renameUserCmd = &cobra.Command{
Use: "rename",
Short: "Renames a user",
Aliases: []string{"mv"},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, _, err := resolveSingleUser(ctx, client, cmd)
if err != nil {
return err
}
newName, _ := cmd.Flags().GetString("new-name")
resp, err := client.RenameUserWithResponse(ctx, strconv.FormatUint(id, util.Base10), newName)
if err != nil {
return fmt.Errorf("renaming user: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.User, "User renamed")
}),
}
+321 -140
View File
@@ -4,62 +4,146 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"reflect"
"slices"
"strings"
"time"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/cenkalti/backoff/v5"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/prometheus/common/model"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v2"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
const (
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
SocketWritePermissions = 0o666
outputFormatJSON = "json"
outputFormatJSONLine = "json-line"
outputFormatYAML = "yaml"
)
func getHeadscaleApp() (*headscale.Headscale, error) {
cfg, err := headscale.GetHeadscaleConfig()
var (
errAPIKeyNotSet = errors.New("HEADSCALE_CLI_API_KEY environment variable needs to be set")
errMissingParameter = errors.New("missing parameters")
errResponseStatus = errors.New("unexpected response status")
)
// apiError turns a non-2xx response into an error, surfacing the server's
// RFC7807 problem detail. detail holds the operation context and errors[] the
// wrapped cause (e.g. "name is too long"); both are joined so the server's
// message text is not lost.
func apiError(statusCode int, problem *clientv1.ErrorModel) error {
if problem == nil {
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
}
parts := make([]string, 0, 2)
if problem.Detail != nil && *problem.Detail != "" {
parts = append(parts, *problem.Detail)
}
if problem.Errors != nil {
for _, e := range *problem.Errors {
if e.Message != nil && *e.Message != "" {
parts = append(parts, *e.Message)
}
}
}
if len(parts) == 0 && problem.Title != nil && *problem.Title != "" {
parts = append(parts, *problem.Title)
}
if len(parts) == 0 {
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
}
return fmt.Errorf("%w: %s", errResponseStatus, strings.Join(parts, ": "))
}
// mustMarkRequired marks the named flags as required, panicking on an unknown
// flag. Only called from init(), where a failure is a programming error.
func mustMarkRequired(cmd *cobra.Command, names ...string) {
for _, n := range names {
err := cmd.MarkFlagRequired(n)
if err != nil {
panic(fmt.Sprintf("marking flag %q required on %q: %v", n, cmd.Name(), err))
}
}
}
func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
cfg, err := types.LoadServerConfig()
if err != nil {
return nil, fmt.Errorf(
"failed to load configuration while creating headscale instance: %w",
"loading configuration: %w",
err,
)
}
app, err := headscale.NewHeadscale(cfg)
app, err := hscontrol.NewHeadscale(cfg)
if err != nil {
return nil, err
}
// We are doing this here, as in the future could be cool to have it also hot-reload
if cfg.ACL.PolicyPath != "" {
aclPath := headscale.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
err = app.LoadACLPolicy(aclPath)
if err != nil {
log.Fatal().
Str("path", aclPath).
Err(err).
Msg("Could not load the ACL policy")
}
return nil, fmt.Errorf("creating new headscale: %w", err)
}
return app, nil
}
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
cfg, err := headscale.GetHeadscaleConfig()
// clientRunE wraps a [cobra.Command.RunE] func, injecting a ready API client
// and a context whose timeout/cancel the wrapper owns.
func clientRunE(
fn func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error,
) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
return fn(ctx, client, cmd, args)
}
}
// withClient runs fn with an API client. For commands that branch on a flag
// before talking to the server, where clientRunE's whole-RunE wrapping does
// not fit.
func withClient(
fn func(ctx context.Context, client *clientv1.ClientWithResponses) error,
) error {
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
log.Fatal().
Err(err).
Caller().
Msgf("Failed to load configuration")
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
return fn(ctx, client)
}
// newHeadscaleCLIWithConfig builds an HTTP client for the Headscale v1 API.
//
// When cfg.CLI.Address is unset the CLI is assumed to run on the server host
// and talks to the unix socket over HTTP without authentication (local trust).
// Otherwise it talks to the remote TCP address over HTTPS and injects the
// configured API key as a bearer token.
func newHeadscaleCLIWithConfig() (context.Context, *clientv1.ClientWithResponses, context.CancelFunc, error) {
cfg, err := types.LoadCLIConfig()
if err != nil {
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
}
log.Debug().
@@ -68,152 +152,249 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
grpcOptions := []grpc.DialOption{
grpc.WithBlock(),
}
address := cfg.CLI.Address
// If the address is not set, we assume that we are on the server hosting headscale.
// If the address is not set, we assume that we are on the server hosting [hscontrol].
if address == "" {
log.Debug().
Str("socket", cfg.UnixSocket).
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
address = cfg.UnixSocket
// Try to give the user better feedback if we cannot write to the headscale
// socket.
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
client, err := newSocketClient(cfg.UnixSocket)
if err != nil {
if os.IsPermission(err) {
log.Fatal().
Err(err).
Str("socket", cfg.UnixSocket).
Msgf("Unable to read/write to headscale socket, do you have the correct permissions?")
}
}
socket.Close()
cancel()
grpcOptions = append(
grpcOptions,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(headscale.GrpcSocketDialer),
)
} else {
// If we are not connecting to a local server, require an API key for authentication
apiKey := cfg.CLI.APIKey
if apiKey == "" {
log.Fatal().Caller().Msgf("HEADSCALE_CLI_API_KEY environment variable needs to be set.")
return nil, nil, nil, err
}
grpcOptions = append(grpcOptions,
grpc.WithPerRPCCredentials(tokenAuth{
token: apiKey,
}),
)
if cfg.CLI.Insecure {
tlsConfig := &tls.Config{
// turn of gosec as we are intentionally setting
// insecure.
//nolint:gosec
InsecureSkipVerify: true,
}
log.Trace().Caller().Str(zf.Address, cfg.UnixSocket).Msg("connecting via unix socket")
grpcOptions = append(grpcOptions,
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
)
} else {
grpcOptions = append(grpcOptions,
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
)
}
return ctx, client, cancel, nil
}
log.Trace().Caller().Str("address", address).Msg("Connecting via gRPC")
conn, err := grpc.DialContext(ctx, address, grpcOptions...)
// Remote connections require an API key for authentication.
apiKey := cfg.CLI.APIKey
if apiKey == "" {
cancel()
return nil, nil, nil, errAPIKeyNotSet
}
client, err := newRemoteClient(address, apiKey, cfg.CLI.Insecure)
if err != nil {
log.Fatal().Caller().Err(err).Msgf("Could not connect: %v", err)
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
cancel()
return nil, nil, nil, err
}
client := v1.NewHeadscaleServiceClient(conn)
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via HTTPS")
return ctx, client, conn, cancel
return ctx, client, cancel, nil
}
func SuccessOutput(result interface{}, override string, outputFormat string) {
var jsonBytes []byte
var err error
// newSocketClient builds an API client that dials the local unix socket. The
// base-URL host is irrelevant; the custom dialer routes every request to the
// socket.
func newSocketClient(socketPath string) (*clientv1.ClientWithResponses, error) {
// Probe for a clearer permission error up front. [os.OpenFile] on a unix
// socket returns ENXIO on Linux (expected); only permission errors are
// actionable. The real connection goes through [net.Dial].
socket, err := os.OpenFile(socketPath, os.O_WRONLY, SocketWritePermissions) //nolint
if err != nil {
if os.IsPermission(err) {
return nil, fmt.Errorf(
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
socketPath,
err,
)
}
} else {
socket.Close()
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialHeadscaleSocket(ctx, socketPath)
},
},
}
return clientv1.NewClientWithResponses(
"http://local",
clientv1.WithHTTPClient(httpClient),
)
}
// dialHeadscaleSocket connects to the unix socket, retrying until it appears or
// ctx (the CLI timeout) expires. The socket is created late in startup (after
// noise key, database, migrations), so a command run right after the server
// starts can race its creation; retrying preserves the old gRPC client's
// blocking-dial tolerance rather than failing on a not-yet-present socket.
func dialHeadscaleSocket(ctx context.Context, socketPath string) (net.Conn, error) {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 50 * time.Millisecond
b.MaxInterval = 1 * time.Second
return backoff.Retry(ctx, func() (net.Conn, error) {
return util.SocketDialer(ctx, socketPath)
}, backoff.WithBackOff(b))
}
// clientBaseURL turns a configured CLI address into a client base URL. A bare
// host[:port] (the historical form) defaults to https; an address that already
// carries a scheme is used as-is, so an explicit http:// or https:// is honoured
// rather than doubled into https://https://...
func clientBaseURL(address string) string {
if strings.Contains(address, "://") {
return address
}
return "https://" + address
}
// newRemoteClient builds an API client for a remote Headscale over HTTPS,
// honouring insecure (skip TLS verification) and injecting the API key as a
// bearer token on every request.
func newRemoteClient(address, apiKey string, insecure bool) (*clientv1.ClientWithResponses, error) {
transport := &http.Transport{}
if insecure {
transport.TLSClientConfig = &tls.Config{
// turn off gosec as we are intentionally setting insecure.
//nolint:gosec
InsecureSkipVerify: true,
}
}
httpClient := &http.Client{Transport: transport}
return clientv1.NewClientWithResponses(
clientBaseURL(address),
clientv1.WithHTTPClient(httpClient),
clientv1.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
req.Header.Set("Authorization", "Bearer "+apiKey)
return nil
}),
)
}
// formatOutput serialises result into the requested format. For the
// default (empty) format the human-readable override string is returned.
func formatOutput(result any, override string, outputFormat string) (string, error) {
switch outputFormat {
case "json":
jsonBytes, err = json.MarshalIndent(result, "", "\t")
case outputFormatJSON:
b, err := json.MarshalIndent(result, "", "\t")
if err != nil {
log.Fatal().Err(err)
return "", fmt.Errorf("marshalling JSON output: %w", err)
}
case "json-line":
jsonBytes, err = json.Marshal(result)
return string(b), nil
case outputFormatJSONLine:
b, err := json.Marshal(result)
if err != nil {
log.Fatal().Err(err)
return "", fmt.Errorf("marshalling JSON-line output: %w", err)
}
case "yaml":
jsonBytes, err = yaml.Marshal(result)
return string(b), nil
case outputFormatYAML:
b, err := yaml.Marshal(result)
if err != nil {
log.Fatal().Err(err)
return "", fmt.Errorf("marshalling YAML output: %w", err)
}
return string(b), nil
default:
//nolint
fmt.Println(override)
return
return override, nil
}
//nolint
fmt.Println(string(jsonBytes))
}
func ErrorOutput(errResult error, override string, outputFormat string) {
// printOutput formats result and writes it to stdout. It reads the --output
// flag from cmd to decide the serialisation format.
func printOutput(cmd *cobra.Command, result any, override string) error {
format, _ := cmd.Flags().GetString("output")
out, err := formatOutput(result, override, format)
if err != nil {
return err
}
fmt.Println(out)
return nil
}
// expirationFromFlag parses the --expiration flag as a Prometheus-style
// duration (e.g. "90d", "1h") and returns an absolute time.
func expirationFromFlag(cmd *cobra.Command) (time.Time, error) {
durationStr, _ := cmd.Flags().GetString("expiration")
duration, err := model.ParseDuration(durationStr)
if err != nil {
return time.Time{}, fmt.Errorf("parsing duration: %w", err)
}
return time.Now().UTC().Add(time.Duration(duration)), nil
}
// confirmAction returns true when the user confirms a prompt, or when
// --force is set. Callers decide what to do when it returns false.
func confirmAction(cmd *cobra.Command, prompt string) bool {
force, _ := cmd.Flags().GetBool("force")
if force {
return true
}
return util.YesNo(prompt)
}
// renderTable prints a human-readable pterm table with the given header row
// and data rows, using the shared header styling.
func renderTable(header []string, rows [][]string) error {
tableData := make(pterm.TableData, 0, 1+len(rows))
tableData = append(tableData, header)
tableData = append(tableData, rows...)
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
}
// printListOutput checks the --output flag: when a machine-readable format is
// requested it serialises data as JSON/YAML; otherwise it calls the render
// callback to produce the human-readable pterm table.
func printListOutput(
cmd *cobra.Command,
data any,
renderTable func() error,
) error {
format, _ := cmd.Flags().GetString("output")
if format != "" {
return printOutput(cmd, data, "")
}
return renderTable()
}
// printError writes err to stderr, formatting it as JSON/YAML when the
// --output flag requests machine-readable output. Used exclusively by
// [Execute] so that every error surfaces in the format the caller asked for.
func printError(err error, outputFormat string) {
type errOutput struct {
Error string `json:"error"`
}
SuccessOutput(errOutput{errResult.Error()}, override, outputFormat)
}
if outputFormat == "" {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
func HasMachineOutputFlag() bool {
for _, arg := range os.Args {
if arg == "json" || arg == "json-line" || arg == "yaml" {
return true
}
return
}
return false
// formatOutput cannot fail here: errOutput is a single string field.
out, _ := formatOutput(errOutput{Error: err.Error()}, "", outputFormat)
fmt.Fprintf(os.Stderr, "%s\n", out)
}
type tokenAuth struct {
token string
}
// Return value is mapped to request headers.
func (t tokenAuth) GetRequestMetadata(
ctx context.Context,
in ...string,
) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + t.token,
}, nil
}
func (tokenAuth) RequireTransportSecurity() bool {
return true
}
func contains[T string](ts []T, t T) bool {
for _, v := range ts {
if reflect.DeepEqual(v, t) {
return true
}
}
return false
func hasMachineOutputFlag() bool {
return slices.ContainsFunc(os.Args, func(arg string) bool {
return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML
})
}

Some files were not shown because too many files have changed in this diff Show More