From b161a92e5806d9ed4e35b585c5985d3ddfdfdfd6 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 3 Jul 2021 11:55:32 +0200 Subject: [PATCH 01/15] Initial work on ACLs --- acls.go | 30 ++++++++ acls_test.go | 33 +++++++++ acls_types.go | 59 ++++++++++++++++ tests/acls/acl_policy_1.hujson | 125 +++++++++++++++++++++++++++++++++ tests/acls/broken.hujson | 1 + tests/acls/invalid.hujson | 4 ++ 6 files changed, 252 insertions(+) create mode 100644 acls.go create mode 100644 acls_test.go create mode 100644 acls_types.go create mode 100644 tests/acls/acl_policy_1.hujson create mode 100644 tests/acls/broken.hujson create mode 100644 tests/acls/invalid.hujson diff --git a/acls.go b/acls.go new file mode 100644 index 00000000..d698e269 --- /dev/null +++ b/acls.go @@ -0,0 +1,30 @@ +package headscale + +import ( + "io" + "os" + + "github.com/tailscale/hujson" +) + +const errorInvalidPolicy = Error("invalid policy") + +func (h *Headscale) ParsePolicy(path string) (*ACLPolicy, error) { + policyFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer policyFile.Close() + + var policy ACLPolicy + b, err := io.ReadAll(policyFile) + if err != nil { + return nil, err + } + err = hujson.Unmarshal(b, &policy) + if policy.IsZero() { + return nil, errorInvalidPolicy + } + + return &policy, err +} diff --git a/acls_test.go b/acls_test.go new file mode 100644 index 00000000..6b01242c --- /dev/null +++ b/acls_test.go @@ -0,0 +1,33 @@ +package headscale + +import ( + "gopkg.in/check.v1" +) + +func (s *Suite) TestWrongPath(c *check.C) { + _, err := h.ParsePolicy("asdfg") + c.Assert(err, check.NotNil) +} + +func (s *Suite) TestBrokenHuJson(c *check.C) { + _, err := h.ParsePolicy("./tests/acls/broken.hujson") + c.Assert(err, check.NotNil) + +} + +func (s *Suite) TestInvalidPolicyHuson(c *check.C) { + _, err := h.ParsePolicy("./tests/acls/invalid.hujson") + c.Assert(err, check.NotNil) + c.Assert(err, check.Equals, errorInvalidPolicy) +} + +func (s *Suite) TestValidCheckHosts(c *check.C) { + p, err := h.ParsePolicy("./tests/acls/acl_policy_1.hujson") + c.Assert(err, check.IsNil) + c.Assert(p, check.NotNil) + c.Assert(p.IsZero(), check.Equals, false) + + hosts, err := p.GetHosts() + c.Assert(err, check.IsNil) + c.Assert(*hosts, check.HasLen, 2) +} diff --git a/acls_types.go b/acls_types.go new file mode 100644 index 00000000..08b383cc --- /dev/null +++ b/acls_types.go @@ -0,0 +1,59 @@ +package headscale + +import ( + "strings" + + "inet.af/netaddr" +) + +type ACLPolicy struct { + Groups Groups `json:"Groups"` + Hosts Hosts `json:"Hosts"` + TagOwners TagOwners `json:"TagOwners"` + ACLs []ACL `json:"ACLs"` + Tests []ACLTest `json:"Tests"` +} + +type ACL struct { + Action string `json:"Action"` + Users []string `json:"Users"` + Ports []string `json:"Ports"` +} + +type Groups map[string][]string + +type Hosts map[string]string + +type TagOwners struct { + TagMontrealWebserver []string `json:"tag:montreal-webserver"` + TagAPIServer []string `json:"tag:api-server"` +} + +type ACLTest struct { + User string `json:"User"` + Allow []string `json:"Allow"` + Deny []string `json:"Deny,omitempty"` +} + +// 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 +} + +func (p ACLPolicy) GetHosts() (*map[string]netaddr.IPPrefix, error) { + hosts := make(map[string]netaddr.IPPrefix) + for k, v := range p.Hosts { + if !strings.Contains(v, "/") { + v = v + "/32" + } + prefix, err := netaddr.ParseIPPrefix(v) + if err != nil { + return nil, err + } + hosts[k] = prefix + } + return &hosts, nil +} diff --git a/tests/acls/acl_policy_1.hujson b/tests/acls/acl_policy_1.hujson new file mode 100644 index 00000000..3e12bf48 --- /dev/null +++ b/tests/acls/acl_policy_1.hujson @@ -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", + "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: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" + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/broken.hujson b/tests/acls/broken.hujson new file mode 100644 index 00000000..98232c64 --- /dev/null +++ b/tests/acls/broken.hujson @@ -0,0 +1 @@ +{ diff --git a/tests/acls/invalid.hujson b/tests/acls/invalid.hujson new file mode 100644 index 00000000..733f6924 --- /dev/null +++ b/tests/acls/invalid.hujson @@ -0,0 +1,4 @@ +{ + "valid_json": true, + "but_a_policy_though": false +} \ No newline at end of file From 5644dadaf9f5e9eee455e62520afad47bb4ad4be Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 3 Jul 2021 12:02:46 +0200 Subject: [PATCH 02/15] Added dependency on hujson --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 14146a1c..5a9ced26 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.7 // indirect github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 + github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 From bbd6a67c46c935d83214b4c64424fda2fab375c9 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 3 Jul 2021 17:31:08 +0200 Subject: [PATCH 03/15] Added more acl test hujsons --- tests/acls/acl_policy_1.hujson | 16 ++-- tests/acls/acl_policy_invalid.hujson | 125 +++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 tests/acls/acl_policy_invalid.hujson diff --git a/tests/acls/acl_policy_1.hujson b/tests/acls/acl_policy_1.hujson index 3e12bf48..c9881d82 100644 --- a/tests/acls/acl_policy_1.hujson +++ b/tests/acls/acl_policy_1.hujson @@ -5,6 +5,10 @@ "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": { @@ -33,8 +37,8 @@ { "Action": "accept", "Users": [ - "group:engineering", - "president@example.com" + "group:example2", + "192.168.1.1" ], "Ports": [ "*:22,3389", @@ -47,7 +51,7 @@ { "Action": "accept", "Users": [ - "group:engineers" + "group:example" ], "Ports": [ "tag:production:*" @@ -58,11 +62,11 @@ { "Action": "accept", "Users": [ - "my-subnet", + "example-host-2", "192.168.1.0/24" ], "Ports": [ - "my-subnet:*", + "example-host-1:*", "192.168.1.0/24:*" ], }, @@ -83,7 +87,7 @@ { "Action": "accept", "Users": [ - "group:montreal-users" + "example-host-1" ], "Ports": [ "tag:montreal-webserver:80,443" diff --git a/tests/acls/acl_policy_invalid.hujson b/tests/acls/acl_policy_invalid.hujson new file mode 100644 index 00000000..ad640dfe --- /dev/null +++ b/tests/acls/acl_policy_invalid.hujson @@ -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" + ], + }, + ], +} \ No newline at end of file From 136aab9dc8f3a1ca9d8a1b4be8fd46f60a927f5a Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sat, 3 Jul 2021 17:31:32 +0200 Subject: [PATCH 04/15] Work in progress in rule generation --- acls.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++--- acls_test.go | 60 ++++++++++++++++++++++++------ acls_types.go | 44 ++++++++++++---------- app.go | 2 + 4 files changed, 169 insertions(+), 38 deletions(-) diff --git a/acls.go b/acls.go index d698e269..414be395 100644 --- a/acls.go +++ b/acls.go @@ -1,30 +1,119 @@ package headscale import ( + "fmt" "io" "os" + "strings" "github.com/tailscale/hujson" + "inet.af/netaddr" + "tailscale.com/tailcfg" ) -const errorInvalidPolicy = Error("invalid policy") +const errorEmptyPolicy = Error("empty policy") +const errorInvalidAction = Error("invalid action") +const errorInvalidUserSection = Error("invalid user section") +const errorInvalidGroup = Error("invalid group") -func (h *Headscale) ParsePolicy(path string) (*ACLPolicy, error) { +func (h *Headscale) LoadPolicy(path string) error { policyFile, err := os.Open(path) if err != nil { - return nil, err + return err } defer policyFile.Close() var policy ACLPolicy b, err := io.ReadAll(policyFile) if err != nil { - return nil, err + return err } err = hujson.Unmarshal(b, &policy) if policy.IsZero() { - return nil, errorInvalidPolicy + return errorEmptyPolicy } - return &policy, err + h.aclPolicy = &policy + return err +} + +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 { + fmt.Printf("acl %d, user %d: ", i, j) + srcs, err := h.generateAclPolicySrcIP(u) + fmt.Printf(" -> %s\n", err) + if err != nil { + return nil, err + } + srcIPs = append(srcIPs, *srcs...) + } + r.SrcIPs = srcIPs + + } + + return &rules, nil +} + +func (h *Headscale) generateAclPolicySrcIP(u string) (*[]string, error) { + if u == "*" { + fmt.Printf("%s -> wildcard", u) + return &[]string{"*"}, nil + } + + if strings.HasPrefix(u, "group:") { + fmt.Printf("%s -> group", u) + if _, ok := h.aclPolicy.Groups[u]; !ok { + return nil, errorInvalidGroup + } + return nil, nil + } + + if strings.HasPrefix(u, "tag:") { + fmt.Printf("%s -> tag", u) + return nil, nil + } + + n, err := h.GetNamespace(u) + if err == nil { + fmt.Printf("%s -> namespace %s", u, n.Name) + 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[u]; ok { + fmt.Printf("%s -> host %s", u, h) + return &[]string{h.String()}, nil + } + + ip, err := netaddr.ParseIP(u) + if err == nil { + fmt.Printf(" %s -> ip %s", u, ip) + return &[]string{ip.String()}, nil + } + + cidr, err := netaddr.ParseIPPrefix(u) + if err == nil { + fmt.Printf("%s -> cidr %s", u, cidr) + return &[]string{cidr.String()}, nil + } + + fmt.Printf("%s: cannot be mapped to anything\n", u) + return nil, errorInvalidUserSection } diff --git a/acls_test.go b/acls_test.go index 6b01242c..fe779322 100644 --- a/acls_test.go +++ b/acls_test.go @@ -5,29 +5,65 @@ import ( ) func (s *Suite) TestWrongPath(c *check.C) { - _, err := h.ParsePolicy("asdfg") + err := h.LoadPolicy("asdfg") c.Assert(err, check.NotNil) } func (s *Suite) TestBrokenHuJson(c *check.C) { - _, err := h.ParsePolicy("./tests/acls/broken.hujson") + err := h.LoadPolicy("./tests/acls/broken.hujson") c.Assert(err, check.NotNil) } func (s *Suite) TestInvalidPolicyHuson(c *check.C) { - _, err := h.ParsePolicy("./tests/acls/invalid.hujson") + err := h.LoadPolicy("./tests/acls/invalid.hujson") c.Assert(err, check.NotNil) - c.Assert(err, check.Equals, errorInvalidPolicy) + c.Assert(err, check.Equals, errorEmptyPolicy) } -func (s *Suite) TestValidCheckHosts(c *check.C) { - p, err := h.ParsePolicy("./tests/acls/acl_policy_1.hujson") +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) - c.Assert(p, check.NotNil) - c.Assert(p.IsZero(), check.Equals, false) - - hosts, err := p.GetHosts() - c.Assert(err, check.IsNil) - c.Assert(*hosts, check.HasLen, 2) +} + +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) TestCheckLoaded(c *check.C) { + err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") + c.Assert(err, check.IsNil) + c.Assert(h.aclPolicy, check.NotNil) +} + +func (s *Suite) TestValidCheckParsedHosts(c *check.C) { + err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") + c.Assert(err, check.IsNil) + c.Assert(h.aclPolicy, check.NotNil) + c.Assert(h.aclPolicy.IsZero(), check.Equals, false) + c.Assert(h.aclPolicy.Hosts, check.HasLen, 2) +} + +func (s *Suite) TestRuleInvalidGeneration(c *check.C) { + err := h.LoadPolicy("./tests/acls/acl_policy_invalid.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.NotNil) + c.Assert(rules, check.IsNil) +} + +func (s *Suite) TestRuleGeneration(c *check.C) { + err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") + c.Assert(err, check.IsNil) + + rules, err := h.generateACLRules() + c.Assert(err, check.IsNil) + c.Assert(rules, check.NotNil) + } diff --git a/acls_types.go b/acls_types.go index 08b383cc..d385362f 100644 --- a/acls_types.go +++ b/acls_types.go @@ -3,6 +3,7 @@ package headscale import ( "strings" + "github.com/tailscale/hujson" "inet.af/netaddr" ) @@ -22,12 +23,9 @@ type ACL struct { type Groups map[string][]string -type Hosts map[string]string +type Hosts map[string]netaddr.IPPrefix -type TagOwners struct { - TagMontrealWebserver []string `json:"tag:montreal-webserver"` - TagAPIServer []string `json:"tag:api-server"` -} +type TagOwners map[string][]string type ACLTest struct { User string `json:"User"` @@ -35,6 +33,27 @@ type ACLTest struct { Deny []string `json:"Deny,omitempty"` } +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 { @@ -42,18 +61,3 @@ func (p ACLPolicy) IsZero() bool { } return false } - -func (p ACLPolicy) GetHosts() (*map[string]netaddr.IPPrefix, error) { - hosts := make(map[string]netaddr.IPPrefix) - for k, v := range p.Hosts { - if !strings.Contains(v, "/") { - v = v + "/32" - } - prefix, err := netaddr.ParseIPPrefix(v) - if err != nil { - return nil, err - } - hosts[k] = prefix - } - return &hosts, nil -} diff --git a/app.go b/app.go index 0cdc3105..4775c6ec 100644 --- a/app.go +++ b/app.go @@ -49,6 +49,8 @@ type Headscale struct { publicKey *wgkey.Key privateKey *wgkey.Private + aclPolicy *ACLPolicy + pollMu sync.Mutex clientsPolling map[uint64]chan []byte // this is by all means a hackity hack } From 07e95393b30c9bd43212fdd4fa57b166f48b8401 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 12:35:18 +0200 Subject: [PATCH 05/15] Rule generation kinda working, missing tests --- acls.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++----- acls_test.go | 17 +++-- 2 files changed, 185 insertions(+), 22 deletions(-) diff --git a/acls.go b/acls.go index 414be395..8ba86186 100644 --- a/acls.go +++ b/acls.go @@ -1,11 +1,15 @@ package headscale import ( + "encoding/json" "fmt" "io" + "log" "os" + "strconv" "strings" + "github.com/davecgh/go-spew/spew" "github.com/tailscale/hujson" "inet.af/netaddr" "tailscale.com/tailcfg" @@ -15,6 +19,9 @@ 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") func (h *Headscale) LoadPolicy(path string) error { policyFile, err := os.Open(path) @@ -59,33 +66,143 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { } r.SrcIPs = srcIPs + destPorts := []tailcfg.NetPortRange{} + for j, d := range a.Ports { + fmt.Printf("acl %d, port %d: ", i, j) + dests, err := h.generateAclPolicyDestPorts(d) + fmt.Printf(" -> %s\n", err) + if err != nil { + return nil, err + } + destPorts = append(destPorts, *dests...) + } + + rules = append(rules, tailcfg.FilterRule{ + SrcIPs: srcIPs, + DstPorts: destPorts, + }) } + // fmt.Println(rules) + spew.Dump(rules) return &rules, nil } func (h *Headscale) generateAclPolicySrcIP(u string) (*[]string, error) { - if u == "*" { - fmt.Printf("%s -> wildcard", u) + 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 == "*" { + fmt.Printf("%s -> wildcard", s) return &[]string{"*"}, nil } - if strings.HasPrefix(u, "group:") { - fmt.Printf("%s -> group", u) - if _, ok := h.aclPolicy.Groups[u]; !ok { + if strings.HasPrefix(s, "group:") { + fmt.Printf("%s -> group", s) + if _, ok := h.aclPolicy.Groups[s]; !ok { return nil, errorInvalidGroup } - return nil, nil + 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(u, "tag:") { - fmt.Printf("%s -> tag", u) - return nil, nil + if strings.HasPrefix(s, "tag:") { + fmt.Printf("%s -> tag", s) + 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 + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return nil, err + } + machines := []Machine{} + if err = db.Where("registered").Find(&machines).Error; err != nil { + log.Printf("Error accessing db: %s", err) + 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(u) + n, err := h.GetNamespace(s) if err == nil { - fmt.Printf("%s -> namespace %s", u, n.Name) + fmt.Printf("%s -> namespace %s", s, n.Name) nodes, err := h.ListMachinesInNamespace(n.Name) if err != nil { return nil, err @@ -97,23 +214,60 @@ func (h *Headscale) generateAclPolicySrcIP(u string) (*[]string, error) { return &ips, nil } - if h, ok := h.aclPolicy.Hosts[u]; ok { - fmt.Printf("%s -> host %s", u, h) + if h, ok := h.aclPolicy.Hosts[s]; ok { + fmt.Printf("%s -> host %s", s, h) return &[]string{h.String()}, nil } - ip, err := netaddr.ParseIP(u) + ip, err := netaddr.ParseIP(s) if err == nil { - fmt.Printf(" %s -> ip %s", u, ip) + fmt.Printf(" %s -> ip %s", s, ip) return &[]string{ip.String()}, nil } - cidr, err := netaddr.ParseIPPrefix(u) + cidr, err := netaddr.ParseIPPrefix(s) if err == nil { - fmt.Printf("%s -> cidr %s", u, cidr) + fmt.Printf("%s -> cidr %s", s, cidr) return &[]string{cidr.String()}, nil } - fmt.Printf("%s: cannot be mapped to anything\n", u) + fmt.Printf("%s: cannot be mapped to anything\n", s) 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 +} diff --git a/acls_test.go b/acls_test.go index fe779322..97f0d334 100644 --- a/acls_test.go +++ b/acls_test.go @@ -58,12 +58,21 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) { c.Assert(rules, check.IsNil) } -func (s *Suite) TestRuleGeneration(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") +func (s *Suite) TestBasicRule(c *check.C) { + err := h.LoadPolicy("./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) - + c.Assert(rules, check.IsNil) } + +// func (s *Suite) TestRuleGeneration(c *check.C) { +// err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") +// c.Assert(err, check.IsNil) + +// rules, err := h.generateACLRules() +// c.Assert(err, check.IsNil) +// c.Assert(rules, check.NotNil) + +// } From d0e970f21dd0935304988764613f928e26e8a4fa Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:01:41 +0200 Subject: [PATCH 06/15] Added more unit tests --- acls.go | 3 - acls_test.go | 77 ++++++++++++++++++- tests/acls/acl_policy_1.hujson | 12 ++- tests/acls/acl_policy_basic_1.hujson | 24 ++++++ .../acl_policy_basic_namespace_as_user.hujson | 20 +++++ tests/acls/acl_policy_basic_range.hujson | 20 +++++ tests/acls/acl_policy_basic_wildcards.hujson | 20 +++++ 7 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 tests/acls/acl_policy_basic_1.hujson create mode 100644 tests/acls/acl_policy_basic_namespace_as_user.hujson create mode 100644 tests/acls/acl_policy_basic_range.hujson create mode 100644 tests/acls/acl_policy_basic_wildcards.hujson diff --git a/acls.go b/acls.go index 8ba86186..151fb3b6 100644 --- a/acls.go +++ b/acls.go @@ -9,7 +9,6 @@ import ( "strconv" "strings" - "github.com/davecgh/go-spew/spew" "github.com/tailscale/hujson" "inet.af/netaddr" "tailscale.com/tailcfg" @@ -82,8 +81,6 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { DstPorts: destPorts, }) } - // fmt.Println(rules) - spew.Dump(rules) return &rules, nil } diff --git a/acls_test.go b/acls_test.go index 97f0d334..34338483 100644 --- a/acls_test.go +++ b/acls_test.go @@ -64,7 +64,82 @@ func (s *Suite) TestBasicRule(c *check.C) { rules, err := h.generateACLRules() c.Assert(err, check.IsNil) - c.Assert(rules, check.IsNil) + c.Assert(rules, check.NotNil) +} + +func (s *Suite) TestPortRange(c *check.C) { + err := h.LoadPolicy("./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.LoadPolicy("./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) + + db, err := h.db() + if err != nil { + c.Fatal(err) + } + + _, 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), + } + db.Save(&m) + + err = h.LoadPolicy("./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) TestRuleGeneration(c *check.C) { diff --git a/tests/acls/acl_policy_1.hujson b/tests/acls/acl_policy_1.hujson index c9881d82..8f70148b 100644 --- a/tests/acls/acl_policy_1.hujson +++ b/tests/acls/acl_policy_1.hujson @@ -20,12 +20,11 @@ // 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", + "group:example", ], // Only a few admins are allowed to create API servers. - "tag:api-server": [ - "group:global-admins", + "tag:production": [ + "group:example", "president@example.com", ], }, @@ -38,7 +37,7 @@ "Action": "accept", "Users": [ "group:example2", - "192.168.1.1" + "192.168.1.0/24" ], "Ports": [ "*:22,3389", @@ -62,8 +61,7 @@ { "Action": "accept", "Users": [ - "example-host-2", - "192.168.1.0/24" + "example-host-2", ], "Ports": [ "example-host-1:*", diff --git a/tests/acls/acl_policy_basic_1.hujson b/tests/acls/acl_policy_basic_1.hujson new file mode 100644 index 00000000..4f86af3d --- /dev/null +++ b/tests/acls/acl_policy_basic_1.hujson @@ -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:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_namespace_as_user.hujson b/tests/acls/acl_policy_basic_namespace_as_user.hujson new file mode 100644 index 00000000..414bdda4 --- /dev/null +++ b/tests/acls/acl_policy_basic_namespace_as_user.hujson @@ -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": [ + "testnamespace", + ], + "Ports": [ + "host-1:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_range.hujson b/tests/acls/acl_policy_basic_range.hujson new file mode 100644 index 00000000..8bcbc798 --- /dev/null +++ b/tests/acls/acl_policy_basic_range.hujson @@ -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", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_wildcards.hujson b/tests/acls/acl_policy_basic_wildcards.hujson new file mode 100644 index 00000000..ec5ce468 --- /dev/null +++ b/tests/acls/acl_policy_basic_wildcards.hujson @@ -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:*", + ], + }, + ], +} \ No newline at end of file From bd86975d1005be4834965d2d1fa20eb12f60bf74 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:10:15 +0200 Subject: [PATCH 07/15] Added missing go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5a9ced26..9e88a5fa 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.7 // indirect github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.8.1 - github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 // indirect + github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 From 401e6aec328b840ffb399a683cd55597ed6e9722 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:23:31 +0200 Subject: [PATCH 08/15] And more tests --- acls_test.go | 69 ++++++++++++++----- tests/acls/acl_policy_basic_groups.hujson | 26 +++++++ .../acl_policy_basic_namespace_as_user.hujson | 2 +- 3 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 tests/acls/acl_policy_basic_groups.hujson diff --git a/acls_test.go b/acls_test.go index 34338483..99c2909a 100644 --- a/acls_test.go +++ b/acls_test.go @@ -5,18 +5,18 @@ import ( ) func (s *Suite) TestWrongPath(c *check.C) { - err := h.LoadPolicy("asdfg") + err := h.LoadAclPolicy("asdfg") c.Assert(err, check.NotNil) } func (s *Suite) TestBrokenHuJson(c *check.C) { - err := h.LoadPolicy("./tests/acls/broken.hujson") + err := h.LoadAclPolicy("./tests/acls/broken.hujson") c.Assert(err, check.NotNil) } func (s *Suite) TestInvalidPolicyHuson(c *check.C) { - err := h.LoadPolicy("./tests/acls/invalid.hujson") + err := h.LoadAclPolicy("./tests/acls/invalid.hujson") c.Assert(err, check.NotNil) c.Assert(err, check.Equals, errorEmptyPolicy) } @@ -36,13 +36,13 @@ func (s *Suite) TestParseInvalidCIDR(c *check.C) { } func (s *Suite) TestCheckLoaded(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") + err := h.LoadAclPolicy("./tests/acls/acl_policy_1.hujson") c.Assert(err, check.IsNil) c.Assert(h.aclPolicy, check.NotNil) } func (s *Suite) TestValidCheckParsedHosts(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") + err := h.LoadAclPolicy("./tests/acls/acl_policy_1.hujson") c.Assert(err, check.IsNil) c.Assert(h.aclPolicy, check.NotNil) c.Assert(h.aclPolicy.IsZero(), check.Equals, false) @@ -50,7 +50,7 @@ func (s *Suite) TestValidCheckParsedHosts(c *check.C) { } func (s *Suite) TestRuleInvalidGeneration(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_invalid.hujson") + err := h.LoadAclPolicy("./tests/acls/acl_policy_invalid.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -59,7 +59,7 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) { } func (s *Suite) TestBasicRule(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_basic_1.hujson") + err := h.LoadAclPolicy("./tests/acls/acl_policy_basic_1.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -68,7 +68,7 @@ func (s *Suite) TestBasicRule(c *check.C) { } func (s *Suite) TestPortRange(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_basic_range.hujson") + err := h.LoadAclPolicy("./tests/acls/acl_policy_basic_range.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -82,7 +82,7 @@ func (s *Suite) TestPortRange(c *check.C) { } func (s *Suite) TestPortWildcard(c *check.C) { - err := h.LoadPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") + err := h.LoadAclPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -126,7 +126,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { } db.Save(&m) - err = h.LoadPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") + err = h.LoadAclPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -142,12 +142,47 @@ func (s *Suite) TestPortNamespace(c *check.C) { c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String()) } -// func (s *Suite) TestRuleGeneration(c *check.C) { -// err := h.LoadPolicy("./tests/acls/acl_policy_1.hujson") -// c.Assert(err, check.IsNil) +func (s *Suite) TestPortGroup(c *check.C) { + n, err := h.CreateNamespace("testnamespace") + c.Assert(err, check.IsNil) -// rules, err := h.generateACLRules() -// c.Assert(err, check.IsNil) -// c.Assert(rules, check.NotNil) + pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) + c.Assert(err, check.IsNil) -// } + db, err := h.db() + if err != nil { + c.Fatal(err) + } + + _, 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), + } + 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()) +} diff --git a/tests/acls/acl_policy_basic_groups.hujson b/tests/acls/acl_policy_basic_groups.hujson new file mode 100644 index 00000000..ed11a0d0 --- /dev/null +++ b/tests/acls/acl_policy_basic_groups.hujson @@ -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:*", + ], + }, + ], +} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_namespace_as_user.hujson b/tests/acls/acl_policy_basic_namespace_as_user.hujson index 414bdda4..881349fc 100644 --- a/tests/acls/acl_policy_basic_namespace_as_user.hujson +++ b/tests/acls/acl_policy_basic_namespace_as_user.hujson @@ -1,4 +1,4 @@ -// This ACL is used to test wildcards +// This ACL is used to test namespace expansion { "Hosts": { From 202d6b506f3bc1700f0d31c2ac069f256fad61ef Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:24:05 +0200 Subject: [PATCH 09/15] Load ACL policy on headscale startup --- acls.go | 9 +++++++-- api.go | 2 +- app.go | 3 +++ cmd/headscale/cli/utils.go | 7 +++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/acls.go b/acls.go index 151fb3b6..281ea2c4 100644 --- a/acls.go +++ b/acls.go @@ -22,7 +22,7 @@ const errorInvalidTag = Error("invalid tag") const errorInvalidNamespace = Error("invalid namespace") const errorInvalidPortFormat = Error("invalid port format") -func (h *Headscale) LoadPolicy(path string) error { +func (h *Headscale) LoadAclPolicy(path string) error { policyFile, err := os.Open(path) if err != nil { return err @@ -40,7 +40,12 @@ func (h *Headscale) LoadPolicy(path string) error { } h.aclPolicy = &policy - return err + rules, err := h.generateACLRules() + if err != nil { + return err + } + h.aclRules = rules + return nil } func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { diff --git a/api.go b/api.go index 92501a4e..ab805ef1 100644 --- a/api.go +++ b/api.go @@ -373,7 +373,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac DNS: []netaddr.IP{}, SearchPaths: []string{}, Domain: "foobar@example.com", - PacketFilter: tailcfg.FilterAllowAll, + PacketFilter: *h.aclRules, DERPMap: h.cfg.DerpMap, UserProfiles: []tailcfg.UserProfile{}, } diff --git a/app.go b/app.go index 4775c6ec..52e72bce 100644 --- a/app.go +++ b/app.go @@ -50,6 +50,7 @@ type Headscale struct { privateKey *wgkey.Private aclPolicy *ACLPolicy + aclRules *[]tailcfg.FilterRule pollMu sync.Mutex clientsPolling map[uint64]chan []byte // this is by all means a hackity hack @@ -84,7 +85,9 @@ func NewHeadscale(cfg Config) (*Headscale, error) { dbString: dbString, privateKey: privKey, publicKey: &pubKey, + aclRules: &tailcfg.FilterAllowAll, // default allowall } + err = h.initDB() if err != nil { return nil, err diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 52a9368e..c606b6d9 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -119,6 +119,13 @@ func getHeadscaleApp() (*headscale.Headscale, error) { if err != nil { 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 } From d446e8a2fb2b1f9939ff39b89872ff264713a440 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:24:27 +0200 Subject: [PATCH 10/15] More stuff in go.sum --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 08d8a172..9453ebe5 100644 --- a/go.sum +++ b/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/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/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/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= From 19443669bf6d2e43b65ebd192b718924dd46164c Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:33:00 +0200 Subject: [PATCH 11/15] Fixed linting issues --- acls.go | 14 +++++++++----- acls_test.go | 22 +++++++++++----------- acls_types.go | 7 +++++++ cmd/headscale/cli/utils.go | 2 +- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/acls.go b/acls.go index 281ea2c4..6cae7e0a 100644 --- a/acls.go +++ b/acls.go @@ -22,7 +22,8 @@ const errorInvalidTag = Error("invalid tag") const errorInvalidNamespace = Error("invalid namespace") const errorInvalidPortFormat = Error("invalid port format") -func (h *Headscale) LoadAclPolicy(path string) error { +// 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 @@ -35,6 +36,9 @@ func (h *Headscale) LoadAclPolicy(path string) error { return err } err = hujson.Unmarshal(b, &policy) + if err != nil { + return err + } if policy.IsZero() { return errorEmptyPolicy } @@ -61,7 +65,7 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { srcIPs := []string{} for j, u := range a.Users { fmt.Printf("acl %d, user %d: ", i, j) - srcs, err := h.generateAclPolicySrcIP(u) + srcs, err := h.generateACLPolicySrcIP(u) fmt.Printf(" -> %s\n", err) if err != nil { return nil, err @@ -73,7 +77,7 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { destPorts := []tailcfg.NetPortRange{} for j, d := range a.Ports { fmt.Printf("acl %d, port %d: ", i, j) - dests, err := h.generateAclPolicyDestPorts(d) + dests, err := h.generateACLPolicyDestPorts(d) fmt.Printf(" -> %s\n", err) if err != nil { return nil, err @@ -90,11 +94,11 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { return &rules, nil } -func (h *Headscale) generateAclPolicySrcIP(u string) (*[]string, error) { +func (h *Headscale) generateACLPolicySrcIP(u string) (*[]string, error) { return h.expandAlias(u) } -func (h *Headscale) generateAclPolicyDestPorts(d string) (*[]tailcfg.NetPortRange, error) { +func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRange, error) { tokens := strings.Split(d, ":") if len(tokens) < 2 || len(tokens) > 3 { return nil, errorInvalidPortFormat diff --git a/acls_test.go b/acls_test.go index 99c2909a..f80349c7 100644 --- a/acls_test.go +++ b/acls_test.go @@ -5,18 +5,18 @@ import ( ) func (s *Suite) TestWrongPath(c *check.C) { - err := h.LoadAclPolicy("asdfg") + err := h.LoadACLPolicy("asdfg") c.Assert(err, check.NotNil) } func (s *Suite) TestBrokenHuJson(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/broken.hujson") + 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") + err := h.LoadACLPolicy("./tests/acls/invalid.hujson") c.Assert(err, check.NotNil) c.Assert(err, check.Equals, errorEmptyPolicy) } @@ -36,13 +36,13 @@ func (s *Suite) TestParseInvalidCIDR(c *check.C) { } func (s *Suite) TestCheckLoaded(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/acl_policy_1.hujson") + err := h.LoadACLPolicy("./tests/acls/acl_policy_1.hujson") c.Assert(err, check.IsNil) c.Assert(h.aclPolicy, check.NotNil) } func (s *Suite) TestValidCheckParsedHosts(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/acl_policy_1.hujson") + err := h.LoadACLPolicy("./tests/acls/acl_policy_1.hujson") c.Assert(err, check.IsNil) c.Assert(h.aclPolicy, check.NotNil) c.Assert(h.aclPolicy.IsZero(), check.Equals, false) @@ -50,7 +50,7 @@ func (s *Suite) TestValidCheckParsedHosts(c *check.C) { } func (s *Suite) TestRuleInvalidGeneration(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/acl_policy_invalid.hujson") + err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -59,7 +59,7 @@ func (s *Suite) TestRuleInvalidGeneration(c *check.C) { } func (s *Suite) TestBasicRule(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/acl_policy_basic_1.hujson") + err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -68,7 +68,7 @@ func (s *Suite) TestBasicRule(c *check.C) { } func (s *Suite) TestPortRange(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/acl_policy_basic_range.hujson") + err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -82,7 +82,7 @@ func (s *Suite) TestPortRange(c *check.C) { } func (s *Suite) TestPortWildcard(c *check.C) { - err := h.LoadAclPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") + err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -126,7 +126,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { } db.Save(&m) - err = h.LoadAclPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") + err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() @@ -171,7 +171,7 @@ func (s *Suite) TestPortGroup(c *check.C) { } db.Save(&m) - err = h.LoadAclPolicy("./tests/acls/acl_policy_basic_groups.hujson") + err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") c.Assert(err, check.IsNil) rules, err := h.generateACLRules() diff --git a/acls_types.go b/acls_types.go index d385362f..01e42d50 100644 --- a/acls_types.go +++ b/acls_types.go @@ -7,6 +7,7 @@ import ( "inet.af/netaddr" ) +// ACLPolicy represents a Tailscale ACL Policy type ACLPolicy struct { Groups Groups `json:"Groups"` Hosts Hosts `json:"Hosts"` @@ -15,24 +16,30 @@ type ACLPolicy struct { 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) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index c606b6d9..a872ec51 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -121,7 +121,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { } // 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"))) + err = h.LoadACLPolicy(absPath(viper.GetString("acl_policy_path"))) if err != nil { log.Printf("Could not load the ACL policy: %s", err) } From a1b8f77b1b93a384323650d9bf94398287cae796 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:40:45 +0200 Subject: [PATCH 12/15] Fixed tests --- acls_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/acls_test.go b/acls_test.go index f80349c7..379b0061 100644 --- a/acls_test.go +++ b/acls_test.go @@ -35,27 +35,9 @@ func (s *Suite) TestParseInvalidCIDR(c *check.C) { c.Assert(err, check.NotNil) } -func (s *Suite) TestCheckLoaded(c *check.C) { - err := h.LoadACLPolicy("./tests/acls/acl_policy_1.hujson") - c.Assert(err, check.IsNil) - c.Assert(h.aclPolicy, check.NotNil) -} - -func (s *Suite) TestValidCheckParsedHosts(c *check.C) { - err := h.LoadACLPolicy("./tests/acls/acl_policy_1.hujson") - c.Assert(err, check.IsNil) - c.Assert(h.aclPolicy, check.NotNil) - c.Assert(h.aclPolicy.IsZero(), check.Equals, false) - c.Assert(h.aclPolicy.Hosts, check.HasLen, 2) -} - func (s *Suite) TestRuleInvalidGeneration(c *check.C) { err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson") - c.Assert(err, check.IsNil) - - rules, err := h.generateACLRules() c.Assert(err, check.NotNil) - c.Assert(rules, check.IsNil) } func (s *Suite) TestBasicRule(c *check.C) { From 315bc6b6771e9e4a407acd83126746175336133b Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:41:38 +0200 Subject: [PATCH 13/15] Added acl path key in example config --- config.json.postgres.example | 3 ++- config.json.sqlite.example | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config.json.postgres.example b/config.json.postgres.example index 7b283c4e..481e1a9b 100644 --- a/config.json.postgres.example +++ b/config.json.postgres.example @@ -14,5 +14,6 @@ "tls_letsencrypt_cache_dir": ".cache", "tls_letsencrypt_challenge_type": "HTTP-01", "tls_cert_path": "", - "tls_key_path": "" + "tls_key_path": "", + "acl_policy_path": "" } diff --git a/config.json.sqlite.example b/config.json.sqlite.example index 787e3e16..0724f59e 100644 --- a/config.json.sqlite.example +++ b/config.json.sqlite.example @@ -10,5 +10,6 @@ "tls_letsencrypt_cache_dir": ".cache", "tls_letsencrypt_challenge_type": "HTTP-01", "tls_cert_path": "", - "tls_key_path": "" + "tls_key_path": "", + "acl_policy_path": "" } From 7590dee1f213b83622a0eaf6bac957ea4cc60ca7 Mon Sep 17 00:00:00 2001 From: Juan Font Date: Sun, 4 Jul 2021 13:47:59 +0200 Subject: [PATCH 14/15] Removed unnecessary prints --- acls.go | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/acls.go b/acls.go index 6cae7e0a..90020a64 100644 --- a/acls.go +++ b/acls.go @@ -64,10 +64,9 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { srcIPs := []string{} for j, u := range a.Users { - fmt.Printf("acl %d, user %d: ", i, j) srcs, err := h.generateACLPolicySrcIP(u) - fmt.Printf(" -> %s\n", err) if err != nil { + log.Printf("Error parsing ACL %d, User %d", i, j) return nil, err } srcIPs = append(srcIPs, *srcs...) @@ -76,10 +75,9 @@ func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) { destPorts := []tailcfg.NetPortRange{} for j, d := range a.Ports { - fmt.Printf("acl %d, port %d: ", i, j) dests, err := h.generateACLPolicyDestPorts(d) - fmt.Printf(" -> %s\n", err) if err != nil { + log.Printf("Error parsing ACL %d, Port %d", i, j) return nil, err } destPorts = append(destPorts, *dests...) @@ -141,12 +139,10 @@ func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRang func (h *Headscale) expandAlias(s string) (*[]string, error) { if s == "*" { - fmt.Printf("%s -> wildcard", s) return &[]string{"*"}, nil } if strings.HasPrefix(s, "group:") { - fmt.Printf("%s -> group", s) if _, ok := h.aclPolicy.Groups[s]; !ok { return nil, errorInvalidGroup } @@ -164,7 +160,6 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) { } if strings.HasPrefix(s, "tag:") { - fmt.Printf("%s -> tag", s) if _, ok := h.aclPolicy.TagOwners[s]; !ok { return nil, errorInvalidTag } @@ -178,7 +173,6 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) { } machines := []Machine{} if err = db.Where("registered").Find(&machines).Error; err != nil { - log.Printf("Error accessing db: %s", err) return nil, err } ips := []string{} @@ -208,7 +202,6 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) { n, err := h.GetNamespace(s) if err == nil { - fmt.Printf("%s -> namespace %s", s, n.Name) nodes, err := h.ListMachinesInNamespace(n.Name) if err != nil { return nil, err @@ -221,23 +214,19 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) { } if h, ok := h.aclPolicy.Hosts[s]; ok { - fmt.Printf("%s -> host %s", s, h) return &[]string{h.String()}, nil } ip, err := netaddr.ParseIP(s) if err == nil { - fmt.Printf(" %s -> ip %s", s, ip) return &[]string{ip.String()}, nil } cidr, err := netaddr.ParseIPPrefix(s) if err == nil { - fmt.Printf("%s -> cidr %s", s, cidr) return &[]string{cidr.String()}, nil } - fmt.Printf("%s: cannot be mapped to anything\n", s) return nil, errorInvalidUserSection } From ecf258f995bab51c008cc900a6ed0058502de2f2 Mon Sep 17 00:00:00 2001 From: Juan Font Alonso Date: Sun, 4 Jul 2021 21:56:13 +0200 Subject: [PATCH 15/15] Use gorm connection pool --- acls.go | 7 +------ acls_test.go | 14 ++------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/acls.go b/acls.go index 90020a64..f4ed4c0e 100644 --- a/acls.go +++ b/acls.go @@ -166,13 +166,8 @@ func (h *Headscale) expandAlias(s string) (*[]string, error) { // This will have HORRIBLE performance. // We need to change the data model to better store tags - db, err := h.db() - if err != nil { - log.Printf("Cannot open DB: %s", err) - return nil, err - } machines := []Machine{} - if err = db.Where("registered").Find(&machines).Error; err != nil { + if err := h.db.Where("registered").Find(&machines).Error; err != nil { return nil, err } ips := []string{} diff --git a/acls_test.go b/acls_test.go index 379b0061..dc5b4b31 100644 --- a/acls_test.go +++ b/acls_test.go @@ -86,11 +86,6 @@ func (s *Suite) TestPortNamespace(c *check.C) { pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) c.Assert(err, check.IsNil) - db, err := h.db() - if err != nil { - c.Fatal(err) - } - _, err = h.GetMachine("testnamespace", "testmachine") c.Assert(err, check.NotNil) ip, _ := h.getAvailableIP() @@ -106,7 +101,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { IPAddress: ip.String(), AuthKeyID: uint(pak.ID), } - db.Save(&m) + h.db.Save(&m) err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson") c.Assert(err, check.IsNil) @@ -131,11 +126,6 @@ func (s *Suite) TestPortGroup(c *check.C) { pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) c.Assert(err, check.IsNil) - db, err := h.db() - if err != nil { - c.Fatal(err) - } - _, err = h.GetMachine("testnamespace", "testmachine") c.Assert(err, check.NotNil) ip, _ := h.getAvailableIP() @@ -151,7 +141,7 @@ func (s *Suite) TestPortGroup(c *check.C) { IPAddress: ip.String(), AuthKeyID: uint(pak.ID), } - db.Save(&m) + h.db.Save(&m) err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") c.Assert(err, check.IsNil)