mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-26 00:06:53 +09:00
1 parent
5401edb6a8
commit
23f7eedac6
2 files changed
+86
-24
No files matched your search
@@ -1,9 +1,9 @@
|
|||||||
// This file implements data-driven via grant compatibility tests using
|
// This file implements data-driven via grant compatibility tests using
|
||||||
// golden data captured from Tailscale SaaS (v29, v30, v31, v33, v35,
|
// golden data captured from Tailscale SaaS. These scenarios exercise via
|
||||||
// v36). These scenarios exercise via grant steering with peer
|
// grant steering with peer connectivity, cross-subnet forwarding and
|
||||||
// connectivity and cross-subnet forwarding.
|
// exit nodes.
|
||||||
//
|
//
|
||||||
// Test data source: ../policy/v2/testdata/grant_results/via-grant-v{29,30,31,33,35,36}.hujson
|
// Test data source: ../policy/v2/testdata/grant_results/, see viaCompatTests
|
||||||
// Source format: github.com/juanfont/headscale/hscontrol/types/testcapture
|
// Source format: github.com/juanfont/headscale/hscontrol/types/testcapture
|
||||||
package servertest_test
|
package servertest_test
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/juanfont/headscale/hscontrol/types/testcapture"
|
"github.com/juanfont/headscale/hscontrol/types/testcapture"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go4.org/netipx"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
)
|
)
|
||||||
@@ -46,14 +47,14 @@ var viaCompatTests = []struct {
|
|||||||
//
|
//
|
||||||
// CROSS-DEPENDENCY WARNING:
|
// CROSS-DEPENDENCY WARNING:
|
||||||
// This test reads golden files from ../policy/v2/testdata/grant_results/
|
// This test reads golden files from ../policy/v2/testdata/grant_results/
|
||||||
// (specifically via-grant-v29, v30, v31, v33, v35, v36). These files are shared
|
// (the ones listed in viaCompatTests). These files are shared
|
||||||
// with TestGrantsCompat in the policy/v2 package. Any changes to the
|
// with TestGrantsCompat in the policy/v2 package. Any changes to the
|
||||||
// file format, field structure, or naming must be coordinated with
|
// file format, field structure, or naming must be coordinated with
|
||||||
// BOTH tests.
|
// BOTH tests.
|
||||||
//
|
//
|
||||||
// Fields consumed by this test (but NOT by TestGrantsCompat):
|
// Fields consumed by this test (but NOT by TestGrantsCompat):
|
||||||
// - captures[name].netmap (Peers, AllowedIPs, PrimaryRoutes, PacketFilterRules)
|
// - captures[name].netmap (Peers, AllowedIPs, PrimaryRoutes, PacketFilterRules)
|
||||||
// - topology.nodes[name].tags (used for servertest node creation)
|
// - topology.nodes[name].tags and .user (used for servertest node creation)
|
||||||
//
|
//
|
||||||
// Fields consumed by TestGrantsCompat (but NOT by this test):
|
// Fields consumed by TestGrantsCompat (but NOT by this test):
|
||||||
// - captures[name].packet_filter_rules (golden filter rule comparison)
|
// - captures[name].packet_filter_rules (golden filter rule comparison)
|
||||||
@@ -88,6 +89,7 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
|
|||||||
|
|
||||||
srv := servertest.NewServer(t)
|
srv := servertest.NewServer(t)
|
||||||
tagUser := srv.CreateUser(t, "tag-user")
|
tagUser := srv.CreateUser(t, "tag-user")
|
||||||
|
users := createCaptureUsers(t, srv)
|
||||||
|
|
||||||
policyJSON := convertCapturePolicy(t, c)
|
policyJSON := convertCapturePolicy(t, c)
|
||||||
|
|
||||||
@@ -100,7 +102,8 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
|
|||||||
srv.App.Change(changes...)
|
srv.App.Change(changes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create tagged clients matching the golden topology.
|
// Create clients matching the golden topology: tagged nodes under
|
||||||
|
// tag-user, user-owned nodes under their capture user.
|
||||||
// Nodes are created in SaaS registration order so headscale assigns
|
// Nodes are created in SaaS registration order so headscale assigns
|
||||||
// sequential DB IDs in the same relative order. This matters for
|
// sequential DB IDs in the same relative order. This matters for
|
||||||
// PrimaryRoutes election which uses lowest-node-ID-wins — the
|
// PrimaryRoutes election which uses lowest-node-ID-wins — the
|
||||||
@@ -110,7 +113,7 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
|
|||||||
|
|
||||||
for _, name := range order {
|
for _, name := range order {
|
||||||
topoNode, exists := c.Topology.Nodes[name]
|
topoNode, exists := c.Topology.Nodes[name]
|
||||||
if !exists || len(topoNode.Tags) == 0 {
|
if !exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +121,17 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
owner := tagUser
|
||||||
|
|
||||||
|
if len(topoNode.Tags) == 0 {
|
||||||
|
require.Containsf(t, users, topoNode.User,
|
||||||
|
"node %s: owner %q is not a capture user", name, topoNode.User)
|
||||||
|
|
||||||
|
owner = users[topoNode.User]
|
||||||
|
}
|
||||||
|
|
||||||
clients[name] = servertest.NewClient(t, srv, name,
|
clients[name] = servertest.NewClient(t, srv, name,
|
||||||
servertest.WithUser(tagUser),
|
servertest.WithUser(owner),
|
||||||
servertest.WithTags(topoNode.Tags...),
|
servertest.WithTags(topoNode.Tags...),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -403,12 +415,14 @@ func compareNetmap(
|
|||||||
var wantDstPrefixes []string
|
var wantDstPrefixes []string
|
||||||
|
|
||||||
for _, dp := range wantRule.DstPorts {
|
for _, dp := range wantRule.DstPorts {
|
||||||
pfx, err := parsePrefixOrAddr(dp.IP)
|
pfxs, err := parseDstPrefixes(dp.IP)
|
||||||
require.NoErrorf(t, err,
|
require.NoErrorf(t, err,
|
||||||
"golden DstPorts[%d].IP %q should parse as prefix or addr", i, dp.IP)
|
"golden DstPorts[%d].IP %q should parse as prefix, addr or range", i, dp.IP)
|
||||||
|
|
||||||
if !isTailscaleIP(pfx) {
|
for _, pfx := range pfxs {
|
||||||
wantDstPrefixes = append(wantDstPrefixes, pfx.String())
|
if !isTailscaleIP(pfx) {
|
||||||
|
wantDstPrefixes = append(wantDstPrefixes, pfx.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,6 +659,35 @@ func parsePrefixOrAddr(s string) (netip.Prefix, error) {
|
|||||||
return netip.PrefixFrom(addr, addr.BitLen()), nil
|
return netip.PrefixFrom(addr, addr.BitLen()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseDstPrefixes parses a golden DstPorts.IP into the prefixes a client
|
||||||
|
// derives from it. Besides a prefix or bare address, SaaS writes "*" for
|
||||||
|
// every address and an address range for autogroup:internet
|
||||||
|
// ("0.0.0.0-9.255.255.255").
|
||||||
|
func parseDstPrefixes(s string) ([]netip.Prefix, error) {
|
||||||
|
if s == "*" {
|
||||||
|
return []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
netip.MustParsePrefix("::/0"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(s, "-") {
|
||||||
|
r, err := netipx.ParseIPRange(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Prefixes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pfx, err := parsePrefixOrAddr(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []netip.Prefix{pfx}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// isTailscaleIP returns true if the prefix is a single-host Tailscale
|
// isTailscaleIP returns true if the prefix is a single-host Tailscale
|
||||||
// address (/32 for IPv4 in CGNAT range, /128 for IPv6 in Tailscale ULA).
|
// address (/32 for IPv4 in CGNAT range, /128 for IPv6 in Tailscale ULA).
|
||||||
func isTailscaleIP(prefix netip.Prefix) bool {
|
func isTailscaleIP(prefix netip.Prefix) bool {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/servertest"
|
"github.com/juanfont/headscale/hscontrol/servertest"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/types/testcapture"
|
"github.com/juanfont/headscale/hscontrol/types/testcapture"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -77,6 +78,7 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
|
|||||||
|
|
||||||
srv := servertest.NewServer(t)
|
srv := servertest.NewServer(t)
|
||||||
tagUser := srv.CreateUser(t, "tag-user")
|
tagUser := srv.CreateUser(t, "tag-user")
|
||||||
|
createCaptureUsers(t, srv)
|
||||||
|
|
||||||
policyJSON := convertCapturePolicy(t, c)
|
policyJSON := convertCapturePolicy(t, c)
|
||||||
|
|
||||||
@@ -377,11 +379,32 @@ func captureNodeOrder(t *testing.T, c *testcapture.Capture) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// captureUserNames maps the SaaS emails in capture policies to the user
|
||||||
|
// names capture topologies give their user-owned nodes.
|
||||||
|
var captureUserNames = map[string]string{
|
||||||
|
"odin@example.com": "odin",
|
||||||
|
"thor@example.org": "thor",
|
||||||
|
"freya@example.com": "freya",
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCaptureUsers creates a headscale user for each capture identity,
|
||||||
|
// keyed by name.
|
||||||
|
func createCaptureUsers(t *testing.T, srv *servertest.TestServer) map[string]*types.User {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
users := make(map[string]*types.User, len(captureUserNames))
|
||||||
|
for _, name := range captureUserNames {
|
||||||
|
users[name] = srv.CreateUser(t, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
// convertCapturePolicy converts a [testcapture.Capture]'s policy for headscale,
|
// convertCapturePolicy converts a [testcapture.Capture]'s policy for headscale,
|
||||||
// replacing SaaS emails with headscale user format. Fails the test if
|
// replacing each SaaS email with the "<name>@" form that resolves to the
|
||||||
// none of the known SaaS emails are present: that would mean the
|
// users [createCaptureUsers] creates. Fails the test if none of the known
|
||||||
// capture was regenerated with a new tag-owner identity and this
|
// SaaS emails are present: that would mean the capture was regenerated
|
||||||
// function needs updating.
|
// with a new identity and captureUserNames needs updating.
|
||||||
func convertCapturePolicy(t *testing.T, c *testcapture.Capture) []byte {
|
func convertCapturePolicy(t *testing.T, c *testcapture.Capture) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -389,21 +412,17 @@ func convertCapturePolicy(t *testing.T, c *testcapture.Capture) []byte {
|
|||||||
|
|
||||||
substituted := false
|
substituted := false
|
||||||
|
|
||||||
for _, email := range []string{
|
for email, name := range captureUserNames {
|
||||||
"odin@example.com",
|
|
||||||
"thor@example.org",
|
|
||||||
"freya@example.com",
|
|
||||||
} {
|
|
||||||
if strings.Contains(s, email) {
|
if strings.Contains(s, email) {
|
||||||
substituted = true
|
substituted = true
|
||||||
s = strings.ReplaceAll(s, email, "tag-user@")
|
s = strings.ReplaceAll(s, email, name+"@")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require.True(
|
require.True(
|
||||||
t,
|
t,
|
||||||
substituted,
|
substituted,
|
||||||
"%s: no known SaaS tag-owner email found in policy; update convertCapturePolicy",
|
"%s: no known SaaS email found in policy; update captureUserNames",
|
||||||
c.TestID,
|
c.TestID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user