From 9b9f7fc19d762641b3591a5b2e232699a9b0d417 Mon Sep 17 00:00:00 2001 From: F3dor Date: Fri, 17 Apr 2026 12:29:20 +0300 Subject: [PATCH] Add Xray-core fuzz test harnesses --- fuzz/xraycore/README.md | 64 +++ fuzz/xraycore/TECHNICAL_NOTE.md | 70 +++ fuzz/xraycore/config_fuzz_test.go | 368 ++++++++++++++++ .../85cbe7a11661b2e3 | 2 + .../invalid_near_json | 2 + .../FuzzXrayCoreFullConfigBuild/minimal_full | 2 + .../FuzzXrayCoreFullConfigBuild/vless_ws_tls | 2 + .../minimal_vless_inbound | 2 + .../vless_settings_bad_uuid | 2 + .../minimal_freedom_outbound | 2 + .../minimal_vless_outbound | 2 + .../dns_hosts | 2 + .../routing_rule | 2 + .../bad_reality_stream | 2 + .../FuzzXrayCoreStreamSettingsBuild/ws_stream | 2 + .../FuzzXrayCoreVLESSFirstPacket/bad_command | 2 + .../bad_ipv6_payload | 2 + .../truncated_uuid | 2 + .../FuzzXrayCoreVLESSFirstPacket/unknown_flow | 2 + .../FuzzXrayCoreVLESSFirstPacket/valid_mux | 2 + .../valid_prefix_garbage_suffix | 2 + .../valid_tcp_domain | 2 + .../valid_tcp_ipv4 | 2 + .../valid_tcp_ipv6 | 2 + .../valid_udp_ipv4 | 2 + .../FuzzXrayCoreVLESSFirstPacket/wrong_uuid | 2 + .../FuzzXrayCoreVLESSFirstPacket/xrv_flow | 2 + .../bad_command | 2 + .../http_get_stage2 | 2 + .../truncated | 2 + .../valid_tcp_domain | 2 + .../bad_domain_length | 2 + .../unknown_flow | 2 + .../valid_mux | 2 + .../valid_tcp_domain | 2 + .../valid_tcp_ipv4 | 2 + .../valid_udp_ipv4 | 2 + .../wrong_uuid | 2 + .../xrv_flow_rejected_on_raw | 2 + fuzz/xraycore/vless_first_packet_fuzz_test.go | 238 ++++++++++ .../vless_inbound_process_fuzz_test.go | 406 ++++++++++++++++++ go.mod | 3 + go.sum | 1 + 43 files changed, 1222 insertions(+) create mode 100644 fuzz/xraycore/README.md create mode 100644 fuzz/xraycore/TECHNICAL_NOTE.md create mode 100644 fuzz/xraycore/config_fuzz_test.go create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/85cbe7a11661b2e3 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/invalid_near_json create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/minimal_full create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/vless_ws_tls create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/minimal_vless_inbound create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/vless_settings_bad_uuid create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_freedom_outbound create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_vless_outbound create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/dns_hosts create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/routing_rule create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/bad_reality_stream create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/ws_stream create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_command create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_ipv6_payload create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/truncated_uuid create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/unknown_flow create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_mux create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_prefix_garbage_suffix create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_domain create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv4 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv6 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_udp_ipv4 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/wrong_uuid create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/xrv_flow create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/bad_command create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/http_get_stage2 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/truncated create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/valid_tcp_domain create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/bad_domain_length create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/unknown_flow create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_mux create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_domain create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_ipv4 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_udp_ipv4 create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/wrong_uuid create mode 100644 fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/xrv_flow_rejected_on_raw create mode 100644 fuzz/xraycore/vless_first_packet_fuzz_test.go create mode 100644 fuzz/xraycore/vless_inbound_process_fuzz_test.go diff --git a/fuzz/xraycore/README.md b/fuzz/xraycore/README.md new file mode 100644 index 00000000..e59ac185 --- /dev/null +++ b/fuzz/xraycore/README.md @@ -0,0 +1,64 @@ +# Xray-core fuzzing, stages 1-2 + +This package contains Go fuzz targets for Xray-core only. It does not fuzz x-ui, the web panel, Telegram bot, installer scripts, Docker wiring, or any external control plane. + +## Targets + +| Target | Surface | Main code paths | +| --- | --- | --- | +| `FuzzXrayCoreFullConfigBuild` | Full JSON config | `infra/conf/serial.DecodeJSONConfig` -> `infra/conf.Config.Build` -> `core.New` | +| `FuzzXrayCoreInboundVLESSConfigBuild` | Inbound and VLESS inbound fragments | `encoding/json` -> `infra/conf.InboundDetourConfig.Build` / `VLessInboundConfig.Build` -> `conf.Config.Build` -> `core.New` | +| `FuzzXrayCoreOutboundConfigBuild` | Outbound and VLESS outbound fragments | `encoding/json` -> `infra/conf.OutboundDetourConfig.Build` / `VLessOutboundConfig.Build` -> `conf.Config.Build` -> `core.New` | +| `FuzzXrayCoreStreamSettingsBuild` | Stream, transport, and security settings | `encoding/json` -> `infra/conf.StreamConfig.Build` | +| `FuzzXrayCoreSniffingRoutingDNSConfigBuild` | Sniffing, routing, and DNS fragments | `encoding/json` -> `SniffingConfig.Build`, `RouterConfig.Build`, `DNSConfig.Build`, optional `conf.Config.Build` | +| `FuzzXrayCoreVLESSFirstPacket` | VLESS inbound pre-auth first packet | byte input -> `proxy/vless/encoding.DecodeRequestHeader` with `MemoryValidator` | +| `FuzzXrayCoreVLESSInboundProcessPreAuth` | VLESS inbound pre-auth handler path | byte input -> fake `net.Conn` -> `proxy/vless/inbound.Handler.Process` -> parser/auth/flow/dispatch decision | +| `FuzzXrayCoreVLESSInboundFallbackPreAuth` | VLESS inbound fallback-enabled pre-auth path | byte input -> fake `net.Conn` -> `Handler.Process` with fallback map -> first-buffer parser or fallback reject decision | + +## Run + +Run seed and regression corpus: + +```sh +go test ./fuzz/xraycore -run=Fuzz -count=1 +``` + +Run individual fuzz targets: + +```sh +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreFullConfigBuild -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreInboundVLESSConfigBuild -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreOutboundConfigBuild -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreStreamSettingsBuild -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreSniffingRoutingDNSConfigBuild -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreVLESSFirstPacket -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreVLESSInboundProcessPreAuth -fuzztime=30s +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreVLESSInboundFallbackPreAuth -fuzztime=30s +``` + +For longer local runs, prefer one target per process and set the global timeout explicitly: + +```sh +go test ./fuzz/xraycore -run=^$ -fuzz=FuzzXrayCoreVLESSFirstPacket -fuzztime=10m -timeout=15m +``` + +## Seed corpus + +Initial seeds are present in two forms: + +1. Programmatic `f.Add` seeds in the fuzz target source files. +2. Persistent Go corpus files under `fuzz/xraycore/testdata/fuzz//`. + +Config seeds include minimal full config, VLESS inbound, VLESS outbound, WebSocket/TLS, gRPC, stream fragments, DNS hosts, routing rules, and near-valid invalid JSON/config samples. + +VLESS first-packet seeds include valid TCP/domain, TCP/IPv4, TCP/IPv6, UDP/IPv4, and Mux first packets for UUID `11111111-1111-1111-1111-111111111111`; truncated variants; wrong UUID; bad command; bad address type; malformed domain length; malformed IPv6 payload; oversized addons; XRV flow on raw transport; unknown protobuf flow; and valid prefix plus garbage suffix. + +Stage 2 also adds persistent corpora for `FuzzXrayCoreVLESSInboundProcessPreAuth` and `FuzzXrayCoreVLESSInboundFallbackPreAuth`. The fallback harness uses a fallback map that is active for decision-making but intentionally has no matchable default target, so fuzzing reaches fallback selection/reject logic without opening real network connections. + +The full-config corpus also contains the minimized known-crash input `{"inBounds":[{"listen":""}]}`. The active fuzz harness quarantines this exact Xray-core empty-domain-listen class so longer runs can continue searching for additional crashes while `TestXrayCoreKnownEmptyListenPanicReproducer` keeps the upstream panic visible. + +## Guardrails + +The targets cap input size, treat malformed parse/build errors as non-crashing outcomes, fail on unexpected nil successful builds, initialize built full configs with `core.New` where practical, and enforce coarse per-iteration elapsed-time checks. The direct VLESS parser target rejects any successful decode that does not authenticate to the configured seed user or that returns an invalid command/address shape. + +The VLESS `Handler.Process` targets use a fake non-blocking `net.Conn` and a recording dispatcher. The oracle fails if malformed, unauthorized, bad-flow, reverse, or structurally incomplete input reaches `DispatchLink`; valid TCP/UDP/Mux seeds must reach `DispatchLink`; and trailing body bytes must not change the parsed first-packet header. diff --git a/fuzz/xraycore/TECHNICAL_NOTE.md b/fuzz/xraycore/TECHNICAL_NOTE.md new file mode 100644 index 00000000..e4fcf149 --- /dev/null +++ b/fuzz/xraycore/TECHNICAL_NOTE.md @@ -0,0 +1,70 @@ +# Stage 1 technical note + +## Covered + +Config fuzzing covers the Xray-core JSON/config build surface without starting listeners: + +- `infra/conf/serial.DecodeJSONConfig` for full JSON config loading. +- `infra/conf.Config.Build` for global config normalization and app/inbound/outbound config construction. +- `core.New` for non-running runtime object initialization after successful full-config builds. +- `InboundDetourConfig.Build` and `VLessInboundConfig.Build` for inbound/VLESS settings, users, decryption, fallback, stream, and sniffing handling. +- `OutboundDetourConfig.Build` and `VLessOutboundConfig.Build` for outbound/VLESS endpoint, user, stream, proxy, and mux handling. +- `StreamConfig.Build` for transport/security fragments: TCP/raw, WS, gRPC, XHTTP, KCP, TLS, REALITY, sockopt, and finalmask paths where reachable from JSON. +- `SniffingConfig.Build`, `RouterConfig.Build`, and `DNSConfig.Build` as focused fragment targets. +- API/stats/metrics are included through full config seeds and `conf.Config.Build`. + +VLESS first-packet fuzzing covers the early inbound pre-auth parser directly: + +- `proxy/vless/encoding.DecodeRequestHeader`. +- `proxy/vless.MemoryValidator` with one configured valid client UUID. +- Version, raw UUID, addons length/value, command, and destination parser handling. +- Both direct reader mode and the first-buffer mode used by VLESS inbound after the first socket read. + +Stage 2 adds handler-level pre-auth coverage: + +- `proxy/vless/inbound.(*Handler).Process` first-read path. +- `connection.SetReadDeadline`, first `buf.Buffer.ReadFrom`, `buf.BufferedReader` setup, and parser call. +- UUID lookup through `MemoryValidator`. +- Flow admission for empty flow, unknown flow, and `xtls-rprx-vision` on a raw fake connection. +- Command/destination admission for TCP, UDP, Mux, and Rvs. +- Success handoff into `routing.Dispatcher.DispatchLink` using a recording dispatcher. +- Fallback-enabled first-buffer parser/reject path without dialing a real fallback target. +- Invariants for wrong UUID, malformed address metadata, malformed addon length/value, bad command, bad version, short reads, and valid prefix plus trailing body bytes. + +## Bugs found + +One Xray-core config-build crash was found by `FuzzXrayCoreFullConfigBuild`: + +- Minimal reproducer: `{"inBounds":[{"listen":""}]}` +- Crash: `panic: runtime error: index out of range [0] with length 0` +- Upstream location: `github.com/xtls/xray-core/infra/conf.(*InboundDetourConfig).Build`, `infra/conf/xray.go:152` +- Cause: empty string `listen` is parsed as a domain address with `Domain() == ""`; the build path indexes `Domain()[0]` while checking for Unix domain sockets. + +The minimized reproducer is retained in `testdata/fuzz/FuzzXrayCoreFullConfigBuild/85cbe7a11661b2e3`. The active fuzz harness now quarantines this known empty-domain-listen class before calling `Config.Build` so subsequent fuzzing can continue. `TestXrayCoreKnownEmptyListenPanicReproducer` directly calls the upstream build path under `recover` and asserts the panic still reproduces; when Xray-core fixes the bug, that test should fail and the quarantine should be removed. + +The deterministic regression test `TestXrayCoreVLESSWrongUUIDRejected` verifies that a packet with a changed non-normalized UUID byte does not authenticate. + +No VLESS first-packet parser crash, hang, or false dispatch was found in the Stage 2 smoke-runs. + +## Problems encountered + +The config loader imports Xray-core serial config support, which requires additional indirect module metadata in the parent module: + +- `github.com/ghodss/yaml` +- `github.com/pelletier/go-toml` +- `gopkg.in/yaml.v2` + +These are Xray-core config-loader dependencies, not fuzzing infrastructure. + +`core.New` is used only after successful build. It initializes Xray runtime objects but does not call `Start`, so it does not bind sockets or run transport lifecycles. + +## Not covered in stage 1 + +- Stateful network harnesses for TCP, WS, gRPC, TLS, REALITY, QUIC, or full VLESS sessions. +- Xray listener accept loops and actual socket deadlines. +- Fallback connection I/O after malformed VLESS first packets. +- Vision/REALITY encrypted first-packet lifecycle beyond the plain request-header parser. +- Long-running resource leak measurement beyond input caps, build/init checks, and elapsed-time guardrails. +- Corpus minimization for Stage 2, because no VLESS failing input was found in smoke-runs. + +These are candidates for stage 2 rather than this stage. diff --git a/fuzz/xraycore/config_fuzz_test.go b/fuzz/xraycore/config_fuzz_test.go new file mode 100644 index 00000000..9f67e709 --- /dev/null +++ b/fuzz/xraycore/config_fuzz_test.go @@ -0,0 +1,368 @@ +package xraycorefuzz + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + core "github.com/xtls/xray-core/core" + xconf "github.com/xtls/xray-core/infra/conf" + xserial "github.com/xtls/xray-core/infra/conf/serial" + + _ "github.com/xtls/xray-core/main/distro/all" +) + +const ( + maxConfigInputBytes = 64 << 10 + maxConfigIteration = 2 * time.Second +) + +func FuzzXrayCoreFullConfigBuild(f *testing.F) { + addStringSeeds(f, + minimalFullConfig, + minimalVLESSInboundConfig, + fullConfigWithAPIStatsDNSRouting, + fullConfigWithVLESSWS, + fullConfigWithVLESSGRPC, + `{`, + `null`, + `{"inbounds":[{"protocol":"vless","port":"not-a-port","settings":{"clients":[]}}]}`, + `{"routing":{"rules":[{"type":"field","domain":["regexp:("],"outboundTag":"direct"}]},"outbounds":[{"protocol":"freedom","tag":"direct"}]}`, + ) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxConfigInputBytes) { + return + } + start := time.Now() + cfg, err := xserial.DecodeJSONConfig(bytes.NewReader(data)) + if err != nil { + return + } + buildAndInit(t, cfg) + failIfSlow(t, start, maxConfigIteration) + }) +} + +func FuzzXrayCoreInboundVLESSConfigBuild(f *testing.F) { + addStringSeeds(f, + minimalVLESSInboundObject, + minimalVLESSInboundSettings, + vlessInboundWithFallback, + vlessInboundWithVisionFlow, + `{"clients":[{"id":"not-a-uuid"}],"decryption":"none"}`, + `{"clients":[{"id":"11111111-1111-1111-1111-111111111111","encryption":"none"}],"decryption":"none"}`, + `{"protocol":"vless","port":443,"settings":{"clients":[]}}`, + ) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxConfigInputBytes/2) { + return + } + start := time.Now() + + var inbound xconf.InboundDetourConfig + if err := json.Unmarshal(data, &inbound); err == nil { + buildAndInit(t, &xconf.Config{InboundConfigs: []xconf.InboundDetourConfig{inbound}}) + } + + if json.Valid(data) { + wrapped := wrapVLESSInboundSettings(data) + var vlessInbound xconf.InboundDetourConfig + if err := json.Unmarshal(wrapped, &vlessInbound); err == nil { + buildAndInit(t, &xconf.Config{InboundConfigs: []xconf.InboundDetourConfig{vlessInbound}}) + } + } + + failIfSlow(t, start, maxConfigIteration) + }) +} + +func FuzzXrayCoreOutboundConfigBuild(f *testing.F) { + addStringSeeds(f, + minimalFreedomOutboundObject, + minimalVLESSOutboundObject, + minimalVLESSOutboundSettings, + `{"protocol":"vless","settings":{"vnext":[]}}`, + `{"protocol":"vless","settings":{"vnext":[{"address":"example.com","port":443,"users":[]}]}}`, + `{"protocol":"freedom","settings":{"domainStrategy":"forceIPv4"}}`, + ) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxConfigInputBytes/2) { + return + } + start := time.Now() + + var outbound xconf.OutboundDetourConfig + if err := json.Unmarshal(data, &outbound); err == nil { + buildAndInit(t, &xconf.Config{OutboundConfigs: []xconf.OutboundDetourConfig{outbound}}) + } + + if json.Valid(data) { + wrapped := wrapVLESSOutboundSettings(data) + var vlessOutbound xconf.OutboundDetourConfig + if err := json.Unmarshal(wrapped, &vlessOutbound); err == nil { + buildAndInit(t, &xconf.Config{OutboundConfigs: []xconf.OutboundDetourConfig{vlessOutbound}}) + } + } + + failIfSlow(t, start, maxConfigIteration) + }) +} + +func FuzzXrayCoreStreamSettingsBuild(f *testing.F) { + addStringSeeds(f, + `{}`, + `{"network":"tcp","security":"none"}`, + `{"network":"ws","wsSettings":{"path":"/vless","headers":{"Host":"example.com"}}}`, + `{"network":"grpc","grpcSettings":{"serviceName":"svc","multiMode":true}}`, + `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"example.com","alpn":["h2","http/1.1"]}}`, + `{"network":"tcp","security":"reality","realitySettings":{"show":false,"dest":"example.com:443","serverNames":["example.com"],"privateKey":"short","shortIds":["00"]}}`, + `{"network":"kcp","kcpSettings":{"mtu":1350,"tti":50,"uplinkCapacity":5,"downlinkCapacity":20,"header":{"type":"wechat-video"}}}`, + `{"network":"xhttp","xhttpSettings":{"path":"/x","mode":"auto"}}`, + ) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxConfigInputBytes/2) { + return + } + start := time.Now() + var stream xconf.StreamConfig + if err := json.Unmarshal(data, &stream); err != nil { + return + } + _, _ = stream.Build() + failIfSlow(t, start, maxConfigIteration) + }) +} + +func FuzzXrayCoreSniffingRoutingDNSConfigBuild(f *testing.F) { + addStringSeeds(f, + `{"enabled":true,"destOverride":["http","tls","quic","fakedns"],"metadataOnly":false,"routeOnly":false}`, + `{"domainStrategy":"IPIfNonMatch","rules":[{"type":"field","domain":["domain:example.com"],"outboundTag":"direct"}]}`, + `{"servers":["1.1.1.1","https://dns.google/dns-query"],"hosts":{"example.com":"127.0.0.1"},"queryStrategy":"UseIPv4"}`, + `{"sniffing":{"enabled":true,"destOverride":["bad-proto"]},"routing":{"rules":[{"type":"field","ip":["geoip:private"],"outboundTag":"direct"}]},"dns":{"servers":["localhost"]}}`, + ) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxConfigInputBytes/2) { + return + } + start := time.Now() + + var sniffing xconf.SniffingConfig + if err := json.Unmarshal(data, &sniffing); err == nil { + _, _ = sniffing.Build() + } + + var routing xconf.RouterConfig + if err := json.Unmarshal(data, &routing); err == nil { + _, _ = routing.Build() + } + + var dns xconf.DNSConfig + if err := json.Unmarshal(data, &dns); err == nil { + _, _ = dns.Build() + } + + var cfg xconf.Config + if err := json.Unmarshal(data, &cfg); err == nil { + buildAndInit(t, &cfg) + } + + failIfSlow(t, start, maxConfigIteration) + }) +} + +func buildAndInit(t *testing.T, cfg *xconf.Config) { + t.Helper() + if hasKnownEmptyDomainListen(cfg) { + return + } + pbConfig, err := cfg.Build() + if err != nil { + return + } + if pbConfig == nil { + t.Fatal("Build returned nil config without error") + } + instance, err := core.New(pbConfig) + if err != nil { + return + } + if err := instance.Close(); err != nil { + t.Fatalf("closing initialized Xray instance failed: %v", err) + } +} + +func TestXrayCoreKnownEmptyListenPanicReproducer(t *testing.T) { + cfg, err := xserial.DecodeJSONConfig(bytes.NewReader([]byte(`{"inBounds":[{"listen":""}]}`))) + if err != nil { + t.Fatalf("failed to decode minimized empty listen reproducer: %v", err) + } + if !hasKnownEmptyDomainListen(cfg) { + t.Fatal("minimized empty listen reproducer was not classified as known Xray-core panic") + } + panicValue := catchPanic(func() { + _, _ = cfg.Build() + }) + if panicValue == nil { + t.Fatal("known Xray-core empty listen panic no longer reproduces; remove the quarantine") + } +} + +func hasKnownEmptyDomainListen(cfg *xconf.Config) bool { + for _, inbound := range cfg.InboundConfigs { + if inbound.ListenOn == nil || inbound.ListenOn.Address == nil { + continue + } + if inbound.ListenOn.Family().IsDomain() && inbound.ListenOn.Domain() == "" { + return true + } + } + return false +} + +func catchPanic(fn func()) (panicValue any) { + defer func() { + panicValue = recover() + }() + fn() + return nil +} + +func addStringSeeds(f *testing.F, seeds ...string) { + for _, seed := range seeds { + f.Add([]byte(seed)) + } +} + +func tooLarge(data []byte, max int) bool { + return len(data) > max +} + +func failIfSlow(t *testing.T, start time.Time, max time.Duration) { + t.Helper() + if elapsed := time.Since(start); elapsed > max { + t.Fatalf("fuzz iteration took %s, max %s", elapsed, max) + } +} + +func wrapVLESSInboundSettings(settings []byte) []byte { + out := make([]byte, 0, len(settings)+96) + out = append(out, `{"protocol":"vless","listen":"127.0.0.1","port":443,"settings":`...) + out = append(out, settings...) + out = append(out, `}`...) + return out +} + +func wrapVLESSOutboundSettings(settings []byte) []byte { + out := make([]byte, 0, len(settings)+64) + out = append(out, `{"protocol":"vless","settings":`...) + out = append(out, settings...) + out = append(out, `}`...) + return out +} + +const minimalFullConfig = `{ + "log": {"loglevel": "warning"}, + "inbounds": [], + "outbounds": [{"protocol": "freedom", "tag": "direct"}] +}` + +const minimalVLESSInboundConfig = `{ + "inbounds": [{ + "tag": "vless-in", + "listen": "127.0.0.1", + "port": 443, + "protocol": "vless", + "settings": { + "clients": [{"id": "11111111-1111-1111-1111-111111111111", "email": "seed@example"}], + "decryption": "none" + }, + "streamSettings": {"network": "tcp", "security": "none"}, + "sniffing": {"enabled": true, "destOverride": ["http", "tls"]} + }], + "outbounds": [{"protocol": "freedom", "tag": "direct"}] +}` + +const fullConfigWithAPIStatsDNSRouting = `{ + "api": {"tag": "api", "services": ["HandlerService", "StatsService"]}, + "stats": {}, + "dns": {"servers": ["1.1.1.1"], "hosts": {"seed.example": "127.0.0.1"}}, + "routing": {"domainStrategy": "IPIfNonMatch", "rules": [{"type": "field", "domain": ["domain:seed.example"], "outboundTag": "direct"}]}, + "inbounds": [], + "outbounds": [{"protocol": "freedom", "tag": "direct"}] +}` + +const fullConfigWithVLESSWS = `{ + "inbounds": [{ + "listen": "127.0.0.1", + "port": 8443, + "protocol": "vless", + "settings": {"clients": [{"id": "11111111-1111-1111-1111-111111111111"}], "decryption": "none"}, + "streamSettings": {"network": "ws", "security": "tls", "wsSettings": {"path": "/ws"}, "tlsSettings": {"serverName": "example.com"}} + }], + "outbounds": [{"protocol": "freedom", "tag": "direct"}] +}` + +const fullConfigWithVLESSGRPC = `{ + "inbounds": [{ + "listen": "127.0.0.1", + "port": 9443, + "protocol": "vless", + "settings": {"clients": [{"id": "11111111-1111-1111-1111-111111111111", "flow": "xtls-rprx-vision"}], "decryption": "none"}, + "streamSettings": {"network": "grpc", "grpcSettings": {"serviceName": "svc", "multiMode": true}} + }], + "outbounds": [{"protocol": "freedom", "tag": "direct"}] +}` + +const minimalVLESSInboundObject = `{ + "tag": "vless-in", + "listen": "127.0.0.1", + "port": 443, + "protocol": "vless", + "settings": {"clients": [{"id": "11111111-1111-1111-1111-111111111111"}], "decryption": "none"} +}` + +const minimalVLESSInboundSettings = `{ + "clients": [{"id": "11111111-1111-1111-1111-111111111111", "email": "seed@example"}], + "decryption": "none" +}` + +const vlessInboundWithFallback = `{ + "clients": [{"id": "11111111-1111-1111-1111-111111111111"}], + "decryption": "none", + "fallbacks": [{"path": "/fallback", "dest": 8080, "xver": 1}] +}` + +const vlessInboundWithVisionFlow = `{ + "clients": [{"id": "11111111-1111-1111-1111-111111111111", "flow": "xtls-rprx-vision"}], + "decryption": "none", + "flow": "xtls-rprx-vision" +}` + +const minimalFreedomOutboundObject = `{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"AsIs"}}` + +const minimalVLESSOutboundObject = `{ + "protocol": "vless", + "tag": "vless-out", + "settings": { + "vnext": [{ + "address": "example.com", + "port": 443, + "users": [{"id": "11111111-1111-1111-1111-111111111111", "encryption": "none"}] + }] + }, + "streamSettings": {"network": "tcp", "security": "none"} +}` + +const minimalVLESSOutboundSettings = `{ + "vnext": [{ + "address": "example.com", + "port": 443, + "users": [{"id": "11111111-1111-1111-1111-111111111111", "encryption": "none"}] + }] +}` diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/85cbe7a11661b2e3 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/85cbe7a11661b2e3 new file mode 100644 index 00000000..f40b5393 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/85cbe7a11661b2e3 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"inBounds\":[{\"listen\":\"\"}]}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/invalid_near_json b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/invalid_near_json new file mode 100644 index 00000000..0d377ac5 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/invalid_near_json @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"inbounds\":[{\"protocol\":\"vless\",\"port\":\"not-a-port\",\"settings\":{\"clients\":[]}}]}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/minimal_full b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/minimal_full new file mode 100644 index 00000000..f08aafd5 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/minimal_full @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"log\":{\"loglevel\":\"warning\"},\"inbounds\":[],\"outbounds\":[{\"protocol\":\"freedom\",\"tag\":\"direct\"}]}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/vless_ws_tls b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/vless_ws_tls new file mode 100644 index 00000000..9f4f2b01 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreFullConfigBuild/vless_ws_tls @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"inbounds\":[{\"listen\":\"127.0.0.1\",\"port\":8443,\"protocol\":\"vless\",\"settings\":{\"clients\":[{\"id\":\"11111111-1111-1111-1111-111111111111\"}],\"decryption\":\"none\"},\"streamSettings\":{\"network\":\"ws\",\"security\":\"tls\",\"wsSettings\":{\"path\":\"/ws\"},\"tlsSettings\":{\"serverName\":\"example.com\"}}}],\"outbounds\":[{\"protocol\":\"freedom\",\"tag\":\"direct\"}]}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/minimal_vless_inbound b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/minimal_vless_inbound new file mode 100644 index 00000000..e1f37a0d --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/minimal_vless_inbound @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"tag\":\"vless-in\",\"listen\":\"127.0.0.1\",\"port\":443,\"protocol\":\"vless\",\"settings\":{\"clients\":[{\"id\":\"11111111-1111-1111-1111-111111111111\"}],\"decryption\":\"none\"}}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/vless_settings_bad_uuid b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/vless_settings_bad_uuid new file mode 100644 index 00000000..e7fb1227 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreInboundVLESSConfigBuild/vless_settings_bad_uuid @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"clients\":[{\"id\":\"not-a-uuid\"}],\"decryption\":\"none\"}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_freedom_outbound b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_freedom_outbound new file mode 100644 index 00000000..3cf71adc --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_freedom_outbound @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"protocol\":\"freedom\",\"tag\":\"direct\",\"settings\":{\"domainStrategy\":\"AsIs\"}}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_vless_outbound b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_vless_outbound new file mode 100644 index 00000000..3f2cfd4b --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreOutboundConfigBuild/minimal_vless_outbound @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"protocol\":\"vless\",\"tag\":\"vless-out\",\"settings\":{\"vnext\":[{\"address\":\"example.com\",\"port\":443,\"users\":[{\"id\":\"11111111-1111-1111-1111-111111111111\",\"encryption\":\"none\"}]}]},\"streamSettings\":{\"network\":\"tcp\",\"security\":\"none\"}}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/dns_hosts b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/dns_hosts new file mode 100644 index 00000000..789c3bc3 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/dns_hosts @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"servers\":[\"1.1.1.1\",\"https://dns.google/dns-query\"],\"hosts\":{\"example.com\":\"127.0.0.1\"},\"queryStrategy\":\"UseIPv4\"}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/routing_rule b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/routing_rule new file mode 100644 index 00000000..a1bf5877 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreSniffingRoutingDNSConfigBuild/routing_rule @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"domainStrategy\":\"IPIfNonMatch\",\"rules\":[{\"type\":\"field\",\"domain\":[\"domain:example.com\"],\"outboundTag\":\"direct\"}]}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/bad_reality_stream b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/bad_reality_stream new file mode 100644 index 00000000..05a06c73 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/bad_reality_stream @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"network\":\"tcp\",\"security\":\"reality\",\"realitySettings\":{\"dest\":\"example.com:443\",\"serverNames\":[\"example.com\"],\"privateKey\":\"short\",\"shortIds\":[\"00\"]}}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/ws_stream b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/ws_stream new file mode 100644 index 00000000..79282e5e --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreStreamSettingsBuild/ws_stream @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"network\":\"ws\",\"wsSettings\":{\"path\":\"/vless\",\"headers\":{\"Host\":\"example.com\"}}}") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_command b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_command new file mode 100644 index 00000000..b30ecbc7 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_command @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\xff\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_ipv6_payload b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_ipv6_payload new file mode 100644 index 00000000..dfc870c6 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/bad_ipv6_payload @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x03\x00\x01") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/truncated_uuid b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/truncated_uuid new file mode 100644 index 00000000..4492b28c --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/truncated_uuid @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/unknown_flow b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/unknown_flow new file mode 100644 index 00000000..4441bb74 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/unknown_flow @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x05\x0a\x03bad\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_mux b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_mux new file mode 100644 index 00000000..78c6707a --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_mux @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x03") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_prefix_garbage_suffix b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_prefix_garbage_suffix new file mode 100644 index 00000000..4fd7368c --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_prefix_garbage_suffix @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\x0bexample.comGET /garbage HTTP/1.1\r\nHost: fuzz\r\n\r\n") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_domain b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_domain new file mode 100644 index 00000000..448757d7 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_domain @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv4 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv4 new file mode 100644 index 00000000..18af15db --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv4 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x00\x50\x01\x7f\x00\x00\x01") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv6 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv6 new file mode 100644 index 00000000..4696c40f --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_tcp_ipv6 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_udp_ipv4 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_udp_ipv4 new file mode 100644 index 00000000..c03f625a --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/valid_udp_ipv4 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x02\x00\x35\x01\x08\x08\x08\x08") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/wrong_uuid b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/wrong_uuid new file mode 100644 index 00000000..d4557501 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/wrong_uuid @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x91\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/xrv_flow b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/xrv_flow new file mode 100644 index 00000000..4965bb24 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSFirstPacket/xrv_flow @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x12\x0a\x10xtls-rprx-vision\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/bad_command b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/bad_command new file mode 100644 index 00000000..b30ecbc7 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/bad_command @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\xff\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/http_get_stage2 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/http_get_stage2 new file mode 100644 index 00000000..1fae13fe --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/http_get_stage2 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("GET /stage2 HTTP/1.1\r\nHost: example.com\r\n\r\n") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/truncated b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/truncated new file mode 100644 index 00000000..8a2fdf9f --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/truncated @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/valid_tcp_domain b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/valid_tcp_domain new file mode 100644 index 00000000..448757d7 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundFallbackPreAuth/valid_tcp_domain @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/bad_domain_length b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/bad_domain_length new file mode 100644 index 00000000..9fdb2d49 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/bad_domain_length @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\xffx") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/unknown_flow b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/unknown_flow new file mode 100644 index 00000000..4441bb74 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/unknown_flow @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x05\x0a\x03bad\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_mux b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_mux new file mode 100644 index 00000000..78c6707a --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_mux @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x03") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_domain b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_domain new file mode 100644 index 00000000..448757d7 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_domain @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_ipv4 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_ipv4 new file mode 100644 index 00000000..18af15db --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_tcp_ipv4 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x00\x50\x01\x7f\x00\x00\x01") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_udp_ipv4 b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_udp_ipv4 new file mode 100644 index 00000000..c03f625a --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/valid_udp_ipv4 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x02\x00\x35\x01\x08\x08\x08\x08") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/wrong_uuid b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/wrong_uuid new file mode 100644 index 00000000..d4557501 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/wrong_uuid @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x91\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x00\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/xrv_flow_rejected_on_raw b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/xrv_flow_rejected_on_raw new file mode 100644 index 00000000..4965bb24 --- /dev/null +++ b/fuzz/xraycore/testdata/fuzz/FuzzXrayCoreVLESSInboundProcessPreAuth/xrv_flow_rejected_on_raw @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\x00\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x12\x0a\x10xtls-rprx-vision\x01\x01\xbb\x02\x0bexample.com") diff --git a/fuzz/xraycore/vless_first_packet_fuzz_test.go b/fuzz/xraycore/vless_first_packet_fuzz_test.go new file mode 100644 index 00000000..696e86ea --- /dev/null +++ b/fuzz/xraycore/vless_first_packet_fuzz_test.go @@ -0,0 +1,238 @@ +package xraycorefuzz + +import ( + "bytes" + "testing" + "time" + + "github.com/xtls/xray-core/common/buf" + xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/proxy/vless" + vlessencoding "github.com/xtls/xray-core/proxy/vless/encoding" +) + +const ( + seedVLESSUUID = "11111111-1111-1111-1111-111111111111" + maxFirstPacketBytes = 16 << 10 + maxPacketIteration = time.Second +) + +func FuzzXrayCoreVLESSFirstPacket(f *testing.F) { + validSeeds := validVLESSFirstPacketSeeds(f) + for _, seed := range validSeeds { + f.Add(seed) + } + for _, seed := range mutateVLESSPacketSeeds(validSeeds[0]) { + f.Add(seed) + } + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxFirstPacketBytes) { + return + } + start := time.Now() + validator := mustVLESSValidator(t) + + checkDecodedVLESSRequest(t, data, validator, false) + if len(data) >= 18 { + checkDecodedVLESSRequest(t, data, validator, true) + } + + failIfSlow(t, start, maxPacketIteration) + }) +} + +func TestXrayCoreVLESSWrongUUIDRejected(t *testing.T) { + valid := mustValidVLESSFirstPacket(t) + wrongUUID := append([]byte(nil), valid...) + wrongUUID[1] ^= 0x80 + + validator := mustVLESSValidator(t) + userSentID, request, _, _, err := vlessencoding.DecodeRequestHeader(false, nil, bytes.NewReader(wrongUUID), validator) + if err == nil || request != nil || userSentID != nil { + t.Fatalf("wrong UUID packet authenticated: userSentID=%x request=%#v err=%v", userSentID, request, err) + } +} + +func checkDecodedVLESSRequest(t *testing.T, data []byte, validator *vless.MemoryValidator, isFirstBuffer bool) { + t.Helper() + + var ( + userSentID []byte + request *protocol.RequestHeader + err error + ) + if isFirstBuffer { + first := buf.FromBytes(append([]byte(nil), data...)) + reader := &buf.BufferedReader{ + Reader: buf.NewReader(bytes.NewReader(nil)), + Buffer: buf.MultiBuffer{first}, + } + userSentID, request, _, _, err = vlessencoding.DecodeRequestHeader(true, first, reader, validator) + } else { + userSentID, request, _, _, err = vlessencoding.DecodeRequestHeader(false, nil, bytes.NewReader(data), validator) + } + if err != nil { + return + } + if request == nil { + t.Fatal("DecodeRequestHeader returned nil request without error") + } + if request.User == nil || request.User.Account == nil { + t.Fatal("DecodeRequestHeader accepted packet without authenticated user") + } + account, ok := request.User.Account.(*vless.MemoryAccount) + if !ok { + t.Fatalf("DecodeRequestHeader returned unexpected account type %T", request.User.Account) + } + validID := mustVLESSID(t) + if !account.ID.Equals(validID) { + t.Fatalf("DecodeRequestHeader authenticated unexpected account %s", account.ID.String()) + } + if len(userSentID) != 16 { + t.Fatalf("DecodeRequestHeader returned malformed user id length %d", len(userSentID)) + } + switch request.Command { + case protocol.RequestCommandTCP, protocol.RequestCommandUDP, protocol.RequestCommandMux, protocol.RequestCommandRvs: + default: + t.Fatalf("DecodeRequestHeader accepted invalid command 0x%x", byte(request.Command)) + } + if request.Address == nil { + t.Fatal("DecodeRequestHeader accepted request without destination address") + } +} + +func mustVLESSValidator(t testing.TB) *vless.MemoryValidator { + t.Helper() + validator := new(vless.MemoryValidator) + user := &protocol.MemoryUser{ + Account: &vless.MemoryAccount{ + ID: mustVLESSID(t), + Encryption: "none", + }, + Email: "seed@example", + } + if err := validator.Add(user); err != nil { + t.Fatalf("failed to add VLESS seed user: %v", err) + } + return validator +} + +func mustVLESSID(t testing.TB) *protocol.ID { + t.Helper() + id, err := uuid.ParseString(seedVLESSUUID) + if err != nil { + t.Fatalf("invalid seed UUID: %v", err) + } + return protocol.NewID(id) +} + +func mustValidVLESSFirstPacket(t testing.TB) []byte { + t.Helper() + return mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443), &vlessencoding.Addons{}) +} + +func validVLESSFirstPacketSeeds(t testing.TB) [][]byte { + t.Helper() + return [][]byte{ + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.ParseAddress("127.0.0.1"), xnet.Port(80), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.ParseAddress("::1"), xnet.Port(443), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandUDP, xnet.ParseAddress("8.8.8.8"), xnet.Port(53), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandMux, nil, 0, &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandRvs, nil, 0, &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443), &vlessencoding.Addons{Flow: vless.XRV}), + vlessPacketWithRawAddons(t, []byte{0x0a, 0x03, 'b', 'a', 'd'}, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443)), + } +} + +func mustVLESSFirstPacket(t testing.TB, command protocol.RequestCommand, address xnet.Address, port xnet.Port, addons *vlessencoding.Addons) []byte { + t.Helper() + user := &protocol.MemoryUser{ + Account: &vless.MemoryAccount{ + ID: mustVLESSID(t), + Encryption: "none", + }, + Email: "seed@example", + } + request := &protocol.RequestHeader{ + Version: vlessencoding.Version, + Command: command, + Address: address, + Port: port, + User: user, + } + var out bytes.Buffer + if err := vlessencoding.EncodeRequestHeader(&out, request, addons); err != nil { + t.Fatalf("failed to build seed VLESS first packet: %v", err) + } + return out.Bytes() +} + +func vlessPacketWithRawAddons(t testing.TB, addons []byte, command protocol.RequestCommand, address xnet.Address, port xnet.Port) []byte { + t.Helper() + if len(addons) > 255 { + t.Fatalf("raw addons too large: %d", len(addons)) + } + base := mustVLESSFirstPacket(t, command, address, port, &vlessencoding.Addons{}) + out := make([]byte, 0, len(base)+len(addons)) + out = append(out, base[:17]...) + out = append(out, byte(len(addons))) + out = append(out, addons...) + out = append(out, base[18:]...) + return out +} + +func mutateVLESSPacketSeeds(valid []byte) [][]byte { + seeds := [][]byte{ + {}, + {0x00}, + {0x00, 0x11}, + bytes.Repeat([]byte{0xff}, 32), + append([]byte(nil), valid[:1]...), + append([]byte(nil), valid[:17]...), + append([]byte(nil), valid[:18]...), + } + + wrongUUID := append([]byte(nil), valid...) + wrongUUID[1] ^= 0x80 + seeds = append(seeds, wrongUUID) + + badVersion := append([]byte(nil), valid...) + badVersion[0] = 0xff + seeds = append(seeds, badVersion) + + badCommand := append([]byte(nil), valid...) + if len(badCommand) > 18 { + badCommand[18] = 0xff + } + seeds = append(seeds, badCommand) + + badAddressType := append([]byte(nil), valid...) + if len(badAddressType) > 21 { + badAddressType[21] = 0xff + } + seeds = append(seeds, badAddressType) + + badDomainLength := append([]byte(nil), valid...) + if len(badDomainLength) > 22 { + badDomainLength[22] = 0xff + } + seeds = append(seeds, badDomainLength) + + oversizedAddons := append([]byte(nil), valid[:17]...) + oversizedAddons = append(oversizedAddons, 0xff, 0x01, 0x02, 0x03) + seeds = append(seeds, oversizedAddons) + + validWithGarbage := append([]byte(nil), valid...) + validWithGarbage = append(validWithGarbage, []byte("GET /garbage HTTP/1.1\r\nHost: fuzz\r\n\r\n")...) + seeds = append(seeds, validWithGarbage) + + badIPv6 := append([]byte(nil), valid[:18]...) + badIPv6 = append(badIPv6, byte(protocol.RequestCommandTCP), 0x01, 0xbb, byte(protocol.AddressTypeIPv6), 0x00, 0x01) + seeds = append(seeds, badIPv6) + + return seeds +} diff --git a/fuzz/xraycore/vless_inbound_process_fuzz_test.go b/fuzz/xraycore/vless_inbound_process_fuzz_test.go new file mode 100644 index 00000000..89f4aa52 --- /dev/null +++ b/fuzz/xraycore/vless_inbound_process_fuzz_test.go @@ -0,0 +1,406 @@ +package xraycorefuzz + +import ( + "bytes" + "context" + "errors" + "io" + stdnet "net" + "sync" + "testing" + "time" + + "github.com/xtls/xray-core/common/buf" + xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + xuuid "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/routing" + xconf "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/vless" + vlessencoding "github.com/xtls/xray-core/proxy/vless/encoding" + vinbound "github.com/xtls/xray-core/proxy/vless/inbound" + "github.com/xtls/xray-core/transport" +) + +const maxInboundProcessIteration = 1500 * time.Millisecond + +func FuzzXrayCoreVLESSInboundProcessPreAuth(f *testing.F) { + for _, seed := range stage2VLESSProcessSeeds(f) { + f.Add(seed) + } + handler := mustStage2PlainVLESSHandler(f) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxFirstPacketBytes) { + return + } + start := time.Now() + dispatcher := newRecordingDispatcher() + conn := newVLESSFuzzConn(data) + err := handler.Process(vlessProcessContext(), xnet.Network_TCP, conn, dispatcher) + + expectedDispatch := shouldVLESSProcessDispatch(t, data, false) + assertVLESSProcessOutcome(t, expectedDispatch, dispatcher, err) + failIfSlow(t, start, maxInboundProcessIteration) + }) +} + +func FuzzXrayCoreVLESSInboundFallbackPreAuth(f *testing.F) { + for _, seed := range stage2VLESSFallbackSeeds(f) { + f.Add(seed) + } + handler := mustStage2FallbackVLESSHandler(f) + + f.Fuzz(func(t *testing.T, data []byte) { + if tooLarge(data, maxFirstPacketBytes) { + return + } + start := time.Now() + dispatcher := newRecordingDispatcher() + conn := newVLESSFuzzConn(data) + err := handler.Process(vlessProcessContext(), xnet.Network_TCP, conn, dispatcher) + + expectedDispatch := shouldVLESSProcessDispatch(t, data, true) + assertVLESSProcessOutcome(t, expectedDispatch, dispatcher, err) + failIfSlow(t, start, maxInboundProcessIteration) + }) +} + +func TestXrayCoreVLESSInboundProcessValidSeedsDispatch(t *testing.T) { + handler := mustStage2PlainVLESSHandler(t) + for i, seed := range stage2ExpectedDispatchSeeds(t) { + dispatcher := newRecordingDispatcher() + err := handler.Process(vlessProcessContext(), xnet.Network_TCP, newVLESSFuzzConn(seed), dispatcher) + if err != nil { + t.Fatalf("valid seed %d was rejected: %v", i, err) + } + if !dispatcher.called { + t.Fatalf("valid seed %d did not reach DispatchLink", i) + } + } +} + +func TestXrayCoreVLESSInboundProcessRejectsMalformedSeeds(t *testing.T) { + handler := mustStage2PlainVLESSHandler(t) + for i, seed := range stage2ExpectedRejectSeeds(t) { + dispatcher := newRecordingDispatcher() + err := handler.Process(vlessProcessContext(), xnet.Network_TCP, newVLESSFuzzConn(seed), dispatcher) + if err == nil { + t.Fatalf("malformed seed %d returned nil error", i) + } + if dispatcher.called { + t.Fatalf("malformed seed %d reached DispatchLink", i) + } + } +} + +func TestXrayCoreVLESSTrailingGarbageDoesNotChangeHeader(t *testing.T) { + valid := mustValidVLESSFirstPacket(t) + withGarbage := append(append([]byte(nil), valid...), []byte("arbitrary trailing body bytes")...) + + left := mustDecodeVLESSHeader(t, valid, false) + right := mustDecodeVLESSHeader(t, withGarbage, false) + if left.destination != right.destination || left.command != right.command || left.flow != right.flow || left.userID != right.userID { + t.Fatalf("trailing bytes changed parsed header: %#v != %#v", left, right) + } +} + +func shouldVLESSProcessDispatch(t testing.TB, data []byte, firstBufferMode bool) bool { + t.Helper() + decoded, ok := decodeVLESSHeaderForOracle(t, data, firstBufferMode) + if !ok { + return false + } + switch decoded.flow { + case "": + case vless.XRV: + return false + default: + return false + } + switch decoded.command { + case protocol.RequestCommandTCP, protocol.RequestCommandUDP, protocol.RequestCommandMux: + return true + case protocol.RequestCommandRvs: + return false + default: + return false + } +} + +type decodedVLESSHeader struct { + userID string + command protocol.RequestCommand + destination string + flow string +} + +func mustDecodeVLESSHeader(t testing.TB, data []byte, firstBufferMode bool) decodedVLESSHeader { + t.Helper() + decoded, ok := decodeVLESSHeaderForOracle(t, data, firstBufferMode) + if !ok { + t.Fatalf("failed to decode VLESS header from valid seed") + } + return decoded +} + +func decodeVLESSHeaderForOracle(t testing.TB, data []byte, firstBufferMode bool) (decodedVLESSHeader, bool) { + t.Helper() + validator := mustVLESSValidator(t) + var ( + request *protocol.RequestHeader + addons *vlessencoding.Addons + err error + ) + if firstBufferMode { + if len(data) < 18 { + return decodedVLESSHeader{}, false + } + first := buf.FromBytes(append([]byte(nil), data...)) + reader := &buf.BufferedReader{ + Reader: buf.NewReader(bytes.NewReader(nil)), + Buffer: buf.MultiBuffer{first}, + } + _, request, addons, _, err = vlessencoding.DecodeRequestHeader(true, first, reader, validator) + } else { + _, request, addons, _, err = vlessencoding.DecodeRequestHeader(false, nil, bytes.NewReader(data), validator) + } + if err != nil || request == nil || request.User == nil || request.User.Account == nil || addons == nil { + return decodedVLESSHeader{}, false + } + account, ok := request.User.Account.(*vless.MemoryAccount) + if !ok || !account.ID.Equals(mustVLESSID(t)) || request.Address == nil { + return decodedVLESSHeader{}, false + } + return decodedVLESSHeader{ + userID: account.ID.String(), + command: request.Command, + destination: request.Destination().String(), + flow: addons.Flow, + }, true +} + +func assertVLESSProcessOutcome(t *testing.T, expectedDispatch bool, dispatcher *recordingDispatcher, err error) { + t.Helper() + if expectedDispatch { + if err != nil { + t.Fatalf("valid first packet was rejected before dispatch: %v", err) + } + if !dispatcher.called { + t.Fatal("valid first packet did not reach DispatchLink") + } + return + } + if dispatcher.called { + t.Fatalf("malformed or unauthorized first packet reached DispatchLink for %s", dispatcher.dest.String()) + } + if err == nil { + t.Fatal("malformed or unauthorized first packet returned nil error without dispatch") + } +} + +func stage2VLESSProcessSeeds(t testing.TB) [][]byte { + t.Helper() + seeds := append([][]byte{}, validVLESSFirstPacketSeeds(t)...) + seeds = append(seeds, mutateVLESSPacketSeeds(mustValidVLESSFirstPacket(t))...) + seeds = append(seeds, + vlessPacketWithRawAddons(t, bytes.Repeat([]byte{0x41}, 255), protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443)), + vlessPacketWithRawAddons(t, []byte{0x0a, 0x03, 'b', 'a', 'd'}, protocol.RequestCommandUDP, xnet.ParseAddress("8.8.8.8"), xnet.Port(53)), + append(mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.ParseAddress("127.0.0.1"), xnet.Port(80), &vlessencoding.Addons{}), 0, 1, 2, 3, 4), + ) + return seeds +} + +func stage2VLESSFallbackSeeds(t testing.TB) [][]byte { + t.Helper() + seeds := stage2VLESSProcessSeeds(t) + seeds = append(seeds, + []byte("GET /stage2 HTTP/1.1\r\nHost: example.com\r\n\r\n"), + []byte("POST /missing HTTP/1.1\r\nHost: example.com\r\nContent-Length: 4\r\n\r\nbody"), + []byte("* HTTP/2.0\r\n\r\n"), + ) + return seeds +} + +func stage2ExpectedDispatchSeeds(t testing.TB) [][]byte { + t.Helper() + return [][]byte{ + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.ParseAddress("127.0.0.1"), xnet.Port(80), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.ParseAddress("::1"), xnet.Port(443), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandUDP, xnet.ParseAddress("8.8.8.8"), xnet.Port(53), &vlessencoding.Addons{}), + mustVLESSFirstPacket(t, protocol.RequestCommandMux, nil, 0, &vlessencoding.Addons{}), + } +} + +func stage2ExpectedRejectSeeds(t testing.TB) [][]byte { + t.Helper() + valid := mustValidVLESSFirstPacket(t) + return [][]byte{ + {}, + {0}, + append([]byte(nil), valid[:17]...), + mutateVLESSPacketSeeds(valid)[7], + mutateVLESSPacketSeeds(valid)[8], + mutateVLESSPacketSeeds(valid)[9], + vlessPacketWithRawAddons(t, []byte{0x0a, 0x03, 'b', 'a', 'd'}, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443)), + mustVLESSFirstPacket(t, protocol.RequestCommandTCP, xnet.DomainAddress("example.com"), xnet.Port(443), &vlessencoding.Addons{Flow: vless.XRV}), + mustVLESSFirstPacket(t, protocol.RequestCommandRvs, nil, 0, &vlessencoding.Addons{}), + } +} + +var ( + stage2HandlersOnce sync.Once + stage2PlainHandler *vinbound.Handler + stage2FBHandler *vinbound.Handler + stage2HandlersErr error +) + +func mustStage2PlainVLESSHandler(t testing.TB) *vinbound.Handler { + t.Helper() + mustInitStage2Handlers(t) + return stage2PlainHandler +} + +func mustStage2FallbackVLESSHandler(t testing.TB) *vinbound.Handler { + t.Helper() + mustInitStage2Handlers(t) + return stage2FBHandler +} + +func mustInitStage2Handlers(t testing.TB) { + t.Helper() + stage2HandlersOnce.Do(func() { + stage2PlainHandler, stage2HandlersErr = newStage2VLESSHandler(nil) + if stage2HandlersErr != nil { + return + } + stage2FBHandler, stage2HandlersErr = newStage2VLESSHandler([]*vinbound.Fallback{ + {Path: "stage2-unreachable-fallback-target", Type: "stage2-invalid-network", Dest: "unused"}, + }) + }) + if stage2HandlersErr != nil { + t.Fatalf("failed to initialize stage2 VLESS handler: %v", stage2HandlersErr) + } +} + +func newStage2VLESSHandler(fallbacks []*vinbound.Fallback) (*vinbound.Handler, error) { + pbConfig, err := (&xconf.Config{ + OutboundConfigs: []xconf.OutboundDetourConfig{{Protocol: "freedom", Tag: "direct"}}, + }).Build() + if err != nil { + return nil, err + } + instance, err := core.New(pbConfig) + if err != nil { + return nil, err + } + user := protocol.ToProtoUser(&protocol.MemoryUser{ + Account: &vless.MemoryAccount{ + ID: protocol.NewID(mustVLESSUUID()), + Encryption: "none", + }, + Email: "seed@example", + }) + rawHandler, err := core.CreateObject(instance, &vinbound.Config{ + Clients: []*protocol.User{user}, + Fallbacks: fallbacks, + Decryption: "none", + }) + if err != nil { + return nil, err + } + handler, ok := rawHandler.(*vinbound.Handler) + if !ok { + return nil, errors.New("VLESS inbound config did not create *inbound.Handler") + } + return handler, nil +} + +func mustVLESSUUID() xuuid.UUID { + id, err := xuuid.ParseString(seedVLESSUUID) + if err != nil { + panic(err) + } + return id +} + +func vlessProcessContext() context.Context { + return session.ContextWithInbound(context.Background(), &session.Inbound{ + Source: xnet.TCPDestination(xnet.ParseAddress("203.0.113.10"), xnet.Port(50000)), + Local: xnet.TCPDestination(xnet.ParseAddress("127.0.0.1"), xnet.Port(443)), + Tag: "stage2-vless", + }) +} + +type recordingDispatcher struct { + called bool + dest xnet.Destination +} + +func newRecordingDispatcher() *recordingDispatcher { + return &recordingDispatcher{} +} + +func (*recordingDispatcher) Type() interface{} { return routing.DispatcherType() } +func (*recordingDispatcher) Start() error { return nil } +func (*recordingDispatcher) Close() error { return nil } + +func (d *recordingDispatcher) Dispatch(context.Context, xnet.Destination) (*transport.Link, error) { + return nil, errors.New("Dispatch should not be used by VLESS Process harness") +} + +func (d *recordingDispatcher) DispatchLink(_ context.Context, dest xnet.Destination, _ *transport.Link) error { + d.called = true + d.dest = dest + return nil +} + +type vlessFuzzConn struct { + reader *bytes.Reader + writes bytes.Buffer +} + +func newVLESSFuzzConn(data []byte) *vlessFuzzConn { + return &vlessFuzzConn{reader: bytes.NewReader(append([]byte(nil), data...))} +} + +func (c *vlessFuzzConn) Read(p []byte) (int, error) { + return c.reader.Read(p) +} + +func (c *vlessFuzzConn) Write(p []byte) (int, error) { + return c.writes.Write(p) +} + +func (*vlessFuzzConn) Close() error { + return nil +} + +func (*vlessFuzzConn) LocalAddr() stdnet.Addr { + return &stdnet.TCPAddr{IP: stdnet.ParseIP("127.0.0.1"), Port: 443} +} + +func (*vlessFuzzConn) RemoteAddr() stdnet.Addr { + return &stdnet.TCPAddr{IP: stdnet.ParseIP("203.0.113.10"), Port: 50000} +} + +func (*vlessFuzzConn) SetDeadline(time.Time) error { + return nil +} + +func (*vlessFuzzConn) SetReadDeadline(time.Time) error { + return nil +} + +func (*vlessFuzzConn) SetWriteDeadline(time.Time) error { + return nil +} + +var _ routing.Dispatcher = (*recordingDispatcher)(nil) +var _ interface { + io.Reader + io.Writer +} = (*vlessFuzzConn)(nil) diff --git a/go.mod b/go.mod index a51dc36b..641b4787 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/cloudwego/base64x v0.1.6 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect github.com/gin-contrib/sse v1.1.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -66,6 +67,7 @@ require ( github.com/miekg/dns v1.1.72 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/pires/go-proxyproto v0.11.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.6.0 // indirect @@ -97,6 +99,7 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 4946712b..c84b5155 100644 --- a/go.sum +++ b/go.sum @@ -266,6 +266,7 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=