mapper: take peer visibility from the peer map only

Fixes #3408
This commit is contained in:
Kristoffer Dalby
2026-09-10 13:55:09 +00:00
parent 5861005ef6
commit e48bc46cc6
10 changed files with 134 additions and 962 deletions
-22
View File
@@ -8,30 +8,8 @@ import (
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"tailscale.com/types/views"
)
// ReduceNodes returns the list of peers authorized to be accessed from a given node.
func ReduceNodes(
node types.NodeView,
nodes views.Slice[types.NodeView],
matchers []matcher.Match,
) views.Slice[types.NodeView] {
var result []types.NodeView
for _, peer := range nodes.All() {
if peer.ID() == node.ID() {
continue
}
if node.CanAccess(matchers, peer) || peer.CanAccess(matchers, node) {
result = append(result, peer)
}
}
return views.SliceOf(result)
}
// ReduceRoutes returns a reduced list of routes for a given node that it can access.
func ReduceRoutes(
node types.NodeView,
+6 -841
View File
@@ -24,836 +24,7 @@ var p = func(prefStr string) netip.Prefix {
return ip
}
func TestReduceNodes(t *testing.T) {
type args struct {
nodes types.Nodes
rules []tailcfg.FilterRule
node *types.Node
}
tests := []struct {
name string
args args
want types.Nodes
}{
{
name: "all hosts can talk to each other",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
node: &types.Node{ // current nodes
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
},
want: types.Nodes{
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
},
{
name: "One host can talk to another, but not all hosts",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
node: &types.Node{ // current nodes
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
},
want: types.Nodes{
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
},
},
{
name: "host cannot directly talk to destination, but return path is authorized",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"100.64.0.3"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
},
want: types.Nodes{
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
},
{
name: "rules allows all hosts to reach one destination",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
node: &types.Node{ // current nodes
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
},
want: types.Nodes{
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
},
},
{
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
},
want: types.Nodes{
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
},
{
name: "rule allows all hosts to reach all destinations",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
},
want: types.Nodes{
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
},
{
name: "without rule all communications are forbidden",
args: args{
nodes: types.Nodes{ // list of all nodes in the database
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
},
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: &types.User{Name: "marc"},
},
},
want: nil,
},
{
// Investigating 699
// Found some nodes: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] nodes=ts-head-8w6paa
// ACL rules generated ACL=[{"DstPorts":[{"Bits":null,"IP":"*","Ports":{"First":0,"Last":65535}}],"SrcIPs":["fd7a:115c:a1e0::3","100.64.0.3","fd7a:115c:a1e0::4","100.64.0.4"]}]
// ACL Cache Map={"100.64.0.3":{"*":{}},"100.64.0.4":{"*":{}},"fd7a:115c:a1e0::3":{"*":{}},"fd7a:115c:a1e0::4":{"*":{}}}
name: "issue-699-broken-star",
args: args{
nodes: types.Nodes{ //
&types.Node{
ID: 1,
Hostname: "ts-head-upcrmb",
IPv4: ap("100.64.0.3"),
IPv6: ap("fd7a:115c:a1e0::3"),
User: &types.User{Name: "user1"},
},
&types.Node{
ID: 2,
Hostname: "ts-unstable-rlwpvr",
IPv4: ap("100.64.0.4"),
IPv6: ap("fd7a:115c:a1e0::4"),
User: &types.User{Name: "user1"},
},
&types.Node{
ID: 3,
Hostname: "ts-head-8w6paa",
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: &types.User{Name: "user2"},
},
&types.Node{
ID: 4,
Hostname: "ts-unstable-lys2ib",
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: &types.User{Name: "user2"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
DstPorts: []tailcfg.NetPortRange{
{
IP: "*",
Ports: tailcfg.PortRange{First: 0, Last: 65535},
},
},
SrcIPs: []string{
"fd7a:115c:a1e0::3", "100.64.0.3",
"fd7a:115c:a1e0::4", "100.64.0.4",
},
},
},
node: &types.Node{ // current nodes
ID: 3,
Hostname: "ts-head-8w6paa",
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: &types.User{Name: "user2"},
},
},
want: types.Nodes{
&types.Node{
ID: 1,
Hostname: "ts-head-upcrmb",
IPv4: ap("100.64.0.3"),
IPv6: ap("fd7a:115c:a1e0::3"),
User: &types.User{Name: "user1"},
},
&types.Node{
ID: 2,
Hostname: "ts-unstable-rlwpvr",
IPv4: ap("100.64.0.4"),
IPv6: ap("fd7a:115c:a1e0::4"),
User: &types.User{Name: "user1"},
},
},
},
{
name: "failing-edge-case-during-p3-refactor",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "peer1",
User: &types.User{Name: "mini"},
},
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "peer2",
User: &types.User{Name: "peer2"},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"100.64.0.1/32"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
{IP: "::/0", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "mini",
User: &types.User{Name: "mini"},
},
},
want: []*types.Node{
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "peer2",
User: &types.User{Name: "peer2"},
},
},
},
{
name: "p4-host-in-netmap-user2-dest-bug",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: &types.User{Name: "user1"},
},
{
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "user1-1",
User: &types.User{Name: "user1"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: &types.User{Name: "user2"},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{
"100.64.0.3/32",
"100.64.0.4/32",
"fd7a:115c:a1e0::3/128",
"fd7a:115c:a1e0::4/128",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
},
},
{
SrcIPs: []string{
"100.64.0.1/32",
"100.64.0.2/32",
"fd7a:115c:a1e0::1/128",
"fd7a:115c:a1e0::2/128",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "user-2-1",
User: &types.User{Name: "user2"},
},
},
want: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: &types.User{Name: "user1"},
},
{
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "user1-1",
User: &types.User{Name: "user1"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: &types.User{Name: "user2"},
},
},
},
{
name: "p4-host-in-netmap-user1-dest-bug",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: &types.User{Name: "user1"},
},
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "user-2-1",
User: &types.User{Name: "user2"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: &types.User{Name: "user2"},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{
"100.64.0.1/32",
"100.64.0.2/32",
"fd7a:115c:a1e0::1/128",
"fd7a:115c:a1e0::2/128",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.1/32", Ports: tailcfg.PortRangeAny},
{IP: "100.64.0.2/32", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny},
},
},
{
SrcIPs: []string{
"100.64.0.1/32",
"100.64.0.2/32",
"fd7a:115c:a1e0::1/128",
"fd7a:115c:a1e0::2/128",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "user1-1",
User: &types.User{Name: "user1"},
},
},
want: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: &types.User{Name: "user1"},
},
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "user-2-1",
User: &types.User{Name: "user2"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: &types.User{Name: "user2"},
},
},
},
{
name: "subnet-router-with-only-route",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "user1",
User: &types.User{Name: "user1"},
},
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "router",
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{
"100.64.0.1/32",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "user1",
User: &types.User{Name: "user1"},
},
},
want: []*types.Node{
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "router",
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
},
},
},
{
name: "subnet-router-with-only-route-smaller-mask-2181",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: &types.User{Name: "node"},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{
"100.64.0.2/32",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.0.2/32", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
},
want: []*types.Node{
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: &types.User{Name: "node"},
},
},
},
{
name: "node-to-subnet-router-with-only-route-smaller-mask-2181",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: &types.User{Name: "node"},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{
"100.64.0.2/32",
},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.0.2/32", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: &types.User{Name: "node"},
},
},
want: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
},
},
// Subnet-to-subnet: routers must see each other when ACL
// uses only subnet CIDRs. Issue #3157.
{
name: "subnet-to-subnet-routers-see-each-other-3157",
args: args{
nodes: []*types.Node{
{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router-a",
User: &types.User{Name: "router-a"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
},
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "router-b",
User: &types.User{Name: "router-b"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"10.88.8.0/24"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.9.0/24", Ports: tailcfg.PortRangeAny},
},
},
},
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router-a",
User: &types.User{Name: "router-a"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
},
},
want: []*types.Node{
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "router-b",
User: &types.User{Name: "router-b"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
},
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matchers := matcher.MatchesFromFilterRules(tt.args.rules)
gotViews := ReduceNodes(
tt.args.node.View(),
tt.args.nodes.ViewSlice(),
matchers,
)
// Convert views back to nodes for comparison in tests
var got types.Nodes
for _, v := range gotViews.All() {
got = append(got, v.AsStruct())
}
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
t.Errorf("ReduceNodes() unexpected result (-want +got):\n%s", diff)
t.Log("Matchers: ")
for _, m := range matchers {
t.Log("\t+", m.DebugString())
}
}
})
}
}
func TestReduceNodesFromPolicy(t *testing.T) {
func TestBuildPeerMapFromPolicy(t *testing.T) {
n := func(id types.NodeID, ip, hostname, username string, routess ...string) *types.Node {
routes := make([]netip.Prefix, 0, len(routess))
for _, route := range routess {
@@ -1107,19 +278,13 @@ func TestReduceNodesFromPolicy(t *testing.T) {
require.NoError(t, err)
assert.Len(t, matchers, tt.wantMatchers)
gotViews := ReduceNodes(
tt.node.View(),
tt.nodes.ViewSlice(),
matchers,
)
// Convert views back to nodes for comparison in tests
var got types.Nodes
for _, v := range gotViews.All() {
got = append(got, v.AsStruct())
var want []types.NodeID
for _, n := range tt.want {
want = append(want, n.ID)
}
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
t.Errorf("TestReduceNodesFromPolicy() unexpected result (-want +got):\n%s", diff)
got := pm.BuildPeerMap(tt.nodes.ViewSlice())[tt.node.ID]
if !assert.ElementsMatch(t, want, got) {
t.Log("Matchers: ")
for _, m := range matchers {
@@ -13,8 +13,8 @@
// - TestRoutesCompat: validates filter rule compilation (compileFilterRulesForNode
// + ReduceFilterRules) against golden file captures.
//
// - TestRoutesCompatPeerVisibility: validates peer visibility (CanAccess /
// ReduceNodes) for the subnet-to-subnet scenarios (f10–f15). These tests
// - TestRoutesCompatPeerVisibility: validates peer visibility (CanAccess)
// for the subnet-to-subnet scenarios (f10–f15). These tests
// derive expected peer relationships from the golden file captures: if
// Tailscale SaaS delivers filter rules to a node, then the subnet routers
// referenced in those rules must be visible as peers. This exercises the