Skip to content

Commit

Permalink
Merge branch 'main' into topic/renovatebot
Browse files Browse the repository at this point in the history
  • Loading branch information
juanfont authored Feb 22, 2022
2 parents 66a1200 + a37339f commit bb14bcd
Show file tree
Hide file tree
Showing 14 changed files with 1,956 additions and 108 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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**:

- 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
179 changes: 167 additions & 12 deletions README.md

Large diffs are not rendered by default.

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 {
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

0 comments on commit bb14bcd

Please sign in to comment.