mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge pull request #763 from tsujamin/autoapprovers
This commit is contained in:
		
						commit
						a507a04650
					
				| @ -16,6 +16,7 @@ | ||||
| - 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 generating pre-auth keys with tags [#767](https://github.com/juanfont/headscale/pull/767) | ||||
| - 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) | ||||
| 
 | ||||
|  | ||||
| @ -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 netip objects.
 | ||||
| func (hosts *Hosts) UnmarshalJSON(data []byte) error { | ||||
| 	newHosts := Hosts{} | ||||
| @ -100,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 netip.Prefix, | ||||
| ) ([]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 := netip.ParsePrefix(autoApprovedPrefix) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		if autoApprovedPrefix.Bits() >= prefix.Bits() && | ||||
| 			autoApprovedPrefix.Contains(prefix.Masked().Addr()) { | ||||
| 			approverAliases = append(approverAliases, autoApproverAliases...) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return approverAliases, nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										58
									
								
								machine.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								machine.go
									
									
									
									
									
								
							| @ -949,6 +949,64 @@ 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) { | ||||
| 	if len(machine.IPAddresses) == 0 { | ||||
| 		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
 | ||||
| 		} | ||||
| 
 | ||||
| 		routeApprovers, err := h.aclPolicy.AutoApprovers.GetRouteApprovers( | ||||
| 			advertisedRoute, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			log.Err(err). | ||||
| 				Str("advertisedRoute", advertisedRoute.String()). | ||||
| 				Uint64("machineId", machine.ID). | ||||
| 				Msg("Failed to resolve autoApprovers for advertised route") | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		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 | ||||
| 				} | ||||
| 
 | ||||
| 				// 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) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (machine *Machine) RoutesToProto() *v1.Routes { | ||||
| 	availableRoutes := machine.GetAdvertisedRoutes() | ||||
| 
 | ||||
|  | ||||
| @ -1050,3 +1050,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, nil) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	nodeKey := key.NewNode() | ||||
| 
 | ||||
| 	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, | ||||
| 		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: []netip.Prefix{defaultRoute, route1, route2}, | ||||
| 		}, | ||||
| 		IPAddresses: []netip.Addr{netip.MustParseAddr("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) | ||||
| } | ||||
|  | ||||
| @ -42,7 +42,11 @@ func (h *Headscale) handlePollCommon( | ||||
| 				Str("machine", machine.Hostname). | ||||
| 				Err(err) | ||||
| 		} | ||||
| 
 | ||||
| 		// update routes with peer information
 | ||||
| 		h.EnableAutoApprovedRoutes(machine) | ||||
| 	} | ||||
| 
 | ||||
| 	// From Tailscale client:
 | ||||
| 	//
 | ||||
| 	// ReadOnly is whether the client just wants to fetch the MapResponse,
 | ||||
|  | ||||
							
								
								
									
										24
									
								
								tests/acls/acl_policy_autoapprovers.hujson
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/acls/acl_policy_autoapprovers.hujson
									
									
									
									
									
										Normal file
									
								
							| @ -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"], | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user