2026-01-28 12:04:52 +00:00
|
|
|
package v2
|
|
|
|
|
|
|
|
|
|
// This file contains compatibility tests for subnet routes and exit nodes.
|
|
|
|
|
// It validates Headscale's ACL engine behavior against documented Tailscale
|
|
|
|
|
// SaaS behavior. Tests document behavioral differences with TODO comments.
|
|
|
|
|
//
|
|
|
|
|
// Source findings: /home/kradalby/acl-explore/findings/{10,11,12,13,14,15}-*.md
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/netip"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
|
"github.com/juanfont/headscale/hscontrol/policy/policyutil"
|
|
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
"tailscale.com/net/tsaddr"
|
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// wildcardSrcIPs represents the SrcIPs used when wildcard (*) source is specified.
|
|
|
|
|
// In Tailscale, this includes the CGNAT range and IPv6 Tailscale range, plus any
|
|
|
|
|
// advertised subnet routes.
|
|
|
|
|
var wildcardSrcIPs = []string{
|
|
|
|
|
"100.64.0.0/10", // CGNAT range for Tailscale IPs
|
|
|
|
|
"fd7a:115c:a1e0::/48", // Tailscale IPv6 range
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// memberSrcIPs represents the SrcIPs for autogroup:member (user-owned nodes).
|
|
|
|
|
// This includes client1, client2, and user1.
|
|
|
|
|
var memberSrcIPs = []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wildcardDstPorts represents wildcard destination ports using {IP: "*"}.
|
|
|
|
|
var wildcardDstPorts = []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setupRouteCompatUsers returns the test users for route compatibility tests.
|
|
|
|
|
func setupRouteCompatUsers() types.Users {
|
|
|
|
|
return types.Users{
|
|
|
|
|
{Model: gorm.Model{ID: 1}, Name: "kratail2tid"},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setupRouteCompatNodes returns the test nodes for route compatibility tests.
|
|
|
|
|
// The node configuration includes:
|
|
|
|
|
// - 2 client nodes (user-owned, no routes)
|
|
|
|
|
// - 1 subnet router (tag:router, 10.33.0.0/16)
|
|
|
|
|
// - 1 exit node (tag:exit, 0.0.0.0/0, ::/0)
|
|
|
|
|
// - 1 multi-router (tag:router + tag:exit, 172.16.0.0/24 + exit routes)
|
|
|
|
|
// - 2 HA routers (tag:ha, both advertise 192.168.1.0/24)
|
|
|
|
|
// - 1 big router (tag:router, 10.0.0.0/8)
|
|
|
|
|
// - 1 user-owned node (user1).
|
|
|
|
|
func setupRouteCompatNodes(users types.Users) types.Nodes {
|
|
|
|
|
// Node: client1 - User-owned client (no routes)
|
|
|
|
|
nodeClient1 := &types.Node{
|
|
|
|
|
ID: 1,
|
|
|
|
|
GivenName: "client1",
|
|
|
|
|
User: &users[0],
|
|
|
|
|
UserID: &users[0].ID,
|
|
|
|
|
IPv4: ptrAddr("100.116.73.38"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::a801:4949"),
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: client2 - User-owned client (no routes)
|
|
|
|
|
nodeClient2 := &types.Node{
|
|
|
|
|
ID: 2,
|
|
|
|
|
GivenName: "client2",
|
|
|
|
|
User: &users[0],
|
|
|
|
|
UserID: &users[0].ID,
|
|
|
|
|
IPv4: ptrAddr("100.89.42.23"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::d01:2a2e"),
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: subnet-router - Tagged with tag:router, advertises 10.33.0.0/16
|
|
|
|
|
nodeSubnetRouter := &types.Node{
|
|
|
|
|
ID: 3,
|
|
|
|
|
GivenName: "subnet-router",
|
|
|
|
|
IPv4: ptrAddr("100.119.139.79"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::4001:8ba0"),
|
|
|
|
|
Tags: []string{"tag:router"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("10.33.0.0/16"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("10.33.0.0/16"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: exit-node - Tagged with tag:exit, advertises exit routes
|
|
|
|
|
nodeExitNode := &types.Node{
|
|
|
|
|
ID: 4,
|
|
|
|
|
GivenName: "exit-node",
|
|
|
|
|
IPv4: ptrAddr("100.121.32.1"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::7f01:2004"),
|
|
|
|
|
Tags: []string{"tag:exit"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: tsaddr.ExitRoutes(),
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: tsaddr.ExitRoutes(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: multi-router - Tagged with tag:router AND tag:exit
|
|
|
|
|
// Advertises both subnet (172.16.0.0/24) and exit routes
|
|
|
|
|
multiRouterRoutes := append([]netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("172.16.0.0/24"),
|
|
|
|
|
}, tsaddr.ExitRoutes()...)
|
|
|
|
|
nodeMultiRouter := &types.Node{
|
|
|
|
|
ID: 5,
|
|
|
|
|
GivenName: "multi-router",
|
|
|
|
|
IPv4: ptrAddr("100.74.117.7"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::c401:7508"),
|
|
|
|
|
Tags: []string{"tag:router", "tag:exit"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: multiRouterRoutes,
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: multiRouterRoutes,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: ha-router1 - Tagged with tag:ha, advertises 192.168.1.0/24
|
|
|
|
|
nodeHARouter1 := &types.Node{
|
|
|
|
|
ID: 6,
|
|
|
|
|
GivenName: "ha-router1",
|
|
|
|
|
IPv4: ptrAddr("100.85.37.108"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::f101:2597"),
|
|
|
|
|
Tags: []string{"tag:ha"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("192.168.1.0/24"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("192.168.1.0/24"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: ha-router2 - Tagged with tag:ha, advertises same 192.168.1.0/24
|
|
|
|
|
nodeHARouter2 := &types.Node{
|
|
|
|
|
ID: 7,
|
|
|
|
|
GivenName: "ha-router2",
|
|
|
|
|
IPv4: ptrAddr("100.119.130.32"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::4501:82a9"),
|
|
|
|
|
Tags: []string{"tag:ha"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("192.168.1.0/24"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("192.168.1.0/24"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: big-router - Tagged with tag:router, advertises 10.0.0.0/8
|
|
|
|
|
nodeBigRouter := &types.Node{
|
|
|
|
|
ID: 8,
|
|
|
|
|
GivenName: "big-router",
|
|
|
|
|
IPv4: ptrAddr("100.100.100.1"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::6401:6401"),
|
|
|
|
|
Tags: []string{"tag:router"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("10.0.0.0/8"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("10.0.0.0/8"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node: user1 - User-owned node (no routes)
|
|
|
|
|
nodeUser1 := &types.Node{
|
|
|
|
|
ID: 9,
|
|
|
|
|
GivenName: "user1",
|
|
|
|
|
User: &users[0],
|
|
|
|
|
UserID: &users[0].ID,
|
|
|
|
|
IPv4: ptrAddr("100.90.199.68"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::2d01:c747"),
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return types.Nodes{
|
|
|
|
|
nodeClient1,
|
|
|
|
|
nodeClient2,
|
|
|
|
|
nodeSubnetRouter,
|
|
|
|
|
nodeExitNode,
|
|
|
|
|
nodeMultiRouter,
|
|
|
|
|
nodeHARouter1,
|
|
|
|
|
nodeHARouter2,
|
|
|
|
|
nodeBigRouter,
|
|
|
|
|
nodeUser1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// routesPolicyPrefix provides the standard groups, tagOwners, and hosts
|
|
|
|
|
// for route compatibility tests.
|
|
|
|
|
const routesPolicyPrefix = `{
|
|
|
|
|
"groups": {
|
|
|
|
|
"group:admins": ["kratail2tid@"],
|
|
|
|
|
"group:empty": []
|
|
|
|
|
},
|
|
|
|
|
"tagOwners": {
|
|
|
|
|
"tag:router": ["kratail2tid@"],
|
|
|
|
|
"tag:exit": ["kratail2tid@"],
|
|
|
|
|
"tag:ha": ["kratail2tid@"]
|
|
|
|
|
},
|
|
|
|
|
"hosts": {
|
|
|
|
|
"internal": "10.0.0.0/8",
|
|
|
|
|
"subnet24": "192.168.1.0/24"
|
|
|
|
|
},
|
|
|
|
|
"acls": [`
|
|
|
|
|
|
|
|
|
|
const routesPolicySuffix = `
|
|
|
|
|
]
|
|
|
|
|
}`
|
|
|
|
|
|
|
|
|
|
// makeRoutesPolicy creates a full policy from just the ACL rules portion.
|
|
|
|
|
func makeRoutesPolicy(aclRules string) string {
|
|
|
|
|
return routesPolicyPrefix + aclRules + routesPolicySuffix
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// routesCompatTest defines a test case for routes compatibility testing.
|
|
|
|
|
type routesCompatTest struct {
|
|
|
|
|
name string // Test name
|
|
|
|
|
policy string // HuJSON policy as multiline raw string
|
|
|
|
|
wantFilters map[string][]tailcfg.FilterRule // node GivenName -> expected filters
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatSubnetBasics tests basic subnet route behavior (Category A).
|
|
|
|
|
// These tests verify that subnet routes are correctly included in SrcIPs for wildcard rules,
|
|
|
|
|
// that tag-based ACLs resolve to node IPs (not routes), and that explicit subnet filters
|
|
|
|
|
// are placed on the correct destination nodes.
|
|
|
|
|
func TestTailscaleRoutesCompatSubnetBasics(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// A1: Wildcard ACL includes subnet routes in SrcIPs
|
|
|
|
|
{
|
|
|
|
|
name: "A1_wildcard_acl_includes_routes_in_srcips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// When using * -> *:*, SrcIPs should include advertised subnet routes
|
|
|
|
|
// (but NOT exit routes 0.0.0.0/0, ::/0).
|
|
|
|
|
// TODO: Verify Tailscale includes subnet routes 10.33.0.0/16, 172.16.0.0/24,
|
|
|
|
|
// 192.168.1.0/24, 10.0.0.0/8 in SrcIPs but NOT 0.0.0.0/0, ::/0
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
// TODO: Tailscale also includes these subnet routes:
|
|
|
|
|
// "10.0.0.0/8",
|
|
|
|
|
// "10.33.0.0/16",
|
|
|
|
|
// "172.16.0.0/24",
|
|
|
|
|
// "192.168.1.0/24",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix DstPorts expansion and exit node coverage for tag-based ACLs
|
|
|
|
|
//
|
|
|
|
|
// A2: Tag-based ACL resolves to node IPs only, NOT routes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - tag:router includes: subnet-router, multi-router, big-router
|
|
|
|
|
// - Each tag:router node receives filter with ALL tag:router IPs in DstPorts
|
|
|
|
|
// - exit-node (tag:exit only) does NOT receive any filter
|
|
|
|
|
// - DstPorts contains ONLY node IPs, NOT advertised routes
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - INCORRECT: Each node only gets ITS OWN IPs in DstPorts (should be ALL tag IPs)
|
|
|
|
|
// - INCORRECT: exit-node receives a filter because multi-router has exit routes
|
|
|
|
|
// and Headscale treats 0.0.0.0/0 as covering node IPs for filter distribution
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Filter compilation only adds destinations for the current node being filtered,
|
|
|
|
|
// not all nodes matching the tag
|
|
|
|
|
// 2. Exit routes (0.0.0.0/0, ::/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. When dst is a tag, include ALL IPs of nodes with that tag in DstPorts
|
|
|
|
|
// 2. Exclude exit routes from filter coverage calculations
|
|
|
|
|
{
|
|
|
|
|
name: "A2_tag_based_acl_excludes_routes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:router"], "dst": ["tag:router:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// EXPECTED (Tailscale) - commented out:
|
|
|
|
|
// wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// "client1": nil,
|
|
|
|
|
// "client2": nil,
|
|
|
|
|
// "exit-node": nil, // tag:exit only, not tag:router
|
|
|
|
|
// "ha-router1": nil,
|
|
|
|
|
// "ha-router2": nil,
|
|
|
|
|
// "user1": nil,
|
|
|
|
|
// "subnet-router": {
|
|
|
|
|
// {
|
|
|
|
|
// SrcIPs: []string{
|
|
|
|
|
// "100.100.100.1/32", "100.119.139.79/32", "100.74.117.7/32",
|
|
|
|
|
// "fd7a:115c:a1e0::4001:8ba0/128", "fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
// "fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
// },
|
|
|
|
|
// DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// // ALL tag:router IPs, not just this node's IP
|
|
|
|
|
// {IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// {IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// {IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// {IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// {IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// {IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// },
|
|
|
|
|
// IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
// },
|
|
|
|
|
// },
|
|
|
|
|
// // Same for multi-router and big-router...
|
|
|
|
|
// },
|
|
|
|
|
//
|
|
|
|
|
// ACTUAL (Headscale) - current behavior:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// INCORRECT: subnet-router only gets its OWN IPs in DstPorts
|
|
|
|
|
// Tailscale includes ALL tag:router IPs
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// INCORRECT: Only this node's IPs, should be ALL tag:router IPs
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// multi-router has BOTH tag:router AND tag:exit
|
|
|
|
|
// Because of exit routes, filter merging includes all tag:router destinations
|
|
|
|
|
// This is actually the CORRECT Tailscale behavior for DstPorts (but wrong filter distribution)
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// All tag:router IPs due to exit route coverage + filter merging
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: big-router only gets its OWN IPs in DstPorts
|
|
|
|
|
// Tailscale includes ALL tag:router IPs
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// INCORRECT: Only this node's IPs, should be ALL tag:router IPs
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: exit-node receives filter due to multi-router having exit routes
|
|
|
|
|
// and Headscale treating 0.0.0.0/0 as covering node IPs
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// A3: Explicit subnet destination - filter goes to router only
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Filter placed ONLY on nodes whose routes cover the destination
|
|
|
|
|
// - subnet-router (10.33.0.0/16) gets filter - exact match
|
|
|
|
|
// - big-router (10.0.0.0/8) gets filter - parent covers child
|
|
|
|
|
// - exit-node and multi-router get NO filter - exit routes (0.0.0.0/0)
|
|
|
|
|
// do NOT count as "covering" subnet destinations for filter placement
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes (0.0.0.0/0) ARE treated as covering all destinations
|
|
|
|
|
// - exit-node and multi-router incorrectly receive the filter
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// hscontrol/policy/v2/filter.go treats exit routes (0.0.0.0/0, ::/0) as
|
|
|
|
|
// covering all destinations, but Tailscale only uses exit routes for
|
|
|
|
|
// actual traffic routing, not for filter distribution.
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// When determining which nodes receive a filter based on route coverage,
|
|
|
|
|
// exclude exit routes (0.0.0.0/0 and ::/0) from the coverage check.
|
|
|
|
|
// Exit nodes should only receive filters when explicitly targeted.
|
|
|
|
|
{
|
|
|
|
|
name: "A3_explicit_subnet_filter_to_router",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// EXPECTED (Tailscale) - commented out:
|
|
|
|
|
// wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// "client1": nil,
|
|
|
|
|
// "client2": nil,
|
|
|
|
|
// "exit-node": nil, // Exit route does NOT cover for filter placement
|
|
|
|
|
// "ha-router1": nil,
|
|
|
|
|
// "ha-router2": nil,
|
|
|
|
|
// "user1": nil,
|
|
|
|
|
// "multi-router": nil, // Exit route does NOT cover for filter placement
|
|
|
|
|
// "subnet-router": {
|
|
|
|
|
// {
|
|
|
|
|
// SrcIPs: []string{"100.64.0.0/10", "fd7a:115c:a1e0::/48"},
|
|
|
|
|
// DstPorts: []tailcfg.NetPortRange{{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
// IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
// },
|
|
|
|
|
// },
|
|
|
|
|
// "big-router": {
|
|
|
|
|
// {
|
|
|
|
|
// SrcIPs: []string{"100.64.0.0/10", "fd7a:115c:a1e0::/48"},
|
|
|
|
|
// DstPorts: []tailcfg.NetPortRange{{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
// IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
// },
|
|
|
|
|
// },
|
|
|
|
|
// },
|
|
|
|
|
//
|
|
|
|
|
// ACTUAL (Headscale) - current behavior:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// subnet-router owns 10.33.0.0/16 - exact match (CORRECT)
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// big-router owns 10.0.0.0/8 which covers 10.33.0.0/16 (CORRECT)
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: exit-node gets filter because 0.0.0.0/0 "covers" destination
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: multi-router gets filter because 0.0.0.0/0 "covers" destination
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// A3b: autogroup:member to subnet - SrcIPs = member IPs only
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - autogroup:member = user-owned nodes only (client1, client2, user1)
|
|
|
|
|
// - Filter goes to subnet-router (exact match) and big-router (parent route)
|
|
|
|
|
// - exit-node and multi-router get NO filter (exit routes don't cover)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - exit-node and multi-router incorrectly receive filters because
|
|
|
|
|
// exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Same as A3 - exit routes should not count for filter distribution
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes (0.0.0.0/0 and ::/0) from coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "A3b_autogroup_member_to_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// EXPECTED (Tailscale) - commented out:
|
|
|
|
|
// wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// "client1": nil,
|
|
|
|
|
// "client2": nil,
|
|
|
|
|
// "exit-node": nil, // Exit route does NOT cover
|
|
|
|
|
// "ha-router1": nil,
|
|
|
|
|
// "ha-router2": nil,
|
|
|
|
|
// "user1": nil,
|
|
|
|
|
// "multi-router": nil, // Exit route does NOT cover
|
|
|
|
|
// "subnet-router": { ... }, // Exact match
|
|
|
|
|
// "big-router": { ... }, // Parent route covers
|
|
|
|
|
// },
|
|
|
|
|
//
|
|
|
|
|
// ACTUAL (Headscale) - current behavior:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// CORRECT: subnet-router gets filter (exact match)
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.89.42.23/32", // client2
|
|
|
|
|
"100.90.199.68/32", // user1
|
|
|
|
|
"100.116.73.38/32", // client1
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// CORRECT: big-router gets filter (parent route 10.0.0.0/8 covers)
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: exit-node receives filter due to 0.0.0.0/0 coverage
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: multi-router receives filter due to 0.0.0.0/0 coverage
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// A4: Multiple routes on same router (172.16.0.0/24 destination)
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - multi-router has 172.16.0.0/24, should get filter (exact match)
|
|
|
|
|
// - exit-node has 0.0.0.0/0 but does NOT cover 172.16.0.0/24 for filter placement
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - multi-router correctly gets filter
|
|
|
|
|
// - exit-node incorrectly gets filter because 0.0.0.0/0 is treated as covering
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Same as A3 - exit routes should not count for filter distribution
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "A4_multiple_routes_same_router",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["172.16.0.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// EXPECTED (Tailscale) - commented out:
|
|
|
|
|
// wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// "client1": nil,
|
|
|
|
|
// "client2": nil,
|
|
|
|
|
// "subnet-router": nil,
|
|
|
|
|
// "exit-node": nil, // 0.0.0.0/0 does NOT cover for filter placement
|
|
|
|
|
// "ha-router1": nil,
|
|
|
|
|
// "ha-router2": nil,
|
|
|
|
|
// "big-router": nil,
|
|
|
|
|
// "user1": nil,
|
|
|
|
|
// "multi-router": { ... }, // Exact match
|
|
|
|
|
// },
|
|
|
|
|
//
|
|
|
|
|
// ACTUAL (Headscale) - current behavior:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// CORRECT: multi-router gets filter (exact match for 172.16.0.0/24)
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "172.16.0.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: exit-node receives filter due to 0.0.0.0/0 coverage
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "172.16.0.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// A5: Host alias to subnet (uses "internal" = "10.0.0.0/8")
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - "internal" resolves to 10.0.0.0/8 via hosts alias
|
|
|
|
|
// - big-router (10.0.0.0/8) gets filter - exact match
|
|
|
|
|
// - subnet-router (10.33.0.0/16) gets filter - child route
|
|
|
|
|
// - exit-node and multi-router get NO filter (exit routes don't cover)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - big-router and subnet-router correctly get filters
|
|
|
|
|
// - exit-node and multi-router incorrectly get filters (exit route coverage)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Same as A3 - exit routes should not count for filter distribution
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "A5_host_alias_to_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["internal:22"]}
|
|
|
|
|
`),
|
|
|
|
|
// EXPECTED (Tailscale) - commented out:
|
|
|
|
|
// wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// "client1": nil,
|
|
|
|
|
// "client2": nil,
|
|
|
|
|
// "exit-node": nil, // Exit route does NOT cover
|
|
|
|
|
// "ha-router1": nil,
|
|
|
|
|
// "ha-router2": nil,
|
|
|
|
|
// "user1": nil,
|
|
|
|
|
// "multi-router": nil, // Exit route does NOT cover
|
|
|
|
|
// "subnet-router": { ... }, // Child route
|
|
|
|
|
// "big-router": { ... }, // Exact match
|
|
|
|
|
// },
|
|
|
|
|
//
|
|
|
|
|
// ACTUAL (Headscale) - current behavior:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// CORRECT: subnet-router gets filter (child route 10.33.0.0/16 within 10.0.0.0/8)
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// CORRECT: big-router gets filter (exact match for 10.0.0.0/8)
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: exit-node receives filter due to 0.0.0.0/0 coverage
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: multi-router receives filter due to 0.0.0.0/0 coverage
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
pol, err := unmarshalPolicy([]byte(tt.policy))
|
|
|
|
|
require.NoError(t, err, "failed to parse policy")
|
|
|
|
|
|
|
|
|
|
err = pol.validate()
|
|
|
|
|
require.NoError(t, err, "policy validation failed")
|
|
|
|
|
|
|
|
|
|
for nodeName, wantFilters := range tt.wantFilters {
|
|
|
|
|
node := findNodeByGivenName(nodes, nodeName)
|
|
|
|
|
require.NotNil(t, node, "node %s not found", nodeName)
|
|
|
|
|
|
|
|
|
|
compiledFilters, err := pol.compileFilterRulesForNode(users, node.View(), nodes.ViewSlice())
|
|
|
|
|
require.NoError(t, err, "failed to compile filters for node %s", nodeName)
|
|
|
|
|
|
|
|
|
|
gotFilters := policyutil.ReduceFilterRules(node.View(), compiledFilters)
|
|
|
|
|
|
|
|
|
|
if len(wantFilters) == 0 && len(gotFilters) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if diff := cmp.Diff(wantFilters, gotFilters, cmpOptions()...); diff != "" {
|
|
|
|
|
t.Errorf("node %s filters mismatch (-want +got):\n%s", nodeName, diff)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatExitNodes tests exit node behavior (Category B).
|
|
|
|
|
// These tests verify that exit routes (0.0.0.0/0, ::/0) are NOT included in SrcIPs,
|
|
|
|
|
// that exit nodes can cover external destinations, and autogroup:internet behavior.
|
|
|
|
|
func TestTailscaleRoutesCompatExitNodes(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
// Standard wildcard filter that all nodes receive for * -> *:* ACL
|
|
|
|
|
wildcardFilter := []tailcfg.FilterRule{
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Verify Tailscale includes subnet routes in SrcIPs for wildcard ACLs
|
|
|
|
|
//
|
|
|
|
|
// B1: Exit routes NOT in SrcIPs with wildcard ACL
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs includes CGNAT + IPv6 Tailscale ranges
|
|
|
|
|
// - SrcIPs also includes advertised subnet routes (10.0.0.0/8, etc.)
|
|
|
|
|
// - Exit routes (0.0.0.0/0, ::/0) are NOT included in SrcIPs
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs only includes CGNAT + IPv6 Tailscale ranges
|
|
|
|
|
// - Subnet routes are NOT included in SrcIPs (might be a difference)
|
|
|
|
|
// - Exit routes correctly NOT included
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale doesn't expand wildcard source to include subnet routes
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED (if needed):
|
|
|
|
|
// Add subnet routes to SrcIPs when source is wildcard
|
|
|
|
|
{
|
|
|
|
|
name: "B1_exit_routes_not_in_srcips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// All nodes receive the same wildcard filter
|
|
|
|
|
// Key verification: exit routes NOT in SrcIPs (they're not - correct!)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"exit-node": wildcardFilter,
|
|
|
|
|
"client1": wildcardFilter,
|
|
|
|
|
"client2": wildcardFilter,
|
|
|
|
|
"multi-router": wildcardFilter,
|
|
|
|
|
"subnet-router": wildcardFilter,
|
|
|
|
|
"ha-router1": wildcardFilter,
|
|
|
|
|
"ha-router2": wildcardFilter,
|
|
|
|
|
"big-router": wildcardFilter,
|
|
|
|
|
"user1": wildcardFilter,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// B2: tag:exit excludes exit routes from DstPorts
|
|
|
|
|
{
|
|
|
|
|
name: "B2_tag_exit_excludes_exit_routes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:exit"], "dst": ["tag:exit:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// tag:exit includes: exit-node, multi-router
|
|
|
|
|
// DstPorts should contain ONLY their node IPs, NOT exit routes
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32", // exit-node IPv4
|
|
|
|
|
"100.74.117.7/32", // multi-router IPv4
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128", // exit-node IPv6
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128", // multi-router IPv6
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Node IPs only, NOT exit routes (0.0.0.0/0, ::/0)
|
|
|
|
|
{IP: "100.121.32.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::7f01:2004/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.121.32.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::7f01:2004/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Verify Tailscale includes subnet routes in SrcIPs
|
|
|
|
|
//
|
|
|
|
|
// B4: Multi-router has both subnet and exit routes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - multi-router has 172.16.0.0/24 (subnet) + 0.0.0.0/0,::/0 (exit)
|
|
|
|
|
// - SrcIPs may include 172.16.0.0/24 but NOT 0.0.0.0/0 or ::/0
|
|
|
|
|
// - Only multi-router node may receive the filter (needs verification)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - All nodes receive the same wildcard filter
|
|
|
|
|
// - SrcIPs is just CGNAT + IPv6 range, no subnet routes
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale distributes wildcard filters to all nodes
|
|
|
|
|
{
|
|
|
|
|
name: "B4_multi_router_has_both_route_types",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// EXPECTED (Tailscale) - commented out:
|
|
|
|
|
// wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// "multi-router": {
|
|
|
|
|
// {
|
|
|
|
|
// SrcIPs: []string{
|
|
|
|
|
// "100.64.0.0/10",
|
|
|
|
|
// "fd7a:115c:a1e0::/48",
|
|
|
|
|
// // Tailscale may include 172.16.0.0/24 here
|
|
|
|
|
// // but definitely NOT 0.0.0.0/0 or ::/0
|
|
|
|
|
// },
|
|
|
|
|
// DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// {IP: "100.64.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// {IP: "fd7a:115c:a1e0::/48", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
// },
|
|
|
|
|
// IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
// },
|
|
|
|
|
// },
|
|
|
|
|
// "client1": nil,
|
|
|
|
|
// "client2": nil,
|
|
|
|
|
// "subnet-router": nil,
|
|
|
|
|
// "exit-node": nil,
|
|
|
|
|
// "ha-router1": nil,
|
|
|
|
|
// "ha-router2": nil,
|
|
|
|
|
// "big-router": nil,
|
|
|
|
|
// "user1": nil,
|
|
|
|
|
// },
|
|
|
|
|
//
|
|
|
|
|
// ACTUAL (Headscale) - all nodes get wildcard filter:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"multi-router": wildcardFilter,
|
|
|
|
|
"client1": wildcardFilter,
|
|
|
|
|
"client2": wildcardFilter,
|
|
|
|
|
"subnet-router": wildcardFilter,
|
|
|
|
|
"exit-node": wildcardFilter,
|
|
|
|
|
"ha-router1": wildcardFilter,
|
|
|
|
|
"ha-router2": wildcardFilter,
|
|
|
|
|
"big-router": wildcardFilter,
|
|
|
|
|
"user1": wildcardFilter,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// B8: autogroup:internet generates no filters
|
|
|
|
|
//
|
2026-01-28 13:08:38 +00:00
|
|
|
// autogroup:internet is handled by exit node routing via AllowedIPs,
|
|
|
|
|
// not by packet filtering. ALL nodes should get null/empty filters.
|
2026-01-28 12:04:52 +00:00
|
|
|
{
|
|
|
|
|
name: "B8_autogroup_internet_no_filters",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:internet:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// B3: Exit node advertises exit routes (verify RoutableIPs)
|
|
|
|
|
//
|
|
|
|
|
// This test verifies that exit-node has 0.0.0.0/0 and ::/0 in RoutableIPs.
|
2026-01-28 12:05:08 +00:00
|
|
|
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
2026-01-28 12:04:52 +00:00
|
|
|
{
|
|
|
|
|
name: "B3_exit_node_advertises_routes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": wildcardFilter,
|
|
|
|
|
"client2": wildcardFilter,
|
|
|
|
|
"subnet-router": wildcardFilter,
|
|
|
|
|
"exit-node": wildcardFilter,
|
|
|
|
|
"multi-router": wildcardFilter,
|
|
|
|
|
"ha-router1": wildcardFilter,
|
|
|
|
|
"ha-router2": wildcardFilter,
|
|
|
|
|
"big-router": wildcardFilter,
|
|
|
|
|
"user1": wildcardFilter,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// B5: Exit node with wildcard destination has ExitNodeOption
|
|
|
|
|
//
|
|
|
|
|
// Exit nodes should have ExitNodeOption=true in MapResponse.
|
2026-01-28 12:05:08 +00:00
|
|
|
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
2026-01-28 12:04:52 +00:00
|
|
|
{
|
|
|
|
|
name: "B5_exit_with_wildcard_dst",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": wildcardFilter,
|
|
|
|
|
"client2": wildcardFilter,
|
|
|
|
|
"subnet-router": wildcardFilter,
|
|
|
|
|
"exit-node": wildcardFilter,
|
|
|
|
|
"multi-router": wildcardFilter,
|
|
|
|
|
"ha-router1": wildcardFilter,
|
|
|
|
|
"ha-router2": wildcardFilter,
|
|
|
|
|
"big-router": wildcardFilter,
|
|
|
|
|
"user1": wildcardFilter,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Verify Tailscale filter distribution for tag source with wildcard destination
|
|
|
|
|
//
|
|
|
|
|
// B6: ExitNodeOption field verification
|
|
|
|
|
//
|
|
|
|
|
// ACL: tag:exit -> *:*
|
|
|
|
|
// Nodes with approved exit routes should have ExitNodeOption=true.
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Need to verify if only exit-tagged nodes receive filters
|
|
|
|
|
// - Or if ALL nodes (destinations) receive filters
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - ALL nodes receive filters (they're all destinations)
|
|
|
|
|
// - SrcIPs = tag:exit node IPs
|
|
|
|
|
// - DstPorts = explicit CIDR ranges (not "*")
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// The test expected only exit-tagged nodes to get filters, but with
|
|
|
|
|
// `tag:exit -> *:*`, all nodes are destinations and should get filters.
|
|
|
|
|
{
|
|
|
|
|
name: "B6_exit_node_option_field",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:exit"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale) - need verification:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil, // Or filter?
|
|
|
|
|
"client2": nil, // Or filter?
|
|
|
|
|
"subnet-router": nil, // Or filter?
|
|
|
|
|
"ha-router1": nil, // Or filter?
|
|
|
|
|
"ha-router2": nil, // Or filter?
|
|
|
|
|
"big-router": nil, // Or filter?
|
|
|
|
|
"user1": nil, // Or filter?
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
// All nodes receive filters (they're all destinations)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Same as B6 - verify Tailscale filter distribution
|
|
|
|
|
//
|
|
|
|
|
// B7: Multiple exit nodes verification
|
|
|
|
|
//
|
|
|
|
|
// ACL: tag:exit -> *:*
|
|
|
|
|
// Both exit-node and multi-router have tag:exit.
|
|
|
|
|
// Same pattern as B6 - all nodes are destinations and receive filters.
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Need to verify actual filter distribution
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - All nodes receive filters (same as B6)
|
|
|
|
|
{
|
|
|
|
|
name: "B7_multiple_exit_nodes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:exit"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale) - need verification:
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil, // Or filter?
|
|
|
|
|
// ... same pattern as B6
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
// All nodes receive filters (same as B6)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// B9: Exit routes appear in peer AllowedIPs
|
|
|
|
|
//
|
|
|
|
|
// When viewing exit-node as a peer, AllowedIPs should include exit routes.
|
2026-01-28 12:05:08 +00:00
|
|
|
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
2026-01-28 12:04:52 +00:00
|
|
|
{
|
|
|
|
|
name: "B9_exit_routes_in_allowedips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": wildcardFilter,
|
|
|
|
|
"client2": wildcardFilter,
|
|
|
|
|
"subnet-router": wildcardFilter,
|
|
|
|
|
"exit-node": wildcardFilter,
|
|
|
|
|
"multi-router": wildcardFilter,
|
|
|
|
|
"ha-router1": wildcardFilter,
|
|
|
|
|
"ha-router2": wildcardFilter,
|
|
|
|
|
"big-router": wildcardFilter,
|
|
|
|
|
"user1": wildcardFilter,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// B10: Exit routes NOT in PrimaryRoutes field
|
|
|
|
|
//
|
|
|
|
|
// PrimaryRoutes is for subnet routes only, not exit routes.
|
|
|
|
|
// Exit routes (0.0.0.0/0, ::/0) should NOT appear in PrimaryRoutes.
|
2026-01-28 12:05:08 +00:00
|
|
|
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
2026-01-28 12:04:52 +00:00
|
|
|
{
|
|
|
|
|
name: "B10_exit_routes_not_in_primaryroutes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": wildcardFilter,
|
|
|
|
|
"client2": wildcardFilter,
|
|
|
|
|
"subnet-router": wildcardFilter,
|
|
|
|
|
"exit-node": wildcardFilter,
|
|
|
|
|
"multi-router": wildcardFilter,
|
|
|
|
|
"ha-router1": wildcardFilter,
|
|
|
|
|
"ha-router2": wildcardFilter,
|
|
|
|
|
"big-router": wildcardFilter,
|
|
|
|
|
"user1": wildcardFilter,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
pol, err := unmarshalPolicy([]byte(tt.policy))
|
|
|
|
|
require.NoError(t, err, "failed to parse policy")
|
|
|
|
|
|
|
|
|
|
err = pol.validate()
|
|
|
|
|
require.NoError(t, err, "policy validation failed")
|
|
|
|
|
|
|
|
|
|
for nodeName, wantFilters := range tt.wantFilters {
|
|
|
|
|
node := findNodeByGivenName(nodes, nodeName)
|
|
|
|
|
require.NotNil(t, node, "node %s not found", nodeName)
|
|
|
|
|
|
|
|
|
|
compiledFilters, err := pol.compileFilterRulesForNode(users, node.View(), nodes.ViewSlice())
|
|
|
|
|
require.NoError(t, err, "failed to compile filters for node %s", nodeName)
|
|
|
|
|
|
|
|
|
|
gotFilters := policyutil.ReduceFilterRules(node.View(), compiledFilters)
|
|
|
|
|
|
|
|
|
|
if len(wantFilters) == 0 && len(gotFilters) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if diff := cmp.Diff(wantFilters, gotFilters, cmpOptions()...); diff != "" {
|
|
|
|
|
t.Errorf("node %s filters mismatch (-want +got):\n%s", nodeName, diff)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatHARouters tests HA router behavior (Category E).
|
|
|
|
|
// These tests verify that multiple routers can advertise the same subnet,
|
|
|
|
|
// and that both receive filters even though only one is primary.
|
|
|
|
|
func TestTailscaleRoutesCompatHARouters(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// E1: Two HA routers advertise same subnet - both enabled
|
|
|
|
|
//
|
|
|
|
|
// ACL: * -> 192.168.1.0/24:*
|
|
|
|
|
// Both ha-router1 and ha-router2 advertise 192.168.1.0/24.
|
|
|
|
|
// Both should receive the filter (both are approved, one is primary).
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only HA routers get filters (exact route match)
|
|
|
|
|
// - Exit nodes do NOT get filters (exit routes don't cover for placement)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - HA routers correctly get filters
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" destination
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Same as A3 - exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "E1_ha_two_routers_same_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
// HA routers correctly get filters, but exit nodes also incorrectly get them
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// CORRECT: Both HA routers get the filter
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters due to 0.0.0.0/0 coverage
|
|
|
|
|
// Tailscale would return nil here
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// E4: HA routers with host alias
|
|
|
|
|
//
|
|
|
|
|
// ACL: * -> subnet24:22 (subnet24 = 192.168.1.0/24)
|
|
|
|
|
// Same as E1 but uses host alias. Exit route coverage issue applies.
|
|
|
|
|
{
|
|
|
|
|
name: "E4_ha_both_get_filters_host_alias",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["subnet24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": { ... },
|
|
|
|
|
"ha-router2": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters due to 0.0.0.0/0 coverage
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// E2: HA primary node appears in peer AllowedIPs
|
|
|
|
|
// Same exit route coverage issue as E1.
|
|
|
|
|
{
|
|
|
|
|
name: "E2_ha_primary_in_allowedips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
// ... only HA routers get filters
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// E3: HA secondary does NOT have route in AllowedIPs
|
|
|
|
|
// Same exit route coverage issue as E1.
|
|
|
|
|
{
|
|
|
|
|
name: "E3_ha_secondary_no_route_in_allowedips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
// ... only HA routers get filters
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// E5: First advertiser becomes primary, both HA routers get filters
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only HA routers get filters (they own 192.168.1.0/24)
|
|
|
|
|
// - Exit nodes do NOT get filters (exit routes don't count for coverage)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - HA routers correctly get filters
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" destination
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Same as E1-E4 - exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "E5_first_advertiser_is_primary",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes incorrectly get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatFilterPlacement tests filter placement rules (Category F).
|
|
|
|
|
// These tests verify that filters go to DESTINATION nodes (route owners),
|
|
|
|
|
// not to source nodes, and that route coverage rules are applied correctly.
|
|
|
|
|
func TestTailscaleRoutesCompatFilterPlacement(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F1: Filter goes to destination node (route owner), not source
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Filter placed on subnet-router (owns 10.33.0.0/16) and big-router (owns 10.0.0.0/8)
|
|
|
|
|
// - Source nodes (clients, user1) get null filters
|
|
|
|
|
// - Exit nodes do NOT get filters (exit routes don't count for coverage)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Correct for subnet-router and big-router
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" destination
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "F1_filter_on_destination_not_source",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes incorrectly get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix DstPorts expansion for autogroup:member to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F2: Subnet as ACL source, autogroup:member as destination
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Each member receives a filter with DstPorts containing ALL member IPs
|
|
|
|
|
// - client1's filter has DstPorts with client1, client2, user1 IPs
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Each member receives filter with DstPorts containing ONLY its own IP
|
|
|
|
|
// - client1's filter has DstPorts with only client1's IP
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// DstPorts is not expanded to include all autogroup:member IPs
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Expand autogroup:member in DstPorts to include all member IPs, not just self
|
|
|
|
|
{
|
|
|
|
|
name: "F2_subnet_as_acl_source",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["10.33.0.0/16"], "dst": ["autogroup:member:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): DstPorts only contains self IP, not all member IPs
|
|
|
|
|
// Additionally, tagged nodes also incorrectly receive filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// Members receive filters with ONLY self IP in DstPorts
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// INCORRECT: Only client1's IPs, should include all members
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// INCORRECT: Only client2's IPs
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// INCORRECT: Only user1's IPs
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Tagged nodes should not get filters but do in Headscale
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// INCORRECT: Exit nodes get filters with all member IPs in DstPorts
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F3: Wildcard source, specific subnet destination
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Filter on subnet-router (owns 10.33.0.0/16) and big-router (owns 10.0.0.0/8)
|
|
|
|
|
// - Exit nodes do NOT get filters (exit routes don't count for coverage)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Correct for subnet-router and big-router
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" destination
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "F3_wildcard_src_specific_dst",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes incorrectly get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F7: Filter DstPorts shows ACL CIDR, not route CIDR
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts.IP = ACL CIDR (10.33.1.0/24), not route CIDR
|
|
|
|
|
// - Only subnet-router and big-router get filters
|
|
|
|
|
// - Exit nodes do NOT get filters
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts.IP correctly uses ACL CIDR (this part works)
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" destination
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "F7_filter_dstports_shows_acl_cidr",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes incorrectly get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix wildcard destination filter distribution to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F4: Specific source (tag:router), wildcard destination
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Filter sent to all non-source nodes (all nodes except tag:router nodes)
|
|
|
|
|
// - Non-router nodes get filter, router nodes don't receive filter for their own traffic
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - All nodes get the filter, including the source nodes themselves
|
|
|
|
|
// - DstPorts uses expanded CGNAT ranges instead of "*"
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Wildcard destination distribution differs - Headscale sends to all nodes
|
|
|
|
|
// DstPorts format differs - Headscale expands "*" to CGNAT ranges
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Review wildcard destination distribution logic
|
|
|
|
|
{
|
|
|
|
|
name: "F4_specific_src_wildcard_dst",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:router"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32", // big-router
|
|
|
|
|
"100.119.139.79/32", // subnet-router
|
|
|
|
|
"100.74.117.7/32", // multi-router
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): All nodes get filters with expanded CGNAT ranges
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32", // multi-router
|
|
|
|
|
"100.100.100.1/32", // big-router
|
|
|
|
|
"100.119.139.79/32", // subnet-router
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix bidirectional subnet access and DstPorts expansion to match Tailscale
|
|
|
|
|
//
|
|
|
|
|
// F5: Bidirectional subnet access
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Rule 1 (member -> subnet): Filters on subnet-router and big-router only
|
|
|
|
|
// - Rule 2 (subnet -> member): All members get filter with all member IPs in DstPorts
|
|
|
|
|
// - Exit nodes do NOT get filters
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - All members get filters (rule 2 distribution to all)
|
|
|
|
|
// - DstPorts only contains self IP, not all member IPs
|
|
|
|
|
// - Exit nodes also get filters (exit route coverage issue)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. autogroup:member DstPorts expansion only includes self
|
|
|
|
|
// 2. Exit routes treated as covering subnet destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Expand autogroup:member in DstPorts to all member IPs
|
|
|
|
|
// 2. Exclude exit routes from filter distribution coverage
|
|
|
|
|
{
|
|
|
|
|
name: "F5_bidirectional_subnet_access",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:*"]},
|
|
|
|
|
{"action": "accept", "src": ["10.33.0.0/16"], "dst": ["autogroup:member:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Multiple issues
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// All members get filters with self-only DstPorts
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// INCORRECT: Only client1's IPs
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters from BOTH rules
|
|
|
|
|
"exit-node": {
|
|
|
|
|
// First filter: from rule 1 (member -> subnet)
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
// Second filter: from rule 2 (subnet -> member)
|
|
|
|
|
// Exit node gets this because 0.0.0.0/0 "covers" member IPs
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
// First filter: from rule 1 (member -> subnet)
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
// Second filter: from rule 2 (subnet -> member)
|
|
|
|
|
// Multi-router gets this because 0.0.0.0/0 "covers" member IPs
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{"10.33.0.0/16"},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.89.42.23/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.90.199.68/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.116.73.38/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::d01:2a2e/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::2d01:c747/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::a801:4949/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F6: Filter SrcIPs expansion with autogroup:member
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only subnet-router and big-router get filters
|
|
|
|
|
// - Exit nodes do NOT get filters
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Correct for subnet-router and big-router
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" destination
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "F6_filter_srcips_expansion",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes incorrectly get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix policy validation to allow undefined tags (matching Tailscale behavior)
|
|
|
|
|
//
|
|
|
|
|
// F8: Route enabled but ACL source doesn't match any nodes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Policy is accepted even if tag doesn't exist (no nodes have that tag)
|
|
|
|
|
// - All nodes get null filters
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Policy parsing fails with "Tag is not defined in the Policy"
|
|
|
|
|
// - Headscale requires all tags to be defined in tagOwners
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale validates that all tags in ACLs are defined in tagOwners
|
|
|
|
|
// Tailscale allows undefined tags (they just match nothing)
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Either relax tag validation or accept that this is a stricter policy mode
|
|
|
|
|
// Using group:empty instead (defined but has no members)
|
|
|
|
|
{
|
|
|
|
|
name: "F8_route_enabled_acl_denies",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["group:empty"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// group:empty has no members, so no source IPs match
|
|
|
|
|
// All nodes should get null filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// F9: ACL allows traffic to subnet but no node has that route
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - No node has 10.99.0.0/16 route
|
|
|
|
|
// - No filters should be generated for any node
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes get filters because 0.0.0.0/0 "covers" 10.99.0.0/16
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "F9_route_disabled_acl_allows",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.99.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Routers with covering routes get filters
|
|
|
|
|
// NOTE: big-router (10.0.0.0/8) covers 10.99.0.0/16, so it correctly gets filter
|
|
|
|
|
// Exit nodes also incorrectly get filters due to 0.0.0.0/0 coverage
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
// big-router (10.0.0.0/8) correctly covers 10.99.0.0/16
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.99.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.99.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.99.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runRoutesCompatTests is a helper to run route compatibility tests.
|
|
|
|
|
func runRoutesCompatTests(t *testing.T, users types.Users, nodes types.Nodes, tests []routesCompatTest) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
pol, err := unmarshalPolicy([]byte(tt.policy))
|
|
|
|
|
require.NoError(t, err, "failed to parse policy")
|
|
|
|
|
|
|
|
|
|
err = pol.validate()
|
|
|
|
|
require.NoError(t, err, "policy validation failed")
|
|
|
|
|
|
|
|
|
|
for nodeName, wantFilters := range tt.wantFilters {
|
|
|
|
|
node := findNodeByGivenName(nodes, nodeName)
|
|
|
|
|
require.NotNil(t, node, "node %s not found", nodeName)
|
|
|
|
|
|
|
|
|
|
compiledFilters, err := pol.compileFilterRulesForNode(users, node.View(), nodes.ViewSlice())
|
|
|
|
|
require.NoError(t, err, "failed to compile filters for node %s", nodeName)
|
|
|
|
|
|
|
|
|
|
gotFilters := policyutil.ReduceFilterRules(node.View(), compiledFilters)
|
|
|
|
|
|
|
|
|
|
if len(wantFilters) == 0 && len(gotFilters) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if diff := cmp.Diff(wantFilters, gotFilters, cmpOptions()...); diff != "" {
|
|
|
|
|
t.Errorf("node %s filters mismatch (-want +got):\n%s", nodeName, diff)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatRouteCoverage tests route coverage rules (Category R).
|
|
|
|
|
// These tests verify that:
|
|
|
|
|
// - Route coverage: R.Bits() <= D.Bits() && R.Contains(D.Addr())
|
|
|
|
|
// - Exit nodes (0.0.0.0/0) receive filters for ANY destination
|
|
|
|
|
// - Parent routes cover child destinations.
|
|
|
|
|
func TestTailscaleRoutesCompatRouteCoverage(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// R1: Exit route covers external destination
|
|
|
|
|
{
|
|
|
|
|
name: "R1_exit_covers_external_dest",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["8.8.8.0/24:53"]}
|
|
|
|
|
`),
|
|
|
|
|
// 8.8.8.0/24 is external (Google DNS range)
|
|
|
|
|
// Exit nodes (0.0.0.0/0) should receive the filter because they cover it
|
|
|
|
|
// TODO: Verify this is Tailscale behavior
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil, // 10.33.0.0/16 doesn't cover 8.8.8.0/24
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil, // 10.0.0.0/8 doesn't cover 8.8.8.0/24
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// Exit nodes cover 8.8.8.0/24
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "8.8.8.0/24", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "8.8.8.0/24", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// R2: Parent route covers child destination
|
|
|
|
|
// TODO: Exit route coverage issue - exit nodes get filters when they shouldn't.
|
|
|
|
|
// TAILSCALE BEHAVIOR: Exit nodes (0.0.0.0/0) do NOT receive filters for internal
|
|
|
|
|
// subnet destinations like 10.33.1.0/24. Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters because Headscale treats exit routes
|
|
|
|
|
// (0.0.0.0/0) as covering all IPv4 destinations, including internal ranges.
|
|
|
|
|
// ROOT CAUSE: routeCoversDestination() returns true for exit routes covering internal IPs.
|
|
|
|
|
{
|
|
|
|
|
name: "R2_parent_route_covers_child_dest",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
// big-router has 10.0.0.0/8 - covers 10.33.1.0/24
|
|
|
|
|
// subnet-router has 10.33.0.0/16 - also covers 10.33.1.0/24
|
|
|
|
|
// Both should receive the filter
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": { ... },
|
|
|
|
|
"big-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters due to exit route coverage
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route 0.0.0.0/0 covers 10.33.1.0/24)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// R3: Sibling routes don't cover each other
|
|
|
|
|
// TODO: Exit route coverage issue - exit nodes get filters when they shouldn't.
|
|
|
|
|
// TAILSCALE BEHAVIOR: Exit nodes do NOT receive filters for internal subnet destinations.
|
|
|
|
|
// subnet-router (10.33.0.0/16) correctly does NOT get filter (sibling doesn't cover sibling).
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters because 0.0.0.0/0 covers 10.34.0.0/16.
|
|
|
|
|
// ROOT CAUSE: routeCoversDestination() returns true for exit routes covering internal IPs.
|
|
|
|
|
{
|
|
|
|
|
name: "R3_sibling_routes_no_coverage",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.34.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
// 10.34.0.0/16 is a sibling to 10.33.0.0/16 (different /16 in 10.0.0.0/8)
|
|
|
|
|
// subnet-router (10.33.0.0/16) should NOT get filter
|
|
|
|
|
// big-router (10.0.0.0/8) SHOULD get filter (parent covers both)
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"big-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters due to exit route coverage
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil, // 10.33.0.0/16 doesn't cover 10.34.0.0/16 (correct)
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// Only big-router covers 10.34.0.0/16
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.34.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route 0.0.0.0/0 covers 10.34.0.0/16)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.34.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.34.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// R4: Exact match route
|
|
|
|
|
// TODO: Exit route coverage issue - exit nodes get filters when they shouldn't.
|
|
|
|
|
// TAILSCALE BEHAVIOR: Exit nodes do NOT receive filters for internal subnet destinations.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters because 0.0.0.0/0 covers 10.33.0.0/16.
|
|
|
|
|
// ROOT CAUSE: routeCoversDestination() returns true for exit routes covering internal IPs.
|
|
|
|
|
{
|
|
|
|
|
name: "R4_exact_match_route",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
// Exact match: subnet-router has exactly 10.33.0.0/16
|
|
|
|
|
// big-router (10.0.0.0/8) also covers it
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": { ... },
|
|
|
|
|
"big-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters due to exit route coverage
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route 0.0.0.0/0 covers 10.33.0.0/16)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatOverlapping tests overlapping route behavior (Category O).
|
|
|
|
|
// These tests verify that multiple routers with overlapping routes all receive filters.
|
|
|
|
|
func TestTailscaleRoutesCompatOverlapping(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// O2: HA routers both get filter
|
|
|
|
|
// TODO: Fix exit route coverage for HA route destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only ha-router1 and ha-router2 get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 192.168.1.0/24).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O2_ha_routers_both_get_filter",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": { filter with 192.168.1.0/24:* },
|
|
|
|
|
"ha-router2": { filter with 192.168.1.0/24:* },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O3: Parent-child routes on different nodes
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.1.0/24).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O3_parent_child_different_nodes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": { filter with 10.33.1.0/24:22 },
|
|
|
|
|
"big-router": { filter with 10.33.1.0/24:22 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O6: Exit route expands filter distribution
|
|
|
|
|
{
|
|
|
|
|
name: "O6_exit_route_expands_filter_dist",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["8.8.8.0/24:53"]}
|
|
|
|
|
`),
|
|
|
|
|
// Only exit nodes cover 8.8.8.0/24
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "8.8.8.0/24", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "8.8.8.0/24", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O12: Filter dest is ACL CIDR, not route CIDR
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.1.0/24).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O12_filter_dest_is_acl_cidr",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": { filter with 10.33.1.0/24:22 },
|
|
|
|
|
"big-router": { filter with 10.33.1.0/24:22 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Must be ACL CIDR "10.33.1.0/24", NOT route "10.33.0.0/16"
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Must be ACL CIDR "10.33.1.0/24", NOT route "10.0.0.0/8"
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatTagResolution tests tag resolution behavior (Category T).
|
|
|
|
|
// These tests verify that tags resolve to node IPs only, NOT to routes.
|
|
|
|
|
func TestTailscaleRoutesCompatTagResolution(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix per-node DstPorts visibility and exit route coverage
|
|
|
|
|
//
|
|
|
|
|
// T1: Tags resolve to IPs, not routes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only tag:router nodes (subnet-router, multi-router, big-router) get filters
|
|
|
|
|
// - DstPorts shows ALL tag:router node IPs to each node
|
|
|
|
|
// - exit-node does NOT get filter (not in tag:router)
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit node also gets filter (0.0.0.0/0 route "covers" tag:router IPs)
|
|
|
|
|
// - Per-node DstPorts visibility: each node only sees its OWN IP in DstPorts
|
|
|
|
|
// (subnet-router sees only subnet-router IPs, big-router sees only big-router IPs)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
// 2. Filter reduction logic scopes DstPorts to per-node visibility
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Exclude exit routes from tag-based filter distribution
|
|
|
|
|
// 2. Show full destination set to all destination nodes (not per-node scoped)
|
|
|
|
|
{
|
|
|
|
|
name: "T1_tags_resolve_to_ips_not_routes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:router"], "dst": ["tag:router:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": { SrcIPs: all tag:router, DstPorts: ALL tag:router IPs },
|
|
|
|
|
"multi-router": { SrcIPs: all tag:router, DstPorts: ALL tag:router IPs },
|
|
|
|
|
"big-router": { SrcIPs: all tag:router, DstPorts: ALL tag:router IPs },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit gets filter, per-node DstPorts scoped to own IPs
|
|
|
|
|
// tag:router = subnet-router (100.119.139.79), multi-router (100.74.117.7), big-router (100.100.100.1)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// INCORRECT: exit-node gets filter due to 0.0.0.0/0 coverage
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32", // big-router
|
|
|
|
|
"100.119.139.79/32", // subnet-router
|
|
|
|
|
"100.74.117.7/32", // multi-router
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// exit-node sees all tag:router IPs (via 0.0.0.0/0 coverage)
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: subnet-router only sees its own IPs in DstPorts
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// Per-node scoped: only subnet-router's own IPs
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: multi-router sees ALL IPs (it has tag:router AND tag:exit with 0.0.0.0/0)
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// multi-router sees ALL tag:router IPs (it has 0.0.0.0/0 exit route coverage)
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: big-router only sees its own IPs in DstPorts
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// Per-node scoped: only big-router's own IPs
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// T2: tag:exit to tag:exit
|
|
|
|
|
{
|
|
|
|
|
name: "T2_tag_to_tag_with_exit",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:exit"], "dst": ["tag:exit:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// tag:exit = exit-node, multi-router
|
|
|
|
|
// DstPorts = node IPs only, NOT exit routes
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// Node IPs only - no exit routes 0.0.0.0/0, ::/0
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.121.32.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::7f01:2004/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.121.32.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::7f01:2004/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// T5: Multi-tag node appears in both src and dst
|
|
|
|
|
{
|
|
|
|
|
name: "T5_multi_tag_node_in_both",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:router"], "dst": ["tag:exit:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// multi-router has BOTH tag:router and tag:exit
|
|
|
|
|
// It should appear in BOTH SrcIPs (as router) and DstPorts (as exit)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
// Source: tag:router nodes
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32", // big-router
|
|
|
|
|
"100.119.139.79/32", // subnet-router
|
|
|
|
|
"100.74.117.7/32", // multi-router (has both tags)
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// Dest: tag:exit nodes (exit-node + multi-router)
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.121.32.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::7f01:2004/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.121.32.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::7f01:2004/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatProtocolPort tests protocol and port restrictions on subnet routes.
|
|
|
|
|
// Category G: Tests from 13-route-acl-interactions.md focusing on protocol/port handling.
|
|
|
|
|
func TestTailscaleRoutesCompatProtocolPort(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// G1: Port restriction on subnet (22 only)
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only subnet-router and big-router get filters
|
|
|
|
|
// - Exit nodes do NOT get filters for subnet destinations
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" everything
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "G1_port_restriction_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { filter with port 22 },
|
|
|
|
|
"big-router": { filter with port 22 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters due to 0.0.0.0/0 coverage
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// G2: Port range on subnet (80-443)
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only subnet-router and big-router get filters
|
|
|
|
|
// - Exit nodes do NOT get filters for subnet destinations
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" everything
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "G2_port_range_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:80-443"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { filter with port 80-443 },
|
|
|
|
|
"big-router": { filter with port 80-443 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters due to 0.0.0.0/0 coverage
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit node route coverage to match Tailscale behavior
|
|
|
|
|
//
|
|
|
|
|
// G7: All ports wildcard
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only subnet-router and big-router get filters
|
|
|
|
|
// - Exit nodes do NOT get filters for subnet destinations
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes also get filters because 0.0.0.0/0 "covers" everything
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit routes from filter distribution coverage checks
|
|
|
|
|
{
|
|
|
|
|
name: "G7_all_ports_wildcard",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { filter with all ports },
|
|
|
|
|
"big-router": { filter with all ports },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit nodes get filters due to 0.0.0.0/0 coverage
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatIPv6 tests IPv6-specific route behavior.
|
|
|
|
|
// Category I: Tests from 15-overlapping-subnets.md focusing on IPv6 handling.
|
|
|
|
|
func TestTailscaleRoutesCompatIPv6(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
|
|
|
|
|
// Create nodes with IPv6 subnet routes
|
|
|
|
|
nodeClient1 := &types.Node{
|
|
|
|
|
ID: 1,
|
|
|
|
|
GivenName: "client1",
|
|
|
|
|
User: &users[0],
|
|
|
|
|
UserID: &users[0].ID,
|
|
|
|
|
IPv4: ptrAddr("100.116.73.38"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::a801:4949"),
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IPv6 subnet router
|
|
|
|
|
nodeIPv6Router := &types.Node{
|
|
|
|
|
ID: 2,
|
|
|
|
|
GivenName: "ipv6-router",
|
|
|
|
|
IPv4: ptrAddr("100.119.139.80"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::4001:8ba1"),
|
|
|
|
|
Tags: []string{"tag:router"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("fd00::/48"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("fd00::/48"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IPv6 child route (more specific)
|
|
|
|
|
nodeIPv6ChildRouter := &types.Node{
|
|
|
|
|
ID: 3,
|
|
|
|
|
GivenName: "ipv6-child-router",
|
|
|
|
|
IPv4: ptrAddr("100.119.139.81"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::4001:8ba2"),
|
|
|
|
|
Tags: []string{"tag:router"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("fd00:1::/64"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("fd00:1::/64"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IPv6 exit node (with ::/0)
|
|
|
|
|
nodeIPv6Exit := &types.Node{
|
|
|
|
|
ID: 4,
|
|
|
|
|
GivenName: "ipv6-exit",
|
|
|
|
|
IPv4: ptrAddr("100.121.32.2"),
|
|
|
|
|
IPv6: ptrAddr("fd7a:115c:a1e0::7f01:2005"),
|
|
|
|
|
Tags: []string{"tag:exit"},
|
|
|
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
|
|
|
RoutableIPs: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("::/0"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ApprovedRoutes: []netip.Prefix{
|
|
|
|
|
netip.MustParsePrefix("::/0"),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nodes := types.Nodes{nodeClient1, nodeIPv6Router, nodeIPv6ChildRouter, nodeIPv6Exit}
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix wildcard DstPorts format, SrcIPs to include subnet routes, and filter distribution
|
|
|
|
|
//
|
|
|
|
|
// I1: IPv6 subnet route with wildcard ACL
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs includes IPv6 subnet route (fd00::/48) in wildcard expansion
|
|
|
|
|
// - DstPorts uses {IP: "*"} for wildcard destinations
|
|
|
|
|
// - Only client1 receives a filter (filter placed on destination node)
|
|
|
|
|
// - Other nodes (routers) do NOT receive filters for wildcard dst
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs doesn't include subnet routes, only CGNAT ranges
|
|
|
|
|
// - DstPorts expands to CGNAT ranges instead of "*"
|
|
|
|
|
// - ALL nodes receive filters (incorrect filter distribution)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Headscale doesn't include subnet routes in wildcard SrcIPs
|
|
|
|
|
// 2. Headscale expands "*" to CGNAT ranges instead of using "*"
|
|
|
|
|
// 3. Headscale distributes filters to all nodes instead of only the destination
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Include advertised subnet routes in wildcard SrcIPs
|
|
|
|
|
// 2. Use {IP: "*"} for wildcard destinations
|
|
|
|
|
// 3. Fix filter distribution to only send to destination nodes
|
|
|
|
|
{
|
|
|
|
|
name: "I1_ipv6_subnet_route",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
// Wildcard ACL - SrcIPs should include IPv6 route (fd00::/48)
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd00::/48", // IPv6 subnet route
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ipv6-router": nil,
|
|
|
|
|
"ipv6-child-router": nil,
|
|
|
|
|
"ipv6-exit": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): All nodes get filters with CGNAT DstPorts
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: All routers get filters (should be nil)
|
|
|
|
|
"ipv6-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ipv6-child-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ipv6-exit": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix IPv6 parent route coverage
|
|
|
|
|
//
|
|
|
|
|
// I4: IPv6 specific ACL targeting fd00:1::/64
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - ipv6-router (fd00::/48) covers fd00:1::/64 - should get filter
|
|
|
|
|
// - ipv6-child-router (fd00:1::/64) exact match - should get filter
|
|
|
|
|
// - ipv6-exit (::/0) covers everything - should get filter
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - ipv6-router (fd00::/48) does NOT get filter - Headscale doesn't recognize
|
|
|
|
|
// that fd00::/48 covers fd00:1::/64 (parent route coverage not working)
|
|
|
|
|
// - ipv6-child-router (fd00:1::/64) gets filter (exact match works)
|
|
|
|
|
// - ipv6-exit (::/0) gets filter (IPv6 exit route coverage works)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale's route coverage logic doesn't properly handle IPv6 parent routes.
|
|
|
|
|
// fd00::/48 should cover fd00:1::/64 but Headscale doesn't recognize this.
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Fix IPv6 parent route coverage in filter distribution logic.
|
|
|
|
|
{
|
|
|
|
|
name: "I4_ipv6_specific_acl",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["fd00:1::/64:443"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
// ipv6-router should get filter (fd00::/48 covers fd00:1::/64)
|
|
|
|
|
"ipv6-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ipv6-child-router should also get filter (exact match)
|
|
|
|
|
"ipv6-child-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ipv6-exit should get filter (::/0 covers everything)
|
|
|
|
|
"ipv6-exit": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): ipv6-router doesn't get filter (parent route coverage broken)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
// INCORRECT: ipv6-router doesn't get filter (should based on parent coverage)
|
|
|
|
|
"ipv6-router": nil,
|
|
|
|
|
// ipv6-child-router gets filter (exact match works)
|
|
|
|
|
"ipv6-child-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ipv6-exit gets filter (::/0 covers everything)
|
|
|
|
|
"ipv6-exit": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix IPv6 parent route coverage
|
|
|
|
|
//
|
|
|
|
|
// I5: IPv6 parent/child route coverage
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - ipv6-router (fd00::/48) covers fd00:1:2::/80 - should get filter
|
|
|
|
|
// - ipv6-child-router (fd00:1::/64) does NOT cover fd00:1:2::/80
|
|
|
|
|
// (fd00:1::/64 = fd00:0001:0000::/64, fd00:1:2::/80 = fd00:0001:0002::/80 - different)
|
|
|
|
|
// - ipv6-exit (::/0) covers everything - should get filter
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - ipv6-router (fd00::/48) does NOT get filter - Headscale doesn't recognize
|
|
|
|
|
// that fd00::/48 covers fd00:1:2::/80 (parent route coverage not working)
|
|
|
|
|
// - ipv6-child-router correctly gets nil (fd00:1::/64 doesn't cover fd00:1:2::/80)
|
|
|
|
|
// - ipv6-exit gets filter (::/0 covers everything)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale's route coverage logic doesn't properly handle IPv6 parent routes.
|
|
|
|
|
// fd00::/48 should cover fd00:1:2::/80 but Headscale doesn't recognize this.
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Fix IPv6 parent route coverage in filter distribution logic.
|
|
|
|
|
{
|
|
|
|
|
name: "I5_ipv6_parent_child_routes",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["fd00:1:2::/80:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
// ipv6-router (fd00::/48) covers fd00:1:2::/80 - should get filter
|
|
|
|
|
"ipv6-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1:2::/80", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ipv6-child-router (fd00:1::/64) does NOT cover fd00:1:2::/80
|
|
|
|
|
"ipv6-child-router": nil,
|
|
|
|
|
// ipv6-exit (::/0) covers everything
|
|
|
|
|
"ipv6-exit": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1:2::/80", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): ipv6-router doesn't get filter (parent route coverage broken)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
// INCORRECT: ipv6-router doesn't get filter (should based on parent coverage)
|
|
|
|
|
"ipv6-router": nil,
|
|
|
|
|
// ipv6-child-router correctly doesn't get filter
|
|
|
|
|
// (fd00:1::/64 doesn't cover fd00:1:2::/80)
|
|
|
|
|
"ipv6-child-router": nil,
|
|
|
|
|
// ipv6-exit gets filter (::/0 covers everything)
|
|
|
|
|
"ipv6-exit": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00:1:2::/80", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// I7: IPv6 exit route coverage (external IPv6 destination)
|
|
|
|
|
{
|
|
|
|
|
name: "I7_ipv6_exit_coverage",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["2001:db8::/32:443"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"ipv6-router": nil, // fd00::/48 doesn't cover 2001:db8::/32
|
|
|
|
|
"ipv6-child-router": nil, // fd00:1::/64 doesn't cover 2001:db8::/32
|
|
|
|
|
// Only ipv6-exit (::/0) should get filter
|
|
|
|
|
"ipv6-exit": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "2001:db8::/32", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatEdgeCases tests edge cases and unusual configurations.
|
|
|
|
|
// Category H: Edge cases from various findings documents.
|
|
|
|
|
func TestTailscaleRoutesCompatEdgeCases(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix wildcard SrcIPs to include subnet routes like Tailscale
|
|
|
|
|
//
|
|
|
|
|
// H1: Verify wildcard SrcIPs format
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs includes CGNAT range + all advertised subnet routes
|
|
|
|
|
// - Exit nodes do NOT get filters for tag:router destination
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs only includes CGNAT range (no subnet routes)
|
|
|
|
|
// - Exit nodes also get filters due to exit route coverage
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Headscale doesn't include subnet routes in wildcard SrcIPs
|
|
|
|
|
// 2. Exit routes (0.0.0.0/0) treated as covering all destinations
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Include advertised subnet routes in wildcard SrcIPs
|
|
|
|
|
// 2. Exclude exit routes from filter distribution
|
|
|
|
|
{
|
|
|
|
|
name: "H1_wildcard_srcips_format",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["tag:router:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10", "fd7a:115c:a1e0::/48",
|
|
|
|
|
"10.0.0.0/8", "10.33.0.0/16", "172.16.0.0/24", "192.168.1.0/24", // routes!
|
|
|
|
|
},
|
|
|
|
|
DstPorts: ... tag:router IPs,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ... multi-router and big-router with same SrcIPs pattern
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale):
|
|
|
|
|
// - SrcIPs missing routes
|
|
|
|
|
// - DstPorts only contains node's own IPs (not all tag:router IPs)
|
|
|
|
|
// - exit-node gets filter
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
// INCORRECT: DstPorts only contains self IPs, not all tag:router IPs
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Only subnet-router's own IPs
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// multi-router has tag:router AND tag:exit, gets all tag:router IPs
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// All tag:router IPs
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Only big-router's own IPs
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit node gets filter (should be nil)
|
|
|
|
|
// Exit-node has tag:exit, NOT tag:router, so shouldn't get filter
|
|
|
|
|
// But due to exit route coverage, it gets ALL tag:router IPs
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.64.0.0/10",
|
|
|
|
|
"fd7a:115c:a1e0::/48",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// All tag:router IPs (exit-node sees all because of route coverage)
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H9: Large prefix (/8) subnet route
|
|
|
|
|
// TODO: Fix exit route coverage and child route coverage
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only big-router (has 10.0.0.0/8) gets the filter.
|
|
|
|
|
// subnet-router (10.33.0.0/16) is a CHILD of 10.0.0.0/8 - doesn't cover parent.
|
|
|
|
|
// Exit nodes do NOT get filters for specific subnet destinations.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters (0.0.0.0/0 covers). subnet-router also
|
|
|
|
|
// gets filter (Headscale incorrectly treats child routes as covering).
|
|
|
|
|
// ROOT CAUSE: Two issues: (1) Exit route coverage, (2) Child route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes and fix route coverage to only include parents.
|
|
|
|
|
{
|
|
|
|
|
name: "H9_large_prefix_works",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.0.0.0/8:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes and child routes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// subnet-router incorrectly gets filter (child route coverage)
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H2: Wildcard DstPorts format
|
|
|
|
|
// TODO: Fix wildcard DstPorts format and filter distribution
|
|
|
|
|
// TAILSCALE BEHAVIOR: DstPorts uses {IP: "*"} for wildcard destinations.
|
|
|
|
|
// Only client1 receives a filter (filter placed on destination node).
|
|
|
|
|
// HEADSCALE BEHAVIOR: DstPorts expands to CGNAT ranges (100.64.0.0/10, fd7a:115c:a1e0::/48).
|
|
|
|
|
// ALL nodes receive filters.
|
|
|
|
|
// ROOT CAUSE: Two issues: (1) Headscale expands "*" to CGNAT ranges instead of using "*",
|
|
|
|
|
// (2) Headscale distributes filters to all nodes instead of only the destination.
|
|
|
|
|
// FIX REQUIRED: Use {IP: "*"} for wildcard destinations and fix filter distribution.
|
|
|
|
|
{
|
|
|
|
|
name: "H2_wildcard_dstports_format",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): DstPorts expanded to CGNAT, all nodes get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: memberSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H3: CGNAT range expansion in wildcard
|
|
|
|
|
// TODO: Fix filter distribution and exit route coverage for tag destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only tag:router nodes (subnet-router, multi-router, big-router)
|
|
|
|
|
// receive filters. Each receives DstPorts containing ALL tag:router node IPs.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (exit route covers tag:router IPs).
|
|
|
|
|
// Each node only sees its OWN IPs in DstPorts, not all tag:router IPs.
|
|
|
|
|
// ROOT CAUSE: Two issues: (1) Exit route coverage gives filters to exit-node,
|
|
|
|
|
// (2) Per-node DstPorts filtering shows only self IPs instead of all tag:router IPs.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from tag-based destinations, fix DstPorts to include all.
|
|
|
|
|
{
|
|
|
|
|
name: "H3_cgnat_range_expansion",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["tag:router:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// All tag:router node IPs
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny}, // big-router
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny}, // subnet-router
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny}, // multi-router
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": nil, // Also tag:router but expected to get filter
|
|
|
|
|
"big-router": nil, // Also tag:router but expected to get filter
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Each node only sees its own IPs in DstPorts,
|
|
|
|
|
// multi-router and big-router also get filters, exit-node incorrectly gets filter
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Only self IPs
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// All tag:router IPs (multi-router sees all)
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Only self IPs
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// exit-node incorrectly gets filter (exit route covers tag:router IPs)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H4: IPv6 range in SrcIPs
|
|
|
|
|
// TODO: Fix wildcard DstPorts format and filter distribution
|
|
|
|
|
// TAILSCALE BEHAVIOR: DstPorts uses {IP: "*"} for wildcard destinations.
|
|
|
|
|
// SrcIPs includes fd7a:115c:a1e0::/48 (IPv6 Tailscale range). Only client1 receives filter.
|
|
|
|
|
// HEADSCALE BEHAVIOR: DstPorts expands to CGNAT ranges. ALL nodes receive filters.
|
|
|
|
|
// ROOT CAUSE: Same as H2 - Headscale expands "*" to CGNAT and distributes to all nodes.
|
|
|
|
|
// FIX REQUIRED: Use {IP: "*"} for wildcard destinations and fix filter distribution.
|
|
|
|
|
{
|
|
|
|
|
name: "H4_ipv6_range_in_srcips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): All nodes get filters with CGNAT DstPorts
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H7: Two nodes claiming same subnet - first is primary
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only ha-router1 and ha-router2 (which have 192.168.1.0/24) get filters.
|
|
|
|
|
// Exit nodes do NOT get filters for specific subnet destinations.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 192.168.1.0/24).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage gives filters to exit-node and multi-router.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "H7_two_nodes_same_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers 192.168.1.0/24)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H10: Very small prefix (/32)
|
|
|
|
|
// TODO: Fix exit route coverage for /32 destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router (10.33.0.0/16) and big-router (10.0.0.0/8) get filters.
|
|
|
|
|
// These routes cover 10.33.0.100/32. Exit nodes do NOT get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.0.100/32).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage gives filters to exit-node and multi-router.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from specific IP destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "H10_very_small_prefix",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.100/32:80"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalR tests additional route coverage scenarios (Category R).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalR(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// R5: Route coverage check logic verification
|
|
|
|
|
// TODO: Exit route coverage issue - exit nodes get filters when they shouldn't.
|
|
|
|
|
// TAILSCALE BEHAVIOR: Exit nodes do NOT receive filters for internal subnet destinations.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters because 0.0.0.0/0 covers 10.33.1.0/24.
|
|
|
|
|
// ROOT CAUSE: routeCoversDestination() returns true for exit routes covering internal IPs.
|
|
|
|
|
{
|
|
|
|
|
name: "R5_route_coverage_check_logic",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
// Route coverage: R.Bits() <= D.Bits() && R.Contains(D.Addr())
|
|
|
|
|
// 10.0.0.0/8 (bits=8) <= 24 && contains 10.33.1.0 -> YES
|
|
|
|
|
// 10.33.0.0/16 (bits=16) <= 24 && contains 10.33.1.0 -> YES
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": { ... },
|
|
|
|
|
"big-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters due to exit route coverage
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route 0.0.0.0/0 covers 10.33.1.0/24)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// R6: IPv6 route coverage
|
|
|
|
|
// TODO: Exit route coverage issue - exit nodes get filters for IPv6 Tailscale range.
|
|
|
|
|
// TAILSCALE BEHAVIOR: No nodes get filters for IPv6 addresses in the Tailscale range
|
|
|
|
|
// (fd7a:115c:a1e0::/48) as these are node IPs, not routed destinations.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters because ::/0 covers all IPv6 addresses.
|
|
|
|
|
// ROOT CAUSE: routeCoversDestination() returns true for exit routes covering all IPs.
|
|
|
|
|
{
|
|
|
|
|
name: "R6_ipv6_route_coverage",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["fd7a:115c:a1e0::1/128:443"]}
|
|
|
|
|
`),
|
|
|
|
|
// Targeting a specific IPv6 in the Tailscale range
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes get filters (exit route ::/0 covers all IPv6)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route ::/0 covers fd7a:115c:a1e0::1)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRange{First: 443, Last: 443}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRange{First: 443, Last: 443}}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// R7: Exit node IPv6 coverage
|
|
|
|
|
{
|
|
|
|
|
name: "R7_exit_ipv6_coverage",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["2001:db8::1/128:443"]}
|
|
|
|
|
`),
|
|
|
|
|
// External IPv6 address - only exit nodes cover
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Exit nodes cover all destinations
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "2001:db8::1/128", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "2001:db8::1/128", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// R8: Mixed IPv4/IPv6 coverage
|
|
|
|
|
// TODO: Multiple coverage issues in this test:
|
|
|
|
|
// 1. Exit route coverage: exit nodes get IPv4 filters (0.0.0.0/0 covers 10.33.0.0/16)
|
|
|
|
|
// 2. Node IP coverage: all nodes get IPv6 filters because their IPv6 addresses are in
|
|
|
|
|
// fd7a:115c:a1e0::/48 which overlaps with the destination fd7a:115c:a1e0::/64
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters (IPv4 only).
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - All nodes get filters for IPv6 (node IPs are in fd7a:115c:a1e0::/48)
|
|
|
|
|
// - Exit nodes get filters for IPv4 (exit route covers 10.33.0.0/16)
|
|
|
|
|
// - subnet-router and big-router get both IPv4 and IPv6
|
|
|
|
|
// ROOT CAUSE: Node IP prefixes incorrectly treated as routes covering destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "R8_mixed_ipv4_ipv6_coverage",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*", "fd7a:115c:a1e0::/64:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// Both IPv4 and IPv6 destinations
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { IPv4 only: 10.33.0.0/16 },
|
|
|
|
|
"big-router": { IPv4 only: 10.33.0.0/16 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Multiple issues - node IPs treated as routes, exit coverage
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// All nodes get IPv6 filters because their IPs are in fd7a:115c:a1e0::/48
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny}},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// subnet-router and big-router get both IPv4 (from routes) and IPv6 (from node IPs)
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes get both IPv4 (exit route) and IPv6 (exit route + node IPs)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::/64", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalO tests additional overlapping route scenarios (Category O).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalO(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// O1: Overlapping routes not merged
|
|
|
|
|
// TODO: Fix wildcard destination handling for nodes with routes
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only client1 gets filters (dst *:* only goes to primary node).
|
|
|
|
|
// HEADSCALE BEHAVIOR: All nodes get filters (dst *:* expands to Headscale IP ranges for all nodes).
|
|
|
|
|
// ROOT CAUSE: Wildcard destination expands to Headscale IP ranges, not literal "*".
|
|
|
|
|
// FIX REQUIRED: Limit *:* distribution to match Tailscale behavior.
|
|
|
|
|
{
|
|
|
|
|
name: "O1_overlapping_routes_not_merged",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": { filter with *:* },
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): All nodes get filters with expanded IP ranges
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// All routers get filters because *:* expands to all Headscale IP ranges
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O4: Three-way hierarchy
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.1.128/25).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O4_three_way_hierarchy",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.128/25:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { filter with 10.33.1.128/25:22 },
|
|
|
|
|
"big-router": { filter with 10.33.1.128/25:22 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.128/25", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.128/25", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.128/25", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.128/25", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O5: Sibling routes with parent ACL
|
|
|
|
|
// TODO: Fix exit route coverage and subnet-router getting parent ACL filter
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only big-router gets filters (exact /8 route match).
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters (0.0.0.0/0 covers 10.0.0.0/8),
|
|
|
|
|
// and subnet-router gets filters (10.33.0.0/16 is within /8).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage + child routes get parent ACL filters.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes and child routes from parent ACL.
|
|
|
|
|
{
|
|
|
|
|
name: "O5_sibling_routes_with_parent_acl",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.0.0.0/8:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": { filter with 10.0.0.0/8:* },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes and subnet-router also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// subnet-router incorrectly gets filter (child route within parent ACL)
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O7: Specific IP targeting with multiple covering routes
|
|
|
|
|
// TODO: Fix exit route coverage for specific IP destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.0.100/32).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O7_specific_ip_targeting",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.100/32:80"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { filter with 10.33.0.100/32:80 },
|
|
|
|
|
"big-router": { filter with 10.33.0.100/32:80 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.100/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O10: ACL dest covered by multiple routes
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.1.0/24).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O10_acl_dest_covered_by_multiple",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { filter with 10.33.1.0/24:22 },
|
|
|
|
|
"big-router": { filter with 10.33.1.0/24:22 },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O11: ACL dest not covered by any route
|
|
|
|
|
// TODO: Fix exit route coverage for uncovered destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: No nodes get filters (no route covers 192.168.99.0/24).
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes get filters (0.0.0.0/0 covers 192.168.99.0/24).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from uncovered destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "O11_acl_dest_not_covered",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.99.0/24:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.99.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.99.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalG tests additional protocol and port scenarios (Category G).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalG(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// G3: Multiple ports on subnet
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.0.0/16).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "G3_multiple_ports_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:22,80,443"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"subnet-router": { ... },
|
|
|
|
|
"big-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// G8: Default IPProto (all protocols)
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
// TAILSCALE BEHAVIOR: Only subnet-router and big-router get filters.
|
|
|
|
|
// HEADSCALE BEHAVIOR: Exit nodes also get filters (0.0.0.0/0 covers 10.33.0.0/16).
|
|
|
|
|
// ROOT CAUSE: Exit route coverage.
|
|
|
|
|
// FIX REQUIRED: Exclude exit nodes from subnet-specific destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "G8_default_ipproto",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
...
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes also get filters
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
// TCP=6, UDP=17, ICMP=1, ICMPv6=58
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes incorrectly get filters (exit route covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalT tests additional tag resolution scenarios (Category T).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalT(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix wildcard destination expansion and filter distribution
|
|
|
|
|
//
|
|
|
|
|
// T3: Tag source includes all tagged nodes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only client1 gets filter (user-owned, thus a valid destination)
|
|
|
|
|
// - DstPorts uses literal "*" for wildcard destination
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - ALL nodes get filters (wildcard destination distributed to everyone)
|
|
|
|
|
// - DstPorts expands to CGNAT ranges instead of "*"
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Wildcard destination distributed to all nodes instead of only non-source nodes
|
|
|
|
|
// 2. DstPorts expands wildcards to explicit CGNAT ranges
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Limit filter distribution for wildcard destinations
|
|
|
|
|
// 2. Use literal "*" in DstPorts for wildcard destinations
|
|
|
|
|
{
|
|
|
|
|
name: "T3_tag_src_includes_all_tagged",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:router"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{ tag:router IPs },
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): ALL nodes get filters, DstPorts expanded to CGNAT ranges
|
|
|
|
|
// tag:router = subnet-router, multi-router, big-router
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
// INCORRECT: All nodes get filters, not just destination nodes
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32", // big-router
|
|
|
|
|
"100.119.139.79/32", // subnet-router
|
|
|
|
|
"100.74.117.7/32", // multi-router
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: DstPorts uses CGNAT ranges instead of "*"
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix per-node DstPorts visibility and exit route coverage
|
|
|
|
|
//
|
|
|
|
|
// T4: Tag destination includes all tagged nodes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only ha-router1 and ha-router2 get filters (tag:ha nodes)
|
|
|
|
|
// - DstPorts shows ALL tag:ha node IPs to each node
|
|
|
|
|
// - exit-node and multi-router do NOT get filters
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes also get filter (0.0.0.0/0 route "covers" tag:ha IPs)
|
|
|
|
|
// - Per-node DstPorts visibility: each node only sees its OWN IP in DstPorts
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Exit routes (0.0.0.0/0) are treated as covering all destinations
|
|
|
|
|
// 2. Filter reduction logic scopes DstPorts to per-node visibility
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Exclude exit routes from tag-based filter distribution
|
|
|
|
|
// 2. Show full destination set to all destination nodes (not per-node scoped)
|
|
|
|
|
{
|
|
|
|
|
name: "T4_tag_dst_includes_all_tagged",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["tag:ha:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"exit-node": nil,
|
|
|
|
|
"multi-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": { DstPorts: ALL tag:ha IPs },
|
|
|
|
|
"ha-router2": { DstPorts: ALL tag:ha IPs },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes get filters, per-node DstPorts scoped
|
|
|
|
|
// tag:ha = ha-router1 (100.85.37.108), ha-router2 (100.119.130.32)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
// INCORRECT: exit-node gets filter due to 0.0.0.0/0 coverage
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
// exit-node sees ALL tag:ha IPs via exit route coverage
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.119.130.32/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.85.37.108/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4501:82a9/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::f101:2597/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: multi-router gets filter due to 0.0.0.0/0 coverage
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
// multi-router sees ALL tag:ha IPs via exit route coverage
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.119.130.32/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "100.85.37.108/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4501:82a9/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::f101:2597/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: ha-router1 only sees its own IPs in DstPorts
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
// Per-node scoped: only ha-router1's own IPs
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.85.37.108/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::f101:2597/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: ha-router2 only sees its own IPs in DstPorts
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
// Per-node scoped: only ha-router2's own IPs
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.119.130.32/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4501:82a9/128", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAutoApprover tests autoApprover behavior (Category D).
|
|
|
|
|
// These tests validate automatic route approval based on tags and prefixes.
|
|
|
|
|
// NOTE: AutoApprover affects route ENABLING, not filter distribution.
|
|
|
|
|
// The filter tests here verify filters ASSUMING routes are enabled.
|
|
|
|
|
func TestTailscaleRoutesCompatAutoApprover(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// D1: Basic route auto-approval with autoApprover
|
|
|
|
|
// 10.0.0.0/8 -> tag:router means routes within 10.0.0.0/8
|
|
|
|
|
// advertised by nodes with tag:router are auto-approved
|
|
|
|
|
{
|
|
|
|
|
name: "D1_basic_route_auto_approval",
|
|
|
|
|
// This test validates that with autoApprover configured,
|
|
|
|
|
// routes matching the prefix/tag combination are enabled.
|
|
|
|
|
// Filter distribution follows standard rules.
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// Assuming route is auto-approved and enabled:
|
|
|
|
|
// Filter goes to subnet-router (route owner) + big-router (parent route)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
// subnet-router owns 10.33.0.0/16
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// big-router owns 10.0.0.0/8 (covers 10.33.0.0/16)
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// exit-node and multi-router also get filter (0.0.0.0/0 covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D2: Nested prefix approval - autoApprover for parent covers child
|
|
|
|
|
{
|
|
|
|
|
name: "D2_nested_prefix_approval",
|
|
|
|
|
// autoApprover 10.0.0.0/8 covers advertised 10.33.0.0/16
|
|
|
|
|
// This test verifies subset prefixes are approved
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D3: Exact prefix approval
|
|
|
|
|
{
|
|
|
|
|
name: "D3_exact_prefix_approval",
|
|
|
|
|
// autoApprover for exactly 10.33.0.0/16 matches advertised 10.33.0.0/16
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D4: Prefix not covered by autoApprover
|
|
|
|
|
// 192.168.0.0/16, but node advertises 10.0.0.0/8 - NOT approved
|
|
|
|
|
// Without approval, route not enabled, no filters distributed
|
|
|
|
|
{
|
|
|
|
|
name: "D4_prefix_not_covered",
|
|
|
|
|
// If autoApprover is 192.168.0.0/16 but we target 10.0.0.0/8
|
|
|
|
|
// the route would NOT be auto-approved
|
|
|
|
|
// This tests that only matching prefixes get filters
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// Only HA routers own 192.168.1.0/24
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// exit-node and multi-router get filter (0.0.0.0/0 covers)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D5: Wrong tag not approved
|
|
|
|
|
// autoApprover 10.0.0.0/8 -> tag:router, but node is tag:ha
|
|
|
|
|
{
|
|
|
|
|
name: "D5_wrong_tag_not_approved",
|
|
|
|
|
// HA routers have tag:ha, not tag:router
|
|
|
|
|
// Their 192.168.1.0/24 route would not be auto-approved
|
|
|
|
|
// by an autoApprover for tag:router
|
|
|
|
|
// But we can still target the route in ACL if manually enabled
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:router"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// tag:router sources: subnet-router, multi-router, big-router
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"subnet-router": nil, // Source, not destination
|
|
|
|
|
"big-router": nil, // Source, not destination
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32", // big-router
|
|
|
|
|
"100.119.139.79/32", // subnet-router
|
|
|
|
|
"100.74.117.7/32", // multi-router
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.100.100.1/32",
|
|
|
|
|
"100.119.139.79/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::4001:8ba0/128",
|
|
|
|
|
"fd7a:115c:a1e0::6401:6401/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix wildcard DstPorts expansion to use "*" instead of CGNAT ranges
|
|
|
|
|
//
|
|
|
|
|
// D6: Exit node auto-approval - wildcard ACL with routes
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts uses literal "*" for wildcard destination
|
|
|
|
|
// - All nodes get filter with DstPorts: [{IP: "*", Ports: 0-65535}]
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts expands to CGNAT ranges instead of using "*"
|
|
|
|
|
// - Uses {IP: "100.64.0.0/10"} and {IP: "fd7a:115c:a1e0::/48"}
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale expands wildcard destinations to explicit IP ranges
|
|
|
|
|
// instead of using the "*" shorthand that Tailscale uses
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Use literal "*" in DstPorts for wildcard destinations
|
|
|
|
|
{
|
|
|
|
|
name: "D6_exit_node_auto_approval",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ... all other nodes same pattern with DstPorts: [{IP: "*"}]
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): DstPorts expanded to CGNAT ranges
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix wildcard DstPorts expansion to use "*" instead of CGNAT ranges
|
|
|
|
|
//
|
|
|
|
|
// D7: Exit auto-approval wrong tag - tag:exit to wildcard destination
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts uses literal "*" for wildcard destination
|
|
|
|
|
// - tag:exit (exit-node, multi-router) can access anywhere
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts expands to CGNAT ranges instead of using "*"
|
|
|
|
|
// - Uses {IP: "100.64.0.0/10"} and {IP: "fd7a:115c:a1e0::/48"}
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale expands wildcard destinations to explicit IP ranges
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Use literal "*" in DstPorts for wildcard destinations
|
|
|
|
|
{
|
|
|
|
|
name: "D7_exit_auto_approval_wrong_tag",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["tag:exit"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32", "100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128", "fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ... all other nodes same pattern with DstPorts: [{IP: "*"}]
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): DstPorts expanded to CGNAT ranges
|
|
|
|
|
// tag:exit = exit-node, multi-router
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32", // exit-node
|
|
|
|
|
"100.74.117.7/32", // multi-router
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.121.32.1/32",
|
|
|
|
|
"100.74.117.7/32",
|
|
|
|
|
"fd7a:115c:a1e0::7f01:2004/128",
|
|
|
|
|
"fd7a:115c:a1e0::c401:7508/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D8: Auto-approval enables route, but ACL still enforced
|
|
|
|
|
// Route is enabled via autoApprover, but restrictive ACL limits access
|
|
|
|
|
{
|
|
|
|
|
name: "D8_auto_approval_acl_interaction",
|
|
|
|
|
// Route auto-approved, but ACL only allows specific source
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["10.33.0.0/16:22"]}
|
|
|
|
|
`),
|
|
|
|
|
// Only autogroup:member sources (user-owned nodes)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil, // Source, not destination
|
|
|
|
|
"client2": nil, // Source, not destination
|
|
|
|
|
"user1": nil, // Source, not destination
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32", // client1
|
|
|
|
|
"100.89.42.23/32", // client2
|
|
|
|
|
"100.90.199.68/32", // user1
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D9: Auto-approval triggers on advertise
|
|
|
|
|
// Policy exists first, then node advertises - triggers approval
|
|
|
|
|
// This is a state/timing test - filter distribution is the same
|
|
|
|
|
{
|
|
|
|
|
name: "D9_auto_approval_triggers_on_advertise",
|
|
|
|
|
// Same as D1 - validates consistent behavior
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D10: Auto-approval retroactive
|
|
|
|
|
// Node advertised first, policy added later - requires re-advertisement
|
|
|
|
|
// Same filter distribution as D1 when route is enabled
|
|
|
|
|
{
|
|
|
|
|
name: "D10_auto_approval_retroactive",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:443"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// D11: Overlapping auto-approvers
|
|
|
|
|
// 10.0.0.0/8 -> tag:router, 10.33.0.0/16 -> tag:special
|
|
|
|
|
// Both are valid for their respective tags
|
|
|
|
|
{
|
|
|
|
|
name: "D11_overlapping_auto_approvers",
|
|
|
|
|
// Both big-router (10.0.0.0/8) and subnet-router (10.33.0.0/16)
|
|
|
|
|
// can be approved by different autoApprover rules
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.0.0.0/8:80"]}
|
|
|
|
|
`),
|
|
|
|
|
// Targeting 10.0.0.0/8 - only big-router exact match + exit nodes
|
|
|
|
|
// subnet-router's 10.33.0.0/16 is WITHIN 10.0.0.0/8 so also gets filter
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.0.0.0/8", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalProtocol tests additional protocol restrictions (G4-G6).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalProtocol(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// G4: Protocol ICMP only
|
|
|
|
|
// proto:icmp results in IPProto=[1] (ICMP only)
|
|
|
|
|
// NOTE: Exit nodes still get filters due to exit route coverage issue (separate TODO)
|
|
|
|
|
{
|
|
|
|
|
name: "G4_protocol_icmp_subnet",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*"], "proto": "icmp"}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes also get filters (exit route coverage issue)
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// G5: Protocol TCP only
|
|
|
|
|
{
|
|
|
|
|
name: "G5_protocol_tcp_only",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:22"], "proto": "tcp"}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
// TCP only
|
|
|
|
|
IPProto: []int{ProtocolTCP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// G6: Protocol UDP only
|
|
|
|
|
{
|
|
|
|
|
name: "G6_protocol_udp_only",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:53"], "proto": "udp"}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
// UDP only
|
|
|
|
|
IPProto: []int{ProtocolUDP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolUDP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolUDP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolUDP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalEdgeCases tests additional edge cases (H5, H6, H8, H11).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalEdgeCases(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// H5: Subnet overlaps CGNAT - cannot be enabled
|
|
|
|
|
// Route 100.64.0.0/24 overlaps with Tailscale CGNAT range
|
|
|
|
|
{
|
|
|
|
|
name: "H5_subnet_overlaps_cgnat",
|
|
|
|
|
// A route overlapping CGNAT cannot be enabled
|
|
|
|
|
// This test verifies no filters are distributed for such routes
|
|
|
|
|
// Using a normal subnet route as baseline
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["100.64.0.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// TODO: Tailscale blocks routes overlapping CGNAT
|
|
|
|
|
// Headscale behavior may differ
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Exit nodes might still get filter since 0.0.0.0/0 covers everything
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.64.0.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.64.0.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H6: Loopback routes not distributed
|
|
|
|
|
// Route 127.0.0.1/32 can be advertised but NOT in peer AllowedIPs
|
|
|
|
|
{
|
|
|
|
|
name: "H6_loopback_routes_not_distributed",
|
|
|
|
|
// Loopback routes are not practical but test edge case handling
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["127.0.0.1/32:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// TODO: Tailscale allows advertising loopback but doesn't distribute
|
|
|
|
|
// Verify Headscale behavior
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Exit nodes might get filter
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "127.0.0.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "127.0.0.1/32", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H8: CGNAT overlap blocked
|
|
|
|
|
// TODO: Fix CGNAT overlap route handling
|
|
|
|
|
// TAILSCALE BEHAVIOR: Routes overlapping CGNAT (100.64.0.0/10) are blocked.
|
|
|
|
|
// Only exit nodes get filters for destinations in the blocked range.
|
|
|
|
|
// HEADSCALE BEHAVIOR: big-router gets filter because its IP (100.100.100.1)
|
|
|
|
|
// is within the destination range 100.100.0.0/16.
|
|
|
|
|
// ROOT CAUSE: Headscale checks if node IPs are in destination range,
|
|
|
|
|
// not just if advertised routes cover the destination.
|
|
|
|
|
// FIX REQUIRED: May need to exclude nodes whose IPs are in destination range.
|
|
|
|
|
{
|
|
|
|
|
name: "H8_cgnat_overlap_blocked",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["100.100.0.0/16:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/*
|
|
|
|
|
EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil, // No filter expected
|
|
|
|
|
"exit-node": { ... },
|
|
|
|
|
"multi-router": { ... },
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): big-router gets filter (its IP 100.100.100.1 is in 100.100.0.0/16)
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
// big-router gets filter because its IP (100.100.100.1) is in destination range
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// H11: IPv6 small prefix /128
|
|
|
|
|
{
|
|
|
|
|
name: "H11_ipv6_small_prefix",
|
|
|
|
|
// /128 is a single IPv6 address - smallest possible prefix
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["fd00::1/128:443"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Exit nodes with ::/0 cover all IPv6
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00::1/128", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "fd00::1/128", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalIPv6 tests additional IPv6 scenarios (I2, I3, I6).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalIPv6(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// TODO: Fix wildcard DstPorts format
|
|
|
|
|
//
|
|
|
|
|
// I2: IPv6 exit route ::/0 - verifies ::/0 NOT in SrcIPs
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts uses {IP: "*"} for wildcard destinations
|
|
|
|
|
// - ::/0 does NOT appear in SrcIPs (exit routes excluded)
|
|
|
|
|
// - Filter distributed to all nodes
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - DstPorts expands to CGNAT ranges instead of "*"
|
|
|
|
|
// - ::/0 correctly excluded from SrcIPs
|
|
|
|
|
// - Filter distributed to all nodes (same as Tailscale)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// Headscale expands "*" to CGNAT ranges instead of using "*".
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Use {IP: "*"} for wildcard destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "I2_ipv6_exit_route",
|
|
|
|
|
// ::/0 is the IPv6 exit route (like 0.0.0.0/0 for IPv4)
|
|
|
|
|
// Should NOT appear in SrcIPs
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "*", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ... same for all nodes with {IP: "*"}
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): DstPorts expanded to CGNAT ranges
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"client2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"user1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: wildcardDstPorts,
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit route coverage and per-node DstPorts filtering
|
|
|
|
|
//
|
|
|
|
|
// I3: IPv6 in wildcard SrcIPs
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs includes fd7a:115c:a1e0::/48 (IPv6 Tailscale range) - CORRECT
|
|
|
|
|
// - Only tag:router nodes receive filters (subnet-router, multi-router, big-router)
|
|
|
|
|
// - Exit-node (tag:exit only) does NOT get filter
|
|
|
|
|
// - Each tag:router node sees ALL tag:router IPs in DstPorts
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - SrcIPs correctly includes fd7a:115c:a1e0::/48 (IPv6 range works)
|
|
|
|
|
// - Exit-node incorrectly gets filter (exit route covers all tag:router IPs)
|
|
|
|
|
// - Each node only sees its OWN IPs in DstPorts (not all tag:router IPs)
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Exit route coverage: exit-node's 0.0.0.0/0 + ::/0 covers tag:router IPs
|
|
|
|
|
// 2. Per-node DstPorts: Headscale only includes self IPs in DstPorts
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// 1. Exclude exit nodes from tag-based destinations
|
|
|
|
|
// 2. Include all matching tag IPs in DstPorts for each destination node
|
|
|
|
|
{
|
|
|
|
|
name: "I3_ipv6_in_wildcard_srcips",
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["tag:router:22"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil, // tag:exit, NOT tag:router
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// ALL tag:router IPs
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// ... multi-router and big-router with same ALL tag:router IPs
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): exit-node gets filter, each node sees only self IPs
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
// INCORRECT: subnet-router only sees its own IPs
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Only self IPs (missing big-router and multi-router IPs)
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// multi-router has tag:router AND tag:exit, gets all tag:router IPs
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// All tag:router IPs (multi-router sees all)
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: big-router only sees its own IPs
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
// Only self IPs (missing subnet-router and multi-router IPs)
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit-node gets filter (should be nil - tag:exit not tag:router)
|
|
|
|
|
// Due to exit route coverage, it sees all tag:router IPs
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "100.100.100.1/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "100.119.139.79/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "100.74.117.7/32", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::4001:8ba0/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::6401:6401/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
{IP: "fd7a:115c:a1e0::c401:7508/128", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// TODO: Fix exit route coverage for subnet destinations
|
|
|
|
|
//
|
|
|
|
|
// I6: Dual-stack node - targeting both IPv4 and IPv6 subnets
|
|
|
|
|
//
|
|
|
|
|
// TAILSCALE BEHAVIOR:
|
|
|
|
|
// - Only subnet-router (10.33.0.0/16) and big-router (10.0.0.0/8) get IPv4 filter
|
|
|
|
|
// - No node has fd00:1::/64 route, so no node gets IPv6 filter
|
|
|
|
|
// - Exit nodes do NOT get filters for specific subnet destinations
|
|
|
|
|
// - Multiple rules with same SrcIPs kept as separate rules
|
|
|
|
|
//
|
|
|
|
|
// HEADSCALE BEHAVIOR:
|
|
|
|
|
// - Exit nodes get filters (exit route covers both subnets)
|
|
|
|
|
// - Rules with same SrcIPs and IPProto are MERGED into single rule
|
|
|
|
|
// with combined DstPorts
|
|
|
|
|
// - No node owns fd00:1::/64, but exit nodes cover it via ::/0
|
|
|
|
|
//
|
|
|
|
|
// ROOT CAUSE:
|
|
|
|
|
// 1. Exit route coverage: 0.0.0.0/0 and ::/0 cover all subnets
|
|
|
|
|
// 2. Filter rule merging: Headscale merges rules with identical SrcIPs/IPProto
|
|
|
|
|
//
|
|
|
|
|
// FIX REQUIRED:
|
|
|
|
|
// Exclude exit nodes from specific subnet destinations.
|
|
|
|
|
{
|
|
|
|
|
name: "I6_dual_stack_node",
|
|
|
|
|
// Target both IPv4 and IPv6 subnets
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.0.0/16:*"]},
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["fd00:1::/64:*"]}
|
|
|
|
|
`),
|
|
|
|
|
/* EXPECTED (Tailscale):
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"exit-node": nil, // Exit nodes shouldn't get subnet filters
|
|
|
|
|
"multi-router": nil, // Only has 172.16.0.0/24 + exit routes
|
|
|
|
|
// subnet-router owns 10.33.0.0/16
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// big-router covers 10.33.0.0/16 via 10.0.0.0/8
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
*/
|
|
|
|
|
// ACTUAL (Headscale): Exit nodes cover both, rules merged
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
// subnet-router owns 10.33.0.0/16
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// big-router covers 10.33.0.0/16 via 10.0.0.0/8
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: Exit-node gets MERGED filter covering both subnets
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
// Both destinations merged into single rule
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// INCORRECT: multi-router gets MERGED filter covering both subnets
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
// Both destinations merged into single rule
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
{IP: "fd00:1::/64", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTailscaleRoutesCompatAdditionalOverlapping tests additional overlapping route scenarios (O8, O9).
|
|
|
|
|
func TestTailscaleRoutesCompatAdditionalOverlapping(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
users := setupRouteCompatUsers()
|
|
|
|
|
nodes := setupRouteCompatNodes(users)
|
|
|
|
|
|
|
|
|
|
tests := []routesCompatTest{
|
|
|
|
|
// O8: Same node overlapping routes
|
|
|
|
|
// Node with 10.0.0.0/8, 10.33.0.0/16, 10.33.1.0/24 - NOT merged
|
|
|
|
|
{
|
|
|
|
|
name: "O8_same_node_overlapping_routes",
|
|
|
|
|
// If a single node advertises multiple overlapping routes,
|
|
|
|
|
// they should all appear separately, not merged
|
|
|
|
|
// big-router has 10.0.0.0/8
|
|
|
|
|
// Let's target a specific child prefix
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["*"], "dst": ["10.33.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
// big-router (10.0.0.0/8) and subnet-router (10.33.0.0/16) both cover
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil,
|
|
|
|
|
"client2": nil,
|
|
|
|
|
"user1": nil,
|
|
|
|
|
"ha-router1": nil,
|
|
|
|
|
"ha-router2": nil,
|
|
|
|
|
"subnet-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"big-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: wildcardSrcIPs,
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "10.33.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// O9: Different nodes same route
|
|
|
|
|
// Two nodes with 192.168.1.0/24 - only first is primary
|
|
|
|
|
{
|
|
|
|
|
name: "O9_different_nodes_same_route",
|
|
|
|
|
// ha-router1 and ha-router2 both have 192.168.1.0/24
|
|
|
|
|
// Both should receive filters, but only one is primary
|
|
|
|
|
policy: makeRoutesPolicy(`
|
|
|
|
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["192.168.1.0/24:*"]}
|
|
|
|
|
`),
|
|
|
|
|
wantFilters: map[string][]tailcfg.FilterRule{
|
|
|
|
|
"client1": nil, // Source
|
|
|
|
|
"client2": nil, // Source
|
|
|
|
|
"user1": nil, // Source
|
|
|
|
|
"subnet-router": nil,
|
|
|
|
|
"big-router": nil,
|
|
|
|
|
// Both HA routers get filter despite sharing route
|
|
|
|
|
"ha-router1": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ha-router2": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Exit nodes also cover
|
|
|
|
|
"exit-node": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"multi-router": {
|
|
|
|
|
{
|
|
|
|
|
SrcIPs: []string{
|
|
|
|
|
"100.116.73.38/32",
|
|
|
|
|
"100.89.42.23/32",
|
|
|
|
|
"100.90.199.68/32",
|
|
|
|
|
"fd7a:115c:a1e0::a801:4949/128",
|
|
|
|
|
"fd7a:115c:a1e0::d01:2a2e/128",
|
|
|
|
|
"fd7a:115c:a1e0::2d01:c747/128",
|
|
|
|
|
},
|
|
|
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
|
|
|
{IP: "192.168.1.0/24", Ports: tailcfg.PortRangeAny},
|
|
|
|
|
},
|
|
|
|
|
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runRoutesCompatTests(t, users, nodes, tests)
|
|
|
|
|
}
|