package conf_test import ( "encoding/json" "strings" "testing" "github.com/xtls/xray-core/common/log" . "github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/transport/internet" finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" "github.com/xtls/xray-core/transport/internet/xdrive" "google.golang.org/protobuf/proto" ) func TestSocketConfig(t *testing.T) { createParser := func() func(string) (proto.Message, error) { return func(s string) (proto.Message, error) { config := new(SocketConfig) if err := json.Unmarshal([]byte(s), config); err != nil { return nil, err } return config.Build() } } // test "tcpFastOpen": true, queue length 256 is expected. other parameters are tested here too expectedOutput := &internet.SocketConfig{ Mark: 1, Tfo: 256, DomainStrategy: internet.DomainStrategy_USE_IP, DialerProxy: "tag", HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{ "mark": 1, "tcpFastOpen": true, "domainStrategy": "UseIP", "dialerProxy": "tag" }`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != 256 { t.Fatalf("unexpected parsed TFO value, which should be 256") } // test "tcpFastOpen": false, disabled TFO is expected expectedOutput = &internet.SocketConfig{ Mark: 0, Tfo: -1, HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{ "tcpFastOpen": false }`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != 0 { t.Fatalf("unexpected parsed TFO value, which should be 0") } // test "tcpFastOpen": 65535, queue length 65535 is expected expectedOutput = &internet.SocketConfig{ Mark: 0, Tfo: 65535, HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{ "tcpFastOpen": 65535 }`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != 65535 { t.Fatalf("unexpected parsed TFO value, which should be 65535") } // test "tcpFastOpen": -65535, disable TFO is expected expectedOutput = &internet.SocketConfig{ Mark: 0, Tfo: -65535, HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{ "tcpFastOpen": -65535 }`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != 0 { t.Fatalf("unexpected parsed TFO value, which should be 0") } // test "tcpFastOpen": 0, no operation is expected expectedOutput = &internet.SocketConfig{ Mark: 0, Tfo: 0, HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{ "tcpFastOpen": 0 }`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != -1 { t.Fatalf("unexpected parsed TFO value, which should be -1") } // test omit "tcpFastOpen", no operation is expected expectedOutput = &internet.SocketConfig{ Mark: 0, Tfo: 0, HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{}`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != -1 { t.Fatalf("unexpected parsed TFO value, which should be -1") } // test "tcpFastOpen": null, no operation is expected expectedOutput = &internet.SocketConfig{ Mark: 0, Tfo: 0, HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, } runMultiTestCase(t, []TestCase{ { Input: `{ "tcpFastOpen": null }`, Parser: createParser(), Output: expectedOutput, }, }) if expectedOutput.ParseTFOValue() != -1 { t.Fatalf("unexpected parsed TFO value, which should be -1") } } func TestHeaderCustomUDPBuild(t *testing.T) { parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) }) runMultiTestCase(t, []TestCase{ { Input: `{ "client": [ { "type": "hex", "packet": "aabb" }, { "rand": 2, "capture": "seed", "randRange": "16-32" } ], "server": [ { "capture": "txid", "transform": { "op": "concat", "args": [ {"reuse": "seed"}, {"u64": 258}, {"type": "hex", "bytes": "c0de"} ] } }, { "reuse": "txid" } ], "mode": "standalone" }`, Parser: parser, Output: &finalmaskcustom.UDPStandaloneConfig{ Client: []*finalmaskcustom.UDPItem{ { RandMax: 255, Packet: []byte{0xAA, 0xBB}, }, { Rand: 2, RandMin: 16, RandMax: 32, Save: "seed", }, }, Server: []*finalmaskcustom.UDPItem{ { RandMax: 255, Save: "txid", Expr: &finalmaskcustom.Expr{ Op: "concat", Args: []*finalmaskcustom.ExprArg{ { Value: &finalmaskcustom.ExprArg_Var{ Var: "seed", }, }, { Value: &finalmaskcustom.ExprArg_U64{ U64: 258, }, }, { Value: &finalmaskcustom.ExprArg_Bytes{ Bytes: []byte{0xC0, 0xDE}, }, }, }, }, }, { RandMax: 255, Var: "txid", }, }, }, }, }) } func TestHeaderCustomTCPBuildRejectsMixedItemKinds(t *testing.T) { parser := loadJSON(func() Buildable { return new(HeaderCustomTCP) }) _, err := parser(`{ "clients": [[ { "packet": [1, 2], "reuse": "txid" } ]] }`) if err == nil || !strings.Contains(err.Error(), "exactly one item kind") { t.Fatalf("expected mixed item kind rejection, got %v", err) } } func TestHeaderCustomUDPBuildRejectsInvalidVariableNames(t *testing.T) { parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) }) _, err := parser(`{ "client": [ { "capture": "bad-name", "rand": 4 } ] }`) if err == nil || !strings.Contains(err.Error(), "invalid variable name") { t.Fatalf("expected invalid variable name rejection, got %v", err) } } func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) { parser := loadJSON(func() Buildable { return new(HeaderCustomUDP) }) _, err := parser(`{ "client": [ { "transform": { "op": "concat" } } ] }`) if err == nil || !strings.Contains(err.Error(), "transform args") { t.Fatalf("expected transform arg rejection, got %v", err) } } func TestXDriveStreamConfig(t *testing.T) { config := new(StreamConfig) if err := json.Unmarshal([]byte(`{ "method": "xdrive", "xdriveSettings": { "remoteFolder": "/tmp/xdrive", "service": "local" } }`), config); err != nil { t.Fatalf("Unmarshal: %v", err) } built, err := config.Build() if err != nil { t.Fatalf("Build: %v", err) } if built.ProtocolName != "xdrive" { t.Fatalf("ProtocolName is %q, want %q", built.ProtocolName, "xdrive") } if len(built.TransportSettings) != 1 || built.TransportSettings[0].ProtocolName != "xdrive" { t.Fatalf("TransportSettings is %v, want a single xdrive entry", built.TransportSettings) } } func TestXDriveRejectsUnknownService(t *testing.T) { config := new(XDriveConfig) if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil { t.Fatalf("Unmarshal: %v", err) } if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template" or "local"`) { t.Fatalf("expected an unknown service error, got %v", err) } } func TestXDriveGoogleDriveName(t *testing.T) { for _, service := range []string{"google-drive", "Google Drive"} { config := &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b", "c"}} built, err := config.Build() if err != nil { t.Fatalf("%s: %v", service, err) } if got := built.(*xdrive.Config).Service; got != "google-drive" { t.Errorf("%s: service is %q, want google-drive", service, got) } config = &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b"}} if _, err := config.Build(); err == nil { t.Errorf("%s: accepted two secrets", service) } } } func TestXDriveTemplateStreamConfig(t *testing.T) { config := new(StreamConfig) if err := json.Unmarshal([]byte(`{ "method": "xdrive", "xdriveSettings": { "remoteFolder": "folder", "service": "template", "secrets": ["user", "pass"], "template": { "flatten": true, "auth": {"type": "basic", "username": "{secret0}", "password": "{secret1}"}, "put": {"method": "PUT", "url": "https://dav.example/{folder}/{name}"}, "get": {"method": "GET", "url": "https://dav.example/{folder}/{name}"}, "delete": {"method": "DELETE", "url": "https://dav.example/{folder}/{name}"}, "list": {"method": "PROPFIND", "url": "https://dav.example/{folder}/", "namesRegex": "/folder/([^<]+)"} } } }`), config); err != nil { t.Fatalf("Unmarshal: %v", err) } built, err := config.Build() if err != nil { t.Fatalf("Build: %v", err) } if built.ProtocolName != "xdrive" { t.Fatalf("ProtocolName is %q, want xdrive", built.ProtocolName) } } type logLines []string func (l *logLines) Handle(msg log.Message) { *l = append(*l, msg.String()) } // "alpn" ["http/1.1"] is warned about where the hello does not carry it, which // is outside ECH with a fingerprint that has an ALPN of its own func TestXDriveHTTP1ALPNWarning(t *testing.T) { var lines logLines log.RegisterHandler(&lines) for _, c := range []struct { method, tlsSettings string warn bool }{ {"xdrive", `{}`, false}, {"xdrive", `{"alpn": ["http/1.1"]}`, true}, {"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "Firefox"}`, true}, {"raw", `{"alpn": ["http/1.1"]}`, false}, {"xdrive", `{"alpn": ["h2", "http/1.1"]}`, false}, {"xdrive", `{"alpn": ["http/1.1"], "echConfigList": "udp://1.1.1.1"}`, false}, {"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "unsafe"}`, false}, {"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellogolang"}`, false}, {"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "randomized"}`, false}, {"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellorandomizednoalpn"}`, false}, } { lines = nil config := new(StreamConfig) if err := json.Unmarshal([]byte(`{ "method": "`+c.method+`", "security": "tls", "tlsSettings": `+c.tlsSettings+`, "xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"} }`), config); err != nil { t.Fatalf("Unmarshal: %v", err) } if _, err := config.Build(); err != nil { t.Fatalf("%s %s: %v", c.method, c.tlsSettings, err) } warned := false for _, line := range lines { warned = warned || strings.HasPrefix(line, "[Warning]") && strings.Contains(line, `"alpn"`) } if warned != c.warn { t.Errorf("%s %s: warned %v, want %v", c.method, c.tlsSettings, warned, c.warn) } } } func TestXDriveTemplateNeedsTemplate(t *testing.T) { config := new(XDriveConfig) if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil { t.Fatalf("Unmarshal: %v", err) } if _, err := config.Build(); err == nil { t.Fatal("Build accepted a template service without a template") } } const ( xdriveStream = `"streamSettings": {"network": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "local"}}` xdriveID = `"id": "27848739-7e62-4138-9fd3-098a63964b6b"` xdriveServer = `"address": "127.0.0.1", "port": 443, ` + xdriveID xdriveEnc = `"encryption": "mlkem768x25519plus.native.0rtt.XKbaq1BuzJKKKPxZbwgRpn-gYQbnhrYketgezASCL1I"` xdriveDec = `"decryption": "mlkem768x25519plus.native.600s.UKfUEMDdjlEnycTMBP0a08acky8vRRKSrM1MRnaYk3s"` ) func TestXDriveOutboundConfig(t *testing.T) { build := func(s string) error { c := new(OutboundDetourConfig) if err := json.Unmarshal([]byte(s), c); err != nil { return err } _, err := c.Build() return err } for _, input := range []string{ `{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": 0, "xudpConcurrency": 8, "xudpProxyUDP443": "allow"}}`, `{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "reverse": {"tag": "r"}}, ` + xdriveStream + `}`, `{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `}]}]}, ` + xdriveStream + `, "mux": {"enabled": true}}`, } { if err := build(input); err != nil { t.Errorf("%s: %v", input, err) } } for _, c := range []struct{ input, err string }{ {`{"protocol": "vless", "settings": {` + xdriveServer + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`}, {`{"protocol": "vless", "settings": {"address": "example.com", "port": 443, ` + xdriveID + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `}`, `"mux"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": false}}`, `"mux"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"mux"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpConcurrency": -1}}`, `"mux"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpProxyUDP443": "skip"}}`, `"mux"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`}, {`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}]}]}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`}, {`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "testpre": 1}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"testpre"`}, {`{"protocol": "vmess", "settings": {` + xdriveServer + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"}, {`{"protocol": "trojan", "settings": {"address": "example.com", "port": 443, "password": "p"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"}, {`{"protocol": "freedom", ` + xdriveStream + `}`, "VLESS"}, } { if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) { t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err) } } } func TestXDriveInboundConfig(t *testing.T) { build := func(s string) error { c := new(InboundDetourConfig) if err := json.Unmarshal([]byte(s), c); err != nil { return err } _, err := c.Build() return err } for _, input := range []string{ `{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`, `{"protocol": "vless", "listen": "@xdrive", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`, } { if err := build(input); err != nil { t.Errorf("%s: %v", input, err) } } for _, c := range []struct{ input, err string }{ {`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], "decryption": "none"}, ` + xdriveStream + `}`, `"decryption"`}, {`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `}`, `"flow"`}, {`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `, "flow": "xtls-rprx-vision"}], ` + xdriveDec + `}, ` + xdriveStream + `}`, `"flow"`}, {`{"protocol": "vmess", "port": 443, "settings": {"clients": [{` + xdriveID + `}]}, ` + xdriveStream + `}`, "VLESS"}, {`{"protocol": "vless", "port": "10800-10803", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`, "one port"}, } { if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) { t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err) } } }