package policyutil_test import ( "encoding/json" "fmt" "net/netip" "testing" "github.com/google/go-cmp/cmp" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy/policyutil" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" "gorm.io/gorm" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/util/must" ) var ap = func(ipStr string) *netip.Addr { ip := netip.MustParseAddr(ipStr) 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{ {IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny}, {IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny}, {IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "64.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "96.0.0.0/6", Ports: tailcfg.PortRangeAny}, {IP: "100.0.0.0/10", Ports: tailcfg.PortRangeAny}, {IP: "100.128.0.0/9", Ports: tailcfg.PortRangeAny}, {IP: "101.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "102.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "104.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "112.0.0.0/4", Ports: tailcfg.PortRangeAny}, {IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "168.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "169.0.0.0/9", Ports: tailcfg.PortRangeAny}, {IP: "169.128.0.0/10", Ports: tailcfg.PortRangeAny}, {IP: "169.192.0.0/11", Ports: tailcfg.PortRangeAny}, {IP: "169.224.0.0/12", Ports: tailcfg.PortRangeAny}, {IP: "169.240.0.0/13", Ports: tailcfg.PortRangeAny}, {IP: "169.248.0.0/14", Ports: tailcfg.PortRangeAny}, {IP: "169.252.0.0/15", Ports: tailcfg.PortRangeAny}, {IP: "169.255.0.0/16", Ports: tailcfg.PortRangeAny}, {IP: "170.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny}, {IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny}, {IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny}, {IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny}, {IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny}, {IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny}, {IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny}, {IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny}, {IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny}, {IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny}, {IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny}, {IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny}, {IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny}, {IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny}, {IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny}, {IP: "224.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "2000::/3", Ports: tailcfg.PortRangeAny}, } func TestTheInternet(t *testing.T) { internetSet := util.TheInternet() internetPrefs := internetSet.Prefixes() for i := range internetPrefs { if internetPrefs[i].String() != hsExitNodeDestForTest[i].IP { t.Errorf( "prefix from internet set %q != hsExit list %q", internetPrefs[i].String(), hsExitNodeDestForTest[i].IP, ) } } if len(internetPrefs) != len(hsExitNodeDestForTest) { t.Fatalf( "expected same length of prefixes, internet: %d, hsExit: %d", len(internetPrefs), len(hsExitNodeDestForTest), ) } } func TestReduceFilterRules(t *testing.T) { users := types.Users{ types.User{Model: gorm.Model{ID: 1}, Name: "mickael"}, types.User{Model: gorm.Model{ID: 2}, Name: "user1"}, types.User{Model: gorm.Model{ID: 3}, Name: "user2"}, types.User{Model: gorm.Model{ID: 4}, Name: "user100"}, types.User{Model: gorm.Model{ID: 5}, Name: "user3"}, } tests := []struct { name string node *types.Node peers types.Nodes pol string want []tailcfg.FilterRule }{ { name: "host1-can-reach-host2-no-rules", pol: ` { "acls": [ { "action": "accept", "proto": "", "src": [ "100.64.0.1" ], "dst": [ "100.64.0.2:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), User: users[0], }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), User: users[0], }, }, want: []tailcfg.FilterRule{}, }, { name: "1604-subnet-routers-are-preserved", pol: ` { "groups": { "group:admins": [ "user1@" ] }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:admins" ], "dst": [ "group:admins:*" ] }, { "action": "accept", "proto": "", "src": [ "group:admins" ], "dst": [ "10.33.0.0/16:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{ netip.MustParsePrefix("10.33.0.0/16"), }, }, }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0::2"), User: users[1], }, }, want: []tailcfg.FilterRule{ { SrcIPs: []string{ "100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128", }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.1/32", Ports: tailcfg.PortRangeAny, }, { IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, { SrcIPs: []string{ "100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128", }, DstPorts: []tailcfg.NetPortRange{ { IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, }, }, { name: "1786-reducing-breaks-exit-nodes-the-client", pol: ` { "groups": { "group:team": [ "user3@", "user2@", "user1@" ] }, "hosts": { "internal": "100.64.0.100/32" }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "internal:*" ] }, { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "autogroup:internet:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0::2"), User: users[2], }, // "internal" exit node &types.Node{ IPv4: ap("100.64.0.100"), IPv6: ap("fd7a:115c:a1e0::100"), User: users[3], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: tsaddr.ExitRoutes(), }, }, }, want: []tailcfg.FilterRule{}, }, { name: "1786-reducing-breaks-exit-nodes-the-exit", pol: ` { "groups": { "group:team": [ "user3@", "user2@", "user1@" ] }, "hosts": { "internal": "100.64.0.100/32" }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "internal:*" ] }, { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "autogroup:internet:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.100"), IPv6: ap("fd7a:115c:a1e0::100"), User: users[3], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: tsaddr.ExitRoutes(), }, }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0::2"), User: users[2], }, &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], }, }, want: []tailcfg.FilterRule{ { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100/32", Ports: tailcfg.PortRangeAny, }, { IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: hsExitNodeDestForTest, IPProto: []int{6, 17}, }, }, }, { name: "1786-reducing-breaks-exit-nodes-the-example-from-issue", pol: ` { "groups": { "group:team": [ "user3@", "user2@", "user1@" ] }, "hosts": { "internal": "100.64.0.100/32" }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "internal:*" ] }, { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "0.0.0.0/5:*", "8.0.0.0/7:*", "11.0.0.0/8:*", "12.0.0.0/6:*", "16.0.0.0/4:*", "32.0.0.0/3:*", "64.0.0.0/2:*", "128.0.0.0/3:*", "160.0.0.0/5:*", "168.0.0.0/6:*", "172.0.0.0/12:*", "172.32.0.0/11:*", "172.64.0.0/10:*", "172.128.0.0/9:*", "173.0.0.0/8:*", "174.0.0.0/7:*", "176.0.0.0/4:*", "192.0.0.0/9:*", "192.128.0.0/11:*", "192.160.0.0/13:*", "192.169.0.0/16:*", "192.170.0.0/15:*", "192.172.0.0/14:*", "192.176.0.0/12:*", "192.192.0.0/10:*", "193.0.0.0/8:*", "194.0.0.0/7:*", "196.0.0.0/6:*", "200.0.0.0/5:*", "208.0.0.0/4:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.100"), IPv6: ap("fd7a:115c:a1e0::100"), User: users[3], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: tsaddr.ExitRoutes(), }, }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0::2"), User: users[2], }, &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], }, }, want: []tailcfg.FilterRule{ { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100/32", Ports: tailcfg.PortRangeAny, }, { IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ {IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny}, {IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny}, {IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "64.0.0.0/2", Ports: tailcfg.PortRangeAny}, {IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "168.0.0.0/6", Ports: tailcfg.PortRangeAny}, {IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny}, {IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny}, {IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny}, {IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny}, {IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny}, {IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny}, {IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny}, {IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny}, {IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny}, {IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny}, {IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny}, {IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny}, {IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny}, {IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny}, {IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny}, {IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny}, {IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny}, {IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny}, }, IPProto: []int{6, 17}, }, }, }, { name: "1786-reducing-breaks-exit-nodes-app-connector-like", pol: ` { "groups": { "group:team": [ "user3@", "user2@", "user1@" ] }, "hosts": { "internal": "100.64.0.100/32" }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "internal:*" ] }, { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "8.0.0.0/8:*", "16.0.0.0/8:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.100"), IPv6: ap("fd7a:115c:a1e0::100"), User: users[3], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")}, }, }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0::2"), User: users[2], }, &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], }, }, want: []tailcfg.FilterRule{ { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100/32", Ports: tailcfg.PortRangeAny, }, { IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "8.0.0.0/8", Ports: tailcfg.PortRangeAny, }, { IP: "16.0.0.0/8", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, }, }, { name: "1786-reducing-breaks-exit-nodes-app-connector-like2", pol: ` { "groups": { "group:team": [ "user3@", "user2@", "user1@" ] }, "hosts": { "internal": "100.64.0.100/32" }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "internal:*" ] }, { "action": "accept", "proto": "", "src": [ "group:team" ], "dst": [ "8.0.0.0/16:*", "16.0.0.0/16:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.100"), IPv6: ap("fd7a:115c:a1e0::100"), User: users[3], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")}, }, }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.2"), IPv6: ap("fd7a:115c:a1e0::2"), User: users[2], }, &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], }, }, want: []tailcfg.FilterRule{ { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100/32", Ports: tailcfg.PortRangeAny, }, { IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "8.0.0.0/16", Ports: tailcfg.PortRangeAny, }, { IP: "16.0.0.0/16", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, }, }, { name: "1817-reduce-breaks-32-mask", pol: ` { "tagOwners": { "tag:access-servers": ["user100@"], }, "groups": { "group:access": [ "user1@" ] }, "hosts": { "dns1": "172.16.0.21/32", "vlan1": "172.16.0.0/24" }, "acls": [ { "action": "accept", "proto": "", "src": [ "group:access" ], "dst": [ "tag:access-servers:*", "dns1:*" ] } ], } `, node: &types.Node{ IPv4: ap("100.64.0.100"), IPv6: ap("fd7a:115c:a1e0::100"), User: users[3], Hostinfo: &tailcfg.Hostinfo{ RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")}, }, ForcedTags: []string{"tag:access-servers"}, }, peers: types.Nodes{ &types.Node{ IPv4: ap("100.64.0.1"), IPv6: ap("fd7a:115c:a1e0::1"), User: users[1], }, }, want: []tailcfg.FilterRule{ { SrcIPs: []string{"100.64.0.1/32", "fd7a:115c:a1e0::1/128"}, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100/32", Ports: tailcfg.PortRangeAny, }, { IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny, }, { IP: "172.16.0.21/32", Ports: tailcfg.PortRangeAny, }, }, IPProto: []int{6, 17}, }, }, }, { 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 { for idx, pmf := range policy.PolicyManagerFuncsForTest([]byte(tt.pol)) { t.Run(fmt.Sprintf("%s-index%d", tt.name, idx), func(t *testing.T) { var pm policy.PolicyManager var err error pm, err = pmf(users, append(tt.peers, tt.node).ViewSlice()) require.NoError(t, err) got, _ := pm.Filter() t.Logf("full filter:\n%s", must.Get(json.MarshalIndent(got, "", " "))) got = policyutil.ReduceFilterRules(tt.node.View(), got) if diff := cmp.Diff(tt.want, got); diff != "" { log.Trace().Interface("got", got).Msg("result") t.Errorf("TestReduceFilterRules() unexpected result (-want +got):\n%s", diff) } }) } } }