As on Tailscale SaaS, the node's own entry keeps Hostname, RequestTags,
RoutableIPs and Services; peers get Hostname, OS, Services without
peerapi-dns-proxy, SSH host keys and AppConnector, and resend on those.
handleRegister, the OIDC auth path and SetNodeExpiry refuse it. Expiries
are stored in UTC, which SQLite can read back, and loaded in UTC, as
PostgreSQL returns the server's zone. The v1 API answers 400.
json.Marshal refuses a year outside [0,9999], so one stored expiry
failed every map with that node. TailNode and key patches send UTC, the
builder drops what fails and the boot scan names the node.
Only a key that speaks for a tagged node re-authenticates it: a tagged
key, or one of the user it was tagged from, which takes it back with a
new node key. Another user's key or login adds that user's own device.
Registration approval runs on the node the writer replaces. A policy
reload approves nothing, and a map request only when its announced
routes change, so neither re-grants a route an admin revoked.
Every destination overlaps /0, so any rule offered a peer's exit routes.
They now need a rule to the internet, the test exit-node visibility
uses; via steering defers them to a regular grant only on that test.
Such a route names a node, not a subnet: it stays approved and listed
but no viewer gets it. Via inclusions skip the election, so the rule
sits on everything RoutesForPeer returns.
A router that reconnects joins as a standby; one that kept its session
takes its routes back once available again, as on Tailscale. Both the
global and the via election use it, in place of anti-flap stickiness.
Peers keep the routes of an offline or expired router, as on Tailscale.
The previous primary keeps the prefix, else the lowest-ID approver;
never one the prober declared unreachable.
Every write inserted a row and nothing deleted one, while every reader
takes only the newest. Writers store and install under one lock, so an
uncommitted insert cannot outlive the newer row that pruned the rest.
Writers validated by installing the policy live, so a store failure
left a refused or unstored policy enforced. Validate on a throwaway
manager, store, then reload from the stored row.
Labels under taken hostnames, case-only renames, hostname changes, long
hostnames and a Service's name; API, peer and self per step, and State
labels as a restart would load them.
The expirable LRU ran its sweeper forever, one goroutine per State. Purge
first so pending waiters get ErrRegistrationExpired instead of hanging.
The OIDC state cache is closed at server shutdown for the same reason.
"Foo" and "foo" could share one DNS name; collisions now ignore case.
A rename never resent the node its own DNS config, so CertDomains lagged.
Updates #2137
The SetPolicy API handler calls State.SetPolicy, which already rebuilds the peer map when the policy changed, and then ReloadPolicy, which rebuilt it again even when nothing changed. So every policy write built the peer map twice.
ReloadPolicy now rebuilds only when the policy really changed. A test checks one build per changed write and none for an unchanged reload.
Part of #3549