package conf_test import ( "encoding/json" "strings" "testing" "time" _ "unsafe" "github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/serial" . "github.com/xtls/xray-core/infra/conf" "google.golang.org/protobuf/proto" ) func TestRouterConfig(t *testing.T) { createParser := func() func(string) (proto.Message, error) { return func(s string) (proto.Message, error) { config := new(RouterConfig) if err := json.Unmarshal([]byte(s), config); err != nil { return nil, err } return config.Build() } } runMultiTestCase(t, []TestCase{ { Input: `{ "domainStrategy": "AsIs", "rules": [ { "domain": [ "baidu.com", "qq.com" ], "outboundTag": "direct" }, { "ip": [ "10.0.0.0/8", "::1/128" ], "outboundTag": "test" },{ "port": "53, 443, 1000-2000", "outboundTag": "test" },{ "port": 123, "outboundTag": "test" } ], "balancers": [ { "tag": "b1", "selector": ["test"], "fallbackTag": "fall" }, { "tag": "b2", "selector": ["test"], "strategy": { "type": "leastload", "settings": { "healthCheck": { "interval": "5m0s", "sampling": 2, "timeout": "5s", "destination": "dest", "connectivity": "conn" }, "costs": [ { "regexp": true, "match": "\\d+(\\.\\d+)", "value": 5 } ], "baselines": ["400ms", "600ms"], "expected": 6, "maxRTT": "1000ms", "tolerance": 0.5 } }, "fallbackTag": "fall" } ] }`, Parser: createParser(), Output: &router.Config{ DomainStrategy: router.Config_AsIs, BalancingRule: []*router.BalancingRule{ { Tag: "b1", OutboundSelector: []string{"test"}, Strategy: "random", FallbackTag: "fall", }, { Tag: "b2", OutboundSelector: []string{"test"}, Strategy: "leastload", StrategySettings: serial.ToTypedMessage(&router.StrategyLeastLoadConfig{ Costs: []*router.StrategyWeight{ { Regexp: true, Match: "\\d+(\\.\\d+)", Value: 5, }, }, Baselines: []int64{ int64(time.Duration(400) * time.Millisecond), int64(time.Duration(600) * time.Millisecond), }, Expected: 6, MaxRTT: int64(time.Duration(1000) * time.Millisecond), Tolerance: 0.5, }), FallbackTag: "fall", }, }, Rule: []*router.RoutingRule{ { Domain: []*geodata.DomainRule{ {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Substr, Value: "baidu.com"}}}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Substr, Value: "qq.com"}}}, }, TargetTag: &router.RoutingRule_Tag{ Tag: "direct", }, }, { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ Custom: &geodata.CIDRRule{ Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8}, }, }, }, { Value: &geodata.IPRule_Custom{ Custom: &geodata.CIDRRule{ Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128}, }, }, }, }, TargetTag: &router.RoutingRule_Tag{ Tag: "test", }, }, { PortList: &net.PortList{ Range: []*net.PortRange{ {From: 53, To: 53}, {From: 443, To: 443}, {From: 1000, To: 2000}, }, }, TargetTag: &router.RoutingRule_Tag{ Tag: "test", }, }, { PortList: &net.PortList{ Range: []*net.PortRange{ {From: 123, To: 123}, }, }, TargetTag: &router.RoutingRule_Tag{ Tag: "test", }, }, }, }, }, { Input: `{ "domainStrategy": "IPIfNonMatch", "rules": [ { "domain": [ "baidu.com", "qq.com" ], "outboundTag": "direct" }, { "ip": [ "10.0.0.0/8", "::1/128" ], "outboundTag": "test" } ] }`, Parser: createParser(), Output: &router.Config{ DomainStrategy: router.Config_IpIfNonMatch, Rule: []*router.RoutingRule{ { Domain: []*geodata.DomainRule{ {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Substr, Value: "baidu.com"}}}, {Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Substr, Value: "qq.com"}}}, }, TargetTag: &router.RoutingRule_Tag{ Tag: "direct", }, }, { Ip: []*geodata.IPRule{ { Value: &geodata.IPRule_Custom{ Custom: &geodata.CIDRRule{ Cidr: &geodata.CIDR{Ip: []byte{10, 0, 0, 0}, Prefix: 8}, }, }, }, { Value: &geodata.IPRule_Custom{ Custom: &geodata.CIDRRule{ Cidr: &geodata.CIDR{Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Prefix: 128}, }, }, }, }, TargetTag: &router.RoutingRule_Tag{ Tag: "test", }, }, }, }, }, }) } func TestRouterScriptConfig(t *testing.T) { for _, test := range []struct { name string input string wantError bool }{ {name: "script only", input: `{"script":"missing.lua"}`}, {name: "empty rules", input: `{"script":"missing.lua","rules":[]}`}, {name: "balancers", input: `{"script":"missing.lua","balancers":[{"tag":"b1","selector":["direct"]}]}`}, {name: "default domain strategy", input: `{"script":"missing.lua","domainStrategy":"AsIs"}`, wantError: true}, {name: "IP domain strategy", input: `{"script":"missing.lua","domainStrategy":"IPIfNonMatch"}`, wantError: true}, {name: "empty domain strategy", input: `{"script":"missing.lua","domainStrategy":""}`, wantError: true}, {name: "rules", input: `{"script":"missing.lua","rules":[{"outboundTag":"direct","domain":["example.com"]}]}`, wantError: true}, {name: "empty script", input: `{"script":"","domainStrategy":"IPOnDemand","rules":[{"outboundTag":"direct","domain":["example.com"]}]}`}, } { t.Run(test.name, func(t *testing.T) { var config RouterConfig if err := json.Unmarshal([]byte(test.input), &config); err != nil { t.Fatal(err) } built, err := config.Build() if test.wantError { if built != nil || err == nil || !strings.Contains(err.Error(), "routing script cannot be used with domainStrategy or rules") { t.Fatalf("Build() = %v, %v; want nil config and script conflict error", built, err) } return } if err != nil { t.Fatal(err) } if built.Script != config.Script || len(built.Rule) != len(config.RuleList) || len(built.BalancingRule) != len(config.Balancers) { t.Fatalf("Build() did not preserve script, rules or balancers: %v", built) } }) } }