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

feat: Add autogroup ACLs #2230

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- Fixed processing of fields in post request in MoveNode rpc [#2179](https://github.com/juanfont/headscale/pull/2179)
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
- Added autogroup ACLs [#2230](https://github.com/juanfont/headscale/pull/2230)

## 0.23.0 (2024-09-18)

Expand Down
175 changes: 166 additions & 9 deletions hscontrol/policy/acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,22 @@ var (
ErrInvalidTag = errors.New("invalid tag")
ErrInvalidPortFormat = errors.New("invalid port format")
ErrWildcardIsNeeded = errors.New("wildcard as port is required for the protocol")
ErrUnknownAutogroup = errors.New("unknown autogroup")
ErrAutogroupSelf = errors.New(`dst "autogroup:self" only works with one src "autogroup:member" or "autogroup:self"`)
)

const (
portRangeBegin = 0
portRangeEnd = 65535
expectedTokenItems = 2

autogroupPrefix = "autogroup:"
autogroupInternet = "autogroup:internet"
autogroupSelf = "autogroup:self"
autogroupMember = "autogroup:member"
autogroupTagged = "autogroup:tagged"
autogroupNonRoot = "autogroup:nonroot"
autogroupDangerAll = "autogroup:danger-all"
)

var theInternetSet *netipx.IPSet
Expand Down Expand Up @@ -68,6 +78,22 @@ func theInternet() *netipx.IPSet {
return theInternetSet
}

var allIPSet *netipx.IPSet

func allIPs() *netipx.IPSet {
if allIPSet != nil {
return allIPSet
}

var build netipx.IPSetBuilder
build.AddPrefix(netip.MustParsePrefix("::/0"))
build.AddPrefix(netip.MustParsePrefix("0.0.0.0/0"))

allTheIps, _ := build.IPSet()

return allTheIps
}

// For some reason golang.org/x/net/internal/iana is an internal package.
const (
protocolICMP = 1 // Internet Control Message
Expand Down Expand Up @@ -169,13 +195,48 @@ func (pol *ACLPolicy) CompileFilterRules(

var rules []tailcfg.FilterRule

for index, acl := range pol.ACLs {
acls := pol.ACLs
for index := 0; index < len(acls); index++ {
acl := acls[index]
destinations := acl.Destinations

if acl.Action != "accept" {
return nil, ErrInvalidAction
}

var srcIPs []string
for srcIndex, src := range acl.Sources {
if strings.HasPrefix(src, autogroupMember) {
// split all autogroup:self and others
var oldDst []string
var newDst []string

for _, dst := range destinations {
if strings.HasPrefix(dst, autogroupSelf) {
newDst = append(newDst, dst)
} else {
oldDst = append(oldDst, dst)
}
}

switch {
case len(oldDst) == 0:
// all moved to new, only need to change source
src = autogroupSelf
case len(newDst) != 0:
// apart moved to new

destinations = oldDst

splitACL := ACL{
Action: acl.Action,
Sources: []string{autogroupSelf},
Destinations: newDst,
}
acls = append(acls, splitACL)
}
}

srcs, err := pol.expandSource(src, nodes)
if err != nil {
return nil, fmt.Errorf("parsing policy, acl index: %d->%d: %w", index, srcIndex, err)
Expand All @@ -189,12 +250,18 @@ func (pol *ACLPolicy) CompileFilterRules(
}

destPorts := []tailcfg.NetPortRange{}
for _, dest := range acl.Destinations {
for _, dest := range destinations {
alias, port, err := parseDestination(dest)
if err != nil {
return nil, err
}

if strings.HasPrefix(alias, autogroupSelf) {
if len(acl.Sources) != 1 || acl.Sources[0] != autogroupSelf && acl.Sources[0] != autogroupMember {
return nil, ErrAutogroupSelf
}
}

expanded, err := pol.ExpandAlias(
nodes,
alias,
Expand Down Expand Up @@ -309,9 +376,19 @@ func (pol *ACLPolicy) CompileSSHPolicy(
AllowLocalPortForwarding: false,
}

for index, sshACL := range pol.SSHs {
sshs := pol.SSHs
for index := 0; index < len(sshs); index++ {
sshACL := sshs[index]
destinations := sshACL.Destinations

var dest netipx.IPSetBuilder
for _, src := range sshACL.Destinations {
for _, src := range destinations {
if strings.HasPrefix(src, autogroupSelf) {
if len(sshACL.Sources) != 1 || sshACL.Sources[0] != autogroupSelf && sshACL.Sources[0] != autogroupMember {
return nil, ErrAutogroupSelf
}
}

expanded, err := pol.ExpandAlias(append(peers, node), src)
if err != nil {
return nil, err
Expand Down Expand Up @@ -361,6 +438,39 @@ func (pol *ACLPolicy) CompileSSHPolicy(
})
}
} else {
if strings.HasPrefix(rawSrc, autogroupMember) {
// split all autogroup:self and others
var oldDst []string
var newDst []string

for _, dst := range destinations {
if strings.HasPrefix(dst, autogroupSelf) {
newDst = append(newDst, dst)
} else {
oldDst = append(oldDst, dst)
}
}

switch {
case len(oldDst) == 0:
// all moved to new, only need to change source
rawSrc = autogroupSelf
case len(newDst) != 0:
// apart moved to new

destinations = oldDst

splitACL := SSH{
Action: sshACL.Action,
Sources: []string{autogroupSelf},
Destinations: newDst,
Users: sshACL.Users,
CheckPeriod: sshACL.CheckPeriod,
}
sshs = append(sshs, splitACL)
}
}

expandedSrcs, err := pol.ExpandAlias(
peers,
rawSrc,
Expand Down Expand Up @@ -561,7 +671,7 @@ func (pol *ACLPolicy) ExpandAlias(
}

if isAutoGroup(alias) {
return expandAutoGroup(alias)
return pol.expandAutoGroup(alias, nodes)
}

// if alias is a user
Expand Down Expand Up @@ -880,13 +990,60 @@ func (pol *ACLPolicy) expandIPsFromIPPrefix(
return build.IPSet()
}

func expandAutoGroup(alias string) (*netipx.IPSet, error) {
func (pol *ACLPolicy) expandAutoGroup(alias string, nodes types.Nodes) (*netipx.IPSet, error) {
switch {
case strings.HasPrefix(alias, "autogroup:internet"):
case strings.HasPrefix(alias, autogroupInternet):
return theInternet(), nil

case strings.HasPrefix(alias, autogroupSelf):
// all user's devices, not tagged devices
var build netipx.IPSetBuilder
if len(nodes) != 0 {
currentNode := nodes[len(nodes)-1]
for _, node := range nodes {
if node.User.ID == currentNode.User.ID {
node.AppendToIPSet(&build)
}
}
}

return build.IPSet()

case strings.HasPrefix(alias, autogroupMember):
// all users (not tagged devices)
var build netipx.IPSetBuilder

for _, node := range nodes {
if len(node.ForcedTags) != 0 { // auto tag
continue
}
if tags, _ := pol.TagsOfNode(node); len(tags) != 0 { // valid tag manual add by user (tagOwner)
continue
}
node.AppendToIPSet(&build)
}

return build.IPSet()

case strings.HasPrefix(alias, autogroupTagged):
// all tagged devices
var build netipx.IPSetBuilder

for _, node := range nodes {
if len(node.ForcedTags) != 0 { // auto tag
node.AppendToIPSet(&build)
} else if tags, _ := pol.TagsOfNode(node); len(tags) != 0 { // valid tag manual add by user (tagOwner)
node.AppendToIPSet(&build)
}
}

return build.IPSet()

case strings.HasPrefix(alias, autogroupDangerAll):
return allIPs(), nil

default:
return nil, fmt.Errorf("unknown autogroup %q", alias)
return nil, fmt.Errorf("%w: %q", ErrUnknownAutogroup, alias)
}
}

Expand All @@ -903,7 +1060,7 @@ func isTag(str string) bool {
}

func isAutoGroup(str string) bool {
return strings.HasPrefix(str, "autogroup:")
return strings.HasPrefix(str, autogroupPrefix)
}

// TagsOfNode will return the tags of the current node.
Expand Down
Loading