mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-08 00:50:20 +09:00
types/change: add NodeKeyRotated for relogin peer patch
Sends a relogin as an incremental PeerChange, not a whole-node add.
This commit is contained in:
committed by
Kristoffer Dalby
parent
21058d1142
commit
4da06925d0
@@ -490,6 +490,35 @@ func EndpointOrDERPUpdate(id types.NodeID, patch *tailcfg.PeerChange) Change {
|
||||
return c
|
||||
}
|
||||
|
||||
// NodeKeyRotated returns a [Change] for a node re-logging in: its NodeKey (and
|
||||
// possibly DiscoKey, key expiry, or endpoints) changed, but nothing structural
|
||||
// did. Peers only need those changed fields, so it is sent as the minimal
|
||||
// incremental [tailcfg.PeerChange] patch rather than re-advertising the whole
|
||||
// node — the smallest update that conveys the rotation, and the least
|
||||
// disruptive for peers reconciling it.
|
||||
func NodeKeyRotated(node types.NodeView) Change {
|
||||
nk := node.NodeKey()
|
||||
dk := node.DiscoKey()
|
||||
|
||||
// KeyExpiry is always set: the zero value clears any prior expiry on the
|
||||
// peer (un-expire), and a non-zero value carries the new expiry.
|
||||
var expiry time.Time
|
||||
if e, ok := node.Expiry().GetOk(); ok {
|
||||
expiry = e
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
c.OriginNode = node.ID()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// UserAdded returns a [Change] for when a user is added or updated.
|
||||
// A full update is sent to refresh user profiles on all nodes.
|
||||
func UserAdded() Change {
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestChange_FieldSync(t *testing.T) {
|
||||
@@ -653,3 +655,34 @@ func TestNodeOnlineOfflineForSubnetRouter(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeKeyRotatedEmitsPatchNotWholeNode proves a relogin is delivered to
|
||||
// peers as an incremental peer patch, not a whole-node add. A whole-node add is
|
||||
// non-patchifiable on the tailscale client whenever Hostinfo changed (which it
|
||||
// does on relogin), forcing the broken NodeMutationAdd path that strands a
|
||||
// re-keyed, momentarily-endpoint-less peer.
|
||||
func TestNodeKeyRotatedEmitsPatchNotWholeNode(t *testing.T) {
|
||||
expiry := time.Now().Add(24 * time.Hour).UTC()
|
||||
node := types.Node{
|
||||
ID: 7,
|
||||
NodeKey: key.NewNode().Public(),
|
||||
DiscoKey: key.NewDisco().Public(),
|
||||
Endpoints: []netip.AddrPort{netip.MustParseAddrPort("192.168.1.9:41641")},
|
||||
Expiry: &expiry,
|
||||
}
|
||||
view := node.View()
|
||||
|
||||
c := NodeKeyRotated(view)
|
||||
|
||||
assert.False(t, c.IsFull(), "relogin must be a peer patch, not a full update")
|
||||
assert.Empty(t, c.PeersChanged, "relogin must not emit a whole-node PeersChanged")
|
||||
require.Len(t, c.PeerPatches, 1, "relogin must emit exactly one peer patch")
|
||||
|
||||
patch := c.PeerPatches[0]
|
||||
assert.Equal(t, view.ID().NodeID(), patch.NodeID)
|
||||
require.NotNil(t, patch.Key, "patch must carry the rotated NodeKey")
|
||||
assert.Equal(t, node.NodeKey, *patch.Key)
|
||||
require.NotNil(t, patch.KeyExpiry, "patch must carry KeyExpiry to (un)expire the peer")
|
||||
assert.Equal(t, expiry, *patch.KeyExpiry)
|
||||
assert.Equal(t, []netip.AddrPort(node.Endpoints), patch.Endpoints, "patch must carry endpoints")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user