diff --git a/component/geodata/router/condition.go b/component/geodata/router/condition.go index 156614aeb..82684d93e 100644 --- a/component/geodata/router/condition.go +++ b/component/geodata/router/condition.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/metacubex/mihomo/component/geodata/strmatcher" + "github.com/metacubex/mihomo/component/trie" ) var matcherTypeMap = map[Domain_Type]strmatcher.Type{ @@ -31,12 +32,69 @@ func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) { return matcher, nil } -type DomainMatcher struct { +type DomainMatcher interface { + ApplyDomain(string) bool +} + +type succinctDomainMatcher struct { + set *trie.DomainSet + otherMatchers []strmatcher.Matcher + not bool +} + +func (m succinctDomainMatcher) ApplyDomain(domain string) bool { + isMatched := m.set.Has(domain) + if !isMatched { + for _, matcher := range m.otherMatchers { + isMatched = matcher.Match(domain) + if isMatched { + break + } + } + } + if m.not { + isMatched = !isMatched + } + return isMatched +} + +func NewSuccinctMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) { + t := trie.New[struct{}]() + m := &succinctDomainMatcher{ + not: not, + } + for _, d := range domains { + switch d.Type { + case Domain_Plain, Domain_Regex: + matcher, err := matcherTypeMap[d.Type].New(d.Value) + if err != nil { + return nil, err + } + m.otherMatchers = append(m.otherMatchers, matcher) + + case Domain_Domain: + err := t.Insert("+."+d.Value, struct{}{}) + if err != nil { + return nil, err + } + + case Domain_Full: + err := t.Insert(d.Value, struct{}{}) + if err != nil { + return nil, err + } + } + } + m.set = t.NewDomainSet() + return m, nil +} + +type v2rayDomainMatcher struct { matchers strmatcher.IndexMatcher not bool } -func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) { +func NewMphMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) { g := strmatcher.NewMphMatcherGroup() for _, d := range domains { matcherType, f := matcherTypeMap[d.Type] @@ -49,14 +107,13 @@ func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) { } } g.Build() - return &DomainMatcher{ + return &v2rayDomainMatcher{ matchers: g, not: not, }, nil } -// NewDomainMatcher new domain matcher. -func NewDomainMatcher(domains []*Domain, not bool) (*DomainMatcher, error) { +func NewDomainMatcher(domains []*Domain, not bool) (DomainMatcher, error) { g := new(strmatcher.MatcherGroup) for _, d := range domains { m, err := domainToMatcher(d) @@ -66,13 +123,13 @@ func NewDomainMatcher(domains []*Domain, not bool) (*DomainMatcher, error) { g.Add(m) } - return &DomainMatcher{ + return &v2rayDomainMatcher{ matchers: g, not: not, }, nil } -func (m *DomainMatcher) ApplyDomain(domain string) bool { +func (m *v2rayDomainMatcher) ApplyDomain(domain string) bool { isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0 if m.not { isMatched = !isMatched diff --git a/component/geodata/utils.go b/component/geodata/utils.go index 4716ccbde..33c12e68c 100644 --- a/component/geodata/utils.go +++ b/component/geodata/utils.go @@ -12,6 +12,7 @@ import ( ) var geoLoaderName = "memconservative" +var geoSiteMatcher = "succinct" // geoLoaderName = "standard" @@ -19,6 +20,10 @@ func LoaderName() string { return geoLoaderName } +func SiteMatcherName() string { + return geoSiteMatcher +} + func SetLoader(newLoader string) { if newLoader == "memc" { newLoader = "memconservative" @@ -26,6 +31,16 @@ func SetLoader(newLoader string) { geoLoaderName = newLoader } +func SetSiteMatcher(newMatcher string) { + switch newMatcher { + case "hybrid": + newMatcher = "mph" + default: + newMatcher = "succinct" + } + geoSiteMatcher = newMatcher +} + func Verify(name string) error { switch name { case C.GeositeName: @@ -41,8 +56,8 @@ func Verify(name string) error { var loadGeoSiteMatcherSF = singleflight.Group{} -func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { - if len(countryCode) == 0 { +func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, int, error) { + if countryCode == "" { return nil, 0, fmt.Errorf("country code could not be empty") } @@ -60,7 +75,7 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) listName := strings.TrimSpace(parts[0]) attrVal := parts[1:] - if len(listName) == 0 { + if listName == "" { return nil, 0, fmt.Errorf("empty listname in rule: %s", countryCode) } @@ -104,7 +119,12 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) matcher, err := router.NewDomainMatcher(domains) mph:minimal perfect hash algorithm */ - matcher, err := router.NewMphMatcherGroup(domains, not) + var matcher router.DomainMatcher + if geoSiteMatcher == "mph" { + matcher, err = router.NewMphMatcherGroup(domains, not) + } else { + matcher, err = router.NewSuccinctMatcherGroup(domains, not) + } if err != nil { return nil, 0, err } diff --git a/config/config.go b/config/config.go index 469a58ca8..9eaf5a494 100644 --- a/config/config.go +++ b/config/config.go @@ -59,6 +59,7 @@ type General struct { GeoUpdateInterval int `json:"geo-update-interval"` GeodataMode bool `json:"geodata-mode"` GeodataLoader string `json:"geodata-loader"` + GeositeMatcher string `json:"geosite-matcher"` TCPConcurrent bool `json:"tcp-concurrent"` FindProcessMode P.FindProcessMode `json:"find-process-mode"` Sniffing bool `json:"sniffing"` @@ -127,11 +128,11 @@ type DNS struct { // FallbackFilter config type FallbackFilter struct { - GeoIP bool `yaml:"geoip"` - GeoIPCode string `yaml:"geoip-code"` - IPCIDR []netip.Prefix `yaml:"ipcidr"` - Domain []string `yaml:"domain"` - GeoSite []*router.DomainMatcher `yaml:"geosite"` + GeoIP bool `yaml:"geoip"` + GeoIPCode string `yaml:"geoip-code"` + IPCIDR []netip.Prefix `yaml:"ipcidr"` + Domain []string `yaml:"domain"` + GeoSite []router.DomainMatcher `yaml:"geosite"` } // Profile config @@ -312,6 +313,7 @@ type RawConfig struct { GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"` GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"` GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"` + GeositeMatcher string `yaml:"geosite-matcher" json:"geosite-matcher"` TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` GlobalClientFingerprint string `yaml:"global-client-fingerprint"` @@ -537,6 +539,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config.Listeners = listener log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) + log.Infoln("Geosite Matcher implementation: %s", geodata.SiteMatcherName()) ruleProviders, err := parseRuleProviders(rawCfg) if err != nil { return nil, err @@ -605,6 +608,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { func parseGeneral(cfg *RawConfig) (*General, error) { geodata.SetLoader(cfg.GeodataLoader) + geodata.SetSiteMatcher(cfg.GeositeMatcher) C.GeoAutoUpdate = cfg.GeoAutoUpdate C.GeoUpdateInterval = cfg.GeoUpdateInterval C.GeoIpUrl = cfg.GeoXUrl.GeoIp @@ -1204,8 +1208,8 @@ func parseFallbackIPCIDR(ips []string) ([]netip.Prefix, error) { return ipNets, nil } -func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainMatcher, error) { - var sites []*router.DomainMatcher +func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]router.DomainMatcher, error) { + var sites []router.DomainMatcher if len(countries) > 0 { if err := geodata.InitGeoSite(); err != nil { return nil, fmt.Errorf("can't initial GeoSite: %s", err) @@ -1267,7 +1271,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul EnhancedMode: cfg.EnhancedMode, FallbackFilter: FallbackFilter{ IPCIDR: []netip.Prefix{}, - GeoSite: []*router.DomainMatcher{}, + GeoSite: []router.DomainMatcher{}, }, } var err error diff --git a/constant/rule_extra.go b/constant/rule_extra.go index 62dc1cc37..b4ba65d99 100644 --- a/constant/rule_extra.go +++ b/constant/rule_extra.go @@ -5,7 +5,7 @@ import ( ) type RuleGeoSite interface { - GetDomainMatcher() *router.DomainMatcher + GetDomainMatcher() router.DomainMatcher } type RuleGeoIP interface { diff --git a/dns/filters.go b/dns/filters.go index 8eb1e48e2..46244c35e 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -74,7 +74,7 @@ func (df *domainFilter) Match(domain string) bool { } type geoSiteFilter struct { - matchers []*router.DomainMatcher + matchers []router.DomainMatcher } func NewGeoSite(group string) (fallbackDomainFilter, error) { @@ -87,7 +87,7 @@ func NewGeoSite(group string) (fallbackDomainFilter, error) { return nil, err } filter := &geoSiteFilter{ - matchers: []*router.DomainMatcher{matcher}, + matchers: []router.DomainMatcher{matcher}, } return filter, nil } diff --git a/dns/resolver.go b/dns/resolver.go index 47ea78d8d..8ea68ed70 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -400,7 +400,7 @@ type FallbackFilter struct { GeoIPCode string IPCIDR []netip.Prefix Domain []string - GeoSite []*router.DomainMatcher + GeoSite []router.DomainMatcher } type Config struct { diff --git a/docs/config.yaml b/docs/config.yaml index f69ab41f0..702ff8316 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -36,6 +36,11 @@ geox-url: geo-auto-update: false # 是否自动更新 geodata geo-update-interval: 24 # 更新间隔,单位:小时 +# Matcher implementation used by GeoSite, available implementations: +# - succinct (default, same as rule-set) +# - mph (from V2Ray, also `hybrid` in Xray) +# geosite-matcher: succinct + log-level: debug # 日志等级 silent/error/warning/info/debug ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 diff --git a/hub/executor/executor.go b/hub/executor/executor.go index b9e27bfa3..783da4d3d 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -146,14 +146,15 @@ func GetGeneral() *config.General { AllowLan: listener.AllowLan(), BindAddress: listener.BindAddress(), }, - Controller: config.Controller{}, - Mode: tunnel.Mode(), - LogLevel: log.Level(), - IPv6: !resolver.DisableIPv6, - GeodataLoader: G.LoaderName(), - Interface: dialer.DefaultInterface.Load(), - Sniffing: tunnel.IsSniffing(), - TCPConcurrent: dialer.GetTcpConcurrent(), + Controller: config.Controller{}, + Mode: tunnel.Mode(), + LogLevel: log.Level(), + IPv6: !resolver.DisableIPv6, + GeodataLoader: G.LoaderName(), + GeositeMatcher: G.SiteMatcherName(), + Interface: dialer.DefaultInterface.Load(), + Sniffing: tunnel.IsSniffing(), + TCPConcurrent: dialer.GetTcpConcurrent(), } return general @@ -401,8 +402,8 @@ func updateGeneral(general *config.General) { } iface.FlushCache() - geodataLoader := general.GeodataLoader - G.SetLoader(geodataLoader) + G.SetLoader(general.GeodataLoader) + G.SetSiteMatcher(general.GeositeMatcher) } func updateUsers(users []auth.AuthUser) { diff --git a/rules/common/geosite.go b/rules/common/geosite.go index e9b19d0ef..1e3c1ab5a 100644 --- a/rules/common/geosite.go +++ b/rules/common/geosite.go @@ -15,7 +15,7 @@ type GEOSITE struct { *Base country string adapter string - matcher *router.DomainMatcher + matcher router.DomainMatcher recodeSize int } @@ -39,7 +39,7 @@ func (gs *GEOSITE) Payload() string { return gs.country } -func (gs *GEOSITE) GetDomainMatcher() *router.DomainMatcher { +func (gs *GEOSITE) GetDomainMatcher() router.DomainMatcher { return gs.matcher }