feat: Add succinct matcher support for GeoSite

and use it by default
This commit is contained in:
H1JK 2023-12-17 00:00:35 +08:00
parent 5b23b979df
commit 2bba8aa14a
9 changed files with 122 additions and 35 deletions

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/metacubex/mihomo/component/geodata/strmatcher" "github.com/metacubex/mihomo/component/geodata/strmatcher"
"github.com/metacubex/mihomo/component/trie"
) )
var matcherTypeMap = map[Domain_Type]strmatcher.Type{ var matcherTypeMap = map[Domain_Type]strmatcher.Type{
@ -31,12 +32,69 @@ func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) {
return matcher, nil 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 matchers strmatcher.IndexMatcher
not bool not bool
} }
func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) { func NewMphMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) {
g := strmatcher.NewMphMatcherGroup() g := strmatcher.NewMphMatcherGroup()
for _, d := range domains { for _, d := range domains {
matcherType, f := matcherTypeMap[d.Type] matcherType, f := matcherTypeMap[d.Type]
@ -49,14 +107,13 @@ func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) {
} }
} }
g.Build() g.Build()
return &DomainMatcher{ return &v2rayDomainMatcher{
matchers: g, matchers: g,
not: not, not: not,
}, nil }, 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) g := new(strmatcher.MatcherGroup)
for _, d := range domains { for _, d := range domains {
m, err := domainToMatcher(d) m, err := domainToMatcher(d)
@ -66,13 +123,13 @@ func NewDomainMatcher(domains []*Domain, not bool) (*DomainMatcher, error) {
g.Add(m) g.Add(m)
} }
return &DomainMatcher{ return &v2rayDomainMatcher{
matchers: g, matchers: g,
not: not, not: not,
}, nil }, nil
} }
func (m *DomainMatcher) ApplyDomain(domain string) bool { func (m *v2rayDomainMatcher) ApplyDomain(domain string) bool {
isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0 isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0
if m.not { if m.not {
isMatched = !isMatched isMatched = !isMatched

View File

@ -12,6 +12,7 @@ import (
) )
var geoLoaderName = "memconservative" var geoLoaderName = "memconservative"
var geoSiteMatcher = "succinct"
// geoLoaderName = "standard" // geoLoaderName = "standard"
@ -19,6 +20,10 @@ func LoaderName() string {
return geoLoaderName return geoLoaderName
} }
func SiteMatcherName() string {
return geoSiteMatcher
}
func SetLoader(newLoader string) { func SetLoader(newLoader string) {
if newLoader == "memc" { if newLoader == "memc" {
newLoader = "memconservative" newLoader = "memconservative"
@ -26,6 +31,16 @@ func SetLoader(newLoader string) {
geoLoaderName = newLoader geoLoaderName = newLoader
} }
func SetSiteMatcher(newMatcher string) {
switch newMatcher {
case "hybrid":
newMatcher = "mph"
default:
newMatcher = "succinct"
}
geoSiteMatcher = newMatcher
}
func Verify(name string) error { func Verify(name string) error {
switch name { switch name {
case C.GeositeName: case C.GeositeName:
@ -41,8 +56,8 @@ func Verify(name string) error {
var loadGeoSiteMatcherSF = singleflight.Group{} var loadGeoSiteMatcherSF = singleflight.Group{}
func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, int, error) {
if len(countryCode) == 0 { if countryCode == "" {
return nil, 0, fmt.Errorf("country code could not be empty") 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]) listName := strings.TrimSpace(parts[0])
attrVal := parts[1:] attrVal := parts[1:]
if len(listName) == 0 { if listName == "" {
return nil, 0, fmt.Errorf("empty listname in rule: %s", countryCode) 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) matcher, err := router.NewDomainMatcher(domains)
mphminimal perfect hash algorithm mphminimal 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }

View File

@ -59,6 +59,7 @@ type General struct {
GeoUpdateInterval int `json:"geo-update-interval"` GeoUpdateInterval int `json:"geo-update-interval"`
GeodataMode bool `json:"geodata-mode"` GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"` GeodataLoader string `json:"geodata-loader"`
GeositeMatcher string `json:"geosite-matcher"`
TCPConcurrent bool `json:"tcp-concurrent"` TCPConcurrent bool `json:"tcp-concurrent"`
FindProcessMode P.FindProcessMode `json:"find-process-mode"` FindProcessMode P.FindProcessMode `json:"find-process-mode"`
Sniffing bool `json:"sniffing"` Sniffing bool `json:"sniffing"`
@ -127,11 +128,11 @@ type DNS struct {
// FallbackFilter config // FallbackFilter config
type FallbackFilter struct { type FallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
GeoIPCode string `yaml:"geoip-code"` GeoIPCode string `yaml:"geoip-code"`
IPCIDR []netip.Prefix `yaml:"ipcidr"` IPCIDR []netip.Prefix `yaml:"ipcidr"`
Domain []string `yaml:"domain"` Domain []string `yaml:"domain"`
GeoSite []*router.DomainMatcher `yaml:"geosite"` GeoSite []router.DomainMatcher `yaml:"geosite"`
} }
// Profile config // Profile config
@ -312,6 +313,7 @@ type RawConfig struct {
GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"` GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"`
GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"` GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"` GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"`
GeositeMatcher string `yaml:"geosite-matcher" json:"geosite-matcher"`
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"`
GlobalClientFingerprint string `yaml:"global-client-fingerprint"` GlobalClientFingerprint string `yaml:"global-client-fingerprint"`
@ -537,6 +539,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Listeners = listener config.Listeners = listener
log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) log.Infoln("Geodata Loader mode: %s", geodata.LoaderName())
log.Infoln("Geosite Matcher implementation: %s", geodata.SiteMatcherName())
ruleProviders, err := parseRuleProviders(rawCfg) ruleProviders, err := parseRuleProviders(rawCfg)
if err != nil { if err != nil {
return nil, err return nil, err
@ -605,6 +608,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
func parseGeneral(cfg *RawConfig) (*General, error) { func parseGeneral(cfg *RawConfig) (*General, error) {
geodata.SetLoader(cfg.GeodataLoader) geodata.SetLoader(cfg.GeodataLoader)
geodata.SetSiteMatcher(cfg.GeositeMatcher)
C.GeoAutoUpdate = cfg.GeoAutoUpdate C.GeoAutoUpdate = cfg.GeoAutoUpdate
C.GeoUpdateInterval = cfg.GeoUpdateInterval C.GeoUpdateInterval = cfg.GeoUpdateInterval
C.GeoIpUrl = cfg.GeoXUrl.GeoIp C.GeoIpUrl = cfg.GeoXUrl.GeoIp
@ -1204,8 +1208,8 @@ func parseFallbackIPCIDR(ips []string) ([]netip.Prefix, error) {
return ipNets, nil return ipNets, nil
} }
func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainMatcher, error) { func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]router.DomainMatcher, error) {
var sites []*router.DomainMatcher var sites []router.DomainMatcher
if len(countries) > 0 { if len(countries) > 0 {
if err := geodata.InitGeoSite(); err != nil { if err := geodata.InitGeoSite(); err != nil {
return nil, fmt.Errorf("can't initial GeoSite: %s", err) 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, EnhancedMode: cfg.EnhancedMode,
FallbackFilter: FallbackFilter{ FallbackFilter: FallbackFilter{
IPCIDR: []netip.Prefix{}, IPCIDR: []netip.Prefix{},
GeoSite: []*router.DomainMatcher{}, GeoSite: []router.DomainMatcher{},
}, },
} }
var err error var err error

View File

@ -5,7 +5,7 @@ import (
) )
type RuleGeoSite interface { type RuleGeoSite interface {
GetDomainMatcher() *router.DomainMatcher GetDomainMatcher() router.DomainMatcher
} }
type RuleGeoIP interface { type RuleGeoIP interface {

View File

@ -74,7 +74,7 @@ func (df *domainFilter) Match(domain string) bool {
} }
type geoSiteFilter struct { type geoSiteFilter struct {
matchers []*router.DomainMatcher matchers []router.DomainMatcher
} }
func NewGeoSite(group string) (fallbackDomainFilter, error) { func NewGeoSite(group string) (fallbackDomainFilter, error) {
@ -87,7 +87,7 @@ func NewGeoSite(group string) (fallbackDomainFilter, error) {
return nil, err return nil, err
} }
filter := &geoSiteFilter{ filter := &geoSiteFilter{
matchers: []*router.DomainMatcher{matcher}, matchers: []router.DomainMatcher{matcher},
} }
return filter, nil return filter, nil
} }

View File

@ -400,7 +400,7 @@ type FallbackFilter struct {
GeoIPCode string GeoIPCode string
IPCIDR []netip.Prefix IPCIDR []netip.Prefix
Domain []string Domain []string
GeoSite []*router.DomainMatcher GeoSite []router.DomainMatcher
} }
type Config struct { type Config struct {

View File

@ -36,6 +36,11 @@ geox-url:
geo-auto-update: false # 是否自动更新 geodata geo-auto-update: false # 是否自动更新 geodata
geo-update-interval: 24 # 更新间隔,单位:小时 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 log-level: debug # 日志等级 silent/error/warning/info/debug
ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录

View File

@ -146,14 +146,15 @@ func GetGeneral() *config.General {
AllowLan: listener.AllowLan(), AllowLan: listener.AllowLan(),
BindAddress: listener.BindAddress(), BindAddress: listener.BindAddress(),
}, },
Controller: config.Controller{}, Controller: config.Controller{},
Mode: tunnel.Mode(), Mode: tunnel.Mode(),
LogLevel: log.Level(), LogLevel: log.Level(),
IPv6: !resolver.DisableIPv6, IPv6: !resolver.DisableIPv6,
GeodataLoader: G.LoaderName(), GeodataLoader: G.LoaderName(),
Interface: dialer.DefaultInterface.Load(), GeositeMatcher: G.SiteMatcherName(),
Sniffing: tunnel.IsSniffing(), Interface: dialer.DefaultInterface.Load(),
TCPConcurrent: dialer.GetTcpConcurrent(), Sniffing: tunnel.IsSniffing(),
TCPConcurrent: dialer.GetTcpConcurrent(),
} }
return general return general
@ -401,8 +402,8 @@ func updateGeneral(general *config.General) {
} }
iface.FlushCache() iface.FlushCache()
geodataLoader := general.GeodataLoader G.SetLoader(general.GeodataLoader)
G.SetLoader(geodataLoader) G.SetSiteMatcher(general.GeositeMatcher)
} }
func updateUsers(users []auth.AuthUser) { func updateUsers(users []auth.AuthUser) {

View File

@ -15,7 +15,7 @@ type GEOSITE struct {
*Base *Base
country string country string
adapter string adapter string
matcher *router.DomainMatcher matcher router.DomainMatcher
recodeSize int recodeSize int
} }
@ -39,7 +39,7 @@ func (gs *GEOSITE) Payload() string {
return gs.country return gs.country
} }
func (gs *GEOSITE) GetDomainMatcher() *router.DomainMatcher { func (gs *GEOSITE) GetDomainMatcher() router.DomainMatcher {
return gs.matcher return gs.matcher
} }