mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-18 14:32:01 +09:00
95ba787417
Adjacency becomes immutable, so a snapshot can resolve peers through its own fresh views instead of storing them. Updates #3417
102 lines
3.3 KiB
Go
102 lines
3.3 KiB
Go
// A via grant scoping autogroup:internet to a tag must surface only
|
|
// the matching exit node to the source — not strip every exit node
|
|
// from the source's view.
|
|
//
|
|
// Spec: https://tailscale.com/docs/features/access-control/grants/grants-via#route-users-through-exit-nodes-based-on-location
|
|
package v2
|
|
|
|
import (
|
|
"net/netip"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/stretchr/testify/require"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// TestIssue3233ViaInternetExitVisibility loads a policy where alice's
|
|
// only access to autogroup:internet is via tag:exit1. Alice sees her
|
|
// tag:exit1 exit node as a peer with 0.0.0.0/0 + ::/0 in AllowedIPs,
|
|
// and does not see bob's tag:exit2 exit node.
|
|
func TestIssue3233ViaInternetExitVisibility(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
users := types.Users{
|
|
{ID: 1, Name: "alice", Email: "alice@headscale.net"},
|
|
{ID: 2, Name: "bob", Email: "bob@headscale.net"},
|
|
}
|
|
|
|
exitRoutes := []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}
|
|
|
|
aliceLaptop := node("alice-laptop", "100.64.0.10", "fd7a:115c:a1e0::a", users[0])
|
|
aliceLaptop.ID = 1
|
|
|
|
aliceExit := node("alice-exit", "100.64.0.11", "fd7a:115c:a1e0::b", users[0])
|
|
aliceExit.ID = 2
|
|
aliceExit.Tags = []string{"tag:exit1"}
|
|
aliceExit.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: exitRoutes}
|
|
aliceExit.ApprovedRoutes = exitRoutes
|
|
|
|
bobExit := node("bob-exit", "100.64.0.21", "fd7a:115c:a1e0::15", users[1])
|
|
bobExit.ID = 3
|
|
bobExit.Tags = []string{"tag:exit2"}
|
|
bobExit.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: exitRoutes}
|
|
bobExit.ApprovedRoutes = exitRoutes
|
|
|
|
nodes := types.Nodes{aliceLaptop, aliceExit, bobExit}
|
|
|
|
policy := `{
|
|
"tagOwners": {
|
|
"tag:exit1": ["alice@headscale.net"],
|
|
"tag:exit2": ["bob@headscale.net"]
|
|
},
|
|
"grants": [
|
|
{
|
|
"src": ["alice@headscale.net"],
|
|
"dst": ["autogroup:internet"],
|
|
"via": ["tag:exit1"],
|
|
"ip": ["*"]
|
|
}
|
|
]
|
|
}`
|
|
|
|
pm, err := NewPolicyManager([]byte(policy), users, nodes.ViewSlice())
|
|
require.NoError(t, err)
|
|
|
|
t.Run("BuildPeerMap_includes_via_tagged_exit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
|
|
|
require.True(t,
|
|
slices.Contains(peerMap[aliceLaptop.ID], aliceExit.ID),
|
|
"alice must see her tag:exit1 exit node as a peer")
|
|
|
|
require.False(t,
|
|
slices.Contains(peerMap[aliceLaptop.ID], bobExit.ID),
|
|
"alice must not see bob's tag:exit2 exit node — via grant scopes to tag:exit1")
|
|
})
|
|
|
|
t.Run("ViaRoutesForPeer_includes_exit_for_matching_tag", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := pm.ViaRoutesForPeer(aliceLaptop.View(), aliceExit.View())
|
|
require.Contains(t, result.Include, tsaddr.AllIPv4(),
|
|
"alice viewing tag:exit1 exit must Include 0.0.0.0/0 — drives AllowedIPs in state.RoutesForPeer")
|
|
require.Contains(t, result.Include, tsaddr.AllIPv6(),
|
|
"alice viewing tag:exit1 exit must Include ::/0 — drives AllowedIPs in state.RoutesForPeer")
|
|
})
|
|
|
|
t.Run("ViaRoutesForPeer_excludes_exit_for_other_tag", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result := pm.ViaRoutesForPeer(aliceLaptop.View(), bobExit.View())
|
|
require.Contains(t, result.Exclude, tsaddr.AllIPv4(),
|
|
"alice viewing tag:exit2 exit must Exclude 0.0.0.0/0 — strips it from AllowedIPs")
|
|
require.Contains(t, result.Exclude, tsaddr.AllIPv6(),
|
|
"alice viewing tag:exit2 exit must Exclude ::/0 — strips it from AllowedIPs")
|
|
})
|
|
}
|