diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 2af3bc6f..7ad16ba0 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -249,7 +249,6 @@ jobs: - TestAutoApproveMultiNetwork/webauth-group.* - TestSubnetRouteACLFiltering - TestGrantViaSubnetSteering - - TestSubnetToSubnetACL - TestHeadscale - TestTailscaleNodesJoiningHeadcale - TestSSHOneUserToAll diff --git a/integration/route_test.go b/integration/route_test.go index 14f2ca59..525d6c2b 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -3493,225 +3493,3 @@ 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") -}