From 1e20972db047ada8f5b0eef7d7fa41f424776f27 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 9 Apr 2026 06:18:43 +0000 Subject: [PATCH] servertest: align via grant compat test with new tscap golden format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous tscap regeneration replaced the legacy uppercase GRANT-V*.hujson files with lowercase via-grant-v*.hujson and switched the topology nodes to anonymized pokémon identifiers, but TestViaGrantMapCompat was still loading files by their old IDs and matching topology entries by their old hostnames, so it could not even open a single golden file. - viaCompatTests: GRANT-V29/V30/V31/V36 → via-grant-v29/v30/v31/v36 so the path Join finds the renamed files on disk. - taggedNodes: rewrite the hardcoded servertest hostname list to the pokémon names tscap/anonymize writes (big-router→caterpie, exit-a→pidgey, …) so the topology lookup picks up the matching entries instead of skipping every node. - goldenNode: track the tscap schema. Replace advertised_routes → routable_ips and is_exit_node → approved_routes; the AdvertisedRoutes field name is kept on the Go side because the rest of the test code already references it. - convertViaPolicy: also rewrite the anonymized odin/thor/freya @example.com emails so the served policy resolves users when the file came from tscap/anonymize. Updates #3157 --- hscontrol/servertest/via_compat_test.go | 49 ++++++++++++++++--------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/hscontrol/servertest/via_compat_test.go b/hscontrol/servertest/via_compat_test.go index dd130134..381ebd62 100644 --- a/hscontrol/servertest/via_compat_test.go +++ b/hscontrol/servertest/via_compat_test.go @@ -38,8 +38,8 @@ type goldenNode struct { Tags []string `json:"tags"` IPv4 string `json:"ipv4"` IPv6 string `json:"ipv6"` - AdvertisedRoutes []string `json:"advertised_routes"` - IsExitNode bool `json:"is_exit_node"` + AdvertisedRoutes []string `json:"routable_ips"` + ApprovedRoutes []string `json:"approved_routes"` } type goldenCapture struct { @@ -70,10 +70,10 @@ var viaCompatTests = []struct { id string desc string }{ - {"GRANT-V29", "crossed subnet steering: group-a via router-a, group-b via router-b"}, - {"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"}, + {"via-grant-v29", "crossed subnet steering: group-a via router-a, group-b via router-b"}, + {"via-grant-v30", "crossed mixed: subnet via router-a/b, exit via exit-b/a"}, + {"via-grant-v31", "peer connectivity + via exit A/B steering"}, + {"via-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) @@ -90,9 +90,10 @@ var viaCompatTests = []struct { // // CROSS-DEPENDENCY WARNING: // This test reads golden files from ../policy/v2/testdata/grant_results/ -// (specifically GRANT-V29, V30, V31, V36). These files are shared with -// TestGrantsCompat in the policy/v2 package. Any changes to the file -// format, field structure, or naming must be coordinated with BOTH tests. +// (specifically via-grant-v29, v30, v31, v36). These files are shared +// with TestGrantsCompat in the policy/v2 package. Any changes to the +// file format, field structure, or naming must be coordinated with +// BOTH tests. // // Fields consumed by this test (but NOT by TestGrantsCompat): // - captures[name].netmap (Peers, AllowedIPs, PrimaryRoutes, PacketFilterRules) @@ -131,15 +132,24 @@ func TestViaGrantMapCompat(t *testing.T) { } } -// taggedNodes are the nodes we create in the servertest. +// taggedNodes are the nodes we create in the servertest. Names match +// the anonymized pokémon identifiers tscap writes into golden files — +// see github.com/kradalby/tscap/anonymize for the full substitution +// table (big-router→caterpie, exit-a→pidgey, etc). var taggedNodes = []string{ - "big-router", - "exit-a", "exit-b", "exit-node", - "group-a-client", "group-b-client", - "router-a", "router-b", - "subnet-router", "tagged-client", - "tagged-server", "tagged-prod", - "multi-exit-router", + "caterpie", // big-router + "pidgey", // exit-a + "pidgeotto", // exit-b + "charmander", // exit-node + "rattata", // group-a-client + "raticate", // group-b-client + "spearow", // router-a + "fearow", // router-b + "squirtle", // subnet-router + "weedle", // tagged-client + "beedrill", // tagged-server + "kakuna", // tagged-prod + "blastoise", // multi-exit-router } func runViaMapCompat(t *testing.T, gf goldenFile) { @@ -624,6 +634,11 @@ func extractHostname(fqdn string) string { // convertViaPolicy converts Tailscale SaaS policy emails to headscale format. func convertViaPolicy(raw json.RawMessage) []byte { s := string(raw) + // Anonymized SaaS emails as written by tscap/anonymize. + s = strings.ReplaceAll(s, "odin@example.com", "tag-user@") + s = strings.ReplaceAll(s, "thor@example.com", "tag-user@") + s = strings.ReplaceAll(s, "freya@example.com", "tag-user@") + // Pre-anonymization emails (legacy captures / local reruns). s = strings.ReplaceAll(s, "kratail2tid@passkey", "tag-user@") s = strings.ReplaceAll(s, "kristoffer@dalby.cc", "tag-user@") s = strings.ReplaceAll(s, "monitorpasskeykradalby@passkey", "tag-user@")