1
0
mirror of https://github.com/v2fly/v2ray-core.git synced 2024-06-24 08:25:23 +00:00

refactor configure file loader for geo loader and v5

This commit is contained in:
Shelikhoo 2021-05-04 03:15:11 +01:00
parent 9458963b5a
commit 0f1fac84ca
No known key found for this signature in database
GPG Key ID: C4D5E79D22B25316
30 changed files with 965 additions and 679 deletions

View File

@ -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 {

View File

@ -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")

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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) {

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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{})
}

View File

@ -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{}
})
}

View File

@ -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"`

View File

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

View File

@ -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)
}

View File

@ -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) {

View File

@ -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
infra/conf/rule/rule.go Normal file
View File

@ -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"`
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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"`

View File

@ -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) {

View File

@ -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.

View File

@ -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

View File

@ -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),
}

View File

@ -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 {

View File

@ -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"`

View File

@ -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"