fix(xray): refuse xhttp downloadSettings the core's dialer cannot use

ValidateOutboundConfig, the save-time gate shared by template saves,
API saves and outbound subscription refreshes, relied on conf.Build,
which accepts a downloadSettings block with no address or on another
transport. The core's XHTTP dialer dereferences that address and
asserts that transport on every dial, so an imported link or a
subscription carrying such a block panicked Xray, and every user's
connections with it, whenever traffic was routed to that outbound.

The built sender settings are now checked for both, and the outbound
is refused with a message that names the missing part.
This commit is contained in:
MHSanaei
2026-11-01 00:41:37 +01:00
parent 68e06a174b
commit c92c7718bf
3 changed files with 89 additions and 0 deletions
@@ -717,3 +717,19 @@ func TestOutboundSubscriptionRefreshCapsXMCMasks(t *testing.T) {
t.Fatalf("kept %d outbounds, tags by address = %v", len(tags), byAddress)
}
}
// The core's XHTTP dialer panics on a downloadSettings block without an address or on another transport.
func TestOutboundSubscriptionRefreshDropsXhttpDownloadSettingsTheCorePanicsOn(t *testing.T) {
setupSettingTestDB(t)
const server = "vless://00000000-0000-4000-8000-000000000000@%s:443?security=tls&type=xhttp&extra=%s#%s\n"
subID := serveOutboundSubscription(t, "p-", func(int) string {
return fmt.Sprintf(server, "1.1.1.1", url.QueryEscape(`{"downloadSettings":{}}`), "Empty") +
fmt.Sprintf(server, "8.8.8.8", url.QueryEscape(`{"downloadSettings":{"address":"8.8.4.4","port":443,"network":"tcp"}}`), "Tcp") +
fmt.Sprintf(server, "9.9.9.9", url.QueryEscape(`{"downloadSettings":{"address":"9.9.9.10","port":443,"network":"xhttp"}}`), "Complete")
})
want := map[string]string{"9.9.9.9": "p-complete"}
if _, got := refreshOutboundTags(t, subID); !maps.Equal(got, want) {
t.Fatalf("tags by address = %v, want %v", got, want)
}
}
+37
View File
@@ -23,6 +23,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
"github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/app/proxyman/command"
"github.com/xtls/xray-core/app/router"
routerService "github.com/xtls/xray-core/app/router/command"
@@ -42,6 +43,7 @@ import (
vlessOutbound "github.com/xtls/xray-core/proxy/vless/outbound"
"github.com/xtls/xray-core/proxy/vmess"
wireguard "github.com/xtls/xray-core/proxy/wireguard"
"github.com/xtls/xray-core/transport/internet/splithttp"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
@@ -221,9 +223,44 @@ func ValidateOutboundConfig(outbound []byte) (err error) {
if err != nil {
return err
}
if err := validateXhttpDownloadSettings(built.SenderSettings); err != nil {
return err
}
return validateHandlerSettings(built.ProxySettings)
}
// validateXhttpDownloadSettings refuses what conf.Build() lets through but the core's XHTTP
// dialer panics on at every dial: a downloadSettings block without an address or not on xhttp.
func validateXhttpDownloadSettings(settings *serial.TypedMessage) error {
if settings == nil {
return nil
}
instance, err := settings.GetInstance()
if err != nil {
return nil
}
sender, ok := instance.(*proxyman.SenderConfig)
if !ok {
return nil
}
transport, err := sender.StreamSettings.GetEffectiveTransportSettings()
if err != nil {
return nil
}
xhttp, ok := transport.(*splithttp.Config)
if !ok || xhttp.DownloadSettings == nil {
return nil
}
if xhttp.DownloadSettings.Address == nil {
return common.NewError("xhttp downloadSettings has no address")
}
transport, _ = xhttp.DownloadSettings.GetEffectiveTransportSettings()
if _, ok := transport.(*splithttp.Config); !ok {
return common.NewError("xhttp downloadSettings network is not xhttp")
}
return nil
}
// validateHandlerSettings refuses what conf.Build() lets through but the core parses only
// in the handler constructor, where one failure aborts the whole core start.
func validateHandlerSettings(settings *serial.TypedMessage) error {
+36
View File
@@ -71,6 +71,42 @@ func TestValidateOutboundConfig_RejectsWireguardRemoteDNSThatIsNotAnIP(t *testin
}
}
// The core's XHTTP dialer dereferences downloadSettings' address and casts its transport to
// XHTTP on every dial, so a block lacking either panics the whole core; conf.Build() lets it through.
func TestValidateOutboundConfig_RejectsXhttpDownloadSettingsTheDialerPanicsOn(t *testing.T) {
outbound := func(network, xhttpSettings string) []byte {
return []byte(`{
"protocol": "vless",
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
"streamSettings": {"network": "` + network + `", "security": "tls", "xhttpSettings": ` + xhttpSettings + `}
}`)
}
for _, rejected := range []string{
`{"downloadSettings": {}}`,
`{"downloadSettings": {"port": 443, "network": "xhttp"}}`,
`{"downloadSettings": {"address": null, "port": 443, "network": "xhttp"}}`,
`{"downloadSettings": {"address": "5.6.7.8", "port": 443}}`,
`{"downloadSettings": {"address": "5.6.7.8", "port": 443, "network": "tcp"}}`,
`{"downloadSettings": {"address": "5.6.7.8", "port": 443, "method": "ws"}}`,
`{"DOWNLOADSETTINGS": {}}`,
} {
err := ValidateOutboundConfig(outbound("xhttp", rejected))
if err == nil || !strings.Contains(err.Error(), "downloadSettings") {
t.Errorf("xhttpSettings %s: want a downloadSettings refusal, got %v", rejected, err)
}
}
for _, accepted := range [][]byte{
outbound("xhttp", `{"path": "/up"}`),
outbound("xhttp", `{"downloadSettings": {"address": "5.6.7.8", "port": 443, "network": "xhttp"}}`),
outbound("xhttp", `{"downloadSettings": {"address": "down.example", "port": 443, "network": "splithttp", "security": "tls", "xhttpSettings": {"path": "/down"}}}`),
outbound("tcp", `{"downloadSettings": {}}`),
} {
if err := ValidateOutboundConfig(accepted); err != nil {
t.Errorf("outbound %s must stay valid, got: %v", accepted, err)
}
}
}
// xray-core 26.10.17 compiles a noise mask's Lua script inside Build (#7124), where the panel would
// look beside its own binary; the xray child finds a relative script in its bin folder.
func TestValidateOutboundConfig_FindsANoiseScriptBesideTheXrayBinary(t *testing.T) {