diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml
index e27b6eda..4a73009b 100644
--- a/.github/workflows/contributors.yml
+++ b/.github/workflows/contributors.yml
@@ -4,13 +4,13 @@ on:
push:
branches:
- main
-
+ workflow_dispatch:
jobs:
add-contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - uses: BobAnkh/add-contributors@master
+ - uses: BobAnkh/add-contributors@v0.2.2
with:
CONTRIBUTOR: "## Contributors"
COLUMN_PER_ROW: "6"
diff --git a/.golangci.yaml b/.golangci.yaml
index 965f5496..153cd7c3 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -48,6 +48,7 @@ linters-settings:
- ip
- ok
- c
+ - tt
gocritic:
disabled-checks:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70bda12d..6c04a170 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
**TBD (TBD):**
+**0.14.0 (2022-xx-xx):**
+
+**UPCOMING BREAKING**:
+From the **next** version (`0.15.0`), all machines will be able to communicate regardless of
+if they are in the same namespace. This means that the behaviour currently limited to ACLs
+will become default. From version `0.15.0`, all limitation of communications must be done
+with ACLs.
+
+This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
+
+**BREAKING**:
+
+- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
+ - Namespaces are now treated as Users
+ - All machines can communicate with all machines by default
+ - Tags should now work correctly and adding a host to Headscale should now reload the rules.
+ - The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
+
**0.13.0 (2022-02-18):**
**Features**:
diff --git a/README.md b/README.md
index 1d86380e..e7ba6649 100644
--- a/README.md
+++ b/README.md
@@ -122,13 +122,6 @@ make build
-
-
-
-
- Juan Font
-
- |
@@ -136,6 +129,13 @@ make build
Kristoffer Dalby
|
+
+
+
+
+ Juan Font
+
+ |
@@ -150,6 +150,13 @@ make build
ohdearaugustin
|
+
+
+
+
+ Alessandro (Ale) Segala
+
+ |
@@ -157,6 +164,15 @@ make build
unreality
|
+
+
+
+
+
+
+ Eugen Biegler
+
+ |
@@ -164,8 +180,27 @@ make build
Aaron Bieber
|
-
-
+
+
+
+
+ Fernando De Lucchi
+
+ |
+
+
+
+
+ Hoàng Đức Hiếu
+
+ |
+
+
+
+
+ Michael G.
+
+ |
@@ -173,6 +208,8 @@ make build
Paul Tötterman
|
+
+
@@ -187,6 +224,20 @@ make build
Silver Bullet
|
+
+
+
+
+ Stefan Majer
+
+ |
+
+
+
+
+ lachy-2849
+
+ |
@@ -194,6 +245,29 @@ make build
thomas
|
+
+
+
+
+ Abraham Ingersoll
+
+ |
+
+
+
+
+
+
+ Adrien Raffin-Caboisse
+
+ |
+
+
+
+
+ Artem Klevtsov
+
+ |
@@ -201,6 +275,13 @@ make build
Arthur Woimbée
|
+
+
+
+
+ Bryan Stenson
+
+ |
@@ -208,8 +289,6 @@ make build
Felix Kronlage-Dammers
|
-
-
@@ -217,6 +296,43 @@ make build
Felix Yan
|
+
+
+
+
+
+
+ JJGadgets
+
+ |
+
+
+
+
+ Jim Tittsler
+
+ |
+
+
+
+
+ Pierre Carru
+
+ |
+
+
+
+
+ rcursaru
+
+ |
+
+
+
+
+ Ryan Fowler
+
+ |
@@ -224,6 +340,15 @@ make build
Shaanan Cohney
|
+
+
+
+
+
+
+ Tanner
+
+ |
@@ -252,8 +377,6 @@ make build
Tjerk Woudsma
|
-
-
@@ -261,6 +384,15 @@ make build
Zakhar Bessarab
|
+
+
+
+
+
+
+ ZiYuan
+
+ |
@@ -268,6 +400,13 @@ make build
derelm
|
+
+
+
+
+ e-zk
+
+ |
@@ -275,6 +414,22 @@ make build
ignoramous
|
+
+
+
+
+ lion24
+
+ |
+
+
+
+
+ Wakeful-Cloud
+
+ |
+
+
diff --git a/acls.go b/acls.go
index 326625b9..db2fc588 100644
--- a/acls.go
+++ b/acls.go
@@ -20,7 +20,6 @@ const (
errInvalidUserSection = Error("invalid user section")
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
- errInvalidNamespace = Error("invalid namespace")
errInvalidPortFormat = Error("invalid port format")
)
@@ -69,13 +68,17 @@ func (h *Headscale) LoadACLPolicy(path string) error {
}
h.aclPolicy = &policy
+
+ return h.UpdateACLRules()
+}
+
+func (h *Headscale) UpdateACLRules() error {
rules, err := h.generateACLRules()
if err != nil {
return err
}
- h.aclRules = rules
-
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
+ h.aclRules = rules
return nil
}
@@ -83,16 +86,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{}
+ if h.aclPolicy == nil {
+ return nil, errEmptyPolicy
+ }
+
+ machines, err := h.ListAllMachines()
+ if err != nil {
+ return nil, err
+ }
+
for index, acl := range h.aclPolicy.ACLs {
if acl.Action != "accept" {
return nil, errInvalidAction
}
- filterRule := tailcfg.FilterRule{}
-
srcIPs := []string{}
for innerIndex, user := range acl.Users {
- srcs, err := h.generateACLPolicySrcIP(user)
+ srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
@@ -101,11 +111,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
}
srcIPs = append(srcIPs, srcs...)
}
- filterRule.SrcIPs = srcIPs
destPorts := []tailcfg.NetPortRange{}
for innerIndex, ports := range acl.Ports {
- dests, err := h.generateACLPolicyDestPorts(ports)
+ dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
@@ -124,11 +133,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
return rules, nil
}
-func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
- return h.expandAlias(u)
+func (h *Headscale) generateACLPolicySrcIP(
+ machines []Machine,
+ aclPolicy ACLPolicy,
+ u string,
+) ([]string, error) {
+ return expandAlias(machines, aclPolicy, u)
}
func (h *Headscale) generateACLPolicyDestPorts(
+ machines []Machine,
+ aclPolicy ACLPolicy,
d string,
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":")
@@ -149,11 +164,11 @@ func (h *Headscale) generateACLPolicyDestPorts(
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
- expanded, err := h.expandAlias(alias)
+ expanded, err := expandAlias(machines, aclPolicy, alias)
if err != nil {
return nil, err
}
- ports, err := h.expandPorts(tokens[len(tokens)-1])
+ ports, err := expandPorts(tokens[len(tokens)-1])
if err != nil {
return nil, err
}
@@ -172,21 +187,28 @@ func (h *Headscale) generateACLPolicyDestPorts(
return dests, nil
}
-func (h *Headscale) expandAlias(alias string) ([]string, error) {
+// expandalias has an input of either
+// - a namespace
+// - a group
+// - a tag
+// and transform these in IPAddresses.
+func expandAlias(
+ machines []Machine,
+ aclPolicy ACLPolicy,
+ alias string,
+) ([]string, error) {
+ ips := []string{}
if alias == "*" {
return []string{"*"}, nil
}
if strings.HasPrefix(alias, "group:") {
- if _, ok := h.aclPolicy.Groups[alias]; !ok {
- return nil, errInvalidGroup
+ namespaces, err := expandGroup(aclPolicy, alias)
+ if err != nil {
+ return ips, err
}
- ips := []string{}
- for _, n := range h.aclPolicy.Groups[alias] {
- nodes, err := h.ListMachinesInNamespace(n)
- if err != nil {
- return nil, errInvalidNamespace
- }
+ for _, n := range namespaces {
+ nodes := filterMachinesByNamespace(machines, n)
for _, node := range nodes {
ips = append(ips, node.IPAddresses.ToStringSlice()...)
}
@@ -196,35 +218,23 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
}
if strings.HasPrefix(alias, "tag:") {
- if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
- return nil, errInvalidTag
+ owners, err := expandTagOwners(aclPolicy, alias)
+ if err != nil {
+ return ips, err
}
-
- // This will have HORRIBLE performance.
- // We need to change the data model to better store tags
- machines := []Machine{}
- if err := h.db.Where("registered").Find(&machines).Error; err != nil {
- return nil, err
- }
- ips := []string{}
- for _, machine := range machines {
- hostinfo := tailcfg.Hostinfo{}
- if len(machine.HostInfo) != 0 {
- hi, err := machine.HostInfo.MarshalJSON()
- if err != nil {
- return nil, err
+ for _, namespace := range owners {
+ machines := filterMachinesByNamespace(machines, namespace)
+ for _, machine := range machines {
+ if len(machine.HostInfo) == 0 {
+ continue
}
- err = json.Unmarshal(hi, &hostinfo)
+ hi, err := machine.GetHostInfo()
if err != nil {
- return nil, err
+ return ips, err
}
-
- // FIXME: Check TagOwners allows this
- for _, t := range hostinfo.RequestTags {
- if alias[4:] == t {
+ for _, t := range hi.RequestTags {
+ if alias == t {
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
-
- break
}
}
}
@@ -233,38 +243,82 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
return ips, nil
}
- n, err := h.GetNamespace(alias)
- if err == nil {
- nodes, err := h.ListMachinesInNamespace(n.Name)
- if err != nil {
- return nil, err
- }
- ips := []string{}
- for _, n := range nodes {
- ips = append(ips, n.IPAddresses.ToStringSlice()...)
- }
-
+ // if alias is a namespace
+ nodes := filterMachinesByNamespace(machines, alias)
+ nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
+ if err != nil {
+ return ips, err
+ }
+ for _, n := range nodes {
+ ips = append(ips, n.IPAddresses.ToStringSlice()...)
+ }
+ if len(ips) > 0 {
return ips, nil
}
- if h, ok := h.aclPolicy.Hosts[alias]; ok {
+ // if alias is an host
+ if h, ok := aclPolicy.Hosts[alias]; ok {
return []string{h.String()}, nil
}
+ // if alias is an IP
ip, err := netaddr.ParseIP(alias)
if err == nil {
return []string{ip.String()}, nil
}
+ // if alias is an CIDR
cidr, err := netaddr.ParseIPPrefix(alias)
if err == nil {
return []string{cidr.String()}, nil
}
- return nil, errInvalidUserSection
+ return ips, errInvalidUserSection
}
-func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
+// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
+// that are correctly tagged since they should not be listed as being in the namespace
+// we assume in this function that we only have nodes from 1 namespace.
+func excludeCorrectlyTaggedNodes(
+ aclPolicy ACLPolicy,
+ nodes []Machine,
+ namespace string,
+) ([]Machine, error) {
+ out := []Machine{}
+ tags := []string{}
+ for tag, ns := range aclPolicy.TagOwners {
+ if containsString(ns, namespace) {
+ tags = append(tags, tag)
+ }
+ }
+ // for each machine if tag is in tags list, don't append it.
+ for _, machine := range nodes {
+ if len(machine.HostInfo) == 0 {
+ out = append(out, machine)
+
+ continue
+ }
+ hi, err := machine.GetHostInfo()
+ if err != nil {
+ return out, err
+ }
+ found := false
+ for _, t := range hi.RequestTags {
+ if containsString(tags, t) {
+ found = true
+
+ break
+ }
+ }
+ if !found {
+ out = append(out, machine)
+ }
+ }
+
+ return out, nil
+}
+
+func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
if portsStr == "*" {
return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
@@ -306,3 +360,64 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
return &ports, nil
}
+
+func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
+ out := []Machine{}
+ for _, machine := range machines {
+ if machine.Namespace.Name == namespace {
+ out = append(out, machine)
+ }
+ }
+
+ return out
+}
+
+// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
+// a group cannot be composed of groups.
+func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
+ var owners []string
+ ows, ok := aclPolicy.TagOwners[tag]
+ if !ok {
+ return []string{}, fmt.Errorf(
+ "%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
+ errInvalidTag,
+ tag,
+ )
+ }
+ for _, owner := range ows {
+ if strings.HasPrefix(owner, "group:") {
+ gs, err := expandGroup(aclPolicy, owner)
+ if err != nil {
+ return []string{}, err
+ }
+ owners = append(owners, gs...)
+ } else {
+ owners = append(owners, owner)
+ }
+ }
+
+ return owners, nil
+}
+
+// expandGroup will return the list of namespace inside the group
+// after some validation.
+func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
+ groups, ok := aclPolicy.Groups[group]
+ if !ok {
+ return []string{}, fmt.Errorf(
+ "group %v isn't registered. %w",
+ group,
+ errInvalidGroup,
+ )
+ }
+ for _, g := range groups {
+ if strings.HasPrefix(g, "group:") {
+ return []string{}, fmt.Errorf(
+ "%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
+ errInvalidGroup,
+ )
+ }
+ }
+
+ return groups, nil
+}
diff --git a/acls_test.go b/acls_test.go
index c35f4f84..5534257d 100644
--- a/acls_test.go
+++ b/acls_test.go
@@ -1,7 +1,14 @@
package headscale
import (
+ "errors"
+ "reflect"
+ "testing"
+
"gopkg.in/check.v1"
+ "gorm.io/datatypes"
+ "inet.af/netaddr"
+ "tailscale.com/tailcfg"
)
func (s *Suite) TestWrongPath(c *check.C) {
@@ -52,6 +59,245 @@ func (s *Suite) TestBasicRule(c *check.C) {
c.Assert(rules, check.NotNil)
}
+// TODO(kradalby): Make tests values safe, independent and descriptive.
+func (s *Suite) TestInvalidAction(c *check.C) {
+ app.aclPolicy = &ACLPolicy{
+ ACLs: []ACL{
+ {Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}},
+ },
+ }
+ err := app.UpdateACLRules()
+ c.Assert(errors.Is(err, errInvalidAction), check.Equals, true)
+}
+
+func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
+ // this ACL is wrong because the group in users sections doesn't exist
+ app.aclPolicy = &ACLPolicy{
+ Groups: Groups{
+ "group:test": []string{"foo"},
+ "group:error": []string{"foo", "group:test"},
+ },
+ ACLs: []ACL{
+ {Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}},
+ },
+ }
+ err := app.UpdateACLRules()
+ c.Assert(errors.Is(err, errInvalidGroup), check.Equals, true)
+}
+
+func (s *Suite) TestInvalidTagOwners(c *check.C) {
+ // this ACL is wrong because no tagOwners own the requested tag for the server
+ app.aclPolicy = &ACLPolicy{
+ ACLs: []ACL{
+ {Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}},
+ },
+ }
+ err := app.UpdateACLRules()
+ c.Assert(errors.Is(err, errInvalidTag), check.Equals, true)
+}
+
+// this test should validate that we can expand a group in a TagOWner section and
+// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
+// the tag is matched in the Users section.
+func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
+ namespace, err := app.CreateNamespace("user1")
+ c.Assert(err, check.IsNil)
+
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+ c.Assert(err, check.IsNil)
+
+ _, err = app.GetMachine("user1", "testmachine")
+ c.Assert(err, check.NotNil)
+ hostInfo := []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
+ )
+ machine := Machine{
+ ID: 0,
+ MachineKey: "foo",
+ NodeKey: "bar",
+ DiscoKey: "faa",
+ Name: "testmachine",
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
+ NamespaceID: namespace.ID,
+ Registered: true,
+ RegisterMethod: RegisterMethodAuthKey,
+ AuthKeyID: uint(pak.ID),
+ HostInfo: datatypes.JSON(hostInfo),
+ }
+ app.db.Save(&machine)
+
+ app.aclPolicy = &ACLPolicy{
+ Groups: Groups{"group:test": []string{"user1", "user2"}},
+ TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
+ ACLs: []ACL{
+ {Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}},
+ },
+ }
+ err = app.UpdateACLRules()
+ c.Assert(err, check.IsNil)
+ c.Assert(app.aclRules, check.HasLen, 1)
+ c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
+ c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1")
+}
+
+// this test should validate that we can expand a group in a TagOWner section and
+// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
+// the tag is matched in the Ports section.
+func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
+ namespace, err := app.CreateNamespace("user1")
+ c.Assert(err, check.IsNil)
+
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+ c.Assert(err, check.IsNil)
+
+ _, err = app.GetMachine("user1", "testmachine")
+ c.Assert(err, check.NotNil)
+ hostInfo := []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
+ )
+ machine := Machine{
+ ID: 1,
+ MachineKey: "12345",
+ NodeKey: "bar",
+ DiscoKey: "faa",
+ Name: "testmachine",
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
+ NamespaceID: namespace.ID,
+ Registered: true,
+ RegisterMethod: RegisterMethodAuthKey,
+ AuthKeyID: uint(pak.ID),
+ HostInfo: datatypes.JSON(hostInfo),
+ }
+ app.db.Save(&machine)
+
+ app.aclPolicy = &ACLPolicy{
+ Groups: Groups{"group:test": []string{"user1", "user2"}},
+ TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
+ ACLs: []ACL{
+ {Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}},
+ },
+ }
+ err = app.UpdateACLRules()
+ c.Assert(err, check.IsNil)
+ c.Assert(app.aclRules, check.HasLen, 1)
+ c.Assert(app.aclRules[0].DstPorts, check.HasLen, 1)
+ c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1")
+}
+
+// need a test with:
+// tag on a host that isn't owned by a tag owners. So the namespace
+// of the host should be valid.
+func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
+ namespace, err := app.CreateNamespace("user1")
+ c.Assert(err, check.IsNil)
+
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+ c.Assert(err, check.IsNil)
+
+ _, err = app.GetMachine("user1", "testmachine")
+ c.Assert(err, check.NotNil)
+ hostInfo := []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:foo\"]}",
+ )
+ machine := Machine{
+ ID: 1,
+ MachineKey: "12345",
+ NodeKey: "bar",
+ DiscoKey: "faa",
+ Name: "testmachine",
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
+ NamespaceID: namespace.ID,
+ Registered: true,
+ RegisterMethod: RegisterMethodAuthKey,
+ AuthKeyID: uint(pak.ID),
+ HostInfo: datatypes.JSON(hostInfo),
+ }
+ app.db.Save(&machine)
+
+ app.aclPolicy = &ACLPolicy{
+ TagOwners: TagOwners{"tag:test": []string{"user1"}},
+ ACLs: []ACL{
+ {Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}},
+ },
+ }
+ err = app.UpdateACLRules()
+ c.Assert(err, check.IsNil)
+ c.Assert(app.aclRules, check.HasLen, 1)
+ c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
+ c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1")
+}
+
+// tag on a host is owned by a tag owner, the tag is valid.
+// an ACL rule is matching the tag to a namespace. It should not be valid since the
+// host should be tied to the tag now.
+func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
+ namespace, err := app.CreateNamespace("user1")
+ c.Assert(err, check.IsNil)
+
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+ c.Assert(err, check.IsNil)
+
+ _, err = app.GetMachine("user1", "webserver")
+ c.Assert(err, check.NotNil)
+ hostInfo := []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"webserver\",\"RequestTags\":[\"tag:webapp\"]}",
+ )
+ machine := Machine{
+ ID: 1,
+ MachineKey: "12345",
+ NodeKey: "bar",
+ DiscoKey: "faa",
+ Name: "webserver",
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
+ NamespaceID: namespace.ID,
+ Registered: true,
+ RegisterMethod: RegisterMethodAuthKey,
+ AuthKeyID: uint(pak.ID),
+ HostInfo: datatypes.JSON(hostInfo),
+ }
+ app.db.Save(&machine)
+ _, err = app.GetMachine("user1", "user")
+ hostInfo = []byte("{\"OS\":\"debian\",\"Hostname\":\"user\"}")
+ c.Assert(err, check.NotNil)
+ machine = Machine{
+ ID: 2,
+ MachineKey: "56789",
+ NodeKey: "bar2",
+ DiscoKey: "faab",
+ Name: "user",
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
+ NamespaceID: namespace.ID,
+ Registered: true,
+ RegisterMethod: RegisterMethodAuthKey,
+ AuthKeyID: uint(pak.ID),
+ HostInfo: datatypes.JSON(hostInfo),
+ }
+ app.db.Save(&machine)
+
+ app.aclPolicy = &ACLPolicy{
+ TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
+ ACLs: []ACL{
+ {
+ Action: "accept",
+ Users: []string{"user1"},
+ Ports: []string{"tag:webapp:80,443"},
+ },
+ },
+ }
+ err = app.UpdateACLRules()
+ c.Assert(err, check.IsNil)
+ c.Assert(app.aclRules, check.HasLen, 1)
+ c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
+ c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.2")
+ c.Assert(app.aclRules[0].DstPorts, check.HasLen, 2)
+ c.Assert(app.aclRules[0].DstPorts[0].Ports.First, check.Equals, uint16(80))
+ c.Assert(app.aclRules[0].DstPorts[0].Ports.Last, check.Equals, uint16(80))
+ c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1")
+ c.Assert(app.aclRules[0].DstPorts[1].Ports.First, check.Equals, uint16(443))
+ c.Assert(app.aclRules[0].DstPorts[1].Ports.Last, check.Equals, uint16(443))
+ c.Assert(app.aclRules[0].DstPorts[1].IP, check.Equals, "100.64.0.1")
+}
+
func (s *Suite) TestPortRange(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
c.Assert(err, check.IsNil)
@@ -94,7 +340,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
ips, _ := app.getAvailableIPs()
machine := Machine{
ID: 0,
- MachineKey: "foo",
+ MachineKey: "12345",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
@@ -165,3 +411,723 @@ func (s *Suite) TestPortGroup(c *check.C) {
c.Assert(len(ips), check.Equals, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
}
+
+func Test_expandGroup(t *testing.T) {
+ type args struct {
+ aclPolicy ACLPolicy
+ group string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "simple test",
+ args: args{
+ aclPolicy: ACLPolicy{
+ Groups: Groups{
+ "group:test": []string{"user1", "user2", "user3"},
+ "group:foo": []string{"user2", "user3"},
+ },
+ },
+ group: "group:test",
+ },
+ want: []string{"user1", "user2", "user3"},
+ wantErr: false,
+ },
+ {
+ name: "InexistantGroup",
+ args: args{
+ aclPolicy: ACLPolicy{
+ Groups: Groups{
+ "group:test": []string{"user1", "user2", "user3"},
+ "group:foo": []string{"user2", "user3"},
+ },
+ },
+ group: "group:undefined",
+ },
+ want: []string{},
+ wantErr: true,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := expandGroup(test.args.aclPolicy, test.args.group)
+ if (err != nil) != test.wantErr {
+ t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
+
+ return
+ }
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("expandGroup() = %v, want %v", got, test.want)
+ }
+ })
+ }
+}
+
+func Test_expandTagOwners(t *testing.T) {
+ type args struct {
+ aclPolicy ACLPolicy
+ tag string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "simple tag expansion",
+ args: args{
+ aclPolicy: ACLPolicy{
+ TagOwners: TagOwners{"tag:test": []string{"user1"}},
+ },
+ tag: "tag:test",
+ },
+ want: []string{"user1"},
+ wantErr: false,
+ },
+ {
+ name: "expand with tag and group",
+ args: args{
+ aclPolicy: ACLPolicy{
+ Groups: Groups{"group:foo": []string{"user1", "user2"}},
+ TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
+ },
+ tag: "tag:test",
+ },
+ want: []string{"user1", "user2"},
+ wantErr: false,
+ },
+ {
+ name: "expand with namespace and group",
+ args: args{
+ aclPolicy: ACLPolicy{
+ Groups: Groups{"group:foo": []string{"user1", "user2"}},
+ TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
+ },
+ tag: "tag:test",
+ },
+ want: []string{"user1", "user2", "user3"},
+ wantErr: false,
+ },
+ {
+ name: "invalid tag",
+ args: args{
+ aclPolicy: ACLPolicy{
+ TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
+ },
+ tag: "tag:test",
+ },
+ want: []string{},
+ wantErr: true,
+ },
+ {
+ name: "invalid group",
+ args: args{
+ aclPolicy: ACLPolicy{
+ Groups: Groups{"group:bar": []string{"user1", "user2"}},
+ TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
+ },
+ tag: "tag:test",
+ },
+ want: []string{},
+ wantErr: true,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := expandTagOwners(test.args.aclPolicy, test.args.tag)
+ if (err != nil) != test.wantErr {
+ t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
+
+ return
+ }
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("expandTagOwners() = %v, want %v", got, test.want)
+ }
+ })
+ }
+}
+
+func Test_expandPorts(t *testing.T) {
+ type args struct {
+ portsStr string
+ }
+ tests := []struct {
+ name string
+ args args
+ want *[]tailcfg.PortRange
+ wantErr bool
+ }{
+ {
+ name: "wildcard",
+ args: args{portsStr: "*"},
+ want: &[]tailcfg.PortRange{
+ {First: portRangeBegin, Last: portRangeEnd},
+ },
+ wantErr: false,
+ },
+ {
+ name: "two ports",
+ args: args{portsStr: "80,443"},
+ want: &[]tailcfg.PortRange{
+ {First: 80, Last: 80},
+ {First: 443, Last: 443},
+ },
+ wantErr: false,
+ },
+ {
+ name: "a range and a port",
+ args: args{portsStr: "80-1024,443"},
+ want: &[]tailcfg.PortRange{
+ {First: 80, Last: 1024},
+ {First: 443, Last: 443},
+ },
+ wantErr: false,
+ },
+ {
+ name: "out of bounds",
+ args: args{portsStr: "854038"},
+ want: nil,
+ wantErr: true,
+ },
+ {
+ name: "wrong port",
+ args: args{portsStr: "85a38"},
+ want: nil,
+ wantErr: true,
+ },
+ {
+ name: "wrong port in first",
+ args: args{portsStr: "a-80"},
+ want: nil,
+ wantErr: true,
+ },
+ {
+ name: "wrong port in last",
+ args: args{portsStr: "80-85a38"},
+ want: nil,
+ wantErr: true,
+ },
+ {
+ name: "wrong port format",
+ args: args{portsStr: "80-85a38-3"},
+ want: nil,
+ wantErr: true,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := expandPorts(test.args.portsStr)
+ if (err != nil) != test.wantErr {
+ t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
+
+ return
+ }
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("expandPorts() = %v, want %v", got, test.want)
+ }
+ })
+ }
+}
+
+func Test_listMachinesInNamespace(t *testing.T) {
+ type args struct {
+ machines []Machine
+ namespace string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []Machine
+ }{
+ {
+ name: "1 machine in namespace",
+ args: args{
+ machines: []Machine{
+ {Namespace: Namespace{Name: "joe"}},
+ },
+ namespace: "joe",
+ },
+ want: []Machine{
+ {Namespace: Namespace{Name: "joe"}},
+ },
+ },
+ {
+ name: "3 machines, 2 in namespace",
+ args: args{
+ machines: []Machine{
+ {ID: 1, Namespace: Namespace{Name: "joe"}},
+ {ID: 2, Namespace: Namespace{Name: "marc"}},
+ {ID: 3, Namespace: Namespace{Name: "marc"}},
+ },
+ namespace: "marc",
+ },
+ want: []Machine{
+ {ID: 2, Namespace: Namespace{Name: "marc"}},
+ {ID: 3, Namespace: Namespace{Name: "marc"}},
+ },
+ },
+ {
+ name: "5 machines, 0 in namespace",
+ args: args{
+ machines: []Machine{
+ {ID: 1, Namespace: Namespace{Name: "joe"}},
+ {ID: 2, Namespace: Namespace{Name: "marc"}},
+ {ID: 3, Namespace: Namespace{Name: "marc"}},
+ {ID: 4, Namespace: Namespace{Name: "marc"}},
+ {ID: 5, Namespace: Namespace{Name: "marc"}},
+ },
+ namespace: "mickael",
+ },
+ want: []Machine{},
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ if got := filterMachinesByNamespace(test.args.machines, test.args.namespace); !reflect.DeepEqual(
+ got,
+ test.want,
+ ) {
+ t.Errorf("listMachinesInNamespace() = %v, want %v", got, test.want)
+ }
+ })
+ }
+}
+
+// nolint
+func Test_expandAlias(t *testing.T) {
+ type args struct {
+ machines []Machine
+ aclPolicy ACLPolicy
+ alias string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "wildcard",
+ args: args{
+ alias: "*",
+ machines: []Machine{
+ {IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}},
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.78.84.227"),
+ },
+ },
+ },
+ aclPolicy: ACLPolicy{},
+ },
+ want: []string{"*"},
+ wantErr: false,
+ },
+ {
+ name: "simple group",
+ args: args{
+ alias: "group:accountant",
+ machines: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ aclPolicy: ACLPolicy{
+ Groups: Groups{"group:accountant": []string{"joe", "marc"}},
+ },
+ },
+ want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
+ wantErr: false,
+ },
+ {
+ name: "wrong group",
+ args: args{
+ alias: "group:hr",
+ machines: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ aclPolicy: ACLPolicy{
+ Groups: Groups{"group:accountant": []string{"joe", "marc"}},
+ },
+ },
+ want: []string{},
+ wantErr: true,
+ },
+ {
+ name: "simple ipaddress",
+ args: args{
+ alias: "10.0.0.3",
+ machines: []Machine{},
+ aclPolicy: ACLPolicy{},
+ },
+ want: []string{"10.0.0.3"},
+ wantErr: false,
+ },
+ {
+ name: "private network",
+ args: args{
+ alias: "homeNetwork",
+ machines: []Machine{},
+ aclPolicy: ACLPolicy{
+ Hosts: Hosts{
+ "homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
+ },
+ },
+ },
+ want: []string{"192.168.1.0/24"},
+ wantErr: false,
+ },
+ {
+ name: "simple host",
+ args: args{
+ alias: "10.0.0.1",
+ machines: []Machine{},
+ aclPolicy: ACLPolicy{},
+ },
+ want: []string{"10.0.0.1"},
+ wantErr: false,
+ },
+ {
+ name: "simple CIDR",
+ args: args{
+ alias: "10.0.0.0/16",
+ machines: []Machine{},
+ aclPolicy: ACLPolicy{},
+ },
+ want: []string{"10.0.0.0/16"},
+ wantErr: false,
+ },
+ {
+ name: "simple tag",
+ args: args{
+ alias: "tag:hr-webserver",
+ machines: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ aclPolicy: ACLPolicy{
+ TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
+ },
+ },
+ want: []string{"100.64.0.1", "100.64.0.2"},
+ wantErr: false,
+ },
+ {
+ name: "No tag defined",
+ args: args{
+ alias: "tag:hr-webserver",
+ machines: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ aclPolicy: ACLPolicy{
+ Groups: Groups{"group:accountant": []string{"joe", "marc"}},
+ TagOwners: TagOwners{
+ "tag:accountant-webserver": []string{"group:accountant"},
+ },
+ },
+ },
+ want: []string{},
+ wantErr: true,
+ },
+ {
+ name: "list host in namespace without correctly tagged servers",
+ args: args{
+ alias: "joe",
+ machines: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ aclPolicy: ACLPolicy{
+ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
+ },
+ },
+ want: []string{"100.64.0.4"},
+ wantErr: false,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := expandAlias(
+ test.args.machines,
+ test.args.aclPolicy,
+ test.args.alias,
+ )
+ if (err != nil) != test.wantErr {
+ t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
+
+ return
+ }
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("expandAlias() = %v, want %v", got, test.want)
+ }
+ })
+ }
+}
+
+func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
+ type args struct {
+ aclPolicy ACLPolicy
+ nodes []Machine
+ namespace string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []Machine
+ wantErr bool
+ }{
+ {
+ name: "exclude nodes with valid tags",
+ args: args{
+ aclPolicy: ACLPolicy{
+ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
+ },
+ nodes: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ namespace: "joe",
+ },
+ want: []Machine{
+ {
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "all nodes have invalid tags, don't exclude them",
+ args: args{
+ aclPolicy: ACLPolicy{
+ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
+ },
+ nodes: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ namespace: "joe",
+ },
+ want: []Machine{
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ HostInfo: []byte(
+ "{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
+ ),
+ },
+ {
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.4"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ wantErr: false,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := excludeCorrectlyTaggedNodes(
+ test.args.aclPolicy,
+ test.args.nodes,
+ test.args.namespace,
+ )
+ if (err != nil) != test.wantErr {
+ t.Errorf(
+ "excludeCorrectlyTaggedNodes() error = %v, wantErr %v",
+ err,
+ test.wantErr,
+ )
+
+ return
+ }
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
+ }
+ })
+ }
+}
diff --git a/api.go b/api.go
index 020ded01..073be5e2 100644
--- a/api.go
+++ b/api.go
@@ -261,7 +261,16 @@ func (h *Headscale) getMapResponse(
var respBody []byte
if req.Compress == "zstd" {
- src, _ := json.Marshal(resp)
+ src, err := json.Marshal(resp)
+ if err != nil {
+ log.Error().
+ Caller().
+ Str("func", "getMapResponse").
+ Err(err).
+ Msg("Failed to marshal response for the client")
+
+ return nil, err
+ }
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
@@ -290,7 +299,16 @@ func (h *Headscale) getMapKeepAliveResponse(
var respBody []byte
var err error
if mapRequest.Compress == "zstd" {
- src, _ := json.Marshal(mapResponse)
+ src, err := json.Marshal(mapResponse)
+ if err != nil {
+ log.Error().
+ Caller().
+ Str("func", "getMapKeepAliveResponse").
+ Err(err).
+ Msg("Failed to marshal keepalive response for the client")
+
+ return nil, err
+ }
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
diff --git a/dns.go b/dns.go
index 8ecd993c..45e0faeb 100644
--- a/dns.go
+++ b/dns.go
@@ -163,7 +163,15 @@ func getMapResponseDNSConfig(
dnsConfig = dnsConfigOrig.Clone()
dnsConfig.Domains = append(
dnsConfig.Domains,
- fmt.Sprintf("%s.%s", machine.Namespace.Name, baseDomain),
+ fmt.Sprintf(
+ "%s.%s",
+ strings.ReplaceAll(
+ machine.Namespace.Name,
+ "@",
+ ".",
+ ), // Replace @ with . for valid domain for machine
+ baseDomain,
+ ),
)
namespaceSet := set.New(set.ThreadSafe)
@@ -171,8 +179,14 @@ func getMapResponseDNSConfig(
for _, p := range peers {
namespaceSet.Add(p.Namespace)
}
- for _, namespace := range namespaceSet.List() {
- dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
+ for _, ns := range namespaceSet.List() {
+ namespace, ok := ns.(Namespace)
+ if !ok {
+ dnsConfig = dnsConfigOrig
+
+ continue
+ }
+ dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
dnsConfig.Routes[dnsRoute] = nil
}
} else {
diff --git a/docs/README.md b/docs/README.md
index 89c74a79..7a3080e3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -39,6 +39,14 @@ use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
+When using ACL's the Namespace borders are no longer applied. All machines
+whichever the Namespace have the ability to communicate with other hosts as
+long as the ACL's permits this exchange.
+
+The [ACLs](acls.md) document should help understand a fictional case of setting
+up ACLs in a small company. All concepts presented in this document could be
+applied outside of business oriented usage.
+
### Apple devices
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
diff --git a/docs/acls.md b/docs/acls.md
new file mode 100644
index 00000000..63e7c6b8
--- /dev/null
+++ b/docs/acls.md
@@ -0,0 +1,141 @@
+# ACLs use case example
+
+Let's build an example use case for a small business (It may be the place where
+ACL's are the most useful).
+
+We have a small company with a boss, an admin, two developers and an intern.
+
+The boss should have access to all servers but not to the users hosts. Admin
+should also have access to all hosts except that their permissions should be
+limited to maintaining the hosts (for example purposes). The developers can do
+anything they want on dev hosts, but only watch on productions hosts. Intern
+can only interact with the development servers.
+
+Each user have at least a device connected to the network and we have some
+servers.
+
+- database.prod
+- database.dev
+- app-server1.prod
+- app-server1.dev
+- billing.internal
+
+## Setup of the network
+
+Let's create the namespaces. Each user should have his own namespace. The users
+here are represented as namespaces.
+
+```bash
+headscale namespaces create boss
+headscale namespaces create admin1
+headscale namespaces create dev1
+headscale namespaces create dev2
+headscale namespaces create intern1
+```
+
+We don't need to create namespaces for the servers because the servers will be
+tagged. When registering the servers we will need to add the flag
+`--advertised-tags=tag:,tag:`, and the user (namespace) that is
+registering the server should be allowed to do it. Since anyone can add tags to
+a server they can register, the check of the tags is done on headscale server
+and only valid tags are applied. A tag is valid if the namespace that is
+registering it is allowed to do it.
+
+Here are the ACL's to implement the same permissions as above:
+
+```json
+{
+ // groups are collections of users having a common scope. A user can be in multiple groups
+ // groups cannot be composed of groups
+ "groups": {
+ "group:boss": ["boss"],
+ "group:dev": ["dev1", "dev2"],
+ "group:admin": ["admin1"],
+ "group:intern": ["intern1"]
+ },
+ // tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.
+ // This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)
+ // and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)
+ "tagOwners": {
+ // the administrators can add servers in production
+ "tag:prod-databases": ["group:admin"],
+ "tag:prod-app-servers": ["group:admin"],
+
+ // the boss can tag any server as internal
+ "tag:internal": ["group:boss"],
+
+ // dev can add servers for dev purposes as well as admins
+ "tag:dev-databases": ["group:admin", "group:dev"],
+ "tag:dev-app-servers": ["group:admin", "group:dev"]
+
+ // interns cannot add servers
+ },
+ "acls": [
+ // boss have access to all servers
+ {
+ "action": "accept",
+ "users": ["group:boss"],
+ "ports": [
+ "tag:prod-databases:*",
+ "tag:prod-app-servers:*",
+ "tag:internal:*",
+ "tag:dev-databases:*",
+ "tag:dev-app-servers:*"
+ ]
+ },
+
+ // admin have only access to administrative ports of the servers
+ {
+ "action": "accept",
+ "users": ["group:admin"],
+ "ports": [
+ "tag:prod-databases:22",
+ "tag:prod-app-servers:22",
+ "tag:internal:22",
+ "tag:dev-databases:22",
+ "tag:dev-app-servers:22"
+ ]
+ },
+
+ // developers have access to databases servers and application servers on all ports
+ // they can only view the applications servers in prod and have no access to databases servers in production
+ {
+ "action": "accept",
+ "users": ["group:dev"],
+ "ports": [
+ "tag:dev-databases:*",
+ "tag:dev-app-servers:*",
+ "tag:prod-app-servers:80,443"
+ ]
+ },
+
+ // servers should be able to talk to database. Database should not be able to initiate connections to
+ // applications servers
+ {
+ "action": "accept",
+ "users": ["tag:dev-app-servers"],
+ "ports": ["tag:dev-databases:5432"]
+ },
+ {
+ "action": "accept",
+ "users": ["tag:prod-app-servers"],
+ "ports": ["tag:prod-databases:5432"]
+ },
+
+ // interns have access to dev-app-servers only in reading mode
+ {
+ "action": "accept",
+ "users": ["group:intern"],
+ "ports": ["tag:dev-app-servers:80,443"]
+ },
+
+ // We still have to allow internal namespaces communications since nothing guarantees that each user have
+ // their own namespaces.
+ { "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
+ { "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
+ { "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
+ { "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
+ { "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
+ ]
+}
+```
diff --git a/machine.go b/machine.go
index 2b462985..3c704ad3 100644
--- a/machine.go
+++ b/machine.go
@@ -119,6 +119,103 @@ func (machine Machine) isExpired() bool {
return time.Now().UTC().After(*machine.Expiry)
}
+func (h *Headscale) ListAllMachines() ([]Machine, error) {
+ machines := []Machine{}
+ if err := h.db.Preload("AuthKey").
+ Preload("AuthKey.Namespace").
+ Preload("Namespace").
+ Where("registered").
+ Find(&machines).Error; err != nil {
+ return nil, err
+ }
+
+ return machines, nil
+}
+
+func containsAddresses(inputs []string, addrs []string) bool {
+ for _, addr := range addrs {
+ if containsString(inputs, addr) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// matchSourceAndDestinationWithRule.
+func matchSourceAndDestinationWithRule(
+ ruleSources []string,
+ ruleDestinations []string,
+ source []string,
+ destination []string,
+) bool {
+ return containsAddresses(ruleSources, source) &&
+ containsAddresses(ruleDestinations, destination)
+}
+
+// getFilteredByACLPeerss should return the list of peers authorized to be accessed from machine.
+func getFilteredByACLPeers(
+ machines []Machine,
+ rules []tailcfg.FilterRule,
+ machine *Machine,
+) Machines {
+ log.Trace().
+ Caller().
+ Str("machine", machine.Name).
+ Msg("Finding peers filtered by ACLs")
+
+ peers := make(map[uint64]Machine)
+ // Aclfilter peers here. We are itering through machines in all namespaces and search through the computed aclRules
+ // for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
+ for _, peer := range machines {
+ if peer.ID == machine.ID {
+ continue
+ }
+ for _, rule := range rules {
+ var dst []string
+ for _, d := range rule.DstPorts {
+ dst = append(dst, d.IP)
+ }
+ if matchSourceAndDestinationWithRule(
+ rule.SrcIPs,
+ dst,
+ machine.IPAddresses.ToStringSlice(),
+ peer.IPAddresses.ToStringSlice(),
+ ) || // match source and destination
+ matchSourceAndDestinationWithRule(
+ rule.SrcIPs,
+ dst,
+ machine.IPAddresses.ToStringSlice(),
+ []string{"*"},
+ ) || // match source and all destination
+ matchSourceAndDestinationWithRule(
+ rule.SrcIPs,
+ dst,
+ peer.IPAddresses.ToStringSlice(),
+ machine.IPAddresses.ToStringSlice(),
+ ) { // match return path
+ peers[peer.ID] = peer
+ }
+ }
+ }
+
+ authorizedPeers := make([]Machine, 0, len(peers))
+ for _, m := range peers {
+ authorizedPeers = append(authorizedPeers, m)
+ }
+ sort.Slice(
+ authorizedPeers,
+ func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID },
+ )
+
+ log.Trace().
+ Caller().
+ Str("machine", machine.Name).
+ Msgf("Found some machines: %v", machines)
+
+ return authorizedPeers
+}
+
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
log.Trace().
Caller().
@@ -206,39 +303,54 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
}
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
- direct, err := h.getDirectPeers(machine)
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot fetch peers")
+ var peers Machines
+ var err error
- return Machines{}, err
+ // If ACLs rules are defined, filter visible host list with the ACLs
+ // else use the classic namespace scope
+ if h.aclPolicy != nil {
+ var machines []Machine
+ machines, err = h.ListAllMachines()
+ if err != nil {
+ log.Error().Err(err).Msg("Error retrieving list of machines")
+
+ return Machines{}, err
+ }
+ peers = getFilteredByACLPeers(machines, h.aclRules, machine)
+ } else {
+ direct, err := h.getDirectPeers(machine)
+ if err != nil {
+ log.Error().
+ Caller().
+ Err(err).
+ Msg("Cannot fetch peers")
+
+ return Machines{}, err
+ }
+
+ shared, err := h.getShared(machine)
+ if err != nil {
+ log.Error().
+ Caller().
+ Err(err).
+ Msg("Cannot fetch peers")
+
+ return Machines{}, err
+ }
+
+ sharedTo, err := h.getSharedTo(machine)
+ if err != nil {
+ log.Error().
+ Caller().
+ Err(err).
+ Msg("Cannot fetch peers")
+
+ return Machines{}, err
+ }
+ peers = append(direct, shared...)
+ peers = append(peers, sharedTo...)
}
- shared, err := h.getShared(machine)
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot fetch peers")
-
- return Machines{}, err
- }
-
- sharedTo, err := h.getSharedTo(machine)
- if err != nil {
- log.Error().
- Caller().
- Err(err).
- Msg("Cannot fetch peers")
-
- return Machines{}, err
- }
-
- peers := append(direct, shared...)
- peers = append(peers, sharedTo...)
-
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
log.Trace().
@@ -597,7 +709,11 @@ func (machine Machine) toNode(
hostname = fmt.Sprintf(
"%s.%s.%s",
machine.Name,
- machine.Namespace.Name,
+ strings.ReplaceAll(
+ machine.Namespace.Name,
+ "@",
+ ".",
+ ), // Replace @ with . for valid domain for machine
baseDomain,
)
} else {
diff --git a/machine_test.go b/machine_test.go
index ff1dc914..b1cd3419 100644
--- a/machine_test.go
+++ b/machine_test.go
@@ -1,11 +1,15 @@
package headscale
import (
+ "fmt"
+ "reflect"
"strconv"
+ "testing"
"time"
"gopkg.in/check.v1"
"inet.af/netaddr"
+ "tailscale.com/tailcfg"
)
func (s *Suite) TestGetMachine(c *check.C) {
@@ -154,6 +158,89 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
}
+func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
+ type base struct {
+ namespace *Namespace
+ key *PreAuthKey
+ }
+
+ stor := make([]base, 0)
+
+ for _, name := range []string{"test", "admin"} {
+ namespace, err := app.CreateNamespace(name)
+ c.Assert(err, check.IsNil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+ c.Assert(err, check.IsNil)
+ stor = append(stor, base{namespace, pak})
+ }
+
+ _, err := app.GetMachineByID(0)
+ c.Assert(err, check.NotNil)
+
+ for index := 0; index <= 10; index++ {
+ machine := Machine{
+ ID: uint64(index),
+ MachineKey: "foo" + strconv.Itoa(index),
+ NodeKey: "bar" + strconv.Itoa(index),
+ DiscoKey: "faa" + strconv.Itoa(index),
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
+ },
+ Name: "testmachine" + strconv.Itoa(index),
+ NamespaceID: stor[index%2].namespace.ID,
+ Registered: true,
+ RegisterMethod: RegisterMethodAuthKey,
+ AuthKeyID: uint(stor[index%2].key.ID),
+ }
+ app.db.Save(&machine)
+ }
+
+ app.aclPolicy = &ACLPolicy{
+ Groups: map[string][]string{
+ "group:test": {"admin"},
+ },
+ Hosts: map[string]netaddr.IPPrefix{},
+ TagOwners: map[string][]string{},
+ ACLs: []ACL{
+ {Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
+ {Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
+ },
+ Tests: []ACLTest{},
+ }
+
+ err = app.UpdateACLRules()
+ c.Assert(err, check.IsNil)
+
+ adminMachine, err := app.GetMachineByID(1)
+ c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace)
+ c.Assert(err, check.IsNil)
+
+ testMachine, err := app.GetMachineByID(2)
+ c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
+ c.Assert(err, check.IsNil)
+
+ _, err = testMachine.GetHostInfo()
+ c.Assert(err, check.IsNil)
+
+ machines, err := app.ListAllMachines()
+ c.Assert(err, check.IsNil)
+
+ peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
+ peersOfAdminMachine := getFilteredByACLPeers(machines, app.aclRules, adminMachine)
+
+ c.Log(peersOfTestMachine)
+ c.Assert(len(peersOfTestMachine), check.Equals, 4)
+ c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4")
+ c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6")
+ c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10")
+
+ c.Log(peersOfAdminMachine)
+ c.Assert(len(peersOfAdminMachine), check.Equals, 9)
+ c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2")
+ c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4")
+ c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7")
+}
+
func (s *Suite) TestExpireMachine(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
@@ -208,3 +295,178 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
c.Assert(deserialized[i], check.Equals, input[i])
}
}
+
+func Test_getFilteredByACLPeers(t *testing.T) {
+ type args struct {
+ machines []Machine
+ rules []tailcfg.FilterRule
+ machine *Machine
+ }
+ tests := []struct {
+ name string
+ args args
+ want Machines
+ }{
+ {
+ name: "all hosts can talk to each other",
+ args: args{
+ machines: []Machine{ // list of all machines in the database
+ {
+ ID: 1,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ ID: 2,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ ID: 3,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ rules: []tailcfg.FilterRule{ // list of all ACLRules registered
+ {
+ SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
+ DstPorts: []tailcfg.NetPortRange{
+ {IP: "*"},
+ },
+ },
+ },
+ machine: &Machine{ // current machine
+ ID: 1,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ want: Machines{
+ {
+ ID: 2,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ ID: 3,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ },
+ {
+ name: "One host can talk to another, but not all hosts",
+ args: args{
+ machines: []Machine{ // list of all machines in the database
+ {
+ ID: 1,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ ID: 2,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ ID: 3,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ rules: []tailcfg.FilterRule{ // list of all ACLRules registered
+ {
+ SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
+ DstPorts: []tailcfg.NetPortRange{
+ {IP: "100.64.0.2"},
+ },
+ },
+ },
+ machine: &Machine{ // current machine
+ ID: 1,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
+ Namespace: Namespace{Name: "joe"},
+ },
+ },
+ want: Machines{
+ {
+ ID: 2,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
+ Namespace: Namespace{Name: "marc"},
+ },
+ },
+ },
+ {
+ name: "host cannot directly talk to destination, but return path is authorized",
+ args: args{
+ machines: []Machine{ // list of all machines in the database
+ {
+ ID: 1,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.1"),
+ },
+ Namespace: Namespace{Name: "joe"},
+ },
+ {
+ ID: 2,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.2"),
+ },
+ Namespace: Namespace{Name: "marc"},
+ },
+ {
+ ID: 3,
+ IPAddresses: MachineAddresses{
+ netaddr.MustParseIP("100.64.0.3"),
+ },
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ rules: []tailcfg.FilterRule{ // list of all ACLRules registered
+ {
+ SrcIPs: []string{"100.64.0.3"},
+ DstPorts: []tailcfg.NetPortRange{
+ {IP: "100.64.0.2"},
+ },
+ },
+ },
+ machine: &Machine{ // current machine
+ ID: 1,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
+ Namespace: Namespace{Name: "marc"},
+ },
+ },
+ want: Machines{
+ {
+ ID: 3,
+ IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
+ Namespace: Namespace{Name: "mickael"},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := getFilteredByACLPeers(
+ tt.args.machines,
+ tt.args.rules,
+ tt.args.machine,
+ )
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("getFilteredByACLPeers() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/poll.go b/poll.go
index dd3956fb..21aa3b33 100644
--- a/poll.go
+++ b/poll.go
@@ -85,12 +85,26 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
Str("machine", machine.Name).
Msg("Found machine in database")
- hostinfo, _ := json.Marshal(req.Hostinfo)
+ hostinfo, err := json.Marshal(req.Hostinfo)
+ if err != nil {
+ return
+ }
machine.Name = req.Hostinfo.Hostname
machine.HostInfo = datatypes.JSON(hostinfo)
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
now := time.Now().UTC()
+ // update ACLRules with peer informations (to update server tags if necessary)
+ if h.aclPolicy != nil {
+ err = h.UpdateACLRules()
+ if err != nil {
+ log.Error().
+ Caller().
+ Str("func", "handleAuthKey").
+ Str("machine", machine.Name).
+ Err(err)
+ }
+ }
// From Tailscale client:
//
// ReadOnly is whether the client just wants to fetch the MapResponse,
@@ -100,7 +114,17 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
// The intended use is for clients to discover the DERP map at start-up
// before their first real endpoint update.
if !req.ReadOnly {
- endpoints, _ := json.Marshal(req.Endpoints)
+ endpoints, err := json.Marshal(req.Endpoints)
+ if err != nil {
+ log.Error().
+ Caller().
+ Str("func", "PollNetMapHandler").
+ Err(err).
+ Msg("Failed to mashal requested endpoints for the client")
+ ctx.String(http.StatusInternalServerError, ":(")
+
+ return
+ }
machine.Endpoints = datatypes.JSON(endpoints)
machine.LastSeen = &now
}
diff --git a/utils.go b/utils.go
index a6be8cc4..3cee5e35 100644
--- a/utils.go
+++ b/utils.go
@@ -212,6 +212,16 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
return ips, nil
}
+func containsString(ss []string, s string) bool {
+ for _, v := range ss {
+ if v == s {
+ return true
+ }
+ }
+
+ return false
+}
+
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
for _, v := range ips {
if v == ip {
|