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

Move the three Xray binary pins (DockerInit.sh, both release.yml
fetches) to v26.10.10, in lockstep with the xtls/xray-core module that
82c77111 already bumped to 701af60772cd.

xdns finalmask (#7090): a domain now takes a "names" list instead of a
single "name", and a resolver is an "addrs" list of udp:// or tcp://
URLs instead of {type, settings:{addr}}. The loader ignores the old
keys rather than failing, so every mask the 26.9.30 seeder wrote would
load with no domain and no resolver and the tunnel would carry nothing.
maskcompat.UpgradeLegacyXdns now lifts both the pre-26.9.30 string lists
and the 26.9.30 objects straight to names/addrs, dropping entries the
old loader refused. A new seeder (XdnsFinalmaskNamesAddrsFix) reruns it
over every stored finalmask, and the save path, GetXrayConfig, both
link importers and the mask editor's mount lift go through it. Template
outbounds now get the build-time heal too: a 26.9.30 template pasted
after the seeder passes the save check, since the core loads that shape
without error. The editor edits names and addrs as tag lists and no
longer requires record types, which the core now defaults (TXT on a
client, A/CNAME/TXT/AAAA on a server). The wire format did not change,
but clients on an older core cannot read the new config shape.

Lua scripts (#6823): dns.script, routing.script and dns servers[].id
are new. The xray page parsed the dns block with a strict schema, so
loading it dropped script and id and the next save deleted them; the
schema now keeps both and the DNS server modal carries the id through
an edit. The hot routing apply built routing in-process, and that build
now resolves the script file from the panel's working directory, so a
script beside the xray binary failed every rule change into a full
restart and broke the node bridge. RoutingService cannot change the
script anyway, so the panel no longer resolves it there.

TLS client roots (#7105): the core now verifies against its bundled
Mozilla roots instead of the OS store unless tlsSettings.useSystemCA is
set. The outbound TLS form gets a switch for it, so an outbound to a
server signed by a privately installed CA can opt back in.
This commit is contained in:
MHSanaei
2026-10-11 07:55:12 +02:00
parent 82c7711189
commit 73a028f52d
37 changed files with 553 additions and 205 deletions
+4 -4
View File
@@ -87,10 +87,10 @@ 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.
// A panel on xray-core 26.9.30 shares xdns objects whose "name" and typed resolvers
// 26.10.10 ignores; imported verbatim, the outbound would dial with no domain.
func TestParseLink_UpgradesLegacyXdnsFinalMask(t *testing.T) {
fm := url.QueryEscape(`{"udp":[{"type":"xdns","settings":{"resolvers":["t.example.com+udp://8.8.8.8:53"]}}]}`)
fm := url.QueryEscape(`{"udp":[{"type":"xdns","settings":{"domains":[{"name":"t.example.com","types":[16],"edns0":1232}],"resolvers":[{"type":"udp","settings":{"addr":"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)
@@ -100,7 +100,7 @@ func TestParseLink_UpgradesLegacyXdnsFinalMask(t *testing.T) {
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"}]}`
want := `{"udp":[{"settings":{"domains":[{"edns0":1232,"names":["t.example.com"],"types":[16]}],"resolvers":[{"addrs":["udp://8.8.8.8:53"]}]},"type":"xdns"}]}`
if string(got) != want {
t.Fatalf("imported finalmask\n got: %s\nwant: %s", got, want)
}
+89 -31
View File
@@ -1,6 +1,9 @@
package maskcompat
import "strings"
import (
"slices"
"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.
@@ -8,8 +11,8 @@ 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.
// UpgradeLegacyXdns rewrites xdns masks into the names/addrs lists xray-core 26.10.10
// (#7090) parses: it ignores 26.9.30's name/type+settings keys and fails the string lists.
func UpgradeLegacyXdns(finalmask any) bool {
fm, _ := finalmask.(map[string]any)
masks, _ := fm["udp"].([]any)
@@ -31,48 +34,56 @@ func UpgradeLegacyXdns(finalmask any) bool {
func upgradeLegacyXdnsSettings(settings map[string]any) bool {
rawDomains, _ := settings["domains"].([]any)
rawResolvers, _ := settings["resolvers"].([]any)
if !hasLegacyXdnsEntry(rawDomains) && !hasLegacyXdnsEntry(rawResolvers) {
if !slices.ContainsFunc(rawDomains, isLegacyXdnsDomain) && !slices.ContainsFunc(rawResolvers, isLegacyXdnsResolver) {
return false
}
domains := make([]any, 0, len(rawDomains))
listed := map[string]bool{}
addDomain := func(domain map[string]any) {
key, _ := domain["name"].(string)
key = strings.ToLower(key)
if key != "" && listed[key] {
return
names, _ := domain["names"].([]any)
for _, name := range names {
if s, ok := name.(string); ok {
listed[strings.ToLower(s)] = true
}
}
listed[key] = true
domains = append(domains, domain)
}
addLegacyDomain := func(domain map[string]any) {
if name, _ := domain["names"].([]any)[0].(string); !listed[strings.ToLower(name)] {
addDomain(domain)
}
}
for _, entry := range rawDomains {
switch value := entry.(type) {
case map[string]any:
addDomain(value)
addDomain(upgradeXdnsDomainObject(value))
case string:
if domain, ok := legacyXdnsDomain(value); ok {
addDomain(domain)
addLegacyDomain(domain)
}
}
}
resolvers := make([]any, 0, len(rawResolvers))
for _, entry := range rawResolvers {
spec, ok := entry.(string)
if !ok {
switch value := entry.(type) {
case string:
head, addr, found := strings.Cut(value, "+udp://")
addr = strings.TrimSpace(addr)
domain, valid := legacyXdnsDomain(head)
if !found || addr == "" || !valid {
continue
}
addLegacyDomain(domain)
resolvers = append(resolvers, map[string]any{"addrs": []any{"udp://" + addr}})
case map[string]any:
if !isLegacyXdnsResolver(value) {
resolvers = append(resolvers, value)
} else if addr, ok := legacyXdnsResolverAddr(value); ok {
resolvers = append(resolvers, map[string]any{"addrs": []any{addr}})
}
default:
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 {
@@ -83,6 +94,40 @@ func upgradeLegacyXdnsSettings(settings map[string]any) bool {
return true
}
// upgradeXdnsDomainObject folds a 26.9.30 domain's single "name" into the "names" list.
func upgradeXdnsDomainObject(domain map[string]any) map[string]any {
raw, legacy := domain["name"]
if !legacy {
return domain
}
delete(domain, "name")
name, _ := raw.(string)
name = strings.TrimSpace(name)
names, _ := domain["names"].([]any)
if name != "" && !slices.Contains(names, any(name)) {
names = append([]any{name}, names...)
}
if names == nil {
names = []any{}
}
domain["names"] = names
return domain
}
// legacyXdnsResolverAddr turns a 26.9.30 {type, settings:{addr}} resolver into the URL
// the addrs list takes; that loader refused every type but udp and tcp.
func legacyXdnsResolverAddr(resolver map[string]any) (string, bool) {
kind, _ := resolver["type"].(string)
kind = strings.ToLower(strings.TrimSpace(kind))
settings, _ := resolver["settings"].(map[string]any)
addr, _ := settings["addr"].(string)
addr = strings.TrimSpace(addr)
if (kind != "udp" && kind != "tcp") || addr == "" {
return "", false
}
return kind + "://" + addr, 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), ""
@@ -94,14 +139,27 @@ func legacyXdnsDomain(spec string) (map[string]any, bool) {
if name == "" || !known {
return nil, false
}
return map[string]any{"name": name, "types": []any{recordType}, "edns0": legacyXdnsEDNS0}, true
return map[string]any{"names": []any{name}, "types": []any{recordType}, "edns0": legacyXdnsEDNS0}, true
}
func hasLegacyXdnsEntry(values []any) bool {
for _, value := range values {
if _, ok := value.(string); ok {
return true
}
func isLegacyXdnsDomain(value any) bool {
switch domain := value.(type) {
case string:
return true
case map[string]any:
_, legacy := domain["name"]
return legacy
}
return false
}
func isLegacyXdnsResolver(value any) bool {
switch resolver := value.(type) {
case string:
return true
case map[string]any:
_, current := resolver["addrs"]
return !current
}
return false
}
+69 -29
View File
@@ -2,14 +2,16 @@ package maskcompat
import (
"encoding/json"
"slices"
"testing"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
)
// 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 {
// coreXdnsView runs an xdns mask's settings through conf.XDNS, the loader the core
// calls at startup, and returns the domain names and resolvers the core would use.
func coreXdnsView(t *testing.T, settings any) (domains, resolvers []string, err error) {
t.Helper()
raw, err := json.Marshal(settings)
if err != nil {
@@ -17,42 +19,75 @@ func buildXdnsSettings(t *testing.T, settings any) error {
}
var mask conf.XDNS
if err := json.Unmarshal(raw, &mask); err != nil {
return err
return nil, nil, err
}
_, err = mask.Build()
return err
built, err := mask.Build()
if err != nil {
return nil, nil, err
}
config := built.(*xdns.Config)
for _, domain := range config.Domains {
domains = append(domains, domain.Name)
}
for _, resolver := range config.Resolvers {
resolvers = append(resolvers, resolver.Type+"://"+resolver.Addr)
}
return domains, resolvers, nil
}
func TestUpgradeLegacyXdns(t *testing.T) {
tests := []struct {
name string
mask string
want string
name string
mask string
want string
wantDomains []string
wantResolvers []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: "bare server domain becomes TXT with the legacy EDNS0 size",
mask: `{"type":"xdns","settings":{"domains":["t.example.com"]}}`,
want: `{"domains":[{"edns0":1232,"names":["t.example.com"],"types":[16]}]}`,
wantDomains: []string{"t.example.com"},
},
{
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: "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,"names":["a.example.com"],"types":[1]},{"edns0":1232,"names":["q.example.com"],"types":[28]},{"edns0":1232,"names":["t.example.com"],"types":[16]}]}`,
wantDomains: []string{"a.example.com", "q.example.com", "t.example.com"},
},
{
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: "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,"names":["t.example.com"],"types":[1]}],"resolvers":[{"addrs":["udp://8.8.8.8:53"]}]}`,
wantDomains: []string{"t.example.com"},
wantResolvers: []string{"udp://8.8.8.8:53"},
},
{
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: "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,"names":["t.example.com"],"types":[16]}],"resolvers":[{"addrs":["udp://1.1.1.1:53"]}]}`,
wantDomains: []string{"t.example.com"},
wantResolvers: []string{"udp://1.1.1.1:53"},
},
{
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]}]}`,
name: "string 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,"names":["t.example.com"],"types":[16]}]}`,
wantDomains: []string{"t.example.com"},
},
{
name: "26.9.30 domain and resolver objects move to names and addrs",
mask: `{"type":"xdns","settings":{"domains":[{"name":"t.example.com","types":[16,28],"edns0":1232,"lenLimit":200}],"resolvers":[{"type":"tcp","settings":{"addr":"8.8.8.8:53"}},{"type":"udp","settings":{"addr":"1.1.1.1:5353"}}],"extraPoll":1}}`,
want: `{"domains":[{"edns0":1232,"lenLimit":200,"names":["t.example.com"],"types":[16,28]}],"extraPoll":1,"resolvers":[{"addrs":["tcp://8.8.8.8:53"]},{"addrs":["udp://1.1.1.1:5353"]}]}`,
wantDomains: []string{"t.example.com"},
wantResolvers: []string{"tcp://8.8.8.8:53", "udp://1.1.1.1:5353"},
},
{
name: "26.9.30 resolvers the old loader refused are dropped",
mask: `{"type":"xdns","settings":{"domains":[{"name":"t.example.com"}],"resolvers":[{"type":"doh","settings":{"addr":"dns.example.com"}},{"type":"udp","settings":{}},{"type":"udp","settings":{"addr":"9.9.9.9:53"}}]}}`,
want: `{"domains":[{"names":["t.example.com"]}],"resolvers":[{"addrs":["udp://9.9.9.9:53"]}]}`,
wantDomains: []string{"t.example.com"},
wantResolvers: []string{"udp://9.9.9.9:53"},
},
}
for _, tc := range tests {
@@ -61,8 +96,9 @@ func TestUpgradeLegacyXdns(t *testing.T) {
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")
domains, resolvers, err := coreXdnsView(t, mask["settings"])
if err == nil && slices.Equal(domains, tc.wantDomains) && slices.Equal(resolvers, tc.wantResolvers) {
t.Fatal("the core already serves the legacy shape as intended; the upgrade is no longer needed")
}
finalmask := map[string]any{"udp": []any{mask}}
if !UpgradeLegacyXdns(finalmask) {
@@ -75,21 +111,25 @@ func TestUpgradeLegacyXdns(t *testing.T) {
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 {
domains, resolvers, err = coreXdnsView(t, mask["settings"])
if err != nil {
t.Fatalf("the core refuses the upgraded settings: %v", err)
}
if !slices.Equal(domains, tc.wantDomains) || !slices.Equal(resolvers, tc.wantResolvers) {
t.Fatalf("the core serves domains %v resolvers %v, want %v %v", domains, resolvers, tc.wantDomains, tc.wantResolvers)
}
})
}
}
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"}}]}`
const current = `{"udp":[{"type":"xdns","settings":{"domains":[{"names":["t.example.com","u.example.com"],"types":[16,28],"edns0":1232}],"resolvers":[{"addrs":["tcp://8.8.8.8:53","1.1.1.1"]}],"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")
t.Fatal("UpgradeLegacyXdns rewrote a mask that is already in the names/addrs shape")
}
var want map[string]any
_ = json.Unmarshal([]byte(current), &want)