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

Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins
(DockerInit.sh, release.yml x2) in lockstep.

Adapt the panel to the upstream changes:

- Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from
  the core. A migration rewrites stored none/plain SS methods to a
  supported cipher and none/zero VMess security to "auto" (on both the
  clients column and inbound settings JSON); the SS build-time heal does
  the same so a row injected after boot cannot brick startup. The removed
  values are dropped from every frontend option list, schema and adapter,
  and coerced to "auto" at the Go link/sub/Clash emit sites and both link
  importers. Fix the CipherType_NONE sentinel that no longer compiles.

- Unencrypted vless/trojan outbounds to a public address are now refused
  by the core. Validate outbounds through the vendored config loader when
  saving the xray template and when storing/merging outbound
  subscriptions, so one such outbound cannot keep the core from starting.

- New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub
  link allowlist, the frontend enum and the FinalMask form (hostname,
  usernames, required password), and document it.

- streamSettings gained a "method" alias for "network"; canonicalize it
  to "network" at inbound save time and in the form adapters/schema so a
  method-keyed config keeps its transport.

- New root "env" config key is passed through xray.Config, compared in
  Equals, and forces a restart in the hot diff.

- REALITY now defaults minClientVer to 26.3.27; update the form
  placeholder.
This commit is contained in:
MHSanaei
2026-07-12 00:30:47 +02:00
parent affcf6c422
commit 814cda3fb4
39 changed files with 709 additions and 69 deletions
+17 -2
View File
@@ -171,6 +171,19 @@ func (x *XrayAPI) DelInbound(tag string) error {
return err
}
// ValidateOutboundConfig builds an outbound JSON object through the vendored
// xray-core config loader, surfacing the exact error the core would raise at
// startup — notably v26.7.11's refusal of unencrypted vless/trojan outbounds
// whose server address is a public IP or domain.
func ValidateOutboundConfig(outbound []byte) error {
detour := new(conf.OutboundDetourConfig)
if err := json.Unmarshal(outbound, detour); err != nil {
return err
}
_, err := detour.Build()
return err
}
// AddOutbound adds a new outbound configuration to the Xray core via gRPC.
func (x *XrayAPI) AddOutbound(outbound []byte) error {
if x.HandlerServiceClient == nil {
@@ -518,6 +531,8 @@ func buildUserAccount(protocolName string, user map[string]any) (*serial.TypedMe
var ssCipherType shadowsocks.CipherType
switch cipher {
case "aes-128-gcm":
ssCipherType = shadowsocks.CipherType_AES_128_GCM
case "aes-256-gcm":
ssCipherType = shadowsocks.CipherType_AES_256_GCM
case "chacha20-poly1305", "chacha20-ietf-poly1305":
@@ -525,10 +540,10 @@ func buildUserAccount(protocolName string, user map[string]any) (*serial.TypedMe
case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305
default:
ssCipherType = shadowsocks.CipherType_NONE
ssCipherType = shadowsocks.CipherType_UNKNOWN
}
if ssCipherType != shadowsocks.CipherType_NONE {
if ssCipherType != shadowsocks.CipherType_UNKNOWN {
return serial.ToTypedMessage(&shadowsocks.Account{
Password: password,
CipherType: ssCipherType,
+4
View File
@@ -24,6 +24,7 @@ type Config struct {
BurstObservatory json_util.RawMessage `json:"burstObservatory,omitempty"`
Metrics json_util.RawMessage `json:"metrics"`
Geodata json_util.RawMessage `json:"geodata,omitempty"`
Env json_util.RawMessage `json:"env,omitempty"`
}
// Equals compares two Config instances for deep equality.
@@ -78,5 +79,8 @@ func (c *Config) Equals(other *Config) bool {
if !bytes.Equal(c.Geodata, other.Geodata) {
return false
}
if !bytes.Equal(c.Env, other.Env) {
return false
}
return true
}
+1
View File
@@ -70,6 +70,7 @@ func ComputeHotDiff(oldCfg, newCfg *Config) (*HotDiff, bool) {
{"burstObservatory", oldCfg.BurstObservatory, newCfg.BurstObservatory},
{"metrics", oldCfg.Metrics, newCfg.Metrics},
{"geodata", oldCfg.Geodata, newCfg.Geodata},
{"env", oldCfg.Env, newCfg.Env},
}
for _, section := range static {
if !rawEqualNormalized(section.old, section.new) {
+6
View File
@@ -143,6 +143,12 @@ func TestComputeHotDiff_StaticSectionChangeNeedsRestart(t *testing.T) {
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
t.Fatal("observatory change must force a restart")
}
newCfg = makeHotConfig()
newCfg.Env = json_util.RawMessage(`{"XRAY_DNS_PATH":"/tmp/dns"}`)
if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
t.Fatal("env change must force a restart: env vars are read only at process start")
}
}
func TestComputeHotDiff_InboundAddRemoveChange(t *testing.T) {
+42
View File
@@ -0,0 +1,42 @@
package xray
import (
"strings"
"testing"
)
// TestValidateOutboundConfig_RejectsUnencryptedPublicVless covers xray-core
// v26.7.11's refusal to build an unencrypted vless outbound to a public
// address — the check now runs in-process, so the panel can surface it before
// a config reaches the core and bricks startup. A private-address outbound and
// a TLS outbound stay valid.
func TestValidateOutboundConfig_RejectsUnencryptedPublicVless(t *testing.T) {
publicPlaintext := `{
"protocol": "vless",
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
"streamSettings": {"network": "tcp", "security": "none"}
}`
if err := ValidateOutboundConfig([]byte(publicPlaintext)); err == nil {
t.Fatal("expected a public unencrypted vless outbound to be rejected")
} else if !strings.Contains(err.Error(), "prohibited") {
t.Fatalf("expected a prohibition error, got: %v", err)
}
privatePlaintext := `{
"protocol": "vless",
"settings": {"address": "10.0.0.1", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
"streamSettings": {"network": "tcp", "security": "none"}
}`
if err := ValidateOutboundConfig([]byte(privatePlaintext)); err != nil {
t.Fatalf("a private-address plaintext vless outbound must stay valid, got: %v", err)
}
publicTLS := `{
"protocol": "vless",
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
"streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "example.com"}}
}`
if err := ValidateOutboundConfig([]byte(publicTLS)); err != nil {
t.Fatalf("a TLS-secured public vless outbound must stay valid, got: %v", err)
}
}