ListPeers now filters named peers against the recipient's adjacency, so
a node the policy hides is never delivered.
Updates #3417
(cherry picked from commit 4c6a2dff52)
A write that cannot move visibility carries the previous adjacency
forward; policy and user changes rebuild it explicitly.
Updates #3417
(cherry picked from commit e3c4c81b18)
Adjacency becomes immutable, so a snapshot can resolve peers through its
own fresh views instead of storing them.
Updates #3417
(cherry picked from commit 95ba787417)
Fixes#3408
0.29's BuildPeerMap returns node views, not node IDs, so the peer map
assertion compares IDs read off the views.
(cherry picked from commit e48bc46cc6)
Matches SaaS; Apple clients hide the exit-node list without a suggestion.
Fixes#3415
0.29's tailscale has no tailcfg/nodecap package, so the constant keeps its
tailcfg name here and in the test.
(cherry picked from commit c604874dba)
Line numbers drift on every upstream bump.
0.29's tailscale has no tailcfg/nodecap package, so the constants keep their
tailcfg names here.
(cherry picked from commit 06fa3075da)
acmeLogger drained and closed the body of every ACME error response before
handing the response back to golang.org/x/crypto/acme. The client parses that
body to classify errors, so badNonce was no longer recognised: isBadNonce
returned false, clearNonces was never called and Client.post treated the 400 as
non-retriable.
One badNonce reply therefore stops certificate renewal for good. autocert
retries every 30-60 minutes, each attempt reuses a nonce stored from the
previous failed response, that nonce has already expired, and the loop repeats
until the process is restarted or the certificate expires.
Restore the body with a fresh reader after logging it.
(cherry picked from commit 7472e98f6c)
The filter lived in ACL.UnmarshalJSON, so grants, ssh and nodeAttrs still
hit RejectUnknownMembers. Strip the members in the HuJSON AST instead, at
the single decode entrypoint. Grant "app" payloads are left untouched.
Fixes#3479
(cherry picked from commit 5401edb6a8)
Online now requires a live session and an unexpired key, derived in one
place by Node.ShouldBeOnline so every writer agrees what online means.
Fixes#3470
Connect keeps 0.29's NodeOnlineFor peer patch; only the not-online branch
is new here. The updateChanges hunk is dropped: 0.29 has no caller for it.
(cherry picked from commit 80c863b15a)
Going offline changes nothing the policy reads, so the row write skips
the policy refresh and peers get only the offline patch.
Updates #3417
(cherry picked from commit dfe0d3f2e5)
Each request is reduced to the narrowest change it justifies, so a
keepalive or endpoint bump no longer resends the whole node to peers.
Updates #3417
0.29 pins an older tailscale where Hostinfo.NetInfo.PreferredDERP is an int,
so the DERPRegionID types are int here.
(cherry picked from commit 59f3ff12a7)
An empty change carries no work for any recipient, so it never becomes
a pending entry. Adds headscale_mapper_changes_dropped_total.
Updates #3417
(cherry picked from commit 286f1d5a12)
The interstitial was the body of /oidc/callback, the URL carrying the
single-use code, so any reload re-entered the spent exchange. Redirect to
GET /register/confirm/{auth_id}, also missing from the route table.
0.29 lacks the authPathURL helper from main, so it is added here.
(cherry picked from commit 6d377b5348)
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.
(cherry picked from commit 622e08f5e6)
A bare 404 is indistinguishable from any other map-path error to a Tailscale
client: it retries forever, still logged in. Only a self node with a past
KeyExpiry reaches NeedsLogin. Also skip the reconnect grace wait, which a
deleted node can never satisfy.
Fixes#3410
(cherry picked from commit b1fb6ed2e6)
It fills PeersRemoved without deleting anything. The batcher now tears down
the session behind every removed id, so a caller would kill a live poll.
Updates #3410
(cherry picked from commit 42bf00523a)
Dropping the batcher entry left serveLongPoll streaming to a node that no
longer exists: Close ranges b.nodes and can no longer reach it, so shutdown
blocks and the client keeps polling instead of re-authenticating.
Updates #3410
(cherry picked from commit 0b69e844f5)
DNS names are case-insensitive, but clients match extra records against
the lowercased query name, so records with mixed-case names (for example
"Printer.fritz.box" in an extra_records_path file) never resolved and
queries fell through to the global nameserver.
Normalize record names to lowercase where the records enter the tailcfg
DNS config, covering both dns.extra_records and extra_records_path.
Fixes#2782
(cherry picked from commit 95ba1f0566)
Covers every change shape under four policy shapes, plus connect churn. The
zero-matcher shape is the gap: buildTailPeers skips ReduceNodes there, so the
peer lookup is the only self filter left.
(cherry picked from commit 8995d8a558)
ListPeers with explicit IDs filtered every node, not every peer, so a change
batch naming the recipient returned it as its own peer. db.ListPeers keeps
this out with `id <> nodeID`; the NodeStore rewrite dropped it.
(cherry picked from commit d9aebf472d)
After Remove/Rename, the extra-records filewatcher retried with
context.Background and the default 15-minute backoff budget, so Close
could not stop Run. Cancel that retry when closeCh closes. If the file
is still missing after the budget, watch the parent directory so a later
recreate is seen.
Close the fsnotify watcher on NewExtraRecordsManager error paths after
NewWatcher succeeds.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
(cherry picked from commit a48a42baf4)
Serve stored a Background context for tailsql and called
context.Done() during signal shutdown. Done only returns the
done channel and does not cancel, so tailsql never stopped
when Headscale shut down.
Create a cancellable child of the Serve context, run
`runTailSQLService` in the listener errgroup so its error is
surfaced, and call the cancel func on shutdown - tailsql
unblocks on ctx.Done.
(cherry picked from commit a9f80d8802)
Fix the inverted HTTP metrics skip condition, which caused the collector to ignore all requests except OPTIONS.
Apply the correction to both the main and Noise routers so regular HTTP traffic is included in http_requests_total and http_request_duration_seconds.
(cherry picked from commit 63ce8f2295)
The instructions are duplicated between the docs and the HTML page
headscale serves at /apple. Reorder the tvOS steps there to match, so
the VPN configuration is installed before the alternate coordination
server URL is set.
(cherry picked from commit efe947507b)
waitForFollowup selected on ctx.Done() and the verdict channel with equal
priority; when both were ready, select picked at random and discarded a
successful registration as a spurious 401 timeout. Check for a completed
verdict first, race the deadline only if none is ready.
Fixes#3385
(cherry picked from commit d28a6b111a)
With node.ephemeral.inactivity_timeout set, ephemeral nodes are
usually deleted after they go offline, but under reconnect churn some
departed nodes stayed in the node list as disconnected indefinitely
until removed manually or until Headscale restarted.
Ephemeral cleanup is timer-based via EphemeralGarbageCollector, not a
periodic LastSeen scan. serveLongPoll cancelled any pending GC timer
at the very start of a long-poll attempt and only rescheduled on a
clean disconnect after Connect. If a reconnect cancelled the timer and
then failed before Connect (for example an UpdateNodeFromMapRequest
error), the deferred cleanup saw connectGen == 0 and returned without
Schedule. The node remained offline with no deletion timer and no
reconciler to recover it.
Cancel the ephemeral GC timer only after a successful Connect, so a
failed reconnect leaves an already-armed inactivity timer intact.
Successful reconnects still cancel GC once the node is online, and a
later disconnect reschedules as before.
Add TestFailedReconnectDoesNotCancelEphemeralGC to lock in the
ordering, plus IsScheduled and DeleteNodeFromStoreForTest helpers for
the test.
Fixes#3382
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit cfd845cb53)
Re-authenticating a tagged node with --advertise-tags checked the tag-owned
node, not the authenticating user, so every tag was rejected.
Fixes#3374
(cherry picked from commit 6275e3a356)
Re-registering a tagged node with a different key discarded the new key's
tags and left a stale auth-key reference; retag on key change and persist it.
Fixes#3370
(cherry picked from commit fc16cc6905)
Tagged nodes never expire, but handleLogout stamped a past expiry on them,
leaving them stuck expired and unable to re-authenticate.
Fixes#3371
(cherry picked from commit 1ed5693fa4)
/key handed out the Noise public key for any v>=39, a floor unrelated
to the handshake's capver.MinSupportedCapabilityVersion. Reject below
the supported floor, matching /ts2021, and drop the stale constant.
Fixes#3380
(cherry picked from commit 5b6e1e17be)
waitForFollowup returns nodeToRegisterResponse for a completed
registration without checking that the Noise session polling for the
result was started with the machine key that opened the registration.
That response carries the registering user's User and Login, so the auth
ID in the followup URL is the only thing protecting it.
handleRegister and handleLogout both call machineKeyMismatch before
handing back a node, so this is the one path of the three that does not.
The key is already available: HandleNodeFromAuthPath resolves the node
from the MachineKey cached in RegistrationData, so on the normal path the
node and the session agree and the check is a no-op.
The auth ID is 96 bits of randomness and is not guessable, so this is not
reachable by brute force. It is logged at info level when a registration
is created, which makes log access the realistic way to obtain one.
The existing followup_registration_success case built its node with
CreateNodeForTest, which picks a random machine key that no real
registration would produce. Set the registering machine key so the
fixture matches the production path.
Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
(cherry picked from commit 0ce3356b89)
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
(cherry picked from commit fc6f216b61)
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
(cherry picked from commit 4946d1c88d)
A valid label can still overflow 255 chars under a long base_domain; gate
RenameNode with the new types.ValidateGivenName.
Updates #3346
(cherry picked from commit c497612c99)
A bare return sent an empty 200 the client read as "unexpected EOF" and
retried forever; emit a real error instead.
Updates #3346
(cherry picked from commit 4e4512c4b7)
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
(cherry picked from commit 08956d51a4)