servertest: bound fixture batching and await observable state

Updates #3544
This commit is contained in:
Kristoffer Dalby committed 2026-10-13 14:35:52 +02:00
1 parent b16b705c13
commit 940260cfdc
6 files changed
+114 -51

No files matched your search

+20 -6
View File
@@ -6,6 +6,7 @@ import (
"github.com/juanfont/headscale/hscontrol/servertest" "github.com/juanfont/headscale/hscontrol/servertest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"tailscale.com/types/netmap"
) )
// TestConsistency verifies that all nodes converge to the same // TestConsistency verifies that all nodes converge to the same
@@ -86,14 +87,27 @@ func TestConsistency(t *testing.T) {
c6 := h.AddClient(t) c6 := h.AddClient(t)
c7 := h.AddClient(t) c7 := h.AddClient(t)
// Wait for new nodes to see at least all other connected // Disconnected peers can satisfy a count before the new peers arrive.
// clients (they may also see the disconnected nodes during // Wait for each connected identity before comparing the mesh.
// the grace period, so we check >= not ==).
connected := h.ConnectedClients() connected := h.ConnectedClients()
minPeers := len(connected) - 1
for _, c := range connected { for _, c := range connected {
c.WaitForPeers(t, minPeers, 30*time.Second) c.WaitForCondition(t, "all connected peers visible", 30*time.Second,
func(nm *netmap.NetworkMap) bool {
seen := make(map[string]bool, len(nm.Peers))
for _, peer := range nm.Peers {
if hi := peer.Hostinfo(); hi.Valid() {
seen[hi.Hostname()] = true
}
}
for _, other := range connected {
if other != c && !seen[other.Name] {
return false
}
}
return true
})
} }
// Verify the new nodes can see each other. // Verify the new nodes can see each other.
+52 -32
View File
@@ -224,12 +224,28 @@ func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
c1.WaitForCondition(t, "first grant policy update", c1.WaitForCondition(t, "first grant policy update",
10*time.Second, 10*time.Second,
func(nm *netmap.NetworkMap) bool { func(nm *netmap.NetworkMap) bool {
return c1.UpdateCount() > countC1 for _, rule := range nm.PacketFilter {
for _, dst := range rule.Dsts {
if dst.Ports.First == 22 && dst.Ports.Last == 22 {
return c1.UpdateCount() > countC1
}
}
}
return false
}) })
c2.WaitForCondition(t, "first grant policy update", c2.WaitForCondition(t, "first grant policy update",
10*time.Second, 10*time.Second,
func(nm *netmap.NetworkMap) bool { func(nm *netmap.NetworkMap) bool {
return c2.UpdateCount() > countC2 for _, rule := range nm.PacketFilter {
for _, dst := range rule.Dsts {
if dst.Ports.First == 22 && dst.Ports.Last == 22 {
return c2.UpdateCount() > countC2
}
}
}
return false
}) })
// Capture the update count after first policy. // Capture the update count after first policy.
@@ -270,12 +286,12 @@ func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
c1.WaitForCondition(t, "second grant policy update", c1.WaitForCondition(t, "second grant policy update",
10*time.Second, 10*time.Second,
func(nm *netmap.NetworkMap) bool { func(nm *netmap.NetworkMap) bool {
return c1.UpdateCount() > updateCountPhase1 return c1.UpdateCount() > updateCountPhase1 && hasCapMatches(nm.PacketFilter)
}) })
c2.WaitForCondition(t, "second grant policy update", c2.WaitForCondition(t, "second grant policy update",
10*time.Second, 10*time.Second,
func(nm *netmap.NetworkMap) bool { func(nm *netmap.NetworkMap) bool {
return c2.UpdateCount() > countC2 return c2.UpdateCount() > countC2 && hasCapMatches(nm.PacketFilter)
}) })
// Verify the second policy added cap grants that weren't // Verify the second policy added cap grants that weren't
@@ -751,25 +767,27 @@ func TestGrantViaSubnetFilterRules(t *testing.T) {
// Critical: the router's [netmap.NetworkMap.PacketFilter] MUST contain rules with // Critical: the router's [netmap.NetworkMap.PacketFilter] MUST contain rules with
// the via-steered subnet (10.0.0.0/24) as a destination. // the via-steered subnet (10.0.0.0/24) as a destination.
// Without this, the router drops traffic forwarded through it. // Without this, the router drops traffic forwarded through it.
routerNM := routerA.Netmap() requireNetmapHolds(t, func(tt require.TestingT) {
require.NotNil(t, routerNM) routerNM := routerA.Netmap()
require.NotNil(t, routerNM.PacketFilter, require.NotNil(tt, routerNM)
"router PacketFilter should not be nil") require.NotNil(tt, routerNM.PacketFilter,
"router PacketFilter should not be nil")
var foundSubnetDst bool var foundSubnetDst bool
for _, m := range routerNM.PacketFilter { for _, m := range routerNM.PacketFilter {
for _, dst := range m.Dsts { for _, dst := range m.Dsts {
dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits()) dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits())
if route.Contains(dstPrefix.Addr()) && dstPrefix.Bits() >= route.Bits() { if route.Contains(dstPrefix.Addr()) && dstPrefix.Bits() >= route.Bits() {
foundSubnetDst = true foundSubnetDst = true
}
} }
} }
}
assert.True(t, foundSubnetDst, assert.True(tt, foundSubnetDst,
"router PacketFilter should contain destination rules for via-steered subnet 10.0.0.0/24; "+ "router PacketFilter should contain destination rules for via-steered subnet 10.0.0.0/24; "+
"without per-node filter compilation for via grants, these rules are missing") "without per-node filter compilation for via grants, these rules are missing")
})
} }
// TestGrantViaSubnetBroaderDstFilterRules verifies that a via grant // TestGrantViaSubnetBroaderDstFilterRules verifies that a via grant
@@ -868,25 +886,27 @@ func TestGrantViaSubnetBroaderDstFilterRules(t *testing.T) {
// Router's PacketFilter must contain the broader grant dst as a // Router's PacketFilter must contain the broader grant dst as a
// destination — that is what Tailscale SaaS emits for via grants. // destination — that is what Tailscale SaaS emits for via grants.
routerNM := routerA.Netmap() requireNetmapHolds(t, func(tt require.TestingT) {
require.NotNil(t, routerNM) routerNM := routerA.Netmap()
require.NotNil(t, routerNM.PacketFilter, require.NotNil(tt, routerNM)
"router PacketFilter should not be nil") require.NotNil(tt, routerNM.PacketFilter,
"router PacketFilter should not be nil")
var foundBroaderDst bool var foundBroaderDst bool
for _, m := range routerNM.PacketFilter { for _, m := range routerNM.PacketFilter {
for _, dst := range m.Dsts { for _, dst := range m.Dsts {
dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits()) dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits())
if dstPrefix == broaderDst { if dstPrefix == broaderDst {
foundBroaderDst = true foundBroaderDst = true
}
} }
} }
}
assert.True(t, foundBroaderDst, assert.True(tt, foundBroaderDst,
"router PacketFilter should contain destination rules for the broader grant dst 10.0.0.0/8; "+ "router PacketFilter should contain destination rules for the broader grant dst 10.0.0.0/8; "+
"the via gate requires advertised-route overlap, and the emitted prefix is the dst") "the via gate requires advertised-route overlap, and the emitted prefix is the dst")
})
} }
// TestGrantViaExitNodeNoFilterRules verifies wire-format SaaS compat: // TestGrantViaExitNodeNoFilterRules verifies wire-format SaaS compat:
+14 -6
View File
@@ -110,20 +110,28 @@ func TestPolicyChanges(t *testing.T) {
t.Parallel() t.Parallel()
h := servertest.NewHarness(t, 2) h := servertest.NewHarness(t, 2)
// Apply policy twice and verify updates arrive both times. // Change the rule each time; reapplying a policy is a no-op.
for round := range 2 { for round, port := range []uint16{22, 23} {
countBefore := h.Client(0).UpdateCount() countBefore := h.Client(0).UpdateCount()
h.ChangePolicy(t, []byte(`{ h.ChangePolicy(t, fmt.Appendf(nil, `{
"acls": [ "acls": [
{"action": "accept", "src": ["*"], "dst": ["*:*"]} {"action": "accept", "src": ["*"], "dst": ["*:%d"]}
] ]
}`)) }`, port))
h.Client(0).WaitForCondition(t, "update after policy change", h.Client(0).WaitForCondition(t, "update after policy change",
10*time.Second, 10*time.Second,
func(nm *netmap.NetworkMap) bool { func(nm *netmap.NetworkMap) bool {
return h.Client(0).UpdateCount() > countBefore for _, rule := range nm.PacketFilter {
for _, dst := range rule.Dsts {
if dst.Ports.First == port && dst.Ports.Last == port {
return h.Client(0).UpdateCount() > countBefore
}
}
}
return false
}) })
t.Logf("round %d: update received", round) t.Logf("round %d: update received", round)
+12 -3
View File
@@ -385,9 +385,18 @@ func TestRaceConnectDuringGracePeriod(t *testing.T) {
surviving := originals[n/2:] surviving := originals[n/2:]
allActive := append(surviving, replacements...) allActive := append(surviving, replacements...)
for _, c := range allActive { requireNetmapHolds(t, func(tt require.TestingT) {
c.WaitForPeers(t, len(allActive)-1, 30*time.Second) for _, client := range allActive {
} for _, peer := range allActive {
if client == peer {
continue
}
_, found := client.PeerByName(peer.Name)
assert.True(tt, found, "%s must see active peer %s", client.Name, peer.Name)
}
}
})
servertest.AssertConsistentState(t, allActive) servertest.AssertConsistentState(t, allActive)
}) })
+2
View File
@@ -146,6 +146,8 @@ func NewServer(tb testing.TB, opts ...ServerOption) *TestServer {
}, },
Taildrop: types.TaildropConfig{Enabled: sc.taildropEnabled}, Taildrop: types.TaildropConfig{Enabled: sc.taildropEnabled},
Tuning: types.Tuning{ Tuning: types.Tuning{
// Small fixtures rarely fill a batch; bound their write latency.
NodeStoreBatchTimeout: 10 * time.Millisecond,
BatchChangeDelay: sc.batchDelay, BatchChangeDelay: sc.batchDelay,
BatcherWorkers: sc.batcherWorkers, BatcherWorkers: sc.batcherWorkers,
NodeMapSessionBufferedChanSize: sc.bufferedChanSize, NodeMapSessionBufferedChanSize: sc.bufferedChanSize,
+14 -4
View File
@@ -624,10 +624,20 @@ func TestStressChurn(t *testing.T) {
servertest.WithUser(user)) servertest.WithUser(user))
} }
// Wait for the new set to converge. // Old disconnected peers can satisfy the count before replacements
for _, c := range clients { // arrive. Wait for every replacement identity to stay visible.
c.WaitForPeers(t, n-1, 30*time.Second) requireNetmapHolds(t, func(tt require.TestingT) {
} for _, client := range clients {
for _, peer := range clients {
if client == peer {
continue
}
_, found := client.PeerByName(peer.Name)
assert.True(tt, found, "%s must see replacement %s", client.Name, peer.Name)
}
}
})
servertest.AssertSymmetricVisibility(t, clients) servertest.AssertSymmetricVisibility(t, clients)
}) })