mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-06 13:15:01 +03:00
feat(xray): update xray-core to v26.9.30 and adapt panel
Bump xtls/xray-core to b26a91de4f (v26.9.30) and the three binary pins (DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted symbols; the sing and sing-shadowsocks indirect deps drop out with the SS2022 rewrite. XDNS finalmask (#6718) replaced its string lists with objects: domains are {name, types, edns0, lenLimit, labelLimit} and client resolvers {type, settings.addr}. The loader no longer parses the old lists, so a single stored xdns mask keeps the whole core from starting. The new leaf package internal/util/maskcompat converts them: "name[:type]" becomes a domain and "name[:type]+udp://addr" a domain plus a udp resolver. A bare name maps to TXT, the type legacy clients queried by default, and each converted domain keeps the 1232-byte EDNS0 the old code always used (without it the server caps answers at 512). It runs from: - the XdnsFinalmaskObjectsFix seeder, over inbound streams, hosts, the xray template, the global sub-JSON mask and cached subscription outbounds; - inbound save (normalizeStreamSettings) and GetXrayConfig, for rows that never went through the seeder; - both link importers, since fm= from an older panel carries the lists. The finalmask form edits the object shape (every key needs a registered field, or the finalmask watch drops it on save) and lifts legacy masks on open. The udp-mask golden fixture moves to the object shape, which TestGoldenStreamFixturesBuildInXray now builds through the core. The wire format changed as well, so pre-upgrade clients need the new core. WireGuard outbound (#6771) dropped settings.domainStrategy and the remoteDNS "local" mode. The endpoint lookup now follows sockopt.domainStrategy and in-tunnel targets the outbound's targetStrategy. The old key is silently ignored, which undid the IPv4-first endpoint lookup the WARP outbound depends on (#5205), and "local" now panics the core at startup because remoteDNS goes through netip.MustParseAddr. The WireguardDomainStrategyFix seeder moves a stored family preference to both keys (a value already set wins) and turns "local" into targetStrategy; the outbound form lifts legacy rows the same way and drops its select, the WARP modal writes the new placement, and the inbound form loses a field the server never read. ValidateOutboundConfig now refuses a non-IP remoteDNS entry, which conf.Build() lets through, on template save and for outbound subscriptions. Noise finalmask items accept type "exp" (#6862), a tag expression. The form offers it for noise items only: header-custom items go through the core's PraseByteSlice, which refuses it. TUN gained autoSystemDnsToGateway (Linux) and autoSystemWfpBlockLeak (Windows). Both pass through the settings schema so a value set in JSON survives the next form save. MASQUE (inbound, outbound, transport) and the XDRIVE transport are new protocols the panel does not offer yet; their new loader refusals only cover configs the panel never generates. The FakeDNS IPv6 pool default, the SS2022 rewrite (same gRPC account; emails are now deduped case-insensitively, as the panel already does), the restored udphop interval default and the rest change no panel-facing config.
This commit is contained in:
@@ -15,6 +15,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/maskcompat"
|
||||
)
|
||||
|
||||
// Outbound is the minimal shape we emit for each parsed link.
|
||||
@@ -766,6 +768,7 @@ func applyFinalMask(stream map[string]any, p url.Values) {
|
||||
var parsed any
|
||||
if json.Unmarshal([]byte(fm), &parsed) == nil {
|
||||
sanitizeFinalMaskQuicParams(parsed)
|
||||
maskcompat.UpgradeLegacyXdns(parsed)
|
||||
stream["finalmask"] = parsed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,25 @@ func TestParseVlessLink_FinalMaskQuicParamsSanitized(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A panel older than xray-core 26.9.30 shares its xdns mask in the string lists the
|
||||
// core no longer parses; imported verbatim, the outbound would fail the whole config.
|
||||
func TestParseLink_UpgradesLegacyXdnsFinalMask(t *testing.T) {
|
||||
fm := url.QueryEscape(`{"udp":[{"type":"xdns","settings":{"resolvers":["t.example.com+udp://8.8.8.8:53"]}}]}`)
|
||||
res, err := ParseLink("vless://uuid@1.2.3.4:53?type=kcp&security=none&fm=" + fm + "#dns")
|
||||
if err != nil {
|
||||
t.Fatalf("parse vless with fm: %v", err)
|
||||
}
|
||||
stream, _ := res.Outbound["streamSettings"].(map[string]any)
|
||||
got, err := json.Marshal(stream["finalmask"])
|
||||
if err != nil {
|
||||
t.Fatalf("marshal finalmask: %v", err)
|
||||
}
|
||||
want := `{"udp":[{"settings":{"domains":[{"edns0":1232,"name":"t.example.com","types":[16]}],"resolvers":[{"settings":{"addr":"8.8.8.8:53"},"type":"udp"}]},"type":"xdns"}]}`
|
||||
if string(got) != want {
|
||||
t.Fatalf("imported finalmask\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFinalMaskQuicParams_ClampsAndRejects(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package maskcompat
|
||||
|
||||
import "strings"
|
||||
|
||||
// legacyXdnsEDNS0 is the EDNS0 payload the pre-26.9.30 xdns always negotiated;
|
||||
// the object shape makes it opt-in and caps every answer at 512 bytes without it.
|
||||
const legacyXdnsEDNS0 = 1232
|
||||
|
||||
var legacyXdnsRecordTypes = map[string]int{"": 16, "txt": 16, "a": 1, "aaaa": 28}
|
||||
|
||||
// UpgradeLegacyXdns rewrites xdns masks from the string lists xray-core 26.9.30
|
||||
// (#6718) no longer parses into its object lists; one legacy mask fails the whole config.
|
||||
func UpgradeLegacyXdns(finalmask any) bool {
|
||||
fm, _ := finalmask.(map[string]any)
|
||||
masks, _ := fm["udp"].([]any)
|
||||
changed := false
|
||||
for _, entry := range masks {
|
||||
mask, _ := entry.(map[string]any)
|
||||
if maskType, _ := mask["type"].(string); !strings.EqualFold(maskType, "xdns") {
|
||||
continue
|
||||
}
|
||||
if settings, ok := mask["settings"].(map[string]any); ok && upgradeLegacyXdnsSettings(settings) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// upgradeLegacyXdnsSettings maps a bare name to TXT, the type legacy clients queried by
|
||||
// default, and drops entries the old core refused instead of keeping them to fail again.
|
||||
func upgradeLegacyXdnsSettings(settings map[string]any) bool {
|
||||
rawDomains, _ := settings["domains"].([]any)
|
||||
rawResolvers, _ := settings["resolvers"].([]any)
|
||||
if !hasLegacyXdnsEntry(rawDomains) && !hasLegacyXdnsEntry(rawResolvers) {
|
||||
return false
|
||||
}
|
||||
domains := make([]any, 0, len(rawDomains)+len(rawResolvers))
|
||||
listed := map[string]bool{}
|
||||
addDomain := func(domain map[string]any) {
|
||||
key, _ := domain["name"].(string)
|
||||
key = strings.ToLower(key)
|
||||
if key != "" && listed[key] {
|
||||
return
|
||||
}
|
||||
listed[key] = true
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
for _, entry := range rawDomains {
|
||||
switch value := entry.(type) {
|
||||
case map[string]any:
|
||||
addDomain(value)
|
||||
case string:
|
||||
if domain, ok := legacyXdnsDomain(value); ok {
|
||||
addDomain(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
resolvers := make([]any, 0, len(rawResolvers))
|
||||
for _, entry := range rawResolvers {
|
||||
spec, ok := entry.(string)
|
||||
if !ok {
|
||||
resolvers = append(resolvers, entry)
|
||||
continue
|
||||
}
|
||||
head, addr, found := strings.Cut(spec, "+udp://")
|
||||
addr = strings.TrimSpace(addr)
|
||||
domain, valid := legacyXdnsDomain(head)
|
||||
if !found || addr == "" || !valid {
|
||||
continue
|
||||
}
|
||||
addDomain(domain)
|
||||
resolvers = append(resolvers, map[string]any{
|
||||
"type": "udp",
|
||||
"settings": map[string]any{"addr": addr},
|
||||
})
|
||||
}
|
||||
settings["domains"] = domains
|
||||
if len(resolvers) > 0 {
|
||||
settings["resolvers"] = resolvers
|
||||
} else {
|
||||
delete(settings, "resolvers")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// legacyXdnsDomain parses the "name[:txt|a|aaaa]" spec both legacy lists used.
|
||||
func legacyXdnsDomain(spec string) (map[string]any, bool) {
|
||||
name, method := strings.TrimSpace(spec), ""
|
||||
if i := strings.LastIndex(name, ":"); i >= 0 {
|
||||
name, method = name[:i], strings.ToLower(name[i+1:])
|
||||
}
|
||||
name = strings.Trim(name, ".")
|
||||
recordType, known := legacyXdnsRecordTypes[method]
|
||||
if name == "" || !known {
|
||||
return nil, false
|
||||
}
|
||||
return map[string]any{"name": name, "types": []any{recordType}, "edns0": legacyXdnsEDNS0}, true
|
||||
}
|
||||
|
||||
func hasLegacyXdnsEntry(values []any) bool {
|
||||
for _, value := range values {
|
||||
if _, ok := value.(string); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package maskcompat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
)
|
||||
|
||||
// buildXdnsSettings runs an xdns mask's settings through conf.XDNS, the loader
|
||||
// the core calls at startup, so the test's verdict is the core's verdict.
|
||||
func buildXdnsSettings(t *testing.T, settings any) error {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal xdns settings: %v", err)
|
||||
}
|
||||
var mask conf.XDNS
|
||||
if err := json.Unmarshal(raw, &mask); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mask.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
func TestUpgradeLegacyXdns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mask string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare server domain becomes TXT with the legacy EDNS0 size",
|
||||
mask: `{"type":"xdns","settings":{"domains":["t.example.com"]}}`,
|
||||
want: `{"domains":[{"edns0":1232,"name":"t.example.com","types":[16]}]}`,
|
||||
},
|
||||
{
|
||||
name: "method suffixes map to their record types",
|
||||
mask: `{"type":"xdns","settings":{"domains":["a.example.com:a","q.example.com:AAAA","t.example.com:txt"]}}`,
|
||||
want: `{"domains":[{"edns0":1232,"name":"a.example.com","types":[1]},{"edns0":1232,"name":"q.example.com","types":[28]},{"edns0":1232,"name":"t.example.com","types":[16]}]}`,
|
||||
},
|
||||
{
|
||||
name: "client resolver splits into its domain and a udp resolver",
|
||||
mask: `{"type":"XDNS","settings":{"resolvers":["t.example.com:a+udp://8.8.8.8:53"]}}`,
|
||||
want: `{"domains":[{"edns0":1232,"name":"t.example.com","types":[1]}],"resolvers":[{"settings":{"addr":"8.8.8.8:53"},"type":"udp"}]}`,
|
||||
},
|
||||
{
|
||||
name: "a resolver for an already listed domain adds no duplicate",
|
||||
mask: `{"type":"xdns","settings":{"domains":["t.example.com"],"resolvers":["T.example.com+udp://1.1.1.1:53"]}}`,
|
||||
want: `{"domains":[{"edns0":1232,"name":"t.example.com","types":[16]}],"resolvers":[{"settings":{"addr":"1.1.1.1:53"},"type":"udp"}]}`,
|
||||
},
|
||||
{
|
||||
name: "entries the old core refused are dropped",
|
||||
mask: `{"type":"xdns","settings":{"domains":["t.example.com","m.example.com:mx"],"resolvers":["1.1.1.1:53"]}}`,
|
||||
want: `{"domains":[{"edns0":1232,"name":"t.example.com","types":[16]}]}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var mask map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.mask), &mask); err != nil {
|
||||
t.Fatalf("unmarshal mask: %v", err)
|
||||
}
|
||||
if err := buildXdnsSettings(t, mask["settings"]); err == nil {
|
||||
t.Fatal("the core accepted the legacy string shape; the upgrade is no longer needed")
|
||||
}
|
||||
finalmask := map[string]any{"udp": []any{mask}}
|
||||
if !UpgradeLegacyXdns(finalmask) {
|
||||
t.Fatal("UpgradeLegacyXdns reported no change for a legacy mask")
|
||||
}
|
||||
got, err := json.Marshal(mask["settings"])
|
||||
if err != nil {
|
||||
t.Fatalf("marshal upgraded settings: %v", err)
|
||||
}
|
||||
if string(got) != tc.want {
|
||||
t.Fatalf("upgraded settings\n got: %s\nwant: %s", got, tc.want)
|
||||
}
|
||||
if err := buildXdnsSettings(t, mask["settings"]); err != nil {
|
||||
t.Fatalf("the core refuses the upgraded settings: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeLegacyXdnsLeavesCurrentShapeAlone(t *testing.T) {
|
||||
const current = `{"udp":[{"type":"xdns","settings":{"domains":[{"name":"t.example.com","types":[16,28],"edns0":1232}],"resolvers":[{"type":"tcp","settings":{"addr":"8.8.8.8:53"}}],"extraPoll":2}},{"type":"salamander","settings":{"password":"x"}}]}`
|
||||
var finalmask map[string]any
|
||||
if err := json.Unmarshal([]byte(current), &finalmask); err != nil {
|
||||
t.Fatalf("unmarshal finalmask: %v", err)
|
||||
}
|
||||
if UpgradeLegacyXdns(finalmask) {
|
||||
t.Fatal("UpgradeLegacyXdns rewrote a mask that is already in the object shape")
|
||||
}
|
||||
var want map[string]any
|
||||
_ = json.Unmarshal([]byte(current), &want)
|
||||
got, _ := json.Marshal(finalmask)
|
||||
wantJSON, _ := json.Marshal(want)
|
||||
if string(got) != string(wantJSON) {
|
||||
t.Fatalf("finalmask changed\n got: %s\nwant: %s", got, wantJSON)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user