From 004ebcaba1dea344d3407ac77713322512ab0471 Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Wed, 24 Aug 2022 20:53:55 +1000 Subject: [PATCH 1/7] initial implementation of autoApprovers support --- acls_types.go | 18 +++++++--- machine.go | 75 +++++++++++++++++++++++++++++++++++++++++ protocol_common_poll.go | 7 ++++ 3 files changed, 95 insertions(+), 5 deletions(-) diff --git a/acls_types.go b/acls_types.go index 0f73d6fd..b9a5d4da 100644 --- a/acls_types.go +++ b/acls_types.go @@ -11,11 +11,12 @@ import ( // ACLPolicy represents a Tailscale ACL Policy. type ACLPolicy struct { - Groups Groups `json:"groups" yaml:"groups"` - Hosts Hosts `json:"hosts" yaml:"hosts"` - TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"` - ACLs []ACL `json:"acls" yaml:"acls"` - Tests []ACLTest `json:"tests" yaml:"tests"` + Groups Groups `json:"groups" yaml:"groups"` + Hosts Hosts `json:"hosts" yaml:"hosts"` + TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"` + ACLs []ACL `json:"acls" yaml:"acls"` + Tests []ACLTest `json:"tests" yaml:"tests"` + AutoApprovers AutoApprovers `json:"autoApprovers" yaml:"autoApprovers"` } // ACL is a basic rule for the ACL Policy. @@ -42,6 +43,13 @@ type ACLTest struct { Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"` } +// AutoApprovers specify which users (namespaces?), groups or tags have their advertised routes +// or exit node status automatically enabled +type AutoApprovers struct { + Routes map[string][]string `json:"routes" yaml:"routes"` + ExitNode []string `json:"exitNode" yaml:"exitNode"` +} + // UnmarshalJSON allows to parse the Hosts directly into netaddr objects. func (hosts *Hosts) UnmarshalJSON(data []byte) error { newHosts := Hosts{} diff --git a/machine.go b/machine.go index 4399029c..928f78e2 100644 --- a/machine.go +++ b/machine.go @@ -930,6 +930,81 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error { return nil } +// Enabled any routes advertised by a machine that match the ACL autoApprovers policy +// TODO simplify by expanding only for current machine, and by checking if approvedIPs contains machine.IPs[0] +func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { + approvedRoutes := make([]netaddr.IPPrefix, 0, len(machine.HostInfo.RoutableIPs)) + machines, err := h.ListMachines() + + if err != nil { + log.Err(err) + return err + } + + for _, advertisedRoute := range machine.HostInfo.RoutableIPs { + log.Debug(). + Uint64("machine", machine.ID). + Str("advertisedRoute", advertisedRoute.String()). + Msg("Client requested to advertise route") + + approved := false + routeApprovers := h.aclPolicy.AutoApprovers.Routes[advertisedRoute.String()] + + if advertisedRoute.Bits() == 0 { + routeApprovers = h.aclPolicy.AutoApprovers.ExitNode + } + + if len(routeApprovers) > 0 { + for _, approvedAlias := range routeApprovers { + + approvedIps, err := expandAlias(machines, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain) + + if err != nil { + log.Err(err). + Str("alias", approvedAlias). + Msg("Failed to expand alias when processing autoApprovers policy") + return err + } + + for _, machineIp := range machine.IPAddresses { + for _, approvedIp := range approvedIps { + approved = machineIp.String() == approvedIp + + if approved { + break + } + } + + if approved { + break + } + } + } + } else { + log.Debug(). + Uint64("client", machine.ID). + Str("advertisedRoute", advertisedRoute.String()). + Msg("Advertised route is not automatically approved") + } + + if approved { + approvedRoutes = append(approvedRoutes, advertisedRoute) + } + } + + for _, approvedRoute := range approvedRoutes { + if !contains(machine.EnabledRoutes, approvedRoute) { + log.Info(). + Str("route", approvedRoute.String()). + Uint64("client", machine.ID). + Msg("Enabling autoApproved route for client") + machine.EnabledRoutes = append(machine.EnabledRoutes, approvedRoute) + } + } + + return nil +} + func (machine *Machine) RoutesToProto() *v1.Routes { availableRoutes := machine.GetAdvertisedRoutes() diff --git a/protocol_common_poll.go b/protocol_common_poll.go index 65dcb556..d6a8fff2 100644 --- a/protocol_common_poll.go +++ b/protocol_common_poll.go @@ -42,7 +42,14 @@ func (h *Headscale) handlePollCommon( Str("machine", machine.Hostname). Err(err) } + + // update routes with peer information + err = h.EnableAutoApprovedRoutes(machine) + if err != nil { + //TODO + } } + // From Tailscale client: // // ReadOnly is whether the client just wants to fetch the MapResponse, From 7653ad40d6900f06ec6817440c05132912f92ae7 Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Wed, 24 Aug 2022 21:30:04 +1000 Subject: [PATCH 2/7] Split GetRouteApprovers from EnableAutoApprovedRoutes --- acls_types.go | 25 +++++++++++++++++ machine.go | 78 ++++++++++++++++++++++----------------------------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/acls_types.go b/acls_types.go index b9a5d4da..94461449 100644 --- a/acls_types.go +++ b/acls_types.go @@ -108,3 +108,28 @@ func (policy ACLPolicy) IsZero() bool { return false } + +// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix +func (autoApprovers *AutoApprovers) GetRouteApprovers( + prefix netaddr.IPPrefix, +) ([]string, error) { + if prefix.Bits() == 0 { + return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent + } + + approverAliases := []string{} + + for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes { + autoApprovedPrefix, err := netaddr.ParseIPPrefix(autoApprovedPrefix) + if err != nil { + return nil, err + } + + if autoApprovedPrefix.Bits() >= prefix.Bits() && + autoApprovedPrefix.Contains(prefix.IP()) { + approverAliases = append(approverAliases, autoApproverAliases...) + } + } + + return approverAliases, nil +} diff --git a/machine.go b/machine.go index 928f78e2..43ec4720 100644 --- a/machine.go +++ b/machine.go @@ -931,60 +931,48 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error { } // Enabled any routes advertised by a machine that match the ACL autoApprovers policy -// TODO simplify by expanding only for current machine, and by checking if approvedIPs contains machine.IPs[0] func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { - approvedRoutes := make([]netaddr.IPPrefix, 0, len(machine.HostInfo.RoutableIPs)) - machines, err := h.ListMachines() - - if err != nil { - log.Err(err) - return err + if len(machine.IPAddresses) == 0 { + return nil // This machine has no IPAddresses, so can't possibly match any autoApprovers ACLs } + approvedRoutes := make([]netaddr.IPPrefix, 0, len(machine.HostInfo.RoutableIPs)) + thisMachine := []Machine{*machine} + for _, advertisedRoute := range machine.HostInfo.RoutableIPs { - log.Debug(). - Uint64("machine", machine.ID). - Str("advertisedRoute", advertisedRoute.String()). - Msg("Client requested to advertise route") - approved := false - routeApprovers := h.aclPolicy.AutoApprovers.Routes[advertisedRoute.String()] - - if advertisedRoute.Bits() == 0 { - routeApprovers = h.aclPolicy.AutoApprovers.ExitNode + if contains(machine.EnabledRoutes, advertisedRoute) { + continue // Skip routes that are already enabled for the node } - if len(routeApprovers) > 0 { - for _, approvedAlias := range routeApprovers { + approved := false + routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(advertisedRoute) - approvedIps, err := expandAlias(machines, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain) - - if err != nil { - log.Err(err). - Str("alias", approvedAlias). - Msg("Failed to expand alias when processing autoApprovers policy") - return err - } - - for _, machineIp := range machine.IPAddresses { - for _, approvedIp := range approvedIps { - approved = machineIp.String() == approvedIp - - if approved { - break - } - } - - if approved { - break - } - } - } - } else { - log.Debug(). - Uint64("client", machine.ID). + if err != nil { + log.Err(err). Str("advertisedRoute", advertisedRoute.String()). - Msg("Advertised route is not automatically approved") + Uint64("machineId", machine.ID). + Msg("Failed to resolve autoApprovers for advertised route") + return err + } + + for _, approvedAlias := range routeApprovers { + + approvedIps, err := expandAlias(thisMachine, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain) + + if err != nil { + log.Err(err). + Str("alias", approvedAlias). + Msg("Failed to expand alias when processing autoApprovers policy") + return err + } + + // approvedIPs should contain all of machine's IPs if it matches the rule, so check for first + approved = contains(approvedIps, machine.IPAddresses[0].String()) + + if approved { + break + } } if approved { From 60cc9ddb3b7951290d74e91dad365c8609227b1a Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Wed, 24 Aug 2022 22:09:06 +1000 Subject: [PATCH 3/7] Add test for autoApprovers feature --- machine.go | 38 +++++++++----------- machine_test.go | 41 ++++++++++++++++++++++ protocol_common_poll.go | 5 +-- tests/acls/acl_policy_autoapprovers.hujson | 24 +++++++++++++ 4 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 tests/acls/acl_policy_autoapprovers.hujson diff --git a/machine.go b/machine.go index 43ec4720..099f7fac 100644 --- a/machine.go +++ b/machine.go @@ -945,9 +945,9 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { continue // Skip routes that are already enabled for the node } - approved := false - routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers(advertisedRoute) - + routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers( + advertisedRoute, + ) if err != nil { log.Err(err). Str("advertisedRoute", advertisedRoute.String()). @@ -957,26 +957,22 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { } for _, approvedAlias := range routeApprovers { + if approvedAlias == machine.Namespace.Name { + approvedRoutes = append(approvedRoutes, advertisedRoute) + } else { + approvedIps, err := expandAlias(thisMachine, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain) + if err != nil { + log.Err(err). + Str("alias", approvedAlias). + Msg("Failed to expand alias when processing autoApprovers policy") + return err + } - approvedIps, err := expandAlias(thisMachine, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain) - - if err != nil { - log.Err(err). - Str("alias", approvedAlias). - Msg("Failed to expand alias when processing autoApprovers policy") - return err + // approvedIPs should contain all of machine's IPs if it matches the rule, so check for first + if contains(approvedIps, machine.IPAddresses[0].String()) { + approvedRoutes = append(approvedRoutes, advertisedRoute) + } } - - // approvedIPs should contain all of machine's IPs if it matches the rule, so check for first - approved = contains(approvedIps, machine.IPAddresses[0].String()) - - if approved { - break - } - } - - if approved { - approvedRoutes = append(approvedRoutes, advertisedRoute) } } diff --git a/machine_test.go b/machine_test.go index 5da0906f..2c0c91d6 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1051,3 +1051,44 @@ func TestHeadscale_GenerateGivenName(t *testing.T) { }) } } + +func (s *Suite) TestAutoApproveRoutes(c *check.C) { + err := app.LoadACLPolicy("./tests/acls/acl_policy_autoapprovers.hujson") + c.Assert(err, check.IsNil) + + namespace, err := app.CreateNamespace("test") + c.Assert(err, check.IsNil) + + pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) + c.Assert(err, check.IsNil) + + nodeKey := key.NewNode() + + defaultRoute := netaddr.MustParseIPPrefix("0.0.0.0/0") + route1 := netaddr.MustParseIPPrefix("10.10.0.0/16") + route2 := netaddr.MustParseIPPrefix("10.11.0.0/16") + + machine := Machine{ + ID: 0, + MachineKey: "foo", + NodeKey: NodePublicKeyStripPrefix(nodeKey.Public()), + DiscoKey: "faa", + Hostname: "test", + NamespaceID: namespace.ID, + RegisterMethod: RegisterMethodAuthKey, + AuthKeyID: uint(pak.ID), + HostInfo: HostInfo{ + RequestTags: []string{"tag:exit"}, + RoutableIPs: []netaddr.IPPrefix{defaultRoute, route1, route2}, + }, + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, + } + + app.db.Save(&machine) + + machine0ByID, err := app.GetMachineByID(0) + c.Assert(err, check.IsNil) + + app.EnableAutoApprovedRoutes(machine0ByID) + c.Assert(machine0ByID.GetEnabledRoutes(), check.HasLen, 3) +} diff --git a/protocol_common_poll.go b/protocol_common_poll.go index d6a8fff2..37e2dbc7 100644 --- a/protocol_common_poll.go +++ b/protocol_common_poll.go @@ -44,10 +44,7 @@ func (h *Headscale) handlePollCommon( } // update routes with peer information - err = h.EnableAutoApprovedRoutes(machine) - if err != nil { - //TODO - } + h.EnableAutoApprovedRoutes(machine) } // From Tailscale client: diff --git a/tests/acls/acl_policy_autoapprovers.hujson b/tests/acls/acl_policy_autoapprovers.hujson new file mode 100644 index 00000000..bf564d88 --- /dev/null +++ b/tests/acls/acl_policy_autoapprovers.hujson @@ -0,0 +1,24 @@ +// This ACL validates autoApprovers support for +// exit nodes and advertised routes + +{ + "tagOwners": { + "tag:exit": ["test"], + }, + + "groups": { + "group:test": ["test"] + }, + + "acls": [ + {"action": "accept", "users": ["*"], "ports": ["*:*"]}, + ], + + "autoApprovers": { + "exitNode": ["tag:exit"], + "routes": { + "10.10.0.0/16": ["group:test"], + "10.11.0.0/16": ["test"], + } + } +} \ No newline at end of file From 842c28adff914d6b6e6ba6cc4461df99b8f88175 Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Mon, 5 Sep 2022 09:33:53 +1000 Subject: [PATCH 4/7] replace netaddr usage with netip --- acls_types.go | 7 ++++--- machine.go | 2 +- machine_test.go | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/acls_types.go b/acls_types.go index 0289e099..6ee5bfdb 100644 --- a/acls_types.go +++ b/acls_types.go @@ -111,7 +111,7 @@ func (policy ACLPolicy) IsZero() bool { // Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix func (autoApprovers *AutoApprovers) GetRouteApprovers( - prefix netaddr.IPPrefix, + prefix netip.Prefix, ) ([]string, error) { if prefix.Bits() == 0 { return autoApprovers.ExitNode, nil // 0.0.0.0/0, ::/0 or equivalent @@ -120,13 +120,14 @@ func (autoApprovers *AutoApprovers) GetRouteApprovers( approverAliases := []string{} for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes { - autoApprovedPrefix, err := netaddr.ParseIPPrefix(autoApprovedPrefix) + autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix) + if err != nil { return nil, err } if autoApprovedPrefix.Bits() >= prefix.Bits() && - autoApprovedPrefix.Contains(prefix.IP()) { + autoApprovedPrefix.Contains(prefix.Masked().Addr()) { approverAliases = append(approverAliases, autoApproverAliases...) } } diff --git a/machine.go b/machine.go index 54984d8d..5b53ef22 100644 --- a/machine.go +++ b/machine.go @@ -936,7 +936,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { return nil // This machine has no IPAddresses, so can't possibly match any autoApprovers ACLs } - approvedRoutes := make([]netaddr.IPPrefix, 0, len(machine.HostInfo.RoutableIPs)) + approvedRoutes := make([]netip.Prefix, 0, len(machine.HostInfo.RoutableIPs)) thisMachine := []Machine{*machine} for _, advertisedRoute := range machine.HostInfo.RoutableIPs { diff --git a/machine_test.go b/machine_test.go index e8c9e9a0..7d50db79 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1063,9 +1063,9 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { nodeKey := key.NewNode() - defaultRoute := netaddr.MustParseIPPrefix("0.0.0.0/0") - route1 := netaddr.MustParseIPPrefix("10.10.0.0/16") - route2 := netaddr.MustParseIPPrefix("10.11.0.0/16") + defaultRoute := netip.MustParsePrefix("0.0.0.0/0") + route1 := netip.MustParsePrefix("10.10.0.0/16") + route2 := netip.MustParsePrefix("10.11.0.0/16") machine := Machine{ ID: 0, @@ -1078,9 +1078,9 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { AuthKeyID: uint(pak.ID), HostInfo: HostInfo{ RequestTags: []string{"tag:exit"}, - RoutableIPs: []netaddr.IPPrefix{defaultRoute, route1, route2}, + RoutableIPs: []netip.Prefix{defaultRoute, route1, route2}, }, - IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, + IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, } app.db.Save(&machine) From 688cba7292219b5e311a62938c909b3319515b1d Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Wed, 7 Sep 2022 21:39:56 +1000 Subject: [PATCH 5/7] fix linting mistakes --- acls_types.go | 3 +-- machine.go | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/acls_types.go b/acls_types.go index 6ee5bfdb..903f848b 100644 --- a/acls_types.go +++ b/acls_types.go @@ -109,7 +109,7 @@ func (policy ACLPolicy) IsZero() bool { return false } -// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix +// Returns the list of autoApproving namespaces, groups or tags for a given IPPrefix. func (autoApprovers *AutoApprovers) GetRouteApprovers( prefix netip.Prefix, ) ([]string, error) { @@ -121,7 +121,6 @@ func (autoApprovers *AutoApprovers) GetRouteApprovers( for autoApprovedPrefix, autoApproverAliases := range autoApprovers.Routes { autoApprovedPrefix, err := netip.ParsePrefix(autoApprovedPrefix) - if err != nil { return nil, err } diff --git a/machine.go b/machine.go index 5b53ef22..ec0ab464 100644 --- a/machine.go +++ b/machine.go @@ -930,17 +930,16 @@ func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error { return nil } -// Enabled any routes advertised by a machine that match the ACL autoApprovers policy -func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { +// Enabled any routes advertised by a machine that match the ACL autoApprovers policy. +func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) { if len(machine.IPAddresses) == 0 { - return nil // This machine has no IPAddresses, so can't possibly match any autoApprovers ACLs + return // This machine has no IPAddresses, so can't possibly match any autoApprovers ACLs } approvedRoutes := make([]netip.Prefix, 0, len(machine.HostInfo.RoutableIPs)) thisMachine := []Machine{*machine} for _, advertisedRoute := range machine.HostInfo.RoutableIPs { - if contains(machine.EnabledRoutes, advertisedRoute) { continue // Skip routes that are already enabled for the node } @@ -953,7 +952,8 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { Str("advertisedRoute", advertisedRoute.String()). Uint64("machineId", machine.ID). Msg("Failed to resolve autoApprovers for advertised route") - return err + + return } for _, approvedAlias := range routeApprovers { @@ -965,7 +965,8 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { log.Err(err). Str("alias", approvedAlias). Msg("Failed to expand alias when processing autoApprovers policy") - return err + + return } // approvedIPs should contain all of machine's IPs if it matches the rule, so check for first @@ -985,8 +986,6 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { machine.EnabledRoutes = append(machine.EnabledRoutes, approvedRoute) } } - - return nil } func (machine *Machine) RoutesToProto() *v1.Routes { From d764f52f24ae63709b0597943c412b9b314da9c4 Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Fri, 23 Sep 2022 18:08:59 +1000 Subject: [PATCH 6/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443305da..58c7c2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Give a warning when running Headscale with reverse proxy improperly configured for WebSockets [#788](https://github.com/juanfont/headscale/pull/788) - Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811) - Added support for JSON logs [#653](https://github.com/juanfont/headscale/issues/653) +- Add support for evaluating `autoApprovers` ACL entries when a machine is registered [#763](https://github.com/juanfont/headscale/pull/763) ## 0.16.4 (2022-08-21) From 7761a7b23ed411db526b0b6935c5f4975266ad93 Mon Sep 17 00:00:00 2001 From: Benjamin George Roberts Date: Fri, 23 Sep 2022 18:46:35 +1000 Subject: [PATCH 7/7] fix autoapprover test following tagged authkey change --- machine_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine_test.go b/machine_test.go index 25075bc2..275ab14e 100644 --- a/machine_test.go +++ b/machine_test.go @@ -1058,7 +1058,7 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { namespace, err := app.CreateNamespace("test") c.Assert(err, check.IsNil) - pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) + pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil, nil) c.Assert(err, check.IsNil) nodeKey := key.NewNode()