ソースを参照

fix(xray): align DNS outbound to spec and repair item-list rules UI

DNS outbound now mirrors xray-core's documented shape: rewriteNetwork
/ rewriteAddress / rewritePort / userLevel replace the legacy network
/ address / port keys, and unset values are dropped on the wire. Old
configs are still accepted on read so saved configs migrate cleanly.

While there, fix two latent bugs in repeat-item editors (DNS rules,
Freedom noise, WireGuard peers):
- The "+" buttons pushed plain objects into arrays of class instances,
  so toJson() crashed on the next read and the JSON tab silently went
  blank. Push proper class instances instead.
- Each item heading lived outside any a-form-item, so the delete icon
  ignored the form's column grid and slumped left. Wrap the heading
  in a form-item with the standard offset wrapper-col and switch the
  flex to space-between so the icon sits at the right of the input
  column, in line with the fields below it.
MHSanaei 19 時間 前
コミット
f68a14a3ca

+ 21 - 20
frontend/src/models/outbound.js

@@ -1292,7 +1292,6 @@ export class Outbound extends CommonClass {
 
     hasAddressPort() {
         return [
-            Protocols.DNS,
             Protocols.VMess,
             Protocols.VLESS,
             Protocols.Trojan,
@@ -1846,15 +1845,17 @@ Outbound.DNSRule = class extends CommonClass {
 
 Outbound.DNSSettings = class extends CommonClass {
     constructor(
-        network = 'udp',
-        address = '',
-        port = 53,
+        rewriteNetwork = '',
+        rewriteAddress = '',
+        rewritePort = 53,
+        userLevel = 0,
         rules = []
     ) {
         super();
-        this.network = network;
-        this.address = address;
-        this.port = port;
+        this.rewriteNetwork = rewriteNetwork;
+        this.rewriteAddress = rewriteAddress;
+        this.rewritePort = rewritePort;
+        this.userLevel = userLevel;
         this.rules = Array.isArray(rules) ? rules.map(rule => rule instanceof Outbound.DNSRule ? rule : Outbound.DNSRule.fromJson(rule)) : [];
     }
 
@@ -1867,25 +1868,25 @@ Outbound.DNSSettings = class extends CommonClass {
     }
 
     static fromJson(json = {}) {
+        // Spec uses rewrite{Network,Address,Port}; older configs used the
+        // bare network/address/port keys — accept both so existing saved
+        // configs keep working after the migration.
         return new Outbound.DNSSettings(
-            json.network,
-            json.address,
-            json.port,
+            json.rewriteNetwork ?? json.network ?? '',
+            json.rewriteAddress ?? json.address ?? '',
+            Number(json.rewritePort ?? json.port ?? 53) || 53,
+            Number(json.userLevel ?? 0) || 0,
             getDNSRulesFromJson(json),
         );
     }
 
     toJson() {
-        const json = {
-            network: this.network,
-            address: this.address,
-            port: this.port,
-        };
-
-        if (this.rules.length > 0) {
-            json.rules = Outbound.DNSRule.toJsonArray(this.rules);
-        }
-
+        const json = {};
+        if (!ObjectUtil.isEmpty(this.rewriteNetwork)) json.rewriteNetwork = this.rewriteNetwork;
+        if (!ObjectUtil.isEmpty(this.rewriteAddress)) json.rewriteAddress = this.rewriteAddress;
+        if (this.rewritePort > 0) json.rewritePort = this.rewritePort;
+        if (this.userLevel > 0) json.userLevel = this.userLevel;
+        if (this.rules.length > 0) json.rules = Outbound.DNSRule.toJsonArray(this.rules);
         return json;
     }
 };

+ 37 - 21
frontend/src/pages/xray/OutboundFormModal.vue

@@ -268,21 +268,23 @@ function regenerateWgKeys() {
 
             <a-form-item label="Noises">
               <a-switch :checked="(outbound.settings.noises || []).length > 0"
-                @change="(checked) => outbound.settings.noises = checked ? [{ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ip' }] : []" />
+                @change="(checked) => outbound.settings.noises = checked ? [new Outbound.FreedomSettings.Noise()] : []" />
               <a-button v-if="outbound.settings.noises && outbound.settings.noises.length > 0" size="small"
                 type="primary" class="ml-8"
-                @click="outbound.settings.noises.push({ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ip' })">
+                @click="outbound.settings.noises.push(new Outbound.FreedomSettings.Noise())">
                 <template #icon>
                   <PlusOutlined />
                 </template>
               </a-button>
             </a-form-item>
             <template v-for="(noise, index) in outbound.settings.noises || []" :key="index">
-              <div class="item-heading">
-                <span>Noise {{ index + 1 }}</span>
-                <DeleteOutlined v-if="outbound.settings.noises.length > 1" class="danger-icon"
-                  @click="outbound.settings.noises.splice(index, 1)" />
-              </div>
+              <a-form-item :wrapper-col="{ md: { span: 14, offset: 8 } }" :colon="false">
+                <div class="item-heading">
+                  <span>Noise {{ index + 1 }}</span>
+                  <DeleteOutlined v-if="outbound.settings.noises.length > 1" class="danger-icon"
+                    @click="outbound.settings.noises.splice(index, 1)" />
+                </div>
+              </a-form-item>
               <a-form-item label="Type">
                 <a-select v-model:value="noise.type">
                   <a-select-option v-for="x in ['rand', 'base64', 'str', 'hex']" :key="x" :value="x">{{ x
@@ -325,24 +327,36 @@ function regenerateWgKeys() {
 
           <!-- ============== DNS ============== -->
           <template v-if="isDNS">
-            <a-form-item :label="t('pages.inbounds.network')">
-              <a-select v-model:value="outbound.settings.network">
+            <a-form-item label="Rewrite network">
+              <a-select v-model:value="outbound.settings.rewriteNetwork" allow-clear placeholder="(unchanged)">
                 <a-select-option v-for="x in ['udp', 'tcp']" :key="x" :value="x">{{ x }}</a-select-option>
               </a-select>
             </a-form-item>
+            <a-form-item label="Rewrite address">
+              <a-input v-model:value="outbound.settings.rewriteAddress" placeholder="(unchanged) e.g. 1.1.1.1" />
+            </a-form-item>
+            <a-form-item label="Rewrite port">
+              <a-input-number v-model:value="outbound.settings.rewritePort" :min="0" :max="65535"
+                :style="{ width: '100%' }" placeholder="(unchanged)" />
+            </a-form-item>
+            <a-form-item label="User level">
+              <a-input-number v-model:value="outbound.settings.userLevel" :min="0" :style="{ width: '100%' }" />
+            </a-form-item>
             <a-form-item label="Rules">
               <a-button size="small" type="primary"
-                @click="outbound.settings.rules.push({ action: 'direct', qtype: '', domain: '' })">
+                @click="outbound.settings.rules.push(new Outbound.DNSRule())">
                 <template #icon>
                   <PlusOutlined />
                 </template>
               </a-button>
             </a-form-item>
             <template v-for="(rule, index) in outbound.settings.rules || []" :key="index">
-              <div class="item-heading">
-                <span>Rule {{ index + 1 }}</span>
-                <DeleteOutlined class="danger-icon" @click="outbound.settings.rules.splice(index, 1)" />
-              </div>
+              <a-form-item :wrapper-col="{ md: { span: 14, offset: 8 } }" :colon="false">
+                <div class="item-heading">
+                  <span>Rule {{ index + 1 }}</span>
+                  <DeleteOutlined class="danger-icon" @click="outbound.settings.rules.splice(index, 1)" />
+                </div>
+              </a-form-item>
               <a-form-item label="Action">
                 <a-select v-model:value="rule.action">
                   <a-select-option v-for="a in DNSRuleActions" :key="a" :value="a">{{ a }}</a-select-option>
@@ -393,18 +407,20 @@ function regenerateWgKeys() {
             </a-form-item>
             <a-form-item label="Peers">
               <a-button size="small" type="primary"
-                @click="outbound.settings.peers.push({ endpoint: '', publicKey: '', psk: '', allowedIPs: [''], keepAlive: 0 })">
+                @click="outbound.settings.peers.push(new Outbound.WireguardSettings.Peer())">
                 <template #icon>
                   <PlusOutlined />
                 </template>
               </a-button>
             </a-form-item>
             <template v-for="(peer, index) in outbound.settings.peers || []" :key="index">
-              <div class="item-heading">
-                <span>Peer {{ index + 1 }}</span>
-                <DeleteOutlined v-if="outbound.settings.peers.length > 1" class="danger-icon"
-                  @click="outbound.settings.peers.splice(index, 1)" />
-              </div>
+              <a-form-item :wrapper-col="{ md: { span: 14, offset: 8 } }" :colon="false">
+                <div class="item-heading">
+                  <span>Peer {{ index + 1 }}</span>
+                  <DeleteOutlined v-if="outbound.settings.peers.length > 1" class="danger-icon"
+                    @click="outbound.settings.peers.splice(index, 1)" />
+                </div>
+              </a-form-item>
               <a-form-item label="Endpoint">
                 <a-input v-model:value="peer.endpoint" />
               </a-form-item>
@@ -993,9 +1009,9 @@ function regenerateWgKeys() {
 .item-heading {
   display: flex;
   align-items: center;
+  justify-content: space-between;
   gap: 8px;
   font-weight: 500;
-  margin: 8px 0 4px;
   opacity: 0.85;
 }
 

+ 5 - 2
frontend/src/pages/xray/OutboundsTab.vue

@@ -128,8 +128,11 @@ function outboundAddresses(o) {
     case Protocols.Trojan:
       serverObj = o.settings?.servers;
       break;
-    case Protocols.DNS:
-      return [`${o.settings?.address || ''}:${o.settings?.port || ''}`];
+    case Protocols.DNS: {
+      const addr = o.settings?.rewriteAddress || o.settings?.address || '';
+      const port = o.settings?.rewritePort || o.settings?.port || '';
+      return addr || port ? [`${addr}:${port}`] : [];
+    }
     case Protocols.Wireguard:
       return (o.settings?.peers || []).map((p) => p.endpoint);
     default: