3x-ui/fuzz/xraycore/config_fuzz_test.go

369 lines
11 KiB
Go
Raw Normal View History

2026-04-17 09:29:20 +00:00
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"}]
}]
}`