Add Xray-core fuzz test harnesses

This commit is contained in:
F3dor 2026-04-17 12:29:20 +03:00
parent 169b216d7e
commit 9b9f7fc19d
43 changed files with 1222 additions and 0 deletions

64
fuzz/xraycore/README.md Normal file
View file

@ -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/<target>/`.
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.

View file

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

View file

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

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"inBounds\":[{\"listen\":\"\"}]}")

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"inbounds\":[{\"protocol\":\"vless\",\"port\":\"not-a-port\",\"settings\":{\"clients\":[]}}]}")

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"log\":{\"loglevel\":\"warning\"},\"inbounds\":[],\"outbounds\":[{\"protocol\":\"freedom\",\"tag\":\"direct\"}]}")

View file

@ -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\"}]}")

View file

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

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"clients\":[{\"id\":\"not-a-uuid\"}],\"decryption\":\"none\"}")

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"protocol\":\"freedom\",\"tag\":\"direct\",\"settings\":{\"domainStrategy\":\"AsIs\"}}")

View file

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

View file

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

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"domainStrategy\":\"IPIfNonMatch\",\"rules\":[{\"type\":\"field\",\"domain\":[\"domain:example.com\"],\"outboundTag\":\"direct\"}]}")

View file

@ -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\"]}}")

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"network\":\"ws\",\"wsSettings\":{\"path\":\"/vless\",\"headers\":{\"Host\":\"example.com\"}}}")

View file

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

View file

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

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x00\x11\x11")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("GET /stage2 HTTP/1.1\r\nHost: example.com\r\n\r\n")

View file

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x00\x11")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

3
go.mod
View file

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

1
go.sum
View file

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