feat(xray): update xray-core to v26.10.21 and adapt panel

Bump xtls/xray-core to 1b1647cd9ddf (v26.10.21) and move the three
binary pins (DockerInit.sh, both release.yml fetches) in lockstep. The
v26.10.10 release was pulled upstream, so the old pins already 404.

XDRIVE (#7182): the core now runs XDRIVE only for VLESS inside VLESS
encryption, with no flow, and an outbound only over Mux; it refuses the
whole config otherwise. The panel offers XDRIVE only for VLESS, refuses
to save an inbound without decryption, never emits a client flow on an
XDRIVE stream (config, JSON subscription, forms), and turns Mux on when
an outbound picks XDRIVE. An inbound or template outbound stored before
the upgrade that the core would refuse is left out of the generated
config with a warning, so it no longer stops every other inbound. The
service "Google Drive" is now "google-drive" (old name read as a
deprecated alias); saves, the schema and both form adapters store the
new name. XDRIVE and MASQUE gain a client xmux; both schemas keep it
instead of stripping it on an inbound save.

Lua scripts (#7124, #7136): the core refuses a routing script beside
rules or domainStrategy, and a dns script beside the fallback/parallel
switches or a domain-scoped server. The panel always keeps its api
routing rule, so a template save now refuses any routing script and a
dns script in a refused combination, and a stored one is dropped from
the generated config so the core still starts. Noise masks gain a Lua
script that the loader compiles inside Build: the in-process build
(template save check, hot add) now resolves a relative script the way
the xray child does, beside the xray binary, instead of failing it. The
mask editor gets a Script field that replaces the noise list.

XDNS (#7140): a resolver takes https:// DoH addresses with worker,
tlsConfig and quicConfig. The mask editor registered only addrs, so a
save dropped the rest; it now edits all four.

Finalmask filter (#7206): a new TCP/UDP mask screens an inbound's source
IPs. The inbound mask editor offers it with its allow and block lists;
it is a no-op on a dialer and an older client core refuses the type, so
the share link and JSON subscription drop it like the Go link already
does.
This commit is contained in:
MHSanaei
2026-10-22 17:10:11 +02:00
parent a8d0700bf2
commit 61b129d5b3
43 changed files with 1055 additions and 95 deletions
+1
View File
@@ -715,6 +715,7 @@ func (s *SubJsonService) streamData(stream string, clientKey string) map[string]
}
}
delete(streamSettings, "sockopt")
dropServerOnlyFinalMasks(streamSettings["finalmask"])
// The inbound's own hop range becomes a mask first, so the global default yields to it.
maskcompat.UpgradeLegacyUDPHop(streamSettings["finalmask"])
+21
View File
@@ -176,6 +176,27 @@ func TestSubJsonServiceMergesWithExistingFinalMask(t *testing.T) {
}
}
// The inbound "filter" mask (xray-core 26.10.21) screens the listener's source IPs: it would hand
// clients the server's IP policy, and a client core older than it refuses the unknown type.
func TestSubJsonServiceDropsTheServerOnlyFilterMask(t *testing.T) {
svc := NewSubJsonService("", "", "", "", nil)
stream := svc.streamData(`{"network":"kcp","security":"none","finalmask":{
"tcp":[{"type":"filter","settings":{"allowedSourceIPs":["10.0.0.0/8"]}}],
"udp":[{"type":"filter","settings":{"blockedSourceIPs":["geoip:cn"]}},{"type":"mkcp-legacy","settings":{"header":""}}]}}`, "")
finalmask, _ := stream["finalmask"].(map[string]any)
if _, ok := finalmask["tcp"]; ok {
t.Fatalf("finalmask.tcp = %#v, want it gone with its only mask", finalmask["tcp"])
}
udp, _ := finalmask["udp"].([]any)
if len(udp) != 1 {
t.Fatalf("finalmask.udp = %#v, want only the mkcp-legacy mask", udp)
}
if mask, _ := udp[0].(map[string]any); mask["type"] != "mkcp-legacy" {
t.Fatalf("finalmask.udp[0] = %#v, want mkcp-legacy", mask)
}
}
func TestSubJsonServiceNoFinalMaskWhenEmpty(t *testing.T) {
svc := NewSubJsonService("", "", "", "", nil)
stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`, "")
+26 -1
View File
@@ -1203,8 +1203,11 @@ func vlessClientEncryption(settings map[string]any) string {
}
// vlessFlowAllowed mirrors inboundCanEnableTlsFlow in internal/web/service: Vision runs over
// TLS/REALITY on raw TCP, or inside VLESS encryption on any transport.
// TLS/REALITY on raw TCP, or inside VLESS encryption on any transport but XDRIVE.
func vlessFlowAllowed(network, security string, settings map[string]any) bool {
if network == "xdrive" {
return false
}
if network == "tcp" && (security == "tls" || security == "reality") {
return true
}
@@ -2706,6 +2709,28 @@ var validFinalMaskTCPTypes = map[string]struct{}{
"xmc": {},
}
// dropServerOnlyFinalMasks removes the inbound "filter" mask (xray-core 26.10.21, #7206): it only
// screens the listener's source IPs, and a client core older than it refuses the unknown type.
func dropServerOnlyFinalMasks(finalmask any) {
fm, _ := finalmask.(map[string]any)
for _, layer := range []string{"tcp", "udp"} {
masks, ok := fm[layer].([]any)
if !ok {
continue
}
kept := slices.DeleteFunc(masks, func(entry any) bool {
mask, _ := entry.(map[string]any)
maskType, _ := mask["type"].(string)
return strings.EqualFold(maskType, "filter")
})
if len(kept) == 0 {
delete(fm, layer)
} else {
fm[layer] = kept
}
}
}
// applyKcpShareParams reconstructs legacy KCP share-link fields from either
// the historical kcpSettings.header/seed shape or the current finalmask model.
// This keeps subscription output compatible while avoiding panics when older
+1
View File
@@ -33,6 +33,7 @@ func TestVlessFlowAllowed(t *testing.T) {
{"xhttp tls vlessenc", "xhttp", "tls", enc, true},
{"xhttp reality no vlessenc", "xhttp", "reality", noEnc, false},
{"ws tls", "ws", "tls", noEnc, false},
{"xdrive vlessenc (xray-core #7182)", "xdrive", "none", enc, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
@@ -248,6 +248,9 @@ func TestGoldenInboundFullFixturesBuildInXray(t *testing.T) {
// TestGoldenStreamFixturesBuildInXray attaches the transport fragments — whole
// stream sections, the security block, sockopt and finalmask — to an inbound.
// goldenVlessDecryption carries an XDRIVE fixture, which xray-core 26.10.17 runs only inside VLESS encryption.
const goldenVlessDecryption = "mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E"
func TestGoldenStreamFixturesBuildInXray(t *testing.T) {
for _, category := range []string{"stream", "security", "sockopt", "finalmask"} {
for name, fixture := range goldenFixtures(t, category) {
@@ -263,11 +266,15 @@ func TestGoldenStreamFixturesBuildInXray(t *testing.T) {
case "finalmask":
stream["finalmask"] = fixture
}
decryption := "none"
if stream["network"] == "xdrive" {
decryption = goldenVlessDecryption
}
inbound := map[string]any{
"tag": "golden-in", "listen": "127.0.0.1", "port": 8443, "protocol": "vless",
"settings": map[string]any{
"clients": []any{map[string]any{"id": "b831381d-6324-4d53-ad4f-8cda48b30811", "email": "golden"}},
"decryption": "none",
"decryption": decryption,
},
"streamSettings": stream,
}
+55 -2
View File
@@ -666,6 +666,7 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
}
inbound.StreamSettings = canonicalizeStreamNetworkKey(inbound.StreamSettings)
inbound.StreamSettings = canonicalizeLegacyXdnsMasks(inbound.StreamSettings)
inbound.StreamSettings = canonicalizeXDriveService(inbound.StreamSettings)
}
// canonicalizeLegacyXdnsMasks stores an xdns mask posted in a pre-26.10.10 shape in
@@ -1006,6 +1007,11 @@ func validateFinalMaskSalamander(streamSettings string) error {
return maskcompat.CheckSalamanderPasswords(stream["finalmask"])
}
const (
xdriveServiceGoogleDrive = "google-drive"
xdriveServiceGoogleDriveLegacy = "Google Drive"
)
// validateXDriveStorage refuses at save the XDRIVE storage xray-core refuses when an inbound starts
// listening (stopping every inbound) or an outbound dials.
func validateXDriveStorage(streamSettings string) error {
@@ -1028,7 +1034,7 @@ func validateXDriveStorage(streamSettings string) error {
if !hasFolder {
return common.NewError(`XDRIVE needs a "remoteFolder": the local folder both ends share.`)
}
case "Google Drive":
case xdriveServiceGoogleDrive, xdriveServiceGoogleDriveLegacy:
if !hasFolder {
return common.NewError(`XDRIVE needs a "remoteFolder": the id of the shared Google Drive folder.`)
}
@@ -1041,11 +1047,52 @@ func validateXDriveStorage(streamSettings string) error {
}
return validateXDriveTemplate(xd.Template)
default:
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, Google Drive or template.", xd.Service))
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, google-drive or template.", xd.Service))
}
return nil
}
// validateXDriveInbound refuses what xray-core 26.10.17 refuses on an XDRIVE inbound (#7182): any
// protocol but VLESS, and VLESS without "decryption". Flow is never emitted there and the port is single.
func validateXDriveInbound(protocol, settings, streamSettings string) error {
var stream struct {
Network string `json:"network"`
}
if json.Unmarshal([]byte(streamSettings), &stream) != nil || stream.Network != "xdrive" {
return nil
}
if protocol != string(model.VLESS) {
return common.NewError(fmt.Sprintf("XDRIVE carries only VLESS, not %s.", protocol))
}
var vless struct {
Decryption string `json:"decryption"`
}
_ = json.Unmarshal([]byte(settings), &vless)
if !vlessEncValueSet(vless.Decryption) {
return common.NewError(`XDRIVE needs VLESS encryption: set the inbound's Decryption.`)
}
return nil
}
// canonicalizeXDriveService stores the "google-drive" service xray-core 26.10.17 names
// (#7182) in place of "Google Drive", which it still reads with a deprecation warning.
func canonicalizeXDriveService(streamSettings string) string {
var stream map[string]any
if json.Unmarshal([]byte(streamSettings), &stream) != nil {
return streamSettings
}
xd, _ := stream["xdriveSettings"].(map[string]any)
if xd == nil || xd["service"] != xdriveServiceGoogleDriveLegacy {
return streamSettings
}
xd["service"] = xdriveServiceGoogleDrive
out, err := json.MarshalIndent(stream, "", " ")
if err != nil {
return streamSettings
}
return string(out)
}
// validateXDriveTemplate applies the checks of xray-core's newTemplateStorage
// (transport/internet/xdrive/template.go), which only run once the listener starts.
func validateXDriveTemplate(raw json.RawMessage) error {
@@ -1254,6 +1301,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if err := validateXDriveStorage(inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateXDriveInbound(string(inbound.Protocol), inbound.Settings, inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateShadowsocks2022Inbound(inbound.Protocol, inbound.Settings); err != nil {
return inbound, false, err
}
@@ -1905,6 +1955,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
if err := validateXDriveStorage(inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateXDriveInbound(string(inbound.Protocol), inbound.Settings, inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateShadowsocks2022Inbound(inbound.Protocol, inbound.Settings); err != nil {
return inbound, false, err
}
+6 -3
View File
@@ -28,7 +28,7 @@ func inboundShadowsocksMethod(protocol, settings string) string {
}
// inboundCanEnableTlsFlow mirrors the frontend's canEnableTlsFlow: xray-core runs Vision over
// TLS/REALITY on raw TCP, or inside VLESS encryption (encryption.CommonConn) on any transport.
// TLS/REALITY on raw TCP, or inside VLESS encryption on any transport but XDRIVE (#7182).
func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool {
if protocol != string(model.VLESS) {
return false
@@ -37,8 +37,11 @@ func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool {
Network string `json:"network"`
Security string `json:"security"`
}
if json.Unmarshal([]byte(streamSettings), &stream) == nil && stream.Network == "tcp" &&
(stream.Security == "tls" || stream.Security == "reality") {
_ = json.Unmarshal([]byte(streamSettings), &stream)
if stream.Network == "xdrive" {
return false
}
if stream.Network == "tcp" && (stream.Security == "tls" || stream.Security == "reality") {
return true
}
return vlessEncryptionEnabled(settings)
@@ -26,10 +26,11 @@ func TestInboundCanEnableTlsFlow(t *testing.T) {
{"vmess tcp tls", string(model.VMESS), `{"network":"tcp","security":"tls"}`, "", false},
{"empty stream", string(model.VLESS), "", "", false},
// xray-core runs Vision inside VLESS encryption on any transport (encryption.CommonConn).
// xray-core runs Vision inside VLESS encryption on any transport (encryption.CommonConn)
// but XDRIVE, which refuses a flow since 26.10.17 (#7182).
{"vless raw vlessenc", string(model.VLESS), `{"network":"tcp","security":"none"}`, `{"decryption":"mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E","encryption":"mlkem768x25519plus.native.0rtt.hT4AY_tPWY9NVuKR3BIXxXq6zx9DqN2X86QPYW09XEM"}`, true},
{"vless ws vlessenc", string(model.VLESS), `{"network":"ws","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true},
{"vless xdrive vlessenc", string(model.VLESS), `{"network":"xdrive","security":"none"}`, `{"decryption":"` + vlessEncValue + `"}`, true},
{"vless xdrive vlessenc", string(model.VLESS), `{"network":"xdrive","security":"none"}`, `{"decryption":"` + vlessEncValue + `"}`, false},
{"vless xdrive no encryption", string(model.VLESS), `{"network":"xdrive","security":"none"}`, `{"decryption":"none"}`, false},
// XHTTP + VLESS encryption (the #5157 case).
+109 -1
View File
@@ -1,20 +1,128 @@
package service
import (
"encoding/json"
"slices"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func xdriveInbound(port int, xdriveSettings string) *model.Inbound {
return &model.Inbound{
Enable: true, Listen: "127.0.0.1", Port: port, Protocol: model.VLESS,
Settings: `{"clients":[],"decryption":"none"}`,
Settings: `{"clients":[],"decryption":"` + goldenVlessDecryption + `"}`,
StreamSettings: `{"network":"xdrive","security":"none","xdriveSettings":` + xdriveSettings + `}`,
}
}
// xray-core 26.10.17 (#7182) refuses to start an XDRIVE inbound that is not VLESS with
// "decryption" set, and with it every other inbound of the config.
func TestSavePathsRefuseXDriveOutsideVlessEncryption(t *testing.T) {
const local = `{"service":"local","remoteFolder":"/tmp/drive"}`
vmess := xdriveInbound(18490, local)
vmess.Protocol = model.VMESS
vmess.Settings = `{"clients":[]}`
plain := xdriveInbound(18491, local)
plain.Settings = `{"clients":[],"decryption":"none"}`
tests := []struct {
name string
save func(t *testing.T) error
want string
}{
{"vmess inbound", func(t *testing.T) error {
_, _, err := (&InboundService{}).AddInbound(vmess)
return err
}, "XDRIVE carries only VLESS, not vmess"},
{"vless inbound without decryption", func(t *testing.T) error {
_, _, err := (&InboundService{}).AddInbound(plain)
return err
}, "XDRIVE needs VLESS encryption"},
{"xray template inbound without decryption", func(t *testing.T) error {
return (&XraySettingService{}).SaveXraySetting(`{"inbounds":[{"tag":"drive","port":18492,"protocol":"vless",
"settings":` + plain.Settings + `,"streamSettings":` + plain.StreamSettings + `}],
"outbounds":[{"protocol":"freedom","tag":"direct"}]}`)
}, "XDRIVE needs VLESS encryption"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
setupBulkDB(t)
if err := tc.save(t); err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("save err = %v, want one containing %q", err, tc.want)
}
})
}
}
// An XDRIVE inbound saved before xray-core 26.10.17 without VLESS encryption makes the core refuse
// the whole config at startup; the build leaves that inbound out so the others keep running.
func TestGetXrayConfig_LeavesOutAStoredXDriveInboundTheCoreRefuses(t *testing.T) {
seedVlessInbound(t, "plain-drive", 43130, nil)
if err := database.GetDB().Model(&model.Inbound{}).Where("tag = ?", "plain-drive").
Update("stream_settings", `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`).Error; err != nil {
t.Fatalf("set stream: %v", err)
}
cfg, err := (&XrayService{}).GetXrayConfig()
if err != nil {
t.Fatalf("GetXrayConfig: %v", err)
}
for _, ib := range cfg.InboundConfigs {
if ib.Tag == "plain-drive" {
t.Fatalf("XDRIVE inbound without decryption emitted: %s", ib.Settings)
}
}
}
// A template XDRIVE outbound from before 26.10.17 lacks the VLESS encryption and mux the core now
// requires; the build drops only that one instead of letting the core refuse the whole config.
func TestGetXrayConfig_DropsATemplateXDriveOutboundTheCoreRefuses(t *testing.T) {
setupConflictDB(t)
const stream = `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`
template := `{"outbounds":[{"protocol":"freedom","tag":"direct"},
{"protocol":"vless","tag":"drive-plain","streamSettings":` + stream + `,
"settings":{"address":"10.0.0.1","port":443,"id":"11111111-2222-4333-8444-555555555555","encryption":"none"}},
{"protocol":"vless","tag":"drive-encrypted","streamSettings":` + stream + `,"mux":{"enabled":true,"concurrency":8},
"settings":{"address":"10.0.0.1","port":443,"id":"11111111-2222-4333-8444-555555555555",
"encryption":"mlkem768x25519plus.native.0rtt.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E"}}]}`
if err := (&SettingService{}).saveSetting("xrayTemplateConfig", template); err != nil {
t.Fatalf("seed template: %v", err)
}
cfg, err := (&XrayService{}).GetXrayConfig()
if err != nil {
t.Fatalf("GetXrayConfig: %v", err)
}
var outbounds []struct {
Tag string `json:"tag"`
}
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
t.Fatalf("decode emitted outbounds: %v", err)
}
var tags []string
for _, ob := range outbounds {
tags = append(tags, ob.Tag)
}
if !slices.Equal(tags, []string{"direct", "drive-encrypted"}) {
t.Fatalf("emitted outbound tags = %v, want [direct drive-encrypted]", tags)
}
}
// xray-core 26.10.17 renamed the service to "google-drive" and reads "Google Drive" only as a
// deprecated alias, so a save posting the old name stores the new one.
func TestAddInboundStoresTheGoogleDriveServiceUnderItsNewName(t *testing.T) {
setupBulkDB(t)
in, _, err := (&InboundService{}).AddInbound(xdriveInbound(18493,
`{"service":"Google Drive","remoteFolder":"f","secrets":["id","secret","token"]}`))
if err != nil {
t.Fatalf("AddInbound: %v", err)
}
if !strings.Contains(in.StreamSettings, `"service": "google-drive"`) {
t.Fatalf("stored streamSettings = %s, want service google-drive", in.StreamSettings)
}
}
// xray-core opens XDRIVE storage when the listener starts; a storage it refuses there stops every inbound.
func TestAddInboundRefusesXDriveStorageTheCoreCannotOpen(t *testing.T) {
tests := []struct {
+101
View File
@@ -184,6 +184,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
// pre-26.9.9 quicParams.udpHop, which the core ignores, or a udphop not last, which it cannot dial.
xrayConfig.OutboundConfigs = healOutboundStreams(xrayConfig.OutboundConfigs,
liftXhttpSessionIDKeys, upgradeStreamLegacyXdns, upgradeStreamLegacyUDPHop, normalizeStreamUDPHop)
xrayConfig.OutboundConfigs = dropXDriveOutboundsRefusedByCore(xrayConfig.OutboundConfigs)
// Bridge amneziawg outbounds before anything else reads OutboundConfigs;
// the core has no amneziawg proxy and would reject the raw entry.
if err := transformAmneziaWGOutbounds(xrayConfig); err != nil {
@@ -410,6 +411,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
}
xrayConfig.InboundConfigs = dropInboundsRefusedByXDrive(xrayConfig.InboundConfigs)
// Merge subscription-derived outbounds (if any) into the final outbounds array.
// These are additive: each subscription is placed before or after the template
@@ -471,6 +473,8 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
injectNodeEgresses(xrayConfig, nodes)
}
xrayConfig.RouterConfig = dropRefusedScript(xrayConfig.RouterConfig, "routing", routingScriptRefused)
xrayConfig.DNSConfig = dropRefusedScript(xrayConfig.DNSConfig, "dns", dnsScriptRefused)
return xrayConfig, nil
}
@@ -1708,6 +1712,103 @@ func healOutboundStreams(raw json_util.RawMessage, heals ...func(stream map[stri
return raw
}
// dropInboundsRefusedByXDrive keeps out an XDRIVE inbound stored before xray-core 26.10.17 made it
// VLESS-encryption only (#7182); the core would refuse the whole config and stop every inbound.
func dropInboundsRefusedByXDrive(inbounds []xray.InboundConfig) []xray.InboundConfig {
return slices.DeleteFunc(inbounds, func(ib xray.InboundConfig) bool {
err := validateXDriveInbound(ib.Protocol, string(ib.Settings), string(ib.StreamSettings))
if err != nil {
logger.Warningf("Inbound %q: not started, %v", ib.Tag, err)
}
return err != nil
})
}
// dropXDriveOutboundsRefusedByCore keeps out an XDRIVE outbound stored before xray-core 26.10.17
// required VLESS encryption and mux on it (#7182), which would stop the whole core at startup.
func dropXDriveOutboundsRefusedByCore(raw json_util.RawMessage) json_util.RawMessage {
var outbounds []json.RawMessage
if len(raw) == 0 || json.Unmarshal(raw, &outbounds) != nil {
return raw
}
kept := make([]json.RawMessage, 0, len(outbounds))
for _, ob := range outbounds {
var probe struct {
Tag string `json:"tag"`
StreamSettings struct {
Network string `json:"network"`
} `json:"streamSettings"`
}
if json.Unmarshal(ob, &probe) == nil && probe.StreamSettings.Network == "xdrive" {
if err := xray.ValidateOutboundConfig(ob); err != nil {
logger.Warningf("Outbound %q: not started, xray-core refuses it: %v", probe.Tag, err)
continue
}
}
kept = append(kept, ob)
}
if len(kept) == len(outbounds) {
return raw
}
if rewritten, err := json.Marshal(kept); err == nil {
return rewritten
}
return raw
}
// routingScriptRefused mirrors xray-core 26.10.17's RouterConfig.Build (#7136): a routing
// "script" replaces rules and domainStrategy, so the core refuses a config holding both.
func routingScriptRefused(routing map[string]any) bool {
script, _ := routing["script"].(string)
rules, _ := routing["rules"].([]any)
return script != "" && (routing["domainStrategy"] != nil || len(rules) > 0)
}
// dnsScriptRefused mirrors xray-core 26.10.17's DNSConfig.Build (#7136): a dns "script" refuses the
// fallback and parallel switches, and any server scoped by domains, skipFallback or finalQuery.
func dnsScriptRefused(dns map[string]any) bool {
if script, _ := dns["script"].(string); script == "" {
return false
}
for _, key := range []string{"disableFallback", "disableFallbackIfMatch", "enableParallelQuery"} {
if on, _ := dns[key].(bool); on {
return true
}
}
servers, _ := dns["servers"].([]any)
return slices.ContainsFunc(servers, func(entry any) bool {
server, _ := entry.(map[string]any)
skipFallback, _ := server["skipFallback"].(bool)
finalQuery, _ := server["finalQuery"].(bool)
return hasDNSDomains(server["domains"]) || skipFallback || finalQuery
})
}
func hasDNSDomains(domains any) bool {
switch v := domains.(type) {
case []any:
return len(v) > 0
case string:
return v != ""
}
return false
}
// dropRefusedScript drops a section's Lua "script" the core would refuse, logging why: the
// panel's own rules and DNS settings stay, since its stats path depends on the api rule.
func dropRefusedScript(raw json_util.RawMessage, section string, refused func(map[string]any) bool) json_util.RawMessage {
var parsed map[string]any
if len(raw) == 0 || json.Unmarshal(raw, &parsed) != nil || !refused(parsed) {
return raw
}
logger.Warningf("%s: ignoring \"script\" %v, xray-core refuses it beside this %s section's other settings", section, parsed["script"], section)
delete(parsed, "script")
if rewritten, err := json.Marshal(parsed); err == nil {
return rewritten
}
return raw
}
func upgradeStreamLegacyXdns(stream map[string]any) bool {
return maskcompat.UpgradeLegacyXdns(stream["finalmask"])
}
@@ -109,14 +109,19 @@ func TestGetXrayConfig_EnabledClientsStillEmitted(t *testing.T) {
}
// A flow kept from a TCP+REALITY stream makes xray demand Vision on a transport that cannot run it
// without VLESS encryption: "XTLS only supports TLS and REALITY directly".
// without VLESS encryption: "XTLS only supports TLS and REALITY directly". XDRIVE refuses it even
// inside VLESS encryption since xray-core 26.10.17 (#7182).
func TestGetXrayConfig_OmitsVisionFlowOnIneligibleStream(t *testing.T) {
cases := []struct {
name string
stream string
name string
stream string
settings string
}{
{"xdrive", `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`},
{"ws over tls", `{"network":"ws","security":"tls","wsSettings":{"path":"/"}}`},
{
"xdrive vlessenc", `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`,
`{"clients":[],"decryption":"` + goldenVlessDecryption + `"}`,
},
{"ws over tls", `{"network":"ws","security":"tls","wsSettings":{"path":"/"}}`, `{"clients":[],"decryption":"none"}`},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -125,7 +130,7 @@ func TestGetXrayConfig_OmitsVisionFlowOnIneligibleStream(t *testing.T) {
{Email: "vision@x", ID: "44444444-4444-4444-4444-444444444444", Enable: true, Flow: "xtls-rprx-vision"},
})
if err := database.GetDB().Model(&model.Inbound{}).Where("tag = ?", tag).
Update("stream_settings", tc.stream).Error; err != nil {
Updates(map[string]any{"stream_settings": tc.stream, "settings": tc.settings}).Error; err != nil {
t.Fatalf("set stream: %v", err)
}
value, _ := emittedClients(t, tag)
@@ -141,7 +146,7 @@ func TestGetXrayConfig_OmitsVisionFlowOnIneligibleStream(t *testing.T) {
}
}
// xray-core runs Vision over TLS/REALITY on RAW, or over VLESS encryption on any transport.
// xray-core runs Vision over TLS/REALITY on RAW, or over VLESS encryption on any transport but XDRIVE.
func TestGetXrayConfig_KeepsVisionFlowWhereTheCoreRunsIt(t *testing.T) {
const vlessenc = `{"clients":[],"decryption":"mlkem768x25519plus.native.600s.WB3OymHLL_DJ1jdHiqcMC-u84TgvfNFlgbZIJE-NtUQ"}`
cases := []struct {
@@ -152,7 +157,6 @@ func TestGetXrayConfig_KeepsVisionFlowWhereTheCoreRunsIt(t *testing.T) {
{"tcp reality", `{"network":"tcp","security":"reality","realitySettings":{}}`, `{"clients":[],"decryption":"none"}`},
{"raw vlessenc", `{"network":"tcp","security":"none"}`, vlessenc},
{"ws vlessenc", `{"network":"ws","security":"none","wsSettings":{"path":"/"}}`, vlessenc},
{"xdrive vlessenc", `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`, vlessenc},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
+22
View File
@@ -53,6 +53,9 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
if err != nil {
return common.NewError("xray template config invalid:", err)
}
if err := checkTemplateLuaScripts(xrayConfig); err != nil {
return err
}
for _, inbound := range xrayConfig.InboundConfigs {
if err := validateFinalMaskSalamander(string(inbound.StreamSettings)); err != nil {
return common.NewError("xray template inbound \""+inbound.Tag+"\":", err)
@@ -60,6 +63,9 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
if err := validateXDriveStorage(string(inbound.StreamSettings)); err != nil {
return common.NewError("xray template inbound \""+inbound.Tag+"\":", err)
}
if err := validateXDriveInbound(inbound.Protocol, string(inbound.Settings), string(inbound.StreamSettings)); err != nil {
return common.NewError("xray template inbound \""+inbound.Tag+"\":", err)
}
}
if len(xrayConfig.OutboundConfigs) > 0 {
var outbounds []json.RawMessage
@@ -111,6 +117,22 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
return nil
}
// checkTemplateLuaScripts refuses the Lua scripts xray-core 26.10.17 refuses (#7136): any routing
// "script", since SaveXraySetting always keeps the api rule, and a dns "script" beside refused settings.
func checkTemplateLuaScripts(cfg *xray.Config) error {
var routing map[string]any
if json.Unmarshal(cfg.RouterConfig, &routing) == nil {
if script, _ := routing["script"].(string); script != "" {
return common.NewError(`xray template routing: "script" cannot be used, xray-core refuses it beside routing rules and the panel always keeps its api rule.`)
}
}
var dns map[string]any
if json.Unmarshal(cfg.DNSConfig, &dns) == nil && dnsScriptRefused(dns) {
return common.NewError(`xray template dns: "script" cannot be used with disableFallback, disableFallbackIfMatch, enableParallelQuery, or a server's domains, skipFallback or finalQuery.`)
}
return nil
}
// shouldSkipLegacyUnencryptedOutboundRejection lets an older running Xray
// core accept an outbound that the newer embedded validator rejects solely
// because it is unencrypted and targets a public address. Unknown or malformed
+59
View File
@@ -5,6 +5,8 @@ import (
"errors"
"strings"
"testing"
"github.com/xtls/xray-core/infra/conf"
)
func TestShouldSkipLegacyUnencryptedOutboundRejection(t *testing.T) {
@@ -251,3 +253,60 @@ func TestEnsureStatsRouting_AcceptsInboundTagAsString(t *testing.T) {
t.Fatalf("api rule with string-form inboundTag should hoist to front, got %q\nfull: %s", got, out)
}
}
// xray-core 26.10.17 (#7136) refuses a routing "script" beside rules, which SaveXraySetting always
// keeps for the api inbound, and a dns "script" beside fallback switches or domain-scoped servers.
func TestCheckXrayConfig_RefusesLuaScriptsTheCoreRefuses(t *testing.T) {
const outbounds = `"outbounds":[{"protocol":"freedom","tag":"direct"}]`
tests := []struct{ name, template, want string }{
{"routing script", `{"routing":{"script":"route.lua"},` + outbounds + `}`, `routing: "script" cannot be used`},
{"dns script beside a domain-scoped server", `{"dns":{"script":"dns.lua","servers":[{"address":"1.1.1.1","domains":["example.com"]}]},` + outbounds + `}`, `dns: "script" cannot be used`},
{"dns script beside parallel queries", `{"dns":{"script":"dns.lua","enableParallelQuery":true,"servers":["1.1.1.1"]},` + outbounds + `}`, `dns: "script" cannot be used`},
{"dns script beside plain servers", `{"dns":{"script":"dns.lua","servers":["1.1.1.1",{"address":"8.8.8.8"}]},` + outbounds + `}`, ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := (&XraySettingService{}).CheckXrayConfig(tc.template)
if tc.want == "" {
if err != nil {
t.Fatalf("CheckXrayConfig = %v, want the script the core accepts kept", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("CheckXrayConfig = %v, want one containing %q", err, tc.want)
}
})
}
}
// A template saved under 26.10.10 can carry those scripts; the build drops them so the core still
// starts, and the vendored loader accepts the routing and dns sections it then emits.
func TestGetXrayConfig_DropsLuaScriptsTheCoreRefuses(t *testing.T) {
setupConflictDB(t)
template := `{"outbounds":[{"protocol":"freedom","tag":"direct"}],
"routing":{"domainStrategy":"AsIs","script":"route.lua","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]},
"dns":{"script":"dns.lua","servers":[{"address":"1.1.1.1","domains":["example.com"]}]}}`
if err := (&SettingService{}).saveSetting("xrayTemplateConfig", template); err != nil {
t.Fatalf("seed template: %v", err)
}
cfg, err := (&XrayService{}).GetXrayConfig()
if err != nil {
t.Fatalf("GetXrayConfig: %v", err)
}
router := new(conf.RouterConfig)
if err := json.Unmarshal(cfg.RouterConfig, router); err != nil {
t.Fatalf("decode routing: %v", err)
}
if _, err := router.Build(); err != nil {
t.Fatalf("xray-core refuses the emitted routing: %v", err)
}
dns := new(conf.DNSConfig)
if err := json.Unmarshal(cfg.DNSConfig, dns); err != nil {
t.Fatalf("decode dns: %v", err)
}
if _, err := dns.Build(); err != nil {
t.Fatalf("xray-core refuses the emitted dns: %v", err)
}
}
+52
View File
@@ -26,6 +26,7 @@ import (
routerService "github.com/xtls/xray-core/app/router/command"
statsService "github.com/xtls/xray-core/app/stats/command"
xnet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/infra/conf"
@@ -146,6 +147,7 @@ func (x *XrayAPI) AddInbound(inbound []byte) error {
logger.Debug("Failed to unmarshal inbound:", err)
return err
}
localizeNoiseScripts(conf.StreamSetting)
config, err := conf.Build()
if err != nil {
logger.Debug("Failed to build inbound Detur:", err)
@@ -185,6 +187,7 @@ func ValidateOutboundConfig(outbound []byte) error {
if err := json.Unmarshal(outbound, detour); err != nil {
return err
}
localizeNoiseScripts(detour.StreamSetting)
built, err := detour.Build()
if err != nil {
return err
@@ -228,6 +231,7 @@ func (x *XrayAPI) AddOutbound(outbound []byte) error {
logger.Debug("Failed to unmarshal outbound:", err)
return err
}
localizeNoiseScripts(conf.StreamSetting)
config, err := conf.Build()
if err != nil {
logger.Debug("Failed to build outbound detour:", err)
@@ -475,6 +479,54 @@ func IsUserExistsErr(err error) bool {
// config builders run inside the panel process. The xray binary resolves assets
// relative to its own executable, but the panel binary lives one level above
// the bin folder, so an explicit location is required.
// localizeNoiseScripts points each noise mask's relative "script" at the file the xray child loads:
// xray-core 26.10.17 (#7124) compiles it inside Build, where the panel would look beside its own binary.
func localizeNoiseScripts(stream *conf.StreamConfig) {
if stream == nil || stream.FinalMask == nil {
return
}
for i := range stream.FinalMask.Udp {
mask := &stream.FinalMask.Udp[i]
if !strings.EqualFold(mask.Type, "noise") || mask.Settings == nil {
continue
}
var settings map[string]any
if json.Unmarshal(*mask.Settings, &settings) != nil {
continue
}
script, _ := settings["script"].(string)
resolved := childLuaScriptPath(script)
if resolved == script {
continue
}
settings["script"] = resolved
if raw, err := json.Marshal(settings); err == nil {
localized := json.RawMessage(raw)
mask.Settings = &localized
}
}
}
// childLuaScriptPath resolves a relative Lua script the way the xray child does: it shares the
// panel's working directory, then looks in its own bin folder instead of the panel's.
func childLuaScriptPath(script string) string {
if script == "" || filepath.IsAbs(script) {
return script
}
if _, err := platform.ResolveLuaFile(script); err == nil {
return script
}
binDir, err := filepath.Abs(config.GetBinFolderPath())
if err != nil {
return script
}
inBin := filepath.Join(binDir, script)
if _, err := os.Stat(inBin); err != nil {
return script
}
return inBin
}
func ensureXrayAssetLocation() {
if os.Getenv("XRAY_LOCATION_ASSET") != "" || os.Getenv("xray.location.asset") != "" {
return
+21
View File
@@ -1,6 +1,8 @@
package xray
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -65,3 +67,22 @@ func TestValidateOutboundConfig_RejectsWireguardRemoteDNSThatIsNotAnIP(t *testin
t.Fatalf("IP remoteDNS entries must stay valid, got: %v", 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) {
bin := t.TempDir()
t.Setenv("XUI_BIN_FOLDER", bin)
t.Chdir(t.TempDir())
if err := os.WriteFile(filepath.Join(bin, "noise.lua"), []byte("function HandleNoise(plan) end\n"), 0o600); err != nil {
t.Fatalf("write script: %v", err)
}
outbound := []byte(`{
"protocol": "vless",
"settings": {"address": "10.0.0.1", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
"streamSettings": {"network": "tcp", "finalmask": {"udp": [{"type": "noise", "settings": {"script": "noise.lua"}}]}}
}`)
if err := ValidateOutboundConfig(outbound); err != nil {
t.Fatalf("ValidateOutboundConfig = %v, want the script beside the xray binary found", err)
}
}