mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-08 14:13:22 +00:00
Geodata: Reduce memory usage again (#5975)
https://github.com/XTLS/Xray-core/pull/5975#issuecomment-4274779560
This commit is contained in:
@@ -158,10 +158,13 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
|
|||||||
clients = append(clients, client)
|
clients = append(clients, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
var domainMatcher geodata.DomainMatcher
|
||||||
|
if len(effectiveRules) > 0 {
|
||||||
|
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If there is no DNS client in config, add a `localhost` DNS client
|
// If there is no DNS client in config, add a `localhost` DNS client
|
||||||
if len(clients) == 0 {
|
if len(clients) == 0 {
|
||||||
@@ -271,6 +274,7 @@ func (s *DNS) sortClients(domain string) []*Client {
|
|||||||
|
|
||||||
// Priority domain matching
|
// Priority domain matching
|
||||||
hasMatch := false
|
hasMatch := false
|
||||||
|
if s.domainMatcher != nil {
|
||||||
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
|
||||||
sort.Slice(matchSlice, func(i, j int) bool {
|
sort.Slice(matchSlice, func(i, j int) bool {
|
||||||
return matchSlice[i] < matchSlice[j]
|
return matchSlice[i] < matchSlice[j]
|
||||||
@@ -292,6 +296,7 @@ func (s *DNS) sortClients(domain string) []*Client {
|
|||||||
return clients
|
return clients
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !(s.disableFallback || s.disableFallbackIfMatch && hasMatch) {
|
if !(s.disableFallback || s.disableFallbackIfMatch && hasMatch) {
|
||||||
// Default round-robin query
|
// Default round-robin query
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// StaticHosts represents static domain-ip mapping in DNS server.
|
// StaticHosts represents static domain-ip mapping in DNS server.
|
||||||
type StaticHosts struct {
|
type StaticHosts struct {
|
||||||
reps [][]net.Address
|
responses [][]net.Address
|
||||||
matcher geodata.DomainMatcher
|
matcher geodata.DomainMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,20 +45,20 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
|
|||||||
rep = append(rep, addr)
|
rep = append(rep, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if len(rep) == 0 {
|
|
||||||
// errors.LogError(context.Background(), "empty value in static hosts, ignore this rule: ", mapping.Domain)
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
reps = append(reps, rep)
|
reps = append(reps, rep)
|
||||||
rules = append(rules, mapping.Domain)
|
rules = append(rules, mapping.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return &StaticHosts{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
|
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &StaticHosts{
|
return &StaticHosts{
|
||||||
reps: reps,
|
responses: reps,
|
||||||
matcher: matcher,
|
matcher: matcher,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
|
|||||||
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
|
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
|
||||||
ips := make([]net.Address, 0)
|
ips := make([]net.Address, 0)
|
||||||
found := false
|
found := false
|
||||||
for _, ruleIdx := range h.matcher.Match(domain) {
|
for _, idx := range h.matcher.Match(domain) {
|
||||||
for _, rep := range h.reps[ruleIdx] {
|
for _, rep := range h.responses[idx] {
|
||||||
if err, ok := rep.(dns.RCodeError); ok {
|
if err, ok := rep.(dns.RCodeError); ok {
|
||||||
if uint16(err) == 0 {
|
if uint16(err) == 0 {
|
||||||
return nil, dns.ErrEmptyResponse
|
return nil, dns.ErrEmptyResponse
|
||||||
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ips = append(ips, h.reps[ruleIdx]...)
|
ips = append(ips, h.responses[idx]...)
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
@@ -122,5 +122,8 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
|
|||||||
|
|
||||||
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
|
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
|
||||||
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
|
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
|
||||||
|
if h.matcher == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return h.lookup(domain, option, 5)
|
return h.lookup(domain, option, 5)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,54 @@ type DomainMatcherFactory interface {
|
|||||||
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
|
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MphDomainMatcherFactory struct{}
|
type MphDomainMatcherFactory struct {
|
||||||
|
sync.Mutex
|
||||||
|
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDomainRulesKey(rules []*DomainRule) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
cache := false
|
||||||
|
for _, r := range rules {
|
||||||
|
switch v := r.Value.(type) {
|
||||||
|
case *DomainRule_Custom:
|
||||||
|
sb.WriteString(v.Custom.Type.String())
|
||||||
|
sb.WriteString(":")
|
||||||
|
sb.WriteString(v.Custom.Value)
|
||||||
|
sb.WriteString(",")
|
||||||
|
case *DomainRule_Geosite:
|
||||||
|
cache = true
|
||||||
|
sb.WriteString(v.Geosite.File)
|
||||||
|
sb.WriteString(":")
|
||||||
|
sb.WriteString(v.Geosite.Code)
|
||||||
|
sb.WriteString("@")
|
||||||
|
sb.WriteString(v.Geosite.Attrs)
|
||||||
|
sb.WriteString(",")
|
||||||
|
default:
|
||||||
|
panic("unknown domain rule type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cache {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
// BuildMatcher implements DomainMatcherFactory.
|
// BuildMatcher implements DomainMatcherFactory.
|
||||||
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil, errors.New("empty domain rule list")
|
||||||
|
}
|
||||||
|
key := buildDomainRulesKey(rules)
|
||||||
|
if key != "" {
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
if g := f.shared[key]; g != nil {
|
||||||
|
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
|
||||||
|
}
|
||||||
g := strmatcher.NewMphValueMatcher()
|
g := strmatcher.NewMphValueMatcher()
|
||||||
for i, r := range rules {
|
for i, r := range rules {
|
||||||
switch v := r.Value.(type) {
|
switch v := r.Value.(type) {
|
||||||
@@ -57,25 +101,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
|
|||||||
if err := g.Build(); err != nil {
|
if err := g.Build(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if key != "" {
|
||||||
|
f.shared[key] = g
|
||||||
|
}
|
||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactDomainMatcherFactory struct {
|
type CompactDomainMatcherFactory struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
|
shared map[string]strmatcher.MatcherSet // TODO: cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, error) {
|
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
|
||||||
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
|
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
|
||||||
|
|
||||||
f.Lock()
|
f.Lock()
|
||||||
defer f.Unlock()
|
defer f.Unlock()
|
||||||
|
|
||||||
if m := f.shared[key]; m != nil {
|
if s := f.shared[key]; s != nil {
|
||||||
return m, nil
|
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
|
||||||
|
|
||||||
g := strmatcher.NewLinearValueMatcher()
|
s := strmatcher.NewLinearAnyMatcher()
|
||||||
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
|
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -87,16 +136,19 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
|
|||||||
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
|
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
g.Add(m, 0)
|
s.Add(m)
|
||||||
}
|
}
|
||||||
f.shared[key] = g
|
f.shared[key] = s
|
||||||
return g, err
|
return s, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildMatcher implements DomainMatcherFactory.
|
// BuildMatcher implements DomainMatcherFactory.
|
||||||
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil, errors.New("empty domain rule list")
|
||||||
|
}
|
||||||
compact := &CompactDomainMatcher{
|
compact := &CompactDomainMatcher{
|
||||||
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
|
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
|
||||||
values: make([]uint32, 0, len(rules)),
|
values: make([]uint32, 0, len(rules)),
|
||||||
}
|
}
|
||||||
for i, r := range rules {
|
for i, r := range rules {
|
||||||
@@ -126,7 +178,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
|
|||||||
|
|
||||||
type CompactDomainMatcher struct {
|
type CompactDomainMatcher struct {
|
||||||
custom strmatcher.ValueMatcher
|
custom strmatcher.ValueMatcher
|
||||||
matchers []strmatcher.MatcherGroup
|
matchers []strmatcher.MatcherSet
|
||||||
values []uint32
|
values []uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +230,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
|||||||
func newDomainMatcherFactory() DomainMatcherFactory {
|
func newDomainMatcherFactory() DomainMatcherFactory {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "ios", "android":
|
case "ios", "android":
|
||||||
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||||
default:
|
default:
|
||||||
return &MphDomainMatcherFactory{}
|
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
||||||
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||||
@@ -31,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
|||||||
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
||||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||||
|
|
||||||
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
|
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
|
||||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
||||||
@@ -50,7 +50,7 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
||||||
matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
|
matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -816,8 +816,10 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
|
|||||||
defer f.Unlock()
|
defer f.Unlock()
|
||||||
|
|
||||||
if ipset := f.shared[key]; ipset != nil {
|
if ipset := f.shared[key]; ipset != nil {
|
||||||
|
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
|
||||||
return ipset, nil
|
return ipset, nil
|
||||||
}
|
}
|
||||||
|
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
|
||||||
|
|
||||||
ipset, err := f.createFrom(func(add func(*CIDR)) error {
|
ipset, err := f.createFrom(func(add func(*CIDR)) error {
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
|
|||||||
53
common/geodata/strmatcher/anymatcher_linear.go
Normal file
53
common/geodata/strmatcher/anymatcher_linear.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
// LinearAnyMatcher is an implementation of AnyMatcher.
|
||||||
|
type LinearAnyMatcher struct {
|
||||||
|
full *FullMatcherSet
|
||||||
|
domain *DomainMatcherSet
|
||||||
|
substr *SubstrMatcherSet
|
||||||
|
regex *SimpleMatcherSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLinearAnyMatcher() *LinearAnyMatcher {
|
||||||
|
return new(LinearAnyMatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add implements AnyMatcher.Add.
|
||||||
|
func (s *LinearAnyMatcher) Add(matcher Matcher) {
|
||||||
|
switch matcher := matcher.(type) {
|
||||||
|
case FullMatcher:
|
||||||
|
if s.full == nil {
|
||||||
|
s.full = NewFullMatcherSet()
|
||||||
|
}
|
||||||
|
s.full.AddFullMatcher(matcher)
|
||||||
|
case DomainMatcher:
|
||||||
|
if s.domain == nil {
|
||||||
|
s.domain = NewDomainMatcherSet()
|
||||||
|
}
|
||||||
|
s.domain.AddDomainMatcher(matcher)
|
||||||
|
case SubstrMatcher:
|
||||||
|
if s.substr == nil {
|
||||||
|
s.substr = new(SubstrMatcherSet)
|
||||||
|
}
|
||||||
|
s.substr.AddSubstrMatcher(matcher)
|
||||||
|
default:
|
||||||
|
if s.regex == nil {
|
||||||
|
s.regex = new(SimpleMatcherSet)
|
||||||
|
}
|
||||||
|
s.regex.AddMatcher(matcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAny implements AnyMatcher.MatchAny.
|
||||||
|
func (s *LinearAnyMatcher) MatchAny(input string) bool {
|
||||||
|
if s.full != nil && s.full.MatchAny(input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.domain != nil && s.domain.MatchAny(input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.substr != nil && s.substr.MatchAny(input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return s.regex != nil && s.regex.MatchAny(input)
|
||||||
|
}
|
||||||
@@ -288,3 +288,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
|
||||||
|
type MatcherSetForAll interface {
|
||||||
|
AddMatcher(matcher Matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
|
||||||
|
type MatcherSetForFull interface {
|
||||||
|
AddFullMatcher(matcher FullMatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
|
||||||
|
type MatcherSetForDomain interface {
|
||||||
|
AddDomainMatcher(matcher DomainMatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
|
||||||
|
type MatcherSetForSubstr interface {
|
||||||
|
AddSubstrMatcher(matcher SubstrMatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
|
||||||
|
type MatcherSetForRegex interface {
|
||||||
|
AddRegexMatcher(matcher *RegexMatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
|
||||||
|
// It returns error if the MatcherSet does not accept the provided Matcher's type.
|
||||||
|
// This function is provided to help writing code to test a MatcherSet.
|
||||||
|
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
|
||||||
|
if s, ok := s.(IndexMatcher); ok {
|
||||||
|
s.Add(matcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s, ok := s.(MatcherSetForAll); ok {
|
||||||
|
s.AddMatcher(matcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch matcher := matcher.(type) {
|
||||||
|
case FullMatcher:
|
||||||
|
if s, ok := s.(MatcherSetForFull); ok {
|
||||||
|
s.AddFullMatcher(matcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case DomainMatcher:
|
||||||
|
if s, ok := s.(MatcherSetForDomain); ok {
|
||||||
|
s.AddDomainMatcher(matcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case SubstrMatcher:
|
||||||
|
if s, ok := s.(MatcherSetForSubstr); ok {
|
||||||
|
s.AddSubstrMatcher(matcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case *RegexMatcher:
|
||||||
|
if s, ok := s.(MatcherSetForRegex); ok {
|
||||||
|
s.AddRegexMatcher(matcher)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("cannot add matcher to matcher set")
|
||||||
|
}
|
||||||
|
|||||||
79
common/geodata/strmatcher/matcherset_domain.go
Normal file
79
common/geodata/strmatcher/matcherset_domain.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
type trieNode2 struct {
|
||||||
|
matched bool
|
||||||
|
children map[string]*trieNode2
|
||||||
|
}
|
||||||
|
|
||||||
|
// DomainMatcherSet is an implementation of MatcherSet.
|
||||||
|
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
|
||||||
|
type DomainMatcherSet struct {
|
||||||
|
root *trieNode2
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainMatcherSet() *DomainMatcherSet {
|
||||||
|
return &DomainMatcherSet{
|
||||||
|
root: new(trieNode2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
|
||||||
|
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
|
||||||
|
node := s.root
|
||||||
|
pattern := matcher.Pattern()
|
||||||
|
for i := len(pattern); i > 0; {
|
||||||
|
var part string
|
||||||
|
for j := i - 1; ; j-- {
|
||||||
|
if pattern[j] == '.' {
|
||||||
|
part = pattern[j+1 : i]
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if j == 0 {
|
||||||
|
part = pattern[j:i]
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.children == nil {
|
||||||
|
node.children = make(map[string]*trieNode2)
|
||||||
|
}
|
||||||
|
next := node.children[part]
|
||||||
|
if next == nil {
|
||||||
|
next = new(trieNode2)
|
||||||
|
node.children[part] = next
|
||||||
|
}
|
||||||
|
node = next
|
||||||
|
}
|
||||||
|
|
||||||
|
node.matched = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAny implements MatcherSet.MatchAny.
|
||||||
|
func (s *DomainMatcherSet) MatchAny(input string) bool {
|
||||||
|
node := s.root
|
||||||
|
for i := len(input); i > 0; {
|
||||||
|
for j := i - 1; ; j-- {
|
||||||
|
if input[j] == '.' {
|
||||||
|
node = node.children[input[j+1:i]]
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if j == 0 {
|
||||||
|
node = node.children[input[j:i]]
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if node.matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if node.children == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
95
common/geodata/strmatcher/matcherset_domain_test.go
Normal file
95
common/geodata/strmatcher/matcherset_domain_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package strmatcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDomainMatcherSet(t *testing.T) {
|
||||||
|
patterns := []struct {
|
||||||
|
Pattern string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Pattern: "example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "google.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "x.a.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "a.b.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "c.a.b.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "x.y.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "x.y.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
Domain string
|
||||||
|
Result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Domain: "x.example.com",
|
||||||
|
Result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "y.com",
|
||||||
|
Result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "a.b.com",
|
||||||
|
Result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "c.a.b.com",
|
||||||
|
Result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "c.a..b.com",
|
||||||
|
Result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: ".com",
|
||||||
|
Result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "com",
|
||||||
|
Result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "",
|
||||||
|
Result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "x.y.com",
|
||||||
|
Result: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s := NewDomainMatcherSet()
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
r := s.MatchAny(testCase.Domain)
|
||||||
|
if !reflect.DeepEqual(r, testCase.Result) {
|
||||||
|
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyDomainMatcherSet(t *testing.T) {
|
||||||
|
s := NewDomainMatcherSet()
|
||||||
|
r := s.MatchAny("example.com")
|
||||||
|
if r {
|
||||||
|
t.Error("Expect false, but ", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
common/geodata/strmatcher/matcherset_full.go
Normal file
24
common/geodata/strmatcher/matcherset_full.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
// FullMatcherSet is an implementation of MatcherSet.
|
||||||
|
// It uses a hash table to facilitate exact match lookup.
|
||||||
|
type FullMatcherSet struct {
|
||||||
|
matchers map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFullMatcherSet() *FullMatcherSet {
|
||||||
|
return &FullMatcherSet{
|
||||||
|
matchers: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
|
||||||
|
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
|
||||||
|
s.matchers[matcher.Pattern()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAny implements MatcherSet.Any.
|
||||||
|
func (s *FullMatcherSet) MatchAny(input string) bool {
|
||||||
|
_, found := s.matchers[input]
|
||||||
|
return found
|
||||||
|
}
|
||||||
65
common/geodata/strmatcher/matcherset_full_test.go
Normal file
65
common/geodata/strmatcher/matcherset_full_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package strmatcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFullMatcherSet(t *testing.T) {
|
||||||
|
patterns := []struct {
|
||||||
|
Pattern string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Pattern: "example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "google.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "x.a.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "x.y.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "x.y.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
Domain string
|
||||||
|
Result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Domain: "example.com",
|
||||||
|
Result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "y.com",
|
||||||
|
Result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domain: "x.y.com",
|
||||||
|
Result: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s := NewFullMatcherSet()
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
r := s.MatchAny(testCase.Domain)
|
||||||
|
if !reflect.DeepEqual(r, testCase.Result) {
|
||||||
|
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyFullMatcherSet(t *testing.T) {
|
||||||
|
s := NewFullMatcherSet()
|
||||||
|
r := s.MatchAny("example.com")
|
||||||
|
if r {
|
||||||
|
t.Error("Expect false, but ", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
common/geodata/strmatcher/matcherset_simple.go
Normal file
22
common/geodata/strmatcher/matcherset_simple.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
// SimpleMatcherSet is an implementation of MatcherSet.
|
||||||
|
// It simply stores all matchers in an array and sequentially matches them.
|
||||||
|
type SimpleMatcherSet struct {
|
||||||
|
matchers []Matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMatcher implements MatcherSetForAll.AddMatcher.
|
||||||
|
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
|
||||||
|
s.matchers = append(s.matchers, matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAny implements MatcherSet.MatchAny.
|
||||||
|
func (s *SimpleMatcherSet) MatchAny(input string) bool {
|
||||||
|
for _, m := range s.matchers {
|
||||||
|
if m.Match(input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
69
common/geodata/strmatcher/matcherset_simple_test.go
Normal file
69
common/geodata/strmatcher/matcherset_simple_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package strmatcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common"
|
||||||
|
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimpleMatcherSet(t *testing.T) {
|
||||||
|
patterns := []struct {
|
||||||
|
pattern string
|
||||||
|
mType Type
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pattern: "example.com",
|
||||||
|
mType: Domain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "example.com",
|
||||||
|
mType: Full,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "example.com",
|
||||||
|
mType: Regex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
output bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "www.example.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "example.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "www.e3ample.com",
|
||||||
|
output: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "xample.com",
|
||||||
|
output: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "xexample.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "examplexcom",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
matcherSet := &SimpleMatcherSet{}
|
||||||
|
for _, entry := range patterns {
|
||||||
|
matcher, err := entry.mType.New(entry.pattern)
|
||||||
|
common.Must(err)
|
||||||
|
common.Must(AddMatcherToSet(matcherSet, matcher))
|
||||||
|
}
|
||||||
|
for _, test := range cases {
|
||||||
|
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
|
||||||
|
t.Error("unexpected output: ", r, " for test case ", test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
common/geodata/strmatcher/matcherset_substr.go
Normal file
24
common/geodata/strmatcher/matcherset_substr.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// SubstrMatcherSet is implementation of MatcherSet,
|
||||||
|
// It is simply implmeneted to comply with the priority specification of Substr matchers.
|
||||||
|
type SubstrMatcherSet struct {
|
||||||
|
patterns []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
|
||||||
|
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
|
||||||
|
s.patterns = append(s.patterns, matcher.Pattern())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAny implements MatcherSet.MatchAny.
|
||||||
|
func (s *SubstrMatcherSet) MatchAny(input string) bool {
|
||||||
|
for _, pattern := range s.patterns {
|
||||||
|
if strings.Contains(input, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
77
common/geodata/strmatcher/matcherset_substr_test.go
Normal file
77
common/geodata/strmatcher/matcherset_substr_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package strmatcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common"
|
||||||
|
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubstrMatcherSet(t *testing.T) {
|
||||||
|
patterns := []struct {
|
||||||
|
pattern string
|
||||||
|
mType Type
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pattern: "apis",
|
||||||
|
mType: Substr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "google",
|
||||||
|
mType: Substr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "apis",
|
||||||
|
mType: Substr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
output bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "google.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "apis.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "googleapis.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "fonts.googleapis.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "apis.googleapis.com",
|
||||||
|
output: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "baidu.com",
|
||||||
|
output: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "goog",
|
||||||
|
output: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "api",
|
||||||
|
output: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
matcherSet := &SubstrMatcherSet{}
|
||||||
|
for _, entry := range patterns {
|
||||||
|
matcher, err := entry.mType.New(entry.pattern)
|
||||||
|
common.Must(err)
|
||||||
|
common.Must(AddMatcherToSet(matcherSet, matcher))
|
||||||
|
}
|
||||||
|
for _, test := range cases {
|
||||||
|
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
|
||||||
|
t.Error("unexpected output: ", r, " for test case ", test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Matcher is the interface to determine a string matches a pattern.
|
// Matcher is the interface to determine a string matches a pattern.
|
||||||
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
|
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
|
||||||
type Matcher interface {
|
type Matcher interface {
|
||||||
// Type returns the matcher's type.
|
// Type returns the matcher's type.
|
||||||
Type() Type
|
Type() Type
|
||||||
@@ -101,3 +101,21 @@ type ValueMatcher interface {
|
|||||||
// MatchAny returns true as soon as one matching matcher is found.
|
// MatchAny returns true as soon as one matching matcher is found.
|
||||||
MatchAny(input string) bool
|
MatchAny(input string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
|
||||||
|
// For example:
|
||||||
|
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
|
||||||
|
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
|
||||||
|
type MatcherSet interface {
|
||||||
|
// MatchAny returns true as soon as one matching matcher is found.
|
||||||
|
MatchAny(input string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
|
||||||
|
type AnyMatcher interface {
|
||||||
|
// Add adds a new Matcher to AnyMatcher.
|
||||||
|
Add(matcher Matcher)
|
||||||
|
|
||||||
|
// MatchAny returns true as soon as one matching matcher is found.
|
||||||
|
MatchAny(input string) bool
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user