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

Bump xtls/xray-core to 5ca6f4b7d4dc (v26.7.28) and move the three binary
pins (DockerInit.sh, the Linux and Windows URLs in release.yml) in lockstep
so the in-process conf.Build() validation and the child binary agree.

XMC finalmask (#6487) is the breaking change. The mask's `usernames` string
list is gone, replaced by a required `profiles` array whose entries each need
a 3-16 character [A-Za-z0-9_] username, a parseable UUID and both Mojang
texture fields; the "default to Dream when empty" fallback was removed, so an
xmc mask saved by an older panel now fails to build and takes the whole
config down with it rather than degrading one inbound.

The textures are a signed blob only Mojang's session server can issue, so a
legacy username cannot be upgraded automatically. The panel now:

- rejects an incomplete xmc mask at save time (AddInbound/UpdateInbound),
  pointing at the specific field that is missing;
- drops only the offending mask when generating the core config, for rows
  that never went through the form (upgrade, node sync, restored backup,
  direct DB edit), warning which inbound lost its obfuscation instead of
  leaving every inbound offline;
- carries legacy usernames into profile stubs in the finalmask form so the
  operator keeps their player names and sees exactly what still needs
  filling in, and edits profiles through a list editor.

No destructive DB migration: unlike the removed shadowsocks ciphers there is
no valid replacement to rewrite to, and dropping the mask from stored rows
would discard the operator's hostname and password for config they can still
repair. The generation-time strip already prevents the startup failure.

Also track the core's xmux maxConnections fallback, lowered from 6 to 3 for
anti-TSPU, in the fresh-XMUX seed so a new panel config matches what the core
would pick on its own.

TUN gained a `desc` key and random utunN naming, but the Go validator no
longer accepts TUN inbounds and the panel only renders legacy saved rows, so
nothing there needs adapting. The remaining commits are REALITY log-warning
wording, gRPC/XHTTP localAddr accuracy and a routing tweak, none of which
change the JSON config surface.

Tests cross-check the panel's profile predicate against conf.XMCProfile.Build()
so a future core release that tightens or relaxes the rules fails loudly
rather than silently emitting configs the core refuses to start on.
This commit is contained in:
Sanaei
2026-07-28 13:14:06 +02:00
parent fd17255f1d
commit 7f7b7e16a4
12 changed files with 541 additions and 26 deletions
+145
View File
@@ -8,10 +8,13 @@ import (
"errors"
"fmt"
"net"
"regexp"
"sort"
"strings"
"time"
"github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
@@ -592,6 +595,142 @@ func validateFinalMaskRealityCombo(streamSettings string) error {
return common.NewError("Finalmask is not supported with REALITY security — it crashes Xray-core on the first connection (see XTLS/Xray-core#6453). Remove the finalmask configuration or switch security to tls/none.")
}
var xmcProfileUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9_]{3,16}$`)
// xmcMaskProfilesComplete reports whether an xmc finalmask carries the signed
// Minecraft session profiles xray-core has required since v26.7.28 (#6487).
// The core replaced the old `usernames` string list with `profiles` objects
// and removed the "default to Dream when empty" fallback, so a mask still on
// the legacy shape — or one whose profiles are incomplete — now fails
// conf.XMC.Build() and takes the entire config down with it rather than
// degrading that one inbound.
//
// The texture fields are a signed blob only Mojang's session server can issue
// (resolve the UUID by username, then fetch the profile with unsigned=false),
// so the panel cannot synthesize a valid profile from a legacy username; an
// incomplete mask can only be reported or dropped.
func xmcMaskProfilesComplete(mask map[string]any) bool {
settings, ok := mask["settings"].(map[string]any)
if !ok {
return false
}
profiles, _ := settings["profiles"].([]any)
if len(profiles) == 0 {
return false
}
for _, entry := range profiles {
profile, ok := entry.(map[string]any)
if !ok {
return false
}
username, _ := profile["username"].(string)
if !xmcProfileUsernamePattern.MatchString(username) {
return false
}
id, _ := profile["uuid"].(string)
if _, err := uuid.Parse(id); err != nil {
return false
}
if value, _ := profile["texturesValue"].(string); value == "" {
return false
}
if signature, _ := profile["texturesSignature"].(string); signature == "" {
return false
}
}
return true
}
// isIncompleteXmcMask reports whether a finalmask.tcp entry is an xmc mask
// xray-core would refuse to build.
func isIncompleteXmcMask(entry any) bool {
mask, ok := entry.(map[string]any)
if !ok {
return false
}
if maskType, _ := mask["type"].(string); maskType != "xmc" {
return false
}
return !xmcMaskProfilesComplete(mask)
}
// incompleteXmcMaskCount counts the stream's xmc finalmask entries that
// xray-core would refuse to build.
func incompleteXmcMaskCount(stream map[string]any) int {
finalmask, ok := stream["finalmask"].(map[string]any)
if !ok {
return 0
}
tcp, _ := finalmask["tcp"].([]any)
count := 0
for _, entry := range tcp {
if isIncompleteXmcMask(entry) {
count++
}
}
return count
}
// stripIncompleteXmcMasks removes every xmc finalmask entry xray-core would
// refuse to build, returning how many were dropped, and clears the finalmask
// object once nothing is left in it.
//
// AddInbound and UpdateInbound reject an incomplete mask at save time, but a
// row that never went through those paths — an upgrade from a panel predating
// v26.7.28, node sync, a restored backup, a direct DB edit — would otherwise
// fail the whole config build and keep every other inbound offline too.
// Dropping only the offending mask degrades that one inbound instead, which
// the accompanying warning tells the admin to reconfigure.
func stripIncompleteXmcMasks(stream map[string]any) int {
finalmask, ok := stream["finalmask"].(map[string]any)
if !ok {
return 0
}
tcp, _ := finalmask["tcp"].([]any)
if len(tcp) == 0 {
return 0
}
kept := make([]any, 0, len(tcp))
dropped := 0
for _, entry := range tcp {
if isIncompleteXmcMask(entry) {
dropped++
continue
}
kept = append(kept, entry)
}
if dropped == 0 {
return 0
}
if len(kept) == 0 {
delete(finalmask, "tcp")
} else {
finalmask["tcp"] = kept
}
if len(finalmask) == 0 {
delete(stream, "finalmask")
}
return dropped
}
// validateFinalMaskXmcProfiles rejects an xmc finalmask without complete
// profiles at save time, so the admin gets a targeted error instead of a core
// that refuses to start (or, after GetXrayConfig heals it, an inbound quietly
// serving without the obfuscation they configured).
func validateFinalMaskXmcProfiles(streamSettings string) error {
if streamSettings == "" {
return nil
}
var stream map[string]any
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return nil
}
if incompleteXmcMaskCount(stream) == 0 {
return nil
}
return common.NewError("XMC finalmask requires at least one complete Minecraft profile — each needs a username (3-16 of A-Z a-z 0-9 _), a UUID, and both texture fields from Mojang's session server (XTLS/Xray-core#6487). Complete the profiles or remove the XMC mask.")
}
// normalizeMtprotoSecret rebuilds every mtproto client's FakeTLS secret so it is
// always valid before the row is persisted, and drops the vestigial inbound-level
// secret and adTag: MTProto is multi-client, so mtg and every share link read
@@ -725,6 +864,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateFinalMaskXmcProfiles(inbound.StreamSettings); err != nil {
return inbound, false, err
}
s.normalizeMtprotoSecret(inbound)
if err := s.normalizeMtprotoXrayPort(inbound, ""); err != nil {
return inbound, false, err
@@ -1148,6 +1290,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateFinalMaskXmcProfiles(inbound.StreamSettings); err != nil {
return inbound, false, err
}
s.normalizeMtprotoSecret(inbound)
inbound.SubSortIndex = normalizeSubSortIndex(inbound.SubSortIndex)