mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
mapper: show an expired peer online, without endpoints or DERP
A connected expired node shows online to its peers. Neither its map entry nor endpoint, DERP or disco patches carry the paths to reach it.
This commit is contained in:
1 parent
6aed0d5f07
commit
5c5fc4feb7
9 files changed
+39
-70
No files matched your search
@@ -323,6 +323,16 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
||||
tn.CapMap = policyv2.PeerCapMap(peer, peerCapMaps[peer.ID()])
|
||||
tn.Hostinfo = types.PeerHostinfo(tn.Hostinfo)
|
||||
|
||||
// As on Tailscale SaaS, a connected expired peer shows online, but
|
||||
// without the endpoints and home DERP region that would reach it.
|
||||
if peer.IsExpired() {
|
||||
if peer.ActiveSessions() > 0 {
|
||||
tn.Online, tn.LastSeen = new(true), nil
|
||||
}
|
||||
|
||||
tn.Endpoints, tn.HomeDERP = nil, 0
|
||||
}
|
||||
|
||||
tailPeers = append(tailPeers, tn)
|
||||
}
|
||||
|
||||
|
||||
@@ -1776,6 +1776,14 @@ func (b *MapResponseBuilder) buildTailPeersCapMapsReference(peers views.Slice[ty
|
||||
tn.CapMap = policyv2.PeerCapMap(peer, allCapMaps[peer.ID()])
|
||||
tn.Hostinfo = types.PeerHostinfo(tn.Hostinfo)
|
||||
|
||||
if peer.IsExpired() {
|
||||
if peer.ActiveSessions() > 0 {
|
||||
tn.Online, tn.LastSeen = new(true), nil
|
||||
}
|
||||
|
||||
tn.Endpoints, tn.HomeDERP = nil, 0
|
||||
}
|
||||
|
||||
tailPeers = append(tailPeers, tn)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,68 +30,11 @@ import (
|
||||
// <test_id>/<step>/<node>/<check>. A listed check that matches fails, so a
|
||||
// fix must delete its entry.
|
||||
var expirySkipReasons = map[string]string{
|
||||
"expiry/C2-expire-r1/r1/online-observer": expiredPeerOffline,
|
||||
"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/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,
|
||||
|
||||
// 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/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/A3-peer-change/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A3-peer-change/laptop/online-peer1": expiredPeerOffline,
|
||||
"expiry/A3-peer-change/laptop/online-peer3": expiredPeerOffline,
|
||||
"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/A4-rename/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A4-rename/laptop/paths-observer": expiredPeerPaths,
|
||||
|
||||
"expiry/A5-advertise/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A5-advertise/laptop/paths-observer": expiredPeerPaths,
|
||||
|
||||
"expiry/A7-dns/laptop/dns": nameserversFromConfig,
|
||||
"expiry/A7-dns/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A7-dns/laptop/paths-observer": expiredPeerPaths,
|
||||
"expiry/A7-dns/observer/dns": nameserversFromConfig,
|
||||
|
||||
"expiry/A8-policy/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A8-policy/laptop/paths-observer": expiredPeerPaths,
|
||||
|
||||
"expiry/A9-ssh/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A9-ssh/laptop/paths-observer": expiredPeerPaths,
|
||||
|
||||
"expiry/A10-reconnect/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A10-reconnect/laptop/paths-observer": expiredPeerPaths,
|
||||
|
||||
"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/A6b-rotate-disco/laptop/online-observer": expiredPeerOffline,
|
||||
"expiry/A6b-rotate-disco/laptop/online-peer1": expiredPeerOffline,
|
||||
"expiry/A6b-rotate-disco/laptop/online-peer3": expiredPeerOffline,
|
||||
"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/A7-dns/laptop/dns": nameserversFromConfig,
|
||||
"expiry/A7-dns/observer/dns": nameserversFromConfig,
|
||||
}
|
||||
|
||||
const (
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@@ -61,11 +61,6 @@ const (
|
||||
ephemeralFollowsKey = "EPHEMERAL_FOLLOWS_KEY: headscale takes a node's ephemeral flag from the key it last logged in with; SaaS keeps the flag the device was created with, so it is collected, or kept, as created"
|
||||
)
|
||||
|
||||
// Used by reauthSkipReasons.
|
||||
const (
|
||||
expiredPeerOffline = "EXPIRED_PEER_OFFLINE: headscale shows an expired peer offline while it is connected; SaaS shows it online"
|
||||
)
|
||||
|
||||
// TestRouteApprovalCompat replays a user-owned router through admin
|
||||
// approvals and revokes, same-key and forced re-auth, a hostname change,
|
||||
// policy pushes, a withdraw and re-advertise, then logout and a login with
|
||||
|
||||
@@ -180,10 +180,11 @@ func TestNodeExpiryPreservesControlConnection(t *testing.T) {
|
||||
assert.True(c, client.Netmap().SelfNode.KeyExpiry().Before(time.Now()))
|
||||
assert.False(c, client.Netmap().SelfNode.Online().Get())
|
||||
|
||||
// Peers still see the connected node online, as on Tailscale SaaS.
|
||||
peer, found := observer.PeerByName(client.Name)
|
||||
if assert.True(c, found) {
|
||||
assert.True(c, peer.Expired())
|
||||
assert.False(c, peer.Online().Get())
|
||||
assert.True(c, peer.Online().Get())
|
||||
}
|
||||
}, 5*time.Second, 10*time.Millisecond, "expiry must take the node offline without ending its control session")
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ import (
|
||||
// <test_id>/<step>[/<node>]/<check>. A listed check that matches fails, so a
|
||||
// fix must delete its entry.
|
||||
var reauthSkipReasons = map[string]string{
|
||||
"owner-routes/r-expired-retag/expire/online": expiredPeerOffline,
|
||||
|
||||
"reauth/late-pak-spent-key/late-pak/text": refusalText,
|
||||
"reauth/late-pak-spent-key/late-pak/legacy-text": refusalText,
|
||||
|
||||
|
||||
@@ -4211,6 +4211,11 @@ func buildMapRequestChangeResponse(
|
||||
return change.NodeAdded(id)
|
||||
}
|
||||
|
||||
// Peers get an expired node without its endpoints or home DERP region.
|
||||
if node.IsExpired() {
|
||||
delta.endpointBroadcast, delta.derpChanged = false, false
|
||||
}
|
||||
|
||||
var currentDERP tailcfg.DERPRegionID
|
||||
|
||||
if delta.derpChanged {
|
||||
|
||||
@@ -6,6 +6,7 @@ package change
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
@@ -612,12 +613,18 @@ func NodeKeyRotated(node types.NodeView) Change {
|
||||
expiry = e.UTC()
|
||||
}
|
||||
|
||||
// Peers get an expired node without its endpoints.
|
||||
var endpoints []netip.AddrPort
|
||||
if !node.IsExpired() {
|
||||
endpoints = node.Endpoints().AsSlice()
|
||||
}
|
||||
|
||||
c := PeerPatched("node key rotated (relogin)", &tailcfg.PeerChange{
|
||||
NodeID: tailcfg.NodeID(node.ID()), //nolint:gosec // NodeID is bounded
|
||||
Key: &nk,
|
||||
DiscoKey: &dk,
|
||||
KeyExpiry: &expiry,
|
||||
Endpoints: node.Endpoints().AsSlice(),
|
||||
Endpoints: endpoints,
|
||||
})
|
||||
c.OriginNode = node.ID()
|
||||
|
||||
|
||||
@@ -1266,11 +1266,13 @@ func TestExpireNode(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.True(c, expired.Expired)
|
||||
assert.False(c, expired.Online)
|
||||
// It keeps its control connection, so it stays online to
|
||||
// its peers, as on Tailscale SaaS.
|
||||
assert.True(c, expired.Online)
|
||||
if assert.NotNil(c, expired.KeyExpiry) {
|
||||
assert.True(c, expired.KeyExpiry.Before(time.Now()))
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.FastPoll, "peer must see the expired node offline")
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.FastPoll, "peer must see the expired node expired and online")
|
||||
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
_, stderr, _ := peer.Execute([]string{"tailscale", "ping", node.Name})
|
||||
|
||||
Reference in new issue
Block a user