From 3723e03712ab0ba88ffac96194efbf5d85793537 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 14:54:47 +0330 Subject: [PATCH 01/12] implement lightwight routing --- app/router/condition_builder.go | 95 +++++++ app/router/condition_builder_other.go | 153 ++++++++++ app/router/config.go | 86 ------ .../platform/filesystem/assets/asset_tools.go | 52 ++++ common/platform/filesystem/assets/assets.go | 261 ++++++++++++++++++ infra/conf/router.go | 114 -------- infra/conf/router_assets.go | 74 +++++ infra/conf/router_assets_other.go | 49 ++++ 8 files changed, 684 insertions(+), 200 deletions(-) create mode 100644 app/router/condition_builder.go create mode 100644 app/router/condition_builder_other.go create mode 100644 common/platform/filesystem/assets/asset_tools.go create mode 100644 common/platform/filesystem/assets/assets.go create mode 100644 infra/conf/router_assets.go create mode 100644 infra/conf/router_assets_other.go diff --git a/app/router/condition_builder.go b/app/router/condition_builder.go new file mode 100644 index 000000000000..f66c20bf24c7 --- /dev/null +++ b/app/router/condition_builder.go @@ -0,0 +1,95 @@ +//go:build windows || wasm + +package router + +import ( + "context" + "regexp" + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +func (rr *RoutingRule) BuildCondition() (Condition, error) { + conds := NewConditionChan() + + if len(rr.InboundTag) > 0 { + conds.Add(NewInboundTagMatcher(rr.InboundTag)) + } + + if len(rr.Networks) > 0 { + conds.Add(NewNetworkMatcher(rr.Networks)) + } + + if len(rr.Protocol) > 0 { + conds.Add(NewProtocolMatcher(rr.Protocol)) + } + + if rr.PortList != nil { + conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) + } + + if rr.SourcePortList != nil { + conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) + } + + if rr.LocalPortList != nil { + conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) + } + + if rr.VlessRouteList != nil { + conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) + } + + if len(rr.UserEmail) > 0 { + conds.Add(NewUserMatcher(rr.UserEmail)) + } + + if len(rr.Attributes) > 0 { + configuredKeys := make(map[string]*regexp.Regexp) + for key, value := range rr.Attributes { + configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) + } + conds.Add(&AttributeMatcher{configuredKeys}) + } + + if len(rr.Geoip) > 0 { + cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) + if err != nil { + return nil, err + } + conds.Add(cond) + } + + if len(rr.SourceGeoip) > 0 { + cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) + if err != nil { + return nil, err + } + conds.Add(cond) + } + + if len(rr.LocalGeoip) > 0 { + cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) + if err != nil { + return nil, err + } + conds.Add(cond) + errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") + } + + 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) + } + errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") + conds.Add(matcher) + } + + if conds.Len() == 0 { + return nil, errors.New("this rule has no effective fields").AtWarning() + } + + return conds, nil +} diff --git a/app/router/condition_builder_other.go b/app/router/condition_builder_other.go new file mode 100644 index 000000000000..8605ecf786dc --- /dev/null +++ b/app/router/condition_builder_other.go @@ -0,0 +1,153 @@ +//go:build !windows && !wasm + +package router + +import ( + "context" + "regexp" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem/assets" + "google.golang.org/protobuf/proto" +) + +func (rr *RoutingRule) BuildCondition() (Condition, error) { + conds := NewConditionChan() + + if len(rr.InboundTag) > 0 { + conds.Add(NewInboundTagMatcher(rr.InboundTag)) + } + + if len(rr.Networks) > 0 { + conds.Add(NewNetworkMatcher(rr.Networks)) + } + + if len(rr.Protocol) > 0 { + conds.Add(NewProtocolMatcher(rr.Protocol)) + } + + if rr.PortList != nil { + conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) + } + + if rr.SourcePortList != nil { + conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) + } + + if rr.LocalPortList != nil { + conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) + } + + if rr.VlessRouteList != nil { + conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) + } + + if len(rr.UserEmail) > 0 { + conds.Add(NewUserMatcher(rr.UserEmail)) + } + + if len(rr.Attributes) > 0 { + configuredKeys := make(map[string]*regexp.Regexp) + for key, value := range rr.Attributes { + configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) + } + conds.Add(&AttributeMatcher{configuredKeys}) + } + + if len(rr.Geoip) > 0 { + geoip, err := getGeoIPList(rr.Geoip) + if err != nil { + return nil, errors.New("failed to build geoip from mmap").Base(err) + } + + cond, err := NewIPMatcher(geoip, MatcherAsType_Target) + if err != nil { + return nil, err + } + conds.Add(cond) + } + + if len(rr.SourceGeoip) > 0 { + cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) + if err != nil { + return nil, err + } + conds.Add(cond) + } + + if len(rr.LocalGeoip) > 0 { + cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) + if err != nil { + return nil, err + } + conds.Add(cond) + errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") + } + + if len(rr.Domain) > 0 { + domains, err := getDomainList(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domains from mmap").Base(err) + } + + matcher, err := NewMphMatcherGroup(domains) + 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) + } + + if conds.Len() == 0 { + return nil, errors.New("this rule has no effective fields").AtWarning() + } + + return conds, nil +} + +func getGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { + geoipList := []*GeoIP{} + for _, ip := range ips { + if ip.CountryCode != "" { + geoMeta := assets.GeoIP.GetGeoMeta(ip.CountryCode) + if geoMeta != nil && geoMeta.Length != 0 { + bs := assets.GeoIP.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) + 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 { + if code, ok := strings.CutPrefix(domain.Value, "geosite:"); ok { + + geoMeta := assets.GeoSite.GetGeoMeta(code) + if geoMeta != nil && geoMeta.Length != 0 { + bs := assets.GeoSite.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) + var geosite GeoSite + + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) + } + domainList = append(domainList, geosite.Domain...) + + } + } else { + domainList = append(domainList, domain) + } + } + return domainList, nil +} diff --git a/app/router/config.go b/app/router/config.go index 9f6a4844408e..94f73d1050ea 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -1,8 +1,6 @@ package router import ( - "context" - "regexp" "strings" "github.com/xtls/xray-core/common/errors" @@ -29,90 +27,6 @@ func (r *Rule) Apply(ctx routing.Context) bool { return r.Condition.Apply(ctx) } -func (rr *RoutingRule) BuildCondition() (Condition, error) { - conds := NewConditionChan() - - if len(rr.InboundTag) > 0 { - conds.Add(NewInboundTagMatcher(rr.InboundTag)) - } - - if len(rr.Networks) > 0 { - conds.Add(NewNetworkMatcher(rr.Networks)) - } - - if len(rr.Protocol) > 0 { - conds.Add(NewProtocolMatcher(rr.Protocol)) - } - - if rr.PortList != nil { - conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) - } - - if rr.SourcePortList != nil { - conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) - } - - if rr.LocalPortList != nil { - conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) - } - - if rr.VlessRouteList != nil { - conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) - } - - if len(rr.UserEmail) > 0 { - conds.Add(NewUserMatcher(rr.UserEmail)) - } - - if len(rr.Attributes) > 0 { - configuredKeys := make(map[string]*regexp.Regexp) - for key, value := range rr.Attributes { - configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) - } - conds.Add(&AttributeMatcher{configuredKeys}) - } - - if len(rr.Geoip) > 0 { - cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) - if err != nil { - return nil, err - } - conds.Add(cond) - } - - if len(rr.SourceGeoip) > 0 { - cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) - if err != nil { - return nil, err - } - conds.Add(cond) - } - - if len(rr.LocalGeoip) > 0 { - cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) - if err != nil { - return nil, err - } - conds.Add(cond) - errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") - } - - 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) - } - errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") - conds.Add(matcher) - } - - if conds.Len() == 0 { - return nil, errors.New("this rule has no effective fields").AtWarning() - } - - return conds, nil -} - // Build builds the balancing rule func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatcher) (*Balancer, error) { switch strings.ToLower(br.Strategy) { diff --git a/common/platform/filesystem/assets/asset_tools.go b/common/platform/filesystem/assets/asset_tools.go new file mode 100644 index 000000000000..c6c9e02473d5 --- /dev/null +++ b/common/platform/filesystem/assets/asset_tools.go @@ -0,0 +1,52 @@ +package assets + +func DecodeVarint(buf []byte) (x uint64, n int) { + for shift := uint(0); shift < 64; shift += 7 { + if n >= len(buf) { + return 0, 0 + } + b := uint64(buf[n]) + n++ + x |= (b & 0x7F) << shift + if (b & 0x80) == 0 { + return x, n + } + } + + // The number is too large to represent in a 64-bit value. + return 0, 0 +} + +func Find(data, code []byte) []byte { + codeL := len(code) + if codeL == 0 { + return nil + } + for { + dataL := len(data) + if dataL < 2 { + return nil + } + x, y := DecodeVarint(data[1:]) + if x == 0 && y == 0 { + return nil + } + headL, bodyL := 1+y, int(x) + dataL -= headL + if dataL < bodyL { + return nil + } + data = data[headL:] + if int(data[1]) == codeL { + for i := 0; i < codeL && data[2+i] == code[i]; i++ { + if i+1 == codeL { + return data[:bodyL] + } + } + } + if dataL == bodyL { + return nil + } + data = data[bodyL:] + } +} diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go new file mode 100644 index 000000000000..b4ec1d0bd552 --- /dev/null +++ b/common/platform/filesystem/assets/assets.go @@ -0,0 +1,261 @@ +//go:build !windows && !wasm + +package assets + +import ( + "bufio" + "fmt" + "os" + "runtime" + "runtime/debug" + "strconv" + "strings" + "sync" + + "github.com/xtls/xray-core/common/platform" + "golang.org/x/sys/unix" +) + +type GeoMeta struct { + Code string + Start int64 + Length int64 +} + +type MMapFile struct { + mu sync.RWMutex + path string + data []byte + size int + + GeoMeta []GeoMeta +} + +var GeoSite *MMapFile +var GeoIP *MMapFile + +func ReadGeoIP(file string) ([]byte, error) { + if GeoIP == nil { + GeoIP = &MMapFile{GeoMeta: []GeoMeta{}} + if err := GeoIP.Open(platform.GetAssetLocation(file)); err != nil { + return nil, err + } + } + return GeoIP.Bytes(), nil +} + +func ReadGeoSite(file string) ([]byte, error) { + if GeoSite == nil { + GeoSite = &MMapFile{GeoMeta: []GeoMeta{}} + if err := GeoSite.Open(platform.GetAssetLocation(file)); err != nil { + return nil, err + } + } + + return GeoSite.Bytes(), nil +} + +// Open maps the file readonly +func (m *MMapFile) Open(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + st, err := f.Stat() + if err != nil { + return err + } + + size64 := st.Size() + if size64 <= 0 { + m.mu.Lock() + _ = m.unmapLocked() + m.path, m.data, m.size = path, nil, 0 + m.mu.Unlock() + return nil + } + if size64 > int64(^uint(0)>>1) { + return fmt.Errorf("file too large: %d", size64) + } + size := int(size64) + + b, err := unix.Mmap(int(f.Fd()), 0, size, unix.PROT_READ, unix.MAP_SHARED) + if err != nil { + return err + } + + _ = unix.Madvise(b, unix.MADV_RANDOM) + + m.mu.Lock() + _ = m.unmapLocked() + m.path = path + m.data = b + m.size = size + m.mu.Unlock() + + m.BuildGeoMetaList() + + return nil +} + +func (m *MMapFile) BuildGeoMetaList() { + if platform.IsAssetMapEnabled() { + m.BuildGeoMetaFromMapFile() + } else { + debug.SetGCPercent(10) + debug.SetMemoryLimit(10) + defer func() { + debug.SetGCPercent(100) + debug.SetMemoryLimit(-1) + }() + m.BuildGeoMetaFromMemmory(func(code []byte, start, length int64) error { + m.AddGeoMeta(string(code), int(start), int(length)) + runtime.GC() + + return nil + }) + + } +} + +func (m *MMapFile) Close() error { + m.mu.Lock() + defer m.mu.Unlock() + return m.unmapLocked() +} + +func (m *MMapFile) Bytes() []byte { + m.mu.RLock() + defer m.mu.RUnlock() + return m.data +} + +func (m *MMapFile) Slice(start, end int64) []byte { + m.mu.RLock() + defer m.mu.RUnlock() + if start < 0 || end < start || int(end) > len(m.data) { + return nil + } + return m.data[start:end] +} + +func (m *MMapFile) AddGeoMeta(code string, start, length int) { + m.mu.RLock() + defer m.mu.RUnlock() + for _, info := range m.GeoMeta { + if info.Code == code { + return + } + } + m.GeoMeta = append(m.GeoMeta, GeoMeta{Code: code, Start: int64(start), Length: int64(length)}) +} + +func (m *MMapFile) GetGeoMeta(code string) *GeoMeta { + m.mu.RLock() + defer m.mu.RUnlock() + for _, info := range m.GeoMeta { + if info.Code == code { + return &info + } + } + return nil +} + +func (m *MMapFile) unmapLocked() error { + if m.data == nil { + return nil + } + err := unix.Munmap(m.data) + m.data = nil + m.size = 0 + return err +} + +func (m *MMapFile) BuildGeoMetaFromMemmory(onEntry func(code []byte, start, length int64) error) error { + data := m.Bytes() + baseLen := len(data) + var off int64 = 0 + + for { + if len(data) < 2 { + return nil + } + + x, n := DecodeVarint(data[1:]) + if x == 0 && n == 0 { + return nil + } + + headL := 1 + n + + if x > uint64(len(data)-headL) { + return fmt.Errorf("corrupt data: bodyLen=%d exceeds remaining=%d", x, len(data)-headL) + } + bodyL := int(x) + + body := data[headL : headL+bodyL] + start := off + int64(headL) + length := int64(bodyL) + + if len(body) >= 2 { + codeL := int(body[1]) + if codeL > 0 && len(body) >= 2+codeL { + code := body[2 : 2+codeL] + + if err := onEntry(code, start, length); err != nil { + return err + } + } + } + + step := headL + bodyL + data = data[step:] + off += int64(step) + + if off > int64(baseLen) { + return fmt.Errorf("corrupt scan: offset overflow") + } + } +} + +func (m *MMapFile) BuildGeoMetaFromMapFile() error { + f, err := os.Open(m.path + ".map") + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + parts := strings.Split(line, ",") + if len(parts) != 3 { + return fmt.Errorf("invalid line: %q", line) + } + + start, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid start byte in %q", line) + } + + length, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + return fmt.Errorf("invalid length in %q", line) + } + m.AddGeoMeta(parts[0], int(start), int(length)) + + } + + if err := scanner.Err(); err != nil { + return err + } + + return nil +} diff --git a/infra/conf/router.go b/infra/conf/router.go index 6e09038bc364..071aeec55092 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -2,14 +2,12 @@ package conf import ( "encoding/json" - "runtime" "strconv" "strings" "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/filesystem" "github.com/xtls/xray-core/common/serial" "google.golang.org/protobuf/proto" ) @@ -183,118 +181,6 @@ var ( SiteCache = make(map[string]*router.GeoSite) ) -func loadFile(file string) ([]byte, error) { - if FileCache[file] == nil { - bs, err := filesystem.ReadAsset(file) - if err != nil { - return nil, errors.New("failed to open file: ", file).Base(err) - } - if len(bs) == 0 { - return nil, errors.New("empty file: ", file) - } - // Do not cache file, may save RAM when there - // are many files, but consume CPU each time. - return bs, nil - FileCache[file] = bs - } - return FileCache[file], nil -} - -func loadIP(file, code string) ([]*router.CIDR, error) { - index := file + ":" + code - if IPCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("code not found in ", file, ": ", code) - } - var geoip router.GeoIP - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) - } - defer runtime.GC() // or debug.FreeOSMemory() - return geoip.Cidr, nil // do not cache geoip - IPCache[index] = &geoip - } - return IPCache[index].Cidr, nil -} - -func loadSite(file, code string) ([]*router.Domain, error) { - index := file + ":" + code - if SiteCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs = find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("list not found in ", file, ": ", code) - } - var geosite router.GeoSite - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) - } - defer runtime.GC() // or debug.FreeOSMemory() - return geosite.Domain, nil // do not cache geosite - SiteCache[index] = &geosite - } - return SiteCache[index].Domain, nil -} - -func DecodeVarint(buf []byte) (x uint64, n int) { - for shift := uint(0); shift < 64; shift += 7 { - if n >= len(buf) { - return 0, 0 - } - b := uint64(buf[n]) - n++ - x |= (b & 0x7F) << shift - if (b & 0x80) == 0 { - return x, n - } - } - - // The number is too large to represent in a 64-bit value. - return 0, 0 -} - -func find(data, code []byte) []byte { - codeL := len(code) - if codeL == 0 { - return nil - } - for { - dataL := len(data) - if dataL < 2 { - return nil - } - x, y := DecodeVarint(data[1:]) - if x == 0 && y == 0 { - return nil - } - headL, bodyL := 1+y, int(x) - dataL -= headL - if dataL < bodyL { - return nil - } - data = data[headL:] - if int(data[1]) == codeL { - for i := 0; i < codeL && data[2+i] == code[i]; i++ { - if i+1 == codeL { - return data[:bodyL] - } - } - } - if dataL == bodyL { - return nil - } - data = data[bodyL:] - } -} - type AttributeMatcher interface { Match(*router.Domain) bool } diff --git a/infra/conf/router_assets.go b/infra/conf/router_assets.go new file mode 100644 index 000000000000..a3a6f9750d0a --- /dev/null +++ b/infra/conf/router_assets.go @@ -0,0 +1,74 @@ +//go:build windows || wasm + +package conf + +import ( + "runtime" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/platform/filesystem/assets" + "google.golang.org/protobuf/proto" +) + +func loadFile(file string) ([]byte, error) { + if FileCache[file] == nil { + bs, err := filesystem.ReadAsset(file) + if err != nil { + return nil, errors.New("failed to open file: ", file).Base(err) + } + if len(bs) == 0 { + return nil, errors.New("empty file: ", file) + } + // Do not cache file, may save RAM when there + // are many files, but consume CPU each time. + return bs, nil + FileCache[file] = bs + } + return FileCache[file], nil +} + +func loadIP(file, code string) ([]*router.CIDR, error) { + index := file + ":" + code + if IPCache[index] == nil { + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = assets.Find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("code not found in ", file, ": ", code) + } + var geoip router.GeoIP + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) + } + defer runtime.GC() // or debug.FreeOSMemory() + return geoip.Cidr, nil // do not cache geoip + IPCache[index] = &geoip + } + return IPCache[index].Cidr, nil +} + +func loadSite(file, code string) ([]*router.Domain, error) { + index := file + ":" + code + if SiteCache[index] == nil { + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = assets.Find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("list not found in ", file, ": ", code) + } + var geosite router.GeoSite + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) + } + defer runtime.GC() // or debug.FreeOSMemory() + return geosite.Domain, nil // do not cache geosite + SiteCache[index] = &geosite + } + return SiteCache[index].Domain, nil +} diff --git a/infra/conf/router_assets_other.go b/infra/conf/router_assets_other.go new file mode 100644 index 000000000000..4f9a88e3eaa8 --- /dev/null +++ b/infra/conf/router_assets_other.go @@ -0,0 +1,49 @@ +//go:build !windows && !wasm + +package conf + +import ( + "runtime" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem/assets" +) + +func loadIP(file, code string) ([]*router.CIDR, error) { + index := file + ":" + code + if IPCache[index] == nil { + _, err := assets.ReadGeoIP(file) + + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + var geoip router.GeoIP + + // dont pass code becuase we have country code in top level router.GeoIP + geoip = router.GeoIP{Cidr: []*router.CIDR{}} + defer runtime.GC() // or debug.FreeOSMemory() + return geoip.Cidr, nil // do not cache geoip + IPCache[index] = &geoip + } + return IPCache[index].Cidr, nil +} + +func loadSite(file, code string) ([]*router.Domain, error) { + index := file + ":" + code + if SiteCache[index] == nil { + var geosite router.GeoSite + _, err := assets.ReadGeoSite(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + + // pass file:code so can build optimized matcher later + domain := router.Domain{Value: "geosite:" + code} + geosite = router.GeoSite{Domain: []*router.Domain{&domain}} + defer runtime.GC() // or debug.FreeOSMemory() + return geosite.Domain, nil // do not cache geosite + SiteCache[index] = &geosite + } + return SiteCache[index].Domain, nil +} From f6593662f4441afbedec6513ebb98ceb4f48967c Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 15:18:36 +0330 Subject: [PATCH 02/12] build for ios and darwin --- app/router/condition_builder.go | 2 +- app/router/condition_builder_other.go | 2 +- common/platform/filesystem/assets/assets.go | 49 +++++++++++---------- common/platform/others.go | 7 +++ common/platform/platform.go | 5 ++- infra/conf/router_assets.go | 2 +- infra/conf/router_assets_other.go | 2 +- 7 files changed, 40 insertions(+), 29 deletions(-) diff --git a/app/router/condition_builder.go b/app/router/condition_builder.go index f66c20bf24c7..1d3100f02cc1 100644 --- a/app/router/condition_builder.go +++ b/app/router/condition_builder.go @@ -1,4 +1,4 @@ -//go:build windows || wasm +//go:build !ios && !darwin package router diff --git a/app/router/condition_builder_other.go b/app/router/condition_builder_other.go index 8605ecf786dc..f4722e354565 100644 --- a/app/router/condition_builder_other.go +++ b/app/router/condition_builder_other.go @@ -1,4 +1,4 @@ -//go:build !windows && !wasm +//go:build ios || darwin package router diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go index b4ec1d0bd552..db783213c5f8 100644 --- a/common/platform/filesystem/assets/assets.go +++ b/common/platform/filesystem/assets/assets.go @@ -1,4 +1,4 @@ -//go:build !windows && !wasm +//go:build ios || darwin package assets @@ -95,31 +95,11 @@ func (m *MMapFile) Open(path string) error { m.size = size m.mu.Unlock() - m.BuildGeoMetaList() + m.buildGeoMetaList() return nil } -func (m *MMapFile) BuildGeoMetaList() { - if platform.IsAssetMapEnabled() { - m.BuildGeoMetaFromMapFile() - } else { - debug.SetGCPercent(10) - debug.SetMemoryLimit(10) - defer func() { - debug.SetGCPercent(100) - debug.SetMemoryLimit(-1) - }() - m.BuildGeoMetaFromMemmory(func(code []byte, start, length int64) error { - m.AddGeoMeta(string(code), int(start), int(length)) - runtime.GC() - - return nil - }) - - } -} - func (m *MMapFile) Close() error { m.mu.Lock() defer m.mu.Unlock() @@ -173,7 +153,27 @@ func (m *MMapFile) unmapLocked() error { return err } -func (m *MMapFile) BuildGeoMetaFromMemmory(onEntry func(code []byte, start, length int64) error) error { +func (m *MMapFile) buildGeoMetaList() { + if platform.IsAssetMapEnabled() { + m.buildGeoMetaFromMappedFile() + } else { + debug.SetGCPercent(10) + debug.SetMemoryLimit(10) + defer func() { + debug.SetGCPercent(100) + debug.SetMemoryLimit(-1) + }() + m.buildGeoMetaFromMemmory(func(code []byte, start, length int64) error { + m.AddGeoMeta(string(code), int(start), int(length)) + runtime.GC() + + return nil + }) + + } +} + +func (m *MMapFile) buildGeoMetaFromMemmory(onEntry func(code []byte, start, length int64) error) error { data := m.Bytes() baseLen := len(data) var off int64 = 0 @@ -220,7 +220,8 @@ func (m *MMapFile) BuildGeoMetaFromMemmory(onEntry func(code []byte, start, leng } } -func (m *MMapFile) BuildGeoMetaFromMapFile() error { +// the mapped file should be along side the geo file, if file is geosite.dat , mapped should be geosite.dat.map +func (m *MMapFile) buildGeoMetaFromMappedFile() error { f, err := os.Open(m.path + ".map") if err != nil { return err diff --git a/common/platform/others.go b/common/platform/others.go index be86b6faaac2..1266344d9cd2 100644 --- a/common/platform/others.go +++ b/common/platform/others.go @@ -34,6 +34,13 @@ func GetAssetLocation(file string) string { return defPath } +// only apply if ios or darwin for now +// IsAssetMapEnabled to load mapped geosite or geoip (file name : geosite.dat.map) +func IsAssetMapEnabled() bool { + isEnabled := NewEnvFlag(AssetMapEnabled).GetValueAsInt(0) + return isEnabled != 0 +} + // GetCertLocation searches for `file` in the env dir and the executable dir func GetCertLocation(file string) string { certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) diff --git a/common/platform/platform.go b/common/platform/platform.go index bc7baf655d20..32c7bc8a8ae7 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -11,7 +11,10 @@ const ( ConfigLocation = "xray.location.config" ConfdirLocation = "xray.location.confdir" AssetLocation = "xray.location.asset" - CertLocation = "xray.location.cert" + // only apply if !windows && !wasm + AssetMapEnabled = "xray.asset.mapped" + + CertLocation = "xray.location.cert" UseReadV = "xray.buf.readv" UseFreedomSplice = "xray.buf.splice" diff --git a/infra/conf/router_assets.go b/infra/conf/router_assets.go index a3a6f9750d0a..1575d2356367 100644 --- a/infra/conf/router_assets.go +++ b/infra/conf/router_assets.go @@ -1,4 +1,4 @@ -//go:build windows || wasm +//go:build !ios && !darwin package conf diff --git a/infra/conf/router_assets_other.go b/infra/conf/router_assets_other.go index 4f9a88e3eaa8..f3c483c44a8b 100644 --- a/infra/conf/router_assets_other.go +++ b/infra/conf/router_assets_other.go @@ -1,4 +1,4 @@ -//go:build !windows && !wasm +//go:build ios || darwin package conf From 6940263d1ab0fb0ca61088baaf563c9cab4754ee Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 15:31:26 +0330 Subject: [PATCH 03/12] improve memory limit --- common/platform/filesystem/assets/assets.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go index db783213c5f8..417d952e133f 100644 --- a/common/platform/filesystem/assets/assets.go +++ b/common/platform/filesystem/assets/assets.go @@ -157,12 +157,12 @@ func (m *MMapFile) buildGeoMetaList() { if platform.IsAssetMapEnabled() { m.buildGeoMetaFromMappedFile() } else { - debug.SetGCPercent(10) - debug.SetMemoryLimit(10) + // set memory limit then unset + debug.SetMemoryLimit(1 << 20) defer func() { - debug.SetGCPercent(100) debug.SetMemoryLimit(-1) }() + m.buildGeoMetaFromMemmory(func(code []byte, start, length int64) error { m.AddGeoMeta(string(code), int(start), int(length)) runtime.GC() From c33fa74655af498e8d991c07829cbc91b0bd80b5 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 16:05:21 +0330 Subject: [PATCH 04/12] apply old memory limit --- common/platform/filesystem/assets/assets.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go index 417d952e133f..775d00d95b7d 100644 --- a/common/platform/filesystem/assets/assets.go +++ b/common/platform/filesystem/assets/assets.go @@ -158,9 +158,10 @@ func (m *MMapFile) buildGeoMetaList() { m.buildGeoMetaFromMappedFile() } else { // set memory limit then unset - debug.SetMemoryLimit(1 << 20) + + oldLimit := debug.SetMemoryLimit(1 << 20) defer func() { - debug.SetMemoryLimit(-1) + debug.SetMemoryLimit(oldLimit) }() m.buildGeoMetaFromMemmory(func(code []byte, start, length int64) error { From cfbf66edcf6e07711568b87db4af151d8fceace4 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 17:04:45 +0330 Subject: [PATCH 05/12] fix typo --- common/platform/filesystem/assets/assets.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go index 775d00d95b7d..2327dffd2ce7 100644 --- a/common/platform/filesystem/assets/assets.go +++ b/common/platform/filesystem/assets/assets.go @@ -164,7 +164,7 @@ func (m *MMapFile) buildGeoMetaList() { debug.SetMemoryLimit(oldLimit) }() - m.buildGeoMetaFromMemmory(func(code []byte, start, length int64) error { + m.buildGeoMetaFromMemory(func(code []byte, start, length int64) error { m.AddGeoMeta(string(code), int(start), int(length)) runtime.GC() @@ -174,7 +174,7 @@ func (m *MMapFile) buildGeoMetaList() { } } -func (m *MMapFile) buildGeoMetaFromMemmory(onEntry func(code []byte, start, length int64) error) error { +func (m *MMapFile) buildGeoMetaFromMemory(onEntry func(code []byte, start, length int64) error) error { data := m.Bytes() baseLen := len(data) var off int64 = 0 From e63537ce75f21a7606b8b3b71891a5cf8268f550 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 17:28:46 +0330 Subject: [PATCH 06/12] remove SetMemoryLimit --- common/platform/filesystem/assets/assets.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go index 2327dffd2ce7..d8e933177dde 100644 --- a/common/platform/filesystem/assets/assets.go +++ b/common/platform/filesystem/assets/assets.go @@ -7,7 +7,6 @@ import ( "fmt" "os" "runtime" - "runtime/debug" "strconv" "strings" "sync" @@ -157,13 +156,6 @@ func (m *MMapFile) buildGeoMetaList() { if platform.IsAssetMapEnabled() { m.buildGeoMetaFromMappedFile() } else { - // set memory limit then unset - - oldLimit := debug.SetMemoryLimit(1 << 20) - defer func() { - debug.SetMemoryLimit(oldLimit) - }() - m.buildGeoMetaFromMemory(func(code []byte, start, length int64) error { m.AddGeoMeta(string(code), int(start), int(length)) runtime.GC() From 28071a1d9a16787e491de819f5839d4dc5efe0c4 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Sun, 4 Jan 2026 18:31:57 +0330 Subject: [PATCH 07/12] improve error handling --- app/router/condition_builder_other.go | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/app/router/condition_builder_other.go b/app/router/condition_builder_other.go index f4722e354565..578ce4866008 100644 --- a/app/router/condition_builder_other.go +++ b/app/router/condition_builder_other.go @@ -111,16 +111,18 @@ func getGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { for _, ip := range ips { if ip.CountryCode != "" { geoMeta := assets.GeoIP.GetGeoMeta(ip.CountryCode) - if geoMeta != nil && geoMeta.Length != 0 { - bs := assets.GeoIP.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) - var geoip GeoIP + if geoMeta == nil || geoMeta.Length == 0 { + return nil, errors.New("geoip not founded: ", ip.CountryCode) + } - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - geoipList = append(geoipList, &geoip) + bs := assets.GeoIP.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) + 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) } @@ -135,16 +137,17 @@ func getDomainList(domains []*Domain) ([]*Domain, error) { if code, ok := strings.CutPrefix(domain.Value, "geosite:"); ok { geoMeta := assets.GeoSite.GetGeoMeta(code) - if geoMeta != nil && geoMeta.Length != 0 { - bs := assets.GeoSite.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) - var geosite GeoSite - - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - domainList = append(domainList, geosite.Domain...) + if geoMeta == nil || geoMeta.Length == 0 { + return nil, errors.New("geosite not founded: ", code) + } + bs := assets.GeoSite.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) + var geosite GeoSite + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) } + domainList = append(domainList, geosite.Domain...) + } else { domainList = append(domainList, domain) } From 32a8b5e5efb4c93c19070f52ec523365310caefa Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Mon, 5 Jan 2026 10:23:49 +0330 Subject: [PATCH 08/12] improve tools --- .../platform/filesystem/assets/asset_tools.go | 51 ++++++++++++++----- common/platform/filesystem/assets/assets.go | 9 ++-- infra/conf/router_assets.go | 4 +- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/common/platform/filesystem/assets/asset_tools.go b/common/platform/filesystem/assets/asset_tools.go index c6c9e02473d5..a3dc709442b9 100644 --- a/common/platform/filesystem/assets/asset_tools.go +++ b/common/platform/filesystem/assets/asset_tools.go @@ -17,36 +17,59 @@ func DecodeVarint(buf []byte) (x uint64, n int) { return 0, 0 } -func Find(data, code []byte) []byte { +func Find(data, code []byte) (found []byte, startByte int) { codeL := len(code) if codeL == 0 { - return nil + return nil, -1 } + + base := data + offset := 0 + for { dataL := len(data) if dataL < 2 { - return nil + return nil, -1 } + x, y := DecodeVarint(data[1:]) if x == 0 && y == 0 { - return nil + return nil, -1 } - headL, bodyL := 1+y, int(x) - dataL -= headL - if dataL < bodyL { - return nil + + headL := 1 + y + bodyL := int(x) + + if dataL < headL+bodyL { + return nil, -1 } + + // Move to body data = data[headL:] + offset += headL + + // Check code match if int(data[1]) == codeL { - for i := 0; i < codeL && data[2+i] == code[i]; i++ { - if i+1 == codeL { - return data[:bodyL] + match := true + for i := 0; i < codeL; i++ { + if data[2+i] != code[i] { + match = false + break } } + if match { + start := offset + end := offset + bodyL + return base[start:end], start + } } - if dataL == bodyL { - return nil - } + + // Advance to next record data = data[bodyL:] + offset += bodyL + + if len(data) == 0 { + return nil, -1 + } } } diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go index d8e933177dde..5a3a04480b68 100644 --- a/common/platform/filesystem/assets/assets.go +++ b/common/platform/filesystem/assets/assets.go @@ -94,9 +94,7 @@ func (m *MMapFile) Open(path string) error { m.size = size m.mu.Unlock() - m.buildGeoMetaList() - - return nil + return m.buildGeoMetaList() } func (m *MMapFile) Close() error { @@ -152,9 +150,9 @@ func (m *MMapFile) unmapLocked() error { return err } -func (m *MMapFile) buildGeoMetaList() { +func (m *MMapFile) buildGeoMetaList() error { if platform.IsAssetMapEnabled() { - m.buildGeoMetaFromMappedFile() + return m.buildGeoMetaFromMappedFile() } else { m.buildGeoMetaFromMemory(func(code []byte, start, length int64) error { m.AddGeoMeta(string(code), int(start), int(length)) @@ -164,6 +162,7 @@ func (m *MMapFile) buildGeoMetaList() { }) } + return nil } func (m *MMapFile) buildGeoMetaFromMemory(onEntry func(code []byte, start, length int64) error) error { diff --git a/infra/conf/router_assets.go b/infra/conf/router_assets.go index 1575d2356367..3df53dd48ad9 100644 --- a/infra/conf/router_assets.go +++ b/infra/conf/router_assets.go @@ -36,7 +36,7 @@ func loadIP(file, code string) ([]*router.CIDR, error) { if err != nil { return nil, errors.New("failed to load file: ", file).Base(err) } - bs = assets.Find(bs, []byte(code)) + bs, _ = assets.Find(bs, []byte(code)) if bs == nil { return nil, errors.New("code not found in ", file, ": ", code) } @@ -58,7 +58,7 @@ func loadSite(file, code string) ([]*router.Domain, error) { if err != nil { return nil, errors.New("failed to load file: ", file).Base(err) } - bs = assets.Find(bs, []byte(code)) + bs, _ = assets.Find(bs, []byte(code)) if bs == nil { return nil, errors.New("list not found in ", file, ": ", code) } From 897e70cdc70f7904033475bc4b71e2b0213d0da5 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Mon, 5 Jan 2026 17:53:06 +0330 Subject: [PATCH 09/12] revert --- app/router/condition_builder.go | 95 ------- app/router/condition_builder_other.go | 156 ----------- app/router/config.go | 166 ++++++++++++ .../platform/filesystem/assets/asset_tools.go | 51 +--- common/platform/filesystem/assets/assets.go | 254 ------------------ common/platform/others.go | 7 - common/platform/platform.go | 5 +- infra/conf/router.go | 80 ++++++ infra/conf/router_assets.go | 74 ----- infra/conf/router_assets_other.go | 49 ---- 10 files changed, 261 insertions(+), 676 deletions(-) delete mode 100644 app/router/condition_builder.go delete mode 100644 app/router/condition_builder_other.go delete mode 100644 common/platform/filesystem/assets/assets.go delete mode 100644 infra/conf/router_assets.go delete mode 100644 infra/conf/router_assets_other.go diff --git a/app/router/condition_builder.go b/app/router/condition_builder.go deleted file mode 100644 index 1d3100f02cc1..000000000000 --- a/app/router/condition_builder.go +++ /dev/null @@ -1,95 +0,0 @@ -//go:build !ios && !darwin - -package router - -import ( - "context" - "regexp" - "strings" - - "github.com/xtls/xray-core/common/errors" -) - -func (rr *RoutingRule) BuildCondition() (Condition, error) { - conds := NewConditionChan() - - if len(rr.InboundTag) > 0 { - conds.Add(NewInboundTagMatcher(rr.InboundTag)) - } - - if len(rr.Networks) > 0 { - conds.Add(NewNetworkMatcher(rr.Networks)) - } - - if len(rr.Protocol) > 0 { - conds.Add(NewProtocolMatcher(rr.Protocol)) - } - - if rr.PortList != nil { - conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) - } - - if rr.SourcePortList != nil { - conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) - } - - if rr.LocalPortList != nil { - conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) - } - - if rr.VlessRouteList != nil { - conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) - } - - if len(rr.UserEmail) > 0 { - conds.Add(NewUserMatcher(rr.UserEmail)) - } - - if len(rr.Attributes) > 0 { - configuredKeys := make(map[string]*regexp.Regexp) - for key, value := range rr.Attributes { - configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) - } - conds.Add(&AttributeMatcher{configuredKeys}) - } - - if len(rr.Geoip) > 0 { - cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) - if err != nil { - return nil, err - } - conds.Add(cond) - } - - if len(rr.SourceGeoip) > 0 { - cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) - if err != nil { - return nil, err - } - conds.Add(cond) - } - - if len(rr.LocalGeoip) > 0 { - cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) - if err != nil { - return nil, err - } - conds.Add(cond) - errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") - } - - 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) - } - errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") - conds.Add(matcher) - } - - if conds.Len() == 0 { - return nil, errors.New("this rule has no effective fields").AtWarning() - } - - return conds, nil -} diff --git a/app/router/condition_builder_other.go b/app/router/condition_builder_other.go deleted file mode 100644 index 578ce4866008..000000000000 --- a/app/router/condition_builder_other.go +++ /dev/null @@ -1,156 +0,0 @@ -//go:build ios || darwin - -package router - -import ( - "context" - "regexp" - "strings" - - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform/filesystem/assets" - "google.golang.org/protobuf/proto" -) - -func (rr *RoutingRule) BuildCondition() (Condition, error) { - conds := NewConditionChan() - - if len(rr.InboundTag) > 0 { - conds.Add(NewInboundTagMatcher(rr.InboundTag)) - } - - if len(rr.Networks) > 0 { - conds.Add(NewNetworkMatcher(rr.Networks)) - } - - if len(rr.Protocol) > 0 { - conds.Add(NewProtocolMatcher(rr.Protocol)) - } - - if rr.PortList != nil { - conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) - } - - if rr.SourcePortList != nil { - conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) - } - - if rr.LocalPortList != nil { - conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) - } - - if rr.VlessRouteList != nil { - conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) - } - - if len(rr.UserEmail) > 0 { - conds.Add(NewUserMatcher(rr.UserEmail)) - } - - if len(rr.Attributes) > 0 { - configuredKeys := make(map[string]*regexp.Regexp) - for key, value := range rr.Attributes { - configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) - } - conds.Add(&AttributeMatcher{configuredKeys}) - } - - if len(rr.Geoip) > 0 { - geoip, err := getGeoIPList(rr.Geoip) - if err != nil { - return nil, errors.New("failed to build geoip from mmap").Base(err) - } - - cond, err := NewIPMatcher(geoip, MatcherAsType_Target) - if err != nil { - return nil, err - } - conds.Add(cond) - } - - if len(rr.SourceGeoip) > 0 { - cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) - if err != nil { - return nil, err - } - conds.Add(cond) - } - - if len(rr.LocalGeoip) > 0 { - cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) - if err != nil { - return nil, err - } - conds.Add(cond) - errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") - } - - if len(rr.Domain) > 0 { - domains, err := getDomainList(rr.Domain) - if err != nil { - return nil, errors.New("failed to build domains from mmap").Base(err) - } - - matcher, err := NewMphMatcherGroup(domains) - 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) - } - - if conds.Len() == 0 { - return nil, errors.New("this rule has no effective fields").AtWarning() - } - - return conds, nil -} - -func getGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { - geoipList := []*GeoIP{} - for _, ip := range ips { - if ip.CountryCode != "" { - geoMeta := assets.GeoIP.GetGeoMeta(ip.CountryCode) - if geoMeta == nil || geoMeta.Length == 0 { - return nil, errors.New("geoip not founded: ", ip.CountryCode) - } - - bs := assets.GeoIP.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) - 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 { - if code, ok := strings.CutPrefix(domain.Value, "geosite:"); ok { - - geoMeta := assets.GeoSite.GetGeoMeta(code) - if geoMeta == nil || geoMeta.Length == 0 { - return nil, errors.New("geosite not founded: ", code) - } - bs := assets.GeoSite.Slice(geoMeta.Start, geoMeta.Start+geoMeta.Length) - var geosite GeoSite - - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("failed Unmarshal :").Base(err) - } - domainList = append(domainList, geosite.Domain...) - - } else { - domainList = append(domainList, domain) - } - } - return domainList, nil -} diff --git a/app/router/config.go b/app/router/config.go index 94f73d1050ea..c09ce93932c3 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -1,11 +1,17 @@ package router import ( + "context" + "regexp" + "runtime" "strings" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/platform/filesystem/assets" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" + "google.golang.org/protobuf/proto" ) type Rule struct { @@ -27,6 +33,107 @@ func (r *Rule) Apply(ctx routing.Context) bool { return r.Condition.Apply(ctx) } +func (rr *RoutingRule) BuildCondition() (Condition, error) { + conds := NewConditionChan() + + if len(rr.InboundTag) > 0 { + conds.Add(NewInboundTagMatcher(rr.InboundTag)) + } + + if len(rr.Networks) > 0 { + conds.Add(NewNetworkMatcher(rr.Networks)) + } + + if len(rr.Protocol) > 0 { + conds.Add(NewProtocolMatcher(rr.Protocol)) + } + + if rr.PortList != nil { + conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) + } + + if rr.SourcePortList != nil { + conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) + } + + if rr.LocalPortList != nil { + conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) + } + + if rr.VlessRouteList != nil { + conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) + } + + if len(rr.UserEmail) > 0 { + conds.Add(NewUserMatcher(rr.UserEmail)) + } + + if len(rr.Attributes) > 0 { + configuredKeys := make(map[string]*regexp.Regexp) + for key, value := range rr.Attributes { + configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) + } + conds.Add(&AttributeMatcher{configuredKeys}) + } + + if len(rr.Geoip) > 0 { + geoip := rr.Geoip + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + var err error + geoip, err = getGeoIPList(rr.Geoip) + if err != nil { + return nil, errors.New("failed to build geoip from mmap").Base(err) + } + } + cond, err := NewIPMatcher(geoip, MatcherAsType_Target) + if err != nil { + return nil, err + } + conds.Add(cond) + } + + if len(rr.SourceGeoip) > 0 { + cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) + if err != nil { + return nil, err + } + conds.Add(cond) + } + + if len(rr.LocalGeoip) > 0 { + cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) + if err != nil { + return nil, err + } + conds.Add(cond) + errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") + } + + if len(rr.Domain) > 0 { + domains := rr.Domain + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + var err error + domains, err = getDomainList(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domains from mmap").Base(err) + } + } + + matcher, err := NewMphMatcherGroup(domains) + 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) + } + + if conds.Len() == 0 { + return nil, errors.New("this rule has no effective fields").AtWarning() + } + + return conds, nil +} + // Build builds the balancing rule func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatcher) (*Balancer, error) { switch strings.ToLower(br.Strategy) { @@ -73,3 +180,62 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch return nil, errors.New("unrecognized balancer type") } } + +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 = assets.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 = assets.Find(bs, []byte(code)) + var geosite GeoSite + + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("failed Unmarshal :").Base(err) + } + domainList = append(domainList, geosite.Domain...) + + } else { + domainList = append(domainList, domain) + } + } + return domainList, nil +} diff --git a/common/platform/filesystem/assets/asset_tools.go b/common/platform/filesystem/assets/asset_tools.go index a3dc709442b9..c6c9e02473d5 100644 --- a/common/platform/filesystem/assets/asset_tools.go +++ b/common/platform/filesystem/assets/asset_tools.go @@ -17,59 +17,36 @@ func DecodeVarint(buf []byte) (x uint64, n int) { return 0, 0 } -func Find(data, code []byte) (found []byte, startByte int) { +func Find(data, code []byte) []byte { codeL := len(code) if codeL == 0 { - return nil, -1 + return nil } - - base := data - offset := 0 - for { dataL := len(data) if dataL < 2 { - return nil, -1 + return nil } - x, y := DecodeVarint(data[1:]) if x == 0 && y == 0 { - return nil, -1 + return nil } - - headL := 1 + y - bodyL := int(x) - - if dataL < headL+bodyL { - return nil, -1 + headL, bodyL := 1+y, int(x) + dataL -= headL + if dataL < bodyL { + return nil } - - // Move to body data = data[headL:] - offset += headL - - // Check code match if int(data[1]) == codeL { - match := true - for i := 0; i < codeL; i++ { - if data[2+i] != code[i] { - match = false - break + for i := 0; i < codeL && data[2+i] == code[i]; i++ { + if i+1 == codeL { + return data[:bodyL] } } - if match { - start := offset - end := offset + bodyL - return base[start:end], start - } } - - // Advance to next record - data = data[bodyL:] - offset += bodyL - - if len(data) == 0 { - return nil, -1 + if dataL == bodyL { + return nil } + data = data[bodyL:] } } diff --git a/common/platform/filesystem/assets/assets.go b/common/platform/filesystem/assets/assets.go deleted file mode 100644 index 5a3a04480b68..000000000000 --- a/common/platform/filesystem/assets/assets.go +++ /dev/null @@ -1,254 +0,0 @@ -//go:build ios || darwin - -package assets - -import ( - "bufio" - "fmt" - "os" - "runtime" - "strconv" - "strings" - "sync" - - "github.com/xtls/xray-core/common/platform" - "golang.org/x/sys/unix" -) - -type GeoMeta struct { - Code string - Start int64 - Length int64 -} - -type MMapFile struct { - mu sync.RWMutex - path string - data []byte - size int - - GeoMeta []GeoMeta -} - -var GeoSite *MMapFile -var GeoIP *MMapFile - -func ReadGeoIP(file string) ([]byte, error) { - if GeoIP == nil { - GeoIP = &MMapFile{GeoMeta: []GeoMeta{}} - if err := GeoIP.Open(platform.GetAssetLocation(file)); err != nil { - return nil, err - } - } - return GeoIP.Bytes(), nil -} - -func ReadGeoSite(file string) ([]byte, error) { - if GeoSite == nil { - GeoSite = &MMapFile{GeoMeta: []GeoMeta{}} - if err := GeoSite.Open(platform.GetAssetLocation(file)); err != nil { - return nil, err - } - } - - return GeoSite.Bytes(), nil -} - -// Open maps the file readonly -func (m *MMapFile) Open(path string) error { - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - - st, err := f.Stat() - if err != nil { - return err - } - - size64 := st.Size() - if size64 <= 0 { - m.mu.Lock() - _ = m.unmapLocked() - m.path, m.data, m.size = path, nil, 0 - m.mu.Unlock() - return nil - } - if size64 > int64(^uint(0)>>1) { - return fmt.Errorf("file too large: %d", size64) - } - size := int(size64) - - b, err := unix.Mmap(int(f.Fd()), 0, size, unix.PROT_READ, unix.MAP_SHARED) - if err != nil { - return err - } - - _ = unix.Madvise(b, unix.MADV_RANDOM) - - m.mu.Lock() - _ = m.unmapLocked() - m.path = path - m.data = b - m.size = size - m.mu.Unlock() - - return m.buildGeoMetaList() -} - -func (m *MMapFile) Close() error { - m.mu.Lock() - defer m.mu.Unlock() - return m.unmapLocked() -} - -func (m *MMapFile) Bytes() []byte { - m.mu.RLock() - defer m.mu.RUnlock() - return m.data -} - -func (m *MMapFile) Slice(start, end int64) []byte { - m.mu.RLock() - defer m.mu.RUnlock() - if start < 0 || end < start || int(end) > len(m.data) { - return nil - } - return m.data[start:end] -} - -func (m *MMapFile) AddGeoMeta(code string, start, length int) { - m.mu.RLock() - defer m.mu.RUnlock() - for _, info := range m.GeoMeta { - if info.Code == code { - return - } - } - m.GeoMeta = append(m.GeoMeta, GeoMeta{Code: code, Start: int64(start), Length: int64(length)}) -} - -func (m *MMapFile) GetGeoMeta(code string) *GeoMeta { - m.mu.RLock() - defer m.mu.RUnlock() - for _, info := range m.GeoMeta { - if info.Code == code { - return &info - } - } - return nil -} - -func (m *MMapFile) unmapLocked() error { - if m.data == nil { - return nil - } - err := unix.Munmap(m.data) - m.data = nil - m.size = 0 - return err -} - -func (m *MMapFile) buildGeoMetaList() error { - if platform.IsAssetMapEnabled() { - return m.buildGeoMetaFromMappedFile() - } else { - m.buildGeoMetaFromMemory(func(code []byte, start, length int64) error { - m.AddGeoMeta(string(code), int(start), int(length)) - runtime.GC() - - return nil - }) - - } - return nil -} - -func (m *MMapFile) buildGeoMetaFromMemory(onEntry func(code []byte, start, length int64) error) error { - data := m.Bytes() - baseLen := len(data) - var off int64 = 0 - - for { - if len(data) < 2 { - return nil - } - - x, n := DecodeVarint(data[1:]) - if x == 0 && n == 0 { - return nil - } - - headL := 1 + n - - if x > uint64(len(data)-headL) { - return fmt.Errorf("corrupt data: bodyLen=%d exceeds remaining=%d", x, len(data)-headL) - } - bodyL := int(x) - - body := data[headL : headL+bodyL] - start := off + int64(headL) - length := int64(bodyL) - - if len(body) >= 2 { - codeL := int(body[1]) - if codeL > 0 && len(body) >= 2+codeL { - code := body[2 : 2+codeL] - - if err := onEntry(code, start, length); err != nil { - return err - } - } - } - - step := headL + bodyL - data = data[step:] - off += int64(step) - - if off > int64(baseLen) { - return fmt.Errorf("corrupt scan: offset overflow") - } - } -} - -// the mapped file should be along side the geo file, if file is geosite.dat , mapped should be geosite.dat.map -func (m *MMapFile) buildGeoMetaFromMappedFile() error { - f, err := os.Open(m.path + ".map") - if err != nil { - return err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - - parts := strings.Split(line, ",") - if len(parts) != 3 { - return fmt.Errorf("invalid line: %q", line) - } - - start, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid start byte in %q", line) - } - - length, err := strconv.ParseInt(parts[2], 10, 64) - if err != nil { - return fmt.Errorf("invalid length in %q", line) - } - m.AddGeoMeta(parts[0], int(start), int(length)) - - } - - if err := scanner.Err(); err != nil { - return err - } - - return nil -} diff --git a/common/platform/others.go b/common/platform/others.go index 1266344d9cd2..be86b6faaac2 100644 --- a/common/platform/others.go +++ b/common/platform/others.go @@ -34,13 +34,6 @@ func GetAssetLocation(file string) string { return defPath } -// only apply if ios or darwin for now -// IsAssetMapEnabled to load mapped geosite or geoip (file name : geosite.dat.map) -func IsAssetMapEnabled() bool { - isEnabled := NewEnvFlag(AssetMapEnabled).GetValueAsInt(0) - return isEnabled != 0 -} - // GetCertLocation searches for `file` in the env dir and the executable dir func GetCertLocation(file string) string { certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) diff --git a/common/platform/platform.go b/common/platform/platform.go index 32c7bc8a8ae7..bc7baf655d20 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -11,10 +11,7 @@ const ( ConfigLocation = "xray.location.config" ConfdirLocation = "xray.location.confdir" AssetLocation = "xray.location.asset" - // only apply if !windows && !wasm - AssetMapEnabled = "xray.asset.mapped" - - CertLocation = "xray.location.cert" + CertLocation = "xray.location.cert" UseReadV = "xray.buf.readv" UseFreedomSplice = "xray.buf.splice" diff --git a/infra/conf/router.go b/infra/conf/router.go index 071aeec55092..cc9381e4d03a 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -2,12 +2,15 @@ package conf import ( "encoding/json" + "runtime" "strconv" "strings" "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/filesystem" + "github.com/xtls/xray-core/common/platform/filesystem/assets" "github.com/xtls/xray-core/common/serial" "google.golang.org/protobuf/proto" ) @@ -181,6 +184,83 @@ var ( SiteCache = make(map[string]*router.GeoSite) ) +func loadFile(file string) ([]byte, error) { + if FileCache[file] == nil { + bs, err := filesystem.ReadAsset(file) + if err != nil { + return nil, errors.New("failed to open file: ", file).Base(err) + } + if len(bs) == 0 { + return nil, errors.New("empty file: ", file) + } + // Do not cache file, may save RAM when there + // are many files, but consume CPU each time. + return bs, nil + FileCache[file] = bs + } + return FileCache[file], nil +} + +func loadIP(file, code string) ([]*router.CIDR, error) { + index := file + ":" + code + if IPCache[index] == nil { + var geoip router.GeoIP + + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + // dont pass code becuase we have country code in top level router.GeoIP + geoip = router.GeoIP{Cidr: []*router.CIDR{}} + } else { + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = assets.Find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("code not found in ", file, ": ", code) + } + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) + } + } + defer runtime.GC() // or debug.FreeOSMemory() + return geoip.Cidr, nil // do not cache geoip + IPCache[index] = &geoip + } + return IPCache[index].Cidr, nil +} + +func loadSite(file, code string) ([]*router.Domain, error) { + index := file + ":" + code + if SiteCache[index] == nil { + var geosite router.GeoSite + + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + // pass file:code so can build optimized matcher later + domain := router.Domain{Value: file + "_" + code} + geosite = router.GeoSite{Domain: []*router.Domain{&domain}} + + } else { + + bs, err := loadFile(file) + if err != nil { + return nil, errors.New("failed to load file: ", file).Base(err) + } + bs = assets.Find(bs, []byte(code)) + if bs == nil { + return nil, errors.New("list not found in ", file, ": ", code) + } + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) + } + } + + defer runtime.GC() // or debug.FreeOSMemory() + return geosite.Domain, nil // do not cache geosite + SiteCache[index] = &geosite + } + return SiteCache[index].Domain, nil +} + type AttributeMatcher interface { Match(*router.Domain) bool } diff --git a/infra/conf/router_assets.go b/infra/conf/router_assets.go deleted file mode 100644 index 3df53dd48ad9..000000000000 --- a/infra/conf/router_assets.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build !ios && !darwin - -package conf - -import ( - "runtime" - - "github.com/xtls/xray-core/app/router" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform/filesystem" - "github.com/xtls/xray-core/common/platform/filesystem/assets" - "google.golang.org/protobuf/proto" -) - -func loadFile(file string) ([]byte, error) { - if FileCache[file] == nil { - bs, err := filesystem.ReadAsset(file) - if err != nil { - return nil, errors.New("failed to open file: ", file).Base(err) - } - if len(bs) == 0 { - return nil, errors.New("empty file: ", file) - } - // Do not cache file, may save RAM when there - // are many files, but consume CPU each time. - return bs, nil - FileCache[file] = bs - } - return FileCache[file], nil -} - -func loadIP(file, code string) ([]*router.CIDR, error) { - index := file + ":" + code - if IPCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs, _ = assets.Find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("code not found in ", file, ": ", code) - } - var geoip router.GeoIP - if err := proto.Unmarshal(bs, &geoip); err != nil { - return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) - } - defer runtime.GC() // or debug.FreeOSMemory() - return geoip.Cidr, nil // do not cache geoip - IPCache[index] = &geoip - } - return IPCache[index].Cidr, nil -} - -func loadSite(file, code string) ([]*router.Domain, error) { - index := file + ":" + code - if SiteCache[index] == nil { - bs, err := loadFile(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - bs, _ = assets.Find(bs, []byte(code)) - if bs == nil { - return nil, errors.New("list not found in ", file, ": ", code) - } - var geosite router.GeoSite - if err := proto.Unmarshal(bs, &geosite); err != nil { - return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) - } - defer runtime.GC() // or debug.FreeOSMemory() - return geosite.Domain, nil // do not cache geosite - SiteCache[index] = &geosite - } - return SiteCache[index].Domain, nil -} diff --git a/infra/conf/router_assets_other.go b/infra/conf/router_assets_other.go deleted file mode 100644 index f3c483c44a8b..000000000000 --- a/infra/conf/router_assets_other.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build ios || darwin - -package conf - -import ( - "runtime" - - "github.com/xtls/xray-core/app/router" - "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform/filesystem/assets" -) - -func loadIP(file, code string) ([]*router.CIDR, error) { - index := file + ":" + code - if IPCache[index] == nil { - _, err := assets.ReadGeoIP(file) - - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - var geoip router.GeoIP - - // dont pass code becuase we have country code in top level router.GeoIP - geoip = router.GeoIP{Cidr: []*router.CIDR{}} - defer runtime.GC() // or debug.FreeOSMemory() - return geoip.Cidr, nil // do not cache geoip - IPCache[index] = &geoip - } - return IPCache[index].Cidr, nil -} - -func loadSite(file, code string) ([]*router.Domain, error) { - index := file + ":" + code - if SiteCache[index] == nil { - var geosite router.GeoSite - _, err := assets.ReadGeoSite(file) - if err != nil { - return nil, errors.New("failed to load file: ", file).Base(err) - } - - // pass file:code so can build optimized matcher later - domain := router.Domain{Value: "geosite:" + code} - geosite = router.GeoSite{Domain: []*router.Domain{&domain}} - defer runtime.GC() // or debug.FreeOSMemory() - return geosite.Domain, nil // do not cache geosite - SiteCache[index] = &geosite - } - return SiteCache[index].Domain, nil -} From fda3cecb53b5c84a2491eb0466f8aee232e8b2d3 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Mon, 5 Jan 2026 20:52:07 +0330 Subject: [PATCH 10/12] change runtime.GOOS for testing --- app/router/config.go | 4 ++-- infra/conf/router.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/router/config.go b/app/router/config.go index c09ce93932c3..7ede50a91871 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -78,7 +78,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { if len(rr.Geoip) > 0 { geoip := rr.Geoip - if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { var err error geoip, err = getGeoIPList(rr.Geoip) if err != nil { @@ -111,7 +111,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) { if len(rr.Domain) > 0 { domains := rr.Domain - if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { var err error domains, err = getDomainList(rr.Domain) if err != nil { diff --git a/infra/conf/router.go b/infra/conf/router.go index cc9381e4d03a..e2a1f1626b5e 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -206,7 +206,7 @@ func loadIP(file, code string) ([]*router.CIDR, error) { if IPCache[index] == nil { var geoip router.GeoIP - if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { // dont pass code becuase we have country code in top level router.GeoIP geoip = router.GeoIP{Cidr: []*router.CIDR{}} } else { @@ -234,7 +234,7 @@ func loadSite(file, code string) ([]*router.Domain, error) { if SiteCache[index] == nil { var geosite router.GeoSite - if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { // pass file:code so can build optimized matcher later domain := router.Domain{Value: file + "_" + code} geosite = router.GeoSite{Domain: []*router.Domain{&domain}} From 20472d1eaf5c7529ccfa696c37289fb1ca82a4a8 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Mon, 5 Jan 2026 23:18:47 +0330 Subject: [PATCH 11/12] move asset_tools.go --- app/router/config.go | 5 ++--- common/platform/filesystem/{assets => }/asset_tools.go | 2 +- infra/conf/router.go | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) rename common/platform/filesystem/{assets => }/asset_tools.go (97%) diff --git a/app/router/config.go b/app/router/config.go index 7ede50a91871..b6f8c5a0d449 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -8,7 +8,6 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/platform/filesystem" - "github.com/xtls/xray-core/common/platform/filesystem/assets" "github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/routing" "google.golang.org/protobuf/proto" @@ -194,7 +193,7 @@ func getGeoIPList(ips []*GeoIP) ([]*GeoIP, error) { if err != nil { return nil, errors.New("failed to load file: ", fileName).Base(err) } - bs = assets.Find(bs, []byte(ip.CountryCode)) + bs = filesystem.Find(bs, []byte(ip.CountryCode)) var geoip GeoIP @@ -225,7 +224,7 @@ func getDomainList(domains []*Domain) ([]*Domain, error) { if err != nil { return nil, errors.New("failed to load file: ", fileName).Base(err) } - bs = assets.Find(bs, []byte(code)) + bs = filesystem.Find(bs, []byte(code)) var geosite GeoSite if err := proto.Unmarshal(bs, &geosite); err != nil { diff --git a/common/platform/filesystem/assets/asset_tools.go b/common/platform/filesystem/asset_tools.go similarity index 97% rename from common/platform/filesystem/assets/asset_tools.go rename to common/platform/filesystem/asset_tools.go index c6c9e02473d5..45316f8084e6 100644 --- a/common/platform/filesystem/assets/asset_tools.go +++ b/common/platform/filesystem/asset_tools.go @@ -1,4 +1,4 @@ -package assets +package filesystem func DecodeVarint(buf []byte) (x uint64, n int) { for shift := uint(0); shift < 64; shift += 7 { diff --git a/infra/conf/router.go b/infra/conf/router.go index e2a1f1626b5e..b0afdde90eff 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -10,7 +10,6 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/platform/filesystem" - "github.com/xtls/xray-core/common/platform/filesystem/assets" "github.com/xtls/xray-core/common/serial" "google.golang.org/protobuf/proto" ) @@ -214,7 +213,7 @@ func loadIP(file, code string) ([]*router.CIDR, error) { if err != nil { return nil, errors.New("failed to load file: ", file).Base(err) } - bs = assets.Find(bs, []byte(code)) + bs = filesystem.Find(bs, []byte(code)) if bs == nil { return nil, errors.New("code not found in ", file, ": ", code) } @@ -245,7 +244,7 @@ func loadSite(file, code string) ([]*router.Domain, error) { if err != nil { return nil, errors.New("failed to load file: ", file).Base(err) } - bs = assets.Find(bs, []byte(code)) + bs = filesystem.Find(bs, []byte(code)) if bs == nil { return nil, errors.New("list not found in ", file, ": ", code) } From c20c16dd4b615a576cd73b5d88db9d6b1f6ea106 Mon Sep 17 00:00:00 2001 From: hossinasaadi Date: Tue, 6 Jan 2026 00:59:28 +0330 Subject: [PATCH 12/12] fix geo attribute --- app/router/condition.go | 42 ++++++++++++++++++++++++++++++++++++ app/router/config.go | 20 ++++++++++++++++- infra/conf/router.go | 48 ++++++----------------------------------- 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/app/router/condition.go b/app/router/condition.go index d21487a20371..214461d156c5 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -305,3 +305,45 @@ func (m *AttributeMatcher) Apply(ctx routing.Context) bool { } return m.Match(attributes) } + +// Geo attribute +type GeoAttributeMatcher interface { + Match(*Domain) bool +} + +type GeoBooleanMatcher string + +func (m GeoBooleanMatcher) Match(domain *Domain) bool { + for _, attr := range domain.Attribute { + if attr.Key == string(m) { + return true + } + } + return false +} + +type GeoAttributeList struct { + Matcher []GeoAttributeMatcher +} + +func (al *GeoAttributeList) Match(domain *Domain) bool { + for _, matcher := range al.Matcher { + if !matcher.Match(domain) { + return false + } + } + return true +} + +func (al *GeoAttributeList) IsEmpty() bool { + return len(al.Matcher) == 0 +} + +func ParseAttrs(attrs []string) *GeoAttributeList { + al := new(GeoAttributeList) + for _, attr := range attrs { + lc := strings.ToLower(attr) + al.Matcher = append(al.Matcher, GeoBooleanMatcher(lc)) + } + return al +} diff --git a/app/router/config.go b/app/router/config.go index b6f8c5a0d449..4aa4fd800466 100644 --- a/app/router/config.go +++ b/app/router/config.go @@ -215,7 +215,7 @@ func getDomainList(domains []*Domain) ([]*Domain, error) { for _, domain := range domains { val := strings.Split(domain.Value, "_") - if len(val) == 2 { + if len(val) >= 2 { fileName := val[0] code := val[1] @@ -230,6 +230,24 @@ func getDomainList(domains []*Domain) ([]*Domain, error) { 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 { diff --git a/infra/conf/router.go b/infra/conf/router.go index b0afdde90eff..8bc3b158cdfc 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -260,54 +260,13 @@ func loadSite(file, code string) ([]*router.Domain, error) { return SiteCache[index].Domain, nil } -type AttributeMatcher interface { - Match(*router.Domain) bool -} - -type BooleanMatcher string - -func (m BooleanMatcher) Match(domain *router.Domain) bool { - for _, attr := range domain.Attribute { - if attr.Key == string(m) { - return true - } - } - return false -} - -type AttributeList struct { - matcher []AttributeMatcher -} - -func (al *AttributeList) Match(domain *router.Domain) bool { - for _, matcher := range al.matcher { - if !matcher.Match(domain) { - return false - } - } - return true -} - -func (al *AttributeList) IsEmpty() bool { - return len(al.matcher) == 0 -} - -func parseAttrs(attrs []string) *AttributeList { - al := new(AttributeList) - for _, attr := range attrs { - lc := strings.ToLower(attr) - al.matcher = append(al.matcher, BooleanMatcher(lc)) - } - return al -} - func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { parts := strings.Split(siteWithAttr, "@") if len(parts) == 0 { return nil, errors.New("empty site") } country := strings.ToUpper(parts[0]) - attrs := parseAttrs(parts[1:]) + attrs := router.ParseAttrs(parts[1:]) domains, err := loadSite(file, country) if err != nil { return nil, err @@ -317,6 +276,11 @@ func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, er return domains, nil } + if runtime.GOOS != "windows" && runtime.GOOS != "wasm" { + domains[0].Value = domains[0].Value + "_" + strings.Join(parts[1:], ",") + return domains, nil + } + filteredDomains := make([]*router.Domain, 0, len(domains)) for _, domain := range domains { if attrs.Match(domain) {