diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2bda3..3c59078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357) - To limit access between nodes, use [ACLs](./docs/acls.md). +### Features + +- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359) + ### Changes - Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346) diff --git a/acls.go b/acls.go index ce14a89..1a597c2 100644 --- a/acls.go +++ b/acls.go @@ -5,11 +5,13 @@ import ( "fmt" "io" "os" + "path/filepath" "strconv" "strings" "github.com/rs/zerolog/log" "github.com/tailscale/hujson" + "gopkg.in/yaml.v3" "inet.af/netaddr" "tailscale.com/tailcfg" ) @@ -53,16 +55,36 @@ func (h *Headscale) LoadACLPolicy(path string) error { 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 + switch filepath.Ext(path) { + case ".yml", ".yaml": + log.Debug(). + Str("path", path). + Bytes("file", policyBytes). + Msg("Loading ACLs from YAML") + + err := yaml.Unmarshal(policyBytes, &policy) + if err != nil { + return err + } + + log.Trace(). + Interface("policy", policy). + Msg("Loaded policy from YAML") + + default: + ast, err := hujson.Parse(policyBytes) + if err != nil { + return err + } + + ast.Standardize() + policyBytes = ast.Pack() + err = json.Unmarshal(policyBytes, &policy) + if err != nil { + return err + } } + if policy.IsZero() { return errEmptyPolicy } diff --git a/acls_test.go b/acls_test.go index edea854..0032243 100644 --- a/acls_test.go +++ b/acls_test.go @@ -323,6 +323,22 @@ func (s *Suite) TestPortWildcard(c *check.C) { c.Assert(rules[0].SrcIPs[0], check.Equals, "*") } +func (s *Suite) TestPortWildcardYAML(c *check.C) { + err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml") + c.Assert(err, check.IsNil) + + rules, err := app.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) { namespace, err := app.CreateNamespace("testnamespace") c.Assert(err, check.IsNil) diff --git a/acls_types.go b/acls_types.go index 08e650f..fb86982 100644 --- a/acls_types.go +++ b/acls_types.go @@ -5,23 +5,24 @@ import ( "strings" "github.com/tailscale/hujson" + "gopkg.in/yaml.v3" "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"` + Groups Groups `json:"Groups" yaml:"Groups"` + Hosts Hosts `json:"Hosts" yaml:"Hosts"` + TagOwners TagOwners `json:"TagOwners" yaml:"TagOwners"` + ACLs []ACL `json:"ACLs" yaml:"ACLs"` + Tests []ACLTest `json:"Tests" yaml:"Tests"` } // ACL is a basic rule for the ACL Policy. type ACL struct { - Action string `json:"Action"` - Users []string `json:"Users"` - Ports []string `json:"Ports"` + Action string `json:"Action" yaml:"Action"` + Users []string `json:"Users" yaml:"Users"` + Ports []string `json:"Ports" yaml:"Ports"` } // Groups references a series of alias in the ACL rules. @@ -35,9 +36,9 @@ 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"` + User string `json:"User" yaml:"User"` + Allow []string `json:"Allow" yaml:"Allow"` + Deny []string `json:"Deny,omitempty" yaml:"Deny,omitempty"` } // UnmarshalJSON allows to parse the Hosts directly into netaddr objects. @@ -69,6 +70,27 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error { return nil } +// UnmarshalYAML allows to parse the Hosts directly into netaddr objects. +func (hosts *Hosts) UnmarshalYAML(data []byte) error { + newHosts := Hosts{} + hostIPPrefixMap := make(map[string]string) + + err := yaml.Unmarshal(data, &hostIPPrefixMap) + if err != nil { + return err + } + for host, prefixStr := range hostIPPrefixMap { + prefix, err := netaddr.ParseIPPrefix(prefixStr) + if err != nil { + return err + } + newHosts[host] = prefix + } + *hosts = newHosts + + return nil +} + // IsZero is perhaps a bit naive here. func (policy ACLPolicy) IsZero() bool { if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 { diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go index e5d9eed..69f61a6 100644 --- a/apple_mobileconfig.go +++ b/apple_mobileconfig.go @@ -4,6 +4,7 @@ import ( "bytes" "html/template" "net/http" + textTemplate "text/template" "github.com/gin-gonic/gin" "github.com/gofrs/uuid" @@ -30,7 +31,7 @@ func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
curl {{.Url}}/apple/ios
curl {{.Url}}/apple/macos
Headscale can be set to the default server by installing a Headscale configuration profile:
@@ -58,7 +59,7 @@ func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
defaults write io.tailscale.ipn.macos ControlURL {{.URL}}
Restart Tailscale.app and log in.
- +