mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-11-04 01:51:04 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			299 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package headscale
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
	"github.com/tailscale/hujson"
 | 
						|
	"inet.af/netaddr"
 | 
						|
	"tailscale.com/tailcfg"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	errEmptyPolicy        = Error("empty policy")
 | 
						|
	errInvalidAction      = Error("invalid action")
 | 
						|
	errInvalidUserSection = Error("invalid user section")
 | 
						|
	errInvalidGroup       = Error("invalid group")
 | 
						|
	errInvalidTag         = Error("invalid tag")
 | 
						|
	errInvalidNamespace   = Error("invalid namespace")
 | 
						|
	errInvalidPortFormat  = Error("invalid port format")
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	Base10             = 10
 | 
						|
	BitSize16          = 16
 | 
						|
	portRangeBegin     = 0
 | 
						|
	portRangeEnd       = 65535
 | 
						|
	expectedTokenItems = 2
 | 
						|
)
 | 
						|
 | 
						|
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
 | 
						|
func (h *Headscale) LoadACLPolicy(path string) error {
 | 
						|
	policyFile, err := os.Open(path)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer policyFile.Close()
 | 
						|
 | 
						|
	var policy ACLPolicy
 | 
						|
	policyBytes, err := io.ReadAll(policyFile)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
	}
 | 
						|
	if policy.IsZero() {
 | 
						|
		return errEmptyPolicy
 | 
						|
	}
 | 
						|
 | 
						|
	h.aclPolicy = &policy
 | 
						|
	rules, err := h.generateACLRules()
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	h.aclRules = rules
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
 | 
						|
	rules := []tailcfg.FilterRule{}
 | 
						|
 | 
						|
	for index, acl := range h.aclPolicy.ACLs {
 | 
						|
		if acl.Action != "accept" {
 | 
						|
			return nil, errInvalidAction
 | 
						|
		}
 | 
						|
 | 
						|
		filterRule := tailcfg.FilterRule{}
 | 
						|
 | 
						|
		srcIPs := []string{}
 | 
						|
		for innerIndex, user := range acl.Users {
 | 
						|
			srcs, err := h.generateACLPolicySrcIP(user)
 | 
						|
			if err != nil {
 | 
						|
				log.Error().
 | 
						|
					Msgf("Error parsing ACL %d, User %d", index, innerIndex)
 | 
						|
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			srcIPs = append(srcIPs, srcs...)
 | 
						|
		}
 | 
						|
		filterRule.SrcIPs = srcIPs
 | 
						|
 | 
						|
		destPorts := []tailcfg.NetPortRange{}
 | 
						|
		for innerIndex, ports := range acl.Ports {
 | 
						|
			dests, err := h.generateACLPolicyDestPorts(ports)
 | 
						|
			if err != nil {
 | 
						|
				log.Error().
 | 
						|
					Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
 | 
						|
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			destPorts = append(destPorts, dests...)
 | 
						|
		}
 | 
						|
 | 
						|
		rules = append(rules, tailcfg.FilterRule{
 | 
						|
			SrcIPs:   srcIPs,
 | 
						|
			DstPorts: destPorts,
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
	return rules, nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
 | 
						|
	return h.expandAlias(u)
 | 
						|
}
 | 
						|
 | 
						|
func (h *Headscale) generateACLPolicyDestPorts(
 | 
						|
	d string,
 | 
						|
) ([]tailcfg.NetPortRange, error) {
 | 
						|
	tokens := strings.Split(d, ":")
 | 
						|
	if len(tokens) < expectedTokenItems || len(tokens) > 3 {
 | 
						|
		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])
 | 
						|
	}
 | 
						|
 | 
						|
	expanded, err := h.expandAlias(alias)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	ports, err := h.expandPorts(tokens[len(tokens)-1])
 | 
						|
	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)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return dests, nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *Headscale) expandAlias(alias string) ([]string, error) {
 | 
						|
	if alias == "*" {
 | 
						|
		return []string{"*"}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.HasPrefix(alias, "group:") {
 | 
						|
		if _, ok := h.aclPolicy.Groups[alias]; !ok {
 | 
						|
			return nil, errInvalidGroup
 | 
						|
		}
 | 
						|
		ips := []string{}
 | 
						|
		for _, n := range h.aclPolicy.Groups[alias] {
 | 
						|
			nodes, err := h.ListMachinesInNamespace(n)
 | 
						|
			if err != nil {
 | 
						|
				return nil, errInvalidNamespace
 | 
						|
			}
 | 
						|
			for _, node := range nodes {
 | 
						|
				ips = append(ips, node.IPAddress)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return ips, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.HasPrefix(alias, "tag:") {
 | 
						|
		if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
 | 
						|
			return nil, errInvalidTag
 | 
						|
		}
 | 
						|
 | 
						|
		// This will have HORRIBLE performance.
 | 
						|
		// We need to change the data model to better store tags
 | 
						|
		machines := []Machine{}
 | 
						|
		if err := h.db.Where("registered").Find(&machines).Error; err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		ips := []string{}
 | 
						|
		for _, machine := range machines {
 | 
						|
			hostinfo := tailcfg.Hostinfo{}
 | 
						|
			if len(machine.HostInfo) != 0 {
 | 
						|
				hi, err := machine.HostInfo.MarshalJSON()
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
				err = json.Unmarshal(hi, &hostinfo)
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
 | 
						|
				// FIXME: Check TagOwners allows this
 | 
						|
				for _, t := range hostinfo.RequestTags {
 | 
						|
					if alias[4:] == t {
 | 
						|
						ips = append(ips, machine.IPAddress)
 | 
						|
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return ips, nil
 | 
						|
	}
 | 
						|
 | 
						|
	n, err := h.GetNamespace(alias)
 | 
						|
	if err == nil {
 | 
						|
		nodes, err := h.ListMachinesInNamespace(n.Name)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		ips := []string{}
 | 
						|
		for _, n := range nodes {
 | 
						|
			ips = append(ips, n.IPAddress)
 | 
						|
		}
 | 
						|
 | 
						|
		return ips, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if h, ok := h.aclPolicy.Hosts[alias]; ok {
 | 
						|
		return []string{h.String()}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	ip, err := netaddr.ParseIP(alias)
 | 
						|
	if err == nil {
 | 
						|
		return []string{ip.String()}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	cidr, err := netaddr.ParseIPPrefix(alias)
 | 
						|
	if err == nil {
 | 
						|
		return []string{cidr.String()}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return nil, errInvalidUserSection
 | 
						|
}
 | 
						|
 | 
						|
func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
 | 
						|
	if portsStr == "*" {
 | 
						|
		return &[]tailcfg.PortRange{
 | 
						|
			{First: portRangeBegin, Last: portRangeEnd},
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	ports := []tailcfg.PortRange{}
 | 
						|
	for _, portStr := range strings.Split(portsStr, ",") {
 | 
						|
		rang := strings.Split(portStr, "-")
 | 
						|
		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),
 | 
						|
			})
 | 
						|
 | 
						|
		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),
 | 
						|
			})
 | 
						|
 | 
						|
		default:
 | 
						|
			return nil, errInvalidPortFormat
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return &ports, nil
 | 
						|
}
 |