mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-12 07:36:07 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user