mirror of
https://github.com/alireza0/x-ui.git
synced 2026-10-14 05:24:22 +00:00
Finalmask: xDNS domain names lists and resolver addrs (Xray-core v26.10.10)
This commit is contained in:
parent
0b36fd86ba
commit
79d8a9161b
14 changed files with 440 additions and 47 deletions
|
|
@ -13,6 +13,7 @@ const (
|
|||
VersionWireguard = 7
|
||||
VersionCore26930 = 8
|
||||
VersionSSKeys = 9
|
||||
VersionXDNS = 10
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
|
|
@ -29,6 +30,7 @@ var registry = []migration{
|
|||
{VersionWireguard, migrateV007Wireguard},
|
||||
{VersionCore26930, migrateV008OutboundCore},
|
||||
{VersionSSKeys, migrateV009ShadowsocksKeys},
|
||||
{VersionXDNS, migrateV010XDNS},
|
||||
}
|
||||
|
||||
func Run(db *gorm.DB) error {
|
||||
|
|
|
|||
79
database/migrations/v010_xdns.go
Normal file
79
database/migrations/v010_xdns.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/alireza0/x-ui/database/model"
|
||||
"github.com/alireza0/x-ui/xray"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// migrateV010XDNS rewrites stored xDNS masks for Xray-core v26.10.10, which
|
||||
// takes a list of "names" per domain and resolvers as {"addrs": [...]}. The
|
||||
// core ignores the older keys without an error, so inbounds and outbounds
|
||||
// saved by earlier panels would keep running with no xDNS domain or resolver.
|
||||
func migrateV010XDNS(db *gorm.DB) error {
|
||||
tx := db.Begin()
|
||||
var err error
|
||||
defer func() {
|
||||
if err == nil {
|
||||
tx.Commit()
|
||||
} else {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// upgrade returns the rewritten stream settings, or "" when there is
|
||||
// nothing to change.
|
||||
upgrade := func(raw string) (string, error) {
|
||||
stream := map[string]any{}
|
||||
if raw == "" || json.Unmarshal([]byte(raw), &stream) != nil || !xray.UpgradeXDNSMasks(stream) {
|
||||
return "", nil
|
||||
}
|
||||
modified, err := json.MarshalIndent(stream, "", " ")
|
||||
return string(modified), err
|
||||
}
|
||||
|
||||
var inbounds []*model.Inbound
|
||||
err = tx.Model(model.Inbound{}).Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
var stream string
|
||||
stream, err = upgrade(inbound.StreamSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stream == "" {
|
||||
continue
|
||||
}
|
||||
err = tx.Model(model.Inbound{}).Where("id = ?", inbound.Id).Update("stream_settings", stream).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var outbounds []*model.Outbound
|
||||
err = tx.Model(model.Outbound{}).Find(&outbounds).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, outbound := range outbounds {
|
||||
var stream string
|
||||
stream, err = upgrade(outbound.StreamSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stream == "" {
|
||||
continue
|
||||
}
|
||||
err = tx.Model(model.Outbound{}).Where("id = ?", outbound.Id).Update("stream_settings", stream).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
68
database/migrations/v010_xdns_test.go
Normal file
68
database/migrations/v010_xdns_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alireza0/x-ui/database/model"
|
||||
)
|
||||
|
||||
const v26930XDNSStream = `{"network":"kcp","finalmask":{"udp":[{"type":"xdns","settings":{
|
||||
"domains":[{"name":"t.example.com","types":[16]}],
|
||||
"resolvers":[{"type":"udp","settings":{"addr":"1.1.1.1:53"}}]}}]}}`
|
||||
|
||||
func TestV010UpgradesInboundsAndOutbounds(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Outbound{}); err != nil {
|
||||
t.Fatalf("automigrate: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Inbound{
|
||||
Id: 1, UserId: 1, Tag: "inbound-kcp", Protocol: model.VLESS, Port: 4443, Enable: true,
|
||||
Settings: `{}`,
|
||||
StreamSettings: `{"network":"kcp","finalmask":{"udp":[{"type":"xdns","settings":{"domains":[{"name":"t.example.com"}]}}]}}`,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed inbound: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Outbound{Tag: "xdns", Protocol: "vless", Settings: `{}`, StreamSettings: v26930XDNSStream}).Error; err != nil {
|
||||
t.Fatalf("seed outbound: %v", err)
|
||||
}
|
||||
|
||||
if err := migrateV010XDNS(db); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
var inbound model.Inbound
|
||||
if err := db.First(&inbound, 1).Error; err != nil {
|
||||
t.Fatalf("reload inbound: %v", err)
|
||||
}
|
||||
assertSameJSON(t, inbound.StreamSettings,
|
||||
`{"network":"kcp","finalmask":{"udp":[{"type":"xdns","settings":{"domains":[{"names":["t.example.com"]}]}}]}}`)
|
||||
|
||||
var outbound model.Outbound
|
||||
if err := db.First(&outbound).Error; err != nil {
|
||||
t.Fatalf("reload outbound: %v", err)
|
||||
}
|
||||
assertSameJSON(t, outbound.StreamSettings, `{"network":"kcp","finalmask":{"udp":[{"type":"xdns","settings":{
|
||||
"domains":[{"names":["t.example.com"],"types":[16]}],
|
||||
"resolvers":[{"addrs":["udp://1.1.1.1:53"]}]}}]}}`)
|
||||
}
|
||||
|
||||
func TestV010LeavesOtherStreamsAlone(t *testing.T) {
|
||||
const stream = "{\n \"network\": \"tcp\"\n}"
|
||||
db := newTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Outbound{}); err != nil {
|
||||
t.Fatalf("automigrate: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Outbound{Tag: "direct", Protocol: "freedom", Settings: `{}`, StreamSettings: stream}).Error; err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
if err := migrateV010XDNS(db); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
var outbound model.Outbound
|
||||
if err := db.First(&outbound).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if outbound.StreamSettings != stream {
|
||||
t.Fatalf("untouched outbound was rewritten: %q", outbound.StreamSettings)
|
||||
}
|
||||
}
|
||||
|
|
@ -1415,12 +1415,23 @@ class UdpMask extends XrayCommonClass {
|
|||
}
|
||||
}
|
||||
|
||||
// Xray-core v26.9.30 turned xDNS domains into objects; older configs carry a
|
||||
// bare "domain" or a list of names.
|
||||
// Xray-core v26.10.10 gives each xDNS domain a list of "names". Older
|
||||
// configs carry v26.9.30's {"name"} objects, a bare "domain" or a list of
|
||||
// names.
|
||||
static xdnsDomains(settings = {}) {
|
||||
return [].concat(settings.domain || [], settings.domains || [])
|
||||
.map(d => typeof d === 'string' ? { name: d, types: [16] } : d)
|
||||
.filter(d => d && d.name !== undefined);
|
||||
return [].concat(settings.domain || [], settings.domains || []).map(d => {
|
||||
if (typeof d === 'string') return { names: [d], types: [] };
|
||||
if (!d || typeof d !== 'object') return null;
|
||||
const { name, ...rest } = d;
|
||||
return { names: d.names || (name ? [name] : []), ...rest, types: d.types || [] };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
// Empty record types leave the choice to the core, which answers all four
|
||||
// on the server.
|
||||
static xdnsDomainsToJson(domains = []) {
|
||||
return domains.map(d => Object.fromEntries(Object.entries(d).filter(([, v]) =>
|
||||
!(v === '' || v == null || (Array.isArray(v) && v.length === 0)))));
|
||||
}
|
||||
|
||||
// Realm settings without the parts left at their defaults. The TLS config for
|
||||
|
|
@ -1565,6 +1576,9 @@ class UdpMask extends XrayCommonClass {
|
|||
if (this.type === 'noise') {
|
||||
settings = { reset: settings.reset || undefined, noise: (settings.noise || []).map(n => UdpMask.noiseItemToJson(n)) };
|
||||
}
|
||||
if (this.type === 'xdns') {
|
||||
settings = { domains: UdpMask.xdnsDomainsToJson(settings.domains) };
|
||||
}
|
||||
if (this.type === 'xicmp') {
|
||||
settings = { dgram: settings.dgram || undefined, ips: settings.ips?.length ? settings.ips : undefined };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -908,35 +908,52 @@ class UdpMask extends CommonClass {
|
|||
}
|
||||
}
|
||||
|
||||
// Xray-core v26.9.30 turned xDNS domains and resolvers into objects. Older
|
||||
// configs carry a bare "domain", or plain string lists where a resolver reads
|
||||
// "name[:type]+udp://addr".
|
||||
// Xray-core v26.10.10 gives each xDNS domain a list of "names" and takes
|
||||
// resolvers as {"addrs": ["udp://host:port", ...]}. Older configs carry
|
||||
// v26.9.30's {"name"} and {"type", "settings": {"addr"}} objects, or plain
|
||||
// strings where a resolver reads "name[:type]+udp://addr". The form keeps
|
||||
// the resolvers as one flat list of addresses.
|
||||
static xdnsSettings(settings = {}) {
|
||||
const domains = [];
|
||||
const domains = UdpMask.xdnsDomains(settings);
|
||||
const resolvers = [];
|
||||
const addDomain = (name, types = [16]) => {
|
||||
const known = domains.find(d => d.name === name);
|
||||
if (known) known.types = [...new Set([...(known.types || []), ...types])];
|
||||
else if (name) domains.push({ name, types });
|
||||
const addType = (name, type) => {
|
||||
const known = domains.find(d => d.names.includes(name));
|
||||
if (!known) domains.push({ names: [name], types: [type] });
|
||||
else if (!known.types.includes(type)) known.types.push(type);
|
||||
};
|
||||
for (const d of [].concat(settings.domain || [], settings.domains || [])) {
|
||||
if (typeof d === 'string') addDomain(d);
|
||||
else if (d && typeof d === 'object') domains.push(d);
|
||||
}
|
||||
for (const r of settings.resolvers || []) {
|
||||
if (typeof r === 'string') {
|
||||
const [head, addr] = r.split('+udp://');
|
||||
if (!addr) continue;
|
||||
const [name, type] = head.split(':');
|
||||
addDomain(name, [UdpMask.XdnsTypes[(type || 'txt').toUpperCase()] || 16]);
|
||||
resolvers.push({ type: 'udp', settings: { addr } });
|
||||
} else if (r && typeof r === 'object') {
|
||||
resolvers.push({ type: r.type || 'udp', settings: { addr: r.settings?.addr || '' } });
|
||||
const plus = r.indexOf('+');
|
||||
const addr = r.slice(plus + 1);
|
||||
if (plus < 0 || !addr.includes('://')) continue;
|
||||
const [name, type] = r.slice(0, plus).split(':');
|
||||
if (name) addType(name, UdpMask.XdnsTypes[(type || 'txt').toUpperCase()] || 16);
|
||||
resolvers.push(addr);
|
||||
} else if (Array.isArray(r?.addrs)) {
|
||||
resolvers.push(...r.addrs);
|
||||
} else if (r?.settings?.addr) {
|
||||
resolvers.push(`${r.type || 'udp'}://${r.settings.addr}`);
|
||||
}
|
||||
}
|
||||
return { domains, resolvers, extraPoll: settings.extraPoll ?? 0 };
|
||||
}
|
||||
|
||||
static xdnsDomains(settings = {}) {
|
||||
return [].concat(settings.domain || [], settings.domains || []).map(d => {
|
||||
if (typeof d === 'string') return { names: [d], types: [] };
|
||||
if (!d || typeof d !== 'object') return null;
|
||||
const { name, ...rest } = d;
|
||||
return { names: d.names || (name ? [name] : []), ...rest, types: d.types || [] };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
// Empty record types leave the choice to the core: TXT on the client, all
|
||||
// four on the server.
|
||||
static xdnsDomainsToJson(domains = []) {
|
||||
return domains.map(d => Object.fromEntries(Object.entries(d).filter(([, v]) =>
|
||||
!(v === '' || v == null || (Array.isArray(v) && v.length === 0)))));
|
||||
}
|
||||
|
||||
// Realm settings without the parts left at their defaults. The TLS config for
|
||||
// the rendezvous server is edited as text; text that does not parse is passed
|
||||
// on as is, so the core reports what is wrong with it.
|
||||
|
|
@ -1080,6 +1097,13 @@ class UdpMask extends CommonClass {
|
|||
settings = { ...settings, noise: (settings.noise || []).map(n => UdpMask.noiseItemToJson(n)) };
|
||||
if (!settings.reset) delete settings.reset;
|
||||
}
|
||||
if (this.type === 'xdns') {
|
||||
settings = {
|
||||
...settings,
|
||||
domains: UdpMask.xdnsDomainsToJson(settings.domains),
|
||||
resolvers: settings.resolvers?.length ? [{ addrs: settings.resolvers }] : [],
|
||||
};
|
||||
}
|
||||
if (['xdns', 'xicmp', 'udphop'].includes(this.type)) {
|
||||
settings = Object.fromEntries(Object.entries(settings).filter(([, v]) =>
|
||||
!(v === '' || v === 0 || v === false || (Array.isArray(v) && v.length === 0))));
|
||||
|
|
|
|||
|
|
@ -914,26 +914,18 @@
|
|||
</template>
|
||||
<template v-if="mask.type === 'xdns'">
|
||||
{{template "form/xdnsDomains" "mask"}}
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.finalmask.resolvers" }}'>
|
||||
<a-icon type="plus" type="primary" size="small"
|
||||
@click="mask.settings.resolvers.push({type: 'udp', settings: {addr: ''}})" />
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.stream.finalmask.resolversDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.stream.finalmask.resolvers" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-select mode="tags" v-model="mask.settings.resolvers" :token-separators="[',', ' ']"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme" placeholder="udp://1.1.1.1:53"></a-select>
|
||||
</a-form-item>
|
||||
<template v-for="(r, rIdx) in mask.settings.resolvers" :key="'resolver-' + rIdx">
|
||||
<a-divider :style="{ margin: '0' }"> {{ i18n "pages.inbounds.stream.finalmask.resolvers" }} [[ rIdx + 1 ]]
|
||||
<a-icon type="delete"
|
||||
@click="() => mask.settings.resolvers.splice(rIdx, 1)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.hysteria.type" }}'>
|
||||
<a-select v-model="r.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="udp">UDP</a-select-option>
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "address" }}'>
|
||||
<a-input v-model.trim="r.settings.addr" placeholder="1.1.1.1:53"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.finalmask.extraPoll" }}'>
|
||||
<a-input-number v-model.number="mask.settings.extraPoll" :min="0" :max="3"></a-input-number>
|
||||
</a-form-item>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{{define "form/xdnsDomains"}}
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.finalmask.domains" }}'>
|
||||
<a-icon type="plus" type="primary" size="small"
|
||||
@click="{{.}}.settings.domains.push({name: '', types: [16], edns0: 1232})" />
|
||||
@click="{{.}}.settings.domains.push({names: [], types: [], edns0: 1232})" />
|
||||
</a-form-item>
|
||||
<template v-for="(d, dIdx) in {{.}}.settings.domains" :key="'xdns-domain-' + dIdx">
|
||||
<a-divider :style="{ margin: '0' }"> {{ i18n "pages.inbounds.stream.finalmask.domains" }} [[ dIdx + 1 ]]
|
||||
|
|
@ -9,10 +9,19 @@
|
|||
@click="() => {{.}}.settings.domains.splice(dIdx, 1)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.domain" }}'>
|
||||
<a-input v-model.trim="d.name" placeholder="t.example.com"></a-input>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.finalmask.domains" }}'>
|
||||
<a-select mode="tags" v-model="d.names" :token-separators="[',', ' ']"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme" placeholder="t.example.com"></a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.finalmask.recordTypes" }}'>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.stream.finalmask.recordTypesDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.stream.finalmask.recordTypes" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-select mode="multiple" v-model="d.types" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(code, name) in UdpMask.XdnsTypes" :value="code">[[ name ]]</a-select-option>
|
||||
</a-select>
|
||||
|
|
|
|||
|
|
@ -447,7 +447,9 @@
|
|||
"maxIncomingStreams" = "Max Incoming Streams"
|
||||
"domains" = "Domains"
|
||||
"resolvers" = "Resolvers"
|
||||
"resolversDesc" = "udp://host:port or tcp://host:port. Without a scheme UDP is used, without a port 53."
|
||||
"recordTypes" = "Record Types"
|
||||
"recordTypesDesc" = "Leave empty for the default: TXT on the client, A, CNAME, TXT and AAAA on the server."
|
||||
"edns0" = "EDNS0"
|
||||
"extraPoll" = "Extra Poll"
|
||||
"dgram" = "Unprivileged ICMP (DGRAM)"
|
||||
|
|
|
|||
|
|
@ -447,7 +447,9 @@
|
|||
"maxIncomingStreams" = "حداکثر استریمهای ورودی"
|
||||
"domains" = "دامنهها"
|
||||
"resolvers" = "ریزالورها"
|
||||
"resolversDesc" = "به شکل udp://host:port یا tcp://host:port. بدون پروتکل UDP و بدون پورت 53 در نظر گرفته میشود."
|
||||
"recordTypes" = "نوع رکوردها"
|
||||
"recordTypesDesc" = "برای مقدار پیشفرض خالی بگذارید: TXT در کلاینت، و A، CNAME، TXT و AAAA در سرور."
|
||||
"edns0" = "EDNS0"
|
||||
"extraPoll" = "Poll اضافی"
|
||||
"dgram" = "ICMP بدون دسترسی روت (DGRAM)"
|
||||
|
|
|
|||
|
|
@ -447,7 +447,9 @@
|
|||
"maxIncomingStreams" = "Макс. входящих потоков"
|
||||
"domains" = "Домены"
|
||||
"resolvers" = "Резолверы"
|
||||
"resolversDesc" = "udp://host:port или tcp://host:port. Без схемы используется UDP, без порта — 53."
|
||||
"recordTypes" = "Типы записей"
|
||||
"recordTypesDesc" = "Оставьте пустым для значения по умолчанию: TXT на клиенте, A, CNAME, TXT и AAAA на сервере."
|
||||
"edns0" = "EDNS0"
|
||||
"extraPoll" = "Доп. опрос"
|
||||
"dgram" = "Непривилегированный ICMP (DGRAM)"
|
||||
|
|
|
|||
|
|
@ -447,7 +447,9 @@
|
|||
"maxIncomingStreams" = "Max Incoming Streams"
|
||||
"domains" = "Tên miền"
|
||||
"resolvers" = "Trình phân giải"
|
||||
"resolversDesc" = "udp://host:port hoặc tcp://host:port. Không có giao thức thì dùng UDP, không có cổng thì dùng 53."
|
||||
"recordTypes" = "Loại bản ghi"
|
||||
"recordTypesDesc" = "Để trống để dùng mặc định: TXT ở client, A, CNAME, TXT và AAAA ở server."
|
||||
"edns0" = "EDNS0"
|
||||
"extraPoll" = "Thăm dò bổ sung"
|
||||
"dgram" = "ICMP không đặc quyền (DGRAM)"
|
||||
|
|
|
|||
|
|
@ -447,7 +447,9 @@
|
|||
"maxIncomingStreams" = "最大入站流"
|
||||
"domains" = "域名"
|
||||
"resolvers" = "解析器"
|
||||
"resolversDesc" = "udp://host:port 或 tcp://host:port。未写协议时使用 UDP,未写端口时使用 53。"
|
||||
"recordTypes" = "记录类型"
|
||||
"recordTypesDesc" = "留空使用默认值:客户端为 TXT,服务端为 A、CNAME、TXT 和 AAAA。"
|
||||
"edns0" = "EDNS0"
|
||||
"extraPoll" = "额外轮询"
|
||||
"dgram" = "非特权 ICMP (DGRAM)"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package xray
|
||||
|
||||
import "strings"
|
||||
|
||||
// Xray-core v26.9.30 dropped "finalmask.quicParams.udpHop" in favour of a
|
||||
// client-only "udphop" UDP mask. The panel still keeps the port range in the
|
||||
// inbound's quicParams, because share links and subscriptions are generated
|
||||
|
|
@ -57,3 +59,147 @@ func MoveUDPHopToMask(stream map[string]any) bool {
|
|||
finalmask["udp"] = append([]any{map[string]any{"type": "udphop", "settings": settings}}, udp...)
|
||||
return true
|
||||
}
|
||||
|
||||
// Xray-core v26.10.10 changed the xDNS mask again: a domain lists its "names"
|
||||
// instead of a single "name", and resolvers are {"addrs": [...]} written as
|
||||
// "udp://host:port" or "tcp://host:port". The panel stored v26.9.30's objects
|
||||
// ({"name"}, {"type", "settings": {"addr"}}) and, before that, plain strings,
|
||||
// where a resolver read "name[:type]+udp://addr". The core ignores all of
|
||||
// these silently, leaving xDNS without domains or resolvers.
|
||||
|
||||
var xdnsRecordTypes = map[string]float64{"a": 1, "cname": 5, "txt": 16, "aaaa": 28}
|
||||
|
||||
// UpgradeXDNSMasks rewrites every xDNS mask in a stream settings map to the
|
||||
// v26.10.10 schema and reports whether anything changed.
|
||||
func UpgradeXDNSMasks(stream map[string]any) bool {
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
udp, _ := finalmask["udp"].([]any)
|
||||
changed := false
|
||||
for _, mask := range udp {
|
||||
m, _ := mask.(map[string]any)
|
||||
if m["type"] != "xdns" {
|
||||
continue
|
||||
}
|
||||
if settings, _ := m["settings"].(map[string]any); settings != nil && upgradeXDNSSettings(settings) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func upgradeXDNSSettings(settings map[string]any) bool {
|
||||
changed := false
|
||||
var domains []any
|
||||
var addrs []any
|
||||
|
||||
// addType records a resolver's record type on the domain holding name,
|
||||
// adding that domain when no domain names it yet.
|
||||
addType := func(name string, recordType float64) {
|
||||
for _, d := range domains {
|
||||
domain, _ := d.(map[string]any)
|
||||
names, _ := domain["names"].([]any)
|
||||
for _, n := range names {
|
||||
if n != name {
|
||||
continue
|
||||
}
|
||||
types, _ := domain["types"].([]any)
|
||||
for _, t := range types {
|
||||
if t == recordType {
|
||||
return
|
||||
}
|
||||
}
|
||||
domain["types"] = append(types, recordType)
|
||||
return
|
||||
}
|
||||
}
|
||||
domains = append(domains, map[string]any{"names": []any{name}, "types": []any{recordType}})
|
||||
}
|
||||
|
||||
var listed []any
|
||||
if d, ok := settings["domain"]; ok {
|
||||
delete(settings, "domain")
|
||||
changed = true
|
||||
if s, ok := d.(string); ok {
|
||||
listed = append(listed, s)
|
||||
} else if list, ok := d.([]any); ok {
|
||||
listed = append(listed, list...)
|
||||
}
|
||||
}
|
||||
if list, ok := settings["domains"].([]any); ok {
|
||||
listed = append(listed, list...)
|
||||
}
|
||||
for _, d := range listed {
|
||||
switch domain := d.(type) {
|
||||
case string:
|
||||
if domain != "" {
|
||||
domains = append(domains, map[string]any{"names": []any{domain}})
|
||||
}
|
||||
changed = true
|
||||
case map[string]any:
|
||||
if _, ok := domain["names"]; !ok {
|
||||
if name, _ := domain["name"].(string); name != "" {
|
||||
domain["names"] = []any{name}
|
||||
}
|
||||
delete(domain, "name")
|
||||
changed = true
|
||||
}
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
}
|
||||
|
||||
var resolvers []any
|
||||
stored, _ := settings["resolvers"].([]any)
|
||||
for _, r := range stored {
|
||||
switch resolver := r.(type) {
|
||||
case string:
|
||||
changed = true
|
||||
head, addr, ok := strings.Cut(resolver, "+")
|
||||
if !ok || !strings.Contains(addr, "://") {
|
||||
continue
|
||||
}
|
||||
name, recordType, _ := strings.Cut(head, ":")
|
||||
code, ok := xdnsRecordTypes[strings.ToLower(recordType)]
|
||||
if !ok {
|
||||
code = 16
|
||||
}
|
||||
if name != "" {
|
||||
addType(name, code)
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
case map[string]any:
|
||||
if _, ok := resolver["addrs"]; ok {
|
||||
resolvers = append(resolvers, resolver)
|
||||
continue
|
||||
}
|
||||
changed = true
|
||||
inner, _ := resolver["settings"].(map[string]any)
|
||||
addr, _ := inner["addr"].(string)
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
network, _ := resolver["type"].(string)
|
||||
if network == "" {
|
||||
network = "udp"
|
||||
}
|
||||
addrs = append(addrs, network+"://"+addr)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(addrs) > 0 {
|
||||
resolvers = append(resolvers, map[string]any{"addrs": addrs})
|
||||
}
|
||||
if len(domains) > 0 {
|
||||
settings["domains"] = domains
|
||||
} else {
|
||||
delete(settings, "domains")
|
||||
}
|
||||
if len(resolvers) > 0 {
|
||||
settings["resolvers"] = resolvers
|
||||
} else {
|
||||
delete(settings, "resolvers")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,3 +78,52 @@ func TestMoveUDPHopToMaskWithoutPorts(t *testing.T) {
|
|||
t.Fatalf("expected nothing left, got %v", stream)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeXDNSMasksFromV26930(t *testing.T) {
|
||||
stream := streamFrom(t, `{"finalmask":{"udp":[
|
||||
{"type":"salamander","settings":{"password":"x"}},
|
||||
{"type":"xdns","settings":{
|
||||
"domains":[{"name":"t.example.com","types":[16,1],"edns0":1232}],
|
||||
"resolvers":[{"type":"udp","settings":{"addr":"1.1.1.1:53"}},{"type":"tcp","settings":{"addr":"8.8.8.8:53"}}],
|
||||
"extraPoll":2}}]}}`)
|
||||
if !UpgradeXDNSMasks(stream) {
|
||||
t.Fatal("reported no change")
|
||||
}
|
||||
want := streamFrom(t, `{"finalmask":{"udp":[
|
||||
{"type":"salamander","settings":{"password":"x"}},
|
||||
{"type":"xdns","settings":{
|
||||
"domains":[{"names":["t.example.com"],"types":[16,1],"edns0":1232}],
|
||||
"resolvers":[{"addrs":["udp://1.1.1.1:53","tcp://8.8.8.8:53"]}],
|
||||
"extraPoll":2}}]}}`)
|
||||
if !reflect.DeepEqual(stream, want) {
|
||||
t.Fatalf("got %v, want %v", stream, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeXDNSMasksFromStrings(t *testing.T) {
|
||||
stream := streamFrom(t, `{"finalmask":{"udp":[{"type":"xdns","settings":{
|
||||
"domain":"a.example.com",
|
||||
"resolvers":["a.example.com:a+udp://1.1.1.1:53","b.example.com+udp://9.9.9.9:53","broken"]}}]}}`)
|
||||
if !UpgradeXDNSMasks(stream) {
|
||||
t.Fatal("reported no change")
|
||||
}
|
||||
want := streamFrom(t, `{"finalmask":{"udp":[{"type":"xdns","settings":{
|
||||
"domains":[{"names":["a.example.com"],"types":[1]},{"names":["b.example.com"],"types":[16]}],
|
||||
"resolvers":[{"addrs":["udp://1.1.1.1:53","udp://9.9.9.9:53"]}]}}]}}`)
|
||||
if !reflect.DeepEqual(stream, want) {
|
||||
t.Fatalf("got %v, want %v", stream, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeXDNSMasksKeepsNewSchema(t *testing.T) {
|
||||
const raw = `{"finalmask":{"udp":[{"type":"xdns","settings":{
|
||||
"domains":[{"names":["a.example.com","b.example.com"]}],
|
||||
"resolvers":[{"addrs":["1.1.1.1","tcp://8.8.8.8"]}]}}]}}`
|
||||
stream := streamFrom(t, raw)
|
||||
if UpgradeXDNSMasks(stream) {
|
||||
t.Fatal("rewrote a mask already in the new schema")
|
||||
}
|
||||
if !reflect.DeepEqual(stream, streamFrom(t, raw)) {
|
||||
t.Fatalf("changed to %v", stream)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue