From 960dfd3e93e781d8271ccf245c003737f251332b Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Thu, 8 Jan 2026 00:01:49 +0330 Subject: [PATCH 01/20] Add serialize MphMatcherGroup --- app/router/condition.go | 34 +++++- app/router/condition_serialize_test.go | 130 +++++++++++++++++++++++ app/router/geosite_compact.go | 126 ++++++++++++++++++++++ common/strmatcher/mph_matcher_compact.go | 112 +++++++++++++++++++ 4 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 app/router/condition_serialize_test.go create mode 100644 app/router/geosite_compact.go create mode 100644 common/strmatcher/mph_matcher_compact.go diff --git a/app/router/condition.go b/app/router/condition.go index 873f121d6461..54af816544c6 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -2,6 +2,7 @@ package router import ( "context" + "io" "os" "path/filepath" "regexp" @@ -52,7 +53,34 @@ var matcherTypeMap = map[Domain_Type]strmatcher.Type{ } type DomainMatcher struct { - matchers strmatcher.IndexMatcher + Matchers strmatcher.IndexMatcher +} + +func SerializeDomainMatcher(domains []*Domain, w io.Writer) error { + + g := strmatcher.NewMphMatcherGroup() + for _, d := range domains { + matcherType, f := matcherTypeMap[d.Type] + if !f { + continue + } + + _, err := g.AddPattern(d.Value, matcherType) + if err != nil { + return err + } + } + g.Build() + // serialize + return g.Serialize(w) +} + +func NewDomainMatcherFromBuffer(data []byte) (*strmatcher.MphMatcherGroup, error) { + matcher, err := strmatcher.NewMphMatcherGroupFromBuffer(data) + if err != nil { + return nil, err + } + return matcher, nil } func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) { @@ -72,12 +100,12 @@ func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) { } g.Build() return &DomainMatcher{ - matchers: g, + Matchers: g, }, nil } func (m *DomainMatcher) ApplyDomain(domain string) bool { - return len(m.matchers.Match(strings.ToLower(domain))) > 0 + return len(m.Matchers.Match(strings.ToLower(domain))) > 0 } // Apply implements Condition. diff --git a/app/router/condition_serialize_test.go b/app/router/condition_serialize_test.go new file mode 100644 index 000000000000..e4dc50cf5e90 --- /dev/null +++ b/app/router/condition_serialize_test.go @@ -0,0 +1,130 @@ +package router_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/platform/filesystem" +) + +func TestDomainMatcherSerialization(t *testing.T) { + + domains := []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.com"}, + {Type: router.Domain_Domain, Value: "v2ray.com"}, + {Type: router.Domain_Full, Value: "full.example.com"}, + } + + var buf bytes.Buffer + if err := router.SerializeDomainMatcher(domains, &buf); err != nil { + t.Fatalf("Serialize failed: %v", err) + } + + matcher, err := router.NewDomainMatcherFromBuffer(buf.Bytes()) + if err != nil { + t.Fatalf("Deserialize failed: %v", err) + } + + dMatcher := &router.DomainMatcher{ + Matchers: matcher, + } + testCases := []struct { + Input string + Match bool + }{ + {"google.com", true}, + {"maps.google.com", true}, + {"v2ray.com", true}, + {"full.example.com", true}, + + {"example.com", false}, + } + + for _, tc := range testCases { + if res := dMatcher.ApplyDomain(tc.Input); res != tc.Match { + t.Errorf("Match(%s) = %v, want %v", tc.Input, res, tc.Match) + } + } +} + +func TestGeoSiteSerialization(t *testing.T) { + sites := []*router.GeoSite{ + { + CountryCode: "CN", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "baidu.cn"}, + {Type: router.Domain_Domain, Value: "qq.com"}, + }, + }, + { + CountryCode: "US", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.com"}, + {Type: router.Domain_Domain, Value: "facebook.com"}, + }, + }, + } + + var buf bytes.Buffer + if err := router.SerializeGeoSiteList(sites, &buf); err != nil { + t.Fatalf("SerializeGeoSiteList failed: %v", err) + } + + tmp := t.TempDir() + path := filepath.Join(tmp, "matcher.cache") + + f, err := os.Create(path) + require.NoError(t, err) + defer f.Close() + _, err = f.Write(buf.Bytes()) + require.NoError(t, err) + + f, err = os.Open(path) + require.NoError(t, err) + + require.NoError(t, err) + data, _ := filesystem.ReadFile(path) + + // cn + gp, err := router.LoadGeoSiteMatcher(data, "CN") + if err != nil { + t.Fatalf("LoadGeoSiteMatcher(CN) failed: %v", err) + } + + cnMatcher := &router.DomainMatcher{ + Matchers: gp, + } + + if !cnMatcher.ApplyDomain("baidu.cn") { + t.Error("CN matcher should match baidu.cn") + } + if cnMatcher.ApplyDomain("google.com") { + t.Error("CN matcher should NOT match google.com") + } + + // us + gp, err = router.LoadGeoSiteMatcher(data, "US") + if err != nil { + t.Fatalf("LoadGeoSiteMatcher(US) failed: %v", err) + } + + usMatcher := &router.DomainMatcher{ + Matchers: gp, + } + if !usMatcher.ApplyDomain("google.com") { + t.Error("US matcher should match google.com") + } + if usMatcher.ApplyDomain("baidu.cn") { + t.Error("US matcher should NOT match baidu.cn") + } + + // unknown + _, err = router.LoadGeoSiteMatcher(data, "unknown") + if err == nil { + t.Error("LoadGeoSiteMatcher(unknown) should fail") + } +} diff --git a/app/router/geosite_compact.go b/app/router/geosite_compact.go new file mode 100644 index 000000000000..3ad39987700c --- /dev/null +++ b/app/router/geosite_compact.go @@ -0,0 +1,126 @@ +package router + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + + "github.com/xtls/xray-core/common/strmatcher" +) + +func SerializeGeoSiteList(sites []*GeoSite, w io.Writer) error { + // data buffers + var buffers [][]byte + var countryCodes []string + + for _, site := range sites { + if site == nil { + continue + } + var buf bytes.Buffer + if err := SerializeDomainMatcher(site.Domain, &buf); err != nil { + return err + } + buffers = append(buffers, buf.Bytes()) + countryCodes = append(countryCodes, site.CountryCode) + } + + // header, count ,4 + if err := binary.Write(w, binary.LittleEndian, uint32(len(buffers))); err != nil { + return err + } + + currentOffset := uint64(4) // header + + // calc index size first + var indexSize uint64 + for _, code := range countryCodes { + indexSize += 1 + uint64(len(code)) + 16 + } + currentOffset += indexSize + + // write entry + for i, code := range countryCodes { + codeBytes := []byte(code) + if len(codeBytes) > 255 { + return errors.New("country code too long") + } + + // len + if _, err := w.Write([]byte{byte(len(codeBytes))}); err != nil { + return err + } + // code + if _, err := w.Write(codeBytes); err != nil { + return err + } + + size := uint64(len(buffers[i])) + + // offset + if err := binary.Write(w, binary.LittleEndian, currentOffset); err != nil { + return err + } + // size + if err := binary.Write(w, binary.LittleEndian, size); err != nil { + return err + } + + currentOffset += size + } + + // data + for _, buf := range buffers { + if _, err := w.Write(buf); err != nil { + return err + } + } + + return nil +} + +func LoadGeoSiteMatcher(data []byte, countryCode string) (*strmatcher.MphMatcherGroup, error) { + if len(data) < 4 { + return nil, errors.New("invalid data length") + } + + count := binary.LittleEndian.Uint32(data[0:4]) + + offset := 4 + targetBytes := []byte(countryCode) + + for range count { + if offset >= len(data) { + return nil, errors.New("index truncated") + } + + codeLen := int(data[offset]) + offset++ + + if offset+codeLen > len(data) { + return nil, errors.New("index code truncated") + } + + code := data[offset : offset+codeLen] + offset += codeLen + + if offset+16 > len(data) { + return nil, errors.New("index meta truncated") + } + + dataOffset := binary.LittleEndian.Uint64(data[offset : offset+8]) + dataSize := binary.LittleEndian.Uint64(data[offset+8 : offset+16]) + offset += 16 + + // match? + if bytes.Equal(code, targetBytes) { + if dataOffset+dataSize > uint64(len(data)) { + return nil, errors.New("data truncated") + } + return NewDomainMatcherFromBuffer(data[dataOffset : dataOffset+dataSize]) + } + } + + return nil, errors.New("country code not found") +} diff --git a/common/strmatcher/mph_matcher_compact.go b/common/strmatcher/mph_matcher_compact.go new file mode 100644 index 000000000000..0e7bcab1d581 --- /dev/null +++ b/common/strmatcher/mph_matcher_compact.go @@ -0,0 +1,112 @@ +package strmatcher + +import ( + "encoding/binary" + "errors" + "io" + "unsafe" +) + +func (g *MphMatcherGroup) Serialize(w io.Writer) error { + // header: level0 4, level1 4, rule count 4, rule data 4 + + var rulesDataLen uint32 + for _, r := range g.rules { + // 2 bytes for length + rulesDataLen += 2 + uint32(len(r)) + } + + header := []uint32{ + uint32(len(g.level0)), + uint32(len(g.level1)), + uint32(len(g.rules)), + rulesDataLen, + } + + if err := binary.Write(w, binary.LittleEndian, header); err != nil { + return err + } + + // level0 + if err := binary.Write(w, binary.LittleEndian, g.level0); err != nil { + return err + } + // level1 + if err := binary.Write(w, binary.LittleEndian, g.level1); err != nil { + return err + } + + // rules + for _, r := range g.rules { + if err := binary.Write(w, binary.LittleEndian, uint16(len(r))); err != nil { + return err + } + if _, err := w.Write([]byte(r)); err != nil { + return err + } + } + + return nil +} + +func NewMphMatcherGroupFromBuffer(data []byte) (*MphMatcherGroup, error) { + if len(data) < 16 { + return nil, errors.New("invalid data length") + } + + l0Len := binary.LittleEndian.Uint32(data[0:4]) + l1Len := binary.LittleEndian.Uint32(data[4:8]) + ruleCount := binary.LittleEndian.Uint32(data[8:12]) + rulesDataLen := binary.LittleEndian.Uint32(data[12:16]) + + offset := 16 + + // check size + requiredSize := offset + int(l0Len)*4 + int(l1Len)*4 + int(rulesDataLen) + if len(data) < requiredSize { + return nil, errors.New("data truncated") + } + + g := NewMphMatcherGroup() + + // level0 + if l0Len > 0 { + g.level0 = unsafe.Slice((*uint32)(unsafe.Pointer(&data[offset])), l0Len) + offset += int(l0Len) * 4 + g.level0Mask = int(l0Len) - 1 + } + + // level1 + if l1Len > 0 { + g.level1 = unsafe.Slice((*uint32)(unsafe.Pointer(&data[offset])), l1Len) + offset += int(l1Len) * 4 + g.level1Mask = int(l1Len) - 1 + } + + // build rules + if ruleCount > 0 { + g.rules = make([]string, ruleCount) + rulesOffset := offset + + for i := range ruleCount { + if rulesOffset+2 > len(data) { + return nil, errors.New("rules truncated") + } + strLen := int(binary.LittleEndian.Uint16(data[rulesOffset : rulesOffset+2])) + rulesOffset += 2 + + if rulesOffset+strLen > len(data) { + return nil, errors.New("rule string truncated") + } + + strBytes := data[rulesOffset : rulesOffset+strLen] + g.rules[i] = unsafe.String(unsafe.SliceData(strBytes), strLen) + + rulesOffset += strLen + } + } + + g.count = uint32(ruleCount) + 1 + + return g, nil +} From 47d70ff85fcdc6182a48d953b1bdc4363cf10132 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Thu, 8 Jan 2026 15:20:49 +0330 Subject: [PATCH 02/20] add xray.cached.matcher flag --- app/router/config.go | 38 ++++++++++++++++++++++++++++++++++--- common/platform/platform.go | 1 + 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/router/config.go b/app/router/config.go index c41e6cfc9625..0d574beb05d7 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" ) @@ -105,9 +107,23 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Domain) > 0 { - matcher, err := NewMphMatcherGroup(rr.Domain) - if err != nil { - return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) + useCachedMatcher := platform.NewEnvFlag(platform.UseCachedMatcher).GetValueAsInt(0) + + var matcher *DomainMatcher + var err error + + if useCachedMatcher != 0 { + matcher, err = GetDomainMathcerWithRuleTag(rr.RuleTag) + if err != nil { + return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err) + } + + } else { + matcher, err = NewMphMatcherGroup(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) + } + } errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") conds.Add(matcher) @@ -172,3 +188,19 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch return nil, errors.New("unrecognized balancer type") } } + +func GetDomainMathcerWithRuleTag(ruleTag string) (*DomainMatcher, error) { + file := "matcher.cache" + bs, err := filesystem.ReadAsset(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + g, err := LoadGeoSiteMatcher(bs, ruleTag) + if err != nil { + return nil, errors.New("failed to load file:", file).Base(err) + } + return &DomainMatcher{ + Matchers: g, + }, nil + +} diff --git a/common/platform/platform.go b/common/platform/platform.go index 80e62874d6e4..f5daae7c752f 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -17,6 +17,7 @@ const ( UseFreedomSplice = "xray.buf.splice" UseVmessPadding = "xray.vmess.padding" UseCone = "xray.cone.disabled" + UseCachedMatcher = "xray.cached.matcher" BufferSize = "xray.ray.buffer.size" BrowserDialerAddress = "xray.browser.dialer" From 8bb856290b1028adfcace3ff3c05cec27d94e35d Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Thu, 8 Jan 2026 15:25:11 +0330 Subject: [PATCH 03/20] comment --- common/strmatcher/mph_matcher_compact.go | 1 + 1 file changed, 1 insertion(+) diff --git a/common/strmatcher/mph_matcher_compact.go b/common/strmatcher/mph_matcher_compact.go index 0e7bcab1d581..a62711d603de 100644 --- a/common/strmatcher/mph_matcher_compact.go +++ b/common/strmatcher/mph_matcher_compact.go @@ -106,6 +106,7 @@ func NewMphMatcherGroupFromBuffer(data []byte) (*MphMatcherGroup, error) { } } + // count in MphMatcherGroup always is 1, may break other this? g.count = uint32(ruleCount) + 1 return g, nil From 7b40b3fb10aec93296a5ce36cf609edadefdd1d7 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Thu, 8 Jan 2026 15:45:56 +0330 Subject: [PATCH 04/20] close file in test --- app/router/condition_serialize_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/router/condition_serialize_test.go b/app/router/condition_serialize_test.go index e4dc50cf5e90..4106f9665f30 100644 --- a/app/router/condition_serialize_test.go +++ b/app/router/condition_serialize_test.go @@ -79,12 +79,13 @@ func TestGeoSiteSerialization(t *testing.T) { f, err := os.Create(path) require.NoError(t, err) - defer f.Close() _, err = f.Write(buf.Bytes()) require.NoError(t, err) + f.Close() f, err = os.Open(path) require.NoError(t, err) + defer f.Close() require.NoError(t, err) data, _ := filesystem.ReadFile(path) From ec626d310f3b20c35208c85cc312e2b896b4bac8 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Thu, 8 Jan 2026 16:41:47 +0330 Subject: [PATCH 05/20] add BuildDomainMatcherCache to RouterConfig --- app/router/config.go | 87 ++++++++++++++++++++++++++++++++++++++++++++ infra/conf/router.go | 43 ++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/app/router/config.go b/app/router/config.go index 0d574beb05d7..2f61b84608f2 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -11,6 +11,7 @@ import ( "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" + "google.golang.org/protobuf/proto" ) type Rule struct { @@ -112,6 +113,15 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { var matcher *DomainMatcher var err error + domains := rr.Domain + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" && useCachedMatcher == 0 { + var err error + domains, err = GetDomainList(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domains from mmap").Base(err) + } + } + if useCachedMatcher != 0 { matcher, err = GetDomainMathcerWithRuleTag(rr.RuleTag) if err != nil { @@ -189,6 +199,83 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch } } +func GetGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { + geoipList := []*GeoIP{} + for _, ip := range ips { + if ip.CountryCode != "" { + val := strings.Split(ip.CountryCode, "_") + fileName := "geoip.dat" + if len(val) == 2 { + fileName = strings.ToLower(val[0]) + } + bs, err := filesystem.ReadAsset(fileName) + if err != nil { + return nil, errors.New("failed to load file: ", fileName).Base(err) + } + bs = filesystem.Find(bs, []byte(ip.CountryCode)) + + var geoip GeoIP + + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) + } + geoipList = append(geoipList, &geoip) + + } else { + geoipList = append(geoipList, ip) + } + } + return geoipList, nil + +} + +func GetDomainList(domains []*Domain) ([]*Domain, error) { + domainList := []*Domain{} + for _, domain := range domains { + val := strings.Split(domain.Value, "_") + + if len(val) >= 2 { + + fileName := val[0] + code := val[1] + + bs, err := filesystem.ReadAsset(fileName) + if err != nil { + return nil, errors.New("failed to load file: ", fileName).Base(err) + } + bs = filesystem.Find(bs, []byte(code)) + var geosite GeoSite + + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) + } + + // parse attr + if len(val) == 3 { + siteWithAttr := strings.Split(val[2], ",") + attrs := ParseAttrs(siteWithAttr) + + if !attrs.IsEmpty() { + filteredDomains := make([]*Domain, 0, len(domains)) + for _, domain := range geosite.Domain { + if attrs.Match(domain) { + filteredDomains = append(filteredDomains, domain) + } + } + geosite.Domain = filteredDomains + } + + } + + domainList = append(domainList, geosite.Domain...) + + } else { + domainList = append(domainList, domain) + } + } + return domainList, nil +} + func GetDomainMathcerWithRuleTag(ruleTag string) (*DomainMatcher, error) { file := "matcher.cache" bs, err := filesystem.ReadAsset(file) diff --git a/infra/conf/router.go b/infra/conf/router.go index 1e1f6b803758..cf1454dc25aa 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -4,7 +4,9 @@ import ( "bufio" "bytes" "encoding/json" + "fmt" "io" + "os" "runtime" "strconv" "strings" @@ -12,6 +14,7 @@ import ( "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/serial" "google.golang.org/protobuf/proto" @@ -665,3 +668,43 @@ func parseRule(msg json.RawMessage) (*router.RoutingRule, error) { } return fieldrule, nil } + +func (c *RouterConfig) BuildDomainMatcherCache() error { + var geosite []*router.GeoSite + + matcherFilePath := platform.GetAssetLocation("matcher.cache") + routerConfig, err := c.Build() + + if len(routerConfig.Rule) == 0 { + return fmt.Errorf("no routing") + } + + for _, rule := range routerConfig.Rule { + domains, err := router.GetDomainList(rule.Domain) + if err != nil { + return errors.New("failed to build domains from mmap").Base(err) + } + // write it with ruleTag key + simpleGeoSite := router.GeoSite{CountryCode: rule.RuleTag, Domain: domains} + + geosite = append(geosite, &simpleGeoSite) + } + + f, err := os.Create(matcherFilePath) + if err != nil { + return err + } + defer f.Close() + + var buf bytes.Buffer + println(len(geosite)) + if err := router.SerializeGeoSiteList(geosite, &buf); err != nil { + return err + } + + if _, err := f.Write(buf.Bytes()); err != nil { + return err + } + + return nil +} From 635d1669fb9419f570a2562f42967a18308b6961 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 11 Jan 2026 22:02:52 +0330 Subject: [PATCH 06/20] read domain matcher path form config --- app/router/config.go | 20 ++++++++----------- app/router/config.pb.go | 40 +++++++++++++++++++++++-------------- app/router/config.proto | 2 ++ app/router/router.go | 4 ++-- common/platform/platform.go | 1 - infra/conf/router.go | 12 ++++++++++- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/app/router/config.go b/app/router/config.go index 2f61b84608f2..6088ec70a857 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" @@ -33,7 +32,7 @@ func (r *Rule) Apply(ctx routing.Context) bool { return r.Condition.Apply(ctx) } -func (rr *RoutingRule) BuildCondition() (Condition, error) { +func (rr *RoutingRule) BuildCondition(cachedDMPath string) (Condition, error) { conds := NewConditionChan() if len(rr.InboundTag) > 0 { @@ -108,13 +107,11 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } if len(rr.Domain) > 0 { - useCachedMatcher := platform.NewEnvFlag(platform.UseCachedMatcher).GetValueAsInt(0) - var matcher *DomainMatcher var err error domains := rr.Domain - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" && useCachedMatcher == 0 { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" && cachedDMPath == "" { var err error domains, err = GetDomainList(rr.Domain) if err != nil { @@ -122,8 +119,8 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { } } - if useCachedMatcher != 0 { - matcher, err = GetDomainMathcerWithRuleTag(rr.RuleTag) + if cachedDMPath != "" { + matcher, err = GetDomainMathcerWithRuleTag(cachedDMPath, rr.RuleTag) if err != nil { return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err) } @@ -276,15 +273,14 @@ func GetDomainList(domains []*Domain) ([]*Domain, error) { return domainList, nil } -func GetDomainMathcerWithRuleTag(ruleTag string) (*DomainMatcher, error) { - file := "matcher.cache" - bs, err := filesystem.ReadAsset(file) +func GetDomainMathcerWithRuleTag(cachedDMPath string, ruleTag string) (*DomainMatcher, error) { + bs, err := filesystem.ReadFile(cachedDMPath) if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) + return nil, errors.New("failed to load file: ", cachedDMPath).Base(err) } g, err := LoadGeoSiteMatcher(bs, ruleTag) if err != nil { - return nil, errors.New("failed to load file:", file).Base(err) + return nil, errors.New("failed to load file:", cachedDMPath).Base(err) } return &DomainMatcher{ Matchers: g, diff --git a/app/router/config.pb.go b/app/router/config.pb.go index ff5838a8eff6..d95baa294a5d 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -892,9 +892,10 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"` - Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"` - BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"` + DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"` + Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"` + BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"` + DomainMatcherPath string `protobuf:"bytes,4,opt,name=domainMatcherPath,proto3" json:"domainMatcherPath,omitempty"` } func (x *Config) Reset() { @@ -948,6 +949,13 @@ func (x *Config) GetBalancingRule() []*BalancingRule { return nil } +func (x *Config) GetDomainMatcherPath() string { + if x != nil { + return x.DomainMatcherPath + } + return "" +} + type Domain_Attribute struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1177,7 +1185,7 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, - 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0xbe, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, @@ -1190,17 +1198,19 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, - 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, - 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, - 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, - 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, - 0x50, 0x01, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, - 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, - 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, - 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x61, 0x74, 0x68, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x61, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, + 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, + 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, + 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, + 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, + 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, + 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, + 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/router/config.proto b/app/router/config.proto index 20da23ba6d7b..f14180834bac 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -160,4 +160,6 @@ message Config { DomainStrategy domain_strategy = 1; repeated RoutingRule rule = 2; repeated BalancingRule balancing_rule = 3; + + string domainMatcherPath = 4; } diff --git a/app/router/router.go b/app/router/router.go index 790bf8e23c70..3f0886d3d346 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -55,7 +55,7 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out r.rules = make([]*Rule, 0, len(config.Rule)) for _, rule := range config.Rule { - cond, err := rule.BuildCondition() + cond, err := rule.BuildCondition(config.DomainMatcherPath) if err != nil { return err } @@ -129,7 +129,7 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error { if r.RuleExists(rule.GetRuleTag()) { return errors.New("duplicate ruleTag ", rule.GetRuleTag()) } - cond, err := rule.BuildCondition() + cond, err := rule.BuildCondition(config.DomainMatcherPath) if err != nil { return err } diff --git a/common/platform/platform.go b/common/platform/platform.go index f5daae7c752f..80e62874d6e4 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -17,7 +17,6 @@ const ( UseFreedomSplice = "xray.buf.splice" UseVmessPadding = "xray.vmess.padding" UseCone = "xray.cone.disabled" - UseCachedMatcher = "xray.cached.matcher" BufferSize = "xray.ray.buffer.size" BrowserDialerAddress = "xray.browser.dialer" diff --git a/infra/conf/router.go b/infra/conf/router.go index cf1454dc25aa..5bbd83ffc3f3 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -80,6 +80,8 @@ type RouterConfig struct { RuleList []json.RawMessage `json:"rules"` DomainStrategy *string `json:"domainStrategy"` Balancers []*BalancingRule `json:"balancers"` + + DomainMatcherPath *string `json:"domainMatcherPath"` } func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy { @@ -122,6 +124,14 @@ func (c *RouterConfig) Build() (*router.Config, error) { } config.BalancingRule = append(config.BalancingRule, balancer) } + + if c.DomainMatcherPath != nil { + path := *c.DomainMatcherPath + if val := strings.Split(path, "assets:"); len(val) == 2 { + path = platform.GetAssetLocation(val[1]) + } + config.DomainMatcherPath = path + } return config, nil } @@ -697,7 +707,7 @@ func (c *RouterConfig) BuildDomainMatcherCache() error { defer f.Close() var buf bytes.Buffer - println(len(geosite)) + if err := router.SerializeGeoSiteList(geosite, &buf); err != nil { return err } From 490f39eab833dbce05eb2d8b01c350d3b9ae732a Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 11 Jan 2026 22:13:39 +0330 Subject: [PATCH 07/20] improve typo --- app/router/config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/router/config.go b/app/router/config.go index 6088ec70a857..c323d6f7e16f 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -32,7 +32,7 @@ func (r *Rule) Apply(ctx routing.Context) bool { return r.Condition.Apply(ctx) } -func (rr *RoutingRule) BuildCondition(cachedDMPath string) (Condition, error) { +func (rr *RoutingRule) BuildCondition(domainMatcherPath string) (Condition, error) { conds := NewConditionChan() if len(rr.InboundTag) > 0 { @@ -111,7 +111,7 @@ func (rr *RoutingRule) BuildCondition(cachedDMPath string) (Condition, error) { var err error domains := rr.Domain - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" && cachedDMPath == "" { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" && domainMatcherPath == "" { var err error domains, err = GetDomainList(rr.Domain) if err != nil { @@ -119,8 +119,8 @@ func (rr *RoutingRule) BuildCondition(cachedDMPath string) (Condition, error) { } } - if cachedDMPath != "" { - matcher, err = GetDomainMathcerWithRuleTag(cachedDMPath, rr.RuleTag) + if domainMatcherPath != "" { + matcher, err = GetDomainMathcerWithRuleTag(domainMatcherPath, rr.RuleTag) if err != nil { return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err) } @@ -273,14 +273,14 @@ func GetDomainList(domains []*Domain) ([]*Domain, error) { return domainList, nil } -func GetDomainMathcerWithRuleTag(cachedDMPath string, ruleTag string) (*DomainMatcher, error) { - bs, err := filesystem.ReadFile(cachedDMPath) +func GetDomainMathcerWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) { + bs, err := filesystem.ReadFile(domainMatcherPath) if err != nil { - return nil, errors.New("failed to load file: ", cachedDMPath).Base(err) + return nil, errors.New("failed to load file: ", domainMatcherPath).Base(err) } g, err := LoadGeoSiteMatcher(bs, ruleTag) if err != nil { - return nil, errors.New("failed to load file:", cachedDMPath).Base(err) + return nil, errors.New("failed to load file:", domainMatcherPath).Base(err) } return &DomainMatcher{ Matchers: g, From be0263ccb1b29adc271f5192ea8a3ba2693f12ad Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Tue, 27 Jan 2026 19:39:20 +0330 Subject: [PATCH 08/20] use NewFileReader --- app/router/condition_serialize_test.go | 6 +- app/router/condition_test.go | 2 +- app/router/config.go | 94 ++------------------------ app/router/geosite_compact.go | 78 +++++++++++++-------- infra/conf/router.go | 12 ++-- 5 files changed, 65 insertions(+), 127 deletions(-) diff --git a/app/router/condition_serialize_test.go b/app/router/condition_serialize_test.go index 4106f9665f30..b1605ef4fa2e 100644 --- a/app/router/condition_serialize_test.go +++ b/app/router/condition_serialize_test.go @@ -91,7 +91,7 @@ func TestGeoSiteSerialization(t *testing.T) { data, _ := filesystem.ReadFile(path) // cn - gp, err := router.LoadGeoSiteMatcher(data, "CN") + gp, err := router.LoadGeoSiteMatcher(bytes.NewReader(data), "CN") if err != nil { t.Fatalf("LoadGeoSiteMatcher(CN) failed: %v", err) } @@ -108,7 +108,7 @@ func TestGeoSiteSerialization(t *testing.T) { } // us - gp, err = router.LoadGeoSiteMatcher(data, "US") + gp, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "US") if err != nil { t.Fatalf("LoadGeoSiteMatcher(US) failed: %v", err) } @@ -124,7 +124,7 @@ func TestGeoSiteSerialization(t *testing.T) { } // unknown - _, err = router.LoadGeoSiteMatcher(data, "unknown") + _, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "unknown") if err == nil { t.Error("LoadGeoSiteMatcher(unknown) should fail") } diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 1272aef6e9f4..63259b500ee9 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -288,7 +288,7 @@ func TestRoutingRule(t *testing.T) { } for _, test := range cases { - cond, err := test.rule.BuildCondition() + cond, err := test.rule.BuildCondition("") common.Must(err) for _, subtest := range test.test { diff --git a/app/router/config.go b/app/router/config.go index c323d6f7e16f..59ab7c9d1d82 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -10,7 +10,6 @@ import ( "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" - "google.golang.org/protobuf/proto" ) type Rule struct { @@ -109,16 +108,6 @@ func (rr *RoutingRule) BuildCondition(domainMatcherPath string) (Condition, erro if len(rr.Domain) > 0 { var matcher *DomainMatcher var err error - - domains := rr.Domain - if runtime.GOOS != "windows" && runtime.GOOS != "wasm" && domainMatcherPath == "" { - var err error - domains, err = GetDomainList(rr.Domain) - if err != nil { - return nil, errors.New("failed to build domains from mmap").Base(err) - } - } - if domainMatcherPath != "" { matcher, err = GetDomainMathcerWithRuleTag(domainMatcherPath, rr.RuleTag) if err != nil { @@ -196,89 +185,14 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch } } -func GetGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { - geoipList := []*GeoIP{} - for _, ip := range ips { - if ip.CountryCode != "" { - val := strings.Split(ip.CountryCode, "_") - fileName := "geoip.dat" - if len(val) == 2 { - fileName = strings.ToLower(val[0]) - } - bs, err := filesystem.ReadAsset(fileName) - if err != nil { - return nil, errors.New("failed to load file: ", fileName).Base(err) - } - bs = filesystem.Find(bs, []byte(ip.CountryCode)) - - var geoip GeoIP - - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - geoipList = append(geoipList, &geoip) - - } else { - geoipList = append(geoipList, ip) - } - } - return geoipList, nil - -} - -func GetDomainList(domains []*Domain) ([]*Domain, error) { - domainList := []*Domain{} - for _, domain := range domains { - val := strings.Split(domain.Value, "_") - - if len(val) >= 2 { - - fileName := val[0] - code := val[1] - - bs, err := filesystem.ReadAsset(fileName) - if err != nil { - return nil, errors.New("failed to load file: ", fileName).Base(err) - } - bs = filesystem.Find(bs, []byte(code)) - var geosite GeoSite - - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - - // parse attr - if len(val) == 3 { - siteWithAttr := strings.Split(val[2], ",") - attrs := ParseAttrs(siteWithAttr) - - if !attrs.IsEmpty() { - filteredDomains := make([]*Domain, 0, len(domains)) - for _, domain := range geosite.Domain { - if attrs.Match(domain) { - filteredDomains = append(filteredDomains, domain) - } - } - geosite.Domain = filteredDomains - } - - } - - domainList = append(domainList, geosite.Domain...) - - } else { - domainList = append(domainList, domain) - } - } - return domainList, nil -} - func GetDomainMathcerWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) { - bs, err := filesystem.ReadFile(domainMatcherPath) + f, err := filesystem.NewFileReader(domainMatcherPath) if err != nil { return nil, errors.New("failed to load file: ", domainMatcherPath).Base(err) } - g, err := LoadGeoSiteMatcher(bs, ruleTag) + defer f.Close() + + g, err := LoadGeoSiteMatcher(f, ruleTag) if err != nil { return nil, errors.New("failed to load file:", domainMatcherPath).Base(err) } diff --git a/app/router/geosite_compact.go b/app/router/geosite_compact.go index 3ad39987700c..88b99d940ce8 100644 --- a/app/router/geosite_compact.go +++ b/app/router/geosite_compact.go @@ -1,6 +1,7 @@ package router import ( + "bufio" "bytes" "encoding/binary" "errors" @@ -80,47 +81,68 @@ func SerializeGeoSiteList(sites []*GeoSite, w io.Writer) error { return nil } -func LoadGeoSiteMatcher(data []byte, countryCode string) (*strmatcher.MphMatcherGroup, error) { - if len(data) < 4 { - return nil, errors.New("invalid data length") +func LoadGeoSiteMatcher(r io.Reader, countryCode string) (*strmatcher.MphMatcherGroup, error) { + br := bufio.NewReaderSize(r, 64*1024) + var count uint32 + if err := binary.Read(br, binary.LittleEndian, &count); err != nil { + return nil, err } - count := binary.LittleEndian.Uint32(data[0:4]) - - offset := 4 targetBytes := []byte(countryCode) + var dataOffset, dataSize uint64 + var found bool - for range count { - if offset >= len(data) { - return nil, errors.New("index truncated") - } + bytesRead := uint64(4) - codeLen := int(data[offset]) - offset++ + for i := uint32(0); i < count; i++ { + codeLen, err := br.ReadByte() + if err != nil { + return nil, err + } + bytesRead++ - if offset+codeLen > len(data) { - return nil, errors.New("index code truncated") + code := make([]byte, int(codeLen)) + if _, err := io.ReadFull(br, code); err != nil { + return nil, err } + bytesRead += uint64(codeLen) - code := data[offset : offset+codeLen] - offset += codeLen + var offsetValue, sizeValue uint64 + if err := binary.Read(br, binary.LittleEndian, &offsetValue); err != nil { + return nil, err + } + bytesRead += 8 + if err := binary.Read(br, binary.LittleEndian, &sizeValue); err != nil { + return nil, err + } + bytesRead += 8 - if offset+16 > len(data) { - return nil, errors.New("index meta truncated") + if bytes.Equal(code, targetBytes) { + dataOffset = offsetValue + dataSize = sizeValue + found = true } + } - dataOffset := binary.LittleEndian.Uint64(data[offset : offset+8]) - dataSize := binary.LittleEndian.Uint64(data[offset+8 : offset+16]) - offset += 16 + if !found { + return nil, errors.New("country code not found") + } - // match? - if bytes.Equal(code, targetBytes) { - if dataOffset+dataSize > uint64(len(data)) { - return nil, errors.New("data truncated") - } - return NewDomainMatcherFromBuffer(data[dataOffset : dataOffset+dataSize]) + if dataOffset < bytesRead { + return nil, errors.New("invalid data offset") + } + + toSkip := dataOffset - bytesRead + if toSkip > 0 { + if _, err := io.CopyN(io.Discard, br, int64(toSkip)); err != nil { + return nil, err } } - return nil, errors.New("country code not found") + data := make([]byte, dataSize) + if _, err := io.ReadFull(br, data); err != nil { + return nil, err + } + + return NewDomainMatcherFromBuffer(data) } diff --git a/infra/conf/router.go b/infra/conf/router.go index 5bbd83ffc3f3..f928face22c9 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -217,6 +217,12 @@ func loadIP(file, code string) ([]*router.CIDR, error) { } func loadSite(file, code string) ([]*router.Domain, error) { + + // TODO + // if os.Getenv("XRAY_CACHED_MATCHER") == "1" { + // return []*router.Domain{&router.Domain{}}, nil + // } + bs, err := loadFile(file, code) if err != nil { return nil, err @@ -690,12 +696,8 @@ func (c *RouterConfig) BuildDomainMatcherCache() error { } for _, rule := range routerConfig.Rule { - domains, err := router.GetDomainList(rule.Domain) - if err != nil { - return errors.New("failed to build domains from mmap").Base(err) - } // write it with ruleTag key - simpleGeoSite := router.GeoSite{CountryCode: rule.RuleTag, Domain: domains} + simpleGeoSite := router.GeoSite{CountryCode: rule.RuleTag, Domain: rule.Domain} geosite = append(geosite, &simpleGeoSite) } From e7437d4cb86fefe1ebbb3044ba4ad95d31b33ef3 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Wed, 28 Jan 2026 16:15:44 +0330 Subject: [PATCH 09/20] remove todo --- infra/conf/router.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/infra/conf/router.go b/infra/conf/router.go index f928face22c9..2b1d4bfc0b57 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -217,12 +217,6 @@ func loadIP(file, code string) ([]*router.CIDR, error) { } func loadSite(file, code string) ([]*router.Domain, error) { - - // TODO - // if os.Getenv("XRAY_CACHED_MATCHER") == "1" { - // return []*router.Domain{&router.Domain{}}, nil - // } - bs, err := loadFile(file, code) if err != nil { return nil, err From f0f024e0dfe660a2ae77e28746976654eeb9ba9c Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Wed, 28 Jan 2026 16:50:57 +0330 Subject: [PATCH 10/20] add custom cache path --- infra/conf/router.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/infra/conf/router.go b/infra/conf/router.go index 2b1d4bfc0b57..2d2cf8bb7e21 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -679,10 +679,14 @@ func parseRule(msg json.RawMessage) (*router.RoutingRule, error) { return fieldrule, nil } -func (c *RouterConfig) BuildDomainMatcherCache() error { +func (c *RouterConfig) BuildDomainMatcherCache(customMatcherFilePath *string) error { var geosite []*router.GeoSite - matcherFilePath := platform.GetAssetLocation("matcher.cache") + + if customMatcherFilePath != nil { + matcherFilePath = *customMatcherFilePath + } + routerConfig, err := c.Build() if len(routerConfig.Rule) == 0 { From 6a8dbd32b9ef7c9b46f2e0ea2eb434931edde6b8 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Thu, 29 Jan 2026 20:49:35 +0330 Subject: [PATCH 11/20] switch to encoding/gob --- app/router/geosite_compact.go | 141 ++++------------------ common/strmatcher/ac_automaton_matcher.go | 122 +++++++++---------- common/strmatcher/matchers.go | 16 ++- common/strmatcher/mph_matcher.go | 106 ++++++++-------- common/strmatcher/mph_matcher_compact.go | 128 +++++--------------- common/strmatcher/strmatcher.go | 23 ++-- 6 files changed, 193 insertions(+), 343 deletions(-) diff --git a/app/router/geosite_compact.go b/app/router/geosite_compact.go index 88b99d940ce8..0040902d3fec 100644 --- a/app/router/geosite_compact.go +++ b/app/router/geosite_compact.go @@ -1,148 +1,59 @@ package router import ( - "bufio" - "bytes" - "encoding/binary" + "encoding/gob" "errors" "io" "github.com/xtls/xray-core/common/strmatcher" ) +type geoSiteListGob struct { + Sites map[string][]byte +} + func SerializeGeoSiteList(sites []*GeoSite, w io.Writer) error { - // data buffers - var buffers [][]byte - var countryCodes []string + data := geoSiteListGob{ + Sites: make(map[string][]byte), + } for _, site := range sites { if site == nil { continue } - var buf bytes.Buffer + var buf bytesWriter if err := SerializeDomainMatcher(site.Domain, &buf); err != nil { return err } - buffers = append(buffers, buf.Bytes()) - countryCodes = append(countryCodes, site.CountryCode) - } - - // header, count ,4 - if err := binary.Write(w, binary.LittleEndian, uint32(len(buffers))); err != nil { - return err - } - - currentOffset := uint64(4) // header - - // calc index size first - var indexSize uint64 - for _, code := range countryCodes { - indexSize += 1 + uint64(len(code)) + 16 + data.Sites[site.CountryCode] = buf.Bytes() } - currentOffset += indexSize - - // write entry - for i, code := range countryCodes { - codeBytes := []byte(code) - if len(codeBytes) > 255 { - return errors.New("country code too long") - } - - // len - if _, err := w.Write([]byte{byte(len(codeBytes))}); err != nil { - return err - } - // code - if _, err := w.Write(codeBytes); err != nil { - return err - } - size := uint64(len(buffers[i])) - - // offset - if err := binary.Write(w, binary.LittleEndian, currentOffset); err != nil { - return err - } - // size - if err := binary.Write(w, binary.LittleEndian, size); err != nil { - return err - } + return gob.NewEncoder(w).Encode(data) +} - currentOffset += size - } +type bytesWriter struct { + data []byte +} - // data - for _, buf := range buffers { - if _, err := w.Write(buf); err != nil { - return err - } - } +func (w *bytesWriter) Write(p []byte) (n int, err error) { + w.data = append(w.data, p...) + return len(p), nil +} - return nil +func (w *bytesWriter) Bytes() []byte { + return w.data } func LoadGeoSiteMatcher(r io.Reader, countryCode string) (*strmatcher.MphMatcherGroup, error) { - br := bufio.NewReaderSize(r, 64*1024) - var count uint32 - if err := binary.Read(br, binary.LittleEndian, &count); err != nil { + var data geoSiteListGob + if err := gob.NewDecoder(r).Decode(&data); err != nil { return nil, err } - targetBytes := []byte(countryCode) - var dataOffset, dataSize uint64 - var found bool - - bytesRead := uint64(4) - - for i := uint32(0); i < count; i++ { - codeLen, err := br.ReadByte() - if err != nil { - return nil, err - } - bytesRead++ - - code := make([]byte, int(codeLen)) - if _, err := io.ReadFull(br, code); err != nil { - return nil, err - } - bytesRead += uint64(codeLen) - - var offsetValue, sizeValue uint64 - if err := binary.Read(br, binary.LittleEndian, &offsetValue); err != nil { - return nil, err - } - bytesRead += 8 - if err := binary.Read(br, binary.LittleEndian, &sizeValue); err != nil { - return nil, err - } - bytesRead += 8 - - if bytes.Equal(code, targetBytes) { - dataOffset = offsetValue - dataSize = sizeValue - found = true - } - } - - if !found { + siteData, ok := data.Sites[countryCode] + if !ok { return nil, errors.New("country code not found") } - if dataOffset < bytesRead { - return nil, errors.New("invalid data offset") - } - - toSkip := dataOffset - bytesRead - if toSkip > 0 { - if _, err := io.CopyN(io.Discard, br, int64(toSkip)); err != nil { - return nil, err - } - } - - data := make([]byte, dataSize) - if _, err := io.ReadFull(br, data); err != nil { - return nil, err - } - - return NewDomainMatcherFromBuffer(data) + return NewDomainMatcherFromBuffer(siteData) } diff --git a/common/strmatcher/ac_automaton_matcher.go b/common/strmatcher/ac_automaton_matcher.go index 24be9dac9193..7844333d1b87 100644 --- a/common/strmatcher/ac_automaton_matcher.go +++ b/common/strmatcher/ac_automaton_matcher.go @@ -7,8 +7,8 @@ import ( const validCharCount = 53 type MatchType struct { - matchType Type - exist bool + Type Type + Exist bool } const ( @@ -17,23 +17,23 @@ const ( ) type Edge struct { - edgeType bool - nextNode int + Type bool + NextNode int } type ACAutomaton struct { - trie [][validCharCount]Edge - fail []int - exists []MatchType - count int + Trie [][validCharCount]Edge + Fail []int + Exists []MatchType + Count int } func newNode() [validCharCount]Edge { var s [validCharCount]Edge for i := range s { s[i] = Edge{ - edgeType: FailEdge, - nextNode: 0, + Type: FailEdge, + NextNode: 0, } } return s @@ -123,11 +123,11 @@ var char2Index = []int{ func NewACAutomaton() *ACAutomaton { ac := new(ACAutomaton) - ac.trie = append(ac.trie, newNode()) - ac.fail = append(ac.fail, 0) - ac.exists = append(ac.exists, MatchType{ - matchType: Full, - exist: false, + ac.Trie = append(ac.Trie, newNode()) + ac.Fail = append(ac.Fail, 0) + ac.Exists = append(ac.Exists, MatchType{ + Type: Full, + Exist: false, }) return ac } @@ -136,53 +136,53 @@ func (ac *ACAutomaton) Add(domain string, t Type) { node := 0 for i := len(domain) - 1; i >= 0; i-- { idx := char2Index[domain[i]] - if ac.trie[node][idx].nextNode == 0 { - ac.count++ - if len(ac.trie) < ac.count+1 { - ac.trie = append(ac.trie, newNode()) - ac.fail = append(ac.fail, 0) - ac.exists = append(ac.exists, MatchType{ - matchType: Full, - exist: false, + if ac.Trie[node][idx].NextNode == 0 { + ac.Count++ + if len(ac.Trie) < ac.Count+1 { + ac.Trie = append(ac.Trie, newNode()) + ac.Fail = append(ac.Fail, 0) + ac.Exists = append(ac.Exists, MatchType{ + Type: Full, + Exist: false, }) } - ac.trie[node][idx] = Edge{ - edgeType: TrieEdge, - nextNode: ac.count, + ac.Trie[node][idx] = Edge{ + Type: TrieEdge, + NextNode: ac.Count, } } - node = ac.trie[node][idx].nextNode + node = ac.Trie[node][idx].NextNode } - ac.exists[node] = MatchType{ - matchType: t, - exist: true, + ac.Exists[node] = MatchType{ + Type: t, + Exist: true, } switch t { case Domain: - ac.exists[node] = MatchType{ - matchType: Full, - exist: true, + ac.Exists[node] = MatchType{ + Type: Full, + Exist: true, } idx := char2Index['.'] - if ac.trie[node][idx].nextNode == 0 { - ac.count++ - if len(ac.trie) < ac.count+1 { - ac.trie = append(ac.trie, newNode()) - ac.fail = append(ac.fail, 0) - ac.exists = append(ac.exists, MatchType{ - matchType: Full, - exist: false, + if ac.Trie[node][idx].NextNode == 0 { + ac.Count++ + if len(ac.Trie) < ac.Count+1 { + ac.Trie = append(ac.Trie, newNode()) + ac.Fail = append(ac.Fail, 0) + ac.Exists = append(ac.Exists, MatchType{ + Type: Full, + Exist: false, }) } - ac.trie[node][idx] = Edge{ - edgeType: TrieEdge, - nextNode: ac.count, + ac.Trie[node][idx] = Edge{ + Type: TrieEdge, + NextNode: ac.Count, } } - node = ac.trie[node][idx].nextNode - ac.exists[node] = MatchType{ - matchType: t, - exist: true, + node = ac.Trie[node][idx].NextNode + ac.Exists[node] = MatchType{ + Type: t, + Exist: true, } default: break @@ -192,8 +192,8 @@ func (ac *ACAutomaton) Add(domain string, t Type) { func (ac *ACAutomaton) Build() { queue := list.New() for i := 0; i < validCharCount; i++ { - if ac.trie[0][i].nextNode != 0 { - queue.PushBack(ac.trie[0][i]) + if ac.Trie[0][i].NextNode != 0 { + queue.PushBack(ac.Trie[0][i]) } } for { @@ -201,16 +201,16 @@ func (ac *ACAutomaton) Build() { if front == nil { break } else { - node := front.Value.(Edge).nextNode + node := front.Value.(Edge).NextNode queue.Remove(front) for i := 0; i < validCharCount; i++ { - if ac.trie[node][i].nextNode != 0 { - ac.fail[ac.trie[node][i].nextNode] = ac.trie[ac.fail[node]][i].nextNode - queue.PushBack(ac.trie[node][i]) + if ac.Trie[node][i].NextNode != 0 { + ac.Fail[ac.Trie[node][i].NextNode] = ac.Trie[ac.Fail[node]][i].NextNode + queue.PushBack(ac.Trie[node][i]) } else { - ac.trie[node][i] = Edge{ - edgeType: FailEdge, - nextNode: ac.trie[ac.fail[node]][i].nextNode, + ac.Trie[node][i] = Edge{ + Type: FailEdge, + NextNode: ac.Trie[ac.Fail[node]][i].NextNode, } } } @@ -230,9 +230,9 @@ func (ac *ACAutomaton) Match(s string) bool { return false } idx := char2Index[chr] - fullMatch = fullMatch && ac.trie[node][idx].edgeType - node = ac.trie[node][idx].nextNode - switch ac.exists[node].matchType { + fullMatch = fullMatch && ac.Trie[node][idx].Type + node = ac.Trie[node][idx].NextNode + switch ac.Exists[node].Type { case Substr: return true case Domain: @@ -243,5 +243,5 @@ func (ac *ACAutomaton) Match(s string) bool { break } } - return fullMatch && ac.exists[node].exist + return fullMatch && ac.Exists[node].Exist } diff --git a/common/strmatcher/matchers.go b/common/strmatcher/matchers.go index b5ab09c4cb9f..915927db8991 100644 --- a/common/strmatcher/matchers.go +++ b/common/strmatcher/matchers.go @@ -39,14 +39,18 @@ func (m domainMatcher) String() string { return "domain:" + string(m) } -type regexMatcher struct { - pattern *regexp.Regexp +type RegexMatcher struct { + Pattern string + reg *regexp.Regexp } -func (m *regexMatcher) Match(s string) bool { - return m.pattern.MatchString(s) +func (m *RegexMatcher) Match(s string) bool { + if m.reg == nil { + m.reg = regexp.MustCompile(m.Pattern) + } + return m.reg.MatchString(s) } -func (m *regexMatcher) String() string { - return "regexp:" + m.pattern.String() +func (m *RegexMatcher) String() string { + return "regexp:" + m.Pattern } diff --git a/common/strmatcher/mph_matcher.go b/common/strmatcher/mph_matcher.go index 3c10cb4920bd..f7d737e9a63b 100644 --- a/common/strmatcher/mph_matcher.go +++ b/common/strmatcher/mph_matcher.go @@ -25,40 +25,40 @@ func RollingHash(s string) uint32 { // 2. `substr` patterns are matched by ac automaton; // 3. `regex` patterns are matched with the regex library. type MphMatcherGroup struct { - ac *ACAutomaton - otherMatchers []matcherEntry - rules []string - level0 []uint32 - level0Mask int - level1 []uint32 - level1Mask int - count uint32 - ruleMap *map[string]uint32 + Ac *ACAutomaton + OtherMatchers []MatcherEntry + Rules []string + Level0 []uint32 + Level0Mask int + Level1 []uint32 + Level1Mask int + Count uint32 + RuleMap *map[string]uint32 } func (g *MphMatcherGroup) AddFullOrDomainPattern(pattern string, t Type) { h := RollingHash(pattern) switch t { case Domain: - (*g.ruleMap)["."+pattern] = h*PrimeRK + uint32('.') + (*g.RuleMap)["."+pattern] = h*PrimeRK + uint32('.') fallthrough case Full: - (*g.ruleMap)[pattern] = h + (*g.RuleMap)[pattern] = h default: } } func NewMphMatcherGroup() *MphMatcherGroup { return &MphMatcherGroup{ - ac: nil, - otherMatchers: nil, - rules: nil, - level0: nil, - level0Mask: 0, - level1: nil, - level1Mask: 0, - count: 1, - ruleMap: &map[string]uint32{}, + Ac: nil, + OtherMatchers: nil, + Rules: nil, + Level0: nil, + Level0Mask: 0, + Level1: nil, + Level1Mask: 0, + Count: 1, + RuleMap: &map[string]uint32{}, } } @@ -66,10 +66,10 @@ func NewMphMatcherGroup() *MphMatcherGroup { func (g *MphMatcherGroup) AddPattern(pattern string, t Type) (uint32, error) { switch t { case Substr: - if g.ac == nil { - g.ac = NewACAutomaton() + if g.Ac == nil { + g.Ac = NewACAutomaton() } - g.ac.Add(pattern, t) + g.Ac.Add(pattern, t) case Full, Domain: pattern = strings.ToLower(pattern) g.AddFullOrDomainPattern(pattern, t) @@ -78,39 +78,39 @@ func (g *MphMatcherGroup) AddPattern(pattern string, t Type) (uint32, error) { if err != nil { return 0, err } - g.otherMatchers = append(g.otherMatchers, matcherEntry{ - m: ®exMatcher{pattern: r}, - id: g.count, + g.OtherMatchers = append(g.OtherMatchers, MatcherEntry{ + M: &RegexMatcher{Pattern: pattern, reg: r}, + Id: g.Count, }) default: panic("Unknown type") } - return g.count, nil + return g.Count, nil } // Build builds a minimal perfect hash table and ac automaton from insert rules func (g *MphMatcherGroup) Build() { - if g.ac != nil { - g.ac.Build() + if g.Ac != nil { + g.Ac.Build() } - keyLen := len(*g.ruleMap) + keyLen := len(*g.RuleMap) if keyLen == 0 { keyLen = 1 - (*g.ruleMap)["empty___"] = RollingHash("empty___") + (*g.RuleMap)["empty___"] = RollingHash("empty___") } - g.level0 = make([]uint32, nextPow2(keyLen/4)) - g.level0Mask = len(g.level0) - 1 - g.level1 = make([]uint32, nextPow2(keyLen)) - g.level1Mask = len(g.level1) - 1 - sparseBuckets := make([][]int, len(g.level0)) + g.Level0 = make([]uint32, nextPow2(keyLen/4)) + g.Level0Mask = len(g.Level0) - 1 + g.Level1 = make([]uint32, nextPow2(keyLen)) + g.Level1Mask = len(g.Level1) - 1 + sparseBuckets := make([][]int, len(g.Level0)) var ruleIdx int - for rule, hash := range *g.ruleMap { - n := int(hash) & g.level0Mask - g.rules = append(g.rules, rule) + for rule, hash := range *g.RuleMap { + n := int(hash) & g.Level0Mask + g.Rules = append(g.Rules, rule) sparseBuckets[n] = append(sparseBuckets[n], ruleIdx) ruleIdx++ } - g.ruleMap = nil + g.RuleMap = nil var buckets []indexBucket for n, vals := range sparseBuckets { if len(vals) > 0 { @@ -119,7 +119,7 @@ func (g *MphMatcherGroup) Build() { } sort.Sort(bySize(buckets)) - occ := make([]bool, len(g.level1)) + occ := make([]bool, len(g.Level1)) var tmpOcc []int for _, bucket := range buckets { seed := uint32(0) @@ -127,7 +127,7 @@ func (g *MphMatcherGroup) Build() { findSeed := true tmpOcc = tmpOcc[:0] for _, i := range bucket.vals { - n := int(strhashFallback(unsafe.Pointer(&g.rules[i]), uintptr(seed))) & g.level1Mask + n := int(strhashFallback(unsafe.Pointer(&g.Rules[i]), uintptr(seed))) & g.Level1Mask if occ[n] { for _, n := range tmpOcc { occ[n] = false @@ -138,10 +138,10 @@ func (g *MphMatcherGroup) Build() { } occ[n] = true tmpOcc = append(tmpOcc, n) - g.level1[n] = uint32(i) + g.Level1[n] = uint32(i) } if findSeed { - g.level0[bucket.n] = seed + g.Level0[bucket.n] = seed break } } @@ -159,11 +159,11 @@ func nextPow2(v int) int { // Lookup searches for s in t and returns its index and whether it was found. func (g *MphMatcherGroup) Lookup(h uint32, s string) bool { - i0 := int(h) & g.level0Mask - seed := g.level0[i0] - i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.level1Mask - n := g.level1[i1] - return s == g.rules[int(n)] + i0 := int(h) & g.Level0Mask + seed := g.Level0[i0] + i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.Level1Mask + n := g.Level1[i1] + return s == g.Rules[int(n)] } // Match implements IndexMatcher.Match. @@ -183,13 +183,13 @@ func (g *MphMatcherGroup) Match(pattern string) []uint32 { result = append(result, 1) return result } - if g.ac != nil && g.ac.Match(pattern) { + if g.Ac != nil && g.Ac.Match(pattern) { result = append(result, 1) return result } - for _, e := range g.otherMatchers { - if e.m.Match(pattern) { - result = append(result, e.id) + for _, e := range g.OtherMatchers { + if e.M.Match(pattern) { + result = append(result, e.Id) return result } } diff --git a/common/strmatcher/mph_matcher_compact.go b/common/strmatcher/mph_matcher_compact.go index a62711d603de..a40b9f568a14 100644 --- a/common/strmatcher/mph_matcher_compact.go +++ b/common/strmatcher/mph_matcher_compact.go @@ -1,113 +1,47 @@ package strmatcher import ( - "encoding/binary" - "errors" + "bytes" + "encoding/gob" "io" - "unsafe" ) -func (g *MphMatcherGroup) Serialize(w io.Writer) error { - // header: level0 4, level1 4, rule count 4, rule data 4 - - var rulesDataLen uint32 - for _, r := range g.rules { - // 2 bytes for length - rulesDataLen += 2 + uint32(len(r)) - } - - header := []uint32{ - uint32(len(g.level0)), - uint32(len(g.level1)), - uint32(len(g.rules)), - rulesDataLen, - } - - if err := binary.Write(w, binary.LittleEndian, header); err != nil { - return err - } - - // level0 - if err := binary.Write(w, binary.LittleEndian, g.level0); err != nil { - return err - } - // level1 - if err := binary.Write(w, binary.LittleEndian, g.level1); err != nil { - return err - } - - // rules - for _, r := range g.rules { - if err := binary.Write(w, binary.LittleEndian, uint16(len(r))); err != nil { - return err - } - if _, err := w.Write([]byte(r)); err != nil { - return err - } - } +func init() { + gob.Register(&RegexMatcher{}) + gob.Register(fullMatcher("")) + gob.Register(substrMatcher("")) + gob.Register(domainMatcher("")) +} - return nil +func (g *MphMatcherGroup) Serialize(w io.Writer) error { + data := MphMatcherGroup{ + Ac: g.Ac, + OtherMatchers: g.OtherMatchers, + Rules: g.Rules, + Level0: g.Level0, + Level0Mask: g.Level0Mask, + Level1: g.Level1, + Level1Mask: g.Level1Mask, + Count: g.Count, + } + return gob.NewEncoder(w).Encode(data) } func NewMphMatcherGroupFromBuffer(data []byte) (*MphMatcherGroup, error) { - if len(data) < 16 { - return nil, errors.New("invalid data length") - } - - l0Len := binary.LittleEndian.Uint32(data[0:4]) - l1Len := binary.LittleEndian.Uint32(data[4:8]) - ruleCount := binary.LittleEndian.Uint32(data[8:12]) - rulesDataLen := binary.LittleEndian.Uint32(data[12:16]) - - offset := 16 - - // check size - requiredSize := offset + int(l0Len)*4 + int(l1Len)*4 + int(rulesDataLen) - if len(data) < requiredSize { - return nil, errors.New("data truncated") + var gData MphMatcherGroup + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&gData); err != nil { + return nil, err } g := NewMphMatcherGroup() - - // level0 - if l0Len > 0 { - g.level0 = unsafe.Slice((*uint32)(unsafe.Pointer(&data[offset])), l0Len) - offset += int(l0Len) * 4 - g.level0Mask = int(l0Len) - 1 - } - - // level1 - if l1Len > 0 { - g.level1 = unsafe.Slice((*uint32)(unsafe.Pointer(&data[offset])), l1Len) - offset += int(l1Len) * 4 - g.level1Mask = int(l1Len) - 1 - } - - // build rules - if ruleCount > 0 { - g.rules = make([]string, ruleCount) - rulesOffset := offset - - for i := range ruleCount { - if rulesOffset+2 > len(data) { - return nil, errors.New("rules truncated") - } - strLen := int(binary.LittleEndian.Uint16(data[rulesOffset : rulesOffset+2])) - rulesOffset += 2 - - if rulesOffset+strLen > len(data) { - return nil, errors.New("rule string truncated") - } - - strBytes := data[rulesOffset : rulesOffset+strLen] - g.rules[i] = unsafe.String(unsafe.SliceData(strBytes), strLen) - - rulesOffset += strLen - } - } - - // count in MphMatcherGroup always is 1, may break other this? - g.count = uint32(ruleCount) + 1 + g.Ac = gData.Ac + g.OtherMatchers = gData.OtherMatchers + g.Rules = gData.Rules + g.Level0 = gData.Level0 + g.Level0Mask = gData.Level0Mask + g.Level1 = gData.Level1 + g.Level1Mask = gData.Level1Mask + g.Count = gData.Count return g, nil } diff --git a/common/strmatcher/strmatcher.go b/common/strmatcher/strmatcher.go index 4035acc3b2f9..d823e429720c 100644 --- a/common/strmatcher/strmatcher.go +++ b/common/strmatcher/strmatcher.go @@ -41,8 +41,9 @@ func (t Type) New(pattern string) (Matcher, error) { if err != nil { return nil, err } - return ®exMatcher{ - pattern: r, + return &RegexMatcher{ + Pattern: pattern, + reg: r, }, nil default: return nil, errors.New("unk type") @@ -55,9 +56,9 @@ type IndexMatcher interface { Match(input string) []uint32 } -type matcherEntry struct { - m Matcher - id uint32 +type MatcherEntry struct { + M Matcher + Id uint32 } // MatcherGroup is an implementation of IndexMatcher. @@ -66,7 +67,7 @@ type MatcherGroup struct { count uint32 fullMatcher FullMatcherGroup domainMatcher DomainMatcherGroup - otherMatchers []matcherEntry + otherMatchers []MatcherEntry } // Add adds a new Matcher into the MatcherGroup, and returns its index. The index will never be 0. @@ -80,9 +81,9 @@ func (g *MatcherGroup) Add(m Matcher) uint32 { case domainMatcher: g.domainMatcher.addMatcher(tm, c) default: - g.otherMatchers = append(g.otherMatchers, matcherEntry{ - m: m, - id: c, + g.otherMatchers = append(g.otherMatchers, MatcherEntry{ + M: m, + Id: c, }) } @@ -95,8 +96,8 @@ func (g *MatcherGroup) Match(pattern string) []uint32 { result = append(result, g.fullMatcher.Match(pattern)...) result = append(result, g.domainMatcher.Match(pattern)...) for _, e := range g.otherMatchers { - if e.m.Match(pattern) { - result = append(result, e.id) + if e.M.Match(pattern) { + result = append(result, e.Id) } } return result From 6d40051155ed4af65889d1f49e2016ecc6e52e9c Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Fri, 30 Jan 2026 15:46:28 +0330 Subject: [PATCH 12/20] add MphCachePath env --- app/router/condition_test.go | 2 +- app/router/config.go | 6 +++- app/router/router.go | 4 +-- common/platform/platform.go | 2 ++ infra/conf/router.go | 49 +++++------------------------ infra/conf/xray.go | 60 ++++++++++++++++++++++++++++++++++++ 6 files changed, 77 insertions(+), 46 deletions(-) diff --git a/app/router/condition_test.go b/app/router/condition_test.go index 63259b500ee9..1272aef6e9f4 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -288,7 +288,7 @@ func TestRoutingRule(t *testing.T) { } for _, test := range cases { - cond, err := test.rule.BuildCondition("") + cond, err := test.rule.BuildCondition() common.Must(err) for _, subtest := range test.test { diff --git a/app/router/config.go b/app/router/config.go index 59ab7c9d1d82..118fa3b0f9ea 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" @@ -31,7 +32,7 @@ func (r *Rule) Apply(ctx routing.Context) bool { return r.Condition.Apply(ctx) } -func (rr *RoutingRule) BuildCondition(domainMatcherPath string) (Condition, error) { +func (rr *RoutingRule) BuildCondition() (Condition, error) { conds := NewConditionChan() if len(rr.InboundTag) > 0 { @@ -108,6 +109,9 @@ func (rr *RoutingRule) BuildCondition(domainMatcherPath string) (Condition, erro if len(rr.Domain) > 0 { var matcher *DomainMatcher var err error + // Check if domain matcher cache is provided via environment + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + if domainMatcherPath != "" { matcher, err = GetDomainMathcerWithRuleTag(domainMatcherPath, rr.RuleTag) if err != nil { diff --git a/app/router/router.go b/app/router/router.go index 3f0886d3d346..790bf8e23c70 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -55,7 +55,7 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out r.rules = make([]*Rule, 0, len(config.Rule)) for _, rule := range config.Rule { - cond, err := rule.BuildCondition(config.DomainMatcherPath) + cond, err := rule.BuildCondition() if err != nil { return err } @@ -129,7 +129,7 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error { if r.RuleExists(rule.GetRuleTag()) { return errors.New("duplicate ruleTag ", rule.GetRuleTag()) } - cond, err := rule.BuildCondition(config.DomainMatcherPath) + cond, err := rule.BuildCondition() if err != nil { return err } diff --git a/common/platform/platform.go b/common/platform/platform.go index 80e62874d6e4..8f59b255dd34 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -24,6 +24,8 @@ const ( XUDPBaseKey = "xray.xudp.basekey" TunFdKey = "xray.tun.fd" + + MphCachePath = "xray.mph.path" ) type EnvFlag struct { diff --git a/infra/conf/router.go b/infra/conf/router.go index 2d2cf8bb7e21..1960b7ddb566 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -4,9 +4,7 @@ import ( "bufio" "bytes" "encoding/json" - "fmt" "io" - "os" "runtime" "strconv" "strings" @@ -217,6 +215,13 @@ func loadIP(file, code string) ([]*router.CIDR, error) { } func loadSite(file, code string) ([]*router.Domain, error) { + + // Check if domain matcher cache is provided via environment + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + if domainMatcherPath != "" { + return []*router.Domain{{}}, nil + } + bs, err := loadFile(file, code) if err != nil { return nil, err @@ -678,43 +683,3 @@ func parseRule(msg json.RawMessage) (*router.RoutingRule, error) { } return fieldrule, nil } - -func (c *RouterConfig) BuildDomainMatcherCache(customMatcherFilePath *string) error { - var geosite []*router.GeoSite - matcherFilePath := platform.GetAssetLocation("matcher.cache") - - if customMatcherFilePath != nil { - matcherFilePath = *customMatcherFilePath - } - - routerConfig, err := c.Build() - - if len(routerConfig.Rule) == 0 { - return fmt.Errorf("no routing") - } - - for _, rule := range routerConfig.Rule { - // write it with ruleTag key - simpleGeoSite := router.GeoSite{CountryCode: rule.RuleTag, Domain: rule.Domain} - - geosite = append(geosite, &simpleGeoSite) - } - - f, err := os.Create(matcherFilePath) - if err != nil { - return err - } - defer f.Close() - - var buf bytes.Buffer - - if err := router.SerializeGeoSiteList(geosite, &buf); err != nil { - return err - } - - if _, err := f.Write(buf.Bytes()); err != nil { - return err - } - - return nil -} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 9e5c1394ab0d..b1bf85d90077 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -1,16 +1,20 @@ package conf import ( + "bytes" "context" "encoding/json" + "os" "path/filepath" "strings" "github.com/xtls/xray-core/app/dispatcher" "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/app/stats" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/serial" core "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/transport/internet" @@ -607,6 +611,62 @@ func (c *Config) Build() (*core.Config, error) { return config, nil } +func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { + var geosite []*router.GeoSite + matcherFilePath := platform.GetAssetLocation("matcher.cache") + + if customMatcherFilePath != nil { + matcherFilePath = *customMatcherFilePath + } + + // get routing + routerConfig, err := c.RouterConfig.Build() + + for _, rule := range routerConfig.Rule { + // write it with ruleTag key + simpleGeoSite := router.GeoSite{CountryCode: rule.RuleTag, Domain: rule.Domain} + + geosite = append(geosite, &simpleGeoSite) + } + + // get dns + dnsConfig, err := c.DNSConfig.Build() + + for _, ns := range dnsConfig.NameServer { + pureDomains := []*router.Domain{} + + // convert to pure domain + for _, pd := range ns.PrioritizedDomain { + pureDomains = append(pureDomains, &router.Domain{ + Type: router.Domain_Type(pd.Type), + Value: pd.Domain, + }) + } + // write it with Tag key + simpleGeoSite := router.GeoSite{CountryCode: ns.Tag, Domain: pureDomains} + + geosite = append(geosite, &simpleGeoSite) + } + + f, err := os.Create(matcherFilePath) + if err != nil { + return err + } + defer f.Close() + + var buf bytes.Buffer + + if err := router.SerializeGeoSiteList(geosite, &buf); err != nil { + return err + } + + if _, err := f.Write(buf.Bytes()); err != nil { + return err + } + + return nil +} + // Convert string to Address. func ParseSendThough(Addr *string) *Address { var addr Address From aa2eae7a1c774c52b68a4078002049d4d08983f1 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Fri, 30 Jan 2026 16:17:16 +0330 Subject: [PATCH 13/20] implement for dns --- app/dns/nameserver.go | 71 ++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index dbab5e8aba21..f38b245a3df7 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -10,6 +10,8 @@ import ( "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/strmatcher" "github.com/xtls/xray-core/core" @@ -17,6 +19,18 @@ import ( "github.com/xtls/xray-core/features/routing" ) +type mphMatcherWrapper struct { + m *strmatcher.MphMatcherGroup +} + +func (w *mphMatcherWrapper) Match(s string) bool { + return w.m.Match(s) != nil +} + +func (w *mphMatcherWrapper) String() string { + return "mph-matcher" +} + // Server is the interface for Name Server. type Server interface { // Name of the Client. @@ -132,29 +146,50 @@ func NewClient( var rules []string ruleCurr := 0 ruleIter := 0 - for i, domain := range ns.PrioritizedDomain { - ns.PrioritizedDomain[i] = nil - domainRule, err := toStrMatcher(domain.Type, domain.Domain) - if err != nil { - errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]") - domainRule, _ = toStrMatcher(DomainMatchingType_Full, "hack.fix.index.for.illegal.domain.rule") + + // Check if domain matcher cache is provided via environment + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + var mphLoaded bool + + if domainMatcherPath != "" && ns.Tag != "" { + f, err := filesystem.NewFileReader(domainMatcherPath) + if err == nil { + defer f.Close() + g, err := router.LoadGeoSiteMatcher(f, ns.Tag) + if err == nil { + errors.LogDebug(ctx, "MphDomainMatcher is enabled for DNS tag: ", ns.Tag) + updateDomainRule(&mphMatcherWrapper{m: g}, 0, *matcherInfos) + rules = append(rules, "[MPH Cache]") + mphLoaded = true + } } - originalRuleIdx := ruleCurr - if ruleCurr < len(ns.OriginalRules) { - rule := ns.OriginalRules[ruleCurr] - if ruleCurr >= len(rules) { - rules = append(rules, rule.Rule) + } + + if !mphLoaded { + for i, domain := range ns.PrioritizedDomain { + ns.PrioritizedDomain[i] = nil + domainRule, err := toStrMatcher(domain.Type, domain.Domain) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]") + domainRule, _ = toStrMatcher(DomainMatchingType_Full, "hack.fix.index.for.illegal.domain.rule") } - ruleIter++ - if ruleIter >= int(rule.Size) { - ruleIter = 0 + originalRuleIdx := ruleCurr + if ruleCurr < len(ns.OriginalRules) { + rule := ns.OriginalRules[ruleCurr] + if ruleCurr >= len(rules) { + rules = append(rules, rule.Rule) + } + ruleIter++ + if ruleIter >= int(rule.Size) { + ruleIter = 0 + ruleCurr++ + } + } else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests) + rules = append(rules, domainRule.String()) ruleCurr++ } - } else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests) - rules = append(rules, domainRule.String()) - ruleCurr++ + updateDomainRule(domainRule, originalRuleIdx, *matcherInfos) } - updateDomainRule(domainRule, originalRuleIdx, *matcherInfos) } ns.PrioritizedDomain = nil runtime.GC() From 7b20612fa1132aaa8800b24ce5bbdaee6e2d1af9 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Fri, 30 Jan 2026 17:55:27 +0330 Subject: [PATCH 14/20] add deps to avoid write geosite twice for dns and router --- app/dns/nameserver.go | 4 +- app/router/condition_serialize_test.go | 31 +++++++++- app/router/config.go | 4 +- app/router/geosite_compact.go | 44 +++++++++++-- common/strmatcher/strmatcher.go | 13 ++++ infra/conf/xray.go | 85 ++++++++++++++++++-------- 6 files changed, 146 insertions(+), 35 deletions(-) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index f38b245a3df7..00d435b59218 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -20,7 +20,7 @@ import ( ) type mphMatcherWrapper struct { - m *strmatcher.MphMatcherGroup + m strmatcher.IndexMatcher } func (w *mphMatcherWrapper) Match(s string) bool { @@ -157,7 +157,7 @@ func NewClient( defer f.Close() g, err := router.LoadGeoSiteMatcher(f, ns.Tag) if err == nil { - errors.LogDebug(ctx, "MphDomainMatcher is enabled for DNS tag: ", ns.Tag) + errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for ", ns.Tag, " dns tag)") updateDomainRule(&mphMatcherWrapper{m: g}, 0, *matcherInfos) rules = append(rules, "[MPH Cache]") mphLoaded = true diff --git a/app/router/condition_serialize_test.go b/app/router/condition_serialize_test.go index b1605ef4fa2e..f940e0687bf2 100644 --- a/app/router/condition_serialize_test.go +++ b/app/router/condition_serialize_test.go @@ -70,7 +70,7 @@ func TestGeoSiteSerialization(t *testing.T) { } var buf bytes.Buffer - if err := router.SerializeGeoSiteList(sites, &buf); err != nil { + if err := router.SerializeGeoSiteList(sites, nil, &buf); err != nil { t.Fatalf("SerializeGeoSiteList failed: %v", err) } @@ -129,3 +129,32 @@ func TestGeoSiteSerialization(t *testing.T) { t.Error("LoadGeoSiteMatcher(unknown) should fail") } } +func TestGeoSiteSerializationWithDeps(t *testing.T) { + sites := []*router.GeoSite{ + { + CountryCode: "geosite:cn", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "baidu.cn"}, + }, + }, + { + CountryCode: "rule-1", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.com"}, + }, + }, + } + deps := map[string][]string{ + "rule-1": {"geosite:cn"}, + } + + var buf bytes.Buffer + err := router.SerializeGeoSiteList(sites, deps, &buf) + require.NoError(t, err) + + matcher, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "rule-1") + require.NoError(t, err) + + require.True(t, matcher.Match("google.com") != nil) + require.True(t, matcher.Match("baidu.cn") != nil) +} diff --git a/app/router/config.go b/app/router/config.go index 118fa3b0f9ea..64f571076432 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -117,15 +117,15 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { if err != nil { return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err) } + errors.LogDebug(context.Background(), "MphDomainMatcher loaded from cache for ", rr.RuleTag, " rule tag)") } else { matcher, err = NewMphMatcherGroup(rr.Domain) if err != nil { return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) } - + errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") } - errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") conds.Add(matcher) rr.Domain = nil runtime.GC() diff --git a/app/router/geosite_compact.go b/app/router/geosite_compact.go index 0040902d3fec..a807b01a3314 100644 --- a/app/router/geosite_compact.go +++ b/app/router/geosite_compact.go @@ -4,17 +4,20 @@ import ( "encoding/gob" "errors" "io" + "runtime" "github.com/xtls/xray-core/common/strmatcher" ) type geoSiteListGob struct { Sites map[string][]byte + Deps map[string][]string } -func SerializeGeoSiteList(sites []*GeoSite, w io.Writer) error { +func SerializeGeoSiteList(sites []*GeoSite, deps map[string][]string, w io.Writer) error { data := geoSiteListGob{ Sites: make(map[string][]byte), + Deps: deps, } for _, site := range sites { @@ -44,16 +47,45 @@ func (w *bytesWriter) Bytes() []byte { return w.data } -func LoadGeoSiteMatcher(r io.Reader, countryCode string) (*strmatcher.MphMatcherGroup, error) { +func LoadGeoSiteMatcher(r io.Reader, countryCode string) (strmatcher.IndexMatcher, error) { var data geoSiteListGob if err := gob.NewDecoder(r).Decode(&data); err != nil { return nil, err } - siteData, ok := data.Sites[countryCode] - if !ok { - return nil, errors.New("country code not found") + return loadWithDeps(&data, countryCode, make(map[string]bool)) +} + +func loadWithDeps(data *geoSiteListGob, code string, visited map[string]bool) (strmatcher.IndexMatcher, error) { + if visited[code] { + return nil, errors.New("cyclic dependency") + } + visited[code] = true + + var matchers []strmatcher.IndexMatcher + + if siteData, ok := data.Sites[code]; ok { + m, err := NewDomainMatcherFromBuffer(siteData) + if err == nil { + matchers = append(matchers, m) + } } - return NewDomainMatcherFromBuffer(siteData) + if deps, ok := data.Deps[code]; ok { + for _, dep := range deps { + m, err := loadWithDeps(data, dep, visited) + if err == nil { + matchers = append(matchers, m) + } + } + } + + if len(matchers) == 0 { + return nil, errors.New("matcher not found for: " + code) + } + if len(matchers) == 1 { + return matchers[0], nil + } + runtime.GC() + return &strmatcher.IndexMatcherGroup{Matchers: matchers}, nil } diff --git a/common/strmatcher/strmatcher.go b/common/strmatcher/strmatcher.go index d823e429720c..40a11123428b 100644 --- a/common/strmatcher/strmatcher.go +++ b/common/strmatcher/strmatcher.go @@ -107,3 +107,16 @@ func (g *MatcherGroup) Match(pattern string) []uint32 { func (g *MatcherGroup) Size() uint32 { return g.count } + +type IndexMatcherGroup struct { + Matchers []IndexMatcher +} + +func (g *IndexMatcherGroup) Match(input string) []uint32 { + for _, m := range g.Matchers { + if res := m.Match(input); len(res) > 0 { + return res + } + } + return nil +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index b1bf85d90077..6813c41cfcf3 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -613,39 +613,76 @@ func (c *Config) Build() (*core.Config, error) { func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { var geosite []*router.GeoSite + deps := make(map[string][]string) + uniqueGeosites := make(map[string]bool) matcherFilePath := platform.GetAssetLocation("matcher.cache") if customMatcherFilePath != nil { matcherFilePath = *customMatcherFilePath } - // get routing - routerConfig, err := c.RouterConfig.Build() - - for _, rule := range routerConfig.Rule { - // write it with ruleTag key - simpleGeoSite := router.GeoSite{CountryCode: rule.RuleTag, Domain: rule.Domain} - - geosite = append(geosite, &simpleGeoSite) + processDomains := func(tag string, rawDomains []string) { + var manualDomains []*router.Domain + var dDeps []string + for _, dStr := range rawDomains { + if strings.HasPrefix(dStr, "geosite:") { + country := strings.ToUpper(dStr[8:]) + depKey := dStr + if !uniqueGeosites[country] { + ds, err := loadGeositeWithAttr("geosite.dat", country) + if err == nil { + uniqueGeosites[country] = true + geosite = append(geosite, &router.GeoSite{CountryCode: depKey, Domain: ds}) + } + } + dDeps = append(dDeps, depKey) + } else { + ds, err := parseDomainRule(dStr) + if err == nil { + manualDomains = append(manualDomains, ds...) + } + } + } + if len(manualDomains) > 0 { + geosite = append(geosite, &router.GeoSite{CountryCode: tag, Domain: manualDomains}) + } + if len(dDeps) > 0 { + deps[tag] = dDeps + } } - // get dns - dnsConfig, err := c.DNSConfig.Build() - - for _, ns := range dnsConfig.NameServer { - pureDomains := []*router.Domain{} - - // convert to pure domain - for _, pd := range ns.PrioritizedDomain { - pureDomains = append(pureDomains, &router.Domain{ - Type: router.Domain_Type(pd.Type), - Value: pd.Domain, - }) + // proccess rules + if c.RouterConfig != nil { + for _, rawRule := range c.RouterConfig.RuleList { + type SimpleRule struct { + RuleTag string `json:"ruleTag"` + Domain *StringList `json:"domain"` + Domains *StringList `json:"domains"` + } + var sr SimpleRule + json.Unmarshal(rawRule, &sr) + if sr.RuleTag == "" { + continue + } + var allDomains []string + if sr.Domain != nil { + allDomains = append(allDomains, *sr.Domain...) + } + if sr.Domains != nil { + allDomains = append(allDomains, *sr.Domains...) + } + processDomains(sr.RuleTag, allDomains) } - // write it with Tag key - simpleGeoSite := router.GeoSite{CountryCode: ns.Tag, Domain: pureDomains} + } - geosite = append(geosite, &simpleGeoSite) + // proccess dns servers + if c.DNSConfig != nil { + for _, ns := range c.DNSConfig.Servers { + if ns.Tag == "" { + continue + } + processDomains(ns.Tag, ns.Domains) + } } f, err := os.Create(matcherFilePath) @@ -656,7 +693,7 @@ func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { var buf bytes.Buffer - if err := router.SerializeGeoSiteList(geosite, &buf); err != nil { + if err := router.SerializeGeoSiteList(geosite, deps, &buf); err != nil { return err } From a6c9d82fc7741d2b9fd126cfe9786a4a38cbc929 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Fri, 30 Jan 2026 18:30:34 +0330 Subject: [PATCH 15/20] improve TestGeoSiteSerialization --- app/router/condition_serialize_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/router/condition_serialize_test.go b/app/router/condition_serialize_test.go index f940e0687bf2..79252a5da6eb 100644 --- a/app/router/condition_serialize_test.go +++ b/app/router/condition_serialize_test.go @@ -137,6 +137,12 @@ func TestGeoSiteSerializationWithDeps(t *testing.T) { {Type: router.Domain_Domain, Value: "baidu.cn"}, }, }, + { + CountryCode: "geosite:google@cn", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.cn"}, + }, + }, { CountryCode: "rule-1", Domain: []*router.Domain{ @@ -145,7 +151,7 @@ func TestGeoSiteSerializationWithDeps(t *testing.T) { }, } deps := map[string][]string{ - "rule-1": {"geosite:cn"}, + "rule-1": {"geosite:cn", "geosite:google@cn"}, } var buf bytes.Buffer @@ -157,4 +163,5 @@ func TestGeoSiteSerializationWithDeps(t *testing.T) { require.True(t, matcher.Match("google.com") != nil) require.True(t, matcher.Match("baidu.cn") != nil) + require.True(t, matcher.Match("google.cn") != nil) } From 3d6bee3fa83e4d5bcf8527383cfb3e8d0c83cbec Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Fri, 30 Jan 2026 21:08:08 +0330 Subject: [PATCH 16/20] Implement for Hosts --- app/dns/dns.go | 30 ++++++- app/dns/hosts.go | 49 ++++++++++- app/dns/hosts_test.go | 56 ++++++++++++ app/router/condition_serialize_test.go | 4 +- app/router/geosite_compact.go | 11 ++- common/strmatcher/mph_matcher.go | 4 + common/strmatcher/strmatcher.go | 21 ++++- infra/conf/xray.go | 117 ++++++++++++++++++++++--- 8 files changed, 270 insertions(+), 22 deletions(-) diff --git a/app/dns/dns.go b/app/dns/dns.go index 603640f1549f..c108208362ea 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -12,9 +12,11 @@ import ( "sync" "time" + "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/strmatcher" "github.com/xtls/xray-core/features/dns" @@ -83,9 +85,31 @@ func New(ctx context.Context, config *Config) (*DNS, error) { return nil, errors.New("unexpected query strategy ", config.QueryStrategy) } - hosts, err := NewStaticHosts(config.StaticHosts) - if err != nil { - return nil, errors.New("failed to create hosts").Base(err) + var hosts *StaticHosts + mphLoaded := false + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + if domainMatcherPath != "" { + if f, err := os.Open(domainMatcherPath); err == nil { + defer f.Close() + if m, err := router.LoadGeoSiteMatcher(f, "HOSTS"); err == nil { + f.Seek(0, 0) + if hostIPs, err := router.LoadGeoSiteHosts(f); err == nil { + if sh, err := NewStaticHostsFromCache(m, hostIPs); err == nil { + hosts = sh + mphLoaded = true + errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for DNS hosts, size: ", sh.matchers.Size()) + } + } + } + } + } + + if !mphLoaded { + sh, err := NewStaticHosts(config.StaticHosts) + if err != nil { + return nil, errors.New("failed to create hosts").Base(err) + } + hosts = sh } var clients []*Client diff --git a/app/dns/hosts.go b/app/dns/hosts.go index 7c9cdee37b71..fab08d54c9e1 100644 --- a/app/dns/hosts.go +++ b/app/dns/hosts.go @@ -14,7 +14,7 @@ import ( // StaticHosts represents static domain-ip mapping in DNS server. type StaticHosts struct { ips [][]net.Address - matchers *strmatcher.MatcherGroup + matchers strmatcher.IndexMatcher } // NewStaticHosts creates a new StaticHosts instance. @@ -124,3 +124,50 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) ( func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) { return h.lookup(domain, option, 5) } +func NewStaticHostsFromCache(matcher strmatcher.IndexMatcher, hostIPs map[string][]string) (*StaticHosts, error) { + sh := &StaticHosts{ + ips: make([][]net.Address, matcher.Size()+1), + matchers: matcher, + } + + order := hostIPs["_ORDER"] + var offset uint32 + + img, ok := matcher.(*strmatcher.IndexMatcherGroup) + if !ok { + // Single matcher (e.g. only manual or only one geosite) + if len(order) > 0 { + pattern := order[0] + ips := parseIPs(hostIPs[pattern]) + for i := uint32(1); i <= matcher.Size(); i++ { + sh.ips[i] = ips + } + } + return sh, nil + } + + for i, m := range img.Matchers { + if i < len(order) { + pattern := order[i] + ips := parseIPs(hostIPs[pattern]) + for j := uint32(1); j <= m.Size(); j++ { + sh.ips[offset+j] = ips + } + offset += m.Size() + } + } + return sh, nil +} + +func parseIPs(raw []string) []net.Address { + addrs := make([]net.Address, 0, len(raw)) + for _, s := range raw { + if len(s) > 1 && s[0] == '#' { + rcode, _ := strconv.Atoi(s[1:]) + addrs = append(addrs, dns.RCodeError(rcode)) + } else { + addrs = append(addrs, net.ParseAddress(s)) + } + } + return addrs +} diff --git a/app/dns/hosts_test.go b/app/dns/hosts_test.go index 2c7f8b69ce76..2b9c24d8422b 100644 --- a/app/dns/hosts_test.go +++ b/app/dns/hosts_test.go @@ -1,10 +1,12 @@ package dns_test import ( + "bytes" "testing" "github.com/google/go-cmp/cmp" . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/features/dns" @@ -130,3 +132,57 @@ func TestStaticHosts(t *testing.T) { } } } +func TestStaticHostsFromCache(t *testing.T) { + sites := []*router.GeoSite{ + { + CountryCode: "cloudflare-dns.com", + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "example.com"}, + }, + }, + { + CountryCode: "geosite:cn", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "baidu.cn"}, + }, + }, + } + deps := map[string][]string{ + "HOSTS": {"cloudflare-dns.com", "geosite:cn"}, + } + hostIPs := map[string][]string{ + "cloudflare-dns.com": {"1.1.1.1"}, + "geosite:cn": {"2.2.2.2"}, + "_ORDER": {"cloudflare-dns.com", "geosite:cn"}, + } + + var buf bytes.Buffer + err := router.SerializeGeoSiteList(sites, deps, hostIPs, &buf) + common.Must(err) + + // Load matcher + m, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "HOSTS") + common.Must(err) + + // Load hostIPs + f := bytes.NewReader(buf.Bytes()) + hips, err := router.LoadGeoSiteHosts(f) + common.Must(err) + + hosts, err := NewStaticHostsFromCache(m, hips) + common.Must(err) + + { + ips, _ := hosts.Lookup("example.com", dns.IPOption{IPv4Enable: true}) + if len(ips) != 1 || ips[0].String() != "1.1.1.1" { + t.Error("failed to lookup example.com from cache") + } + } + + { + ips, _ := hosts.Lookup("baidu.cn", dns.IPOption{IPv4Enable: true}) + if len(ips) != 1 || ips[0].String() != "2.2.2.2" { + t.Error("failed to lookup baidu.cn from cache deps") + } + } +} diff --git a/app/router/condition_serialize_test.go b/app/router/condition_serialize_test.go index 79252a5da6eb..4c6ff46467f6 100644 --- a/app/router/condition_serialize_test.go +++ b/app/router/condition_serialize_test.go @@ -70,7 +70,7 @@ func TestGeoSiteSerialization(t *testing.T) { } var buf bytes.Buffer - if err := router.SerializeGeoSiteList(sites, nil, &buf); err != nil { + if err := router.SerializeGeoSiteList(sites, nil, nil, &buf); err != nil { t.Fatalf("SerializeGeoSiteList failed: %v", err) } @@ -155,7 +155,7 @@ func TestGeoSiteSerializationWithDeps(t *testing.T) { } var buf bytes.Buffer - err := router.SerializeGeoSiteList(sites, deps, &buf) + err := router.SerializeGeoSiteList(sites, deps, nil, &buf) require.NoError(t, err) matcher, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "rule-1") diff --git a/app/router/geosite_compact.go b/app/router/geosite_compact.go index a807b01a3314..50fee83fce06 100644 --- a/app/router/geosite_compact.go +++ b/app/router/geosite_compact.go @@ -12,12 +12,14 @@ import ( type geoSiteListGob struct { Sites map[string][]byte Deps map[string][]string + Hosts map[string][]string } -func SerializeGeoSiteList(sites []*GeoSite, deps map[string][]string, w io.Writer) error { +func SerializeGeoSiteList(sites []*GeoSite, deps map[string][]string, hosts map[string][]string, w io.Writer) error { data := geoSiteListGob{ Sites: make(map[string][]byte), Deps: deps, + Hosts: hosts, } for _, site := range sites { @@ -89,3 +91,10 @@ func loadWithDeps(data *geoSiteListGob, code string, visited map[string]bool) (s runtime.GC() return &strmatcher.IndexMatcherGroup{Matchers: matchers}, nil } +func LoadGeoSiteHosts(r io.Reader) (map[string][]string, error) { + var data geoSiteListGob + if err := gob.NewDecoder(r).Decode(&data); err != nil { + return nil, err + } + return data.Hosts, nil +} diff --git a/common/strmatcher/mph_matcher.go b/common/strmatcher/mph_matcher.go index f7d737e9a63b..ff3dea65c5a7 100644 --- a/common/strmatcher/mph_matcher.go +++ b/common/strmatcher/mph_matcher.go @@ -302,3 +302,7 @@ func readUnaligned64(p unsafe.Pointer) uint64 { q := (*[8]byte)(p) return uint64(q[0]) | uint64(q[1])<<8 | uint64(q[2])<<16 | uint64(q[3])<<24 | uint64(q[4])<<32 | uint64(q[5])<<40 | uint64(q[6])<<48 | uint64(q[7])<<56 } + +func (g *MphMatcherGroup) Size() uint32 { + return g.Count +} diff --git a/common/strmatcher/strmatcher.go b/common/strmatcher/strmatcher.go index 40a11123428b..89e7dae68053 100644 --- a/common/strmatcher/strmatcher.go +++ b/common/strmatcher/strmatcher.go @@ -54,6 +54,8 @@ func (t Type) New(pattern string) (Matcher, error) { type IndexMatcher interface { // Match returns the index of a matcher that matches the input. It returns empty array if no such matcher exists. Match(input string) []uint32 + // Size returns the number of matchers in the group. + Size() uint32 } type MatcherEntry struct { @@ -113,10 +115,27 @@ type IndexMatcherGroup struct { } func (g *IndexMatcherGroup) Match(input string) []uint32 { + var offset uint32 for _, m := range g.Matchers { if res := m.Match(input); len(res) > 0 { - return res + if offset == 0 { + return res + } + shifted := make([]uint32, len(res)) + for i, id := range res { + shifted[i] = id + offset + } + return shifted } + offset += m.Size() } return nil } + +func (g *IndexMatcherGroup) Size() uint32 { + var count uint32 + for _, m := range g.Matchers { + count += m.Size() + } + return count +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index 6813c41cfcf3..39a1f76365b6 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -6,6 +6,7 @@ import ( "encoding/json" "os" "path/filepath" + "sort" "strings" "github.com/xtls/xray-core/app/dispatcher" @@ -615,27 +616,41 @@ func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { var geosite []*router.GeoSite deps := make(map[string][]string) uniqueGeosites := make(map[string]bool) + uniqueTags := make(map[string]bool) matcherFilePath := platform.GetAssetLocation("matcher.cache") if customMatcherFilePath != nil { matcherFilePath = *customMatcherFilePath } + processGeosite := func(dStr string) bool { + prefix := "" + if strings.HasPrefix(dStr, "geosite:") { + prefix = "geosite:" + } else if strings.HasPrefix(dStr, "ext-domain:") { + prefix = "ext-domain:" + } + if prefix == "" { + return false + } + key := strings.ToLower(dStr) + country := strings.ToUpper(dStr[len(prefix):]) + if !uniqueGeosites[country] { + ds, err := loadGeositeWithAttr("geosite.dat", country) + if err == nil { + uniqueGeosites[country] = true + geosite = append(geosite, &router.GeoSite{CountryCode: key, Domain: ds}) + } + } + return true + } + processDomains := func(tag string, rawDomains []string) { var manualDomains []*router.Domain var dDeps []string for _, dStr := range rawDomains { - if strings.HasPrefix(dStr, "geosite:") { - country := strings.ToUpper(dStr[8:]) - depKey := dStr - if !uniqueGeosites[country] { - ds, err := loadGeositeWithAttr("geosite.dat", country) - if err == nil { - uniqueGeosites[country] = true - geosite = append(geosite, &router.GeoSite{CountryCode: depKey, Domain: ds}) - } - } - dDeps = append(dDeps, depKey) + if processGeosite(dStr) { + dDeps = append(dDeps, strings.ToLower(dStr)) } else { ds, err := parseDomainRule(dStr) if err == nil { @@ -644,10 +659,13 @@ func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { } } if len(manualDomains) > 0 { - geosite = append(geosite, &router.GeoSite{CountryCode: tag, Domain: manualDomains}) + if !uniqueTags[tag] { + uniqueTags[tag] = true + geosite = append(geosite, &router.GeoSite{CountryCode: tag, Domain: manualDomains}) + } } if len(dDeps) > 0 { - deps[tag] = dDeps + deps[tag] = append(deps[tag], dDeps...) } } @@ -685,6 +703,77 @@ func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { } } + var hostIPs map[string][]string + if c.DNSConfig != nil && c.DNSConfig.Hosts != nil { + hostIPs = make(map[string][]string) + var hostDeps []string + var hostPatterns []string + + // use raw map to avoid expanding geosites + var domains []string + for domain := range c.DNSConfig.Hosts.Hosts { + domains = append(domains, domain) + } + sort.Strings(domains) + + manualHostGroups := make(map[string][]*router.Domain) + manualHostIPs := make(map[string][]string) + manualHostNames := make(map[string]string) + + for _, domain := range domains { + ha := c.DNSConfig.Hosts.Hosts[domain] + m := getHostMapping(ha) + + var ips []string + if m.ProxiedDomain != "" { + ips = append(ips, m.ProxiedDomain) + } else { + for _, ip := range m.Ip { + ips = append(ips, net.IPAddress(ip).String()) + } + } + + if processGeosite(domain) { + tag := strings.ToLower(domain) + hostDeps = append(hostDeps, tag) + hostIPs[tag] = ips + hostPatterns = append(hostPatterns, domain) + } else { + // build manual domains by their destination IPs + sort.Strings(ips) + ipKey := strings.Join(ips, ",") + ds, err := parseDomainRule(domain) + if err == nil { + manualHostGroups[ipKey] = append(manualHostGroups[ipKey], ds...) + manualHostIPs[ipKey] = ips + if _, ok := manualHostNames[ipKey]; !ok { + manualHostNames[ipKey] = domain + } + } + } + } + + // create manual host groups + var ipKeys []string + for k := range manualHostGroups { + ipKeys = append(ipKeys, k) + } + sort.Strings(ipKeys) + + for _, k := range ipKeys { + tag := manualHostNames[k] + geosite = append(geosite, &router.GeoSite{CountryCode: tag, Domain: manualHostGroups[k]}) + hostDeps = append(hostDeps, tag) + hostIPs[tag] = manualHostIPs[k] + + // record tag _ORDER links the matcher to IP addresses + hostPatterns = append(hostPatterns, tag) + } + + deps["HOSTS"] = hostDeps + hostIPs["_ORDER"] = hostPatterns + } + f, err := os.Create(matcherFilePath) if err != nil { return err @@ -693,7 +782,7 @@ func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { var buf bytes.Buffer - if err := router.SerializeGeoSiteList(geosite, deps, &buf); err != nil { + if err := router.SerializeGeoSiteList(geosite, deps, hostIPs, &buf); err != nil { return err } From 9347f95c39b7d2f6fa22ac55262db9a0b65bc87e Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sat, 31 Jan 2026 14:49:48 +0330 Subject: [PATCH 17/20] remove DomainMatcherPath from config --- app/router/config.go | 4 ++-- app/router/config.pb.go | 40 +++++++++++++++------------------------- app/router/config.proto | 2 -- infra/conf/router.go | 10 ---------- 4 files changed, 17 insertions(+), 39 deletions(-) diff --git a/app/router/config.go b/app/router/config.go index 64f571076432..4288f2af302e 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -113,7 +113,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) if domainMatcherPath != "" { - matcher, err = GetDomainMathcerWithRuleTag(domainMatcherPath, rr.RuleTag) + matcher, err = GetDomainMatcherWithRuleTag(domainMatcherPath, rr.RuleTag) if err != nil { return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err) } @@ -189,7 +189,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch } } -func GetDomainMathcerWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) { +func GetDomainMatcherWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) { f, err := filesystem.NewFileReader(domainMatcherPath) if err != nil { return nil, errors.New("failed to load file: ", domainMatcherPath).Base(err) diff --git a/app/router/config.pb.go b/app/router/config.pb.go index d95baa294a5d..ff5838a8eff6 100644 --- a/app/router/config.pb.go +++ b/app/router/config.pb.go @@ -892,10 +892,9 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"` - Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"` - BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"` - DomainMatcherPath string `protobuf:"bytes,4,opt,name=domainMatcherPath,proto3" json:"domainMatcherPath,omitempty"` + DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"` + Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"` + BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"` } func (x *Config) Reset() { @@ -949,13 +948,6 @@ func (x *Config) GetBalancingRule() []*BalancingRule { return nil } -func (x *Config) GetDomainMatcherPath() string { - if x != nil { - return x.DomainMatcherPath - } - return "" -} - type Domain_Attribute struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1185,7 +1177,7 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x52, 0x54, 0x54, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6c, 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x74, 0x6f, 0x6c, - 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0xbe, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x65, 0x72, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x0f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, @@ -1198,19 +1190,17 @@ var file_app_router_config_proto_rawDesc = []byte{ 0x67, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x62, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x61, 0x74, 0x68, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x61, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x41, - 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, 0x6f, 0x6e, - 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, 0x6e, 0x44, - 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, - 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x01, - 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, - 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, - 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x61, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x22, 0x3c, 0x0a, 0x0e, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x08, 0x0a, + 0x04, 0x41, 0x73, 0x49, 0x73, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x70, 0x49, 0x66, 0x4e, + 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x49, 0x70, 0x4f, + 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x10, 0x03, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, + 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x50, 0x01, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, + 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, + 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, + 0x41, 0x70, 0x70, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/app/router/config.proto b/app/router/config.proto index f14180834bac..20da23ba6d7b 100644 --- a/app/router/config.proto +++ b/app/router/config.proto @@ -160,6 +160,4 @@ message Config { DomainStrategy domain_strategy = 1; repeated RoutingRule rule = 2; repeated BalancingRule balancing_rule = 3; - - string domainMatcherPath = 4; } diff --git a/infra/conf/router.go b/infra/conf/router.go index 1960b7ddb566..bc3246108855 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -78,8 +78,6 @@ type RouterConfig struct { RuleList []json.RawMessage `json:"rules"` DomainStrategy *string `json:"domainStrategy"` Balancers []*BalancingRule `json:"balancers"` - - DomainMatcherPath *string `json:"domainMatcherPath"` } func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy { @@ -122,14 +120,6 @@ func (c *RouterConfig) Build() (*router.Config, error) { } config.BalancingRule = append(config.BalancingRule, balancer) } - - if c.DomainMatcherPath != nil { - path := *c.DomainMatcherPath - if val := strings.Split(path, "assets:"); len(val) == 2 { - path = platform.GetAssetLocation(val[1]) - } - config.DomainMatcherPath = path - } return config, nil } From 6771e1dea44fc1d35a602248d306bf04d4ac63df Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sat, 31 Jan 2026 15:25:15 +0330 Subject: [PATCH 18/20] add cmd BuildCache --- main/commands/all/buildcache.go | 52 +++++++++++++++++++++++++++++++++ main/commands/all/commands.go | 1 + 2 files changed, 53 insertions(+) create mode 100644 main/commands/all/buildcache.go diff --git a/main/commands/all/buildcache.go b/main/commands/all/buildcache.go new file mode 100644 index 000000000000..eba2f4231c81 --- /dev/null +++ b/main/commands/all/buildcache.go @@ -0,0 +1,52 @@ +package all + +import ( + "os" + + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/infra/conf/serial" + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdBuildCache = &base.Command{ + UsageLine: `{{.Exec}} buildCache [-c config.json] [-o domain.cache]`, + Short: `Build domain matcher cache`, + Long: ` +Build domain matcher cache from a configuration file. + +Example: {{.Exec}} buildCache -c config.json -o domain.cache +`, +} + +func init() { + cmdBuildCache.Run = executeBuildCache +} + +var ( + configPath = cmdBuildCache.Flag.String("c", "config.json", "Config file path") + outputPath = cmdBuildCache.Flag.String("o", "domain.cache", "Output cache file path") +) + +func executeBuildCache(cmd *base.Command, args []string) { + cf, err := os.Open(*configPath) + if err != nil { + base.Fatalf("failed to open config file: %v", err) + } + defer cf.Close() + + // prevent using existing cache + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + if domainMatcherPath != "" { + os.Setenv("XRAY_MPH_PATH", "") + defer os.Setenv("XRAY_MPH_PATH", domainMatcherPath) + } + + config, err := serial.DecodeJSONConfig(cf) + if err != nil { + base.Fatalf("failed to decode config file: %v", err) + } + + if err := config.BuildMPHCache(outputPath); err != nil { + base.Fatalf("failed to build MPH cache: %v", err) + } +} diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go index fba3a4b8bb43..b97cae604f09 100644 --- a/main/commands/all/commands.go +++ b/main/commands/all/commands.go @@ -19,5 +19,6 @@ func init() { cmdMLDSA65, cmdMLKEM768, cmdVLESSEnc, + cmdBuildCache, ) } From bc79b2ef67451e50df0cd767c529a0fdef82d751 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sat, 31 Jan 2026 15:42:47 +0330 Subject: [PATCH 19/20] rename env --- common/platform/platform.go | 2 +- main/commands/all/buildcache.go | 18 +++++++++--------- main/commands/all/commands.go | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/platform/platform.go b/common/platform/platform.go index 8f59b255dd34..6446873be7a9 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -25,7 +25,7 @@ const ( TunFdKey = "xray.tun.fd" - MphCachePath = "xray.mph.path" + MphCachePath = "xray.mph.cache" ) type EnvFlag struct { diff --git a/main/commands/all/buildcache.go b/main/commands/all/buildcache.go index eba2f4231c81..6c45205ec663 100644 --- a/main/commands/all/buildcache.go +++ b/main/commands/all/buildcache.go @@ -8,26 +8,26 @@ import ( "github.com/xtls/xray-core/main/commands/base" ) -var cmdBuildCache = &base.Command{ - UsageLine: `{{.Exec}} buildCache [-c config.json] [-o domain.cache]`, +var cmdBuildMphCache = &base.Command{ + UsageLine: `{{.Exec}} buildMphCache [-c config.json] [-o domain.cache]`, Short: `Build domain matcher cache`, Long: ` Build domain matcher cache from a configuration file. -Example: {{.Exec}} buildCache -c config.json -o domain.cache +Example: {{.Exec}} buildMphCache -c config.json -o domain.cache `, } func init() { - cmdBuildCache.Run = executeBuildCache + cmdBuildMphCache.Run = executeBuildMphCache } var ( - configPath = cmdBuildCache.Flag.String("c", "config.json", "Config file path") - outputPath = cmdBuildCache.Flag.String("o", "domain.cache", "Output cache file path") + configPath = cmdBuildMphCache.Flag.String("c", "config.json", "Config file path") + outputPath = cmdBuildMphCache.Flag.String("o", "domain.cache", "Output cache file path") ) -func executeBuildCache(cmd *base.Command, args []string) { +func executeBuildMphCache(cmd *base.Command, args []string) { cf, err := os.Open(*configPath) if err != nil { base.Fatalf("failed to open config file: %v", err) @@ -37,8 +37,8 @@ func executeBuildCache(cmd *base.Command, args []string) { // prevent using existing cache domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) if domainMatcherPath != "" { - os.Setenv("XRAY_MPH_PATH", "") - defer os.Setenv("XRAY_MPH_PATH", domainMatcherPath) + os.Setenv("XRAY_MPH_CACHE", "") + defer os.Setenv("XRAY_MPH_CACHE", domainMatcherPath) } config, err := serial.DecodeJSONConfig(cf) diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go index b97cae604f09..20b92bb01b97 100644 --- a/main/commands/all/commands.go +++ b/main/commands/all/commands.go @@ -19,6 +19,6 @@ func init() { cmdMLDSA65, cmdMLKEM768, cmdVLESSEnc, - cmdBuildCache, + cmdBuildMphCache, ) } From 9e3939d281fa6de85dc967e8fd4f33dbf91d54b5 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sat, 31 Jan 2026 15:51:27 +0330 Subject: [PATCH 20/20] rename file --- main/commands/all/{buildcache.go => buildmphcache.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename main/commands/all/{buildcache.go => buildmphcache.go} (100%) diff --git a/main/commands/all/buildcache.go b/main/commands/all/buildmphcache.go similarity index 100% rename from main/commands/all/buildcache.go rename to main/commands/all/buildmphcache.go