Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions hscontrol/db/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/net/tsaddr"
"tailscale.com/types/key"
"tailscale.com/types/ptr"
)
Expand Down Expand Up @@ -232,6 +233,17 @@ func SetApprovedRoutes(
return nil
}

// When approving exit routes, ensure both IPv4 and IPv6 are included
// If either 0.0.0.0/0 or ::/0 is being approved, both should be approved
hasIPv4Exit := slices.Contains(routes, tsaddr.AllIPv4())
hasIPv6Exit := slices.Contains(routes, tsaddr.AllIPv6())

if hasIPv4Exit && !hasIPv6Exit {
routes = append(routes, tsaddr.AllIPv6())
} else if hasIPv6Exit && !hasIPv4Exit {
routes = append(routes, tsaddr.AllIPv4())
}

b, err := json.Marshal(routes)
if err != nil {
return err
Expand Down
6 changes: 3 additions & 3 deletions hscontrol/db/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ func TestAutoApproveRoutes(t *testing.T) {
require.NoError(t, err)
}

newRoutes2, changed2 := policy.ApproveRoutesWithPolicy(pm, nodeTagged.View(), node.ApprovedRoutes, tt.routes)
newRoutes2, changed2 := policy.ApproveRoutesWithPolicy(pm, nodeTagged.View(), nodeTagged.ApprovedRoutes, tt.routes)
if changed2 {
err = SetApprovedRoutes(adb.DB, nodeTagged.ID, newRoutes2)
require.NoError(t, err)
Expand All @@ -490,7 +490,7 @@ func TestAutoApproveRoutes(t *testing.T) {
if len(expectedRoutes1) == 0 {
expectedRoutes1 = nil
}
if diff := cmp.Diff(expectedRoutes1, node1ByID.SubnetRoutes(), util.Comparers...); diff != "" {
if diff := cmp.Diff(expectedRoutes1, node1ByID.AllApprovedRoutes(), util.Comparers...); diff != "" {
t.Errorf("unexpected enabled routes (-want +got):\n%s", diff)
}

Expand All @@ -501,7 +501,7 @@ func TestAutoApproveRoutes(t *testing.T) {
if len(expectedRoutes2) == 0 {
expectedRoutes2 = nil
}
if diff := cmp.Diff(expectedRoutes2, node2ByID.SubnetRoutes(), util.Comparers...); diff != "" {
if diff := cmp.Diff(expectedRoutes2, node2ByID.AllApprovedRoutes(), util.Comparers...); diff != "" {
t.Errorf("unexpected enabled routes (-want +got):\n%s", diff)
}
})
Expand Down
4 changes: 3 additions & 1 deletion hscontrol/mapper/tail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,12 @@ func TestTailNode(t *testing.T) {
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
tsaddr.AllIPv6(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
},
ApprovedRoutes: []netip.Prefix{tsaddr.AllIPv4(), netip.MustParsePrefix("192.168.0.0/24")},
ApprovedRoutes: []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), netip.MustParsePrefix("192.168.0.0/24")},
CreatedAt: created,
},
dnsConfig: &tailcfg.DNSConfig{},
Expand Down Expand Up @@ -150,6 +151,7 @@ func TestTailNode(t *testing.T) {
Hostinfo: hiview(tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
tsaddr.AllIPv4(),
tsaddr.AllIPv6(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("172.0.0.0/10"),
},
Expand Down
10 changes: 10 additions & 0 deletions hscontrol/policy/matcher/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
)

Expand Down Expand Up @@ -91,3 +92,12 @@ func (m *Match) SrcsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
func (m *Match) DestsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
return slices.ContainsFunc(prefixes, m.dests.OverlapsPrefix)
}

// DestsIsTheInternet reports if the destination is equal to "the internet"
// which is a IPSet that represents "autogroup:internet" and is special
// cased for exit nodes.
func (m Match) DestsIsTheInternet() bool {
return m.dests.Equal(util.TheInternet()) ||
m.dests.ContainsPrefix(tsaddr.AllIPv4()) ||
m.dests.ContainsPrefix(tsaddr.AllIPv6())
}
278 changes: 277 additions & 1 deletion hscontrol/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/juanfont/headscale/hscontrol/policy/matcher"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"tailscale.com/tailcfg"
Expand Down Expand Up @@ -782,12 +783,287 @@ func TestReduceNodes(t *testing.T) {
got = append(got, v.AsStruct())
}
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
t.Errorf("FilterNodesByACL() unexpected result (-want +got):\n%s", diff)
t.Errorf("ReduceNodes() unexpected result (-want +got):\n%s", diff)
t.Log("Matchers: ")
for _, m := range matchers {
t.Log("\t+", m.DebugString())
}
}
})
}
}

func TestReduceNodesFromPolicy(t *testing.T) {
n := func(id types.NodeID, ip, hostname, username string, routess ...string) *types.Node {
var routes []netip.Prefix
for _, route := range routess {
routes = append(routes, netip.MustParsePrefix(route))
}

return &types.Node{
ID: id,
IPv4: ap(ip),
Hostname: hostname,
User: types.User{Name: username},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: routes,
},
ApprovedRoutes: routes,
}
}

type args struct {
}
tests := []struct {
name string
nodes types.Nodes
policy string
node *types.Node
want types.Nodes
wantMatchers int
}{
{
name: "2788-exit-node-too-visible",
nodes: types.Nodes{
n(1, "100.64.0.1", "mobile", "mobile"),
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
policy: `
{
"hosts": {
"mobile": "100.64.0.1/32",
"server": "100.64.0.2/32",
"exit": "100.64.0.3/32"
},

"acls": [
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"server:80"
]
}
]
}`,
node: n(1, "100.64.0.1", "mobile", "mobile"),
want: types.Nodes{
n(2, "100.64.0.2", "server", "server"),
},
wantMatchers: 1,
},
{
name: "2788-exit-node-autogroup:internet",
nodes: types.Nodes{
n(1, "100.64.0.1", "mobile", "mobile"),
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
policy: `
{
"hosts": {
"mobile": "100.64.0.1/32",
"server": "100.64.0.2/32",
"exit": "100.64.0.3/32"
},

"acls": [
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"server:80"
]
},
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"autogroup:internet:*"
]
}
]
}`,
node: n(1, "100.64.0.1", "mobile", "mobile"),
want: types.Nodes{
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
wantMatchers: 2,
},
{
name: "2788-exit-node-0000-route",
nodes: types.Nodes{
n(1, "100.64.0.1", "mobile", "mobile"),
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
policy: `
{
"hosts": {
"mobile": "100.64.0.1/32",
"server": "100.64.0.2/32",
"exit": "100.64.0.3/32"
},

"acls": [
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"server:80"
]
},
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"0.0.0.0/0:*"
]
}
]
}`,
node: n(1, "100.64.0.1", "mobile", "mobile"),
want: types.Nodes{
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
wantMatchers: 2,
},
{
name: "2788-exit-node-::0-route",
nodes: types.Nodes{
n(1, "100.64.0.1", "mobile", "mobile"),
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
policy: `
{
"hosts": {
"mobile": "100.64.0.1/32",
"server": "100.64.0.2/32",
"exit": "100.64.0.3/32"
},

"acls": [
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"server:80"
]
},
{
"action": "accept",
"src": [
"mobile"
],
"dst": [
"::0/0:*"
]
}
]
}`,
node: n(1, "100.64.0.1", "mobile", "mobile"),
want: types.Nodes{
n(2, "100.64.0.2", "server", "server"),
n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"),
},
wantMatchers: 2,
},
{
name: "2784-split-exit-node-access",
nodes: types.Nodes{
n(1, "100.64.0.1", "user", "user"),
n(2, "100.64.0.2", "exit1", "exit", "0.0.0.0/0", "::/0"),
n(3, "100.64.0.3", "exit2", "exit", "0.0.0.0/0", "::/0"),
n(4, "100.64.0.4", "otheruser", "otheruser"),
},
policy: `
{
"hosts": {
"user": "100.64.0.1/32",
"exit1": "100.64.0.2/32",
"exit2": "100.64.0.3/32",
"otheruser": "100.64.0.4/32",
},

"acls": [
{
"action": "accept",
"src": [
"user"
],
"dst": [
"exit1:*"
]
},
{
"action": "accept",
"src": [
"otheruser"
],
"dst": [
"exit2:*"
]
}
]
}`,
node: n(1, "100.64.0.1", "user", "user"),
want: types.Nodes{
n(2, "100.64.0.2", "exit1", "exit", "0.0.0.0/0", "::/0"),
},
wantMatchers: 2,
},
}

for _, tt := range tests {
for idx, pmf := range PolicyManagerFuncsForTest([]byte(tt.policy)) {
t.Run(fmt.Sprintf("%s-index%d", tt.name, idx), func(t *testing.T) {
var pm PolicyManager
var err error
pm, err = pmf(nil, tt.nodes.ViewSlice())
require.NoError(t, err)

matchers, err := pm.MatchersForNode(tt.node.View())
require.NoError(t, err)
assert.Len(t, matchers, tt.wantMatchers)

gotViews := ReduceNodes(
tt.node.View(),
tt.nodes.ViewSlice(),
matchers,
)
// Convert views back to nodes for comparison in tests
var got types.Nodes
for _, v := range gotViews.All() {
got = append(got, v.AsStruct())
}
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
t.Errorf("TestReduceNodesFromPolicy() unexpected result (-want +got):\n%s", diff)
t.Log("Matchers: ")
for _, m := range matchers {
t.Log("\t+", m.DebugString())
}
}
})
}
}
}

func TestSSHPolicyRules(t *testing.T) {
users := []types.User{
{Name: "user1", Model: gorm.Model{ID: 1}},
Expand Down
Loading
Loading