fb0ee8ead4
Replace cidranger third-party dependency with a custom binary trie for zero-allocation CIDR containment lookups. Migrate ipMatcher from string-keyed map to netip.Addr for correct IPv4/IPv6 normalization. Extract shared splitHostPort helper to eliminate repeated host:port parsing boilerplate across all matchers. Add comprehensive benchmarks.
313 lines
6.6 KiB
Go
313 lines
6.6 KiB
Go
// Package matcher provides pattern matching utilities for IP addresses,
|
|
// CIDR blocks, domains, wildcard patterns, and address:port combinations.
|
|
package matcher
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
"github.com/gobwas/glob"
|
|
)
|
|
|
|
// Matcher is a generic pattern matcher that tests whether a given value
|
|
// matches any of the pre-configured patterns.
|
|
type Matcher interface {
|
|
Match(v string) bool
|
|
}
|
|
|
|
// splitHostPort splits addr into host and port without allocating when
|
|
// addr contains no colon (i.e. no port). For inputs with a colon,
|
|
// net.SplitHostPort is used as normal.
|
|
func splitHostPort(addr string) (host, port string) {
|
|
if !strings.Contains(addr, ":") {
|
|
return addr, ""
|
|
}
|
|
host, port, _ = net.SplitHostPort(addr)
|
|
if host == "" {
|
|
host = addr
|
|
}
|
|
return
|
|
}
|
|
|
|
type ipMatcher struct {
|
|
ips map[netip.Addr]struct{}
|
|
}
|
|
|
|
// IPMatcher creates a Matcher for a list of IP addresses.
|
|
func IPMatcher(ips []net.IP) Matcher {
|
|
matcher := &ipMatcher{
|
|
ips: make(map[netip.Addr]struct{}),
|
|
}
|
|
for _, ip := range ips {
|
|
addr, ok := netip.AddrFromSlice(ip)
|
|
if !ok {
|
|
continue
|
|
}
|
|
matcher.ips[addr.Unmap()] = struct{}{}
|
|
}
|
|
return matcher
|
|
}
|
|
|
|
func (m *ipMatcher) Match(ip string) bool {
|
|
if m == nil || len(m.ips) == 0 {
|
|
return false
|
|
}
|
|
addr, err := netip.ParseAddr(ip)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, ok := m.ips[addr.Unmap()]
|
|
return ok
|
|
}
|
|
|
|
type addrMatcher struct {
|
|
addrs map[string][]*xnet.PortRange
|
|
}
|
|
|
|
// AddrMatcher creates a Matcher for a list of HOST:PORT addresses.
|
|
// The host can be an IP address (e.g. 192.168.1.1), a plain domain such as
|
|
// "example.com", or a special pattern ".example.com" that matches
|
|
// "example.com" and any subdomain "abc.example.com", "def.abc.example.com"
|
|
// etc. The PORT can be a single port number or port range MIN-MAX
|
|
// (e.g. 0-65535).
|
|
func AddrMatcher(addrs []string) Matcher {
|
|
matcher := &addrMatcher{
|
|
addrs: make(map[string][]*xnet.PortRange),
|
|
}
|
|
for _, addr := range addrs {
|
|
host, port := splitHostPort(addr)
|
|
if host == "" {
|
|
host = strings.ToLower(addr)
|
|
matcher.addrs[host] = append(matcher.addrs[host], nil)
|
|
continue
|
|
}
|
|
host = strings.ToLower(host)
|
|
pr := &xnet.PortRange{}
|
|
if err := pr.Parse(port); err != nil {
|
|
pr = nil
|
|
}
|
|
matcher.addrs[host] = append(matcher.addrs[host], pr)
|
|
}
|
|
return matcher
|
|
}
|
|
|
|
func (m *addrMatcher) Match(addr string) bool {
|
|
if m == nil || len(m.addrs) == 0 {
|
|
return false
|
|
}
|
|
host, sp := splitHostPort(addr)
|
|
host = strings.ToLower(host)
|
|
port, _ := strconv.Atoi(sp)
|
|
|
|
if prs, ok := m.addrs[host]; ok {
|
|
if portRangeMatch(prs, port) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if prs, ok := m.addrs["."+host]; ok {
|
|
if portRangeMatch(prs, port) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
for {
|
|
if index := strings.IndexByte(host, '.'); index > 0 {
|
|
if prs, ok := m.addrs[host[index:]]; ok {
|
|
if portRangeMatch(prs, port) {
|
|
return true
|
|
}
|
|
}
|
|
host = host[index+1:]
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// portRangeMatch returns true if any PortRange in prs contains port.
|
|
// A nil entry means "any port" and always matches.
|
|
func portRangeMatch(prs []*xnet.PortRange, port int) bool {
|
|
for _, pr := range prs {
|
|
if pr == nil || pr.Contains(port) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type cidrMatcher struct {
|
|
trie *cidrTrie
|
|
}
|
|
|
|
// CIDRMatcher creates a Matcher for a list of CIDR notation IP addresses.
|
|
func CIDRMatcher(inets []*net.IPNet) Matcher {
|
|
trie := newCIDRTrie()
|
|
for _, inet := range inets {
|
|
if prefix, ok := ipNetToPrefix(inet); ok {
|
|
trie.insert(prefix)
|
|
}
|
|
}
|
|
return &cidrMatcher{trie: trie}
|
|
}
|
|
|
|
func (m *cidrMatcher) Match(ip string) bool {
|
|
if m == nil || m.trie == nil {
|
|
return false
|
|
}
|
|
addr, err := netip.ParseAddr(ip)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return m.trie.contains(addr.Unmap())
|
|
}
|
|
|
|
type domainMatcher struct {
|
|
domains map[string]struct{}
|
|
}
|
|
|
|
// DomainMatcher creates a Matcher for a list of domains.
|
|
// The domain should be a plain domain such as "example.com",
|
|
// or a special pattern ".example.com" that matches "example.com"
|
|
// and any subdomain "abc.example.com", "def.abc.example.com" etc.
|
|
func DomainMatcher(domains []string) Matcher {
|
|
matcher := &domainMatcher{
|
|
domains: make(map[string]struct{}),
|
|
}
|
|
for _, domain := range domains {
|
|
matcher.domains[strings.ToLower(domain)] = struct{}{}
|
|
}
|
|
return matcher
|
|
}
|
|
|
|
func (m *domainMatcher) Match(domain string) bool {
|
|
if m == nil || len(m.domains) == 0 {
|
|
return false
|
|
}
|
|
|
|
domain = strings.ToLower(domain)
|
|
|
|
if _, ok := m.domains[domain]; ok {
|
|
return true
|
|
}
|
|
|
|
if _, ok := m.domains["."+domain]; ok {
|
|
return true
|
|
}
|
|
|
|
for {
|
|
if index := strings.IndexByte(domain, '.'); index > 0 {
|
|
if _, ok := m.domains[domain[index:]]; ok {
|
|
return true
|
|
}
|
|
domain = domain[index+1:]
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return false
|
|
}
|
|
|
|
type wildcardMatcherPattern struct {
|
|
glob glob.Glob
|
|
pr *xnet.PortRange
|
|
}
|
|
|
|
type wildcardMatcher struct {
|
|
patterns []wildcardMatcherPattern
|
|
}
|
|
|
|
// WildcardMatcher creates a Matcher for wildcard domain patterns.
|
|
// The pattern can be a wildcard such as "*.example.com",
|
|
// "*.example.com:80", or "*.example.com:0-65535".
|
|
func WildcardMatcher(patterns []string) Matcher {
|
|
matcher := &wildcardMatcher{}
|
|
for _, pattern := range patterns {
|
|
host, port := splitHostPort(pattern)
|
|
if host == "" {
|
|
host = pattern
|
|
}
|
|
g, err := glob.Compile(strings.ToLower(host))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pr := &xnet.PortRange{}
|
|
if err := pr.Parse(port); err != nil {
|
|
pr = nil
|
|
}
|
|
|
|
matcher.patterns = append(matcher.patterns, wildcardMatcherPattern{
|
|
glob: g,
|
|
pr: pr,
|
|
})
|
|
}
|
|
|
|
return matcher
|
|
}
|
|
|
|
func (m *wildcardMatcher) Match(addr string) bool {
|
|
if m == nil || len(m.patterns) == 0 {
|
|
return false
|
|
}
|
|
|
|
host, sp := splitHostPort(addr)
|
|
host = strings.ToLower(host)
|
|
port, _ := strconv.Atoi(sp)
|
|
for _, pattern := range m.patterns {
|
|
if pattern.glob.Match(host) {
|
|
if pattern.pr == nil || pattern.pr.Contains(port) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type ipRangeMatcher struct {
|
|
ranges []xnet.IPRange
|
|
}
|
|
|
|
// IPRangeMatcher creates a Matcher for a list of IP ranges (e.g. "192.168.1.1-192.168.1.255").
|
|
func IPRangeMatcher(ranges []xnet.IPRange) Matcher {
|
|
matcher := &ipRangeMatcher{
|
|
ranges: ranges,
|
|
}
|
|
return matcher
|
|
}
|
|
|
|
func (m *ipRangeMatcher) Match(addr string) bool {
|
|
if m == nil || len(m.ranges) == 0 {
|
|
return false
|
|
}
|
|
|
|
host, _ := splitHostPort(addr)
|
|
adr, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for _, ra := range m.ranges {
|
|
if ra.Contains(adr) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type nopMatcher struct{}
|
|
|
|
// NopMatcher creates a Matcher that never matches anything.
|
|
func NopMatcher() Matcher {
|
|
return &nopMatcher{}
|
|
}
|
|
|
|
func (m *nopMatcher) Match(addr string) bool {
|
|
return false
|
|
}
|