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
+29 -1
View File
@@ -503,7 +503,9 @@ func (s *InboundService) getAllEmailSubIDs() (map[string]string, error) {
// Only vmess, vless, trojan, shadowsocks, hysteria, wireguard, and tunnel
// protocols use streamSettings (wireguard for finalmask UDP masks and sockopt on
// its listener; tunnel for sockopt, notably sockopt.tproxy for its TProxy/redirect
// mode).
// mode). Streams keyed on "method" — xray-core v26.7.11's preferred alias for
// "network" — are canonicalized to "network", which every panel reader (link
// generation, port-conflict detection, flow eligibility) keys on.
func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
protocolsWithStream := map[model.Protocol]bool{
model.VMESS: true,
@@ -517,7 +519,33 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
if !protocolsWithStream[inbound.Protocol] {
inbound.StreamSettings = ""
return
}
inbound.StreamSettings = canonicalizeStreamNetworkKey(inbound.StreamSettings)
}
// canonicalizeStreamNetworkKey rewrites a streamSettings JSON that names its
// transport under "method" to the panel-canonical "network" key. When both
// keys are present, "method" wins — matching xray-core's own precedence.
func canonicalizeStreamNetworkKey(streamSettings string) string {
if streamSettings == "" {
return streamSettings
}
var stream map[string]any
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return streamSettings
}
method, ok := stream["method"].(string)
if !ok || method == "" {
return streamSettings
}
stream["network"] = method
delete(stream, "method")
out, err := json.MarshalIndent(stream, "", " ")
if err != nil {
return streamSettings
}
return string(out)
}
// finalMaskRealityTcpMasks returns the stream's finalmask.tcp masks when the
+38 -7
View File
@@ -17,8 +17,34 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/util/link"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
// filterOutboundsRejectedByCore drops outbounds the vendored xray-core config
// loader refuses to build — since v26.7.11 that includes unencrypted
// vless/trojan outbounds to public addresses — because one such outbound in
// the merged config would keep the whole core from starting.
func filterOutboundsRejectedByCore(label string, outbounds []any) ([]any, []string) {
kept := make([]any, 0, len(outbounds))
var dropped []string
for _, ob := range outbounds {
raw, err := json.Marshal(ob)
if err == nil {
if buildErr := xray.ValidateOutboundConfig(raw); buildErr != nil {
tag := ""
if m, ok := ob.(map[string]any); ok {
tag, _ = m["tag"].(string)
}
logger.Warningf("%s: dropping outbound %q rejected by xray-core: %v", label, tag, buildErr)
dropped = append(dropped, fmt.Sprintf("%s: %v", tag, buildErr))
continue
}
}
kept = append(kept, ob)
}
return kept, dropped
}
// maxOutboundSubscriptionBytes caps a single outbound subscription response.
// It is larger than the 2 MiB user-facing subscription cap because an outbound
// subscription may aggregate many upstream outbounds into one document.
@@ -347,24 +373,28 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
}
identJSON, _ := json.Marshal(newIdent)
asAny := make([]any, len(parsed))
for i := range parsed {
asAny[i] = map[string]any(parsed[i])
}
kept, droppedByCore := filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), asAny)
// Persist the outbounds (as compact JSON array)
obsJSON, _ := json.Marshal(parsed)
obsJSON, _ := json.Marshal(kept)
sub.LastFetchedOutbounds = string(obsJSON)
sub.LinkIdentities = string(identJSON)
sub.LastUpdated = time.Now().Unix()
sub.LastError = ""
if len(droppedByCore) > 0 {
sub.LastError = fmt.Sprintf("dropped %d outbound(s) the xray core rejects: %s", len(droppedByCore), droppedByCore[0])
}
if err := database.GetDB().Save(sub).Error; err != nil {
return nil, err
}
// Return as []any for the config merger
result := make([]any, len(parsed))
for i := range parsed {
result[i] = parsed[i]
}
return result, nil
return kept, nil
}
func (s *OutboundSubscriptionService) recordError(sub *model.OutboundSubscription, err error) {
@@ -456,6 +486,7 @@ func (s *OutboundSubscriptionService) activeOutboundsSplit() (prepend []any, app
logger.Warningf("outbound sub %d has corrupt LastFetchedOutbounds: %v", sub.Id, err)
continue
}
arr, _ = filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), arr)
if sub.Prepend {
prepend = append(prepend, arr...)
} else {
@@ -0,0 +1,57 @@
package service
import (
"encoding/json"
"testing"
)
// TestCanonicalizeStreamNetworkKey covers xray-core v26.7.11's "method" alias
// for streamSettings "network": a config keyed on "method" (from an imported
// or API-authored inbound) must be folded back to the panel-canonical
// "network" key that every downstream reader — link generation, port-conflict
// detection, flow eligibility — depends on.
func TestCanonicalizeStreamNetworkKey(t *testing.T) {
tests := []struct {
name string
in string
wantNetwork string
wantMethod bool
}{
{
name: "method alias becomes network",
in: `{"method": "ws", "security": "tls"}`,
wantNetwork: "ws",
},
{
name: "method wins when both present",
in: `{"method": "grpc", "network": "tcp"}`,
wantNetwork: "grpc",
},
{
name: "plain network untouched",
in: `{"network": "tcp"}`,
wantNetwork: "tcp",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := canonicalizeStreamNetworkKey(tc.in)
var stream map[string]any
if err := json.Unmarshal([]byte(got), &stream); err != nil {
t.Fatalf("result is not valid JSON: %v", err)
}
if stream["network"] != tc.wantNetwork {
t.Fatalf("network = %v, want %q", stream["network"], tc.wantNetwork)
}
if _, hasMethod := stream["method"]; hasMethod {
t.Fatalf("method key must be removed, got %s", got)
}
})
}
}
func TestCanonicalizeStreamNetworkKey_EmptyPassthrough(t *testing.T) {
if got := canonicalizeStreamNetworkKey(""); got != "" {
t.Fatalf("empty stream must round-trip, got %q", got)
}
}
+15
View File
@@ -41,6 +41,21 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
if err != nil {
return common.NewError("xray template config invalid:", err)
}
if len(xrayConfig.OutboundConfigs) > 0 {
var outbounds []json.RawMessage
if err := json.Unmarshal(xrayConfig.OutboundConfigs, &outbounds); err != nil {
return common.NewError("xray template config invalid: outbounds is not an array:", err)
}
for _, outbound := range outbounds {
if err := xray.ValidateOutboundConfig(outbound); err != nil {
tagged := struct {
Tag string `json:"tag"`
}{}
_ = json.Unmarshal(outbound, &tagged)
return common.NewError("xray core rejects outbound \""+tagged.Tag+"\":", err)
}
}
}
return nil
}