mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-08 00:50:20 +09:00
integration: add subnet-to-subnet ACL routing test
Add TestSubnetToSubnetACL which creates two subnet routers on separate Docker networks and verifies that ACL rules referencing only subnet CIDRs (not node IPs) as source and destination produce correct peer visibility, route distribution, and end-to-end connectivity. Updates #3157 Updates #3169
This commit is contained in:
@@ -3493,3 +3493,223 @@ func TestGrantViaSubnetSteering(t *testing.T) {
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, assertTimeout, 200*time.Millisecond, "Client B traceroute should go through Router B")
|
||||
}
|
||||
|
||||
// TestSubnetToSubnetACL verifies that when ACL rules reference only subnet
|
||||
// CIDRs as source and destination, the subnet routers for those subnets
|
||||
// see each other as peers and exchange routes, enabling end-to-end
|
||||
// connectivity between the two physical subnets.
|
||||
//
|
||||
// This is a regression test for https://github.com/juanfont/headscale/issues/3157
|
||||
func TestSubnetToSubnetACL(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
routerAUser := "routera"
|
||||
routerBUser := "routerb"
|
||||
|
||||
spec := ScenarioSpec{
|
||||
NodesPerUser: 1,
|
||||
Users: []string{routerAUser, routerBUser},
|
||||
Networks: map[string]NetworkSpec{
|
||||
"usernet1": {Users: []string{routerAUser}},
|
||||
"usernet2": {Users: []string{routerBUser}},
|
||||
},
|
||||
ExtraService: map[string][]extraServiceFunc{
|
||||
"usernet1": {Webservice},
|
||||
"usernet2": {Webservice},
|
||||
},
|
||||
Versions: []string{"head"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
prefA, err := scenario.SubnetOfNetwork("usernet1")
|
||||
require.NoError(t, err)
|
||||
prefB, err := scenario.SubnetOfNetwork("usernet2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// ACL: only subnet-A ↔ subnet-B traffic is allowed.
|
||||
// No rules reference node IPs directly.
|
||||
aclPolicy := &policyv2.Policy{
|
||||
ACLs: []policyv2.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Sources: []policyv2.Alias{
|
||||
prefixp(prefA.String()),
|
||||
},
|
||||
Destinations: []policyv2.AliasWithPorts{
|
||||
aliasWithPorts(prefixp(prefB.String()), tailcfg.PortRangeAny),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: "accept",
|
||||
Sources: []policyv2.Alias{
|
||||
prefixp(prefB.String()),
|
||||
},
|
||||
Destinations: []policyv2.AliasWithPorts{
|
||||
aliasWithPorts(prefixp(prefA.String()), tailcfg.PortRangeAny),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(
|
||||
[]tsic.Option{tsic.WithAcceptRoutes()},
|
||||
hsic.WithTestName("subnet2subnet"),
|
||||
hsic.WithACLPolicy(aclPolicy),
|
||||
)
|
||||
requireNoErrHeadscaleEnv(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
requireNoErrGetHeadscale(t, err)
|
||||
|
||||
routerAClients, err := scenario.ListTailscaleClients(routerAUser)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, routerAClients, 1)
|
||||
routerA := routerAClients[0]
|
||||
|
||||
routerBClients, err := scenario.ListTailscaleClients(routerBUser)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, routerBClients, 1)
|
||||
routerB := routerBClients[0]
|
||||
|
||||
// Advertise subnet routes.
|
||||
_, _, err = routerA.Execute([]string{
|
||||
"tailscale", "set",
|
||||
"--advertise-routes=" + prefA.String(),
|
||||
})
|
||||
require.NoErrorf(t, err, "failed to advertise route on router A")
|
||||
|
||||
_, _, err = routerB.Execute([]string{
|
||||
"tailscale", "set",
|
||||
"--advertise-routes=" + prefB.String(),
|
||||
})
|
||||
require.NoErrorf(t, err, "failed to advertise route on router B")
|
||||
|
||||
// Wait for advertisements to propagate.
|
||||
var nodes []*v1.Node
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
||||
// Both should have 1 available route.
|
||||
for _, n := range nodes {
|
||||
requireNodeRouteCountWithCollect(c, n, 1, 0, 0)
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 100*time.Millisecond,
|
||||
"route advertisements should propagate")
|
||||
|
||||
// Approve routes on both routers.
|
||||
for _, n := range nodes {
|
||||
_, err = headscale.ApproveRoutes(
|
||||
n.GetId(),
|
||||
util.MustStringsToPrefixes(n.GetAvailableRoutes()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Wait for approvals to propagate.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
||||
for _, n := range nodes {
|
||||
requireNodeRouteCountWithCollect(c, n, 1, 1, 1)
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond,
|
||||
"route approvals should propagate")
|
||||
|
||||
// Verify both routers see each other's subnet routes.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
statusA, err := routerA.Status()
|
||||
assert.NoError(c, err)
|
||||
|
||||
for _, peerKey := range statusA.Peers() {
|
||||
peerStatus := statusA.Peer[peerKey]
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*prefB})
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond,
|
||||
"Router A should see Router B's subnet route")
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
statusB, err := routerB.Status()
|
||||
assert.NoError(c, err)
|
||||
|
||||
for _, peerKey := range statusB.Peers() {
|
||||
peerStatus := statusB.Peer[peerKey]
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*prefA})
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond,
|
||||
"Router B should see Router A's subnet route")
|
||||
|
||||
// Verify end-to-end connectivity: Router A → webservice on network B.
|
||||
usernet2, err := scenario.Network("usernet2")
|
||||
require.NoError(t, err)
|
||||
|
||||
servicesB, err := scenario.Services("usernet2")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, servicesB, 1)
|
||||
|
||||
webB := servicesB[0]
|
||||
webBIP := netip.MustParseAddr(webB.GetIPInNetwork(usernet2))
|
||||
urlB := fmt.Sprintf("http://%s/etc/hostname", webBIP)
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
result, err := routerA.Curl(urlB)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, result, 13)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond,
|
||||
"Router A should reach webservice on network B")
|
||||
|
||||
// Verify end-to-end connectivity: Router B → webservice on network A.
|
||||
usernet1, err := scenario.Network("usernet1")
|
||||
require.NoError(t, err)
|
||||
|
||||
servicesA, err := scenario.Services("usernet1")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, servicesA, 1)
|
||||
|
||||
webA := servicesA[0]
|
||||
webAIP := netip.MustParseAddr(webA.GetIPInNetwork(usernet1))
|
||||
urlA := fmt.Sprintf("http://%s/etc/hostname", webAIP)
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
result, err := routerB.Curl(urlA)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, result, 13)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond,
|
||||
"Router B should reach webservice on network A")
|
||||
|
||||
// Verify traceroute paths.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := routerA.Traceroute(webBIP)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := routerB.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for routerB") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond,
|
||||
"Traceroute from Router A should go through Router B")
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := routerB.Traceroute(webAIP)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := routerA.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for routerA") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond,
|
||||
"Traceroute from Router B should go through Router A")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user