mapper: keep an expired node authorised in its own entry

Tailscale SaaS sends an expired node MachineAuthorized and no Expired
flag; its past KeyExpiry already moves the client to NeedsLogin.
This commit is contained in:
Kristoffer Dalby committed 2026-11-02 17:46:53 +00:00
1 parent 433e96af96
commit 360b8acfd2
3 files changed
+7 -16

No files matched your search

+5
View File
@@ -109,6 +109,11 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
func(p netip.Prefix) bool { return !b.mapper.cfg.ServesRoute(p) },
)
// As on Tailscale SaaS, an expired node's own entry stays authorised and
// unmarked; the client reads its expiry from the past KeyExpiry.
tailnode.MachineAuthorized = true
tailnode.Expired = false
b.resp.Node = tailnode
return b
@@ -34,25 +34,21 @@ var expirySkipReasons = map[string]string{
"expiry/C2-expire-r1/r1/online-r2": expiredPeerOffline,
"expiry/C2-expire-r1/r1/paths-observer": expiredPeerPaths,
"expiry/C2-expire-r1/r1/paths-r2": expiredPeerPaths,
"expiry/C2-expire-r1/r1/self": selfExpiredFlags,
"expiry/C4-expire-r2/r2/online-observer": expiredPeerOffline,
"expiry/C4-expire-r2/r2/online-r1": expiredPeerOffline,
"expiry/C4-expire-r2/r2/paths-observer": expiredPeerPaths,
"expiry/C4-expire-r2/r2/paths-r1": expiredPeerPaths,
"expiry/C4-expire-r2/r2/self": selfExpiredFlags,
// A client asked for this expiry; once it passes the node is expired
// like any other.
"expiry/D3-past-expiry/subject/online-observer": expiredPeerOffline,
"expiry/D3-past-expiry/subject/paths-observer": expiredPeerPaths,
"expiry/D3-past-expiry/subject/self": selfExpiredFlags,
"expiry/A2-expire/laptop/online-observer": expiredPeerOffline,
"expiry/A2-expire/laptop/online-peer1": expiredPeerOffline,
"expiry/A2-expire/laptop/paths-observer": expiredPeerPaths,
"expiry/A2-expire/laptop/paths-peer1": expiredPeerPaths,
"expiry/A2-expire/laptop/self": selfExpiredFlags,
"expiry/A3-peer-change/laptop/online-observer": expiredPeerOffline,
"expiry/A3-peer-change/laptop/online-peer1": expiredPeerOffline,
@@ -60,39 +56,31 @@ var expirySkipReasons = map[string]string{
"expiry/A3-peer-change/laptop/paths-observer": expiredPeerPaths,
"expiry/A3-peer-change/laptop/paths-peer1": expiredPeerPaths,
"expiry/A3-peer-change/laptop/paths-peer3": expiredPeerPaths,
"expiry/A3-peer-change/laptop/self": selfExpiredFlags,
"expiry/A4-rename/laptop/online-observer": expiredPeerOffline,
"expiry/A4-rename/laptop/paths-observer": expiredPeerPaths,
"expiry/A4-rename/laptop/self": selfExpiredFlags,
"expiry/A5-advertise/laptop/online-observer": expiredPeerOffline,
"expiry/A5-advertise/laptop/paths-observer": expiredPeerPaths,
"expiry/A5-advertise/laptop/self": selfExpiredFlags,
"expiry/A7-dns/laptop/dns": nameserversFromConfig,
"expiry/A7-dns/laptop/online-observer": expiredPeerOffline,
"expiry/A7-dns/laptop/paths-observer": expiredPeerPaths,
"expiry/A7-dns/laptop/self": selfExpiredFlags,
"expiry/A7-dns/observer/dns": nameserversFromConfig,
"expiry/A8-policy/laptop/online-observer": expiredPeerOffline,
"expiry/A8-policy/laptop/paths-observer": expiredPeerPaths,
"expiry/A8-policy/laptop/self": selfExpiredFlags,
"expiry/A9-ssh/laptop/online-observer": expiredPeerOffline,
"expiry/A9-ssh/laptop/paths-observer": expiredPeerPaths,
"expiry/A9-ssh/laptop/self": selfExpiredFlags,
"expiry/A10-reconnect/laptop/online-observer": expiredPeerOffline,
"expiry/A10-reconnect/laptop/paths-observer": expiredPeerPaths,
"expiry/A10-reconnect/laptop/self": selfExpiredFlags,
"expiry/A6a-expire-again/laptop/online-observer": expiredPeerOffline,
"expiry/A6a-expire-again/laptop/online-peer1": expiredPeerOffline,
"expiry/A6a-expire-again/laptop/paths-observer": expiredPeerPaths,
"expiry/A6a-expire-again/laptop/paths-peer1": expiredPeerPaths,
"expiry/A6a-expire-again/laptop/self": selfExpiredFlags,
"expiry/A6b-rotate-disco/laptop/online-observer": expiredPeerOffline,
"expiry/A6b-rotate-disco/laptop/online-peer1": expiredPeerOffline,
@@ -100,11 +88,9 @@ var expirySkipReasons = map[string]string{
"expiry/A6b-rotate-disco/laptop/paths-observer": expiredPeerPaths,
"expiry/A6b-rotate-disco/laptop/paths-peer1": expiredPeerPaths,
"expiry/A6b-rotate-disco/laptop/paths-peer3": expiredPeerPaths,
"expiry/A6b-rotate-disco/laptop/self": selfExpiredFlags,
}
const (
selfExpiredFlags = "SELF_EXPIRED_FLAGS: headscale sends an expired node its own entry with MachineAuthorized false and Expired set; SaaS keeps MachineAuthorized true and leaves Expired out"
expiredPeerPaths = "EXPIRED_PEER_PATHS: headscale sends peers an expired node's endpoints and home DERP; SaaS leaves both out"
nameserversFromConfig = "NAMESERVERS_FROM_CONFIG: headscale reads nameservers from its config and cannot change them while running; SaaS pushes a change to every node, an expired one too"
)
+2 -2
View File
@@ -177,7 +177,7 @@ func TestNodeExpiryPreservesControlConnection(t *testing.T) {
assert.Equal(c, 1, current.ActiveSessions())
assert.Equal(c, epoch, current.SessionEpoch())
assert.True(c, h.Server.App.MapBatcher().IsConnected(id))
assert.True(c, client.Netmap().SelfNode.Expired())
assert.True(c, client.Netmap().SelfNode.KeyExpiry().Before(time.Now()))
assert.False(c, client.Netmap().SelfNode.Online().Get())
peer, found := observer.PeerByName(client.Name)
@@ -220,7 +220,7 @@ func TestNodeExpiryPreservesControlConnection(t *testing.T) {
assert.Equal(c, epoch, current.SessionEpoch())
assert.Equal(c, nodeKey, current.NodeKey())
assert.Equal(c, 1, current.ActiveSessions())
assert.False(c, client.Netmap().SelfNode.Expired())
assert.True(c, client.Netmap().SelfNode.KeyExpiry().IsZero())
assert.True(c, client.Netmap().SelfNode.Online().Get())
peer, found := observer.PeerByName(client.Name)