Skip to content

Update ACL syntax and add support for protocol filtering #618

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

Merged
merged 14 commits into from
Jun 11, 2022
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.16.0 (2022-xx-xx)

### BREAKING

- Old ACL syntax is no longer supported ("users" & "ports" -> "src" & "dst"). Please check [the new syntax](https://tailscale.com/kb/1018/acls/).

### Changes

- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
Expand All @@ -22,6 +26,7 @@
- This change disables the logs by default
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
- Use new ACL syntax [#618](https://github.com/juanfont/headscale/pull/618)

## 0.15.0 (2022-03-20)

Expand Down
90 changes: 77 additions & 13 deletions acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
errInvalidGroup = Error("invalid group")
errInvalidTag = Error("invalid tag")
errInvalidPortFormat = Error("invalid port format")
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
)

const (
Expand Down Expand Up @@ -123,23 +124,31 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
}

srcIPs := []string{}
for innerIndex, user := range acl.Users {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
for innerIndex, src := range acl.Sources {
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)

return nil, err
}
srcIPs = append(srcIPs, srcs...)
}

protocols, needsWildcard, err := parseProtocol(acl.Protocol)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)

return nil, err
}

destPorts := []tailcfg.NetPortRange{}
for innerIndex, ports := range acl.Ports {
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)

return nil, err
}
Expand All @@ -149,6 +158,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPs,
DstPorts: destPorts,
IPProto: protocols,
})
}

Expand All @@ -158,17 +168,18 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
func (h *Headscale) generateACLPolicySrcIP(
machines []Machine,
aclPolicy ACLPolicy,
u string,
src string,
) ([]string, error) {
return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain)
return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
}

func (h *Headscale) generateACLPolicyDestPorts(
func (h *Headscale) generateACLPolicyDest(
machines []Machine,
aclPolicy ACLPolicy,
d string,
dest string,
needsWildcard bool,
) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":")
tokens := strings.Split(dest, ":")
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
return nil, errInvalidPortFormat
}
Expand All @@ -195,7 +206,7 @@ func (h *Headscale) generateACLPolicyDestPorts(
if err != nil {
return nil, err
}
ports, err := expandPorts(tokens[len(tokens)-1])
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
if err != nil {
return nil, err
}
Expand All @@ -214,6 +225,54 @@ func (h *Headscale) generateACLPolicyDestPorts(
return dests, nil
}

// parseProtocol reads the proto field of the ACL and generates a list of
// protocols that will be allowed, following the IANA IP protocol number
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
//
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
// as per Tailscale behaviour (see tailcfg.FilterRule).
//
// Also returns a boolean indicating if the protocol
// requires all the destinations to use wildcard as port number (only TCP,
// UDP and SCTP support specifying ports).
func parseProtocol(protocol string) ([]int, bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these numbers will trigger "magicnumber" when the linter is up and running, and in any case, it would be better to probably have constants for these

switch protocol {
case "":
return []int{1, 58, 6, 17}, false, nil
case "igmp":
return []int{2}, true, nil
case "ipv4", "ip-in-ip":
return []int{4}, true, nil
case "tcp":
return []int{6}, false, nil
case "egp":
return []int{8}, true, nil
case "igp":
return []int{9}, true, nil
case "udp":
return []int{17}, false, nil
case "gre":
return []int{47}, true, nil
case "esp":
return []int{50}, true, nil
case "ah":
return []int{51}, true, nil
case "sctp":
return []int{132}, false, nil
case "icmp":
return []int{1, 58}, true, nil

default:
protocolNumber, err := strconv.Atoi(protocol)
if err != nil {
return nil, false, err
}
needsWildcard := protocolNumber != 6 && protocolNumber != 17 && protocolNumber != 132 // nolint
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
needsWildcard := protocolNumber != 6 && protocolNumber != 17 && protocolNumber != 132 // nolint
needsWildcard := protocolNumber != 6 && protocolNumber != 17 && protocolNumber != 132

That wont do ;)


return []int{protocolNumber}, needsWildcard, nil
}
}

// expandalias has an input of either
// - a namespace
// - a group
Expand Down Expand Up @@ -268,6 +327,7 @@ func expandAlias(
alias,
)
}

return ips, nil
} else {
return ips, err
Expand Down Expand Up @@ -359,13 +419,17 @@ func excludeCorrectlyTaggedNodes(
return out
}

func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
if portsStr == "*" {
return &[]tailcfg.PortRange{
{First: portRangeBegin, Last: portRangeEnd},
}, nil
}

if needsWildcard {
return nil, errWildcardIsNeeded
}

ports := []tailcfg.PortRange{}
for _, portStr := range strings.Split(portsStr, ",") {
rang := strings.Split(portStr, "-")
Expand Down
Loading