Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements on the ACLs and bug fixing #320

Merged
merged 30 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9b7d657
Return all peers instead of peers in same namespace
madjam002 Dec 29, 2021
e482dfe
feat(machine): add ACLFilter if ACL's are enabled.
restanrm Feb 3, 2022
e9949b4
feat(acls): simplify updating rules
restanrm Feb 3, 2022
fb45138
feat(acls): check acl owners and add bunch of tests
restanrm Feb 5, 2022
97eac3b
feat(acl): update frequently the aclRules
restanrm Feb 6, 2022
de59946
feat(acls): rewrite functions to be testable
restanrm Feb 7, 2022
7b5ba9f
docs(acl): add configuration example to explain acls
restanrm Feb 14, 2022
aceaba6
docs(changelog): bump changelog
restanrm Feb 14, 2022
9cedbba
chore(all): update some files for linter
restanrm Feb 14, 2022
d8c4c31
chore(fmt): apply make fmt command
restanrm Feb 14, 2022
5f642ee
chore(lint): more lint fixing
restanrm Feb 14, 2022
f073d8f
chore(lint): ignore linting on test_expandalias
restanrm Feb 14, 2022
21df798
Merge branch 'main' into feat-improve-acls-usage
kradalby Feb 18, 2022
4f9ece1
Apply suggestions from code review on changelog
restanrm Feb 20, 2022
d00251c
fix(acls,machines): apply code review suggestions
restanrm Feb 20, 2022
5e167cc
fix(tests): fix naming issues related to code review
restanrm Feb 20, 2022
b3d0fb7
fix(machine): revert modifications
restanrm Feb 20, 2022
5242025
fix(machines): renaming following review comments
restanrm Feb 20, 2022
ecb3ee6
Merge branch 'main' into feat-improve-acls-usage
kradalby Feb 21, 2022
960412a
fix(machines): simplify complex if check
restanrm Feb 21, 2022
9c6ce02
fix(machines): use ListAllMachines function
restanrm Feb 21, 2022
f006860
feat(machines): untie dependency with class for filter func
restanrm Feb 21, 2022
5ab6237
tests(machines): test all combinations of peer filtering
restanrm Feb 21, 2022
4bbe005
chore(machines): apply lint
restanrm Feb 21, 2022
25550f8
chore(format): run prettier on repo
restanrm Feb 21, 2022
211fe40
chore(linter): ignore tt var as it's generated code (vscode)
restanrm Feb 21, 2022
50af44b
fix: add error checking in acl and poll
restanrm Feb 21, 2022
baae266
Update acls_test.go
restanrm Feb 21, 2022
650108c
chore(fmt): apply fmt
restanrm Feb 21, 2022
d971f0f
fix(acls_test): fix comment in go code
restanrm Feb 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ linters-settings:
- ip
- ok
- c
- tt

gocritic:
disabled-checks:
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
restanrm marked this conversation as resolved.
Show resolved Hide resolved

- 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**:
Expand Down
233 changes: 174 additions & 59 deletions acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)

Expand Down Expand Up @@ -69,30 +68,41 @@ 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
}

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 {
restanrm marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand All @@ -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)
Expand All @@ -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, ":")
Expand All @@ -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
}
Expand All @@ -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()...)
}
Expand All @@ -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
}

// 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
owners, err := expandTagOwners(aclPolicy, alias)
if err != nil {
return ips, 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
}
}
}
Expand All @@ -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},
Expand Down Expand Up @@ -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
}
Loading