mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-06 14:50:07 +09:00
state: refresh policy nodes inside the peer map build
One peer build per tag/user/IP/route write; callers detect policy moves via NodesGeneration. Per-node caches only store results for the node pm holds, so a mapper reading mid-build cannot pin a stale filter.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
@@ -811,3 +813,169 @@ func TestNoSelfAsPeerDuringRealNodeChurn(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackfillNodeIPsReachesBackfilledNode proves the node that receives a
|
||||
// backfilled address learns it too. Peers pick it up from the policy
|
||||
// refresh, but that refresh carries no self node, so the node itself kept
|
||||
// serving its old addresses until an unrelated self update.
|
||||
func TestBackfillNodeIPsReachesBackfilledNode(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
p4 := netip.MustParsePrefix("100.64.0.0/10")
|
||||
p6 := netip.MustParsePrefix("fd7a:115c:a1e0::/48")
|
||||
cfg := &types.Config{
|
||||
Database: types.DatabaseConfig{
|
||||
Type: types.DatabaseSqlite,
|
||||
Sqlite: types.SqliteConfig{Path: tmp + "/h.db"},
|
||||
},
|
||||
PrefixV4: &p4,
|
||||
PrefixV6: &p6,
|
||||
IPAllocation: types.IPAllocationStrategySequential,
|
||||
BaseDomain: "headscale.test",
|
||||
Policy: types.PolicyConfig{Mode: types.PolicyModeDB},
|
||||
DERP: types.DERPConfig{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{999: {RegionID: 999}},
|
||||
},
|
||||
},
|
||||
Tuning: types.Tuning{
|
||||
NodeStoreBatchSize: state.TestBatchSize,
|
||||
NodeStoreBatchTimeout: state.TestBatchTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
database, err := db.NewHeadscaleDatabase(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := database.CreateUserForTest("u1")
|
||||
nodes := database.CreateRegisteredNodesForTest(user, 2, "bf")
|
||||
target := nodes[0].ID
|
||||
require.NoError(t, database.DB.Model(&types.Node{}).Where("id = ?", target).Update("ipv6", nil).Error)
|
||||
// Backfill copies the stored Hostinfo, which a registered client always has.
|
||||
require.NoError(t, database.DB.Model(&types.Node{}).Where("1 = 1").Update("host_info", "{}").Error)
|
||||
require.NoError(t, database.Close())
|
||||
|
||||
s, err := state.NewState(cfg)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = s.Close() })
|
||||
|
||||
backfilled, cs, err := s.BackfillNodeIPs()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, backfilled)
|
||||
|
||||
stored, ok := s.GetNodeByID(target)
|
||||
require.True(t, ok)
|
||||
require.True(t, stored.IPv6().Valid(), "backfill must assign an IPv6 address")
|
||||
|
||||
m := &mapper{state: s, cfg: cfg}
|
||||
nc := newMockNodeConnection(target)
|
||||
|
||||
var self *tailcfg.Node
|
||||
|
||||
for _, ch := range change.FilterForNode(target, cs) {
|
||||
resp, err := generateMapResponse(nc, m, ch)
|
||||
require.NoError(t, err)
|
||||
|
||||
if resp != nil && resp.Node != nil {
|
||||
self = resp.Node
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, self, "the backfilled node must receive its own node")
|
||||
assert.Contains(t, self.Addresses, netip.PrefixFrom(stored.IPv6().Get(), 128))
|
||||
}
|
||||
|
||||
var errInjectedExpiry = errors.New("injected expiry failure")
|
||||
|
||||
// TestFailedExpiryOfPrimaryAnnouncesBackup expires the primary of an HA
|
||||
// route while the database write fails. The NodeStore already moved the
|
||||
// route to the backup, so the change returned with the error must still
|
||||
// tell a client about the new primary, as the successful path does;
|
||||
// otherwise the client drops the old primary's route with no replacement.
|
||||
func TestFailedExpiryOfPrimaryAnnouncesBackup(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
p4 := netip.MustParsePrefix("100.64.0.0/10")
|
||||
p6 := netip.MustParsePrefix("fd7a:115c:a1e0::/48")
|
||||
cfg := &types.Config{
|
||||
Database: types.DatabaseConfig{
|
||||
Type: types.DatabaseSqlite,
|
||||
Sqlite: types.SqliteConfig{Path: tmp + "/h.db"},
|
||||
},
|
||||
PrefixV4: &p4,
|
||||
PrefixV6: &p6,
|
||||
IPAllocation: types.IPAllocationStrategySequential,
|
||||
BaseDomain: "headscale.test",
|
||||
Policy: types.PolicyConfig{Mode: types.PolicyModeDB},
|
||||
DERP: types.DERPConfig{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{999: {RegionID: 999}},
|
||||
},
|
||||
},
|
||||
Tuning: types.Tuning{
|
||||
NodeStoreBatchSize: state.TestBatchSize,
|
||||
NodeStoreBatchTimeout: state.TestBatchTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
route := netip.MustParsePrefix("10.77.0.0/24")
|
||||
|
||||
database, err := db.NewHeadscaleDatabase(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
user := database.CreateUserForTest("u1")
|
||||
nodes := database.CreateRegisteredNodesForTest(user, 3, "ha")
|
||||
|
||||
for _, n := range nodes[:2] {
|
||||
n.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: []netip.Prefix{route}}
|
||||
n.ApprovedRoutes = []netip.Prefix{route}
|
||||
require.NoError(t, database.DB.Save(n).Error)
|
||||
}
|
||||
|
||||
require.NoError(t, database.Close())
|
||||
|
||||
s, err := state.NewState(cfg)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = s.Close() })
|
||||
|
||||
primary, backup, client := nodes[0].ID, nodes[1].ID, nodes[2].ID
|
||||
for _, id := range []types.NodeID{primary, backup, client} {
|
||||
s.Connect(id)
|
||||
}
|
||||
|
||||
require.Equal(t, []netip.Prefix{route}, s.GetNodePrimaryRoutes(primary))
|
||||
|
||||
require.NoError(t, s.DB().DB.Callback().Update().Before("gorm:update").
|
||||
Register("fail_node_update", func(tx *gorm.DB) {
|
||||
if tx.Statement.Table == "nodes" {
|
||||
_ = tx.AddError(errInjectedExpiry)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(func() { _ = s.DB().DB.Callback().Update().Remove("fail_node_update") })
|
||||
|
||||
past := time.Now().Add(-time.Hour)
|
||||
_, c, err := s.SetNodeExpiry(primary, &past)
|
||||
require.ErrorIs(t, err, errInjectedExpiry)
|
||||
require.Equal(t, []netip.Prefix{route}, s.GetNodePrimaryRoutes(backup),
|
||||
"the NodeStore moved the route to the backup")
|
||||
|
||||
m := &mapper{state: s, cfg: cfg}
|
||||
nc := newMockNodeConnection(client)
|
||||
|
||||
var backupRoutes []netip.Prefix
|
||||
|
||||
for _, ch := range change.FilterForNode(client, []change.Change{c}) {
|
||||
resp, err := generateMapResponse(nc, m, ch)
|
||||
require.NoError(t, err)
|
||||
|
||||
if resp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range append(resp.Peers, resp.PeersChanged...) {
|
||||
if p.ID == backup.NodeID() {
|
||||
backupRoutes = p.PrimaryRoutes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Contains(t, backupRoutes, route, "the client must learn the backup is primary: %s", c.Type())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user