Browse Source

refactor configure file loader for geo loader and v5

Shelikhoo 4 years ago
parent
commit
0f1fac84ca

+ 3 - 1
infra/conf/common.go → infra/conf/cfgcommon/common.go

@@ -1,4 +1,4 @@
-package conf
+package cfgcommon
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
@@ -9,6 +9,8 @@ import (
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 )
 )
 
 
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
+
 type StringList []string
 type StringList []string
 
 
 func NewStringList(raw []string) *StringList {
 func NewStringList(raw []string) *StringList {

+ 26 - 25
infra/conf/common_test.go → infra/conf/cfgcommon/common_test.go

@@ -1,22 +1,23 @@
-package conf_test
+package cfgcommon_test
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"os"
 	"os"
 	"testing"
 	"testing"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/google/go-cmp/cmp/cmpopts"
 
 
 	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
-	. "github.com/v2fly/v2ray-core/v4/infra/conf"
 )
 )
 
 
 func TestStringListUnmarshalError(t *testing.T) {
 func TestStringListUnmarshalError(t *testing.T) {
 	rawJSON := `1234`
 	rawJSON := `1234`
-	list := new(StringList)
+	list := new(cfgcommon.StringList)
 	err := json.Unmarshal([]byte(rawJSON), list)
 	err := json.Unmarshal([]byte(rawJSON), list)
 	if err == nil {
 	if err == nil {
 		t.Error("expected error, but got nil")
 		t.Error("expected error, but got nil")
@@ -25,7 +26,7 @@ func TestStringListUnmarshalError(t *testing.T) {
 
 
 func TestStringListLen(t *testing.T) {
 func TestStringListLen(t *testing.T) {
 	rawJSON := `"a, b, c, d"`
 	rawJSON := `"a, b, c, d"`
-	var list StringList
+	var list cfgcommon.StringList
 	err := json.Unmarshal([]byte(rawJSON), &list)
 	err := json.Unmarshal([]byte(rawJSON), &list)
 	common.Must(err)
 	common.Must(err)
 	if r := cmp.Diff([]string(list), []string{"a", " b", " c", " d"}); r != "" {
 	if r := cmp.Diff([]string(list), []string{"a", " b", " c", " d"}); r != "" {
@@ -35,7 +36,7 @@ func TestStringListLen(t *testing.T) {
 
 
 func TestIPParsing(t *testing.T) {
 func TestIPParsing(t *testing.T) {
 	rawJSON := "\"8.8.8.8\""
 	rawJSON := "\"8.8.8.8\""
-	var address Address
+	var address cfgcommon.Address
 	err := json.Unmarshal([]byte(rawJSON), &address)
 	err := json.Unmarshal([]byte(rawJSON), &address)
 	common.Must(err)
 	common.Must(err)
 	if r := cmp.Diff(address.IP(), net.IP{8, 8, 8, 8}); r != "" {
 	if r := cmp.Diff(address.IP(), net.IP{8, 8, 8, 8}); r != "" {
@@ -45,7 +46,7 @@ func TestIPParsing(t *testing.T) {
 
 
 func TestDomainParsing(t *testing.T) {
 func TestDomainParsing(t *testing.T) {
 	rawJSON := "\"v2fly.org\""
 	rawJSON := "\"v2fly.org\""
-	var address Address
+	var address cfgcommon.Address
 	common.Must(json.Unmarshal([]byte(rawJSON), &address))
 	common.Must(json.Unmarshal([]byte(rawJSON), &address))
 	if address.Domain() != "v2fly.org" {
 	if address.Domain() != "v2fly.org" {
 		t.Error("domain: ", address.Domain())
 		t.Error("domain: ", address.Domain())
@@ -55,7 +56,7 @@ func TestDomainParsing(t *testing.T) {
 func TestURLParsing(t *testing.T) {
 func TestURLParsing(t *testing.T) {
 	{
 	{
 		rawJSON := "\"https://dns.google/dns-query\""
 		rawJSON := "\"https://dns.google/dns-query\""
-		var address Address
+		var address cfgcommon.Address
 		common.Must(json.Unmarshal([]byte(rawJSON), &address))
 		common.Must(json.Unmarshal([]byte(rawJSON), &address))
 		if address.Domain() != "https://dns.google/dns-query" {
 		if address.Domain() != "https://dns.google/dns-query" {
 			t.Error("URL: ", address.Domain())
 			t.Error("URL: ", address.Domain())
@@ -63,7 +64,7 @@ func TestURLParsing(t *testing.T) {
 	}
 	}
 	{
 	{
 		rawJSON := "\"https+local://dns.google/dns-query\""
 		rawJSON := "\"https+local://dns.google/dns-query\""
-		var address Address
+		var address cfgcommon.Address
 		common.Must(json.Unmarshal([]byte(rawJSON), &address))
 		common.Must(json.Unmarshal([]byte(rawJSON), &address))
 		if address.Domain() != "https+local://dns.google/dns-query" {
 		if address.Domain() != "https+local://dns.google/dns-query" {
 			t.Error("URL: ", address.Domain())
 			t.Error("URL: ", address.Domain())
@@ -73,7 +74,7 @@ func TestURLParsing(t *testing.T) {
 
 
 func TestInvalidAddressJson(t *testing.T) {
 func TestInvalidAddressJson(t *testing.T) {
 	rawJSON := "1234"
 	rawJSON := "1234"
-	var address Address
+	var address cfgcommon.Address
 	err := json.Unmarshal([]byte(rawJSON), &address)
 	err := json.Unmarshal([]byte(rawJSON), &address)
 	if err == nil {
 	if err == nil {
 		t.Error("nil error")
 		t.Error("nil error")
@@ -81,7 +82,7 @@ func TestInvalidAddressJson(t *testing.T) {
 }
 }
 
 
 func TestStringNetwork(t *testing.T) {
 func TestStringNetwork(t *testing.T) {
-	var network Network
+	var network cfgcommon.Network
 	common.Must(json.Unmarshal([]byte(`"tcp"`), &network))
 	common.Must(json.Unmarshal([]byte(`"tcp"`), &network))
 	if v := network.Build(); v != net.Network_TCP {
 	if v := network.Build(); v != net.Network_TCP {
 		t.Error("network: ", v)
 		t.Error("network: ", v)
@@ -89,7 +90,7 @@ func TestStringNetwork(t *testing.T) {
 }
 }
 
 
 func TestArrayNetworkList(t *testing.T) {
 func TestArrayNetworkList(t *testing.T) {
-	var list NetworkList
+	var list cfgcommon.NetworkList
 	common.Must(json.Unmarshal([]byte("[\"Tcp\"]"), &list))
 	common.Must(json.Unmarshal([]byte("[\"Tcp\"]"), &list))
 
 
 	nlist := list.Build()
 	nlist := list.Build()
@@ -102,7 +103,7 @@ func TestArrayNetworkList(t *testing.T) {
 }
 }
 
 
 func TestStringNetworkList(t *testing.T) {
 func TestStringNetworkList(t *testing.T) {
-	var list NetworkList
+	var list cfgcommon.NetworkList
 	common.Must(json.Unmarshal([]byte("\"TCP, ip\""), &list))
 	common.Must(json.Unmarshal([]byte("\"TCP, ip\""), &list))
 
 
 	nlist := list.Build()
 	nlist := list.Build()
@@ -115,7 +116,7 @@ func TestStringNetworkList(t *testing.T) {
 }
 }
 
 
 func TestInvalidNetworkJson(t *testing.T) {
 func TestInvalidNetworkJson(t *testing.T) {
-	var list NetworkList
+	var list cfgcommon.NetworkList
 	err := json.Unmarshal([]byte("0"), &list)
 	err := json.Unmarshal([]byte("0"), &list)
 	if err == nil {
 	if err == nil {
 		t.Error("nil error")
 		t.Error("nil error")
@@ -123,10 +124,10 @@ func TestInvalidNetworkJson(t *testing.T) {
 }
 }
 
 
 func TestIntPort(t *testing.T) {
 func TestIntPort(t *testing.T) {
-	var portRange PortRange
+	var portRange cfgcommon.PortRange
 	common.Must(json.Unmarshal([]byte("1234"), &portRange))
 	common.Must(json.Unmarshal([]byte("1234"), &portRange))
 
 
-	if r := cmp.Diff(portRange, PortRange{
+	if r := cmp.Diff(portRange, cfgcommon.PortRange{
 		From: 1234, To: 1234,
 		From: 1234, To: 1234,
 	}); r != "" {
 	}); r != "" {
 		t.Error(r)
 		t.Error(r)
@@ -134,7 +135,7 @@ func TestIntPort(t *testing.T) {
 }
 }
 
 
 func TestOverRangeIntPort(t *testing.T) {
 func TestOverRangeIntPort(t *testing.T) {
-	var portRange PortRange
+	var portRange cfgcommon.PortRange
 	err := json.Unmarshal([]byte("70000"), &portRange)
 	err := json.Unmarshal([]byte("70000"), &portRange)
 	if err == nil {
 	if err == nil {
 		t.Error("nil error")
 		t.Error("nil error")
@@ -149,10 +150,10 @@ func TestOverRangeIntPort(t *testing.T) {
 func TestEnvPort(t *testing.T) {
 func TestEnvPort(t *testing.T) {
 	common.Must(os.Setenv("PORT", "1234"))
 	common.Must(os.Setenv("PORT", "1234"))
 
 
-	var portRange PortRange
+	var portRange cfgcommon.PortRange
 	common.Must(json.Unmarshal([]byte("\"env:PORT\""), &portRange))
 	common.Must(json.Unmarshal([]byte("\"env:PORT\""), &portRange))
 
 
-	if r := cmp.Diff(portRange, PortRange{
+	if r := cmp.Diff(portRange, cfgcommon.PortRange{
 		From: 1234, To: 1234,
 		From: 1234, To: 1234,
 	}); r != "" {
 	}); r != "" {
 		t.Error(r)
 		t.Error(r)
@@ -160,10 +161,10 @@ func TestEnvPort(t *testing.T) {
 }
 }
 
 
 func TestSingleStringPort(t *testing.T) {
 func TestSingleStringPort(t *testing.T) {
-	var portRange PortRange
+	var portRange cfgcommon.PortRange
 	common.Must(json.Unmarshal([]byte("\"1234\""), &portRange))
 	common.Must(json.Unmarshal([]byte("\"1234\""), &portRange))
 
 
-	if r := cmp.Diff(portRange, PortRange{
+	if r := cmp.Diff(portRange, cfgcommon.PortRange{
 		From: 1234, To: 1234,
 		From: 1234, To: 1234,
 	}); r != "" {
 	}); r != "" {
 		t.Error(r)
 		t.Error(r)
@@ -171,10 +172,10 @@ func TestSingleStringPort(t *testing.T) {
 }
 }
 
 
 func TestStringPairPort(t *testing.T) {
 func TestStringPairPort(t *testing.T) {
-	var portRange PortRange
+	var portRange cfgcommon.PortRange
 	common.Must(json.Unmarshal([]byte("\"1234-5678\""), &portRange))
 	common.Must(json.Unmarshal([]byte("\"1234-5678\""), &portRange))
 
 
-	if r := cmp.Diff(portRange, PortRange{
+	if r := cmp.Diff(portRange, cfgcommon.PortRange{
 		From: 1234, To: 5678,
 		From: 1234, To: 5678,
 	}); r != "" {
 	}); r != "" {
 		t.Error(r)
 		t.Error(r)
@@ -182,7 +183,7 @@ func TestStringPairPort(t *testing.T) {
 }
 }
 
 
 func TestOverRangeStringPort(t *testing.T) {
 func TestOverRangeStringPort(t *testing.T) {
-	var portRange PortRange
+	var portRange cfgcommon.PortRange
 	err := json.Unmarshal([]byte("\"65536\""), &portRange)
 	err := json.Unmarshal([]byte("\"65536\""), &portRange)
 	if err == nil {
 	if err == nil {
 		t.Error("nil error")
 		t.Error("nil error")
@@ -205,7 +206,7 @@ func TestOverRangeStringPort(t *testing.T) {
 }
 }
 
 
 func TestUserParsing(t *testing.T) {
 func TestUserParsing(t *testing.T) {
-	user := new(User)
+	user := new(cfgcommon.User)
 	common.Must(json.Unmarshal([]byte(`{
 	common.Must(json.Unmarshal([]byte(`{
     "id": "96edb838-6d68-42ef-a933-25f7ac3a9d09",
     "id": "96edb838-6d68-42ef-a933-25f7ac3a9d09",
     "email": "love@v2fly.org",
     "email": "love@v2fly.org",
@@ -223,7 +224,7 @@ func TestUserParsing(t *testing.T) {
 }
 }
 
 
 func TestInvalidUserJson(t *testing.T) {
 func TestInvalidUserJson(t *testing.T) {
-	user := new(User)
+	user := new(cfgcommon.User)
 	err := json.Unmarshal([]byte(`{"email": 1234}`), user)
 	err := json.Unmarshal([]byte(`{"email": 1234}`), user)
 	if err == nil {
 	if err == nil {
 		t.Error("nil error")
 		t.Error("nil error")

+ 9 - 0
infra/conf/cfgcommon/errors.generated.go

@@ -0,0 +1,9 @@
+package cfgcommon
+
+import "github.com/v2fly/v2ray-core/v4/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 42 - 0
infra/conf/cfgcommon/session.go

@@ -0,0 +1,42 @@
+package cfgcommon
+
+import (
+	"context"
+
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/geodata"
+)
+
+type configureLoadingContext int
+
+const confContextKey = configureLoadingContext(1)
+
+type configureLoadingEnvironment struct {
+	geoLoader geodata.Loader
+}
+
+func (c *configureLoadingEnvironment) GetGeoLoader() geodata.Loader {
+	if c.geoLoader == nil {
+		var err error
+		c.geoLoader, err = geodata.GetGeoDataLoader("standard")
+		common.Must(err)
+	}
+	return c.geoLoader
+}
+
+type ConfigureLoadingEnvironment interface {
+	GetGeoLoader() geodata.Loader
+}
+
+func NewConfigureLoadingContext(ctx context.Context) context.Context {
+	environment := &configureLoadingEnvironment{}
+	return context.WithValue(ctx, confContextKey, environment)
+}
+
+func GetConfigureLoadingEnvironment(ctx context.Context) ConfigureLoadingEnvironment {
+	return ctx.Value(confContextKey).(ConfigureLoadingEnvironment)
+}
+
+func SetGeoDataLoader(ctx context.Context, loader geodata.Loader) {
+	GetConfigureLoadingEnvironment(ctx).(*configureLoadingEnvironment).geoLoader = loader
+}

+ 46 - 19
infra/conf/dns.go

@@ -1,38 +1,45 @@
 package conf
 package conf
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"sort"
 	"sort"
 	"strings"
 	"strings"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/geodata"
+	rule2 "github.com/v2fly/v2ray-core/v4/infra/conf/rule"
+
 	"github.com/v2fly/v2ray-core/v4/app/dns"
 	"github.com/v2fly/v2ray-core/v4/app/dns"
 	"github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/net"
 )
 )
 
 
 type NameServerConfig struct {
 type NameServerConfig struct {
-	Address      *Address
-	ClientIP     *Address
+	Address      *cfgcommon.Address
+	ClientIP     *cfgcommon.Address
 	Port         uint16
 	Port         uint16
 	SkipFallback bool
 	SkipFallback bool
 	Domains      []string
 	Domains      []string
-	ExpectIPs    StringList
+	ExpectIPs    cfgcommon.StringList
+
+	cfgctx context.Context
 }
 }
 
 
 func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
-	var address Address
+	var address cfgcommon.Address
 	if err := json.Unmarshal(data, &address); err == nil {
 	if err := json.Unmarshal(data, &address); err == nil {
 		c.Address = &address
 		c.Address = &address
 		return nil
 		return nil
 	}
 	}
 
 
 	var advanced struct {
 	var advanced struct {
-		Address      *Address   `json:"address"`
-		ClientIP     *Address   `json:"clientIp"`
-		Port         uint16     `json:"port"`
-		SkipFallback bool       `json:"skipFallback"`
-		Domains      []string   `json:"domains"`
-		ExpectIPs    StringList `json:"expectIps"`
+		Address      *cfgcommon.Address   `json:"address"`
+		ClientIP     *cfgcommon.Address   `json:"clientIp"`
+		Port         uint16               `json:"port"`
+		SkipFallback bool                 `json:"skipFallback"`
+		Domains      []string             `json:"domains"`
+		ExpectIPs    cfgcommon.StringList `json:"expectIps"`
 	}
 	}
 	if err := json.Unmarshal(data, &advanced); err == nil {
 	if err := json.Unmarshal(data, &advanced); err == nil {
 		c.Address = advanced.Address
 		c.Address = advanced.Address
@@ -63,6 +70,8 @@ func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType {
 }
 }
 
 
 func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 func (c *NameServerConfig) Build() (*dns.NameServer, error) {
+	cfgctx := c.cfgctx
+
 	if c.Address == nil {
 	if c.Address == nil {
 		return nil, newError("NameServer address is not specified.")
 		return nil, newError("NameServer address is not specified.")
 	}
 	}
@@ -71,7 +80,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 	var originalRules []*dns.NameServer_OriginalRule
 	var originalRules []*dns.NameServer_OriginalRule
 
 
 	for _, rule := range c.Domains {
 	for _, rule := range c.Domains {
-		parsedDomain, err := parseDomainRule(rule)
+		parsedDomain, err := rule2.ParseDomainRule(cfgctx, rule)
 		if err != nil {
 		if err != nil {
 			return nil, newError("invalid domain rule: ", rule).Base(err)
 			return nil, newError("invalid domain rule: ", rule).Base(err)
 		}
 		}
@@ -88,7 +97,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 		})
 		})
 	}
 	}
 
 
-	geoipList, err := toCidrList(c.ExpectIPs)
+	geoipList, err := rule2.ToCidrList(cfgctx, c.ExpectIPs)
 	if err != nil {
 	if err != nil {
 		return nil, newError("invalid IP rule: ", c.ExpectIPs).Base(err)
 		return nil, newError("invalid IP rule: ", c.ExpectIPs).Base(err)
 	}
 	}
@@ -126,22 +135,24 @@ var typeMap = map[router.Domain_Type]dns.DomainMatchingType{
 type DNSConfig struct {
 type DNSConfig struct {
 	Servers         []*NameServerConfig     `json:"servers"`
 	Servers         []*NameServerConfig     `json:"servers"`
 	Hosts           map[string]*HostAddress `json:"hosts"`
 	Hosts           map[string]*HostAddress `json:"hosts"`
-	ClientIP        *Address                `json:"clientIp"`
+	ClientIP        *cfgcommon.Address      `json:"clientIp"`
 	Tag             string                  `json:"tag"`
 	Tag             string                  `json:"tag"`
 	QueryStrategy   string                  `json:"queryStrategy"`
 	QueryStrategy   string                  `json:"queryStrategy"`
 	DisableCache    bool                    `json:"disableCache"`
 	DisableCache    bool                    `json:"disableCache"`
 	DisableFallback bool                    `json:"disableFallback"`
 	DisableFallback bool                    `json:"disableFallback"`
+
+	GeoLoader string `json:"geoLoader"`
 }
 }
 
 
 type HostAddress struct {
 type HostAddress struct {
-	addr  *Address
-	addrs []*Address
+	addr  *cfgcommon.Address
+	addrs []*cfgcommon.Address
 }
 }
 
 
 // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
 // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
 func (h *HostAddress) UnmarshalJSON(data []byte) error {
 func (h *HostAddress) UnmarshalJSON(data []byte) error {
-	addr := new(Address)
-	var addrs []*Address
+	addr := new(cfgcommon.Address)
+	var addrs []*cfgcommon.Address
 	switch {
 	switch {
 	case json.Unmarshal(data, &addr) == nil:
 	case json.Unmarshal(data, &addr) == nil:
 		h.addr = addr
 		h.addr = addr
@@ -181,6 +192,21 @@ func getHostMapping(ha *HostAddress) *dns.Config_HostMapping {
 
 
 // Build implements Buildable
 // Build implements Buildable
 func (c *DNSConfig) Build() (*dns.Config, error) {
 func (c *DNSConfig) Build() (*dns.Config, error) {
+	cfgctx := cfgcommon.NewConfigureLoadingContext(context.Background())
+
+	if c.GeoLoader == "" {
+		c.GeoLoader = "standard"
+	}
+
+	if loader, err := geodata.GetGeoDataLoader(c.GeoLoader); err == nil {
+		cfgcommon.SetGeoDataLoader(cfgctx, loader)
+	} else {
+		return nil, newError("unable to create geo data loader ").Base(err)
+	}
+
+	cfgEnv := cfgcommon.GetConfigureLoadingEnvironment(cfgctx)
+	geoLoader := cfgEnv.GetGeoLoader()
+
 	config := &dns.Config{
 	config := &dns.Config{
 		Tag:             c.Tag,
 		Tag:             c.Tag,
 		DisableCache:    c.DisableCache,
 		DisableCache:    c.DisableCache,
@@ -205,6 +231,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
 	}
 	}
 
 
 	for _, server := range c.Servers {
 	for _, server := range c.Servers {
+		server.cfgctx = cfgctx
 		ns, err := server.Build()
 		ns, err := server.Build()
 		if err != nil {
 		if err != nil {
 			return nil, newError("failed to build nameserver").Base(err)
 			return nil, newError("failed to build nameserver").Base(err)
@@ -238,7 +265,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
 				if len(listName) == 0 {
 				if len(listName) == 0 {
 					return nil, newError("empty geosite rule: ", domain)
 					return nil, newError("empty geosite rule: ", domain)
 				}
 				}
-				geositeList, err := loadGeosite(listName)
+				geositeList, err := geoLoader.LoadGeosite(listName)
 				if err != nil {
 				if err != nil {
 					return nil, newError("failed to load geosite: ", listName).Base(err)
 					return nil, newError("failed to load geosite: ", listName).Base(err)
 				}
 				}
@@ -299,7 +326,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
 				}
 				}
 				filename := kv[0]
 				filename := kv[0]
 				list := kv[1]
 				list := kv[1]
-				geositeList, err := loadGeositeWithAttr(filename, list)
+				geositeList, err := geoLoader.LoadGeositeWithAttr(filename, list)
 				if err != nil {
 				if err != nil {
 					return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err)
 					return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err)
 				}
 				}

+ 4 - 3
infra/conf/dns_proxy.go

@@ -2,15 +2,16 @@ package conf
 
 
 import (
 import (
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/proxy/dns"
 	"github.com/v2fly/v2ray-core/v4/proxy/dns"
 )
 )
 
 
 type DNSOutboundConfig struct {
 type DNSOutboundConfig struct {
-	Network Network  `json:"network"`
-	Address *Address `json:"address"`
-	Port    uint16   `json:"port"`
+	Network cfgcommon.Network  `json:"network"`
+	Address *cfgcommon.Address `json:"address"`
+	Port    uint16             `json:"port"`
 }
 }
 
 
 func (c *DNSOutboundConfig) Build() (proto.Message, error) {
 func (c *DNSOutboundConfig) Build() (proto.Message, error) {

+ 2 - 0
infra/conf/dns_test.go

@@ -16,6 +16,8 @@ import (
 	"github.com/v2fly/v2ray-core/v4/common/platform"
 	"github.com/v2fly/v2ray-core/v4/common/platform"
 	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
 	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
 	"github.com/v2fly/v2ray-core/v4/infra/conf"
 	"github.com/v2fly/v2ray-core/v4/infra/conf"
+
+	_ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard"
 )
 )
 
 
 func init() {
 func init() {

+ 7 - 6
infra/conf/dokodemo.go

@@ -2,17 +2,18 @@ package conf
 
 
 import (
 import (
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
 
 
 	"github.com/v2fly/v2ray-core/v4/proxy/dokodemo"
 	"github.com/v2fly/v2ray-core/v4/proxy/dokodemo"
 )
 )
 
 
 type DokodemoConfig struct {
 type DokodemoConfig struct {
-	Host         *Address     `json:"address"`
-	PortValue    uint16       `json:"port"`
-	NetworkList  *NetworkList `json:"network"`
-	TimeoutValue uint32       `json:"timeout"`
-	Redirect     bool         `json:"followRedirect"`
-	UserLevel    uint32       `json:"userLevel"`
+	Host         *cfgcommon.Address     `json:"address"`
+	PortValue    uint16                 `json:"port"`
+	NetworkList  *cfgcommon.NetworkList `json:"network"`
+	TimeoutValue uint32                 `json:"timeout"`
+	Redirect     bool                   `json:"followRedirect"`
+	UserLevel    uint32                 `json:"userLevel"`
 }
 }
 
 
 func (v *DokodemoConfig) Build() (proto.Message, error) {
 func (v *DokodemoConfig) Build() (proto.Message, error) {

+ 51 - 0
infra/conf/geodata/attr.go

@@ -0,0 +1,51 @@
+package geodata
+
+import (
+	"strings"
+
+	"github.com/v2fly/v2ray-core/v4/app/router"
+)
+
+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 {
+		trimmedAttr := strings.ToLower(strings.TrimSpace(attr))
+		if len(trimmedAttr) == 0 {
+			continue
+		}
+		al.matcher = append(al.matcher, BooleanMatcher(trimmedAttr))
+	}
+	return al
+}
+
+type AttributeMatcher interface {
+	Match(*router.Domain) bool
+}
+
+type BooleanMatcher string
+
+func (m BooleanMatcher) Match(domain *router.Domain) bool {
+	for _, attr := range domain.Attribute {
+		if strings.EqualFold(attr.GetKey(), string(m)) {
+			return true
+		}
+	}
+	return false
+}

+ 9 - 0
infra/conf/geodata/errors.generated.go

@@ -0,0 +1,9 @@
+package geodata
+
+import "github.com/v2fly/v2ray-core/v4/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 83 - 0
infra/conf/geodata/geodata.go

@@ -0,0 +1,83 @@
+package geodata
+
+import (
+	"strings"
+
+	"github.com/v2fly/v2ray-core/v4/app/router"
+)
+
+type loader struct {
+	LoaderImplementation
+}
+
+func (l *loader) LoadGeosite(list string) ([]*router.Domain, error) {
+	return l.LoadGeositeWithAttr("geosite.dat", list)
+}
+
+func (l *loader) LoadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
+	parts := strings.Split(siteWithAttr, "@")
+	if len(parts) == 0 {
+		return nil, newError("empty rule")
+	}
+	list := strings.TrimSpace(parts[0])
+	attrVal := parts[1:]
+
+	if len(list) == 0 {
+		return nil, newError("empty listname in rule: ", siteWithAttr)
+	}
+
+	domains, err := l.LoadSite(file, list)
+	if err != nil {
+		return nil, err
+	}
+
+	attrs := parseAttrs(attrVal)
+	if attrs.IsEmpty() {
+		if strings.Contains(siteWithAttr, "@") {
+			newError("empty attribute list: ", siteWithAttr)
+		}
+		return domains, nil
+	}
+
+	filteredDomains := make([]*router.Domain, 0, len(domains))
+	hasAttrMatched := false
+	for _, domain := range domains {
+		if attrs.Match(domain) {
+			hasAttrMatched = true
+			filteredDomains = append(filteredDomains, domain)
+		}
+	}
+	if !hasAttrMatched {
+		newError("attribute match no rule: geosite:", siteWithAttr)
+	}
+
+	return filteredDomains, nil
+}
+
+func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) {
+	return l.LoadIP("geoip.dat", country)
+}
+
+var loaders map[string]func() LoaderImplementation
+
+func RegisterGeoDataLoaderImplementationCreator(name string, loader func() LoaderImplementation) {
+	if loaders == nil {
+		loaders = map[string]func() LoaderImplementation{}
+	}
+	loaders[name] = loader
+}
+
+func getGeoDataLoaderImplementation(name string) (LoaderImplementation, error) {
+	if geoLoader, ok := loaders[name]; ok {
+		return geoLoader(), nil
+	}
+	return nil, newError("unable to locate GeoData loader ", name)
+}
+
+func GetGeoDataLoader(name string) (Loader, error) {
+	if loadImpl, err := getGeoDataLoaderImplementation(name); err == nil {
+		return &loader{loadImpl}, nil
+	} else { // nolint:golint
+		return nil, err
+	}
+}

+ 17 - 0
infra/conf/geodata/geodataproto.go

@@ -0,0 +1,17 @@
+package geodata
+
+import "github.com/v2fly/v2ray-core/v4/app/router"
+
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
+
+type LoaderImplementation interface {
+	LoadSite(filename, list string) ([]*router.Domain, error)
+	LoadIP(filename, country string) ([]*router.CIDR, error)
+}
+
+type Loader interface {
+	LoaderImplementation
+	LoadGeosite(list string) ([]*router.Domain, error)
+	LoadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error)
+	LoadGeoIP(country string) ([]*router.CIDR, error)
+}

+ 9 - 0
infra/conf/geodata/standard/errors.generated.go

@@ -0,0 +1,9 @@
+package standard
+
+import "github.com/v2fly/v2ray-core/v4/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 69 - 0
infra/conf/geodata/standard/standard.go

@@ -0,0 +1,69 @@
+package standard
+
+import (
+	"strings"
+
+	"github.com/v2fly/v2ray-core/v4/infra/conf/geodata"
+
+	"github.com/golang/protobuf/proto"
+
+	"github.com/v2fly/v2ray-core/v4/app/router"
+	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
+)
+
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
+
+func loadIP(filename, country string) ([]*router.CIDR, error) {
+	geoipBytes, err := filesystem.ReadAsset(filename)
+	if err != nil {
+		return nil, newError("failed to open file: ", filename).Base(err)
+	}
+	var geoipList router.GeoIPList
+	if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
+		return nil, err
+	}
+
+	for _, geoip := range geoipList.Entry {
+		if strings.EqualFold(geoip.CountryCode, country) {
+			return geoip.Cidr, nil
+		}
+	}
+
+	return nil, newError("country not found in ", filename, ": ", country)
+}
+
+func loadSite(filename, list string) ([]*router.Domain, error) {
+	geositeBytes, err := filesystem.ReadAsset(filename)
+	if err != nil {
+		return nil, newError("failed to open file: ", filename).Base(err)
+	}
+	var geositeList router.GeoSiteList
+	if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
+		return nil, err
+	}
+
+	for _, site := range geositeList.Entry {
+		if strings.EqualFold(site.CountryCode, list) {
+			return site.Domain, nil
+		}
+	}
+
+	return nil, newError("list not found in ", filename, ": ", list)
+}
+
+type standardLoader struct {
+}
+
+func (d standardLoader) LoadSite(filename, list string) ([]*router.Domain, error) {
+	return loadSite(filename, list)
+}
+
+func (d standardLoader) LoadIP(filename, country string) ([]*router.CIDR, error) {
+	return loadIP(filename, country)
+}
+
+func init() {
+	geodata.RegisterGeoDataLoaderImplementationCreator("standard", func() geodata.LoaderImplementation {
+		return standardLoader{}
+	})
+}

+ 5 - 3
infra/conf/http.go

@@ -3,6 +3,8 @@ package conf
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
@@ -47,9 +49,9 @@ func (c *HTTPServerConfig) Build() (proto.Message, error) {
 }
 }
 
 
 type HTTPRemoteConfig struct {
 type HTTPRemoteConfig struct {
-	Address *Address          `json:"address"`
-	Port    uint16            `json:"port"`
-	Users   []json.RawMessage `json:"users"`
+	Address *cfgcommon.Address `json:"address"`
+	Port    uint16             `json:"port"`
+	Users   []json.RawMessage  `json:"users"`
 }
 }
 type HTTPClientConfig struct {
 type HTTPClientConfig struct {
 	Servers []*HTTPRemoteConfig `json:"servers"`
 	Servers []*HTTPRemoteConfig `json:"servers"`

+ 1 - 0
infra/conf/observatory.go

@@ -2,6 +2,7 @@ package conf
 
 
 import (
 import (
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
+
 	"github.com/v2fly/v2ray-core/v4/app/observatory"
 	"github.com/v2fly/v2ray-core/v4/app/observatory"
 )
 )
 
 

+ 21 - 504
infra/conf/router.go

@@ -1,15 +1,15 @@
 package conf
 package conf
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
-	"strconv"
 	"strings"
 	"strings"
 
 
-	"github.com/golang/protobuf/proto"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/geodata"
+	rule2 "github.com/v2fly/v2ray-core/v4/infra/conf/rule"
 
 
 	"github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/app/router"
-	"github.com/v2fly/v2ray-core/v4/common/net"
-	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
 )
 )
 
 
 type RouterRulesConfig struct {
 type RouterRulesConfig struct {
@@ -24,9 +24,9 @@ type StrategyConfig struct {
 }
 }
 
 
 type BalancingRule struct {
 type BalancingRule struct {
-	Tag       string         `json:"tag"`
-	Selectors StringList     `json:"selector"`
-	Strategy  StrategyConfig `json:"strategy"`
+	Tag       string               `json:"tag"`
+	Selectors cfgcommon.StringList `json:"selector"`
+	Strategy  StrategyConfig       `json:"strategy"`
 }
 }
 
 
 func (r *BalancingRule) Build() (*router.BalancingRule, error) {
 func (r *BalancingRule) Build() (*router.BalancingRule, error) {
@@ -61,6 +61,7 @@ type RouterConfig struct {
 	Balancers      []*BalancingRule   `json:"balancers"`
 	Balancers      []*BalancingRule   `json:"balancers"`
 
 
 	DomainMatcher string `json:"domainMatcher"`
 	DomainMatcher string `json:"domainMatcher"`
+	GeoLoader     string `json:"geoLoader"`
 }
 }
 
 
 func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
 func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
@@ -87,6 +88,18 @@ func (c *RouterConfig) Build() (*router.Config, error) {
 	config := new(router.Config)
 	config := new(router.Config)
 	config.DomainStrategy = c.getDomainStrategy()
 	config.DomainStrategy = c.getDomainStrategy()
 
 
+	cfgctx := cfgcommon.NewConfigureLoadingContext(context.Background())
+
+	if c.GeoLoader == "" {
+		c.GeoLoader = "standard"
+	}
+
+	if loader, err := geodata.GetGeoDataLoader(c.GeoLoader); err == nil {
+		cfgcommon.SetGeoDataLoader(cfgctx, loader)
+	} else {
+		return nil, newError("unable to create geo data loader ").Base(err)
+	}
+
 	var rawRuleList []json.RawMessage
 	var rawRuleList []json.RawMessage
 	if c != nil {
 	if c != nil {
 		rawRuleList = c.RuleList
 		rawRuleList = c.RuleList
@@ -97,7 +110,7 @@ func (c *RouterConfig) Build() (*router.Config, error) {
 	}
 	}
 
 
 	for _, rawRule := range rawRuleList {
 	for _, rawRule := range rawRuleList {
-		rule, err := ParseRule(rawRule)
+		rule, err := rule2.ParseRule(cfgctx, rawRule)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
@@ -117,499 +130,3 @@ func (c *RouterConfig) Build() (*router.Config, error) {
 	}
 	}
 	return config, nil
 	return config, nil
 }
 }
-
-type RouterRule struct {
-	Type        string `json:"type"`
-	OutboundTag string `json:"outboundTag"`
-	BalancerTag string `json:"balancerTag"`
-
-	DomainMatcher string `json:"domainMatcher"`
-}
-
-func ParseIP(s string) (*router.CIDR, error) {
-	var addr, mask string
-	i := strings.Index(s, "/")
-	if i < 0 {
-		addr = s
-	} else {
-		addr = s[:i]
-		mask = s[i+1:]
-	}
-	ip := net.ParseAddress(addr)
-	switch ip.Family() {
-	case net.AddressFamilyIPv4:
-		bits := uint32(32)
-		if len(mask) > 0 {
-			bits64, err := strconv.ParseUint(mask, 10, 32)
-			if err != nil {
-				return nil, newError("invalid network mask for router: ", mask).Base(err)
-			}
-			bits = uint32(bits64)
-		}
-		if bits > 32 {
-			return nil, newError("invalid network mask for router: ", bits)
-		}
-		return &router.CIDR{
-			Ip:     []byte(ip.IP()),
-			Prefix: bits,
-		}, nil
-	case net.AddressFamilyIPv6:
-		bits := uint32(128)
-		if len(mask) > 0 {
-			bits64, err := strconv.ParseUint(mask, 10, 32)
-			if err != nil {
-				return nil, newError("invalid network mask for router: ", mask).Base(err)
-			}
-			bits = uint32(bits64)
-		}
-		if bits > 128 {
-			return nil, newError("invalid network mask for router: ", bits)
-		}
-		return &router.CIDR{
-			Ip:     []byte(ip.IP()),
-			Prefix: bits,
-		}, nil
-	default:
-		return nil, newError("unsupported address for router: ", s)
-	}
-}
-
-func loadGeoIP(country string) ([]*router.CIDR, error) {
-	return loadIP("geoip.dat", country)
-}
-
-func loadIP(filename, country string) ([]*router.CIDR, error) {
-	geoipBytes, err := filesystem.ReadAsset(filename)
-	if err != nil {
-		return nil, newError("failed to open file: ", filename).Base(err)
-	}
-	var geoipList router.GeoIPList
-	if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
-		return nil, err
-	}
-
-	for _, geoip := range geoipList.Entry {
-		if strings.EqualFold(geoip.CountryCode, country) {
-			return geoip.Cidr, nil
-		}
-	}
-
-	return nil, newError("country not found in ", filename, ": ", country)
-}
-
-func loadSite(filename, list string) ([]*router.Domain, error) {
-	geositeBytes, err := filesystem.ReadAsset(filename)
-	if err != nil {
-		return nil, newError("failed to open file: ", filename).Base(err)
-	}
-	var geositeList router.GeoSiteList
-	if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
-		return nil, err
-	}
-
-	for _, site := range geositeList.Entry {
-		if strings.EqualFold(site.CountryCode, list) {
-			return site.Domain, nil
-		}
-	}
-
-	return nil, newError("list not found in ", filename, ": ", list)
-}
-
-type AttributeMatcher interface {
-	Match(*router.Domain) bool
-}
-
-type BooleanMatcher string
-
-func (m BooleanMatcher) Match(domain *router.Domain) bool {
-	for _, attr := range domain.Attribute {
-		if strings.EqualFold(attr.GetKey(), 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 {
-		trimmedAttr := strings.ToLower(strings.TrimSpace(attr))
-		if len(trimmedAttr) == 0 {
-			continue
-		}
-		al.matcher = append(al.matcher, BooleanMatcher(trimmedAttr))
-	}
-	return al
-}
-
-func loadGeosite(list string) ([]*router.Domain, error) {
-	return loadGeositeWithAttr("geosite.dat", list)
-}
-
-func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
-	parts := strings.Split(siteWithAttr, "@")
-	if len(parts) == 0 {
-		return nil, newError("empty rule")
-	}
-	list := strings.TrimSpace(parts[0])
-	attrVal := parts[1:]
-
-	if len(list) == 0 {
-		return nil, newError("empty listname in rule: ", siteWithAttr)
-	}
-
-	domains, err := loadSite(file, list)
-	if err != nil {
-		return nil, err
-	}
-
-	attrs := parseAttrs(attrVal)
-	if attrs.IsEmpty() {
-		if strings.Contains(siteWithAttr, "@") {
-			newError("empty attribute list: ", siteWithAttr)
-		}
-		return domains, nil
-	}
-
-	filteredDomains := make([]*router.Domain, 0, len(domains))
-	hasAttrMatched := false
-	for _, domain := range domains {
-		if attrs.Match(domain) {
-			hasAttrMatched = true
-			filteredDomains = append(filteredDomains, domain)
-		}
-	}
-	if !hasAttrMatched {
-		newError("attribute match no rule: geosite:", siteWithAttr)
-	}
-
-	return filteredDomains, nil
-}
-
-func parseDomainRule(domain string) ([]*router.Domain, error) {
-	if strings.HasPrefix(domain, "geosite:") {
-		list := domain[8:]
-		if len(list) == 0 {
-			return nil, newError("empty listname in rule: ", domain)
-		}
-		domains, err := loadGeosite(list)
-		if err != nil {
-			return nil, newError("failed to load geosite: ", list).Base(err)
-		}
-
-		return domains, nil
-	}
-
-	var isExtDatFile = 0
-	{
-		const prefix = "ext:"
-		if strings.HasPrefix(domain, prefix) {
-			isExtDatFile = len(prefix)
-		}
-		const prefixQualified = "ext-domain:"
-		if strings.HasPrefix(domain, prefixQualified) {
-			isExtDatFile = len(prefixQualified)
-		}
-	}
-
-	if isExtDatFile != 0 {
-		kv := strings.Split(domain[isExtDatFile:], ":")
-		if len(kv) != 2 {
-			return nil, newError("invalid external resource: ", domain)
-		}
-		filename := kv[0]
-		list := kv[1]
-		domains, err := loadGeositeWithAttr(filename, list)
-		if err != nil {
-			return nil, newError("failed to load external geosite: ", list, " from ", filename).Base(err)
-		}
-
-		return domains, nil
-	}
-
-	domainRule := new(router.Domain)
-	switch {
-	case strings.HasPrefix(domain, "regexp:"):
-		regexpVal := domain[7:]
-		if len(regexpVal) == 0 {
-			return nil, newError("empty regexp type of rule: ", domain)
-		}
-		domainRule.Type = router.Domain_Regex
-		domainRule.Value = regexpVal
-
-	case strings.HasPrefix(domain, "domain:"):
-		domainName := domain[7:]
-		if len(domainName) == 0 {
-			return nil, newError("empty domain type of rule: ", domain)
-		}
-		domainRule.Type = router.Domain_Domain
-		domainRule.Value = domainName
-
-	case strings.HasPrefix(domain, "full:"):
-		fullVal := domain[5:]
-		if len(fullVal) == 0 {
-			return nil, newError("empty full domain type of rule: ", domain)
-		}
-		domainRule.Type = router.Domain_Full
-		domainRule.Value = fullVal
-
-	case strings.HasPrefix(domain, "keyword:"):
-		keywordVal := domain[8:]
-		if len(keywordVal) == 0 {
-			return nil, newError("empty keyword type of rule: ", domain)
-		}
-		domainRule.Type = router.Domain_Plain
-		domainRule.Value = keywordVal
-
-	case strings.HasPrefix(domain, "dotless:"):
-		domainRule.Type = router.Domain_Regex
-		switch substr := domain[8:]; {
-		case substr == "":
-			domainRule.Value = "^[^.]*$"
-		case !strings.Contains(substr, "."):
-			domainRule.Value = "^[^.]*" + substr + "[^.]*$"
-		default:
-			return nil, newError("substr in dotless rule should not contain a dot: ", substr)
-		}
-
-	default:
-		domainRule.Type = router.Domain_Plain
-		domainRule.Value = domain
-	}
-	return []*router.Domain{domainRule}, nil
-}
-
-func toCidrList(ips StringList) ([]*router.GeoIP, error) {
-	var geoipList []*router.GeoIP
-	var customCidrs []*router.CIDR
-
-	for _, ip := range ips {
-		if strings.HasPrefix(ip, "geoip:") {
-			country := ip[6:]
-			isReverseMatch := false
-			if strings.HasPrefix(ip, "geoip:!") {
-				country = ip[7:]
-				isReverseMatch = true
-			}
-			if len(country) == 0 {
-				return nil, newError("empty country name in rule")
-			}
-			geoip, err := loadGeoIP(country)
-			if err != nil {
-				return nil, newError("failed to load geoip: ", country).Base(err)
-			}
-
-			geoipList = append(geoipList, &router.GeoIP{
-				CountryCode:  strings.ToUpper(country),
-				Cidr:         geoip,
-				ReverseMatch: isReverseMatch,
-			})
-
-			continue
-		}
-
-		var isExtDatFile = 0
-		{
-			const prefix = "ext:"
-			if strings.HasPrefix(ip, prefix) {
-				isExtDatFile = len(prefix)
-			}
-			const prefixQualified = "ext-ip:"
-			if strings.HasPrefix(ip, prefixQualified) {
-				isExtDatFile = len(prefixQualified)
-			}
-		}
-
-		if isExtDatFile != 0 {
-			kv := strings.Split(ip[isExtDatFile:], ":")
-			if len(kv) != 2 {
-				return nil, newError("invalid external resource: ", ip)
-			}
-
-			filename := kv[0]
-			country := kv[1]
-			if len(filename) == 0 || len(country) == 0 {
-				return nil, newError("empty filename or empty country in rule")
-			}
-
-			isReverseMatch := false
-			if strings.HasPrefix(country, "!") {
-				country = country[1:]
-				isReverseMatch = true
-			}
-			geoip, err := loadIP(filename, country)
-			if err != nil {
-				return nil, newError("failed to load geoip: ", country, " from ", filename).Base(err)
-			}
-
-			geoipList = append(geoipList, &router.GeoIP{
-				CountryCode:  strings.ToUpper(filename + "_" + country),
-				Cidr:         geoip,
-				ReverseMatch: isReverseMatch,
-			})
-
-			continue
-		}
-
-		ipRule, err := ParseIP(ip)
-		if err != nil {
-			return nil, newError("invalid IP: ", ip).Base(err)
-		}
-		customCidrs = append(customCidrs, ipRule)
-	}
-
-	if len(customCidrs) > 0 {
-		geoipList = append(geoipList, &router.GeoIP{
-			Cidr: customCidrs,
-		})
-	}
-
-	return geoipList, nil
-}
-
-func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
-	type RawFieldRule struct {
-		RouterRule
-		Domain     *StringList  `json:"domain"`
-		Domains    *StringList  `json:"domains"`
-		IP         *StringList  `json:"ip"`
-		Port       *PortList    `json:"port"`
-		Network    *NetworkList `json:"network"`
-		SourceIP   *StringList  `json:"source"`
-		SourcePort *PortList    `json:"sourcePort"`
-		User       *StringList  `json:"user"`
-		InboundTag *StringList  `json:"inboundTag"`
-		Protocols  *StringList  `json:"protocol"`
-		Attributes string       `json:"attrs"`
-	}
-	rawFieldRule := new(RawFieldRule)
-	err := json.Unmarshal(msg, rawFieldRule)
-	if err != nil {
-		return nil, err
-	}
-
-	rule := new(router.RoutingRule)
-	switch {
-	case len(rawFieldRule.OutboundTag) > 0:
-		rule.TargetTag = &router.RoutingRule_Tag{
-			Tag: rawFieldRule.OutboundTag,
-		}
-	case len(rawFieldRule.BalancerTag) > 0:
-		rule.TargetTag = &router.RoutingRule_BalancingTag{
-			BalancingTag: rawFieldRule.BalancerTag,
-		}
-	default:
-		return nil, newError("neither outboundTag nor balancerTag is specified in routing rule")
-	}
-
-	if rawFieldRule.DomainMatcher != "" {
-		rule.DomainMatcher = rawFieldRule.DomainMatcher
-	}
-
-	if rawFieldRule.Domain != nil {
-		for _, domain := range *rawFieldRule.Domain {
-			rules, err := parseDomainRule(domain)
-			if err != nil {
-				return nil, newError("failed to parse domain rule: ", domain).Base(err)
-			}
-			rule.Domain = append(rule.Domain, rules...)
-		}
-	}
-
-	if rawFieldRule.Domains != nil {
-		for _, domain := range *rawFieldRule.Domains {
-			rules, err := parseDomainRule(domain)
-			if err != nil {
-				return nil, newError("failed to parse domain rule: ", domain).Base(err)
-			}
-			rule.Domain = append(rule.Domain, rules...)
-		}
-	}
-
-	if rawFieldRule.IP != nil {
-		geoipList, err := toCidrList(*rawFieldRule.IP)
-		if err != nil {
-			return nil, err
-		}
-		rule.Geoip = geoipList
-	}
-
-	if rawFieldRule.Port != nil {
-		rule.PortList = rawFieldRule.Port.Build()
-	}
-
-	if rawFieldRule.Network != nil {
-		rule.Networks = rawFieldRule.Network.Build()
-	}
-
-	if rawFieldRule.SourceIP != nil {
-		geoipList, err := toCidrList(*rawFieldRule.SourceIP)
-		if err != nil {
-			return nil, err
-		}
-		rule.SourceGeoip = geoipList
-	}
-
-	if rawFieldRule.SourcePort != nil {
-		rule.SourcePortList = rawFieldRule.SourcePort.Build()
-	}
-
-	if rawFieldRule.User != nil {
-		for _, s := range *rawFieldRule.User {
-			rule.UserEmail = append(rule.UserEmail, s)
-		}
-	}
-
-	if rawFieldRule.InboundTag != nil {
-		for _, s := range *rawFieldRule.InboundTag {
-			rule.InboundTag = append(rule.InboundTag, s)
-		}
-	}
-
-	if rawFieldRule.Protocols != nil {
-		for _, s := range *rawFieldRule.Protocols {
-			rule.Protocol = append(rule.Protocol, s)
-		}
-	}
-
-	if len(rawFieldRule.Attributes) > 0 {
-		rule.Attributes = rawFieldRule.Attributes
-	}
-
-	return rule, nil
-}
-
-func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) {
-	rawRule := new(RouterRule)
-	err := json.Unmarshal(msg, rawRule)
-	if err != nil {
-		return nil, newError("invalid router rule").Base(err)
-	}
-	if strings.EqualFold(rawRule.Type, "field") {
-		fieldrule, err := parseFieldRule(msg)
-		if err != nil {
-			return nil, newError("invalid field rule").Base(err)
-		}
-		return fieldrule, nil
-	}
-
-	return nil, newError("unknown router rule type: ", rawRule.Type)
-}

+ 0 - 51
infra/conf/router_test.go

@@ -2,67 +2,16 @@ package conf_test
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"errors"
-	"io/fs"
-	"os"
-	"path/filepath"
 	"testing"
 	"testing"
 	_ "unsafe"
 	_ "unsafe"
 
 
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/app/router"
 	"github.com/v2fly/v2ray-core/v4/app/router"
-	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/net"
-	"github.com/v2fly/v2ray-core/v4/common/platform"
-	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
 	. "github.com/v2fly/v2ray-core/v4/infra/conf"
 	. "github.com/v2fly/v2ray-core/v4/infra/conf"
 )
 )
 
 
-func init() {
-	wd, err := os.Getwd()
-	common.Must(err)
-
-	tempPath := filepath.Join(wd, "..", "..", "testing", "temp")
-	geoipPath := filepath.Join(tempPath, "geoip.dat")
-
-	os.Setenv("v2ray.location.asset", tempPath)
-
-	common.Must(os.MkdirAll(tempPath, 0755))
-
-	if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && errors.Is(err, fs.ErrNotExist) {
-		if _, err := os.Stat(geoipPath); err != nil && errors.Is(err, fs.ErrNotExist) {
-			geoipBytes, err := common.FetchHTTPContent(geoipURL)
-			common.Must(err)
-			common.Must(filesystem.WriteFile(geoipPath, geoipBytes))
-		}
-	}
-}
-
-//go:linkname toCidrList github.com/v2fly/v2ray-core/v4/infra/conf.toCidrList
-func toCidrList(ips StringList) ([]*router.GeoIP, error)
-
-func TestToCidrList(t *testing.T) {
-	t.Log(os.Getenv("v2ray.location.asset"))
-
-	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoiptestrouter.dat"), platform.GetAssetLocation("geoip.dat")))
-
-	ips := StringList([]string{
-		"geoip:us",
-		"geoip:cn",
-		"geoip:!cn",
-		"ext:geoiptestrouter.dat:!cn",
-		"ext:geoiptestrouter.dat:ca",
-		"ext-ip:geoiptestrouter.dat:!cn",
-		"ext-ip:geoiptestrouter.dat:!ca",
-	})
-
-	_, err := toCidrList(ips)
-	if err != nil {
-		t.Fatalf("Failed to parse geoip list, got %s", err)
-	}
-}
-
 func TestRouterConfig(t *testing.T) {
 func TestRouterConfig(t *testing.T) {
 	createParser := func() func(string) (proto.Message, error) {
 	createParser := func() func(string) (proto.Message, error) {
 		return func(s string) (proto.Message, error) {
 		return func(s string) (proto.Message, error) {

+ 9 - 0
infra/conf/rule/errors.generated.go

@@ -0,0 +1,9 @@
+package rule
+
+import "github.com/v2fly/v2ray-core/v4/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+	return errors.New(values...).WithPathObj(errPathObjHolder{})
+}

+ 394 - 0
infra/conf/rule/rule.go

@@ -0,0 +1,394 @@
+package rule
+
+import (
+	"context"
+	"encoding/json"
+	"strconv"
+	"strings"
+
+	"github.com/v2fly/v2ray-core/v4/app/router"
+	"github.com/v2fly/v2ray-core/v4/common/net"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+)
+
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
+
+func parseDomainRule(ctx context.Context, domain string) ([]*router.Domain, error) {
+	cfgEnv := cfgcommon.GetConfigureLoadingEnvironment(ctx)
+	geoLoader := cfgEnv.GetGeoLoader()
+
+	if strings.HasPrefix(domain, "geosite:") {
+		list := domain[8:]
+		if len(list) == 0 {
+			return nil, newError("empty listname in rule: ", domain)
+		}
+		domains, err := geoLoader.LoadGeosite(list)
+		if err != nil {
+			return nil, newError("failed to load geosite: ", list).Base(err)
+		}
+
+		return domains, nil
+	}
+
+	var isExtDatFile = 0
+	{
+		const prefix = "ext:"
+		if strings.HasPrefix(domain, prefix) {
+			isExtDatFile = len(prefix)
+		}
+		const prefixQualified = "ext-domain:"
+		if strings.HasPrefix(domain, prefixQualified) {
+			isExtDatFile = len(prefixQualified)
+		}
+	}
+
+	if isExtDatFile != 0 {
+		kv := strings.Split(domain[isExtDatFile:], ":")
+		if len(kv) != 2 {
+			return nil, newError("invalid external resource: ", domain)
+		}
+		filename := kv[0]
+		list := kv[1]
+		domains, err := geoLoader.LoadGeositeWithAttr(filename, list)
+		if err != nil {
+			return nil, newError("failed to load external geosite: ", list, " from ", filename).Base(err)
+		}
+
+		return domains, nil
+	}
+
+	domainRule := new(router.Domain)
+	switch {
+	case strings.HasPrefix(domain, "regexp:"):
+		regexpVal := domain[7:]
+		if len(regexpVal) == 0 {
+			return nil, newError("empty regexp type of rule: ", domain)
+		}
+		domainRule.Type = router.Domain_Regex
+		domainRule.Value = regexpVal
+
+	case strings.HasPrefix(domain, "domain:"):
+		domainName := domain[7:]
+		if len(domainName) == 0 {
+			return nil, newError("empty domain type of rule: ", domain)
+		}
+		domainRule.Type = router.Domain_Domain
+		domainRule.Value = domainName
+
+	case strings.HasPrefix(domain, "full:"):
+		fullVal := domain[5:]
+		if len(fullVal) == 0 {
+			return nil, newError("empty full domain type of rule: ", domain)
+		}
+		domainRule.Type = router.Domain_Full
+		domainRule.Value = fullVal
+
+	case strings.HasPrefix(domain, "keyword:"):
+		keywordVal := domain[8:]
+		if len(keywordVal) == 0 {
+			return nil, newError("empty keyword type of rule: ", domain)
+		}
+		domainRule.Type = router.Domain_Plain
+		domainRule.Value = keywordVal
+
+	case strings.HasPrefix(domain, "dotless:"):
+		domainRule.Type = router.Domain_Regex
+		switch substr := domain[8:]; {
+		case substr == "":
+			domainRule.Value = "^[^.]*$"
+		case !strings.Contains(substr, "."):
+			domainRule.Value = "^[^.]*" + substr + "[^.]*$"
+		default:
+			return nil, newError("substr in dotless rule should not contain a dot: ", substr)
+		}
+
+	default:
+		domainRule.Type = router.Domain_Plain
+		domainRule.Value = domain
+	}
+	return []*router.Domain{domainRule}, nil
+}
+
+func toCidrList(ctx context.Context, ips cfgcommon.StringList) ([]*router.GeoIP, error) {
+	cfgEnv := cfgcommon.GetConfigureLoadingEnvironment(ctx)
+	geoLoader := cfgEnv.GetGeoLoader()
+
+	var geoipList []*router.GeoIP
+	var customCidrs []*router.CIDR
+
+	for _, ip := range ips {
+		if strings.HasPrefix(ip, "geoip:") {
+			country := ip[6:]
+			isReverseMatch := false
+			if strings.HasPrefix(ip, "geoip:!") {
+				country = ip[7:]
+				isReverseMatch = true
+			}
+			if len(country) == 0 {
+				return nil, newError("empty country name in rule")
+			}
+			geoip, err := geoLoader.LoadGeoIP(country)
+			if err != nil {
+				return nil, newError("failed to load geoip: ", country).Base(err)
+			}
+
+			geoipList = append(geoipList, &router.GeoIP{
+				CountryCode:  strings.ToUpper(country),
+				Cidr:         geoip,
+				ReverseMatch: isReverseMatch,
+			})
+
+			continue
+		}
+
+		var isExtDatFile = 0
+		{
+			const prefix = "ext:"
+			if strings.HasPrefix(ip, prefix) {
+				isExtDatFile = len(prefix)
+			}
+			const prefixQualified = "ext-ip:"
+			if strings.HasPrefix(ip, prefixQualified) {
+				isExtDatFile = len(prefixQualified)
+			}
+		}
+
+		if isExtDatFile != 0 {
+			kv := strings.Split(ip[isExtDatFile:], ":")
+			if len(kv) != 2 {
+				return nil, newError("invalid external resource: ", ip)
+			}
+
+			filename := kv[0]
+			country := kv[1]
+			if len(filename) == 0 || len(country) == 0 {
+				return nil, newError("empty filename or empty country in rule")
+			}
+
+			isReverseMatch := false
+			if strings.HasPrefix(country, "!") {
+				country = country[1:]
+				isReverseMatch = true
+			}
+			geoip, err := geoLoader.LoadIP(filename, country)
+			if err != nil {
+				return nil, newError("failed to load geoip: ", country, " from ", filename).Base(err)
+			}
+
+			geoipList = append(geoipList, &router.GeoIP{
+				CountryCode:  strings.ToUpper(filename + "_" + country),
+				Cidr:         geoip,
+				ReverseMatch: isReverseMatch,
+			})
+
+			continue
+		}
+
+		ipRule, err := ParseIP(ip)
+		if err != nil {
+			return nil, newError("invalid IP: ", ip).Base(err)
+		}
+		customCidrs = append(customCidrs, ipRule)
+	}
+
+	if len(customCidrs) > 0 {
+		geoipList = append(geoipList, &router.GeoIP{
+			Cidr: customCidrs,
+		})
+	}
+
+	return geoipList, nil
+}
+
+func parseFieldRule(ctx context.Context, msg json.RawMessage) (*router.RoutingRule, error) {
+	type RawFieldRule struct {
+		RouterRule
+		Domain     *cfgcommon.StringList  `json:"domain"`
+		Domains    *cfgcommon.StringList  `json:"domains"`
+		IP         *cfgcommon.StringList  `json:"ip"`
+		Port       *cfgcommon.PortList    `json:"port"`
+		Network    *cfgcommon.NetworkList `json:"network"`
+		SourceIP   *cfgcommon.StringList  `json:"source"`
+		SourcePort *cfgcommon.PortList    `json:"sourcePort"`
+		User       *cfgcommon.StringList  `json:"user"`
+		InboundTag *cfgcommon.StringList  `json:"inboundTag"`
+		Protocols  *cfgcommon.StringList  `json:"protocol"`
+		Attributes string                 `json:"attrs"`
+	}
+	rawFieldRule := new(RawFieldRule)
+	err := json.Unmarshal(msg, rawFieldRule)
+	if err != nil {
+		return nil, err
+	}
+
+	rule := new(router.RoutingRule)
+	switch {
+	case len(rawFieldRule.OutboundTag) > 0:
+		rule.TargetTag = &router.RoutingRule_Tag{
+			Tag: rawFieldRule.OutboundTag,
+		}
+	case len(rawFieldRule.BalancerTag) > 0:
+		rule.TargetTag = &router.RoutingRule_BalancingTag{
+			BalancingTag: rawFieldRule.BalancerTag,
+		}
+	default:
+		return nil, newError("neither outboundTag nor balancerTag is specified in routing rule")
+	}
+
+	if rawFieldRule.DomainMatcher != "" {
+		rule.DomainMatcher = rawFieldRule.DomainMatcher
+	}
+
+	if rawFieldRule.Domain != nil {
+		for _, domain := range *rawFieldRule.Domain {
+			rules, err := parseDomainRule(ctx, domain)
+			if err != nil {
+				return nil, newError("failed to parse domain rule: ", domain).Base(err)
+			}
+			rule.Domain = append(rule.Domain, rules...)
+		}
+	}
+
+	if rawFieldRule.Domains != nil {
+		for _, domain := range *rawFieldRule.Domains {
+			rules, err := parseDomainRule(ctx, domain)
+			if err != nil {
+				return nil, newError("failed to parse domain rule: ", domain).Base(err)
+			}
+			rule.Domain = append(rule.Domain, rules...)
+		}
+	}
+
+	if rawFieldRule.IP != nil {
+		geoipList, err := toCidrList(ctx, *rawFieldRule.IP)
+		if err != nil {
+			return nil, err
+		}
+		rule.Geoip = geoipList
+	}
+
+	if rawFieldRule.Port != nil {
+		rule.PortList = rawFieldRule.Port.Build()
+	}
+
+	if rawFieldRule.Network != nil {
+		rule.Networks = rawFieldRule.Network.Build()
+	}
+
+	if rawFieldRule.SourceIP != nil {
+		geoipList, err := toCidrList(ctx, *rawFieldRule.SourceIP)
+		if err != nil {
+			return nil, err
+		}
+		rule.SourceGeoip = geoipList
+	}
+
+	if rawFieldRule.SourcePort != nil {
+		rule.SourcePortList = rawFieldRule.SourcePort.Build()
+	}
+
+	if rawFieldRule.User != nil {
+		for _, s := range *rawFieldRule.User {
+			rule.UserEmail = append(rule.UserEmail, s)
+		}
+	}
+
+	if rawFieldRule.InboundTag != nil {
+		for _, s := range *rawFieldRule.InboundTag {
+			rule.InboundTag = append(rule.InboundTag, s)
+		}
+	}
+
+	if rawFieldRule.Protocols != nil {
+		for _, s := range *rawFieldRule.Protocols {
+			rule.Protocol = append(rule.Protocol, s)
+		}
+	}
+
+	if len(rawFieldRule.Attributes) > 0 {
+		rule.Attributes = rawFieldRule.Attributes
+	}
+
+	return rule, nil
+}
+
+func ParseRule(ctx context.Context, msg json.RawMessage) (*router.RoutingRule, error) {
+	rawRule := new(RouterRule)
+	err := json.Unmarshal(msg, rawRule)
+	if err != nil {
+		return nil, newError("invalid router rule").Base(err)
+	}
+	if strings.EqualFold(rawRule.Type, "field") {
+		fieldrule, err := parseFieldRule(ctx, msg)
+		if err != nil {
+			return nil, newError("invalid field rule").Base(err)
+		}
+		return fieldrule, nil
+	}
+
+	return nil, newError("unknown router rule type: ", rawRule.Type)
+}
+
+func ParseIP(s string) (*router.CIDR, error) {
+	var addr, mask string
+	i := strings.Index(s, "/")
+	if i < 0 {
+		addr = s
+	} else {
+		addr = s[:i]
+		mask = s[i+1:]
+	}
+	ip := net.ParseAddress(addr)
+	switch ip.Family() {
+	case net.AddressFamilyIPv4:
+		bits := uint32(32)
+		if len(mask) > 0 {
+			bits64, err := strconv.ParseUint(mask, 10, 32)
+			if err != nil {
+				return nil, newError("invalid network mask for router: ", mask).Base(err)
+			}
+			bits = uint32(bits64)
+		}
+		if bits > 32 {
+			return nil, newError("invalid network mask for router: ", bits)
+		}
+		return &router.CIDR{
+			Ip:     []byte(ip.IP()),
+			Prefix: bits,
+		}, nil
+	case net.AddressFamilyIPv6:
+		bits := uint32(128)
+		if len(mask) > 0 {
+			bits64, err := strconv.ParseUint(mask, 10, 32)
+			if err != nil {
+				return nil, newError("invalid network mask for router: ", mask).Base(err)
+			}
+			bits = uint32(bits64)
+		}
+		if bits > 128 {
+			return nil, newError("invalid network mask for router: ", bits)
+		}
+		return &router.CIDR{
+			Ip:     []byte(ip.IP()),
+			Prefix: bits,
+		}, nil
+	default:
+		return nil, newError("unsupported address for router: ", s)
+	}
+}
+
+func ParseDomainRule(ctx context.Context, domain string) ([]*router.Domain, error) {
+	return parseDomainRule(ctx, domain)
+}
+
+func ToCidrList(ctx context.Context, ips cfgcommon.StringList) ([]*router.GeoIP, error) {
+	return toCidrList(ctx, ips)
+}
+
+type RouterRule struct {
+	Type        string `json:"type"`
+	OutboundTag string `json:"outboundTag"`
+	BalancerTag string `json:"balancerTag"`
+
+	DomainMatcher string `json:"domainMatcher"`
+}

+ 72 - 0
infra/conf/rule/rule_test.go

@@ -0,0 +1,72 @@
+package rule_test
+
+import (
+	"context"
+	"errors"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/common/platform"
+	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/geodata"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/rule"
+
+	_ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard"
+)
+
+const (
+	geoipURL = "https://raw.githubusercontent.com/v2fly/geoip/release/geoip.dat"
+)
+
+func init() {
+	wd, err := os.Getwd()
+	common.Must(err)
+
+	tempPath := filepath.Join(wd, "..", "..", "testing", "temp")
+	geoipPath := filepath.Join(tempPath, "geoip.dat")
+
+	os.Setenv("v2ray.location.asset", tempPath)
+
+	common.Must(os.MkdirAll(tempPath, 0755))
+
+	if _, err := os.Stat(platform.GetAssetLocation("geoip.dat")); err != nil && errors.Is(err, fs.ErrNotExist) {
+		if _, err := os.Stat(geoipPath); err != nil && errors.Is(err, fs.ErrNotExist) {
+			geoipBytes, err := common.FetchHTTPContent(geoipURL)
+			common.Must(err)
+			common.Must(filesystem.WriteFile(geoipPath, geoipBytes))
+		}
+	}
+}
+
+func TestToCidrList(t *testing.T) {
+	t.Log(os.Getenv("v2ray.location.asset"))
+
+	common.Must(filesystem.CopyFile(platform.GetAssetLocation("geoiptestrouter.dat"), platform.GetAssetLocation("geoip.dat")))
+
+	ips := cfgcommon.StringList([]string{
+		"geoip:us",
+		"geoip:cn",
+		"geoip:!cn",
+		"ext:geoiptestrouter.dat:!cn",
+		"ext:geoiptestrouter.dat:ca",
+		"ext-ip:geoiptestrouter.dat:!cn",
+		"ext-ip:geoiptestrouter.dat:!ca",
+	})
+
+	cfgctx := cfgcommon.NewConfigureLoadingContext(context.Background())
+
+	if loader, err := geodata.GetGeoDataLoader("standard"); err == nil {
+		cfgcommon.SetGeoDataLoader(cfgctx, loader)
+	} else {
+		t.Fatal(err)
+	}
+
+	_, err := rule.ToCidrList(cfgctx, ips)
+	if err != nil {
+		t.Fatalf("Failed to parse geoip list, got %s", err)
+	}
+}

+ 17 - 15
infra/conf/shadowsocks.go

@@ -3,6 +3,8 @@ package conf
 import (
 import (
 	"strings"
 	"strings"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
@@ -26,13 +28,13 @@ func cipherFromString(c string) shadowsocks.CipherType {
 }
 }
 
 
 type ShadowsocksServerConfig struct {
 type ShadowsocksServerConfig struct {
-	Cipher      string       `json:"method"`
-	Password    string       `json:"password"`
-	UDP         bool         `json:"udp"`
-	Level       byte         `json:"level"`
-	Email       string       `json:"email"`
-	NetworkList *NetworkList `json:"network"`
-	IVCheck     bool         `json:"ivCheck"`
+	Cipher      string                 `json:"method"`
+	Password    string                 `json:"password"`
+	UDP         bool                   `json:"udp"`
+	Level       byte                   `json:"level"`
+	Email       string                 `json:"email"`
+	NetworkList *cfgcommon.NetworkList `json:"network"`
+	IVCheck     bool                   `json:"ivCheck"`
 }
 }
 
 
 func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
 func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
@@ -62,14 +64,14 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
 }
 }
 
 
 type ShadowsocksServerTarget struct {
 type ShadowsocksServerTarget struct {
-	Address  *Address `json:"address"`
-	Port     uint16   `json:"port"`
-	Cipher   string   `json:"method"`
-	Password string   `json:"password"`
-	Email    string   `json:"email"`
-	Ota      bool     `json:"ota"`
-	Level    byte     `json:"level"`
-	IVCheck  bool     `json:"ivCheck"`
+	Address  *cfgcommon.Address `json:"address"`
+	Port     uint16             `json:"port"`
+	Cipher   string             `json:"method"`
+	Password string             `json:"password"`
+	Email    string             `json:"email"`
+	Ota      bool               `json:"ota"`
+	Level    byte               `json:"level"`
+	IVCheck  bool               `json:"ivCheck"`
 }
 }
 
 
 type ShadowsocksClientConfig struct {
 type ShadowsocksClientConfig struct {

+ 11 - 9
infra/conf/socks.go

@@ -3,6 +3,8 @@ package conf
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
@@ -28,12 +30,12 @@ const (
 )
 )
 
 
 type SocksServerConfig struct {
 type SocksServerConfig struct {
-	AuthMethod string          `json:"auth"`
-	Accounts   []*SocksAccount `json:"accounts"`
-	UDP        bool            `json:"udp"`
-	Host       *Address        `json:"ip"`
-	Timeout    uint32          `json:"timeout"`
-	UserLevel  uint32          `json:"userLevel"`
+	AuthMethod string             `json:"auth"`
+	Accounts   []*SocksAccount    `json:"accounts"`
+	UDP        bool               `json:"udp"`
+	Host       *cfgcommon.Address `json:"ip"`
+	Timeout    uint32             `json:"timeout"`
+	UserLevel  uint32             `json:"userLevel"`
 }
 }
 
 
 func (v *SocksServerConfig) Build() (proto.Message, error) {
 func (v *SocksServerConfig) Build() (proto.Message, error) {
@@ -66,9 +68,9 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
 }
 }
 
 
 type SocksRemoteConfig struct {
 type SocksRemoteConfig struct {
-	Address *Address          `json:"address"`
-	Port    uint16            `json:"port"`
-	Users   []json.RawMessage `json:"users"`
+	Address *cfgcommon.Address `json:"address"`
+	Port    uint16             `json:"port"`
+	Users   []json.RawMessage  `json:"users"`
 }
 }
 type SocksClientConfig struct {
 type SocksClientConfig struct {
 	Servers []*SocksRemoteConfig `json:"servers"`
 	Servers []*SocksRemoteConfig `json:"servers"`

+ 11 - 9
infra/conf/transport_authenticators.go

@@ -3,6 +3,8 @@ package conf
 import (
 import (
 	"sort"
 	"sort"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/transport/internet/headers/http"
 	"github.com/v2fly/v2ray-core/v4/transport/internet/headers/http"
@@ -57,13 +59,13 @@ func (DTLSAuthenticator) Build() (proto.Message, error) {
 }
 }
 
 
 type AuthenticatorRequest struct {
 type AuthenticatorRequest struct {
-	Version string                 `json:"version"`
-	Method  string                 `json:"method"`
-	Path    StringList             `json:"path"`
-	Headers map[string]*StringList `json:"headers"`
+	Version string                           `json:"version"`
+	Method  string                           `json:"method"`
+	Path    cfgcommon.StringList             `json:"path"`
+	Headers map[string]*cfgcommon.StringList `json:"headers"`
 }
 }
 
 
-func sortMapKeys(m map[string]*StringList) []string {
+func sortMapKeys(m map[string]*cfgcommon.StringList) []string {
 	var keys []string
 	var keys []string
 	for key := range m {
 	for key := range m {
 		keys = append(keys, key)
 		keys = append(keys, key)
@@ -133,10 +135,10 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
 }
 }
 
 
 type AuthenticatorResponse struct {
 type AuthenticatorResponse struct {
-	Version string                 `json:"version"`
-	Status  string                 `json:"status"`
-	Reason  string                 `json:"reason"`
-	Headers map[string]*StringList `json:"headers"`
+	Version string                           `json:"version"`
+	Status  string                           `json:"status"`
+	Reason  string                           `json:"reason"`
+	Headers map[string]*cfgcommon.StringList `json:"headers"`
 }
 }
 
 
 func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
 func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {

+ 11 - 9
infra/conf/transport_internet.go

@@ -5,6 +5,8 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"strings"
 	"strings"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
 	"github.com/v2fly/v2ray-core/v4/common/platform/filesystem"
@@ -170,8 +172,8 @@ func (c *WebSocketConfig) Build() (proto.Message, error) {
 }
 }
 
 
 type HTTPConfig struct {
 type HTTPConfig struct {
-	Host *StringList `json:"host"`
-	Path string      `json:"path"`
+	Host *cfgcommon.StringList `json:"host"`
+	Path string                `json:"path"`
 }
 }
 
 
 // Build implements Buildable.
 // Build implements Buildable.
@@ -292,13 +294,13 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
 }
 }
 
 
 type TLSConfig struct {
 type TLSConfig struct {
-	Insecure                         bool             `json:"allowInsecure"`
-	Certs                            []*TLSCertConfig `json:"certificates"`
-	ServerName                       string           `json:"serverName"`
-	ALPN                             *StringList      `json:"alpn"`
-	EnableSessionResumption          bool             `json:"enableSessionResumption"`
-	DisableSystemRoot                bool             `json:"disableSystemRoot"`
-	PinnedPeerCertificateChainSha256 *[]string        `json:"pinnedPeerCertificateChainSha256"`
+	Insecure                         bool                  `json:"allowInsecure"`
+	Certs                            []*TLSCertConfig      `json:"certificates"`
+	ServerName                       string                `json:"serverName"`
+	ALPN                             *cfgcommon.StringList `json:"alpn"`
+	EnableSessionResumption          bool                  `json:"enableSessionResumption"`
+	DisableSystemRoot                bool                  `json:"disableSystemRoot"`
+	PinnedPeerCertificateChainSha256 *[]string             `json:"pinnedPeerCertificateChainSha256"`
 }
 }
 
 
 // Build implements Buildable.
 // Build implements Buildable.

+ 7 - 5
infra/conf/trojan.go

@@ -6,6 +6,8 @@ import (
 	"strconv"
 	"strconv"
 	"syscall"
 	"syscall"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/net"
@@ -16,11 +18,11 @@ import (
 
 
 // TrojanServerTarget is configuration of a single trojan server
 // TrojanServerTarget is configuration of a single trojan server
 type TrojanServerTarget struct {
 type TrojanServerTarget struct {
-	Address  *Address `json:"address"`
-	Port     uint16   `json:"port"`
-	Password string   `json:"password"`
-	Email    string   `json:"email"`
-	Level    byte     `json:"level"`
+	Address  *cfgcommon.Address `json:"address"`
+	Port     uint16             `json:"port"`
+	Password string             `json:"password"`
+	Email    string             `json:"email"`
+	Level    byte               `json:"level"`
 }
 }
 
 
 // TrojanClientConfig is configuration of trojan servers
 // TrojanClientConfig is configuration of trojan servers

+ 16 - 14
infra/conf/v2ray.go

@@ -6,6 +6,8 @@ import (
 	"os"
 	"os"
 	"strings"
 	"strings"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	core "github.com/v2fly/v2ray-core/v4"
 	core "github.com/v2fly/v2ray-core/v4"
 	"github.com/v2fly/v2ray-core/v4/app/dispatcher"
 	"github.com/v2fly/v2ray-core/v4/app/dispatcher"
 	"github.com/v2fly/v2ray-core/v4/app/proxyman"
 	"github.com/v2fly/v2ray-core/v4/app/proxyman"
@@ -58,9 +60,9 @@ func toProtocolList(s []string) ([]proxyman.KnownProtocols, error) {
 }
 }
 
 
 type SniffingConfig struct {
 type SniffingConfig struct {
-	Enabled      bool        `json:"enabled"`
-	DestOverride *StringList `json:"destOverride"`
-	MetadataOnly bool        `json:"metadataOnly"`
+	Enabled      bool                  `json:"enabled"`
+	DestOverride *cfgcommon.StringList `json:"destOverride"`
+	MetadataOnly bool                  `json:"metadataOnly"`
 }
 }
 
 
 // Build implements Buildable.
 // Build implements Buildable.
@@ -148,13 +150,13 @@ func (c *InboundDetourAllocationConfig) Build() (*proxyman.AllocationStrategy, e
 
 
 type InboundDetourConfig struct {
 type InboundDetourConfig struct {
 	Protocol       string                         `json:"protocol"`
 	Protocol       string                         `json:"protocol"`
-	PortRange      *PortRange                     `json:"port"`
-	ListenOn       *Address                       `json:"listen"`
+	PortRange      *cfgcommon.PortRange           `json:"port"`
+	ListenOn       *cfgcommon.Address             `json:"listen"`
 	Settings       *json.RawMessage               `json:"settings"`
 	Settings       *json.RawMessage               `json:"settings"`
 	Tag            string                         `json:"tag"`
 	Tag            string                         `json:"tag"`
 	Allocation     *InboundDetourAllocationConfig `json:"allocate"`
 	Allocation     *InboundDetourAllocationConfig `json:"allocate"`
 	StreamSetting  *StreamConfig                  `json:"streamSettings"`
 	StreamSetting  *StreamConfig                  `json:"streamSettings"`
-	DomainOverride *StringList                    `json:"domainOverride"`
+	DomainOverride *cfgcommon.StringList          `json:"domainOverride"`
 	SniffingConfig *SniffingConfig                `json:"sniffing"`
 	SniffingConfig *SniffingConfig                `json:"sniffing"`
 }
 }
 
 
@@ -253,13 +255,13 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
 }
 }
 
 
 type OutboundDetourConfig struct {
 type OutboundDetourConfig struct {
-	Protocol      string           `json:"protocol"`
-	SendThrough   *Address         `json:"sendThrough"`
-	Tag           string           `json:"tag"`
-	Settings      *json.RawMessage `json:"settings"`
-	StreamSetting *StreamConfig    `json:"streamSettings"`
-	ProxySettings *ProxyConfig     `json:"proxySettings"`
-	MuxSettings   *MuxConfig       `json:"mux"`
+	Protocol      string             `json:"protocol"`
+	SendThrough   *cfgcommon.Address `json:"sendThrough"`
+	Tag           string             `json:"tag"`
+	Settings      *json.RawMessage   `json:"settings"`
+	StreamSetting *StreamConfig      `json:"streamSettings"`
+	ProxySettings *ProxyConfig       `json:"proxySettings"`
+	MuxSettings   *MuxConfig         `json:"mux"`
 }
 }
 
 
 // Build implements Buildable.
 // Build implements Buildable.
@@ -617,7 +619,7 @@ func (c *Config) Build() (*core.Config, error) {
 
 
 	// Backward compatibility.
 	// Backward compatibility.
 	if len(inbounds) > 0 && inbounds[0].PortRange == nil && c.Port > 0 {
 	if len(inbounds) > 0 && inbounds[0].PortRange == nil && c.Port > 0 {
-		inbounds[0].PortRange = &PortRange{
+		inbounds[0].PortRange = &cfgcommon.PortRange{
 			From: uint32(c.Port),
 			From: uint32(c.Port),
 			To:   uint32(c.Port),
 			To:   uint32(c.Port),
 		}
 		}

+ 5 - 3
infra/conf/vless.go

@@ -6,6 +6,8 @@ import (
 	"strconv"
 	"strconv"
 	"syscall"
 	"syscall"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/net"
 	"github.com/v2fly/v2ray-core/v4/common/net"
@@ -120,9 +122,9 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) {
 }
 }
 
 
 type VLessOutboundVnext struct {
 type VLessOutboundVnext struct {
-	Address *Address          `json:"address"`
-	Port    uint16            `json:"port"`
-	Users   []json.RawMessage `json:"users"`
+	Address *cfgcommon.Address `json:"address"`
+	Port    uint16             `json:"port"`
+	Users   []json.RawMessage  `json:"users"`
 }
 }
 
 
 type VLessOutboundConfig struct {
 type VLessOutboundConfig struct {

+ 5 - 3
infra/conf/vmess.go

@@ -4,6 +4,8 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"strings"
 	"strings"
 
 
+	"github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
 
 
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
 	"github.com/v2fly/v2ray-core/v4/common/protocol"
@@ -115,9 +117,9 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) {
 }
 }
 
 
 type VMessOutboundTarget struct {
 type VMessOutboundTarget struct {
-	Address *Address          `json:"address"`
-	Port    uint16            `json:"port"`
-	Users   []json.RawMessage `json:"users"`
+	Address *cfgcommon.Address `json:"address"`
+	Port    uint16             `json:"port"`
+	Users   []json.RawMessage  `json:"users"`
 }
 }
 type VMessOutboundConfig struct {
 type VMessOutboundConfig struct {
 	Receivers []*VMessOutboundTarget `json:"vnext"`
 	Receivers []*VMessOutboundTarget `json:"vnext"`

+ 3 - 0
main/distro/all/all.go

@@ -67,6 +67,9 @@ import (
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/headers/wechat"
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/headers/wechat"
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/headers/wireguard"
 	_ "github.com/v2fly/v2ray-core/v4/transport/internet/headers/wireguard"
 
 
+	// Geo loaders
+	_ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard"
+
 	// JSON config support. Choose only one from the two below.
 	// JSON config support. Choose only one from the two below.
 	// The following line loads JSON from v2ctl
 	// The following line loads JSON from v2ctl
 	// _ "github.com/v2fly/v2ray-core/v4/main/json"
 	// _ "github.com/v2fly/v2ray-core/v4/main/json"