From 76c7cb334287dcbd1bb99bb10c47e90c7576a784 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 8 Apr 2026 08:21:31 +0000 Subject: [PATCH] compat tests: switch to anonymized norse-god + pokemon names tscap now anonymizes captures at write time (see kradalby/tscap package anonymize): SaaS user identifiers become odin/thor/freya and node hostnames become original-151 pokemon names. Update the four policy v2 compat test setups to construct types.User and types.Node objects with the new names so the in-test topology matches what the captured testdata files contain. The convertPolicyUserEmails / convertSaaSEmail helpers become passthroughs because tscap already produces the final email form. Updates #3157 Updates #3169 --- .../v2/tailscale_acl_data_compat_test.go | 29 ++-- .../policy/v2/tailscale_grants_compat_test.go | 138 +++++++++--------- .../v2/tailscale_routes_data_compat_test.go | 18 +-- .../v2/tailscale_ssh_data_compat_test.go | 65 ++++----- 4 files changed, 110 insertions(+), 140 deletions(-) diff --git a/hscontrol/policy/v2/tailscale_acl_data_compat_test.go b/hscontrol/policy/v2/tailscale_acl_data_compat_test.go index 256127b2..6f7bb970 100644 --- a/hscontrol/policy/v2/tailscale_acl_data_compat_test.go +++ b/hscontrol/policy/v2/tailscale_acl_data_compat_test.go @@ -39,55 +39,56 @@ func ptrAddr(s string) *netip.Addr { } // setupACLCompatUsers returns the 3 test users for ACL compatibility tests. -// Email addresses use @example.com domain, matching the converted Tailscale -// policy format (Tailscale uses @passkey and @dalby.cc). +// Names and emails match the anonymized identifiers tscap writes into the +// capture files (see github.com/kradalby/tscap/anonymize): users get +// norse-god names and nodes get original-151 pokémon names. func setupACLCompatUsers() types.Users { return types.Users{ - {Model: gorm.Model{ID: 1}, Name: "kratail2tid", Email: "kratail2tid@example.com"}, - {Model: gorm.Model{ID: 2}, Name: "kristoffer", Email: "kristoffer@example.com"}, - {Model: gorm.Model{ID: 3}, Name: "monitorpasskeykradalby", Email: "monitorpasskeykradalby@example.com"}, + {Model: gorm.Model{ID: 1}, Name: "odin", Email: "odin@example.com"}, + {Model: gorm.Model{ID: 2}, Name: "thor", Email: "thor@example.com"}, + {Model: gorm.Model{ID: 3}, Name: "freya", Email: "freya@example.com"}, } } // setupACLCompatNodes returns the 8 test nodes for ACL compatibility tests. -// Uses the same topology as the grants compat tests. +// Node GivenNames match tscap's anonymized pokémon naming. func setupACLCompatNodes(users types.Users) types.Nodes { return types.Nodes{ { - ID: 1, GivenName: "user1", + ID: 1, GivenName: "bulbasaur", User: &users[0], UserID: &users[0].ID, IPv4: ptrAddr("100.90.199.68"), IPv6: ptrAddr("fd7a:115c:a1e0::2d01:c747"), Hostinfo: &tailcfg.Hostinfo{}, }, { - ID: 2, GivenName: "user-kris", + ID: 2, GivenName: "ivysaur", User: &users[1], UserID: &users[1].ID, IPv4: ptrAddr("100.110.121.96"), IPv6: ptrAddr("fd7a:115c:a1e0::1737:7960"), Hostinfo: &tailcfg.Hostinfo{}, }, { - ID: 3, GivenName: "user-mon", + ID: 3, GivenName: "venusaur", User: &users[2], UserID: &users[2].ID, IPv4: ptrAddr("100.103.90.82"), IPv6: ptrAddr("fd7a:115c:a1e0::9e37:5a52"), Hostinfo: &tailcfg.Hostinfo{}, }, { - ID: 4, GivenName: "tagged-server", + ID: 4, GivenName: "beedrill", IPv4: ptrAddr("100.108.74.26"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"), Tags: []string{"tag:server"}, Hostinfo: &tailcfg.Hostinfo{}, }, { - ID: 5, GivenName: "tagged-prod", + ID: 5, GivenName: "kakuna", IPv4: ptrAddr("100.103.8.15"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"), Tags: []string{"tag:prod"}, Hostinfo: &tailcfg.Hostinfo{}, }, { - ID: 6, GivenName: "tagged-client", + ID: 6, GivenName: "weedle", IPv4: ptrAddr("100.83.200.69"), IPv6: ptrAddr("fd7a:115c:a1e0::c537:c845"), Tags: []string{"tag:client"}, Hostinfo: &tailcfg.Hostinfo{}, }, { - ID: 7, GivenName: "subnet-router", + ID: 7, GivenName: "squirtle", IPv4: ptrAddr("100.92.142.61"), IPv6: ptrAddr("fd7a:115c:a1e0::3e37:8e3d"), Tags: []string{"tag:router"}, Hostinfo: &tailcfg.Hostinfo{ @@ -96,7 +97,7 @@ func setupACLCompatNodes(users types.Users) types.Nodes { ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, }, { - ID: 8, GivenName: "exit-node", + ID: 8, GivenName: "charmander", IPv4: ptrAddr("100.85.66.106"), IPv6: ptrAddr("fd7a:115c:a1e0::7c37:426a"), Tags: []string{"tag:exit"}, Hostinfo: &tailcfg.Hostinfo{}, }, diff --git a/hscontrol/policy/v2/tailscale_grants_compat_test.go b/hscontrol/policy/v2/tailscale_grants_compat_test.go index 6ee0f36a..9aba7bf8 100644 --- a/hscontrol/policy/v2/tailscale_grants_compat_test.go +++ b/hscontrol/policy/v2/tailscale_grants_compat_test.go @@ -36,23 +36,26 @@ import ( // setupGrantsCompatUsers returns the 3 test users for grants compatibility tests. -// Email addresses use @example.com domain, matching the converted Tailscale policy format. +// Users get norse-god names; nodes get original-151 pokémon names — matching +// the anonymized identifiers tscap writes into the capture files +// (see github.com/kradalby/tscap/anonymize). func setupGrantsCompatUsers() types.Users { return types.Users{ - {Model: gorm.Model{ID: 1}, Name: "kratail2tid", Email: "kratail2tid@example.com"}, - {Model: gorm.Model{ID: 2}, Name: "kristoffer", Email: "kristoffer@example.com"}, - {Model: gorm.Model{ID: 3}, Name: "monitorpasskeykradalby", Email: "monitorpasskeykradalby@example.com"}, + {Model: gorm.Model{ID: 1}, Name: "odin", Email: "odin@example.com"}, + {Model: gorm.Model{ID: 2}, Name: "thor", Email: "thor@example.com"}, + {Model: gorm.Model{ID: 3}, Name: "freya", Email: "freya@example.com"}, } } -// setupGrantsCompatNodes returns the 8 test nodes for grants compatibility tests. +// setupGrantsCompatNodes returns the 15 test nodes for grants compatibility tests. // The node configuration matches the Tailscale test environment: -// - 3 user-owned nodes (user1, user-kris, user-mon) -// - 5 tagged nodes (tagged-server, tagged-prod, tagged-client, subnet-router, exit-node) +// - 3 user-owned nodes (bulbasaur, ivysaur, venusaur) +// - 12 tagged nodes (beedrill, kakuna, weedle, squirtle, charmander, +// pidgey, pidgeotto, rattata, raticate, spearow, fearow, blastoise) func setupGrantsCompatNodes(users types.Users) types.Nodes { - nodeUser1 := &types.Node{ + nodeBulbasaur := &types.Node{ ID: 1, - GivenName: "user1", + GivenName: "bulbasaur", User: &users[0], UserID: &users[0].ID, IPv4: ptrAddr("100.90.199.68"), @@ -60,9 +63,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { Hostinfo: &tailcfg.Hostinfo{}, } - nodeUserKris := &types.Node{ + nodeIvysaur := &types.Node{ ID: 2, - GivenName: "user-kris", + GivenName: "ivysaur", User: &users[1], UserID: &users[1].ID, IPv4: ptrAddr("100.110.121.96"), @@ -70,9 +73,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { Hostinfo: &tailcfg.Hostinfo{}, } - nodeUserMon := &types.Node{ + nodeVenusaur := &types.Node{ ID: 3, - GivenName: "user-mon", + GivenName: "venusaur", User: &users[2], UserID: &users[2].ID, IPv4: ptrAddr("100.103.90.82"), @@ -80,36 +83,36 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { Hostinfo: &tailcfg.Hostinfo{}, } - nodeTaggedServer := &types.Node{ + nodeBeedrill := &types.Node{ ID: 4, - GivenName: "tagged-server", + GivenName: "beedrill", IPv4: ptrAddr("100.108.74.26"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"), Tags: []string{"tag:server"}, Hostinfo: &tailcfg.Hostinfo{}, } - nodeTaggedProd := &types.Node{ + nodeKakuna := &types.Node{ ID: 5, - GivenName: "tagged-prod", + GivenName: "kakuna", IPv4: ptrAddr("100.103.8.15"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"), Tags: []string{"tag:prod"}, Hostinfo: &tailcfg.Hostinfo{}, } - nodeTaggedClient := &types.Node{ + nodeWeedle := &types.Node{ ID: 6, - GivenName: "tagged-client", + GivenName: "weedle", IPv4: ptrAddr("100.83.200.69"), IPv6: ptrAddr("fd7a:115c:a1e0::c537:c845"), Tags: []string{"tag:client"}, Hostinfo: &tailcfg.Hostinfo{}, } - nodeSubnetRouter := &types.Node{ + nodeSquirtle := &types.Node{ ID: 7, - GivenName: "subnet-router", + GivenName: "squirtle", IPv4: ptrAddr("100.92.142.61"), IPv6: ptrAddr("fd7a:115c:a1e0::3e37:8e3d"), Tags: []string{"tag:router"}, @@ -119,9 +122,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, } - nodeExitNode := &types.Node{ + nodeCharmander := &types.Node{ ID: 8, - GivenName: "exit-node", + GivenName: "charmander", IPv4: ptrAddr("100.85.66.106"), IPv6: ptrAddr("fd7a:115c:a1e0::7c37:426a"), Tags: []string{"tag:exit"}, @@ -139,9 +142,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { // --- New nodes for expanded via grant topology --- - nodeExitA := &types.Node{ + nodePidgey := &types.Node{ ID: 9, - GivenName: "exit-a", + GivenName: "pidgey", IPv4: ptrAddr("100.124.195.93"), IPv6: ptrAddr("fd7a:115c:a1e0::7837:c35d"), Tags: []string{"tag:exit-a"}, @@ -157,9 +160,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { }, } - nodeExitB := &types.Node{ + nodePidgeotto := &types.Node{ ID: 10, - GivenName: "exit-b", + GivenName: "pidgeotto", IPv4: ptrAddr("100.116.18.24"), IPv6: ptrAddr("fd7a:115c:a1e0::ff37:1218"), Tags: []string{"tag:exit-b"}, @@ -175,27 +178,27 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { }, } - nodeGroupA := &types.Node{ + nodeRattata := &types.Node{ ID: 11, - GivenName: "group-a-client", + GivenName: "rattata", IPv4: ptrAddr("100.107.162.14"), IPv6: ptrAddr("fd7a:115c:a1e0::a237:a20e"), Tags: []string{"tag:group-a"}, Hostinfo: &tailcfg.Hostinfo{}, } - nodeGroupB := &types.Node{ + nodeRaticate := &types.Node{ ID: 12, - GivenName: "group-b-client", + GivenName: "raticate", IPv4: ptrAddr("100.77.135.18"), IPv6: ptrAddr("fd7a:115c:a1e0::4b37:8712"), Tags: []string{"tag:group-b"}, Hostinfo: &tailcfg.Hostinfo{}, } - nodeRouterA := &types.Node{ + nodeSpearow := &types.Node{ ID: 13, - GivenName: "router-a", + GivenName: "spearow", IPv4: ptrAddr("100.109.43.124"), IPv6: ptrAddr("fd7a:115c:a1e0::a537:2b7c"), Tags: []string{"tag:router-a"}, @@ -205,9 +208,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.44.0.0/16")}, } - nodeRouterB := &types.Node{ + nodeFearow := &types.Node{ ID: 14, - GivenName: "router-b", + GivenName: "fearow", IPv4: ptrAddr("100.65.172.123"), IPv6: ptrAddr("fd7a:115c:a1e0::5a37:ac7c"), Tags: []string{"tag:router-b"}, @@ -217,9 +220,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.55.0.0/16")}, } - nodeMultiExitRouter := &types.Node{ + nodeBlastoise := &types.Node{ ID: 15, - GivenName: "multi-exit-router", + GivenName: "blastoise", IPv4: ptrAddr("100.105.127.107"), IPv6: ptrAddr("fd7a:115c:a1e0::9537:7f6b"), Tags: []string{"tag:exit", "tag:router"}, @@ -238,21 +241,21 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes { } return types.Nodes{ - nodeUser1, - nodeUserKris, - nodeUserMon, - nodeTaggedServer, - nodeTaggedProd, - nodeTaggedClient, - nodeSubnetRouter, - nodeExitNode, - nodeExitA, - nodeExitB, - nodeGroupA, - nodeGroupB, - nodeRouterA, - nodeRouterB, - nodeMultiExitRouter, + nodeBulbasaur, + nodeIvysaur, + nodeVenusaur, + nodeBeedrill, + nodeKakuna, + nodeWeedle, + nodeSquirtle, + nodeCharmander, + nodePidgey, + nodePidgeotto, + nodeRattata, + nodeRaticate, + nodeSpearow, + nodeFearow, + nodeBlastoise, } } @@ -267,22 +270,12 @@ func findGrantsNode(nodes types.Nodes, name string) *types.Node { return nil } -// convertPolicyUserEmails converts Tailscale SaaS user email formats to -// headscale-compatible @example.com format in the raw policy JSON. -// -// Tailscale uses provider-specific email formats: -// - kratail2tid@passkey (passkey auth) -// - kristoffer@dalby.cc (email auth) -// - monitorpasskeykradalby@passkey (passkey auth) -// -// Headscale resolves users by Email field, so we convert all to @example.com. +// convertPolicyUserEmails used to map SaaS-side emails to @example.com. +// tscap now anonymizes the policy JSON at write time (kratail2tid -> odin, +// kristoffer -> thor, monitorpasskeykradalby -> freya), so the captured +// FullPolicy is already in its final form and this is a passthrough. func convertPolicyUserEmails(policyJSON []byte) []byte { - s := string(policyJSON) - s = strings.ReplaceAll(s, "kratail2tid@passkey", "kratail2tid@example.com") - s = strings.ReplaceAll(s, "kristoffer@dalby.cc", "kristoffer@example.com") - s = strings.ReplaceAll(s, "monitorpasskeykradalby@passkey", "monitorpasskeykradalby@example.com") - - return []byte(s) + return policyJSON } // loadGrantTestFile loads and parses a single grant capture HuJSON file. @@ -351,11 +344,12 @@ func TestGrantsCompat(t *testing.T) { // Determine which node set to use based on the test's topology. // Tests captured with the expanded 15-node topology (V26+) have - // nodes like exit-a, group-a-client, etc. Tests from the original - // 8-node topology should only use the first 8 nodes to avoid - // resolving extra IPs from nodes that weren't present during capture. + // nodes like pidgey (exit-a), rattata (group-a-client), etc. + // Tests from the original 8-node topology should only use the + // first 8 nodes to avoid resolving extra IPs from nodes that + // weren't present during capture. nodes := allNodes - if _, hasNewNodes := tf.Captures["exit-a"]; !hasNewNodes { + if _, hasNewNodes := tf.Captures["pidgey"]; !hasNewNodes { nodes = allNodes[:8] } @@ -504,7 +498,7 @@ func testGrantSuccess( for nodeName, capture := range tf.Captures { t.Run(nodeName, func(t *testing.T) { // Check if this node was offline during capture. - // tagged-prod was frequently offline (132 of 188 success tests). + // kakuna (tag:prod) was frequently offline (132 of 188 success tests). // When offline, packet_filter_rules is null and topology shows // hostname="unknown" with empty tags. captureIsNull := len(capture.PacketFilterRules) == 0 || diff --git a/hscontrol/policy/v2/tailscale_routes_data_compat_test.go b/hscontrol/policy/v2/tailscale_routes_data_compat_test.go index 9457aaa8..e5a53d32 100644 --- a/hscontrol/policy/v2/tailscale_routes_data_compat_test.go +++ b/hscontrol/policy/v2/tailscale_routes_data_compat_test.go @@ -57,21 +57,11 @@ func loadRoutesTestFile(t *testing.T, path string) *testcapture.Capture { return c } -// convertSaaSEmail rewrites a Tailscale SaaS user email to its -// headscale-equivalent @example.com form, matching the convention used by -// convertPolicyUserEmails. The kristoffer@dalby.cc and *@passkey domains -// are normalized to @example.com so that policy resolution finds the user -// objects we set up in the test. +// convertSaaSEmail used to map SaaS-side emails to @example.com placeholders. +// tscap now anonymizes captures at write time (norse-god names + pokémon +// hostnames), so the captured topology emails are already in their final +// form and this is a passthrough. func convertSaaSEmail(email string) string { - switch email { - case "kratail2tid@passkey": - return "kratail2tid@example.com" - case "kristoffer@dalby.cc": - return "kristoffer@example.com" - case "monitorpasskeykradalby@passkey": - return "monitorpasskeykradalby@example.com" - } - return email } diff --git a/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go b/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go index ee2f222b..3e72dd4c 100644 --- a/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go +++ b/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go @@ -19,7 +19,6 @@ package v2 import ( "encoding/json" "path/filepath" - "strings" "testing" "github.com/google/go-cmp/cmp" @@ -33,43 +32,41 @@ import ( // setupSSHDataCompatUsers returns the 3 test users for SSH data-driven -// compatibility tests. The user configuration matches the Tailscale test -// environment with email domains preserved for localpart matching: -// - kratail2tid@example.com (converted from @passkey) -// - kristoffer@dalby.cc (kept as-is — different domain for localpart exclusion) -// - monitorpasskeykradalby@example.com (converted from @passkey) +// compatibility tests. Users get norse-god names; nodes get original-151 +// pokémon names — matching the anonymized identifiers tscap writes into +// the capture files (see github.com/kradalby/tscap/anonymize). func setupSSHDataCompatUsers() types.Users { return types.Users{ { Model: gorm.Model{ID: 1}, - Name: "kratail2tid", - Email: "kratail2tid@example.com", + Name: "odin", + Email: "odin@example.com", }, { Model: gorm.Model{ID: 2}, - Name: "kristoffer", - Email: "kristoffer@dalby.cc", + Name: "thor", + Email: "thor@example.com", }, { Model: gorm.Model{ID: 3}, - Name: "monitorpasskeykradalby", - Email: "monitorpasskeykradalby@example.com", + Name: "freya", + Email: "freya@example.com", }, } } -// setupSSHDataCompatNodes returns the 5 test nodes for SSH data-driven -// compatibility tests. Node GivenNames match the keys in the JSON files: -// - user1 (owned by kratail2tid) -// - user-kris (owned by kristoffer) -// - user-mon (owned by monitorpasskeykradalby) -// - tagged-server (tag:server) -// - tagged-prod (tag:prod) +// setupSSHDataCompatNodes returns the test nodes for SSH data-driven +// compatibility tests. Node GivenNames match the anonymized pokémon names: +// - bulbasaur (owned by odin) +// - ivysaur (owned by thor) +// - venusaur (owned by freya) +// - beedrill (tag:server) +// - kakuna (tag:prod) func setupSSHDataCompatNodes(users types.Users) types.Nodes { return types.Nodes{ &types.Node{ ID: 1, - GivenName: "user1", + GivenName: "bulbasaur", User: &users[0], UserID: &users[0].ID, IPv4: ptrAddr("100.90.199.68"), @@ -78,7 +75,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes { }, &types.Node{ ID: 2, - GivenName: "user-kris", + GivenName: "ivysaur", User: &users[1], UserID: &users[1].ID, IPv4: ptrAddr("100.110.121.96"), @@ -87,7 +84,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes { }, &types.Node{ ID: 3, - GivenName: "user-mon", + GivenName: "venusaur", User: &users[2], UserID: &users[2].ID, IPv4: ptrAddr("100.103.90.82"), @@ -96,7 +93,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes { }, &types.Node{ ID: 4, - GivenName: "tagged-server", + GivenName: "beedrill", IPv4: ptrAddr("100.108.74.26"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"), Tags: []string{"tag:server"}, @@ -104,7 +101,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes { }, &types.Node{ ID: 5, - GivenName: "tagged-prod", + GivenName: "kakuna", IPv4: ptrAddr("100.103.8.15"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"), Tags: []string{"tag:prod"}, @@ -113,19 +110,6 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes { } } -// convertSSHPolicyEmails converts Tailscale SaaS email domains to -// headscale-compatible format in the raw policy JSON. -// -// The @passkey domain is converted to @example.com. The @dalby.cc domain -// is kept as-is to preserve localpart matching semantics (kristoffer should -// NOT match localpart:*@example.com, just as it doesn't match -// localpart:*@passkey in Tailscale SaaS). -func convertSSHPolicyEmails(s string) string { - s = strings.ReplaceAll(s, "@passkey", "@example.com") - - return s -} - // loadSSHTestFile loads and parses a single SSH capture HuJSON file. func loadSSHTestFile(t *testing.T, path string) *testcapture.Capture { t.Helper() @@ -196,10 +180,11 @@ func TestSSHDataCompat(t *testing.T) { return } - // Use the captured full policy verbatim (with email-domain conversion). - policyJSON := convertSSHPolicyEmails(string(tf.Input.FullPolicy)) + // Use the captured full policy verbatim. Anonymization in + // tscap already rewrites SaaS emails to @example.com. + policyJSON := tf.Input.FullPolicy - pol, err := unmarshalPolicy([]byte(policyJSON)) + pol, err := unmarshalPolicy(policyJSON) require.NoError( t, err,