2023-05-21 16:37:59 +00:00
package policy
2021-07-03 09:55:32 +00:00
import (
2021-07-04 10:35:18 +00:00
"encoding/json"
2022-04-15 16:01:13 +00:00
"errors"
2021-07-03 15:31:32 +00:00
"fmt"
2021-07-03 09:55:32 +00:00
"io"
2022-09-03 21:46:14 +00:00
"net/netip"
2021-07-03 09:55:32 +00:00
"os"
2022-02-27 08:04:48 +00:00
"path/filepath"
2021-07-04 10:35:18 +00:00
"strconv"
2021-07-03 15:31:32 +00:00
"strings"
2022-09-30 18:44:23 +00:00
"time"
2021-07-03 09:55:32 +00:00
2023-05-21 16:37:59 +00:00
"github.com/juanfont/headscale/hscontrol/types"
2023-05-11 07:09:18 +00:00
"github.com/juanfont/headscale/hscontrol/util"
2021-08-05 17:18:18 +00:00
"github.com/rs/zerolog/log"
2021-07-03 09:55:32 +00:00
"github.com/tailscale/hujson"
2023-04-28 14:11:02 +00:00
"go4.org/netipx"
2022-02-27 08:04:48 +00:00
"gopkg.in/yaml.v3"
2022-11-24 15:35:55 +00:00
"tailscale.com/envknob"
2021-07-03 15:31:32 +00:00
"tailscale.com/tailcfg"
2021-07-03 09:55:32 +00:00
)
2023-05-11 07:09:18 +00:00
var (
2023-05-21 16:37:59 +00:00
ErrEmptyPolicy = errors . New ( "empty policy" )
ErrInvalidAction = errors . New ( "invalid action" )
ErrInvalidGroup = errors . New ( "invalid group" )
ErrInvalidTag = errors . New ( "invalid tag" )
ErrInvalidPortFormat = errors . New ( "invalid port format" )
ErrWildcardIsNeeded = errors . New ( "wildcard as port is required for the protocol" )
2021-11-04 22:16:56 +00:00
)
2021-07-03 09:55:32 +00:00
2021-11-14 17:31:51 +00:00
const (
2021-11-15 17:24:24 +00:00
portRangeBegin = 0
portRangeEnd = 65535
expectedTokenItems = 2
2021-11-14 17:31:51 +00:00
)
2022-06-26 09:43:17 +00:00
// For some reason golang.org/x/net/internal/iana is an internal package.
2022-06-11 12:09:08 +00:00
const (
protocolICMP = 1 // Internet Control Message
protocolIGMP = 2 // Internet Group Management
protocolIPv4 = 4 // IPv4 encapsulation
protocolTCP = 6 // Transmission Control
protocolEGP = 8 // Exterior Gateway Protocol
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
protocolUDP = 17 // User Datagram
protocolGRE = 47 // Generic Routing Encapsulation
protocolESP = 50 // Encap Security Payload
protocolAH = 51 // Authentication Header
protocolIPv6ICMP = 58 // ICMP for IPv6
protocolSCTP = 132 // Stream Control Transmission Protocol
ProtocolFC = 133 // Fibre Channel
)
2022-11-24 16:26:52 +00:00
var featureEnableSSH = envknob . RegisterBool ( "HEADSCALE_EXPERIMENTAL_FEATURE_SSH" )
2022-11-24 15:35:55 +00:00
2023-05-10 08:19:16 +00:00
// LoadACLPolicyFromPath loads the ACL policy from the specify path, and generates the ACL rules.
2023-05-21 16:37:59 +00:00
func LoadACLPolicyFromPath ( path string ) ( * ACLPolicy , error ) {
2021-12-01 19:02:00 +00:00
log . Debug ( ) .
Str ( "func" , "LoadACLPolicy" ) .
Str ( "path" , path ) .
Msg ( "Loading ACL policy from path" )
2021-07-03 09:55:32 +00:00
policyFile , err := os . Open ( path )
if err != nil {
2023-05-21 16:37:59 +00:00
return nil , err
2021-07-03 09:55:32 +00:00
}
defer policyFile . Close ( )
2021-11-14 19:32:03 +00:00
policyBytes , err := io . ReadAll ( policyFile )
2021-07-03 09:55:32 +00:00
if err != nil {
2023-05-21 16:37:59 +00:00
return nil , err
2021-07-03 09:55:32 +00:00
}
2021-11-05 07:24:00 +00:00
2023-05-10 08:19:16 +00:00
log . Debug ( ) .
Str ( "path" , path ) .
Bytes ( "file" , policyBytes ) .
Msg ( "Loading ACLs" )
2022-02-27 08:04:48 +00:00
switch filepath . Ext ( path ) {
case ".yml" , ".yaml" :
2023-05-21 16:37:59 +00:00
return LoadACLPolicyFromBytes ( policyBytes , "yaml" )
2023-05-10 08:19:16 +00:00
}
2022-02-27 08:04:48 +00:00
2023-05-21 16:37:59 +00:00
return LoadACLPolicyFromBytes ( policyBytes , "hujson" )
2023-05-10 08:19:16 +00:00
}
2023-05-21 16:37:59 +00:00
func LoadACLPolicyFromBytes ( acl [ ] byte , format string ) ( * ACLPolicy , error ) {
2023-05-10 08:19:16 +00:00
var policy ACLPolicy
switch format {
case "yaml" :
err := yaml . Unmarshal ( acl , & policy )
2022-02-27 08:04:48 +00:00
if err != nil {
2023-05-21 16:37:59 +00:00
return nil , err
2022-02-27 08:04:48 +00:00
}
default :
2023-05-10 08:19:16 +00:00
ast , err := hujson . Parse ( acl )
2022-02-27 08:04:48 +00:00
if err != nil {
2023-05-21 16:37:59 +00:00
return nil , err
2022-02-27 08:04:48 +00:00
}
ast . Standardize ( )
2023-05-10 08:19:16 +00:00
acl = ast . Pack ( )
err = json . Unmarshal ( acl , & policy )
2022-02-27 08:04:48 +00:00
if err != nil {
2023-05-21 16:37:59 +00:00
return nil , err
2022-02-27 08:04:48 +00:00
}
2021-07-04 11:33:00 +00:00
}
2022-02-27 08:04:48 +00:00
2021-07-03 09:55:32 +00:00
if policy . IsZero ( ) {
2023-05-21 16:37:59 +00:00
return nil , ErrEmptyPolicy
2021-07-03 09:55:32 +00:00
}
2023-05-21 16:37:59 +00:00
return & policy , nil
2022-02-03 19:00:41 +00:00
}
2023-05-21 16:37:59 +00:00
// TODO(kradalby): This needs to be replace with something that generates
// the rules as needed and not stores it on the global object, rules are
// per node and that should be taken into account.
func GenerateFilterRules (
policy * ACLPolicy ,
machines types . Machines ,
stripEmailDomain bool ,
) ( [ ] tailcfg . FilterRule , * tailcfg . SSHPolicy , error ) {
if policy == nil {
2023-05-31 15:26:19 +00:00
return [ ] tailcfg . FilterRule { } , & tailcfg . SSHPolicy { } , nil
2022-11-30 23:37:58 +00:00
}
2023-05-21 16:37:59 +00:00
rules , err := policy . generateFilterRules ( machines , stripEmailDomain )
2021-07-04 11:24:05 +00:00
if err != nil {
2023-05-21 16:37:59 +00:00
return [ ] tailcfg . FilterRule { } , & tailcfg . SSHPolicy { } , err
2021-07-04 11:24:05 +00:00
}
2023-04-26 15:27:51 +00:00
2021-12-01 19:02:00 +00:00
log . Trace ( ) . Interface ( "ACL" , rules ) . Msg ( "ACL rules generated" )
2022-02-14 14:26:54 +00:00
2023-05-21 16:37:59 +00:00
var sshPolicy * tailcfg . SSHPolicy
2022-11-24 15:35:55 +00:00
if featureEnableSSH ( ) {
2023-05-21 16:37:59 +00:00
sshRules , err := generateSSHRules ( policy , machines , stripEmailDomain )
2022-11-24 15:35:55 +00:00
if err != nil {
2023-05-21 16:37:59 +00:00
return [ ] tailcfg . FilterRule { } , & tailcfg . SSHPolicy { } , err
2022-11-24 15:35:55 +00:00
}
log . Trace ( ) . Interface ( "SSH" , sshRules ) . Msg ( "SSH rules generated" )
2023-05-21 16:37:59 +00:00
if sshPolicy == nil {
sshPolicy = & tailcfg . SSHPolicy { }
2022-11-24 15:35:55 +00:00
}
2023-05-21 16:37:59 +00:00
sshPolicy . Rules = sshRules
} else if policy != nil && len ( policy . SSHs ) > 0 {
2022-11-24 16:26:52 +00:00
log . Info ( ) . Msg ( "SSH ACLs has been defined, but HEADSCALE_EXPERIMENTAL_FEATURE_SSH is not enabled, this is a unstable feature, check docs before activating" )
2022-09-30 18:44:23 +00:00
}
2023-05-21 16:37:59 +00:00
return rules , sshPolicy , nil
2021-07-03 15:31:32 +00:00
}
2023-04-26 09:19:47 +00:00
// generateFilterRules takes a set of machines and an ACLPolicy and generates a
// set of Tailscale compatible FilterRules used to allow traffic on clients.
2023-04-26 09:24:44 +00:00
func ( pol * ACLPolicy ) generateFilterRules (
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2023-04-26 12:04:12 +00:00
stripEmailDomain bool ,
2023-01-30 08:39:27 +00:00
) ( [ ] tailcfg . FilterRule , error ) {
2021-07-03 15:31:32 +00:00
rules := [ ] tailcfg . FilterRule { }
2023-04-26 09:19:47 +00:00
for index , acl := range pol . ACLs {
2021-11-14 19:32:03 +00:00
if acl . Action != "accept" {
2023-05-21 16:37:59 +00:00
return nil , ErrInvalidAction
2021-07-03 15:31:32 +00:00
}
srcIPs := [ ] string { }
2023-04-26 09:19:47 +00:00
for srcIndex , src := range acl . Sources {
2023-04-26 12:04:12 +00:00
srcs , err := pol . getIPsFromSource ( src , machines , stripEmailDomain )
2021-07-03 15:31:32 +00:00
if err != nil {
2021-08-05 17:18:18 +00:00
log . Error ( ) .
2023-04-26 09:19:47 +00:00
Interface ( "src" , src ) .
Int ( "ACL index" , index ) .
Int ( "Src index" , srcIndex ) .
Msgf ( "Error parsing ACL" )
2021-11-14 15:46:09 +00:00
2021-07-03 15:31:32 +00:00
return nil , err
}
2021-11-04 22:16:56 +00:00
srcIPs = append ( srcIPs , srcs ... )
2021-07-03 15:31:32 +00:00
}
2022-06-08 15:43:59 +00:00
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
}
2021-07-04 10:35:18 +00:00
destPorts := [ ] tailcfg . NetPortRange { }
2023-04-26 09:19:47 +00:00
for destIndex , dest := range acl . Destinations {
dests , err := pol . getNetPortRangeFromDestination (
2022-08-04 08:47:00 +00:00
dest ,
2023-04-26 09:19:47 +00:00
machines ,
2022-08-04 08:47:00 +00:00
needsWildcard ,
2023-04-26 12:04:12 +00:00
stripEmailDomain ,
2022-08-04 08:47:00 +00:00
)
2021-07-04 10:35:18 +00:00
if err != nil {
2021-08-05 17:18:18 +00:00
log . Error ( ) .
2023-04-26 09:19:47 +00:00
Interface ( "dest" , dest ) .
Int ( "ACL index" , index ) .
Int ( "dest index" , destIndex ) .
Msgf ( "Error parsing ACL" )
2021-11-14 15:46:09 +00:00
2021-07-04 10:35:18 +00:00
return nil , err
}
2021-11-04 22:16:56 +00:00
destPorts = append ( destPorts , dests ... )
2021-07-04 10:35:18 +00:00
}
rules = append ( rules , tailcfg . FilterRule {
SrcIPs : srcIPs ,
DstPorts : destPorts ,
2022-06-08 15:43:59 +00:00
IPProto : protocols ,
2021-07-04 10:35:18 +00:00
} )
2021-07-03 15:31:32 +00:00
}
2021-11-04 22:16:56 +00:00
return rules , nil
2021-07-03 15:31:32 +00:00
}
2023-05-21 16:37:59 +00:00
func generateSSHRules (
policy * ACLPolicy ,
machines types . Machines ,
stripEmailDomain bool ,
) ( [ ] * tailcfg . SSHRule , error ) {
2022-09-30 18:44:23 +00:00
rules := [ ] * tailcfg . SSHRule { }
2023-05-21 16:37:59 +00:00
if policy == nil {
return nil , ErrEmptyPolicy
2022-09-30 18:44:23 +00:00
}
acceptAction := tailcfg . SSHAction {
Message : "" ,
Reject : false ,
Accept : true ,
SessionDuration : 0 ,
AllowAgentForwarding : false ,
HoldAndDelegate : "" ,
AllowLocalPortForwarding : true ,
}
rejectAction := tailcfg . SSHAction {
Message : "" ,
Reject : true ,
Accept : false ,
SessionDuration : 0 ,
AllowAgentForwarding : false ,
HoldAndDelegate : "" ,
AllowLocalPortForwarding : false ,
}
2023-05-21 16:37:59 +00:00
for index , sshACL := range policy . SSHs {
2022-09-30 18:44:23 +00:00
action := rejectAction
switch sshACL . Action {
case "accept" :
action = acceptAction
case "check" :
checkAction , err := sshCheckAction ( sshACL . CheckPeriod )
if err != nil {
log . Error ( ) .
Msgf ( "Error parsing SSH %d, check action with unparsable duration '%s'" , index , sshACL . CheckPeriod )
} else {
action = * checkAction
}
default :
log . Error ( ) .
2023-05-21 16:37:59 +00:00
Msgf ( "Error parsing SSH %d, unknown action '%s', skipping" , index , sshACL . Action )
2022-09-30 18:44:23 +00:00
2023-05-21 16:37:59 +00:00
continue
2022-09-30 18:44:23 +00:00
}
principals := make ( [ ] * tailcfg . SSHPrincipal , 0 , len ( sshACL . Sources ) )
for innerIndex , rawSrc := range sshACL . Sources {
2023-04-28 14:11:02 +00:00
if isWildcard ( rawSrc ) {
2022-09-30 18:44:23 +00:00
principals = append ( principals , & tailcfg . SSHPrincipal {
2023-04-28 14:11:02 +00:00
Any : true ,
2022-09-30 18:44:23 +00:00
} )
2023-04-28 14:11:02 +00:00
} else if isGroup ( rawSrc ) {
2023-05-21 16:37:59 +00:00
users , err := policy . getUsersInGroup ( rawSrc , stripEmailDomain )
2023-04-28 14:11:02 +00:00
if err != nil {
log . Error ( ) .
Msgf ( "Error parsing SSH %d, Source %d" , index , innerIndex )
return nil , err
}
for _ , user := range users {
principals = append ( principals , & tailcfg . SSHPrincipal {
UserLogin : user ,
} )
}
} else {
2023-05-21 16:37:59 +00:00
expandedSrcs , err := policy . ExpandAlias (
2023-04-28 14:11:02 +00:00
machines ,
rawSrc ,
2023-05-21 16:37:59 +00:00
stripEmailDomain ,
2023-04-28 14:11:02 +00:00
)
if err != nil {
log . Error ( ) .
Msgf ( "Error parsing SSH %d, Source %d" , index , innerIndex )
return nil , err
}
for _ , expandedSrc := range expandedSrcs . Prefixes ( ) {
principals = append ( principals , & tailcfg . SSHPrincipal {
NodeIP : expandedSrc . Addr ( ) . String ( ) ,
} )
}
2022-09-30 18:44:23 +00:00
}
}
userMap := make ( map [ string ] string , len ( sshACL . Users ) )
for _ , user := range sshACL . Users {
userMap [ user ] = "="
}
rules = append ( rules , & tailcfg . SSHRule {
2023-04-28 14:11:02 +00:00
Principals : principals ,
SSHUsers : userMap ,
Action : & action ,
2022-09-30 18:44:23 +00:00
} )
}
return rules , nil
}
func sshCheckAction ( duration string ) ( * tailcfg . SSHAction , error ) {
sessionLength , err := time . ParseDuration ( duration )
if err != nil {
return nil , err
}
return & tailcfg . SSHAction {
Message : "" ,
Reject : false ,
Accept : true ,
SessionDuration : sessionLength ,
AllowAgentForwarding : false ,
HoldAndDelegate : "" ,
AllowLocalPortForwarding : true ,
} , nil
}
2023-04-26 09:19:47 +00:00
// getIPsFromSource returns a set of Source IPs that would be associated
// with the given src alias.
func ( pol * ACLPolicy ) getIPsFromSource (
2022-06-08 11:40:15 +00:00
src string ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2022-11-30 23:37:58 +00:00
stripEmaildomain bool ,
2022-02-14 14:54:51 +00:00
) ( [ ] string , error ) {
2023-05-21 16:37:59 +00:00
ipSet , err := pol . ExpandAlias ( machines , src , stripEmaildomain )
2023-04-28 14:11:02 +00:00
if err != nil {
return [ ] string { } , err
}
prefixes := [ ] string { }
for _ , prefix := range ipSet . Prefixes ( ) {
prefixes = append ( prefixes , prefix . String ( ) )
}
return prefixes , nil
2021-07-04 10:35:18 +00:00
}
2023-04-26 09:19:47 +00:00
// getNetPortRangeFromDestination returns a set of tailcfg.NetPortRange
// which are associated with the dest alias.
func ( pol * ACLPolicy ) getNetPortRangeFromDestination (
2022-06-08 11:40:15 +00:00
dest string ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2022-06-08 15:43:59 +00:00
needsWildcard bool ,
2022-11-30 23:37:58 +00:00
stripEmaildomain bool ,
2021-11-13 08:36:45 +00:00
) ( [ ] tailcfg . NetPortRange , error ) {
2023-04-16 10:26:35 +00:00
var tokens [ ] string
log . Trace ( ) . Str ( "destination" , dest ) . Msg ( "generating policy destination" )
// Check if there is a IPv4/6:Port combination, IPv6 has more than
// three ":".
tokens = strings . Split ( dest , ":" )
2021-11-15 17:24:24 +00:00
if len ( tokens ) < expectedTokenItems || len ( tokens ) > 3 {
2023-04-16 10:26:35 +00:00
port := tokens [ len ( tokens ) - 1 ]
maybeIPv6Str := strings . TrimSuffix ( dest , ":" + port )
log . Trace ( ) . Str ( "maybeIPv6Str" , maybeIPv6Str ) . Msg ( "" )
if maybeIPv6 , err := netip . ParseAddr ( maybeIPv6Str ) ; err != nil && ! maybeIPv6 . Is6 ( ) {
log . Trace ( ) . Err ( err ) . Msg ( "trying to parse as IPv6" )
return nil , fmt . Errorf (
"failed to parse destination, tokens %v: %w" ,
tokens ,
2023-05-21 16:37:59 +00:00
ErrInvalidPortFormat ,
2023-04-16 10:26:35 +00:00
)
} else {
tokens = [ ] string { maybeIPv6Str , port }
}
2021-07-04 10:35:18 +00:00
}
2023-04-16 10:26:35 +00:00
log . Trace ( ) . Strs ( "tokens" , tokens ) . Msg ( "generating policy destination" )
2021-07-04 10:35:18 +00:00
var alias string
// We can have here stuff like:
// git-server:*
// 192.168.1.0/24:22
2023-04-16 10:26:35 +00:00
// fd7a:115c:a1e0::2:22
// fd7a:115c:a1e0::2/128:22
2021-07-04 10:35:18 +00:00
// tag:montreal-webserver:80,443
// tag:api-server:443
// example-host-1:*
2021-11-15 17:24:24 +00:00
if len ( tokens ) == expectedTokenItems {
2021-07-04 10:35:18 +00:00
alias = tokens [ 0 ]
} else {
alias = fmt . Sprintf ( "%s:%s" , tokens [ 0 ] , tokens [ 1 ] )
}
2023-05-21 16:37:59 +00:00
expanded , err := pol . ExpandAlias (
2022-03-01 20:01:46 +00:00
machines ,
alias ,
2022-11-30 23:37:58 +00:00
stripEmaildomain ,
2022-03-01 20:01:46 +00:00
)
2021-07-04 10:35:18 +00:00
if err != nil {
return nil , err
}
2022-06-08 15:43:59 +00:00
ports , err := expandPorts ( tokens [ len ( tokens ) - 1 ] , needsWildcard )
2021-07-04 10:35:18 +00:00
if err != nil {
return nil , err
}
dests := [ ] tailcfg . NetPortRange { }
2023-04-28 14:11:02 +00:00
for _ , dest := range expanded . Prefixes ( ) {
for _ , port := range * ports {
2021-07-04 10:35:18 +00:00
pr := tailcfg . NetPortRange {
2023-04-28 14:11:02 +00:00
IP : dest . String ( ) ,
Ports : port ,
2021-07-04 10:35:18 +00:00
}
dests = append ( dests , pr )
}
}
2021-11-14 15:46:09 +00:00
2021-11-04 22:16:56 +00:00
return dests , nil
2021-07-04 10:35:18 +00:00
}
2022-06-08 15:43:59 +00:00
// 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 ) {
switch protocol {
case "" :
2022-12-05 19:12:33 +00:00
return nil , false , nil
2022-06-08 15:43:59 +00:00
case "igmp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolIGMP } , true , nil
2022-06-08 15:43:59 +00:00
case "ipv4" , "ip-in-ip" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolIPv4 } , true , nil
2022-06-08 15:43:59 +00:00
case "tcp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolTCP } , false , nil
2022-06-08 15:43:59 +00:00
case "egp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolEGP } , true , nil
2022-06-08 15:43:59 +00:00
case "igp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolIGP } , true , nil
2022-06-08 15:43:59 +00:00
case "udp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolUDP } , false , nil
2022-06-08 15:43:59 +00:00
case "gre" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolGRE } , true , nil
2022-06-08 15:43:59 +00:00
case "esp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolESP } , true , nil
2022-06-08 15:43:59 +00:00
case "ah" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolAH } , true , nil
2022-06-08 15:43:59 +00:00
case "sctp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolSCTP } , false , nil
2022-06-08 15:43:59 +00:00
case "icmp" :
2022-06-11 12:09:08 +00:00
return [ ] int { protocolICMP , protocolIPv6ICMP } , true , nil
2022-06-08 15:43:59 +00:00
default :
protocolNumber , err := strconv . Atoi ( protocol )
if err != nil {
return nil , false , err
}
2022-08-04 08:47:00 +00:00
needsWildcard := protocolNumber != protocolTCP &&
protocolNumber != protocolUDP &&
protocolNumber != protocolSCTP
2022-06-08 15:43:59 +00:00
return [ ] int { protocolNumber } , needsWildcard , nil
}
}
2022-02-05 16:18:39 +00:00
// expandalias has an input of either
2023-01-17 16:43:44 +00:00
// - a user
2022-02-05 16:18:39 +00:00
// - a group
// - a tag
2023-01-30 08:39:27 +00:00
// - a host
2023-04-16 10:26:35 +00:00
// - an ip
// - a cidr
2022-02-14 14:26:54 +00:00
// and transform these in IPAddresses.
2023-05-21 16:37:59 +00:00
func ( pol * ACLPolicy ) ExpandAlias (
machines types . Machines ,
2022-02-14 14:54:51 +00:00
alias string ,
2022-03-01 20:01:46 +00:00
stripEmailDomain bool ,
2023-04-28 14:11:02 +00:00
) ( * netipx . IPSet , error ) {
if isWildcard ( alias ) {
2023-05-21 16:37:59 +00:00
return util . ParseIPSet ( "*" , nil )
2021-07-03 15:31:32 +00:00
}
2023-04-28 14:11:02 +00:00
build := netipx . IPSetBuilder { }
2022-03-02 20:46:02 +00:00
log . Debug ( ) .
Str ( "alias" , alias ) .
Msg ( "Expanding" )
2023-04-26 08:58:26 +00:00
// if alias is a group
2023-04-28 14:11:02 +00:00
if isGroup ( alias ) {
2023-04-26 08:58:26 +00:00
return pol . getIPsFromGroup ( alias , machines , stripEmailDomain )
2021-07-03 15:31:32 +00:00
}
2023-04-26 08:58:26 +00:00
// if alias is a tag
2023-04-28 14:11:02 +00:00
if isTag ( alias ) {
2023-04-26 08:58:26 +00:00
return pol . getIPsFromTag ( alias , machines , stripEmailDomain )
2021-07-03 15:31:32 +00:00
}
2023-01-17 16:43:44 +00:00
// if alias is a user
2023-04-28 14:11:02 +00:00
if ips , err := pol . getIPsForUser ( alias , machines , stripEmailDomain ) ; ips != nil {
return ips , err
2021-07-03 15:31:32 +00:00
}
2022-02-07 15:12:05 +00:00
// if alias is an host
2023-04-26 12:04:12 +00:00
// Note, this is recursive.
2023-04-26 08:58:26 +00:00
if h , ok := pol . Hosts [ alias ] ; ok {
2023-05-21 16:37:59 +00:00
log . Trace ( ) . Str ( "host" , h . String ( ) ) . Msg ( "ExpandAlias got hosts entry" )
2023-04-16 10:26:35 +00:00
2023-05-21 16:37:59 +00:00
return pol . ExpandAlias ( machines , h . String ( ) , stripEmailDomain )
2021-07-03 15:31:32 +00:00
}
2022-02-07 15:12:05 +00:00
// if alias is an IP
2023-04-16 10:26:35 +00:00
if ip , err := netip . ParseAddr ( alias ) ; err == nil {
2023-04-26 08:58:26 +00:00
return pol . getIPsFromSingleIP ( ip , machines )
2021-07-03 15:31:32 +00:00
}
2023-04-26 08:58:26 +00:00
// if alias is an IP Prefix (CIDR)
if prefix , err := netip . ParsePrefix ( alias ) ; err == nil {
return pol . getIPsFromIPPrefix ( prefix , machines )
2021-07-03 15:31:32 +00:00
}
2022-03-02 20:46:02 +00:00
log . Warn ( ) . Msgf ( "No IPs found with the alias %v" , alias )
2023-04-28 14:11:02 +00:00
return build . IPSet ( )
2021-07-03 09:55:32 +00:00
}
2021-07-04 10:35:18 +00:00
2022-02-07 15:12:05 +00:00
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
2023-01-17 16:43:44 +00:00
// that are correctly tagged since they should not be listed as being in the user
// we assume in this function that we only have nodes from 1 user.
2022-02-14 14:54:51 +00:00
func excludeCorrectlyTaggedNodes (
2023-04-26 08:58:26 +00:00
aclPolicy * ACLPolicy ,
2023-05-21 16:37:59 +00:00
nodes types . Machines ,
2023-01-17 16:43:44 +00:00
user string ,
2022-08-04 08:42:47 +00:00
stripEmailDomain bool ,
2023-05-21 16:37:59 +00:00
) types . Machines {
out := types . Machines { }
2022-02-07 15:12:05 +00:00
tags := [ ] string { }
2022-08-11 12:12:45 +00:00
for tag := range aclPolicy . TagOwners {
2023-04-26 08:58:26 +00:00
owners , _ := getTagOwners ( aclPolicy , user , stripEmailDomain )
2023-01-17 16:43:44 +00:00
ns := append ( owners , user )
2023-05-11 07:09:18 +00:00
if util . StringOrPrefixListContains ( ns , user ) {
2022-02-07 15:12:05 +00:00
tags = append ( tags , tag )
}
2022-02-05 16:18:39 +00:00
}
2022-02-07 15:12:05 +00:00
// for each machine if tag is in tags list, don't append it.
for _ , machine := range nodes {
2022-03-01 16:34:24 +00:00
hi := machine . GetHostInfo ( )
2022-02-14 14:26:54 +00:00
2022-02-07 15:12:05 +00:00
found := false
for _ , t := range hi . RequestTags {
2023-05-11 07:09:18 +00:00
if util . StringOrPrefixListContains ( tags , t ) {
2022-02-07 15:12:05 +00:00
found = true
2022-02-14 14:26:54 +00:00
2022-02-07 15:12:05 +00:00
break
2022-02-05 16:18:39 +00:00
}
}
2022-04-15 16:01:13 +00:00
if len ( machine . ForcedTags ) > 0 {
found = true
}
2022-02-07 15:12:05 +00:00
if ! found {
out = append ( out , machine )
2022-02-05 16:18:39 +00:00
}
}
2022-02-14 14:26:54 +00:00
2022-03-02 08:15:14 +00:00
return out
2022-02-05 16:18:39 +00:00
}
2022-06-08 15:43:59 +00:00
func expandPorts ( portsStr string , needsWildcard bool ) ( * [ ] tailcfg . PortRange , error ) {
2023-04-28 14:11:02 +00:00
if isWildcard ( portsStr ) {
2021-11-14 17:31:51 +00:00
return & [ ] tailcfg . PortRange {
2021-11-15 17:24:24 +00:00
{ First : portRangeBegin , Last : portRangeEnd } ,
2021-11-14 17:31:51 +00:00
} , nil
2021-07-04 10:35:18 +00:00
}
2022-06-08 15:43:59 +00:00
if needsWildcard {
2023-05-21 16:37:59 +00:00
return nil , ErrWildcardIsNeeded
2022-06-08 15:43:59 +00:00
}
2021-07-04 10:35:18 +00:00
ports := [ ] tailcfg . PortRange { }
2021-11-14 19:32:03 +00:00
for _ , portStr := range strings . Split ( portsStr , "," ) {
2023-04-16 10:26:35 +00:00
log . Trace ( ) . Msgf ( "parsing portstring: %s" , portStr )
2021-11-14 19:32:03 +00:00
rang := strings . Split ( portStr , "-" )
2021-11-14 17:44:37 +00:00
switch len ( rang ) {
case 1 :
2023-05-11 07:09:18 +00:00
port , err := strconv . ParseUint ( rang [ 0 ] , util . Base10 , util . BitSize16 )
2021-07-04 10:35:18 +00:00
if err != nil {
return nil , err
}
ports = append ( ports , tailcfg . PortRange {
2021-11-14 19:32:03 +00:00
First : uint16 ( port ) ,
Last : uint16 ( port ) ,
2021-07-04 10:35:18 +00:00
} )
2021-11-14 17:44:37 +00:00
2021-11-15 17:24:24 +00:00
case expectedTokenItems :
2023-05-11 07:09:18 +00:00
start , err := strconv . ParseUint ( rang [ 0 ] , util . Base10 , util . BitSize16 )
2021-07-04 10:35:18 +00:00
if err != nil {
return nil , err
}
2023-05-11 07:09:18 +00:00
last , err := strconv . ParseUint ( rang [ 1 ] , util . Base10 , util . BitSize16 )
2021-07-04 10:35:18 +00:00
if err != nil {
return nil , err
}
ports = append ( ports , tailcfg . PortRange {
First : uint16 ( start ) ,
Last : uint16 ( last ) ,
} )
2021-11-14 17:44:37 +00:00
default :
2023-05-21 16:37:59 +00:00
return nil , ErrInvalidPortFormat
2021-07-04 10:35:18 +00:00
}
}
2021-11-14 15:46:09 +00:00
2021-07-04 10:35:18 +00:00
return & ports , nil
}
2022-02-07 15:12:05 +00:00
2023-05-21 16:37:59 +00:00
func filterMachinesByUser ( machines types . Machines , user string ) types . Machines {
out := types . Machines { }
2022-02-07 15:12:05 +00:00
for _ , machine := range machines {
2023-01-17 16:43:44 +00:00
if machine . User . Name == user {
2022-02-07 15:12:05 +00:00
out = append ( out , machine )
}
}
2022-02-14 14:26:54 +00:00
2022-02-07 15:12:05 +00:00
return out
}
2023-04-26 08:58:26 +00:00
// getTagOwners will return a list of user. An owner can be either a user or a group
2022-02-14 14:26:54 +00:00
// a group cannot be composed of groups.
2023-04-26 08:58:26 +00:00
func getTagOwners (
pol * ACLPolicy ,
2022-03-01 20:01:46 +00:00
tag string ,
stripEmailDomain bool ,
) ( [ ] string , error ) {
2022-02-07 15:12:05 +00:00
var owners [ ] string
2023-04-26 08:58:26 +00:00
ows , ok := pol . TagOwners [ tag ]
2022-02-07 15:12:05 +00:00
if ! ok {
2022-02-14 14:54:51 +00:00
return [ ] string { } , fmt . Errorf (
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners" ,
2023-05-21 16:37:59 +00:00
ErrInvalidTag ,
2022-02-14 14:54:51 +00:00
tag ,
)
2022-02-07 15:12:05 +00:00
}
2022-02-14 14:26:54 +00:00
for _ , owner := range ows {
2023-04-28 14:11:02 +00:00
if isGroup ( owner ) {
2023-04-26 08:58:26 +00:00
gs , err := pol . getUsersInGroup ( owner , stripEmailDomain )
2022-02-07 15:12:05 +00:00
if err != nil {
return [ ] string { } , err
}
owners = append ( owners , gs ... )
} else {
2022-02-14 14:26:54 +00:00
owners = append ( owners , owner )
2022-02-07 15:12:05 +00:00
}
}
2022-02-14 14:26:54 +00:00
2022-02-07 15:12:05 +00:00
return owners , nil
}
2023-04-26 08:58:26 +00:00
// getUsersInGroup will return the list of user inside the group
2022-02-14 14:26:54 +00:00
// after some validation.
2023-04-26 08:58:26 +00:00
func ( pol * ACLPolicy ) getUsersInGroup (
2022-03-01 20:01:46 +00:00
group string ,
stripEmailDomain bool ,
) ( [ ] string , error ) {
2023-04-26 08:58:26 +00:00
users := [ ] string { }
log . Trace ( ) . Caller ( ) . Interface ( "pol" , pol ) . Msg ( "test" )
aclGroups , ok := pol . Groups [ group ]
2022-02-07 15:12:05 +00:00
if ! ok {
2022-02-14 14:54:51 +00:00
return [ ] string { } , fmt . Errorf (
"group %v isn't registered. %w" ,
group ,
2023-05-21 16:37:59 +00:00
ErrInvalidGroup ,
2022-02-14 14:54:51 +00:00
)
2022-02-07 15:12:05 +00:00
}
2022-03-01 20:01:46 +00:00
for _ , group := range aclGroups {
2023-04-28 14:11:02 +00:00
if isGroup ( group ) {
2022-02-14 14:54:51 +00:00
return [ ] string { } , fmt . Errorf (
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups" ,
2023-05-21 16:37:59 +00:00
ErrInvalidGroup ,
2022-02-14 14:54:51 +00:00
)
2022-02-07 15:12:05 +00:00
}
2023-05-21 16:37:59 +00:00
grp , err := util . NormalizeToFQDNRules ( group , stripEmailDomain )
2022-03-01 20:01:46 +00:00
if err != nil {
return [ ] string { } , fmt . Errorf (
"failed to normalize group %q, err: %w" ,
group ,
2023-05-21 16:37:59 +00:00
ErrInvalidGroup ,
2022-03-01 20:01:46 +00:00
)
}
2023-04-26 08:58:26 +00:00
users = append ( users , grp )
}
return users , nil
}
func ( pol * ACLPolicy ) getIPsFromGroup (
group string ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2023-04-26 08:58:26 +00:00
stripEmailDomain bool ,
2023-04-28 14:11:02 +00:00
) ( * netipx . IPSet , error ) {
build := netipx . IPSetBuilder { }
2023-04-26 08:58:26 +00:00
users , err := pol . getUsersInGroup ( group , stripEmailDomain )
if err != nil {
2023-04-28 14:11:02 +00:00
return & netipx . IPSet { } , err
2023-04-26 08:58:26 +00:00
}
2023-04-28 14:11:02 +00:00
for _ , user := range users {
filteredMachines := filterMachinesByUser ( machines , user )
for _ , machine := range filteredMachines {
machine . IPAddresses . AppendToIPSet ( & build )
2023-04-26 08:58:26 +00:00
}
}
2023-04-28 14:11:02 +00:00
return build . IPSet ( )
2023-04-26 08:58:26 +00:00
}
func ( pol * ACLPolicy ) getIPsFromTag (
alias string ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2023-04-26 08:58:26 +00:00
stripEmailDomain bool ,
2023-04-28 14:11:02 +00:00
) ( * netipx . IPSet , error ) {
build := netipx . IPSetBuilder { }
2023-04-26 08:58:26 +00:00
// check for forced tags
for _ , machine := range machines {
2023-05-11 07:09:18 +00:00
if util . StringOrPrefixListContains ( machine . ForcedTags , alias ) {
2023-04-28 14:11:02 +00:00
machine . IPAddresses . AppendToIPSet ( & build )
2023-04-26 08:58:26 +00:00
}
}
// find tag owners
owners , err := getTagOwners ( pol , alias , stripEmailDomain )
if err != nil {
2023-05-21 16:37:59 +00:00
if errors . Is ( err , ErrInvalidTag ) {
2023-04-28 14:11:02 +00:00
ipSet , _ := build . IPSet ( )
if len ( ipSet . Prefixes ( ) ) == 0 {
return ipSet , fmt . Errorf (
2023-04-26 08:58:26 +00:00
"%w. %v isn't owned by a TagOwner and no forced tags are defined" ,
2023-05-21 16:37:59 +00:00
ErrInvalidTag ,
2023-04-26 08:58:26 +00:00
alias ,
)
}
2023-04-28 14:11:02 +00:00
return build . IPSet ( )
2023-04-26 08:58:26 +00:00
} else {
2023-04-28 14:11:02 +00:00
return nil , err
2023-04-26 08:58:26 +00:00
}
}
// filter out machines per tag owner
for _ , user := range owners {
machines := filterMachinesByUser ( machines , user )
for _ , machine := range machines {
hi := machine . GetHostInfo ( )
2023-05-11 07:09:18 +00:00
if util . StringOrPrefixListContains ( hi . RequestTags , alias ) {
2023-04-28 14:11:02 +00:00
machine . IPAddresses . AppendToIPSet ( & build )
2023-04-26 08:58:26 +00:00
}
}
}
2023-04-28 14:11:02 +00:00
return build . IPSet ( )
2023-04-26 08:58:26 +00:00
}
func ( pol * ACLPolicy ) getIPsForUser (
user string ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2023-04-26 08:58:26 +00:00
stripEmailDomain bool ,
2023-04-28 14:11:02 +00:00
) ( * netipx . IPSet , error ) {
build := netipx . IPSetBuilder { }
2023-04-26 08:58:26 +00:00
2023-04-28 14:11:02 +00:00
filteredMachines := filterMachinesByUser ( machines , user )
filteredMachines = excludeCorrectlyTaggedNodes ( pol , filteredMachines , user , stripEmailDomain )
2023-04-26 08:58:26 +00:00
2023-04-28 14:11:02 +00:00
// shortcurcuit if we have no machines to get ips from.
if len ( filteredMachines ) == 0 {
return nil , nil //nolint
2023-04-26 08:58:26 +00:00
}
2023-04-28 14:11:02 +00:00
for _ , machine := range filteredMachines {
machine . IPAddresses . AppendToIPSet ( & build )
}
return build . IPSet ( )
2023-04-26 08:58:26 +00:00
}
func ( pol * ACLPolicy ) getIPsFromSingleIP (
ip netip . Addr ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2023-04-28 14:11:02 +00:00
) ( * netipx . IPSet , error ) {
2023-05-21 16:37:59 +00:00
log . Trace ( ) . Str ( "ip" , ip . String ( ) ) . Msg ( "ExpandAlias got ip" )
2023-04-26 08:58:26 +00:00
matches := machines . FilterByIP ( ip )
2023-04-28 14:11:02 +00:00
build := netipx . IPSetBuilder { }
build . Add ( ip )
2023-04-26 08:58:26 +00:00
for _ , machine := range matches {
2023-04-28 14:11:02 +00:00
machine . IPAddresses . AppendToIPSet ( & build )
2023-04-26 08:58:26 +00:00
}
2023-04-28 14:11:02 +00:00
return build . IPSet ( )
2023-04-26 08:58:26 +00:00
}
func ( pol * ACLPolicy ) getIPsFromIPPrefix (
prefix netip . Prefix ,
2023-05-21 16:37:59 +00:00
machines types . Machines ,
2023-04-28 14:11:02 +00:00
) ( * netipx . IPSet , error ) {
2023-04-26 08:58:26 +00:00
log . Trace ( ) . Str ( "prefix" , prefix . String ( ) ) . Msg ( "expandAlias got prefix" )
2023-04-28 14:11:02 +00:00
build := netipx . IPSetBuilder { }
build . AddPrefix ( prefix )
2023-04-26 08:58:26 +00:00
// This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
for _ , machine := range machines {
for _ , ip := range machine . IPAddresses {
// log.Trace().
// Msgf("checking if machine ip (%s) is part of prefix (%s): %v, is single ip prefix (%v), addr: %s", ip.String(), prefix.String(), prefix.Contains(ip), prefix.IsSingleIP(), prefix.Addr().String())
if prefix . Contains ( ip ) {
2023-04-28 14:11:02 +00:00
machine . IPAddresses . AppendToIPSet ( & build )
2023-04-26 08:58:26 +00:00
}
}
2022-02-07 15:12:05 +00:00
}
2022-02-14 14:26:54 +00:00
2023-04-28 14:11:02 +00:00
return build . IPSet ( )
}
func isWildcard ( str string ) bool {
return str == "*"
}
func isGroup ( str string ) bool {
return strings . HasPrefix ( str , "group:" )
}
func isTag ( str string ) bool {
return strings . HasPrefix ( str , "tag:" )
2022-02-07 15:12:05 +00:00
}
2023-05-21 16:37:59 +00:00
// getTags will return the tags of the current machine.
// Invalid tags are tags added by a user on a node, and that user doesn't have authority to add this tag.
// Valid tags are tags added by a user that is allowed in the ACL policy to add this tag.
func ( pol * ACLPolicy ) GetTagsOfMachine (
machine types . Machine ,
stripEmailDomain bool ,
) ( [ ] string , [ ] string ) {
validTags := make ( [ ] string , 0 )
invalidTags := make ( [ ] string , 0 )
validTagMap := make ( map [ string ] bool )
invalidTagMap := make ( map [ string ] bool )
for _ , tag := range machine . HostInfo . RequestTags {
owners , err := getTagOwners ( pol , tag , stripEmailDomain )
if errors . Is ( err , ErrInvalidTag ) {
invalidTagMap [ tag ] = true
continue
}
var found bool
for _ , owner := range owners {
if machine . User . Name == owner {
found = true
}
}
if found {
validTagMap [ tag ] = true
} else {
invalidTagMap [ tag ] = true
}
}
for tag := range invalidTagMap {
invalidTags = append ( invalidTags , tag )
}
for tag := range validTagMap {
validTags = append ( validTags , tag )
}
return validTags , invalidTags
}
// FilterMachinesByACL returns the list of peers authorized to be accessed from a given machine.
func FilterMachinesByACL (
machine * types . Machine ,
machines types . Machines ,
filter [ ] tailcfg . FilterRule ,
) types . Machines {
result := types . Machines { }
for index , peer := range machines {
if peer . ID == machine . ID {
continue
}
if machine . CanAccess ( filter , & machines [ index ] ) || peer . CanAccess ( filter , machine ) {
result = append ( result , peer )
}
}
return result
}