policy: reduce routes sent to peers based on packetfilter (#2561)
* notifier: use convenience funcs Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: reduce routes based on policy Fixes #2365 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * hsic: more helper methods Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: more test cases Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * integration: add route with filter acl integration test Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * integration: correct route reduce test, now failing Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * mapper: compare peer routes against node Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * hs: more output to debug strings Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * types/node: slice.ContainsFunc Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: more reduce route test Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * changelog: add entry for route filter Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
b9868f6516
commit
45e38cb080
16 changed files with 903 additions and 47 deletions
|
@ -2,6 +2,7 @@ package matcher
|
|||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"slices"
|
||||
|
||||
|
@ -15,6 +16,21 @@ type Match struct {
|
|||
dests *netipx.IPSet
|
||||
}
|
||||
|
||||
func (m Match) DebugString() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("Match:\n")
|
||||
sb.WriteString(" Sources:\n")
|
||||
for _, prefix := range m.srcs.Prefixes() {
|
||||
sb.WriteString(" " + prefix.String() + "\n")
|
||||
}
|
||||
sb.WriteString(" Destinations:\n")
|
||||
for _, prefix := range m.dests.Prefixes() {
|
||||
sb.WriteString(" " + prefix.String() + "\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func MatchesFromFilterRules(rules []tailcfg.FilterRule) []Match {
|
||||
matches := make([]Match, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||
"net/netip"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||
|
||||
policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
|
@ -33,7 +34,7 @@ type PolicyManager interface {
|
|||
}
|
||||
|
||||
// NewPolicyManager returns a new policy manager, the version is determined by
|
||||
// the environment flag "HEADSCALE_EXPERIMENTAL_POLICY_V2".
|
||||
// the environment flag "HEADSCALE_POLICY_V1".
|
||||
func NewPolicyManager(pol []byte, users []types.User, nodes types.Nodes) (PolicyManager, error) {
|
||||
var polMan PolicyManager
|
||||
var err error
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/samber/lo"
|
||||
|
@ -12,8 +13,8 @@ import (
|
|||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// FilterNodesByACL returns the list of peers authorized to be accessed from a given node.
|
||||
func FilterNodesByACL(
|
||||
// ReduceNodes returns the list of peers authorized to be accessed from a given node.
|
||||
func ReduceNodes(
|
||||
node *types.Node,
|
||||
nodes types.Nodes,
|
||||
matchers []matcher.Match,
|
||||
|
@ -33,6 +34,23 @@ func FilterNodesByACL(
|
|||
return result
|
||||
}
|
||||
|
||||
// ReduceRoutes returns a reduced list of routes for a given node that it can access.
|
||||
func ReduceRoutes(
|
||||
node *types.Node,
|
||||
routes []netip.Prefix,
|
||||
matchers []matcher.Match,
|
||||
) []netip.Prefix {
|
||||
var result []netip.Prefix
|
||||
|
||||
for _, route := range routes {
|
||||
if node.CanAccessRoute(matchers, route) {
|
||||
result = append(result, route)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ReduceFilterRules takes a node and a set of rules and removes all rules and destinations
|
||||
// that are not relevant to that particular node.
|
||||
func ReduceFilterRules(node *types.Node, rules []tailcfg.FilterRule) []tailcfg.FilterRule {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"gorm.io/gorm"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
var ap = func(ipStr string) *netip.Addr {
|
||||
|
@ -23,6 +25,11 @@ var ap = func(ipStr string) *netip.Addr {
|
|||
return &ip
|
||||
}
|
||||
|
||||
var p = func(prefStr string) netip.Prefix {
|
||||
ip := netip.MustParsePrefix(prefStr)
|
||||
return ip
|
||||
}
|
||||
|
||||
// hsExitNodeDestForTest is the list of destination IP ranges that are allowed when
|
||||
// we use headscale "autogroup:internet".
|
||||
var hsExitNodeDestForTest = []tailcfg.NetPortRange{
|
||||
|
@ -762,6 +769,54 @@ func TestReduceFilterRules(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2365-only-route-policy",
|
||||
pol: `
|
||||
{
|
||||
"hosts": {
|
||||
"router": "100.64.0.1/32",
|
||||
"node": "100.64.0.2/32"
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": [
|
||||
"*"
|
||||
],
|
||||
"dst": [
|
||||
"router:8000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"src": [
|
||||
"node"
|
||||
],
|
||||
"dst": [
|
||||
"172.26.0.0/16:*"
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
`,
|
||||
node: &types.Node{
|
||||
IPv4: ap("100.64.0.2"),
|
||||
IPv6: ap("fd7a:115c:a1e0::2"),
|
||||
User: users[3],
|
||||
},
|
||||
peers: types.Nodes{
|
||||
&types.Node{
|
||||
IPv4: ap("100.64.0.1"),
|
||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
||||
User: users[1],
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
RoutableIPs: []netip.Prefix{p("172.16.0.0/24"), p("10.10.11.0/24"), p("10.10.12.0/24")},
|
||||
},
|
||||
ApprovedRoutes: []netip.Prefix{p("172.16.0.0/24"), p("10.10.11.0/24"), p("10.10.12.0/24")},
|
||||
},
|
||||
},
|
||||
want: []tailcfg.FilterRule{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -773,6 +828,7 @@ func TestReduceFilterRules(t *testing.T) {
|
|||
pm, err = pmf(users, append(tt.peers, tt.node))
|
||||
require.NoError(t, err)
|
||||
got, _ := pm.Filter()
|
||||
t.Logf("full filter:\n%s", must.Get(json.MarshalIndent(got, "", " ")))
|
||||
got = ReduceFilterRules(tt.node, got)
|
||||
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
|
@ -784,7 +840,7 @@ func TestReduceFilterRules(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFilterNodesByACL(t *testing.T) {
|
||||
func TestReduceNodes(t *testing.T) {
|
||||
type args struct {
|
||||
nodes types.Nodes
|
||||
rules []tailcfg.FilterRule
|
||||
|
@ -1530,7 +1586,7 @@ func TestFilterNodesByACL(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
matchers := matcher.MatchesFromFilterRules(tt.args.rules)
|
||||
got := FilterNodesByACL(
|
||||
got := ReduceNodes(
|
||||
tt.args.node,
|
||||
tt.args.nodes,
|
||||
matchers,
|
||||
|
@ -1946,3 +2002,470 @@ func TestSSHPolicyRules(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
func TestReduceRoutes(t *testing.T) {
|
||||
type args struct {
|
||||
node *types.Node
|
||||
routes []netip.Prefix
|
||||
rules []tailcfg.FilterRule
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []netip.Prefix
|
||||
}{
|
||||
{
|
||||
name: "node-can-access-all-routes",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"),
|
||||
User: types.User{Name: "user1"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node-can-access-specific-route",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"),
|
||||
User: types.User{Name: "user1"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.0.0.0/24"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node-can-access-multiple-specific-routes",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"),
|
||||
User: types.User{Name: "user1"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.0.0.0/24"},
|
||||
{IP: "192.168.1.0/24"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node-can-access-overlapping-routes",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"),
|
||||
User: types.User{Name: "user1"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/16"), // Overlaps with the first one
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.0.0.0/16"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/16"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node-with-no-matching-rules",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"),
|
||||
User: types.User{Name: "user1"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // Different source IP
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "node-with-both-ipv4-and-ipv6",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"),
|
||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
||||
User: types.User{Name: "user1"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("2001:db8::/64"),
|
||||
netip.MustParsePrefix("192.168.1.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"fd7a:115c:a1e0::1"}, // IPv6 source
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "2001:db8::/64"}, // IPv6 destination
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"}, // IPv4 source
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.0.0.0/24"}, // IPv4 destination
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("2001:db8::/64"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "router-with-multiple-routes-and-node-with-specific-access",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"), // Node IP
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"}, // Any source
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.1"}, // Router node
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // Node IP
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24"}, // Only one subnet allowed
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node-with-access-to-one-subnet-and-partial-overlap",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"),
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.10.0/16"), // Overlaps with the first one
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24"}, // Only specific subnet
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.10.0/16"), // With current implementation, this is included because it overlaps with the allowed subnet
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node-with-access-to-wildcard-subnet",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"),
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.0.0/16"}, // Broader subnet that includes all three
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-nodes-with-different-subnet-permissions",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"),
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"}, // Different node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.11.0/24"},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // Our node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24"},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.3"}, // Different node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.12.0/24"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exactly-matching-users-acl-example",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"), // node with IP 100.64.0.2
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
// This represents the rule: action: accept, src: ["*"], dst: ["router:0"]
|
||||
SrcIPs: []string{"*"}, // Any source
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.1"}, // Router IP
|
||||
},
|
||||
},
|
||||
{
|
||||
// This represents the rule: action: accept, src: ["node"], dst: ["10.10.10.0/24:*"]
|
||||
SrcIPs: []string{"100.64.0.2"}, // Node IP
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24", Ports: tailcfg.PortRangeAny}, // All ports on this subnet
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "acl-all-source-nodes-can-access-router-only-node-can-access-10.10.10.0-24",
|
||||
args: args{
|
||||
// When testing from router node's perspective
|
||||
node: &types.Node{
|
||||
ID: 1,
|
||||
IPv4: ap("100.64.0.1"), // router with IP 100.64.0.1
|
||||
User: types.User{Name: "router"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.1"}, // Router can be accessed by all
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // Only node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24"}, // Can access this subnet
|
||||
},
|
||||
},
|
||||
// Add a rule for router to access its own routes
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.1"}, // Router node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*"}, // Can access everything
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Router needs explicit rules to access routes
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "acl-specific-port-ranges-for-subnets",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"), // node
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24", Ports: tailcfg.PortRange{First: 22, Last: 22}}, // Only SSH
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.11.0/24", Ports: tailcfg.PortRange{First: 80, Last: 80}}, // Only HTTP
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Should get both subnets with specific port ranges
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "acl-order-of-rules-and-rule-specificity",
|
||||
args: args{
|
||||
node: &types.Node{
|
||||
ID: 2,
|
||||
IPv4: ap("100.64.0.2"), // node
|
||||
User: types.User{Name: "node"},
|
||||
},
|
||||
routes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
// First rule allows all traffic
|
||||
{
|
||||
SrcIPs: []string{"*"}, // Any source
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*", Ports: tailcfg.PortRangeAny}, // Any destination and any port
|
||||
},
|
||||
},
|
||||
// Second rule is more specific but should be overridden by the first rule
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.2"}, // node
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.10.10.0/24"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Due to the first rule allowing all traffic, node should have access to all routes
|
||||
want: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.10.10.0/24"),
|
||||
netip.MustParsePrefix("10.10.11.0/24"),
|
||||
netip.MustParsePrefix("10.10.12.0/24"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
matchers := matcher.MatchesFromFilterRules(tt.args.rules)
|
||||
got := ReduceRoutes(
|
||||
tt.args.node,
|
||||
tt.args.routes,
|
||||
matchers,
|
||||
)
|
||||
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
|
||||
t.Errorf("ReduceRoutes() unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,6 +152,10 @@ func (pm *PolicyManager) SetPolicy(polB []byte) (bool, error) {
|
|||
|
||||
// Filter returns the current filter rules for the entire tailnet and the associated matchers.
|
||||
func (pm *PolicyManager) Filter() ([]tailcfg.FilterRule, []matcher.Match) {
|
||||
if pm == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
return pm.filter, pm.matchers
|
||||
|
@ -159,6 +163,10 @@ func (pm *PolicyManager) Filter() ([]tailcfg.FilterRule, []matcher.Match) {
|
|||
|
||||
// SetUsers updates the users in the policy manager and updates the filter rules.
|
||||
func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) {
|
||||
if pm == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
pm.users = users
|
||||
|
@ -167,6 +175,10 @@ func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) {
|
|||
|
||||
// SetNodes updates the nodes in the policy manager and updates the filter rules.
|
||||
func (pm *PolicyManager) SetNodes(nodes types.Nodes) (bool, error) {
|
||||
if pm == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
pm.nodes = nodes
|
||||
|
@ -238,6 +250,10 @@ func (pm *PolicyManager) Version() int {
|
|||
}
|
||||
|
||||
func (pm *PolicyManager) DebugString() string {
|
||||
if pm == nil {
|
||||
return "PolicyManager is not setup"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, "PolicyManager (v%d):\n\n", pm.Version())
|
||||
|
@ -281,6 +297,14 @@ func (pm *PolicyManager) DebugString() string {
|
|||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString("Matchers:\n")
|
||||
sb.WriteString("an internal structure used to filter nodes and routes\n")
|
||||
for _, match := range pm.matchers {
|
||||
sb.WriteString(match.DebugString())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n\n")
|
||||
sb.WriteString(pm.nodes.DebugString())
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue