1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-08-10 13:46:46 +02:00

integration: add route with filter acl integration test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-05-03 23:00:22 +02:00
parent 3c5e35840b
commit c38667e105
No known key found for this signature in database
2 changed files with 153 additions and 4 deletions

View File

@ -70,6 +70,7 @@ jobs:
- TestSubnetRouterMultiNetwork
- TestSubnetRouterMultiNetworkExitNode
- TestAutoApproveMultiNetwork
- TestSubnetRouteACLFiltering
- TestHeadscale
- TestTailscaleNodesJoiningHeadcale
- TestSSHOneUserToAll

View File

@ -1,6 +1,7 @@
package integration
import (
"encoding/json"
"fmt"
"net/netip"
"sort"
@ -9,7 +10,7 @@ import (
"slices"
"github.com/google/go-cmp/cmp"
cmpdiff "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
@ -940,7 +941,7 @@ func TestSubnetRouteACL(t *testing.T) {
},
}
if diff := cmp.Diff(wantClientFilter, clientNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
if diff := cmpdiff.Diff(wantClientFilter, clientNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
t.Errorf("Client (%s) filter, unexpected result (-want +got):\n%s", client.Hostname(), diff)
}
@ -990,7 +991,7 @@ func TestSubnetRouteACL(t *testing.T) {
},
}
if diff := cmp.Diff(wantSubnetFilter, subnetNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
if diff := cmpdiff.Diff(wantSubnetFilter, subnetNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
}
}
@ -2007,7 +2008,7 @@ func requirePeerSubnetRoutes(t *testing.T, status *ipnstate.PeerStatus, expected
return !slices.ContainsFunc(status.TailscaleIPs, p.Contains)
})
if diff := cmp.Diff(expected, got, util.PrefixComparer, cmpopts.EquateEmpty()); diff != "" {
if diff := cmpdiff.Diff(expected, got, util.PrefixComparer, cmpopts.EquateEmpty()); diff != "" {
t.Fatalf("peer %s (%s) subnet routes, unexpected result (-want +got):\n%s", status.HostName, status.ID, diff)
}
}
@ -2018,3 +2019,150 @@ func requireNodeRouteCount(t *testing.T, node *v1.Node, announced, approved, sub
require.Lenf(t, node.GetApprovedRoutes(), approved, "expected %q approved routes(%v) to have %d route, had %d", node.GetName(), node.GetApprovedRoutes(), approved, len(node.GetApprovedRoutes()))
require.Lenf(t, node.GetSubnetRoutes(), subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.GetName(), node.GetSubnetRoutes(), subnet, len(node.GetSubnetRoutes()))
}
// TestSubnetRouteACLFiltering tests that a node can only access subnet routes
// that are explicitly allowed in the ACL.
func TestSubnetRouteACLFiltering(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
// Use router and node users for better clarity
routerUser := "router"
nodeUser := "node"
spec := ScenarioSpec{
NodesPerUser: 1,
Users: []string{routerUser, nodeUser},
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
// Set up the ACL policy that allows the node to access only one of the subnet routes (10.10.10.0/24)
aclPolicyStr := fmt.Sprintf(`{
"hosts": {
"router": "100.64.0.1/32",
"node": "100.64.0.2/32"
},
"acls": [
{
"action": "accept",
"src": [
"*"
],
"dst": [
"router:0"
]
},
{
"action": "accept",
"src": [
"node"
],
"dst": [
"10.10.10.0/24:*"
]
}
]
}`)
// Create ACL policy
aclPolicy := &policyv1.ACLPolicy{}
err = json.Unmarshal([]byte(aclPolicyStr), aclPolicy)
require.NoError(t, err)
err = scenario.CreateHeadscaleEnv([]tsic.Option{
tsic.WithAcceptRoutes(),
}, hsic.WithTestName("routeaclfilter"), hsic.WithACLPolicy(aclPolicy))
assertNoErrHeadscaleEnv(t, err)
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
headscale, err := scenario.Headscale()
assertNoErrGetHeadscale(t, err)
// Sort clients by ID for consistent order
slices.SortFunc(allClients, func(a, b TailscaleClient) int {
return b.MustIPv4().Compare(a.MustIPv4())
})
// Get the router and node clients
routerClient := allClients[0]
nodeClient := allClients[1]
// Set up the subnet routes for the router
routes := []string{
"10.10.10.0/24", // This should be accessible by the client
"10.10.11.0/24", // These should NOT be accessible
"10.10.12.0/24",
}
routeArg := "--advertise-routes=" + routes[0] + "," + routes[1] + "," + routes[2]
command := []string{
"tailscale",
"set",
routeArg,
}
_, _, err = routerClient.Execute(command)
require.NoErrorf(t, err, "failed to advertise routes: %s", err)
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
// List nodes and verify the router has 3 available routes
nodes, err := headscale.NodesByUser()
require.NoError(t, err)
require.Len(t, nodes, 2)
// Find the router node
routerNode := nodes[routerUser][0]
nodeNode := nodes[nodeUser][0]
require.NotNil(t, routerNode, "Router node not found")
require.NotNil(t, nodeNode, "Client node not found")
// Check that the router has 3 routes available but not approved yet
requireNodeRouteCount(t, routerNode, 3, 0, 0)
requireNodeRouteCount(t, nodeNode, 0, 0, 0)
// Approve all routes for the router
_, err = headscale.ApproveRoutes(
routerNode.GetId(),
util.MustStringsToPrefixes(routerNode.GetAvailableRoutes()),
)
require.NoError(t, err)
// Give some time for the routes to propagate
time.Sleep(5 * time.Second)
// List nodes and verify the router has 3 available routes
nodes, err = headscale.NodesByUser()
require.NoError(t, err)
require.Len(t, nodes, 2)
// Find the router node
routerNode = nodes[routerUser][0]
// Check that the router has 3 routes now approved and available
requireNodeRouteCount(t, routerNode, 3, 3, 3)
// Now check the client node status
nodeStatus, err := nodeClient.Status()
require.NoError(t, err)
routerStatus, err := routerClient.Status()
require.NoError(t, err)
// Check that the node can see the subnet routes from the router
routerPeerStatus := nodeStatus.Peer[routerStatus.Self.PublicKey]
// The node should only have 1 subnet route (10.10.10.0/24)
expectedRoutes := []netip.Prefix{netip.MustParsePrefix("10.10.10.0/24")}
requirePeerSubnetRoutes(t, routerPeerStatus, expectedRoutes)
}