headscale/acls.go

547 lines
12 KiB
Go
Raw Normal View History

2021-07-03 09:55:32 +00:00
package headscale
import (
"encoding/json"
"errors"
2021-07-03 15:31:32 +00:00
"fmt"
2021-07-03 09:55:32 +00:00
"io"
"os"
2022-02-27 08:04:48 +00:00
"path/filepath"
"strconv"
2021-07-03 15:31:32 +00:00
"strings"
2021-07-03 09:55:32 +00:00
2021-08-05 17:18:18 +00:00
"github.com/rs/zerolog/log"
2021-07-03 09:55:32 +00:00
"github.com/tailscale/hujson"
2022-02-27 08:04:48 +00:00
"gopkg.in/yaml.v3"
2021-07-03 15:31:32 +00:00
"inet.af/netaddr"
"tailscale.com/tailcfg"
2021-07-03 09:55:32 +00:00
)
const (
2022-03-03 22:53:08 +00:00
errEmptyPolicy = Error("empty policy")
errInvalidAction = Error("invalid action")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
)
2021-07-03 09:55:32 +00:00
const (
Base8 = 8
Base10 = 10
BitSize16 = 16
BitSize32 = 32
BitSize64 = 64
portRangeBegin = 0
portRangeEnd = 65535
expectedTokenItems = 2
)
2021-11-13 08:39:04 +00:00
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
2021-07-04 11:33:00 +00:00
func (h *Headscale) LoadACLPolicy(path string) error {
log.Debug().
Str("func", "LoadACLPolicy").
Str("path", path).
Msg("Loading ACL policy from path")
2021-07-03 09:55:32 +00:00
policyFile, err := os.Open(path)
if err != nil {
2021-07-03 15:31:32 +00:00
return err
2021-07-03 09:55:32 +00:00
}
defer policyFile.Close()
var policy ACLPolicy
policyBytes, err := io.ReadAll(policyFile)
2021-07-03 09:55:32 +00:00
if err != nil {
2021-07-03 15:31:32 +00:00
return err
2021-07-03 09:55:32 +00:00
}
2021-11-05 07:24:00 +00:00
2022-02-27 08:04:48 +00:00
switch filepath.Ext(path) {
case ".yml", ".yaml":
log.Debug().
Str("path", path).
Bytes("file", policyBytes).
Msg("Loading ACLs from YAML")
err := yaml.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
log.Trace().
Interface("policy", policy).
Msg("Loaded policy from YAML")
default:
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
2021-07-04 11:33:00 +00:00
}
2022-02-27 08:04:48 +00:00
2021-07-03 09:55:32 +00:00
if policy.IsZero() {
2021-11-15 16:33:16 +00:00
return errEmptyPolicy
2021-07-03 09:55:32 +00:00
}
2021-07-03 15:31:32 +00:00
h.aclPolicy = &policy
2022-02-03 19:00:41 +00:00
return h.UpdateACLRules()
}
func (h *Headscale) UpdateACLRules() error {
2021-07-04 11:24:05 +00:00
rules, err := h.generateACLRules()
if err != nil {
return err
}
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
2022-02-03 19:00:41 +00:00
h.aclRules = rules
2021-07-04 11:24:05 +00:00
return nil
2021-07-03 15:31:32 +00:00
}
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
2021-07-03 15:31:32 +00:00
rules := []tailcfg.FilterRule{}
if h.aclPolicy == nil {
return nil, errEmptyPolicy
}
2022-02-25 09:26:34 +00:00
machines, err := h.ListMachines()
if err != nil {
return nil, err
}
for index, acl := range h.aclPolicy.ACLs {
if acl.Action != "accept" {
2021-11-15 16:33:16 +00:00
return nil, errInvalidAction
2021-07-03 15:31:32 +00:00
}
srcIPs := []string{}
for innerIndex, src := range acl.Sources {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
2021-07-03 15:31:32 +00:00
if err != nil {
2021-08-05 17:18:18 +00:00
log.Error().
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
2021-11-14 15:46:09 +00:00
2021-07-03 15:31:32 +00:00
return nil, err
}
srcIPs = append(srcIPs, srcs...)
2021-07-03 15:31:32 +00:00
}
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
return nil, err
}
destPorts := []tailcfg.NetPortRange{}
for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
if err != nil {
2021-08-05 17:18:18 +00:00
log.Error().
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
2021-11-14 15:46:09 +00:00
return nil, err
}
destPorts = append(destPorts, dests...)
}
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPs,
DstPorts: destPorts,
IPProto: protocols,
})
2021-07-03 15:31:32 +00:00
}
return rules, nil
2021-07-03 15:31:32 +00:00
}
2022-02-14 14:54:51 +00:00
func (h *Headscale) generateACLPolicySrcIP(
machines []Machine,
aclPolicy ACLPolicy,
src string,
2022-02-14 14:54:51 +00:00
) ([]string, error) {
return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
}
func (h *Headscale) generateACLPolicyDest(
machines []Machine,
aclPolicy ACLPolicy,
dest string,
needsWildcard bool,
2021-11-13 08:36:45 +00:00
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
2021-11-15 16:33:16 +00:00
return nil, errInvalidPortFormat
}
var alias string
// We can have here stuff like:
// git-server:*
// 192.168.1.0/24:22
// tag:montreal-webserver:80,443
// tag:api-server:443
// example-host-1:*
if len(tokens) == expectedTokenItems {
alias = tokens[0]
} else {
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
2022-03-01 20:01:46 +00:00
expanded, err := expandAlias(
machines,
aclPolicy,
alias,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil {
return nil, err
}
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
if err != nil {
return nil, err
}
dests := []tailcfg.NetPortRange{}
for _, d := range expanded {
for _, p := range *ports {
pr := tailcfg.NetPortRange{
IP: d,
Ports: p,
}
dests = append(dests, pr)
}
}
2021-11-14 15:46:09 +00:00
return dests, nil
}
// parseProtocol reads the proto field of the ACL and generates a list of
// protocols that will be allowed, following the IANA IP protocol number
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
//
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
// as per Tailscale behaviour (see tailcfg.FilterRule).
//
// Also returns a boolean indicating if the protocol
// requires all the destinations to use wildcard as port number (only TCP,
// UDP and SCTP support specifying ports).
func parseProtocol(protocol string) ([]int, bool, error) {
switch protocol {
case "":
return []int{1, 58, 6, 17}, false, nil
case "igmp":
return []int{2}, true, nil
case "ipv4", "ip-in-ip":
return []int{4}, true, nil
case "tcp":
return []int{6}, false, nil
case "egp":
return []int{8}, true, nil
case "igp":
return []int{9}, true, nil
case "udp":
return []int{17}, false, nil
case "gre":
return []int{47}, true, nil
case "esp":
return []int{50}, true, nil
case "ah":
return []int{51}, true, nil
case "sctp":
return []int{132}, false, nil
case "icmp":
return []int{1, 58}, true, nil
default:
protocolNumber, err := strconv.Atoi(protocol)
if err != nil {
return nil, false, err
}
needsWildcard := protocolNumber != 6 && protocolNumber != 17 && protocolNumber != 132 // nolint
return []int{protocolNumber}, needsWildcard, nil
}
}
// expandalias has an input of either
// - a namespace
// - a group
// - a tag
// and transform these in IPAddresses.
2022-02-14 14:54:51 +00:00
func expandAlias(
machines []Machine,
aclPolicy ACLPolicy,
alias string,
2022-03-01 20:01:46 +00:00
stripEmailDomain bool,
2022-02-14 14:54:51 +00:00
) ([]string, error) {
ips := []string{}
if alias == "*" {
return []string{"*"}, nil
2021-07-03 15:31:32 +00:00
}
log.Debug().
Str("alias", alias).
Msg("Expanding")
if strings.HasPrefix(alias, "group:") {
2022-03-01 20:01:46 +00:00
namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
if err != nil {
return ips, err
2021-07-03 15:31:32 +00:00
}
for _, n := range namespaces {
nodes := filterMachinesByNamespace(machines, n)
for _, node := range nodes {
2022-01-16 13:16:59 +00:00
ips = append(ips, node.IPAddresses.ToStringSlice()...)
}
}
return ips, nil
2021-07-03 15:31:32 +00:00
}
if strings.HasPrefix(alias, "tag:") {
// check for forced tags
for _, machine := range machines {
if contains(machine.ForcedTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
// find tag owners
2022-03-01 20:01:46 +00:00
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
if err != nil {
if errors.Is(err, errInvalidTag) {
if len(ips) == 0 {
2022-04-15 16:27:57 +00:00
return ips, fmt.Errorf(
2022-05-16 12:59:46 +00:00
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
2022-04-15 16:27:57 +00:00
errInvalidTag,
alias,
)
}
return ips, nil
} else {
return ips, err
}
}
// filter out machines per tag owner
for _, namespace := range owners {
machines := filterMachinesByNamespace(machines, namespace)
for _, machine := range machines {
2022-03-01 16:34:24 +00:00
hi := machine.GetHostInfo()
if contains(hi.RequestTags, alias) {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
}
}
}
return ips, nil
2021-07-03 15:31:32 +00:00
}
// if alias is a namespace
nodes := filterMachinesByNamespace(machines, alias)
2022-03-02 08:15:14 +00:00
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
for _, n := range nodes {
ips = append(ips, n.IPAddresses.ToStringSlice()...)
}
if len(ips) > 0 {
return ips, nil
2021-07-03 15:31:32 +00:00
}
// if alias is an host
if h, ok := aclPolicy.Hosts[alias]; ok {
return []string{h.String()}, nil
2021-07-03 15:31:32 +00:00
}
// if alias is an IP
ip, err := netaddr.ParseIP(alias)
2021-07-03 15:31:32 +00:00
if err == nil {
return []string{ip.String()}, nil
2021-07-03 15:31:32 +00:00
}
// if alias is an CIDR
cidr, err := netaddr.ParseIPPrefix(alias)
2021-07-03 15:31:32 +00:00
if err == nil {
return []string{cidr.String()}, nil
2021-07-03 15:31:32 +00:00
}
log.Warn().Msgf("No IPs found with the alias %v", alias)
return ips, nil
2021-07-03 09:55:32 +00:00
}
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
// that are correctly tagged since they should not be listed as being in the namespace
// we assume in this function that we only have nodes from 1 namespace.
2022-02-14 14:54:51 +00:00
func excludeCorrectlyTaggedNodes(
aclPolicy ACLPolicy,
nodes []Machine,
namespace string,
2022-03-02 08:15:14 +00:00
) []Machine {
out := []Machine{}
tags := []string{}
for tag, ns := range aclPolicy.TagOwners {
if contains(ns, namespace) {
tags = append(tags, tag)
}
}
// for each machine if tag is in tags list, don't append it.
for _, machine := range nodes {
2022-03-01 16:34:24 +00:00
hi := machine.GetHostInfo()
found := false
for _, t := range hi.RequestTags {
if contains(tags, t) {
found = true
break
}
}
if len(machine.ForcedTags) > 0 {
found = true
}
if !found {
out = append(out, machine)
}
}
2022-03-02 08:15:14 +00:00
return out
}
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
if portsStr == "*" {
return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
}, nil
}
if needsWildcard {
return nil, errWildcardIsNeeded
}
ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") {
rang := strings.Split(portStr, "-")
2021-11-14 17:44:37 +00:00
switch len(rang) {
case 1:
port, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
First: uint16(port),
Last: uint16(port),
})
2021-11-14 17:44:37 +00:00
case expectedTokenItems:
start, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil {
return nil, err
}
last, err := strconv.ParseUint(rang[1], Base10, BitSize16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
First: uint16(start),
Last: uint16(last),
})
2021-11-14 17:44:37 +00:00
default:
2021-11-15 16:33:16 +00:00
return nil, errInvalidPortFormat
}
}
2021-11-14 15:46:09 +00:00
return &ports, nil
}
func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
out := []Machine{}
for _, machine := range machines {
if machine.Namespace.Name == namespace {
out = append(out, machine)
}
}
return out
}
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups.
2022-03-01 20:01:46 +00:00
func expandTagOwners(
aclPolicy ACLPolicy,
tag string,
stripEmailDomain bool,
) ([]string, error) {
var owners []string
ows, ok := aclPolicy.TagOwners[tag]
if !ok {
2022-02-14 14:54:51 +00:00
return []string{}, fmt.Errorf(
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
errInvalidTag,
tag,
)
}
for _, owner := range ows {
if strings.HasPrefix(owner, "group:") {
2022-03-01 20:01:46 +00:00
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
if err != nil {
return []string{}, err
}
owners = append(owners, gs...)
} else {
owners = append(owners, owner)
}
}
return owners, nil
}
// expandGroup will return the list of namespace inside the group
// after some validation.
2022-03-01 20:01:46 +00:00
func expandGroup(
aclPolicy ACLPolicy,
group string,
stripEmailDomain bool,
) ([]string, error) {
outGroups := []string{}
aclGroups, ok := aclPolicy.Groups[group]
if !ok {
2022-02-14 14:54:51 +00:00
return []string{}, fmt.Errorf(
"group %v isn't registered. %w",
group,
errInvalidGroup,
)
}
2022-03-01 20:01:46 +00:00
for _, group := range aclGroups {
if strings.HasPrefix(group, "group:") {
2022-02-14 14:54:51 +00:00
return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup,
)
}
grp, err := NormalizeToFQDNRules(group, stripEmailDomain)
2022-03-01 20:01:46 +00:00
if err != nil {
return []string{}, fmt.Errorf(
"failed to normalize group %q, err: %w",
group,
errInvalidGroup,
)
}
outGroups = append(outGroups, grp)
}
2022-03-01 20:01:46 +00:00
return outGroups, nil
}