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 } // MatchFromFilterRule derives a [Match] from a [tailcfg.FilterRule]. The // destination IP set is the union of [tailcfg.FilterRule.DstPorts][].IP // and [tailcfg.FilterRule.CapGrant][].Dsts: cap-grant-only rules (e.g. // tailscale.com/cap/relay) carry their destinations in CapGrant.Dsts and // would otherwise contribute nothing to peer-visibility derivation in // [policy.BuildPeerMap] / [policy.ReduceNodes], hiding the cap target // from the source unless a companion IP-level rule also exists. func MatchFromFilterRule(rule tailcfg.FilterRule) Match { srcs := new(netipx.IPSetBuilder) dests := new(netipx.IPSetBuilder) for _, srcIP := range rule.SrcIPs { set, _ := util.ParseIPSet(srcIP, nil) srcs.AddSet(set) } for _, dp := range rule.DstPorts { set, _ := util.ParseIPSet(dp.IP, nil) dests.AddSet(set) } for _, cg := range rule.CapGrant { for _, pref := range cg.Dsts { dests.AddPrefix(pref) } } srcsSet, _ := srcs.IPSet() destsSet, _ := dests.IPSet() return Match{ srcs: srcsSet, dests: destsSet, } } // MatchFromStrings builds a [Match] from raw source and destination // strings. Unparseable entries are silently dropped (fail-open): the // resulting [Match] is narrower than the input described, but never // wider. Callers that need strict validation should pre-validate // their inputs via [util.ParseIPSet]. 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 whether the destination covers "the // internet" — the set represented by autogroup:internet, special-cased // for exit nodes. Returns true if either family's /0 is contained // (0.0.0.0/0 or ::/0), or if dests is a superset of [util.TheInternet]. A // single-family /0 counts because operators may write it directly and // it still denotes the whole internet for that family. func (m *Match) DestsIsTheInternet() bool { if m.dests.ContainsPrefix(tsaddr.AllIPv4()) || m.dests.ContainsPrefix(tsaddr.AllIPv6()) { return true } // Superset-of-[util.TheInternet] check handles merged filter rules // where the internet prefixes are combined with other dests. theInternet := util.TheInternet() for _, prefix := range theInternet.Prefixes() { if !m.dests.ContainsPrefix(prefix) { return false } } return true }