diff --git a/infra/conf/common.go b/infra/conf/cfgcommon/common.go similarity index 98% rename from infra/conf/common.go rename to infra/conf/cfgcommon/common.go index 66d6325c4..20025a3ba 100644 --- a/infra/conf/common.go +++ b/infra/conf/cfgcommon/common.go @@ -1,4 +1,4 @@ -package conf +package cfgcommon import ( "encoding/json" @@ -9,6 +9,8 @@ import ( "github.com/v2fly/v2ray-core/v4/common/protocol" ) +//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen + type StringList []string func NewStringList(raw []string) *StringList { diff --git a/infra/conf/common_test.go b/infra/conf/cfgcommon/common_test.go similarity index 83% rename from infra/conf/common_test.go rename to infra/conf/cfgcommon/common_test.go index 7dbc0e58f..5acab2bb3 100644 --- a/infra/conf/common_test.go +++ b/infra/conf/cfgcommon/common_test.go @@ -1,22 +1,23 @@ -package conf_test +package cfgcommon_test import ( "encoding/json" "os" "testing" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/v2fly/v2ray-core/v4/common" "github.com/v2fly/v2ray-core/v4/common/net" "github.com/v2fly/v2ray-core/v4/common/protocol" - . "github.com/v2fly/v2ray-core/v4/infra/conf" ) func TestStringListUnmarshalError(t *testing.T) { rawJSON := `1234` - list := new(StringList) + list := new(cfgcommon.StringList) err := json.Unmarshal([]byte(rawJSON), list) if err == nil { t.Error("expected error, but got nil") @@ -25,7 +26,7 @@ func TestStringListUnmarshalError(t *testing.T) { func TestStringListLen(t *testing.T) { rawJSON := `"a, b, c, d"` - var list StringList + var list cfgcommon.StringList err := json.Unmarshal([]byte(rawJSON), &list) common.Must(err) 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) { rawJSON := "\"8.8.8.8\"" - var address Address + var address cfgcommon.Address err := json.Unmarshal([]byte(rawJSON), &address) common.Must(err) 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) { rawJSON := "\"v2fly.org\"" - var address Address + var address cfgcommon.Address common.Must(json.Unmarshal([]byte(rawJSON), &address)) if address.Domain() != "v2fly.org" { t.Error("domain: ", address.Domain()) @@ -55,7 +56,7 @@ func TestDomainParsing(t *testing.T) { func TestURLParsing(t *testing.T) { { rawJSON := "\"https://dns.google/dns-query\"" - var address Address + var address cfgcommon.Address common.Must(json.Unmarshal([]byte(rawJSON), &address)) if address.Domain() != "https://dns.google/dns-query" { t.Error("URL: ", address.Domain()) @@ -63,7 +64,7 @@ func TestURLParsing(t *testing.T) { } { rawJSON := "\"https+local://dns.google/dns-query\"" - var address Address + var address cfgcommon.Address common.Must(json.Unmarshal([]byte(rawJSON), &address)) if address.Domain() != "https+local://dns.google/dns-query" { t.Error("URL: ", address.Domain()) @@ -73,7 +74,7 @@ func TestURLParsing(t *testing.T) { func TestInvalidAddressJson(t *testing.T) { rawJSON := "1234" - var address Address + var address cfgcommon.Address err := json.Unmarshal([]byte(rawJSON), &address) if err == nil { t.Error("nil error") @@ -81,7 +82,7 @@ func TestInvalidAddressJson(t *testing.T) { } func TestStringNetwork(t *testing.T) { - var network Network + var network cfgcommon.Network common.Must(json.Unmarshal([]byte(`"tcp"`), &network)) if v := network.Build(); v != net.Network_TCP { t.Error("network: ", v) @@ -89,7 +90,7 @@ func TestStringNetwork(t *testing.T) { } func TestArrayNetworkList(t *testing.T) { - var list NetworkList + var list cfgcommon.NetworkList common.Must(json.Unmarshal([]byte("[\"Tcp\"]"), &list)) nlist := list.Build() @@ -102,7 +103,7 @@ func TestArrayNetworkList(t *testing.T) { } func TestStringNetworkList(t *testing.T) { - var list NetworkList + var list cfgcommon.NetworkList common.Must(json.Unmarshal([]byte("\"TCP, ip\""), &list)) nlist := list.Build() @@ -115,7 +116,7 @@ func TestStringNetworkList(t *testing.T) { } func TestInvalidNetworkJson(t *testing.T) { - var list NetworkList + var list cfgcommon.NetworkList err := json.Unmarshal([]byte("0"), &list) if err == nil { t.Error("nil error") @@ -123,10 +124,10 @@ func TestInvalidNetworkJson(t *testing.T) { } func TestIntPort(t *testing.T) { - var portRange PortRange + var portRange cfgcommon.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, }); r != "" { t.Error(r) @@ -134,7 +135,7 @@ func TestIntPort(t *testing.T) { } func TestOverRangeIntPort(t *testing.T) { - var portRange PortRange + var portRange cfgcommon.PortRange err := json.Unmarshal([]byte("70000"), &portRange) if err == nil { t.Error("nil error") @@ -149,10 +150,10 @@ func TestOverRangeIntPort(t *testing.T) { func TestEnvPort(t *testing.T) { common.Must(os.Setenv("PORT", "1234")) - var portRange PortRange + var portRange cfgcommon.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, }); r != "" { t.Error(r) @@ -160,10 +161,10 @@ func TestEnvPort(t *testing.T) { } func TestSingleStringPort(t *testing.T) { - var portRange PortRange + var portRange cfgcommon.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, }); r != "" { t.Error(r) @@ -171,10 +172,10 @@ func TestSingleStringPort(t *testing.T) { } func TestStringPairPort(t *testing.T) { - var portRange PortRange + var portRange cfgcommon.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, }); r != "" { t.Error(r) @@ -182,7 +183,7 @@ func TestStringPairPort(t *testing.T) { } func TestOverRangeStringPort(t *testing.T) { - var portRange PortRange + var portRange cfgcommon.PortRange err := json.Unmarshal([]byte("\"65536\""), &portRange) if err == nil { t.Error("nil error") @@ -205,7 +206,7 @@ func TestOverRangeStringPort(t *testing.T) { } func TestUserParsing(t *testing.T) { - user := new(User) + user := new(cfgcommon.User) common.Must(json.Unmarshal([]byte(`{ "id": "96edb838-6d68-42ef-a933-25f7ac3a9d09", "email": "love@v2fly.org", @@ -223,7 +224,7 @@ func TestUserParsing(t *testing.T) { } func TestInvalidUserJson(t *testing.T) { - user := new(User) + user := new(cfgcommon.User) err := json.Unmarshal([]byte(`{"email": 1234}`), user) if err == nil { t.Error("nil error") diff --git a/infra/conf/cfgcommon/errors.generated.go b/infra/conf/cfgcommon/errors.generated.go new file mode 100644 index 000000000..8b520ad79 --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/cfgcommon/session.go b/infra/conf/cfgcommon/session.go new file mode 100644 index 000000000..283fe31f1 --- /dev/null +++ b/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 +} diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 12c8308ed..af3b048bb 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -1,38 +1,45 @@ package conf import ( + "context" "encoding/json" "sort" "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/router" "github.com/v2fly/v2ray-core/v4/common/net" ) type NameServerConfig struct { - Address *Address - ClientIP *Address + Address *cfgcommon.Address + ClientIP *cfgcommon.Address Port uint16 SkipFallback bool Domains []string - ExpectIPs StringList + ExpectIPs cfgcommon.StringList + + cfgctx context.Context } func (c *NameServerConfig) UnmarshalJSON(data []byte) error { - var address Address + var address cfgcommon.Address if err := json.Unmarshal(data, &address); err == nil { c.Address = &address return nil } 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 { c.Address = advanced.Address @@ -63,6 +70,8 @@ func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType { } func (c *NameServerConfig) Build() (*dns.NameServer, error) { + cfgctx := c.cfgctx + if c.Address == nil { return nil, newError("NameServer address is not specified.") } @@ -71,7 +80,7 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) { var originalRules []*dns.NameServer_OriginalRule for _, rule := range c.Domains { - parsedDomain, err := parseDomainRule(rule) + parsedDomain, err := rule2.ParseDomainRule(cfgctx, rule) if err != nil { 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 { 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 { Servers []*NameServerConfig `json:"servers"` Hosts map[string]*HostAddress `json:"hosts"` - ClientIP *Address `json:"clientIp"` + ClientIP *cfgcommon.Address `json:"clientIp"` Tag string `json:"tag"` QueryStrategy string `json:"queryStrategy"` DisableCache bool `json:"disableCache"` DisableFallback bool `json:"disableFallback"` + + GeoLoader string `json:"geoLoader"` } type HostAddress struct { - addr *Address - addrs []*Address + addr *cfgcommon.Address + addrs []*cfgcommon.Address } // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON func (h *HostAddress) UnmarshalJSON(data []byte) error { - addr := new(Address) - var addrs []*Address + addr := new(cfgcommon.Address) + var addrs []*cfgcommon.Address switch { case json.Unmarshal(data, &addr) == nil: h.addr = addr @@ -181,6 +192,21 @@ func getHostMapping(ha *HostAddress) *dns.Config_HostMapping { // Build implements Buildable 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{ Tag: c.Tag, DisableCache: c.DisableCache, @@ -205,6 +231,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) { } for _, server := range c.Servers { + server.cfgctx = cfgctx ns, err := server.Build() if err != nil { return nil, newError("failed to build nameserver").Base(err) @@ -238,7 +265,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) { if len(listName) == 0 { return nil, newError("empty geosite rule: ", domain) } - geositeList, err := loadGeosite(listName) + geositeList, err := geoLoader.LoadGeosite(listName) if err != nil { return nil, newError("failed to load geosite: ", listName).Base(err) } @@ -299,7 +326,7 @@ func (c *DNSConfig) Build() (*dns.Config, error) { } filename := kv[0] list := kv[1] - geositeList, err := loadGeositeWithAttr(filename, list) + geositeList, err := geoLoader.LoadGeositeWithAttr(filename, list) if err != nil { return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err) } diff --git a/infra/conf/dns_proxy.go b/infra/conf/dns_proxy.go index 4e87bdbf9..89f0d27eb 100644 --- a/infra/conf/dns_proxy.go +++ b/infra/conf/dns_proxy.go @@ -2,15 +2,16 @@ package conf import ( "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/proxy/dns" ) 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) { diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index fdddbb4fa..01cffdde3 100644 --- a/infra/conf/dns_test.go +++ b/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/filesystem" "github.com/v2fly/v2ray-core/v4/infra/conf" + + _ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard" ) func init() { diff --git a/infra/conf/dokodemo.go b/infra/conf/dokodemo.go index 4f262c35f..9072b80af 100644 --- a/infra/conf/dokodemo.go +++ b/infra/conf/dokodemo.go @@ -2,17 +2,18 @@ package conf import ( "github.com/golang/protobuf/proto" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" "github.com/v2fly/v2ray-core/v4/proxy/dokodemo" ) 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) { diff --git a/infra/conf/geodata/attr.go b/infra/conf/geodata/attr.go new file mode 100644 index 000000000..47f94a337 --- /dev/null +++ b/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 +} diff --git a/infra/conf/geodata/errors.generated.go b/infra/conf/geodata/errors.generated.go new file mode 100644 index 000000000..2770f1a1a --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/geodata/geodata.go b/infra/conf/geodata/geodata.go new file mode 100644 index 000000000..1abcb37ac --- /dev/null +++ b/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 + } +} diff --git a/infra/conf/geodata/geodataproto.go b/infra/conf/geodata/geodataproto.go new file mode 100644 index 000000000..4e9676794 --- /dev/null +++ b/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) +} diff --git a/infra/conf/geodata/standard/errors.generated.go b/infra/conf/geodata/standard/errors.generated.go new file mode 100644 index 000000000..5fd35e26c --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/geodata/standard/standard.go b/infra/conf/geodata/standard/standard.go new file mode 100644 index 000000000..937ac980d --- /dev/null +++ b/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{} + }) +} diff --git a/infra/conf/http.go b/infra/conf/http.go index b659e2a52..16b7dfee0 100644 --- a/infra/conf/http.go +++ b/infra/conf/http.go @@ -3,6 +3,8 @@ package conf import ( "encoding/json" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/protocol" @@ -47,9 +49,9 @@ func (c *HTTPServerConfig) Build() (proto.Message, error) { } 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 { Servers []*HTTPRemoteConfig `json:"servers"` diff --git a/infra/conf/observatory.go b/infra/conf/observatory.go index 35048ed0a..f7356bcbd 100644 --- a/infra/conf/observatory.go +++ b/infra/conf/observatory.go @@ -2,6 +2,7 @@ package conf import ( "github.com/golang/protobuf/proto" + "github.com/v2fly/v2ray-core/v4/app/observatory" ) diff --git a/infra/conf/router.go b/infra/conf/router.go index 9cc9d5281..d9f65c6ae 100644 --- a/infra/conf/router.go +++ b/infra/conf/router.go @@ -1,15 +1,15 @@ package conf import ( + "context" "encoding/json" - "strconv" "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/common/net" - "github.com/v2fly/v2ray-core/v4/common/platform/filesystem" ) type RouterRulesConfig struct { @@ -24,9 +24,9 @@ type StrategyConfig 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) { @@ -61,6 +61,7 @@ type RouterConfig struct { Balancers []*BalancingRule `json:"balancers"` DomainMatcher string `json:"domainMatcher"` + GeoLoader string `json:"geoLoader"` } func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy { @@ -87,6 +88,18 @@ func (c *RouterConfig) Build() (*router.Config, error) { config := new(router.Config) 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 if c != nil { rawRuleList = c.RuleList @@ -97,7 +110,7 @@ func (c *RouterConfig) Build() (*router.Config, error) { } for _, rawRule := range rawRuleList { - rule, err := ParseRule(rawRule) + rule, err := rule2.ParseRule(cfgctx, rawRule) if err != nil { return nil, err } @@ -117,499 +130,3 @@ func (c *RouterConfig) Build() (*router.Config, error) { } 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) -} diff --git a/infra/conf/router_test.go b/infra/conf/router_test.go index 2bc55472c..672eda4bc 100644 --- a/infra/conf/router_test.go +++ b/infra/conf/router_test.go @@ -2,67 +2,16 @@ package conf_test import ( "encoding/json" - "errors" - "io/fs" - "os" - "path/filepath" "testing" _ "unsafe" "github.com/golang/protobuf/proto" "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/platform" - "github.com/v2fly/v2ray-core/v4/common/platform/filesystem" . "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) { createParser := func() func(string) (proto.Message, error) { return func(s string) (proto.Message, error) { diff --git a/infra/conf/rule/errors.generated.go b/infra/conf/rule/errors.generated.go new file mode 100644 index 000000000..061cef0c4 --- /dev/null +++ b/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{}) +} diff --git a/infra/conf/rule/rule.go b/infra/conf/rule/rule.go new file mode 100644 index 000000000..0cef2344e --- /dev/null +++ b/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"` +} diff --git a/infra/conf/rule/rule_test.go b/infra/conf/rule/rule_test.go new file mode 100644 index 000000000..12248c9b1 --- /dev/null +++ b/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) + } +} diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index c603262fc..6fa703f5c 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -3,6 +3,8 @@ package conf import ( "strings" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/protocol" @@ -26,13 +28,13 @@ func cipherFromString(c string) shadowsocks.CipherType { } 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) { @@ -62,14 +64,14 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { } 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 { diff --git a/infra/conf/socks.go b/infra/conf/socks.go index 73cf370bd..c6328ebbc 100644 --- a/infra/conf/socks.go +++ b/infra/conf/socks.go @@ -3,6 +3,8 @@ package conf import ( "encoding/json" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/protocol" @@ -28,12 +30,12 @@ const ( ) 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) { @@ -66,9 +68,9 @@ func (v *SocksServerConfig) Build() (proto.Message, error) { } 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 { Servers []*SocksRemoteConfig `json:"servers"` diff --git a/infra/conf/transport_authenticators.go b/infra/conf/transport_authenticators.go index c7fa7da3a..4a17207b6 100644 --- a/infra/conf/transport_authenticators.go +++ b/infra/conf/transport_authenticators.go @@ -3,6 +3,8 @@ package conf import ( "sort" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/transport/internet/headers/http" @@ -57,13 +59,13 @@ func (DTLSAuthenticator) Build() (proto.Message, error) { } 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 for key := range m { keys = append(keys, key) @@ -133,10 +135,10 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { } 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) { diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 1a4157435..f0f1e96f9 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -5,6 +5,8 @@ import ( "encoding/json" "strings" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/platform/filesystem" @@ -170,8 +172,8 @@ func (c *WebSocketConfig) Build() (proto.Message, error) { } type HTTPConfig struct { - Host *StringList `json:"host"` - Path string `json:"path"` + Host *cfgcommon.StringList `json:"host"` + Path string `json:"path"` } // Build implements Buildable. @@ -292,13 +294,13 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) { } 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. diff --git a/infra/conf/trojan.go b/infra/conf/trojan.go index d2b7217f8..510667146 100644 --- a/infra/conf/trojan.go +++ b/infra/conf/trojan.go @@ -6,6 +6,8 @@ import ( "strconv" "syscall" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/net" @@ -16,11 +18,11 @@ import ( // TrojanServerTarget is configuration of a single trojan server 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 diff --git a/infra/conf/v2ray.go b/infra/conf/v2ray.go index 237994cb8..7fdcd8a67 100644 --- a/infra/conf/v2ray.go +++ b/infra/conf/v2ray.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + core "github.com/v2fly/v2ray-core/v4" "github.com/v2fly/v2ray-core/v4/app/dispatcher" "github.com/v2fly/v2ray-core/v4/app/proxyman" @@ -58,9 +60,9 @@ func toProtocolList(s []string) ([]proxyman.KnownProtocols, error) { } 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. @@ -148,13 +150,13 @@ func (c *InboundDetourAllocationConfig) Build() (*proxyman.AllocationStrategy, e type InboundDetourConfig struct { 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"` Tag string `json:"tag"` Allocation *InboundDetourAllocationConfig `json:"allocate"` StreamSetting *StreamConfig `json:"streamSettings"` - DomainOverride *StringList `json:"domainOverride"` + DomainOverride *cfgcommon.StringList `json:"domainOverride"` SniffingConfig *SniffingConfig `json:"sniffing"` } @@ -253,13 +255,13 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { } 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. @@ -617,7 +619,7 @@ func (c *Config) Build() (*core.Config, error) { // Backward compatibility. if len(inbounds) > 0 && inbounds[0].PortRange == nil && c.Port > 0 { - inbounds[0].PortRange = &PortRange{ + inbounds[0].PortRange = &cfgcommon.PortRange{ From: uint32(c.Port), To: uint32(c.Port), } diff --git a/infra/conf/vless.go b/infra/conf/vless.go index d4c6a9b48..0ea171ace 100644 --- a/infra/conf/vless.go +++ b/infra/conf/vless.go @@ -6,6 +6,8 @@ import ( "strconv" "syscall" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/net" @@ -120,9 +122,9 @@ func (c *VLessInboundConfig) Build() (proto.Message, error) { } 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 { diff --git a/infra/conf/vmess.go b/infra/conf/vmess.go index 4c330d0d5..e0ed895d4 100644 --- a/infra/conf/vmess.go +++ b/infra/conf/vmess.go @@ -4,6 +4,8 @@ import ( "encoding/json" "strings" + "github.com/v2fly/v2ray-core/v4/infra/conf/cfgcommon" + "github.com/golang/protobuf/proto" "github.com/v2fly/v2ray-core/v4/common/protocol" @@ -115,9 +117,9 @@ func (c *VMessInboundConfig) Build() (proto.Message, error) { } 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 { Receivers []*VMessOutboundTarget `json:"vnext"` diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 241e9744d..87a51d36a 100644 --- a/main/distro/all/all.go +++ b/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/wireguard" + // Geo loaders + _ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard" + // JSON config support. Choose only one from the two below. // The following line loads JSON from v2ctl // _ "github.com/v2fly/v2ray-core/v4/main/json"