commit
e67a98b758
19 changed files with 880 additions and 3 deletions
263
acls.go
Normal file
263
acls.go
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tailscale/hujson"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorEmptyPolicy = Error("empty policy")
|
||||||
|
const errorInvalidAction = Error("invalid action")
|
||||||
|
const errorInvalidUserSection = Error("invalid user section")
|
||||||
|
const errorInvalidGroup = Error("invalid group")
|
||||||
|
const errorInvalidTag = Error("invalid tag")
|
||||||
|
const errorInvalidNamespace = Error("invalid namespace")
|
||||||
|
const errorInvalidPortFormat = Error("invalid port format")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
b, err := io.ReadAll(policyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = hujson.Unmarshal(b, &policy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policy.IsZero() {
|
||||||
|
return errorEmptyPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
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 i, a := range h.aclPolicy.ACLs {
|
||||||
|
if a.Action != "accept" {
|
||||||
|
return nil, errorInvalidAction
|
||||||
|
}
|
||||||
|
|
||||||
|
r := tailcfg.FilterRule{}
|
||||||
|
|
||||||
|
srcIPs := []string{}
|
||||||
|
for j, u := range a.Users {
|
||||||
|
srcs, err := h.generateACLPolicySrcIP(u)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error parsing ACL %d, User %d", i, j)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
srcIPs = append(srcIPs, *srcs...)
|
||||||
|
}
|
||||||
|
r.SrcIPs = srcIPs
|
||||||
|
|
||||||
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
|
for j, d := range a.Ports {
|
||||||
|
dests, err := h.generateACLPolicyDestPorts(d)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error parsing ACL %d, Port %d", i, j)
|
||||||
|
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) < 2 || len(tokens) > 3 {
|
||||||
|
return nil, errorInvalidPortFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
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) == 2 {
|
||||||
|
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(s string) (*[]string, error) {
|
||||||
|
if s == "*" {
|
||||||
|
return &[]string{"*"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(s, "group:") {
|
||||||
|
if _, ok := h.aclPolicy.Groups[s]; !ok {
|
||||||
|
return nil, errorInvalidGroup
|
||||||
|
}
|
||||||
|
ips := []string{}
|
||||||
|
for _, n := range h.aclPolicy.Groups[s] {
|
||||||
|
nodes, err := h.ListMachinesInNamespace(n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorInvalidNamespace
|
||||||
|
}
|
||||||
|
for _, node := range *nodes {
|
||||||
|
ips = append(ips, node.IPAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(s, "tag:") {
|
||||||
|
if _, ok := h.aclPolicy.TagOwners[s]; !ok {
|
||||||
|
return nil, errorInvalidTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 _, m := range machines {
|
||||||
|
hostinfo := tailcfg.Hostinfo{}
|
||||||
|
if len(m.HostInfo) != 0 {
|
||||||
|
hi, err := m.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 s[4:] == t {
|
||||||
|
ips = append(ips, m.IPAddress)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := h.GetNamespace(s)
|
||||||
|
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[s]; ok {
|
||||||
|
return &[]string{h.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := netaddr.ParseIP(s)
|
||||||
|
if err == nil {
|
||||||
|
return &[]string{ip.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cidr, err := netaddr.ParseIPPrefix(s)
|
||||||
|
if err == nil {
|
||||||
|
return &[]string{cidr.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errorInvalidUserSection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) {
|
||||||
|
if s == "*" {
|
||||||
|
return &[]tailcfg.PortRange{{First: 0, Last: 65535}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := []tailcfg.PortRange{}
|
||||||
|
for _, p := range strings.Split(s, ",") {
|
||||||
|
rang := strings.Split(p, "-")
|
||||||
|
if len(rang) == 1 {
|
||||||
|
pi, err := strconv.ParseUint(rang[0], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports = append(ports, tailcfg.PortRange{
|
||||||
|
First: uint16(pi),
|
||||||
|
Last: uint16(pi),
|
||||||
|
})
|
||||||
|
} else if len(rang) == 2 {
|
||||||
|
start, err := strconv.ParseUint(rang[0], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
last, err := strconv.ParseUint(rang[1], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ports = append(ports, tailcfg.PortRange{
|
||||||
|
First: uint16(start),
|
||||||
|
Last: uint16(last),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return nil, errorInvalidPortFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ports, nil
|
||||||
|
}
|
160
acls_test.go
Normal file
160
acls_test.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/check.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Suite) TestWrongPath(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("asdfg")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestBrokenHuJson(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("./tests/acls/broken.hujson")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestInvalidPolicyHuson(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("./tests/acls/invalid.hujson")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
c.Assert(err, check.Equals, errorEmptyPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestParseHosts(c *check.C) {
|
||||||
|
var hs Hosts
|
||||||
|
err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`))
|
||||||
|
c.Assert(hs, check.NotNil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestParseInvalidCIDR(c *check.C) {
|
||||||
|
var hs Hosts
|
||||||
|
err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`))
|
||||||
|
c.Assert(hs, check.IsNil)
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestBasicRule(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := h.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestPortRange(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := h.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
|
||||||
|
c.Assert(*rules, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400))
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestPortWildcard(c *check.C) {
|
||||||
|
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := h.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
|
||||||
|
c.Assert(*rules, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
|
||||||
|
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].SrcIPs[0], check.Equals, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestPortNamespace(c *check.C) {
|
||||||
|
n, err := h.CreateNamespace("testnamespace")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine("testnamespace", "testmachine")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
ip, _ := h.getAvailableIP()
|
||||||
|
m := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "foo",
|
||||||
|
NodeKey: "bar",
|
||||||
|
DiscoKey: "faa",
|
||||||
|
Name: "testmachine",
|
||||||
|
NamespaceID: n.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: ip.String(),
|
||||||
|
AuthKeyID: uint(pak.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m)
|
||||||
|
|
||||||
|
err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := h.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
|
||||||
|
c.Assert(*rules, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
|
||||||
|
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip")
|
||||||
|
c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestPortGroup(c *check.C) {
|
||||||
|
n, err := h.CreateNamespace("testnamespace")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
_, err = h.GetMachine("testnamespace", "testmachine")
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
ip, _ := h.getAvailableIP()
|
||||||
|
m := Machine{
|
||||||
|
ID: 0,
|
||||||
|
MachineKey: "foo",
|
||||||
|
NodeKey: "bar",
|
||||||
|
DiscoKey: "faa",
|
||||||
|
Name: "testmachine",
|
||||||
|
NamespaceID: n.ID,
|
||||||
|
Registered: true,
|
||||||
|
RegisterMethod: "authKey",
|
||||||
|
IPAddress: ip.String(),
|
||||||
|
AuthKeyID: uint(pak.ID),
|
||||||
|
}
|
||||||
|
h.db.Save(&m)
|
||||||
|
|
||||||
|
err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := h.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
|
||||||
|
c.Assert(*rules, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
|
||||||
|
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
|
||||||
|
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1)
|
||||||
|
c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip")
|
||||||
|
c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String())
|
||||||
|
}
|
70
acls_types.go
Normal file
70
acls_types.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tailscale/hujson"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ACLPolicy represents a Tailscale ACL Policy
|
||||||
|
type ACLPolicy struct {
|
||||||
|
Groups Groups `json:"Groups"`
|
||||||
|
Hosts Hosts `json:"Hosts"`
|
||||||
|
TagOwners TagOwners `json:"TagOwners"`
|
||||||
|
ACLs []ACL `json:"ACLs"`
|
||||||
|
Tests []ACLTest `json:"Tests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACL is a basic rule for the ACL Policy
|
||||||
|
type ACL struct {
|
||||||
|
Action string `json:"Action"`
|
||||||
|
Users []string `json:"Users"`
|
||||||
|
Ports []string `json:"Ports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups references a series of alias in the ACL rules
|
||||||
|
type Groups map[string][]string
|
||||||
|
|
||||||
|
// Hosts are alias for IP addresses or subnets
|
||||||
|
type Hosts map[string]netaddr.IPPrefix
|
||||||
|
|
||||||
|
// TagOwners specify what users (namespaces?) are allow to use certain tags
|
||||||
|
type TagOwners map[string][]string
|
||||||
|
|
||||||
|
// ACLTest is not implemented, but should be use to check if a certain rule is allowed
|
||||||
|
type ACLTest struct {
|
||||||
|
User string `json:"User"`
|
||||||
|
Allow []string `json:"Allow"`
|
||||||
|
Deny []string `json:"Deny,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects
|
||||||
|
func (h *Hosts) UnmarshalJSON(data []byte) error {
|
||||||
|
hosts := Hosts{}
|
||||||
|
hs := make(map[string]string)
|
||||||
|
err := hujson.Unmarshal(data, &hs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range hs {
|
||||||
|
if !strings.Contains(v, "/") {
|
||||||
|
v = v + "/32"
|
||||||
|
}
|
||||||
|
prefix, err := netaddr.ParseIPPrefix(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hosts[k] = prefix
|
||||||
|
}
|
||||||
|
*h = hosts
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero is perhaps a bit naive here
|
||||||
|
func (p ACLPolicy) IsZero() bool {
|
||||||
|
if len(p.Groups) == 0 && len(p.Hosts) == 0 && len(p.ACLs) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
2
api.go
2
api.go
|
@ -361,7 +361,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
|
||||||
DNS: []netaddr.IP{},
|
DNS: []netaddr.IP{},
|
||||||
SearchPaths: []string{},
|
SearchPaths: []string{},
|
||||||
Domain: "foobar@example.com",
|
Domain: "foobar@example.com",
|
||||||
PacketFilter: tailcfg.FilterAllowAll,
|
PacketFilter: *h.aclRules,
|
||||||
DERPMap: h.cfg.DerpMap,
|
DERPMap: h.cfg.DerpMap,
|
||||||
UserProfiles: []tailcfg.UserProfile{},
|
UserProfiles: []tailcfg.UserProfile{},
|
||||||
}
|
}
|
||||||
|
|
5
app.go
5
app.go
|
@ -51,6 +51,9 @@ type Headscale struct {
|
||||||
publicKey *wgkey.Key
|
publicKey *wgkey.Key
|
||||||
privateKey *wgkey.Private
|
privateKey *wgkey.Private
|
||||||
|
|
||||||
|
aclPolicy *ACLPolicy
|
||||||
|
aclRules *[]tailcfg.FilterRule
|
||||||
|
|
||||||
pollMu sync.Mutex
|
pollMu sync.Mutex
|
||||||
clientsPolling map[uint64]chan []byte // this is by all means a hackity hack
|
clientsPolling map[uint64]chan []byte // this is by all means a hackity hack
|
||||||
}
|
}
|
||||||
|
@ -84,7 +87,9 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||||
dbString: dbString,
|
dbString: dbString,
|
||||||
privateKey: privKey,
|
privateKey: privKey,
|
||||||
publicKey: &pubKey,
|
publicKey: &pubKey,
|
||||||
|
aclRules: &tailcfg.FilterAllowAll, // default allowall
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.initDB()
|
err = h.initDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -119,6 +119,13 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We are doing this here, as in the future could be cool to have it also hot-reload
|
||||||
|
err = h.LoadACLPolicy(absPath(viper.GetString("acl_policy_path")))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not load the ACL policy: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,5 +14,6 @@
|
||||||
"tls_letsencrypt_cache_dir": ".cache",
|
"tls_letsencrypt_cache_dir": ".cache",
|
||||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||||
"tls_cert_path": "",
|
"tls_cert_path": "",
|
||||||
"tls_key_path": ""
|
"tls_key_path": "",
|
||||||
|
"acl_policy_path": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,5 +10,6 @@
|
||||||
"tls_letsencrypt_cache_dir": ".cache",
|
"tls_letsencrypt_cache_dir": ".cache",
|
||||||
"tls_letsencrypt_challenge_type": "HTTP-01",
|
"tls_letsencrypt_challenge_type": "HTTP-01",
|
||||||
"tls_cert_path": "",
|
"tls_cert_path": "",
|
||||||
"tls_key_path": ""
|
"tls_key_path": "",
|
||||||
|
"acl_policy_path": ""
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -10,6 +10,7 @@ require (
|
||||||
github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
||||||
github.com/spf13/cobra v1.1.3
|
github.com/spf13/cobra v1.1.3
|
||||||
github.com/spf13/viper v1.8.1
|
github.com/spf13/viper v1.8.1
|
||||||
|
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -746,6 +746,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
||||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||||
|
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
|
||||||
|
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
|
||||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||||
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||||
|
|
127
tests/acls/acl_policy_1.hujson
Normal file
127
tests/acls/acl_policy_1.hujson
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
{
|
||||||
|
// Declare static groups of users beyond those in the identity service.
|
||||||
|
"Groups": {
|
||||||
|
"group:example": [
|
||||||
|
"user1@example.com",
|
||||||
|
"user2@example.com",
|
||||||
|
],
|
||||||
|
"group:example2": [
|
||||||
|
"user1@example.com",
|
||||||
|
"user2@example.com",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
||||||
|
"Hosts": {
|
||||||
|
"example-host-1": "100.100.100.100",
|
||||||
|
"example-host-2": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
// Define who is allowed to use which tags.
|
||||||
|
"TagOwners": {
|
||||||
|
// Everyone in the montreal-admins or global-admins group are
|
||||||
|
// allowed to tag servers as montreal-webserver.
|
||||||
|
"tag:montreal-webserver": [
|
||||||
|
"group:example",
|
||||||
|
],
|
||||||
|
// Only a few admins are allowed to create API servers.
|
||||||
|
"tag:production": [
|
||||||
|
"group:example",
|
||||||
|
"president@example.com",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Access control lists.
|
||||||
|
"ACLs": [
|
||||||
|
// Engineering users, plus the president, can access port 22 (ssh)
|
||||||
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
||||||
|
// ports on git-server or ci-server.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"group:example2",
|
||||||
|
"192.168.1.0/24"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"*:22,3389",
|
||||||
|
"git-server:*",
|
||||||
|
"ci-server:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Allow engineer users to access any port on a device tagged with
|
||||||
|
// tag:production.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"group:example"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"tag:production:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
||||||
|
// on both networks.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"example-host-2",
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"example-host-1:*",
|
||||||
|
"192.168.1.0/24:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Allow every user of your network to access anything on the network.
|
||||||
|
// Comment out this section if you want to define specific ACL
|
||||||
|
// restrictions above.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"*:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// All users in Montreal are allowed to access the Montreal web
|
||||||
|
// servers.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"example-host-1"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"tag:montreal-webserver:80,443"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Montreal web servers are allowed to make outgoing connections to
|
||||||
|
// the API servers, but only on https port 443.
|
||||||
|
// In contrast, this doesn't grant API servers the right to initiate
|
||||||
|
// any connections.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"tag:montreal-webserver"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"tag:api-server:443"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Declare tests to check functionality of ACL rules
|
||||||
|
"Tests": [
|
||||||
|
{
|
||||||
|
"User": "user1@example.com",
|
||||||
|
"Allow": [
|
||||||
|
"example-host-1:22",
|
||||||
|
"example-host-2:80"
|
||||||
|
],
|
||||||
|
"Deny": [
|
||||||
|
"exapmle-host-2:100"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"User": "user2@example.com",
|
||||||
|
"Allow": [
|
||||||
|
"100.60.3.4:22"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
24
tests/acls/acl_policy_basic_1.hujson
Normal file
24
tests/acls/acl_policy_basic_1.hujson
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// This ACL is a very basic example to validate the
|
||||||
|
// expansion of hosts
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"Hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"ACLs": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"subnet-1",
|
||||||
|
"192.168.1.0/24"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"*:22,3389",
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
26
tests/acls/acl_policy_basic_groups.hujson
Normal file
26
tests/acls/acl_policy_basic_groups.hujson
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// This ACL is used to test group expansion
|
||||||
|
|
||||||
|
{
|
||||||
|
"Groups": {
|
||||||
|
"group:example": [
|
||||||
|
"testnamespace",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
"Hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"ACLs": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"group:example",
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
20
tests/acls/acl_policy_basic_namespace_as_user.hujson
Normal file
20
tests/acls/acl_policy_basic_namespace_as_user.hujson
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// This ACL is used to test namespace expansion
|
||||||
|
|
||||||
|
{
|
||||||
|
"Hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"ACLs": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"testnamespace",
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
20
tests/acls/acl_policy_basic_range.hujson
Normal file
20
tests/acls/acl_policy_basic_range.hujson
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// This ACL is used to test the port range expansion
|
||||||
|
|
||||||
|
{
|
||||||
|
"Hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"ACLs": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"subnet-1",
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"host-1:5400-5500",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
20
tests/acls/acl_policy_basic_wildcards.hujson
Normal file
20
tests/acls/acl_policy_basic_wildcards.hujson
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// This ACL is used to test wildcards
|
||||||
|
|
||||||
|
{
|
||||||
|
"Hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"ACLs": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
125
tests/acls/acl_policy_invalid.hujson
Normal file
125
tests/acls/acl_policy_invalid.hujson
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
{
|
||||||
|
// Declare static groups of users beyond those in the identity service.
|
||||||
|
"Groups": {
|
||||||
|
"group:example": [
|
||||||
|
"user1@example.com",
|
||||||
|
"user2@example.com",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
||||||
|
"Hosts": {
|
||||||
|
"example-host-1": "100.100.100.100",
|
||||||
|
"example-host-2": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
// Define who is allowed to use which tags.
|
||||||
|
"TagOwners": {
|
||||||
|
// Everyone in the montreal-admins or global-admins group are
|
||||||
|
// allowed to tag servers as montreal-webserver.
|
||||||
|
"tag:montreal-webserver": [
|
||||||
|
"group:montreal-admins",
|
||||||
|
"group:global-admins",
|
||||||
|
],
|
||||||
|
// Only a few admins are allowed to create API servers.
|
||||||
|
"tag:api-server": [
|
||||||
|
"group:global-admins",
|
||||||
|
"example-host-1",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Access control lists.
|
||||||
|
"ACLs": [
|
||||||
|
// Engineering users, plus the president, can access port 22 (ssh)
|
||||||
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
||||||
|
// ports on git-server or ci-server.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"group:engineering",
|
||||||
|
"president@example.com"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"*:22,3389",
|
||||||
|
"git-server:*",
|
||||||
|
"ci-server:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Allow engineer users to access any port on a device tagged with
|
||||||
|
// tag:production.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"group:engineers"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"tag:production:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
||||||
|
// on both networks.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"my-subnet",
|
||||||
|
"192.168.1.0/24"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"my-subnet:*",
|
||||||
|
"192.168.1.0/24:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Allow every user of your network to access anything on the network.
|
||||||
|
// Comment out this section if you want to define specific ACL
|
||||||
|
// restrictions above.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"*:*"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// All users in Montreal are allowed to access the Montreal web
|
||||||
|
// servers.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"group:montreal-users"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"tag:montreal-webserver:80,443"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Montreal web servers are allowed to make outgoing connections to
|
||||||
|
// the API servers, but only on https port 443.
|
||||||
|
// In contrast, this doesn't grant API servers the right to initiate
|
||||||
|
// any connections.
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"Users": [
|
||||||
|
"tag:montreal-webserver"
|
||||||
|
],
|
||||||
|
"Ports": [
|
||||||
|
"tag:api-server:443"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Declare tests to check functionality of ACL rules
|
||||||
|
"Tests": [
|
||||||
|
{
|
||||||
|
"User": "user1@example.com",
|
||||||
|
"Allow": [
|
||||||
|
"example-host-1:22",
|
||||||
|
"example-host-2:80"
|
||||||
|
],
|
||||||
|
"Deny": [
|
||||||
|
"exapmle-host-2:100"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"User": "user2@example.com",
|
||||||
|
"Allow": [
|
||||||
|
"100.60.3.4:22"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
1
tests/acls/broken.hujson
Normal file
1
tests/acls/broken.hujson
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{
|
4
tests/acls/invalid.hujson
Normal file
4
tests/acls/invalid.hujson
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"valid_json": true,
|
||||||
|
"but_a_policy_though": false
|
||||||
|
}
|
Loading…
Reference in a new issue