mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-25 07:46:49 +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:
1 parent
21058d1142
commit
4da06925d0
2 files changed
+62
No files matched your search
@@ -490,6 +490,35 @@ func EndpointOrDERPUpdate(id types.NodeID, patch *tailcfg.PeerChange) Change {
|
|||||||
return c
|
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.
|
// UserAdded returns a [Change] for when a user is added or updated.
|
||||||
// A full update is sent to refresh user profiles on all nodes.
|
// A full update is sent to refresh user profiles on all nodes.
|
||||||
func UserAdded() Change {
|
func UserAdded() Change {
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestChange_FieldSync(t *testing.T) {
|
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