mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-08 00:50:20 +09:00
servertest: improve via compat test infrastructure
Add big-router to the taggedNodes list for via compat tests. This node has tags [tag:router, tag:exit] and is needed for future V-series golden file tests. Existing tests are unaffected. Enhance compareNetmap to compare PacketFilter rule content (destination prefixes, source non-emptiness), not just rule count. Six golden files have full SrcIPs/DstPorts data that was previously unvalidated. Add Tier 3 route inference to inferNodeRoutes: scan each node's own packet_filter_rules DstPorts for non-Tailscale-IP route prefixes. This catches secondary HA routers whose routes don't appear in AllowedIPs but do receive filter rules. Updates #3157 Updates #3169
This commit is contained in:
@@ -74,6 +74,11 @@ var viaCompatTests = []struct {
|
||||
{"GRANT-V30", "crossed mixed: subnet via router-a/b, exit via exit-b/a"},
|
||||
{"GRANT-V31", "peer connectivity + via exit A/B steering"},
|
||||
{"GRANT-V36", "full complex: peer connectivity + crossed subnet + crossed exit"},
|
||||
// TODO: V33 and V35 are not yet compatible due to:
|
||||
// - V33: PrimaryRoutes election differs (node ID ordering between SaaS and headscale)
|
||||
// - V35: Extra peer visibility (autoApprovers create different routing topology)
|
||||
// The inferNodeRoutes Tier 3 fix enables route inference for these files,
|
||||
// but the above issues need resolution first.
|
||||
}
|
||||
|
||||
// TestViaGrantMapCompat loads golden captures from Tailscale SaaS and
|
||||
@@ -128,6 +133,7 @@ func TestViaGrantMapCompat(t *testing.T) {
|
||||
|
||||
// taggedNodes are the nodes we create in the servertest.
|
||||
var taggedNodes = []string{
|
||||
"big-router",
|
||||
"exit-a", "exit-b", "exit-node",
|
||||
"group-a-client", "group-b-client",
|
||||
"router-a", "router-b",
|
||||
@@ -387,15 +393,68 @@ func compareNetmap(
|
||||
}
|
||||
}
|
||||
|
||||
// Compare PacketFilter rule count.
|
||||
// Compare PacketFilter rules (IP-independent).
|
||||
// Golden file SrcIPs contain literal Tailscale SaaS CGNAT addresses
|
||||
// that differ from headscale-allocated IPs, so we compare structural
|
||||
// properties rather than literal SrcIP values.
|
||||
var wantFilterRules []tailcfg.FilterRule
|
||||
if len(want.PacketFilterRules) > 0 &&
|
||||
string(want.PacketFilterRules) != "null" {
|
||||
_ = json.Unmarshal(want.PacketFilterRules, &wantFilterRules)
|
||||
}
|
||||
|
||||
assert.Lenf(t, got.PacketFilter, len(wantFilterRules),
|
||||
"PacketFilter rule count mismatch")
|
||||
if !assert.Lenf(t, got.PacketFilter, len(wantFilterRules),
|
||||
"PacketFilter rule count mismatch") {
|
||||
return
|
||||
}
|
||||
|
||||
// Compare destination prefixes per rule — subnet CIDRs like
|
||||
// 10.44.0.0/16 are stable between Tailscale SaaS and headscale.
|
||||
// Source IPs differ entirely (different CGNAT allocation), so
|
||||
// we only validate that sources exist (non-empty), not their
|
||||
// exact count or values.
|
||||
for i := range wantFilterRules {
|
||||
wantRule := wantFilterRules[i]
|
||||
gotMatch := got.PacketFilter[i]
|
||||
|
||||
// Verify both rules have sources (don't compare exact
|
||||
// count — IP allocation differs between SaaS and headscale).
|
||||
if len(wantRule.SrcIPs) > 0 {
|
||||
assert.NotEmptyf(t, gotMatch.Srcs,
|
||||
"PacketFilter[%d]: golden has %d SrcIPs but headscale has none",
|
||||
i, len(wantRule.SrcIPs))
|
||||
}
|
||||
|
||||
// Destination prefixes: extract non-Tailscale-IP CIDRs
|
||||
// from both golden and headscale rules and compare.
|
||||
var wantDstPrefixes []string
|
||||
|
||||
for _, dp := range wantRule.DstPorts {
|
||||
pfx, err := netip.ParsePrefix(dp.IP)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isTailscaleIP(pfx) {
|
||||
wantDstPrefixes = append(wantDstPrefixes, pfx.String())
|
||||
}
|
||||
}
|
||||
|
||||
var gotDstPrefixes []string
|
||||
|
||||
for _, dst := range gotMatch.Dsts {
|
||||
pfx := dst.Net
|
||||
if !isTailscaleIP(pfx) {
|
||||
gotDstPrefixes = append(gotDstPrefixes, pfx.String())
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(wantDstPrefixes)
|
||||
slices.Sort(gotDstPrefixes)
|
||||
|
||||
assert.Equalf(t, wantDstPrefixes, gotDstPrefixes,
|
||||
"PacketFilter[%d]: non-Tailscale destination prefixes mismatch", i)
|
||||
}
|
||||
}
|
||||
|
||||
type peerSummary struct {
|
||||
@@ -484,9 +543,10 @@ func inferNodeRoutes(gf goldenFile) map[string][]netip.Prefix {
|
||||
}
|
||||
}
|
||||
|
||||
// Infer from the golden netmap: scan all captures for peers with
|
||||
// route prefixes in AllowedIPs. If node X appears as a peer with
|
||||
// route prefix 10.44.0.0/16, then X should advertise that route.
|
||||
// Tier 2: Infer from the golden netmap — scan all captures for
|
||||
// peers with route prefixes in AllowedIPs. If node X appears as
|
||||
// a peer with route prefix 10.44.0.0/16, then X should advertise
|
||||
// that route.
|
||||
for _, capture := range gf.Captures {
|
||||
if capture.Netmap == nil {
|
||||
continue
|
||||
@@ -509,6 +569,45 @@ func inferNodeRoutes(gf goldenFile) map[string][]netip.Prefix {
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: Infer from packet_filter_rules DstPorts — if a node
|
||||
// receives filter rules with non-Tailscale-IP destination
|
||||
// prefixes, it must be advertising those routes. This catches
|
||||
// secondary HA routers whose routes don't appear in AllowedIPs
|
||||
// (only the primary gets the route in AllowedIPs) but DO receive
|
||||
// filter rules for those routes.
|
||||
for nodeName, capture := range gf.Captures {
|
||||
if len(capture.PacketFilterRules) == 0 ||
|
||||
string(capture.PacketFilterRules) == "null" {
|
||||
continue
|
||||
}
|
||||
|
||||
var rules []tailcfg.FilterRule
|
||||
|
||||
err := json.Unmarshal(capture.PacketFilterRules, &rules)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
for _, dp := range rule.DstPorts {
|
||||
prefix, err := netip.ParsePrefix(dp.IP)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if isTailscaleIP(prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(result[nodeName], prefix) {
|
||||
result[nodeName] = append(
|
||||
result[nodeName], prefix,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user