types,policy: add tests for subnet-to-subnet route visibility

Add unit tests covering the subnet-to-subnet ACL scenario where both
src and dst are subnet CIDRs served by different subnet routers.

Tests cover CanAccess (peer visibility), ReduceRoutes (route filtering),
and ReduceNodes (peer list reduction). Positive cases for routers that
advertise the referenced subnets, and negative cases ensuring regular
nodes and unrelated routers gain no extra visibility.

These tests currently fail and will be fixed in the next commit.

Updates #3157
This commit is contained in:
Ubuntu
2026-04-02 13:23:14 +00:00
parent 835db974b5
commit 45d5544736
2 changed files with 357 additions and 0 deletions
+180
View File
@@ -768,6 +768,65 @@ func TestReduceNodes(t *testing.T) {
},
},
},
// 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 {
@@ -2230,6 +2289,127 @@ func TestReduceRoutes(t *testing.T) {
netip.MustParsePrefix("192.168.1.0/14"),
},
},
// Subnet-to-subnet tests for issue #3157.
// When an ACL references subnet CIDRs as both source and destination,
// the subnet routers for those subnets must receive routes to each
// other's subnets.
{
name: "subnet-to-subnet-src-router-gets-dst-route-3157",
args: args{
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
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"),
},
},
routes: []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},
},
},
},
},
want: []netip.Prefix{
netip.MustParsePrefix("10.99.9.0/24"),
},
},
{
name: "subnet-to-subnet-dst-router-gets-src-route-3157",
args: args{
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
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"),
},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"10.88.8.0/24"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.9.0/24", Ports: tailcfg.PortRangeAny},
},
},
},
},
want: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
{
name: "subnet-to-subnet-regular-node-no-route-leak-3157",
args: args{
node: &types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: &types.User{Name: "regular-node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
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},
},
},
},
},
want: nil,
},
{
name: "subnet-to-subnet-unrelated-router-no-route-leak-3157",
args: args{
node: &types.Node{
ID: 4,
IPv4: ap("100.64.0.4"),
User: &types.User{Name: "router-c"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("172.16.0.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("172.16.0.0/24"),
},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"10.88.8.0/24"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.9.0/24", Ports: tailcfg.PortRangeAny},
},
},
},
},
want: nil,
},
}
for _, tt := range tests {
+177
View File
@@ -113,6 +113,183 @@ func Test_NodeCanAccess(t *testing.T) {
},
want: true,
},
// Subnet-to-subnet tests for issue #3157.
// When ACL src and dst are both subnet CIDRs, subnet
// routers advertising those subnets must see each other.
{
name: "subnet-to-subnet-src-router-sees-dst-router-3157",
node1: Node{
IPv4: iap("100.64.0.1"),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
node2: Node{
IPv4: iap("100.64.0.2"),
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},
},
},
},
want: true,
},
{
// With a unidirectional ACL (src=A→dst=B), the dst
// router cannot access the src router. Bidirectional
// peer visibility comes from ReduceNodes checking
// both A.CanAccess(B) || B.CanAccess(A).
name: "subnet-to-subnet-unidirectional-dst-cannot-access-src-3157",
node1: Node{
IPv4: iap("100.64.0.2"),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("10.99.9.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("10.99.9.0/24"),
},
},
node2: Node{
IPv4: iap("100.64.0.1"),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"10.88.8.0/24"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.9.0/24", Ports: tailcfg.PortRangeAny},
},
},
},
want: false,
},
{
// With a bidirectional ACL, both routers can access
// each other.
name: "subnet-to-subnet-bidirectional-3157",
node1: Node{
IPv4: iap("100.64.0.2"),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("10.99.9.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("10.99.9.0/24"),
},
},
node2: Node{
IPv4: iap("100.64.0.1"),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("10.88.8.0/24"),
},
},
rules: []tailcfg.FilterRule{
{
SrcIPs: []string{"10.88.8.0/24"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.99.9.0/24", Ports: tailcfg.PortRangeAny},
},
},
{
SrcIPs: []string{"10.99.9.0/24"},
DstPorts: []tailcfg.NetPortRange{
{IP: "10.88.8.0/24", Ports: tailcfg.PortRangeAny},
},
},
},
want: true,
},
{
name: "subnet-to-subnet-regular-node-excluded-3157",
node1: Node{
IPv4: iap("100.64.0.3"),
},
node2: Node{
IPv4: iap("100.64.0.2"),
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},
},
},
},
want: false,
},
{
name: "subnet-to-subnet-unrelated-router-excluded-3157",
node1: Node{
IPv4: iap("100.64.0.3"),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("172.16.0.0/24"),
},
},
ApprovedRoutes: []netip.Prefix{
netip.MustParsePrefix("172.16.0.0/24"),
},
},
node2: Node{
IPv4: iap("100.64.0.2"),
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},
},
},
},
want: false,
},
}
for _, tt := range tests {