Tailscale merges multiple ACL rules into fewer FilterRule entries when they have identical SrcIPs and IPProto, combining their DstPorts arrays. This change implements the same behavior in Headscale. Add mergeFilterRules() which uses O(n) hash map lookup to merge rules with identical keys. DstPorts are NOT deduplicated to match Tailscale behavior. Also fix DestsIsTheInternet() to handle merged filter rules where TheInternet is combined with other destinations - now uses superset check instead of equality check. Updates #3036
116 lines
2.6 KiB
Go
116 lines
2.6 KiB
Go
package matcher
|
|
|
|
import (
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"go4.org/netipx"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
type Match struct {
|
|
srcs *netipx.IPSet
|
|
dests *netipx.IPSet
|
|
}
|
|
|
|
func (m Match) DebugString() string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("Match:\n")
|
|
sb.WriteString(" Sources:\n")
|
|
for _, prefix := range m.srcs.Prefixes() {
|
|
sb.WriteString(" " + prefix.String() + "\n")
|
|
}
|
|
sb.WriteString(" Destinations:\n")
|
|
for _, prefix := range m.dests.Prefixes() {
|
|
sb.WriteString(" " + prefix.String() + "\n")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func MatchesFromFilterRules(rules []tailcfg.FilterRule) []Match {
|
|
matches := make([]Match, 0, len(rules))
|
|
for _, rule := range rules {
|
|
matches = append(matches, MatchFromFilterRule(rule))
|
|
}
|
|
|
|
return matches
|
|
}
|
|
|
|
func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
|
|
dests := []string{}
|
|
for _, dest := range rule.DstPorts {
|
|
dests = append(dests, dest.IP)
|
|
}
|
|
|
|
return MatchFromStrings(rule.SrcIPs, dests)
|
|
}
|
|
|
|
func MatchFromStrings(sources, destinations []string) Match {
|
|
srcs := new(netipx.IPSetBuilder)
|
|
dests := new(netipx.IPSetBuilder)
|
|
|
|
for _, srcIP := range sources {
|
|
set, _ := util.ParseIPSet(srcIP, nil)
|
|
|
|
srcs.AddSet(set)
|
|
}
|
|
|
|
for _, dest := range destinations {
|
|
set, _ := util.ParseIPSet(dest, nil)
|
|
|
|
dests.AddSet(set)
|
|
}
|
|
|
|
srcsSet, _ := srcs.IPSet()
|
|
destsSet, _ := dests.IPSet()
|
|
|
|
match := Match{
|
|
srcs: srcsSet,
|
|
dests: destsSet,
|
|
}
|
|
|
|
return match
|
|
}
|
|
|
|
func (m *Match) SrcsContainsIPs(ips ...netip.Addr) bool {
|
|
return slices.ContainsFunc(ips, m.srcs.Contains)
|
|
}
|
|
|
|
func (m *Match) DestsContainsIP(ips ...netip.Addr) bool {
|
|
return slices.ContainsFunc(ips, m.dests.Contains)
|
|
}
|
|
|
|
func (m *Match) SrcsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
|
|
return slices.ContainsFunc(prefixes, m.srcs.OverlapsPrefix)
|
|
}
|
|
|
|
func (m *Match) DestsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
|
|
return slices.ContainsFunc(prefixes, m.dests.OverlapsPrefix)
|
|
}
|
|
|
|
// DestsIsTheInternet reports if the destination contains "the internet"
|
|
// which is a IPSet that represents "autogroup:internet" and is special
|
|
// cased for exit nodes.
|
|
// This checks if dests is a superset of TheInternet(), which handles
|
|
// merged filter rules where TheInternet is combined with other destinations.
|
|
func (m Match) DestsIsTheInternet() bool {
|
|
if m.dests.ContainsPrefix(tsaddr.AllIPv4()) ||
|
|
m.dests.ContainsPrefix(tsaddr.AllIPv6()) {
|
|
return true
|
|
}
|
|
|
|
// Check if dests contains all prefixes of TheInternet (superset check)
|
|
theInternet := util.TheInternet()
|
|
for _, prefix := range theInternet.Prefixes() {
|
|
if !m.dests.ContainsPrefix(prefix) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|