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,