mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-23 14:25:01 +03:00
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:
@@ -666,8 +666,8 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
|
||||
inbound.StreamSettings = canonicalizeLegacyXdnsMasks(inbound.StreamSettings)
|
||||
}
|
||||
|
||||
// canonicalizeLegacyXdnsMasks stores an xdns mask posted in the pre-26.9.30 string
|
||||
// lists in the object shape the core parses, as GetXrayConfig would heal it anyway.
|
||||
// canonicalizeLegacyXdnsMasks stores an xdns mask posted in a pre-26.10.10 shape in
|
||||
// the names/addrs lists the core reads, as GetXrayConfig would heal it anyway.
|
||||
func canonicalizeLegacyXdnsMasks(streamSettings string) string {
|
||||
if streamSettings == "" {
|
||||
return streamSettings
|
||||
|
||||
@@ -8,9 +8,13 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
const legacyXdnsStream = `{"network":"kcp","security":"none","kcpSettings":{"mtu":900},"finalmask":{"udp":[{"type":"xdns","settings":{"domains":["t.example.com"]}}]}}`
|
||||
// legacyXdnsStream carries the 26.9.30 xdns objects, whose "name" and typed resolvers
|
||||
// xray-core 26.10.10 ignores: the mask would load with no domain at all.
|
||||
const legacyXdnsStream = `{"network":"kcp","security":"none","kcpSettings":{"mtu":900},"finalmask":{"udp":[{"type":"xdns","settings":{"domains":[{"name":"t.example.com","types":[16],"edns0":1232}],"resolvers":[{"type":"udp","settings":{"addr":"8.8.8.8:53"}}]}}]}}`
|
||||
|
||||
func firstXdnsDomain(t *testing.T, stream map[string]any) any {
|
||||
const upgradedXdnsSettings = `{"domains":[{"edns0":1232,"names":["t.example.com"],"types":[16]}],"resolvers":[{"addrs":["udp://8.8.8.8:53"]}]}`
|
||||
|
||||
func assertXdnsSettingsUpgraded(t *testing.T, stream map[string]any) {
|
||||
t.Helper()
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
udp, _ := finalmask["udp"].([]any)
|
||||
@@ -18,17 +22,18 @@ func firstXdnsDomain(t *testing.T, stream map[string]any) any {
|
||||
t.Fatalf("finalmask.udp = %v, want one mask", finalmask["udp"])
|
||||
}
|
||||
mask, _ := udp[0].(map[string]any)
|
||||
settings, _ := mask["settings"].(map[string]any)
|
||||
domains, _ := settings["domains"].([]any)
|
||||
if len(domains) != 1 {
|
||||
t.Fatalf("xdns domains = %v, want one", settings["domains"])
|
||||
got, err := json.Marshal(mask["settings"])
|
||||
if err != nil {
|
||||
t.Fatalf("marshal xdns settings: %v", err)
|
||||
}
|
||||
if string(got) != upgradedXdnsSettings {
|
||||
t.Fatalf("xdns settings\n got: %s\nwant: %s", got, upgradedXdnsSettings)
|
||||
}
|
||||
return domains[0]
|
||||
}
|
||||
|
||||
// An API client can still post the pre-26.9.30 string lists; stored as sent they would
|
||||
// reach the sub links and the form in a shape the core no longer parses.
|
||||
func TestAddInbound_StoresXdnsMaskInObjectShape(t *testing.T) {
|
||||
// An API client or an older panel's export can still post the 26.9.30 shape; stored as
|
||||
// sent it would reach the sub links and the form in a shape the core silently empties.
|
||||
func TestAddInbound_StoresXdnsMaskInNamesShape(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
in := &model.Inbound{
|
||||
Tag: "in-45300-kcp", Enable: true, Listen: "0.0.0.0", Port: 45300, Protocol: model.VLESS,
|
||||
@@ -46,28 +51,16 @@ func TestAddInbound_StoresXdnsMaskInObjectShape(t *testing.T) {
|
||||
if err := json.Unmarshal([]byte(stored.StreamSettings), &stream); err != nil {
|
||||
t.Fatalf("stored stream is not JSON: %v", err)
|
||||
}
|
||||
domain, ok := firstXdnsDomain(t, stream).(map[string]any)
|
||||
if !ok || domain["name"] != "t.example.com" {
|
||||
t.Fatalf("stored xdns domain = %#v, want an object named t.example.com", firstXdnsDomain(t, stream))
|
||||
}
|
||||
assertXdnsSettingsUpgraded(t, stream)
|
||||
}
|
||||
|
||||
// A row that never went through the save path (restored backup, node sync, direct DB
|
||||
// edit) must still reach the core in a shape it builds, or it keeps every inbound down.
|
||||
// edit) must still reach the core with its xdns domains, or the mask serves nothing.
|
||||
func TestGetXrayConfig_UpgradesLegacyXdnsMask(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
seedInboundConflict(t, "in-45301-kcp", "127.0.0.1", 45301, model.VLESS,
|
||||
legacyXdnsStream, `{"clients":[],"decryption":"none"}`)
|
||||
|
||||
var legacy map[string]any
|
||||
if err := json.Unmarshal([]byte(`{"tag":"in-45301-kcp","listen":"127.0.0.1","port":45301,"protocol":"vless",
|
||||
"settings":{"clients":[],"decryption":"none"},"streamSettings":`+legacyXdnsStream+`}`), &legacy); err != nil {
|
||||
t.Fatalf("decode legacy inbound: %v", err)
|
||||
}
|
||||
if err := buildGoldenInbound(t, legacy); err == nil {
|
||||
t.Fatal("xray-core accepted the legacy xdns lists; the heal is no longer needed")
|
||||
}
|
||||
|
||||
cfg, err := (&XrayService{}).GetXrayConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("GetXrayConfig: %v", err)
|
||||
@@ -84,8 +77,39 @@ func TestGetXrayConfig_UpgradesLegacyXdnsMask(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &emitted); err != nil {
|
||||
t.Fatalf("decode emitted inbound: %v", err)
|
||||
}
|
||||
stream, _ := emitted["streamSettings"].(map[string]any)
|
||||
assertXdnsSettingsUpgraded(t, stream)
|
||||
assertXrayAccepts(t, "the healed xdns inbound", buildGoldenInbound(t, emitted))
|
||||
return
|
||||
}
|
||||
t.Fatal("inbound in-45301-kcp not found in the generated config")
|
||||
}
|
||||
|
||||
// A template pasted from a 26.9.30 panel after the seeder ran passes the save check,
|
||||
// since the core loads that shape without error; only the build-time heal catches it.
|
||||
func TestGetXrayConfig_UpgradesTemplateOutboundXdnsMask(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
template := `{"outbounds":[{"protocol":"freedom","tag":"direct"},{"protocol":"vless","tag":"dns-tunnel",
|
||||
"settings":{"vnext":[{"address":"t.example.com","port":53,"users":[{"id":"c9f0c2d0-0000-4000-8000-000000000000","encryption":"none"}]}]},
|
||||
"streamSettings":` + legacyXdnsStream + `}]}`
|
||||
if err := (&SettingService{}).saveSetting("xrayTemplateConfig", template); err != nil {
|
||||
t.Fatalf("seed template: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := (&XrayService{}).GetXrayConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("GetXrayConfig: %v", err)
|
||||
}
|
||||
var outbounds []map[string]any
|
||||
if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
|
||||
t.Fatalf("decode emitted outbounds: %v", err)
|
||||
}
|
||||
for _, outbound := range outbounds {
|
||||
if outbound["tag"] == "dns-tunnel" {
|
||||
stream, _ := outbound["streamSettings"].(map[string]any)
|
||||
assertXdnsSettingsUpgraded(t, stream)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("outbound dns-tunnel not found in the generated config")
|
||||
}
|
||||
|
||||
@@ -180,10 +180,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
xrayConfig.API = ensureAPIServices(xrayConfig.API)
|
||||
xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
|
||||
xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig)
|
||||
// Template outbounds authored before the xray-core #6258 XHTTP rename may
|
||||
// still carry sessionPlacement/sessionKey; lift them too (same reason as
|
||||
// the per-inbound lift below).
|
||||
xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
|
||||
// A pasted or restored template can still carry pre-#6258 XHTTP session keys or a
|
||||
// pre-26.10.10 xdns mask the core silently empties; heal them like the inbounds below.
|
||||
xrayConfig.OutboundConfigs = healOutboundStreams(xrayConfig.OutboundConfigs,
|
||||
liftXhttpSessionIDKeys, upgradeStreamLegacyXdns)
|
||||
// Bridge amneziawg outbounds before anything else reads OutboundConfigs;
|
||||
// the core has no amneziawg proxy and would reject the raw entry.
|
||||
if err := transformAmneziaWGOutbounds(xrayConfig); err != nil {
|
||||
@@ -376,7 +376,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
logger.Warningf("Inbound %q: dropping %d XMC finalmask mask(s) without complete Minecraft profiles — reconfigure them to restore the obfuscation (see XTLS/Xray-core#6487)", inbound.Tag, dropped)
|
||||
}
|
||||
|
||||
// A row that skipped the save path can still carry the pre-26.9.30 xdns lists.
|
||||
// A row that skipped the save path can still carry an xdns shape older than 26.10.10.
|
||||
maskcompat.UpgradeLegacyXdns(stream["finalmask"])
|
||||
|
||||
// xray-core v26.6.22 (#6258) renamed the XHTTP session keys and
|
||||
@@ -1712,11 +1712,9 @@ func liftXhttpSessionIDKeys(stream map[string]any) bool {
|
||||
return changed
|
||||
}
|
||||
|
||||
// liftOutboundsXhttpSessionIDKeys applies liftXhttpSessionIDKeys to every
|
||||
// outbound's streamSettings in the raw outbounds array. The original bytes are
|
||||
// returned untouched when nothing needs lifting, so an unchanged config never
|
||||
// looks modified to the hot-reload diff.
|
||||
func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMessage {
|
||||
// healOutboundStreams applies every heal to each outbound's streamSettings, returning the
|
||||
// original bytes when none changed anything so the hot-reload diff sees no edit.
|
||||
func healOutboundStreams(raw json_util.RawMessage, heals ...func(stream map[string]any) bool) json_util.RawMessage {
|
||||
if len(raw) == 0 {
|
||||
return raw
|
||||
}
|
||||
@@ -1727,8 +1725,10 @@ func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMess
|
||||
changed := false
|
||||
for _, ob := range outbounds {
|
||||
if stream, ok := ob["streamSettings"].(map[string]any); ok {
|
||||
if liftXhttpSessionIDKeys(stream) {
|
||||
changed = true
|
||||
for _, heal := range heals {
|
||||
if heal(stream) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1740,3 +1740,7 @@ func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMess
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func upgradeStreamLegacyXdns(stream map[string]any) bool {
|
||||
return maskcompat.UpgradeLegacyXdns(stream["finalmask"])
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ func TestLiftXhttpSessionIDKeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLiftOutboundsXhttpSessionIDKeys(t *testing.T) {
|
||||
func TestHealOutboundStreams_LiftsXhttpSessionIDKeys(t *testing.T) {
|
||||
raw := json_util.RawMessage(`[{"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"sessionKey":"x_session","sessionPlacement":"query"}}}]`)
|
||||
out := liftOutboundsXhttpSessionIDKeys(raw)
|
||||
out := healOutboundStreams(raw, liftXhttpSessionIDKeys)
|
||||
|
||||
var parsed []map[string]any
|
||||
if err := json.Unmarshal(out, &parsed); err != nil {
|
||||
@@ -75,7 +75,7 @@ func TestLiftOutboundsXhttpSessionIDKeys(t *testing.T) {
|
||||
|
||||
// Unchanged input must return byte-identical output (no spurious hot-reload).
|
||||
clean := json_util.RawMessage(`[{"protocol":"freedom"}]`)
|
||||
if got := liftOutboundsXhttpSessionIDKeys(clean); string(got) != string(clean) {
|
||||
if got := healOutboundStreams(clean, liftXhttpSessionIDKeys); string(got) != string(clean) {
|
||||
t.Fatalf("clean outbounds were rewritten: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "اسم الخادم",
|
||||
"verifyPeerName": "التحقق من اسم peer",
|
||||
"pinnedSha256": "SHA256 مثبت",
|
||||
"useSystemCA": "استخدام CA النظام",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "فاصل keep alive",
|
||||
|
||||
@@ -2047,6 +2047,7 @@
|
||||
"serverNamePlaceholder": "server name",
|
||||
"verifyPeerName": "Verify peer name",
|
||||
"pinnedSha256": "Pinned SHA256",
|
||||
"useSystemCA": "Use system CA",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Keep alive interval",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "nombre del servidor",
|
||||
"verifyPeerName": "Verificar nombre del peer",
|
||||
"pinnedSha256": "SHA256 pinned",
|
||||
"useSystemCA": "Usar CA del sistema",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Intervalo keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "نام سرور",
|
||||
"verifyPeerName": "تایید نام Peer",
|
||||
"pinnedSha256": "SHA256 پینشده",
|
||||
"useSystemCA": "استفاده از CA سیستم",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "بازه Keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "nama server",
|
||||
"verifyPeerName": "Verifikasi nama peer",
|
||||
"pinnedSha256": "SHA256 pinned",
|
||||
"useSystemCA": "Gunakan CA sistem",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Interval keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "サーバー名",
|
||||
"verifyPeerName": "peer 名を検証",
|
||||
"pinnedSha256": "Pinned SHA256",
|
||||
"useSystemCA": "システムの CA を使用",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "keep alive 間隔",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "nome do servidor",
|
||||
"verifyPeerName": "Verificar nome do peer",
|
||||
"pinnedSha256": "SHA256 pinned",
|
||||
"useSystemCA": "Usar CA do sistema",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Intervalo keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "имя сервера",
|
||||
"verifyPeerName": "Проверять имя peer",
|
||||
"pinnedSha256": "Pinned SHA256",
|
||||
"useSystemCA": "Использовать системные CA",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Интервал keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "sunucu adı",
|
||||
"verifyPeerName": "Peer Adını Doğrula",
|
||||
"pinnedSha256": "Sabitlenmiş SHA256",
|
||||
"useSystemCA": "Sistem CA'sını kullan",
|
||||
"shortId": "Kısa Kimlik",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Keep Alive Aralığı",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "ім'я сервера",
|
||||
"verifyPeerName": "Перевіряти ім'я peer",
|
||||
"pinnedSha256": "Pinned SHA256",
|
||||
"useSystemCA": "Використовувати системні CA",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Інтервал keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "tên máy chủ",
|
||||
"verifyPeerName": "Xác minh tên peer",
|
||||
"pinnedSha256": "SHA256 pinned",
|
||||
"useSystemCA": "Dùng CA của hệ thống",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "Khoảng keep alive",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "服务器名",
|
||||
"verifyPeerName": "验证 peer 名称",
|
||||
"pinnedSha256": "Pinned SHA256",
|
||||
"useSystemCA": "使用系统 CA",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "keep alive 间隔",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"serverNamePlaceholder": "伺服器名稱",
|
||||
"verifyPeerName": "驗證 peer 名稱",
|
||||
"pinnedSha256": "Pinned SHA256",
|
||||
"useSystemCA": "使用系統 CA",
|
||||
"shortId": "Short ID",
|
||||
"sockopts": "Sockopts",
|
||||
"keepAliveInterval": "keep alive 間隔",
|
||||
|
||||
Reference in New Issue
Block a user