From e9ffc4f2df6bbe5cf1077432bf06ff37efc31145 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 26 Feb 2025 19:15:04 +0100 Subject: [PATCH] split policy -> policy and v1 This commit split out the common policy logic and policy implementation into separate packages. policy contains functions that are independent of the policy implementation, this typically means logic that works on tailcfg types and generic formats. In addition, it defines the PolicyManager interface which the v1 implements. v1 is a subpackage which implements the PolicyManager using the "original" policy implementation. Signed-off-by: Kristoffer Dalby --- hscontrol/mapper/mapper_test.go | 53 +- hscontrol/mapper/tail_test.go | 10 +- hscontrol/policy/pm.go | 236 +-- hscontrol/policy/policy.go | 109 ++ hscontrol/policy/policy_test.go | 1455 +++++++++++++++++ hscontrol/policy/{ => v1}/acls.go | 121 +- hscontrol/policy/{ => v1}/acls_test.go | 1384 +--------------- hscontrol/policy/{ => v1}/acls_types.go | 2 +- hscontrol/policy/v1/policy.go | 187 +++ .../policy/{pm_test.go => v1/policy_test.go} | 2 +- hscontrol/poll.go | 26 +- hscontrol/util/net.go | 31 +- 12 files changed, 1874 insertions(+), 1742 deletions(-) create mode 100644 hscontrol/policy/policy.go create mode 100644 hscontrol/policy/policy_test.go rename hscontrol/policy/{ => v1}/acls.go (88%) rename hscontrol/policy/{ => v1}/acls_test.go (66%) rename hscontrol/policy/{ => v1}/acls_types.go (99%) create mode 100644 hscontrol/policy/v1/policy.go rename hscontrol/policy/{pm_test.go => v1/policy_test.go} (99%) diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index 51c09411..6dd3387d 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -11,6 +11,7 @@ import ( "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/require" "gorm.io/gorm" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" @@ -246,7 +247,7 @@ func Test_fullMapResponse(t *testing.T) { tests := []struct { name string - pol *policy.ACLPolicy + pol []byte node *types.Node peers types.Nodes @@ -258,7 +259,7 @@ func Test_fullMapResponse(t *testing.T) { // { // name: "empty-node", // node: types.Node{}, - // pol: &policy.ACLPolicy{}, + // pol: &policyv1.ACLPolicy{}, // dnsConfig: &tailcfg.DNSConfig{}, // baseDomain: "", // want: nil, @@ -266,7 +267,6 @@ func Test_fullMapResponse(t *testing.T) { // }, { name: "no-pol-no-peers-map-response", - pol: &policy.ACLPolicy{}, node: mini, peers: types.Nodes{}, derpMap: &tailcfg.DERPMap{}, @@ -284,10 +284,15 @@ func Test_fullMapResponse(t *testing.T) { DNSConfig: &tailcfg.DNSConfig{}, Domain: "", CollectServices: "false", - PacketFilter: []tailcfg.FilterRule{}, - UserProfiles: []tailcfg.UserProfile{{ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"}}, - SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, - ControlTime: &time.Time{}, + UserProfiles: []tailcfg.UserProfile{ + { + ID: tailcfg.UserID(user1.ID), + LoginName: "user1", + DisplayName: "user1", + }, + }, + PacketFilter: tailcfg.FilterAllowAll, + ControlTime: &time.Time{}, Debug: &tailcfg.Debug{ DisableLogTail: true, }, @@ -296,7 +301,6 @@ func Test_fullMapResponse(t *testing.T) { }, { name: "no-pol-with-peer-map-response", - pol: &policy.ACLPolicy{}, node: mini, peers: types.Nodes{ peer1, @@ -318,13 +322,12 @@ func Test_fullMapResponse(t *testing.T) { DNSConfig: &tailcfg.DNSConfig{}, Domain: "", CollectServices: "false", - PacketFilter: []tailcfg.FilterRule{}, UserProfiles: []tailcfg.UserProfile{ {ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"}, {ID: tailcfg.UserID(user2.ID), LoginName: "user2", DisplayName: "user2"}, }, - SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, - ControlTime: &time.Time{}, + PacketFilter: tailcfg.FilterAllowAll, + ControlTime: &time.Time{}, Debug: &tailcfg.Debug{ DisableLogTail: true, }, @@ -333,18 +336,17 @@ func Test_fullMapResponse(t *testing.T) { }, { name: "with-pol-map-response", - pol: &policy.ACLPolicy{ - Hosts: policy.Hosts{ - "mini": netip.MustParsePrefix("100.64.0.1/32"), - }, - ACLs: []policy.ACL{ - { - Action: "accept", - Sources: []string{"100.64.0.2"}, - Destinations: []string{"mini:*"}, - }, - }, - }, + pol: []byte(` + { + "acls": [ + { + "action": "accept", + "src": ["100.64.0.2"], + "dst": ["user1:*"], + }, + ], + } + `), node: mini, peers: types.Nodes{ peer1, @@ -374,11 +376,11 @@ func Test_fullMapResponse(t *testing.T) { }, }, }, + SSHPolicy: &tailcfg.SSHPolicy{}, UserProfiles: []tailcfg.UserProfile{ {ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"}, {ID: tailcfg.UserID(user2.ID), LoginName: "user2", DisplayName: "user2"}, }, - SSHPolicy: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{}}, ControlTime: &time.Time{}, Debug: &tailcfg.Debug{ DisableLogTail: true, @@ -390,7 +392,8 @@ func Test_fullMapResponse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{user1, user2}, append(tt.peers, tt.node)) + polMan, err := policy.NewPolicyManager(tt.pol, []types.User{user1, user2}, append(tt.peers, tt.node)) + require.NoError(t, err) primary := routes.New() primary.SetRoutes(tt.node.ID, tt.node.SubnetRoutes()...) diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index 6a620467..919ea43c 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -11,6 +11,7 @@ import ( "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/require" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -49,7 +50,7 @@ func TestTailNode(t *testing.T) { tests := []struct { name string node *types.Node - pol *policy.ACLPolicy + pol []byte dnsConfig *tailcfg.DNSConfig baseDomain string want *tailcfg.Node @@ -61,7 +62,6 @@ func TestTailNode(t *testing.T) { GivenName: "empty", Hostinfo: &tailcfg.Hostinfo{}, }, - pol: &policy.ACLPolicy{}, dnsConfig: &tailcfg.DNSConfig{}, baseDomain: "", want: &tailcfg.Node{ @@ -117,7 +117,6 @@ func TestTailNode(t *testing.T) { ApprovedRoutes: []netip.Prefix{tsaddr.AllIPv4(), netip.MustParsePrefix("192.168.0.0/24")}, CreatedAt: created, }, - pol: &policy.ACLPolicy{}, dnsConfig: &tailcfg.DNSConfig{}, baseDomain: "", want: &tailcfg.Node{ @@ -179,7 +178,8 @@ func TestTailNode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - polMan, _ := policy.NewPolicyManagerForTest(tt.pol, []types.User{}, types.Nodes{tt.node}) + polMan, err := policy.NewPolicyManager(tt.pol, []types.User{}, types.Nodes{tt.node}) + require.NoError(t, err) primary := routes.New() cfg := &types.Config{ BaseDomain: tt.baseDomain, @@ -248,7 +248,7 @@ func TestNodeExpiry(t *testing.T) { tn, err := tailNode( node, 0, - &policy.PolicyManagerV1{}, + nil, // TODO(kradalby): removed in merge but error? nil, &types.Config{}, ) diff --git a/hscontrol/policy/pm.go b/hscontrol/policy/pm.go index 980dc5aa..24f68ca1 100644 --- a/hscontrol/policy/pm.go +++ b/hscontrol/policy/pm.go @@ -1,219 +1,81 @@ package policy import ( - "fmt" - "io" "net/netip" - "os" - "sync" + policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/types" - "github.com/rs/zerolog/log" - "go4.org/netipx" + "tailscale.com/envknob" "tailscale.com/tailcfg" - "tailscale.com/util/deephash" +) + +var ( + polv2 = envknob.Bool("HEADSCALE_EXPERIMENTAL_POLICY_V2") ) type PolicyManager interface { Filter() []tailcfg.FilterRule SSHPolicy(*types.Node) (*tailcfg.SSHPolicy, error) - Tags(*types.Node) []string - ApproversForRoute(netip.Prefix) []string - ExpandAlias(string) (*netipx.IPSet, error) SetPolicy([]byte) (bool, error) SetUsers(users []types.User) (bool, error) SetNodes(nodes types.Nodes) (bool, error) + // NodeCanHaveTag reports whether the given node can have the given tag. + NodeCanHaveTag(*types.Node, string) bool // NodeCanApproveRoute reports whether the given node can approve the given route. NodeCanApproveRoute(*types.Node, netip.Prefix) bool + + Version() int + DebugString() string } -func NewPolicyManagerFromPath(path string, users []types.User, nodes types.Nodes) (PolicyManager, error) { - policyFile, err := os.Open(path) - if err != nil { - return nil, err - } - defer policyFile.Close() - - policyBytes, err := io.ReadAll(policyFile) - if err != nil { - return nil, err - } - - return NewPolicyManager(policyBytes, users, nodes) -} - -func NewPolicyManager(polB []byte, users []types.User, nodes types.Nodes) (PolicyManager, error) { - var pol *ACLPolicy +// NewPolicyManager returns a new policy manager, the version is determined by +// the environment flag "HEADSCALE_EXPERIMENTAL_POLICY_V2". +func NewPolicyManager(pol []byte, users []types.User, nodes types.Nodes) (PolicyManager, error) { + var polMan PolicyManager var err error - if polB != nil && len(polB) > 0 { - pol, err = LoadACLPolicyFromBytes(polB) + if polv2 { + polMan, err = policyv2.NewPolicyManager(pol, users, nodes) if err != nil { - return nil, fmt.Errorf("parsing policy: %w", err) + return nil, err + } + } else { + polMan, err = policyv1.NewPolicyManager(pol, users, nodes) + if err != nil { + return nil, err } } - pm := PolicyManagerV1{ - pol: pol, - users: users, - nodes: nodes, - } - - _, err = pm.updateLocked() - if err != nil { - return nil, err - } - - return &pm, nil + return polMan, err } -func NewPolicyManagerForTest(pol *ACLPolicy, users []types.User, nodes types.Nodes) (PolicyManager, error) { - pm := PolicyManagerV1{ - pol: pol, - users: users, - nodes: nodes, - } +// PolicyManagersForTest returns all available PostureManagers to be used +// in tests to validate them in tests that try to determine that they +// behave the same. +func PolicyManagersForTest(pol []byte, users []types.User, nodes types.Nodes) ([]PolicyManager, error) { + var polMans []PolicyManager - _, err := pm.updateLocked() - if err != nil { - return nil, err - } - - return &pm, nil -} - -type PolicyManagerV1 struct { - mu sync.Mutex - pol *ACLPolicy - - users []types.User - nodes types.Nodes - - filterHash deephash.Sum - filter []tailcfg.FilterRule -} - -// updateLocked updates the filter rules based on the current policy and nodes. -// It must be called with the lock held. -func (pm *PolicyManagerV1) updateLocked() (bool, error) { - filter, err := pm.pol.CompileFilterRules(pm.users, pm.nodes) - if err != nil { - return false, fmt.Errorf("compiling filter rules: %w", err) - } - - filterHash := deephash.Hash(&filter) - if filterHash == pm.filterHash { - return false, nil - } - - pm.filter = filter - pm.filterHash = filterHash - - return true, nil -} - -func (pm *PolicyManagerV1) Filter() []tailcfg.FilterRule { - pm.mu.Lock() - defer pm.mu.Unlock() - return pm.filter -} - -func (pm *PolicyManagerV1) SSHPolicy(node *types.Node) (*tailcfg.SSHPolicy, error) { - pm.mu.Lock() - defer pm.mu.Unlock() - - return pm.pol.CompileSSHPolicy(node, pm.users, pm.nodes) -} - -func (pm *PolicyManagerV1) SetPolicy(polB []byte) (bool, error) { - if len(polB) == 0 { - return false, nil - } - - pol, err := LoadACLPolicyFromBytes(polB) - if err != nil { - return false, fmt.Errorf("parsing policy: %w", err) - } - - pm.mu.Lock() - defer pm.mu.Unlock() - - pm.pol = pol - - return pm.updateLocked() -} - -// SetUsers updates the users in the policy manager and updates the filter rules. -func (pm *PolicyManagerV1) SetUsers(users []types.User) (bool, error) { - pm.mu.Lock() - defer pm.mu.Unlock() - - pm.users = users - return pm.updateLocked() -} - -// SetNodes updates the nodes in the policy manager and updates the filter rules. -func (pm *PolicyManagerV1) SetNodes(nodes types.Nodes) (bool, error) { - pm.mu.Lock() - defer pm.mu.Unlock() - pm.nodes = nodes - return pm.updateLocked() -} - -func (pm *PolicyManagerV1) Tags(node *types.Node) []string { - if pm == nil { - return nil - } - - tags, invalid := pm.pol.TagsOfNode(pm.users, node) - log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy") - return tags -} - -func (pm *PolicyManagerV1) ApproversForRoute(route netip.Prefix) []string { - // TODO(kradalby): This can be a parse error of the address in the policy, - // in the new policy this will be typed and not a problem, in this policy - // we will just return empty list - if pm.pol == nil { - return nil - } - approvers, _ := pm.pol.AutoApprovers.GetRouteApprovers(route) - return approvers -} - -func (pm *PolicyManagerV1) ExpandAlias(alias string) (*netipx.IPSet, error) { - ips, err := pm.pol.ExpandAlias(pm.nodes, pm.users, alias) - if err != nil { - return nil, err - } - return ips, nil -} - -func (pm *PolicyManagerV1) NodeCanApproveRoute(node *types.Node, route netip.Prefix) bool { - if pm.pol == nil { - return false - } - - pm.mu.Lock() - defer pm.mu.Unlock() - - approvers, _ := pm.pol.AutoApprovers.GetRouteApprovers(route) - - for _, approvedAlias := range approvers { - if approvedAlias == node.User.Username() { - return true - } else { - ips, err := pm.pol.ExpandAlias(pm.nodes, pm.users, approvedAlias) - if err != nil { - return false - } - - // approvedIPs should contain all of node's IPs if it matches the rule, so check for first - if ips.Contains(*node.IPv4) { - return true - } + for _, pmf := range PolicyManagerFuncsForTest(pol) { + pm, err := pmf(users, nodes) + if err != nil { + return nil, err } + polMans = append(polMans, pm) } - return false + return polMans, nil +} + +func PolicyManagerFuncsForTest(pol []byte) []func([]types.User, types.Nodes) (PolicyManager, error) { + var polmanFuncs []func([]types.User, types.Nodes) (PolicyManager, error) + + polmanFuncs = append(polmanFuncs, func(u []types.User, n types.Nodes) (PolicyManager, error) { + return policyv1.NewPolicyManager(pol, u, n) + }) + polmanFuncs = append(polmanFuncs, func(u []types.User, n types.Nodes) (PolicyManager, error) { + return policyv2.NewPolicyManager(pol, u, n) + }) + + return polmanFuncs } diff --git a/hscontrol/policy/policy.go b/hscontrol/policy/policy.go new file mode 100644 index 00000000..ba375beb --- /dev/null +++ b/hscontrol/policy/policy.go @@ -0,0 +1,109 @@ +package policy + +import ( + "net/netip" + "slices" + + "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" + "github.com/samber/lo" + "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" +) + +// FilterNodesByACL returns the list of peers authorized to be accessed from a given node. +func FilterNodesByACL( + node *types.Node, + nodes types.Nodes, + filter []tailcfg.FilterRule, +) types.Nodes { + var result types.Nodes + + for index, peer := range nodes { + if peer.ID == node.ID { + continue + } + + if node.CanAccess(filter, nodes[index]) || peer.CanAccess(filter, node) { + result = append(result, peer) + } + } + + 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 { + ret := []tailcfg.FilterRule{} + + for _, rule := range rules { + // record if the rule is actually relevant for the given node. + var dests []tailcfg.NetPortRange + DEST_LOOP: + for _, dest := range rule.DstPorts { + expanded, err := util.ParseIPSet(dest.IP, nil) + // Fail closed, if we can't parse it, then we should not allow + // access. + if err != nil { + continue DEST_LOOP + } + + if node.InIPSet(expanded) { + dests = append(dests, dest) + continue DEST_LOOP + } + + // If the node exposes routes, ensure they are note removed + // when the filters are reduced. + if node.Hostinfo != nil { + if len(node.Hostinfo.RoutableIPs) > 0 { + for _, routableIP := range node.Hostinfo.RoutableIPs { + if expanded.OverlapsPrefix(routableIP) { + dests = append(dests, dest) + continue DEST_LOOP + } + } + } + } + } + + if len(dests) > 0 { + ret = append(ret, tailcfg.FilterRule{ + SrcIPs: rule.SrcIPs, + DstPorts: dests, + IPProto: rule.IPProto, + }) + } + } + + return ret +} + +// AutoApproveRoutes approves any route that can be autoapproved from +// the nodes perspective according to the given policy. +// It reports true if any routes were approved. +func AutoApproveRoutes(pm PolicyManager, node *types.Node) bool { + if pm == nil { + return false + } + var newApproved []netip.Prefix + for _, route := range node.AnnouncedRoutes() { + if pm.NodeCanApproveRoute(node, route) { + newApproved = append(newApproved, route) + } + } + if newApproved != nil { + newApproved = append(newApproved, node.ApprovedRoutes...) + tsaddr.SortPrefixes(newApproved) + newApproved = slices.Compact(newApproved) + newApproved = lo.Filter(newApproved, func(route netip.Prefix, index int) bool { + return route.IsValid() + }) + node.ApprovedRoutes = newApproved + + return true + } + + return false +} diff --git a/hscontrol/policy/policy_test.go b/hscontrol/policy/policy_test.go new file mode 100644 index 00000000..e67af16f --- /dev/null +++ b/hscontrol/policy/policy_test.go @@ -0,0 +1,1455 @@ +package policy + +import ( + "fmt" + "net/netip" + "testing" + + "github.com/google/go-cmp/cmp" + "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" +) + +var ap = func(ipStr string) *netip.Addr { + ip := netip.MustParseAddr(ipStr) + 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), + ) + } +} + +// addAtForFilterV1 returns a copy of the given userslice +// and adds "@" character to the Name field. +// This is a "compatibility" move to allow the old tests +// to run against the "new" format which requires "@". +func addAtForFilterV1(users types.Users) types.Users { + ret := make(types.Users, len(users)) + for idx := range users { + ret[idx] = users[idx] + ret[idx].Name = ret[idx].Name + "@" + } + return ret +} + +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, + }, + }, + }, + { + 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, + }, + }, + }, + }, + }, + { + 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, + }, + }, + }, + { + SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, + DstPorts: hsExitNodeDestForTest, + }, + }, + }, + { + 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, + }, + }, + }, + { + 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}, + // This should not be included I believe, seems like + // this is a bug in the v1 code. + // For example: + // If a src or dst includes "64.0.0.0/2:*", it will include 100.64/16 range, which + // means that it will need to fetch the IPv6 addrs of the node to include the full range. + // Clearly, if a user sets the dst to be "64.0.0.0/2:*", it is likely more of a exit node + // and this would be strange behaviour. + // TODO(kradalby): Remove before launch. + {IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny}, + // End + {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}, + }, + }, + }, + }, + { + 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, + }, + }, + }, + { + 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, + }, + }, + }, + }, + }, + { + 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, + }, + }, + }, + { + 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, + }, + }, + }, + }, + }, + { + name: "1817-reduce-breaks-32-mask", + pol: ` +{ + "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, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + for idx, pmf := range PolicyManagerFuncsForTest([]byte(tt.pol)) { + version := idx + 1 + t.Run(fmt.Sprintf("%s-v%d", tt.name, version), func(t *testing.T) { + var pm PolicyManager + var err error + if version == 1 { + pm, err = pmf(addAtForFilterV1(users), append(tt.peers, tt.node)) + } else { + pm, err = pmf(users, append(tt.peers, tt.node)) + } + require.NoError(t, err) + got := pm.Filter() + got = ReduceFilterRules(tt.node, 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) + } + }) + } + } +} + +func TestFilterNodesByACL(t *testing.T) { + type args struct { + nodes types.Nodes + rules []tailcfg.FilterRule + node *types.Node + } + tests := []struct { + name string + args args + want types.Nodes + }{ + { + name: "all hosts can talk to each other", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "*"}, + }, + }, + }, + node: &types.Node{ // current nodes + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + }, + { + name: "One host can talk to another, but not all hosts", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.2"}, + }, + }, + }, + node: &types.Node{ // current nodes + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + }, + }, + { + name: "host cannot directly talk to destination, but return path is authorized", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"100.64.0.3"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.2"}, + }, + }, + }, + node: &types.Node{ // current nodes + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + }, + { + name: "rules allows all hosts to reach one destination", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.2"}, + }, + }, + }, + node: &types.Node{ // current nodes + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + }, + }, + { + name: "rules allows all hosts to reach one destination, destination can reach all hosts", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.2"}, + }, + }, + }, + node: &types.Node{ // current nodes + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + }, + { + name: "rule allows all hosts to reach all destinations", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "*"}, + }, + }, + }, + node: &types.Node{ // current nodes + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + }, + { + name: "without rule all communications are forbidden", + args: args{ + nodes: types.Nodes{ // list of all nodes in the database + &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + User: types.User{Name: "joe"}, + }, + &types.Node{ + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + &types.Node{ + ID: 3, + IPv4: ap("100.64.0.3"), + User: types.User{Name: "mickael"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + }, + node: &types.Node{ // current nodes + ID: 2, + IPv4: ap("100.64.0.2"), + User: types.User{Name: "marc"}, + }, + }, + want: nil, + }, + { + // Investigating 699 + // Found some nodes: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] nodes=ts-head-8w6paa + // ACL rules generated ACL=[{"DstPorts":[{"Bits":null,"IP":"*","Ports":{"First":0,"Last":65535}}],"SrcIPs":["fd7a:115c:a1e0::3","100.64.0.3","fd7a:115c:a1e0::4","100.64.0.4"]}] + // ACL Cache Map={"100.64.0.3":{"*":{}},"100.64.0.4":{"*":{}},"fd7a:115c:a1e0::3":{"*":{}},"fd7a:115c:a1e0::4":{"*":{}}} + name: "issue-699-broken-star", + args: args{ + nodes: types.Nodes{ // + &types.Node{ + ID: 1, + Hostname: "ts-head-upcrmb", + IPv4: ap("100.64.0.3"), + IPv6: ap("fd7a:115c:a1e0::3"), + User: types.User{Name: "user1"}, + }, + &types.Node{ + ID: 2, + Hostname: "ts-unstable-rlwpvr", + IPv4: ap("100.64.0.4"), + IPv6: ap("fd7a:115c:a1e0::4"), + User: types.User{Name: "user1"}, + }, + &types.Node{ + ID: 3, + Hostname: "ts-head-8w6paa", + IPv4: ap("100.64.0.1"), + IPv6: ap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user2"}, + }, + &types.Node{ + ID: 4, + Hostname: "ts-unstable-lys2ib", + IPv4: ap("100.64.0.2"), + IPv6: ap("fd7a:115c:a1e0::2"), + User: types.User{Name: "user2"}, + }, + }, + rules: []tailcfg.FilterRule{ // list of all ACLRules registered + { + DstPorts: []tailcfg.NetPortRange{ + { + IP: "*", + Ports: tailcfg.PortRange{First: 0, Last: 65535}, + }, + }, + SrcIPs: []string{ + "fd7a:115c:a1e0::3", "100.64.0.3", + "fd7a:115c:a1e0::4", "100.64.0.4", + }, + }, + }, + node: &types.Node{ // current nodes + ID: 3, + Hostname: "ts-head-8w6paa", + IPv4: ap("100.64.0.1"), + IPv6: ap("fd7a:115c:a1e0::1"), + User: types.User{Name: "user2"}, + }, + }, + want: types.Nodes{ + &types.Node{ + ID: 1, + Hostname: "ts-head-upcrmb", + IPv4: ap("100.64.0.3"), + IPv6: ap("fd7a:115c:a1e0::3"), + User: types.User{Name: "user1"}, + }, + &types.Node{ + ID: 2, + Hostname: "ts-unstable-rlwpvr", + IPv4: ap("100.64.0.4"), + IPv6: ap("fd7a:115c:a1e0::4"), + User: types.User{Name: "user1"}, + }, + }, + }, + { + name: "failing-edge-case-during-p3-refactor", + args: args{ + nodes: []*types.Node{ + { + ID: 1, + IPv4: ap("100.64.0.2"), + Hostname: "peer1", + User: types.User{Name: "mini"}, + }, + { + ID: 2, + IPv4: ap("100.64.0.3"), + Hostname: "peer2", + User: types.User{Name: "peer2"}, + }, + }, + rules: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32"}, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny}, + {IP: "::/0", Ports: tailcfg.PortRangeAny}, + }, + }, + }, + node: &types.Node{ + ID: 0, + IPv4: ap("100.64.0.1"), + Hostname: "mini", + User: types.User{Name: "mini"}, + }, + }, + want: []*types.Node{ + { + ID: 2, + IPv4: ap("100.64.0.3"), + Hostname: "peer2", + User: types.User{Name: "peer2"}, + }, + }, + }, + { + name: "p4-host-in-netmap-user2-dest-bug", + args: args{ + nodes: []*types.Node{ + { + ID: 1, + IPv4: ap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, + }, + { + ID: 0, + IPv4: ap("100.64.0.1"), + Hostname: "user1-1", + User: types.User{Name: "user1"}, + }, + { + ID: 3, + IPv4: ap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, + }, + }, + rules: []tailcfg.FilterRule{ + { + SrcIPs: []string{ + "100.64.0.3/32", + "100.64.0.4/32", + "fd7a:115c:a1e0::3/128", + "fd7a:115c:a1e0::4/128", + }, + DstPorts: []tailcfg.NetPortRange{ + {IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny}, + {IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny}, + }, + }, + { + 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.3/32", Ports: tailcfg.PortRangeAny}, + {IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny}, + }, + }, + }, + node: &types.Node{ + ID: 2, + IPv4: ap("100.64.0.3"), + Hostname: "user-2-1", + User: types.User{Name: "user2"}, + }, + }, + want: []*types.Node{ + { + ID: 1, + IPv4: ap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, + }, + { + ID: 0, + IPv4: ap("100.64.0.1"), + Hostname: "user1-1", + User: types.User{Name: "user1"}, + }, + { + ID: 3, + IPv4: ap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, + }, + }, + }, + { + name: "p4-host-in-netmap-user1-dest-bug", + args: args{ + nodes: []*types.Node{ + { + ID: 1, + IPv4: ap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, + }, + { + ID: 2, + IPv4: ap("100.64.0.3"), + Hostname: "user-2-1", + User: types.User{Name: "user2"}, + }, + { + ID: 3, + IPv4: ap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, + }, + }, + rules: []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: "100.64.0.2/32", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny}, + }, + }, + { + 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.3/32", Ports: tailcfg.PortRangeAny}, + {IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny}, + {IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny}, + }, + }, + }, + node: &types.Node{ + ID: 0, + IPv4: ap("100.64.0.1"), + Hostname: "user1-1", + User: types.User{Name: "user1"}, + }, + }, + want: []*types.Node{ + { + ID: 1, + IPv4: ap("100.64.0.2"), + Hostname: "user1-2", + User: types.User{Name: "user1"}, + }, + { + ID: 2, + IPv4: ap("100.64.0.3"), + Hostname: "user-2-1", + User: types.User{Name: "user2"}, + }, + { + ID: 3, + IPv4: ap("100.64.0.4"), + Hostname: "user2-2", + User: types.User{Name: "user2"}, + }, + }, + }, + + { + name: "subnet-router-with-only-route", + args: args{ + nodes: []*types.Node{ + { + ID: 1, + IPv4: ap("100.64.0.1"), + Hostname: "user1", + User: types.User{Name: "user1"}, + }, + { + ID: 2, + IPv4: ap("100.64.0.2"), + Hostname: "router", + User: types.User{Name: "router"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, + }, + ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, + }, + }, + rules: []tailcfg.FilterRule{ + { + SrcIPs: []string{ + "100.64.0.1/32", + }, + DstPorts: []tailcfg.NetPortRange{ + {IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny}, + }, + }, + }, + node: &types.Node{ + ID: 1, + IPv4: ap("100.64.0.1"), + Hostname: "user1", + User: types.User{Name: "user1"}, + }, + }, + want: []*types.Node{ + { + ID: 2, + IPv4: ap("100.64.0.2"), + Hostname: "router", + User: types.User{Name: "router"}, + Hostinfo: &tailcfg.Hostinfo{ + RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, + }, + ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FilterNodesByACL( + tt.args.node, + tt.args.nodes, + tt.args.rules, + ) + if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" { + t.Errorf("FilterNodesByACL() unexpected result (-want +got):\n%s", diff) + } + }) + } +} diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/v1/acls.go similarity index 88% rename from hscontrol/policy/acls.go rename to hscontrol/policy/v1/acls.go index eab7063b..945f171a 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/v1/acls.go @@ -1,11 +1,10 @@ -package policy +package v1 import ( "encoding/json" "errors" "fmt" "io" - "iter" "net/netip" "os" "slices" @@ -18,7 +17,6 @@ import ( "github.com/rs/zerolog/log" "github.com/tailscale/hujson" "go4.org/netipx" - "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" ) @@ -37,38 +35,6 @@ const ( expectedTokenItems = 2 ) -var theInternetSet *netipx.IPSet - -// theInternet returns the IPSet for the Internet. -// https://www.youtube.com/watch?v=iDbyYGrswtg -func theInternet() *netipx.IPSet { - if theInternetSet != nil { - return theInternetSet - } - - var internetBuilder netipx.IPSetBuilder - internetBuilder.AddPrefix(netip.MustParsePrefix("2000::/3")) - internetBuilder.AddPrefix(tsaddr.AllIPv4()) - - // Delete Private network addresses - // https://datatracker.ietf.org/doc/html/rfc1918 - internetBuilder.RemovePrefix(netip.MustParsePrefix("fc00::/7")) - internetBuilder.RemovePrefix(netip.MustParsePrefix("10.0.0.0/8")) - internetBuilder.RemovePrefix(netip.MustParsePrefix("172.16.0.0/12")) - internetBuilder.RemovePrefix(netip.MustParsePrefix("192.168.0.0/16")) - - // Delete Tailscale networks - internetBuilder.RemovePrefix(tsaddr.TailscaleULARange()) - internetBuilder.RemovePrefix(tsaddr.CGNATRange()) - - // Delete "can't find DHCP networks" - internetBuilder.RemovePrefix(netip.MustParsePrefix("fe80::/10")) // link-local - internetBuilder.RemovePrefix(netip.MustParsePrefix("169.254.0.0/16")) - - theInternetSet, _ := internetBuilder.IPSet() - return theInternetSet -} - // For some reason golang.org/x/net/internal/iana is an internal package. const ( protocolICMP = 1 // Internet Control Message @@ -240,53 +206,6 @@ func (pol *ACLPolicy) CompileFilterRules( return rules, nil } -// 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 { - // TODO(kradalby): Make this nil and not alloc unless needed - ret := []tailcfg.FilterRule{} - - for _, rule := range rules { - // record if the rule is actually relevant for the given node. - var dests []tailcfg.NetPortRange - DEST_LOOP: - for _, dest := range rule.DstPorts { - expanded, err := util.ParseIPSet(dest.IP, nil) - // Fail closed, if we can't parse it, then we should not allow - // access. - if err != nil { - continue DEST_LOOP - } - - if node.InIPSet(expanded) { - dests = append(dests, dest) - continue DEST_LOOP - } - - // If the node exposes routes, ensure they are note removed - // when the filters are reduced. - if len(node.SubnetRoutes()) > 0 { - for _, routableIP := range node.SubnetRoutes() { - if expanded.OverlapsPrefix(routableIP) { - dests = append(dests, dest) - continue DEST_LOOP - } - } - } - } - - if len(dests) > 0 { - ret = append(ret, tailcfg.FilterRule{ - SrcIPs: rule.SrcIPs, - DstPorts: dests, - IPProto: rule.IPProto, - }) - } - } - - return ret -} - func (pol *ACLPolicy) CompileSSHPolicy( node *types.Node, users []types.User, @@ -418,7 +337,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( if err != nil { return nil, fmt.Errorf("parsing SSH policy, expanding alias, index: %d->%d: %w", index, innerIndex, err) } - for addr := range ipSetAll(ips) { + for addr := range util.IPSetAddrIter(ips) { principals = append(principals, &tailcfg.SSHPrincipal{ NodeIP: addr.String(), }) @@ -441,19 +360,6 @@ func (pol *ACLPolicy) CompileSSHPolicy( }, nil } -// ipSetAll returns a function that iterates over all the IPs in the IPSet. -func ipSetAll(ipSet *netipx.IPSet) iter.Seq[netip.Addr] { - return func(yield func(netip.Addr) bool) { - for _, rng := range ipSet.Ranges() { - for ip := rng.From(); ip.Compare(rng.To()) <= 0; ip = ip.Next() { - if !yield(ip) { - return - } - } - } - } -} - func sshCheckAction(duration string) (*tailcfg.SSHAction, error) { sessionLength, err := time.ParseDuration(duration) if err != nil { @@ -950,7 +856,7 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix( func expandAutoGroup(alias string) (*netipx.IPSet, error) { switch { case strings.HasPrefix(alias, "autogroup:internet"): - return theInternet(), nil + return util.TheInternet(), nil default: return nil, fmt.Errorf("unknown autogroup %q", alias) @@ -1084,24 +990,3 @@ func findUserFromToken(users []types.User, token string) (types.User, error) { return potentialUsers[0], nil } - -// FilterNodesByACL returns the list of peers authorized to be accessed from a given node. -func FilterNodesByACL( - node *types.Node, - nodes types.Nodes, - filter []tailcfg.FilterRule, -) types.Nodes { - var result types.Nodes - - for index, peer := range nodes { - if peer.ID == node.ID { - continue - } - - if node.CanAccess(filter, nodes[index]) || peer.CanAccess(filter, node) { - result = append(result, peer) - } - } - - return result -} diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/v1/acls_test.go similarity index 66% rename from hscontrol/policy/acls_test.go rename to hscontrol/policy/v1/acls_test.go index a7b12b1d..4c8ab306 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/v1/acls_test.go @@ -1,4 +1,4 @@ -package policy +package v1 import ( "database/sql" @@ -17,7 +17,6 @@ import ( "go4.org/netipx" "gopkg.in/check.v1" "gorm.io/gorm" - "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" ) @@ -2020,731 +2019,6 @@ var tsExitNodeDest = []tailcfg.NetPortRange{ }, } -// hsExitNodeDest is the list of destination IP ranges that are allowed when -// we use headscale "autogroup:internet". -var hsExitNodeDest = []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 := theInternet() - - internetPrefs := internetSet.Prefixes() - - for i := range internetPrefs { - if internetPrefs[i].String() != hsExitNodeDest[i].IP { - t.Errorf( - "prefix from internet set %q != hsExit list %q", - internetPrefs[i].String(), - hsExitNodeDest[i].IP, - ) - } - } - - if len(internetPrefs) != len(hsExitNodeDest) { - t.Fatalf( - "expected same length of prefixes, internet: %d, hsExit: %d", - len(internetPrefs), - len(hsExitNodeDest), - ) - } -} - -func TestReduceFilterRules(t *testing.T) { - users := []types.User{ - {Model: gorm.Model{ID: 1}, Name: "mickael"}, - {Model: gorm.Model{ID: 2}, Name: "user1"}, - {Model: gorm.Model{ID: 3}, Name: "user2"}, - {Model: gorm.Model{ID: 4}, Name: "user100"}, - } - - tests := []struct { - name string - node *types.Node - peers types.Nodes - pol ACLPolicy - want []tailcfg.FilterRule - }{ - { - name: "host1-can-reach-host2-no-rules", - pol: ACLPolicy{ - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"100.64.0.1"}, - Destinations: []string{"100.64.0.2:*"}, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), - User: users[0], - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), - User: users[0], - }, - }, - want: []tailcfg.FilterRule{}, - }, - { - name: "1604-subnet-routers-are-preserved", - pol: ACLPolicy{ - Groups: Groups{ - "group:admins": {"user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:admins"}, - Destinations: []string{"group:admins:*"}, - }, - { - Action: "accept", - Sources: []string{"group:admins"}, - Destinations: []string{"10.33.0.0/16:*"}, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("fd7a:115c:a1e0::1"), - User: users[1], - Hostinfo: &tailcfg.Hostinfo{ - RoutableIPs: []netip.Prefix{ - netip.MustParsePrefix("10.33.0.0/16"), - }, - }, - ApprovedRoutes: []netip.Prefix{ - netip.MustParsePrefix("10.33.0.0/16"), - }, - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("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, - }, - }, - }, - { - 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, - }, - }, - }, - }, - }, - { - name: "1786-reducing-breaks-exit-nodes-the-client", - pol: ACLPolicy{ - Hosts: Hosts{ - // Exit node - "internal": netip.MustParsePrefix("100.64.0.100/32"), - }, - Groups: Groups{ - "group:team": {"user3", "user2", "user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "internal:*", - }, - }, - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "autogroup:internet:*", - }, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("fd7a:115c:a1e0::1"), - User: users[1], - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0::2"), - User: users[2], - }, - // "internal" exit node - &types.Node{ - IPv4: iap("100.64.0.100"), - IPv6: iap("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: ACLPolicy{ - Hosts: Hosts{ - // Exit node - "internal": netip.MustParsePrefix("100.64.0.100/32"), - }, - Groups: Groups{ - "group:team": {"user3", "user2", "user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "internal:*", - }, - }, - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "autogroup:internet:*", - }, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.100"), - IPv6: iap("fd7a:115c:a1e0::100"), - User: types.User{Name: "user100"}, - Hostinfo: &tailcfg.Hostinfo{ - RoutableIPs: tsaddr.ExitRoutes(), - }, - ApprovedRoutes: tsaddr.ExitRoutes(), - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0::2"), - User: users[2], - }, - &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("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, - }, - }, - }, - { - SrcIPs: []string{ - "100.64.0.1/32", - "100.64.0.2/32", - "fd7a:115c:a1e0::1/128", - "fd7a:115c:a1e0::2/128", - }, - DstPorts: hsExitNodeDest, - }, - }, - }, - { - name: "1786-reducing-breaks-exit-nodes-the-example-from-issue", - pol: ACLPolicy{ - Hosts: Hosts{ - // Exit node - "internal": netip.MustParsePrefix("100.64.0.100/32"), - }, - Groups: Groups{ - "group:team": {"user3", "user2", "user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "internal:*", - }, - }, - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "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: iap("100.64.0.100"), - IPv6: iap("fd7a:115c:a1e0::100"), - User: users[3], - Hostinfo: &tailcfg.Hostinfo{ - RoutableIPs: tsaddr.ExitRoutes(), - }, - ApprovedRoutes: tsaddr.ExitRoutes(), - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0::2"), - User: users[2], - }, - &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("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, - }, - }, - }, - { - 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: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::100/128", 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}, - }, - }, - }, - }, - { - name: "1786-reducing-breaks-exit-nodes-app-connector-like", - pol: ACLPolicy{ - Hosts: Hosts{ - // Exit node - "internal": netip.MustParsePrefix("100.64.0.100/32"), - }, - Groups: Groups{ - "group:team": {"user3", "user2", "user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "internal:*", - }, - }, - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "8.0.0.0/8:*", - "16.0.0.0/8:*", - }, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.100"), - IPv6: iap("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"), - }, - }, - ApprovedRoutes: []netip.Prefix{ - netip.MustParsePrefix("8.0.0.0/16"), - netip.MustParsePrefix("16.0.0.0/16"), - }, - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0::2"), - User: users[2], - }, - &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("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, - }, - }, - }, - { - 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, - }, - }, - }, - }, - }, - { - name: "1786-reducing-breaks-exit-nodes-app-connector-like2", - pol: ACLPolicy{ - Hosts: Hosts{ - // Exit node - "internal": netip.MustParsePrefix("100.64.0.100/32"), - }, - Groups: Groups{ - "group:team": {"user3", "user2", "user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "internal:*", - }, - }, - { - Action: "accept", - Sources: []string{"group:team"}, - Destinations: []string{ - "8.0.0.0/16:*", - "16.0.0.0/16:*", - }, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.100"), - IPv6: iap("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"), - }, - }, - ApprovedRoutes: []netip.Prefix{ - netip.MustParsePrefix("8.0.0.0/8"), - netip.MustParsePrefix("16.0.0.0/8"), - }, - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0::2"), - User: users[2], - }, - &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("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, - }, - }, - }, - { - 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, - }, - }, - }, - }, - }, - { - name: "1817-reduce-breaks-32-mask", - pol: ACLPolicy{ - Hosts: Hosts{ - "vlan1": netip.MustParsePrefix("172.16.0.0/24"), - "dns1": netip.MustParsePrefix("172.16.0.21/32"), - }, - Groups: Groups{ - "group:access": {"user1"}, - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"group:access"}, - Destinations: []string{ - "tag:access-servers:*", - "dns1:*", - }, - }, - }, - }, - node: &types.Node{ - IPv4: iap("100.64.0.100"), - IPv6: iap("fd7a:115c:a1e0::100"), - User: users[3], - Hostinfo: &tailcfg.Hostinfo{ - RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")}, - }, - ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")}, - ForcedTags: []string{"tag:access-servers"}, - }, - peers: types.Nodes{ - &types.Node{ - IPv4: iap("100.64.0.1"), - IPv6: iap("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, - }, - }, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, _ := tt.pol.CompileFilterRules( - users, - append(tt.peers, tt.node), - ) - - got = ReduceFilterRules(tt.node, 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) - } - }) - } -} - func Test_getTags(t *testing.T) { users := []types.User{ { @@ -2885,662 +2159,6 @@ func Test_getTags(t *testing.T) { } } -func Test_getFilteredByACLPeers(t *testing.T) { - type args struct { - nodes types.Nodes - rules []tailcfg.FilterRule - node *types.Node - } - tests := []struct { - name string - args args - want types.Nodes - }{ - { - name: "all hosts can talk to each other", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "*"}, - }, - }, - }, - node: &types.Node{ // current nodes - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - }, - { - name: "One host can talk to another, but not all hosts", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.64.0.2"}, - }, - }, - }, - node: &types.Node{ // current nodes - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - }, - }, - { - name: "host cannot directly talk to destination, but return path is authorized", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - SrcIPs: []string{"100.64.0.3"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.64.0.2"}, - }, - }, - }, - node: &types.Node{ // current nodes - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - }, - { - name: "rules allows all hosts to reach one destination", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - SrcIPs: []string{"*"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.64.0.2"}, - }, - }, - }, - node: &types.Node{ // current nodes - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - }, - }, - { - name: "rules allows all hosts to reach one destination, destination can reach all hosts", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - SrcIPs: []string{"*"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.64.0.2"}, - }, - }, - }, - node: &types.Node{ // current nodes - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - }, - { - name: "rule allows all hosts to reach all destinations", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - SrcIPs: []string{"*"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "*"}, - }, - }, - }, - node: &types.Node{ // current nodes - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - }, - { - name: "without rule all communications are forbidden", - args: args{ - nodes: types.Nodes{ // list of all nodes in the database - &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - User: types.User{Name: "joe"}, - }, - &types.Node{ - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - &types.Node{ - ID: 3, - IPv4: iap("100.64.0.3"), - User: types.User{Name: "mickael"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - }, - node: &types.Node{ // current nodes - ID: 2, - IPv4: iap("100.64.0.2"), - User: types.User{Name: "marc"}, - }, - }, - want: nil, - }, - { - // Investigating 699 - // Found some nodes: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] nodes=ts-head-8w6paa - // ACL rules generated ACL=[{"DstPorts":[{"Bits":null,"IP":"*","Ports":{"First":0,"Last":65535}}],"SrcIPs":["fd7a:115c:a1e0::3","100.64.0.3","fd7a:115c:a1e0::4","100.64.0.4"]}] - // ACL Cache Map={"100.64.0.3":{"*":{}},"100.64.0.4":{"*":{}},"fd7a:115c:a1e0::3":{"*":{}},"fd7a:115c:a1e0::4":{"*":{}}} - name: "issue-699-broken-star", - args: args{ - nodes: types.Nodes{ // - &types.Node{ - ID: 1, - Hostname: "ts-head-upcrmb", - IPv4: iap("100.64.0.3"), - IPv6: iap("fd7a:115c:a1e0::3"), - User: types.User{Name: "user1"}, - }, - &types.Node{ - ID: 2, - Hostname: "ts-unstable-rlwpvr", - IPv4: iap("100.64.0.4"), - IPv6: iap("fd7a:115c:a1e0::4"), - User: types.User{Name: "user1"}, - }, - &types.Node{ - ID: 3, - Hostname: "ts-head-8w6paa", - IPv4: iap("100.64.0.1"), - IPv6: iap("fd7a:115c:a1e0::1"), - User: types.User{Name: "user2"}, - }, - &types.Node{ - ID: 4, - Hostname: "ts-unstable-lys2ib", - IPv4: iap("100.64.0.2"), - IPv6: iap("fd7a:115c:a1e0::2"), - User: types.User{Name: "user2"}, - }, - }, - rules: []tailcfg.FilterRule{ // list of all ACLRules registered - { - DstPorts: []tailcfg.NetPortRange{ - { - IP: "*", - Ports: tailcfg.PortRange{First: 0, Last: 65535}, - }, - }, - SrcIPs: []string{ - "fd7a:115c:a1e0::3", "100.64.0.3", - "fd7a:115c:a1e0::4", "100.64.0.4", - }, - }, - }, - node: &types.Node{ // current nodes - ID: 3, - Hostname: "ts-head-8w6paa", - IPv4: iap("100.64.0.1"), - IPv6: iap("fd7a:115c:a1e0::1"), - User: types.User{Name: "user2"}, - }, - }, - want: types.Nodes{ - &types.Node{ - ID: 1, - Hostname: "ts-head-upcrmb", - IPv4: iap("100.64.0.3"), - IPv6: iap("fd7a:115c:a1e0::3"), - User: types.User{Name: "user1"}, - }, - &types.Node{ - ID: 2, - Hostname: "ts-unstable-rlwpvr", - IPv4: iap("100.64.0.4"), - IPv6: iap("fd7a:115c:a1e0::4"), - User: types.User{Name: "user1"}, - }, - }, - }, - { - name: "failing-edge-case-during-p3-refactor", - args: args{ - nodes: []*types.Node{ - { - ID: 1, - IPv4: iap("100.64.0.2"), - Hostname: "peer1", - User: types.User{Name: "mini"}, - }, - { - ID: 2, - IPv4: iap("100.64.0.3"), - Hostname: "peer2", - User: types.User{Name: "peer2"}, - }, - }, - rules: []tailcfg.FilterRule{ - { - SrcIPs: []string{"100.64.0.1/32"}, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny}, - {IP: "::/0", Ports: tailcfg.PortRangeAny}, - }, - }, - }, - node: &types.Node{ - ID: 0, - IPv4: iap("100.64.0.1"), - Hostname: "mini", - User: types.User{Name: "mini"}, - }, - }, - want: []*types.Node{ - { - ID: 2, - IPv4: iap("100.64.0.3"), - Hostname: "peer2", - User: types.User{Name: "peer2"}, - }, - }, - }, - { - name: "p4-host-in-netmap-user2-dest-bug", - args: args{ - nodes: []*types.Node{ - { - ID: 1, - IPv4: iap("100.64.0.2"), - Hostname: "user1-2", - User: types.User{Name: "user1"}, - }, - { - ID: 0, - IPv4: iap("100.64.0.1"), - Hostname: "user1-1", - User: types.User{Name: "user1"}, - }, - { - ID: 3, - IPv4: iap("100.64.0.4"), - Hostname: "user2-2", - User: types.User{Name: "user2"}, - }, - }, - rules: []tailcfg.FilterRule{ - { - SrcIPs: []string{ - "100.64.0.3/32", - "100.64.0.4/32", - "fd7a:115c:a1e0::3/128", - "fd7a:115c:a1e0::4/128", - }, - DstPorts: []tailcfg.NetPortRange{ - {IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny}, - {IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny}, - }, - }, - { - 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.3/32", Ports: tailcfg.PortRangeAny}, - {IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny}, - }, - }, - }, - node: &types.Node{ - ID: 2, - IPv4: iap("100.64.0.3"), - Hostname: "user-2-1", - User: types.User{Name: "user2"}, - }, - }, - want: []*types.Node{ - { - ID: 1, - IPv4: iap("100.64.0.2"), - Hostname: "user1-2", - User: types.User{Name: "user1"}, - }, - { - ID: 0, - IPv4: iap("100.64.0.1"), - Hostname: "user1-1", - User: types.User{Name: "user1"}, - }, - { - ID: 3, - IPv4: iap("100.64.0.4"), - Hostname: "user2-2", - User: types.User{Name: "user2"}, - }, - }, - }, - { - name: "p4-host-in-netmap-user1-dest-bug", - args: args{ - nodes: []*types.Node{ - { - ID: 1, - IPv4: iap("100.64.0.2"), - Hostname: "user1-2", - User: types.User{Name: "user1"}, - }, - { - ID: 2, - IPv4: iap("100.64.0.3"), - Hostname: "user-2-1", - User: types.User{Name: "user2"}, - }, - { - ID: 3, - IPv4: iap("100.64.0.4"), - Hostname: "user2-2", - User: types.User{Name: "user2"}, - }, - }, - rules: []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: "100.64.0.2/32", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny}, - }, - }, - { - 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.3/32", Ports: tailcfg.PortRangeAny}, - {IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny}, - {IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny}, - }, - }, - }, - node: &types.Node{ - ID: 0, - IPv4: iap("100.64.0.1"), - Hostname: "user1-1", - User: types.User{Name: "user1"}, - }, - }, - want: []*types.Node{ - { - ID: 1, - IPv4: iap("100.64.0.2"), - Hostname: "user1-2", - User: types.User{Name: "user1"}, - }, - { - ID: 2, - IPv4: iap("100.64.0.3"), - Hostname: "user-2-1", - User: types.User{Name: "user2"}, - }, - { - ID: 3, - IPv4: iap("100.64.0.4"), - Hostname: "user2-2", - User: types.User{Name: "user2"}, - }, - }, - }, - - { - name: "subnet-router-with-only-route", - args: args{ - nodes: []*types.Node{ - { - ID: 1, - IPv4: iap("100.64.0.1"), - Hostname: "user1", - User: types.User{Name: "user1"}, - }, - { - ID: 2, - IPv4: iap("100.64.0.2"), - Hostname: "router", - User: types.User{Name: "router"}, - Hostinfo: &tailcfg.Hostinfo{ - RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, - }, - ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, - }, - }, - rules: []tailcfg.FilterRule{ - { - SrcIPs: []string{ - "100.64.0.1/32", - }, - DstPorts: []tailcfg.NetPortRange{ - {IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny}, - }, - }, - }, - node: &types.Node{ - ID: 1, - IPv4: iap("100.64.0.1"), - Hostname: "user1", - User: types.User{Name: "user1"}, - }, - }, - want: []*types.Node{ - { - ID: 2, - IPv4: iap("100.64.0.2"), - Hostname: "router", - User: types.User{Name: "router"}, - Hostinfo: &tailcfg.Hostinfo{ - RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, - }, - ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := FilterNodesByACL( - tt.args.node, - tt.args.nodes, - tt.args.rules, - ) - if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" { - t.Errorf("FilterNodesByACL() unexpected result (-want +got):\n%s", diff) - } - }) - } -} - func TestSSHRules(t *testing.T) { users := []types.User{ { diff --git a/hscontrol/policy/acls_types.go b/hscontrol/policy/v1/acls_types.go similarity index 99% rename from hscontrol/policy/acls_types.go rename to hscontrol/policy/v1/acls_types.go index 5b5d1838..8c4584c7 100644 --- a/hscontrol/policy/acls_types.go +++ b/hscontrol/policy/v1/acls_types.go @@ -1,4 +1,4 @@ -package policy +package v1 import ( "encoding/json" diff --git a/hscontrol/policy/v1/policy.go b/hscontrol/policy/v1/policy.go new file mode 100644 index 00000000..6341bc6c --- /dev/null +++ b/hscontrol/policy/v1/policy.go @@ -0,0 +1,187 @@ +package v1 + +import ( + "fmt" + "io" + "net/netip" + "os" + "sync" + + "github.com/juanfont/headscale/hscontrol/types" + "github.com/rs/zerolog/log" + "tailscale.com/tailcfg" + "tailscale.com/util/deephash" +) + +func NewPolicyManagerFromPath(path string, users []types.User, nodes types.Nodes) (*PolicyManager, error) { + policyFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer policyFile.Close() + + policyBytes, err := io.ReadAll(policyFile) + if err != nil { + return nil, err + } + + return NewPolicyManager(policyBytes, users, nodes) +} + +func NewPolicyManager(polB []byte, users []types.User, nodes types.Nodes) (*PolicyManager, error) { + var pol *ACLPolicy + var err error + if polB != nil && len(polB) > 0 { + pol, err = LoadACLPolicyFromBytes(polB) + if err != nil { + return nil, fmt.Errorf("parsing policy: %w", err) + } + } + + pm := PolicyManager{ + pol: pol, + users: users, + nodes: nodes, + } + + _, err = pm.updateLocked() + if err != nil { + return nil, err + } + + return &pm, nil +} + +type PolicyManager struct { + mu sync.Mutex + pol *ACLPolicy + + users []types.User + nodes types.Nodes + + filterHash deephash.Sum + filter []tailcfg.FilterRule +} + +// updateLocked updates the filter rules based on the current policy and nodes. +// It must be called with the lock held. +func (pm *PolicyManager) updateLocked() (bool, error) { + filter, err := pm.pol.CompileFilterRules(pm.users, pm.nodes) + if err != nil { + return false, fmt.Errorf("compiling filter rules: %w", err) + } + + filterHash := deephash.Hash(&filter) + if filterHash == pm.filterHash { + return false, nil + } + + pm.filter = filter + pm.filterHash = filterHash + + return true, nil +} + +func (pm *PolicyManager) Filter() []tailcfg.FilterRule { + pm.mu.Lock() + defer pm.mu.Unlock() + return pm.filter +} + +func (pm *PolicyManager) SSHPolicy(node *types.Node) (*tailcfg.SSHPolicy, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + + return pm.pol.CompileSSHPolicy(node, pm.users, pm.nodes) +} + +func (pm *PolicyManager) SetPolicy(polB []byte) (bool, error) { + if len(polB) == 0 { + return false, nil + } + + pol, err := LoadACLPolicyFromBytes(polB) + if err != nil { + return false, fmt.Errorf("parsing policy: %w", err) + } + + pm.mu.Lock() + defer pm.mu.Unlock() + + pm.pol = pol + + return pm.updateLocked() +} + +// SetUsers updates the users in the policy manager and updates the filter rules. +func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + + pm.users = users + return pm.updateLocked() +} + +// SetNodes updates the nodes in the policy manager and updates the filter rules. +func (pm *PolicyManager) SetNodes(nodes types.Nodes) (bool, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + pm.nodes = nodes + return pm.updateLocked() +} + +func (pm *PolicyManager) NodeCanHaveTag(node *types.Node, tag string) bool { + if pm == nil || pm.pol == nil { + return false + } + + pm.mu.Lock() + defer pm.mu.Unlock() + + tags, invalid := pm.pol.TagsOfNode(pm.users, node) + log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy") + + for _, t := range tags { + if t == tag { + return true + } + } + + return false +} + +func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefix) bool { + if pm == nil || pm.pol == nil { + return false + } + + pm.mu.Lock() + defer pm.mu.Unlock() + + approvers, _ := pm.pol.AutoApprovers.GetRouteApprovers(route) + + for _, approvedAlias := range approvers { + if approvedAlias == node.User.Username() { + return true + } else { + ips, err := pm.pol.ExpandAlias(pm.nodes, pm.users, approvedAlias) + if err != nil { + return false + } + + // approvedIPs should contain all of node's IPs if it matches the rule, so check for first + if ips.Contains(*node.IPv4) { + return true + } + } + } + return false +} + +func (pm *PolicyManager) Version() int { + return 1 +} + +func (pm *PolicyManager) DebugString() string { + return "not implemented for v1" +} diff --git a/hscontrol/policy/pm_test.go b/hscontrol/policy/v1/policy_test.go similarity index 99% rename from hscontrol/policy/pm_test.go rename to hscontrol/policy/v1/policy_test.go index 24b78e4d..e250db2a 100644 --- a/hscontrol/policy/pm_test.go +++ b/hscontrol/policy/v1/policy_test.go @@ -1,4 +1,4 @@ -package policy +package v1 import ( "testing" diff --git a/hscontrol/poll.go b/hscontrol/poll.go index 7d9e1ab4..6c11bb04 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -10,10 +10,9 @@ import ( "time" "github.com/juanfont/headscale/hscontrol/mapper" + "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" - "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" - "github.com/samber/lo" "github.com/sasha-s/go-deadlock" xslices "golang.org/x/exp/slices" "tailscale.com/net/tsaddr" @@ -459,25 +458,10 @@ func (m *mapSession) handleEndpointUpdate() { // TODO(kradalby): I am not sure if we need this? nodesChangedHook(m.h.db, m.h.polMan, m.h.nodeNotifier) - // Take all the routes presented to us by the node and check - // if any of them should be auto approved by the policy. - // If any of them are, add them to the approved routes of the node. - // Keep all the old entries and compact the list to remove duplicates. - var newApproved []netip.Prefix - for _, route := range m.node.Hostinfo.RoutableIPs { - if m.h.polMan.NodeCanApproveRoute(m.node, route) { - newApproved = append(newApproved, route) - } - } - if newApproved != nil { - newApproved = append(newApproved, m.node.ApprovedRoutes...) - slices.SortFunc(newApproved, util.ComparePrefix) - slices.Compact(newApproved) - newApproved = lo.Filter(newApproved, func(route netip.Prefix, index int) bool { - return route.IsValid() - }) - m.node.ApprovedRoutes = newApproved - + // Approve routes if they are auto-approved by the policy. + // If any of them are approved, report them to the primary route tracker + // and send updates accordingly. + if policy.AutoApproveRoutes(m.h.polMan, m.node) { if m.h.primaryRoutes.SetRoutes(m.node.ID, m.node.SubnetRoutes()...) { ctx := types.NotifyCtx(m.ctx, "poll-primary-change", m.node.Hostname) m.h.nodeNotifier.NotifyAll(ctx, types.UpdateFull()) diff --git a/hscontrol/util/net.go b/hscontrol/util/net.go index 665ce1dd..5a355073 100644 --- a/hscontrol/util/net.go +++ b/hscontrol/util/net.go @@ -1,10 +1,13 @@ package util import ( - "cmp" "context" "net" "net/netip" + "sync" + + "go4.org/netipx" + "tailscale.com/net/tsaddr" ) func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) { @@ -49,3 +52,29 @@ func MustStringsToPrefixes(strings []string) []netip.Prefix { return ret } + +// TheInternet returns the IPSet for the Internet. +// https://www.youtube.com/watch?v=iDbyYGrswtg +var TheInternet = sync.OnceValue(func() *netipx.IPSet { + var internetBuilder netipx.IPSetBuilder + internetBuilder.AddPrefix(netip.MustParsePrefix("2000::/3")) + internetBuilder.AddPrefix(tsaddr.AllIPv4()) + + // Delete Private network addresses + // https://datatracker.ietf.org/doc/html/rfc1918 + internetBuilder.RemovePrefix(netip.MustParsePrefix("fc00::/7")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("10.0.0.0/8")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("172.16.0.0/12")) + internetBuilder.RemovePrefix(netip.MustParsePrefix("192.168.0.0/16")) + + // Delete Tailscale networks + internetBuilder.RemovePrefix(tsaddr.TailscaleULARange()) + internetBuilder.RemovePrefix(tsaddr.CGNATRange()) + + // Delete "can't find DHCP networks" + internetBuilder.RemovePrefix(netip.MustParsePrefix("fe80::/10")) // link-local + internetBuilder.RemovePrefix(netip.MustParsePrefix("169.254.0.0/16")) + + theInternetSet, _ := internetBuilder.IPSet() + return theInternetSet +})