Forráskód Böngészése

fix(panel): read the outbound protocol id in the Outbounds row like the core (#6528)

* fix(panel): read the outbound protocol id in the address column like the core

outboundAddresses switched on the raw id, so a row the core runs normally but
spelled "VMess", "Trojan" or "WireGuard" fell through to default and rendered
an empty Address column in the outbounds table, the card view and the
subscription table -- a populated server that looks absent, which is what
sends an operator to recreate a correct outbound.

The id is folded once before the switch, the way isUdpOutbound already folds
the transport name.

* fix(panel): fill the outbound address column from one protocol-id rule

outboundAddresses folded the id inline while isUntestable, two functions
below it in the same file, reads it through isOutboundProtocol — so the
"the core lowercases the id" rule lived in two places and two tests. It
now routes through the shared helper, which keeps the rule with the
module that owns it.

Two further gaps in the same switch, reported in the same review:
hysteria and amneziawg are both selectable in the outbound form but had
no case, so a canonically spelled row rendered a blank Address cell that
case folding could not reach; and the VLESS branch returned a bare ":"
for a row whose servers sit in vnext, which this change newly reached
for a "VLESS" spelling.

Tests: the hysteria/amneziawg cases and the bare-separator case are red
on the pre-fix switch.

* fix(panel): read the vnext shape of a vless outbound in the address column

The vless branch read only the flat settings.address/port, so a row whose
servers sit in vnext — the shape the probe's extractor reads first
(internal/web/service/outbound/outbound.go:259-269) — rendered a bare ":"
separator, or nothing at all before this branch folded the id. It now
reads vnext first and falls back to the flat pair, the order the
extractor uses, which also makes it agree with what a probe of that row
would say.

Test: "reads the vnext server of a vless row" is red on the pre-fix
branch.

* fix(panel): read the protocol id of the outbound stream tags like the core

The identity cell gated the network and security tags on an exact-match
includes() over four ids, so the same "VMess" row whose address this
branch now shows still rendered without its ws/tls tags — the row was
half-readable. It now asks the shared isOutboundProtocol, the rule every
other reader on the page uses.

Test: "renders the stream tags and the address of a VMess row" is red
without this change (['VMess'] vs ['VMess','ws','tls']).
BlindMaster24 23 órája
szülő
commit
c0271e231d

+ 17 - 10
frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts

@@ -25,27 +25,34 @@ export function originalOutboundIndex(rows: OutboundRow[], positionalIndex: numb
 
 export function outboundAddresses(o: OutboundRow): string[] {
   const settings = o.settings as Record<string, unknown> | undefined;
-  switch (o.protocol) {
-    case Protocols.VMess: {
+  switch (true) {
+    case isOutboundProtocol(o, Protocols.VMess): {
       const serverObj = settings?.vnext as Array<{ address: string; port: number }> | undefined;
       return serverObj ? serverObj.map((s) => `${s.address}:${s.port}`) : [];
     }
-    case Protocols.VLESS:
-      return [`${settings?.address || ''}:${settings?.port || ''}`];
-    case Protocols.HTTP:
-    case Protocols.Socks:
-    case Protocols.Shadowsocks:
-    case Protocols.Trojan: {
+    case isOutboundProtocol(o, Protocols.VLESS):
+    case isOutboundProtocol(o, Protocols.Hysteria): {
+      // A vless row carries either shape, and the probe reads both.
+      const vnext = settings?.vnext as Array<{ address?: string; port?: number }> | undefined;
+      const addr = vnext?.[0]?.address || (settings?.address as string | undefined);
+      const port = vnext?.[0]?.port || (settings?.port as string | number | undefined);
+      return addr || port ? [`${addr || ''}:${port || ''}`] : [];
+    }
+    case isOutboundProtocol(o, Protocols.HTTP):
+    case isOutboundProtocol(o, Protocols.Socks):
+    case isOutboundProtocol(o, Protocols.Shadowsocks):
+    case isOutboundProtocol(o, Protocols.Trojan): {
       const serverObj = settings?.servers as Array<{ address: string; port: number }> | undefined;
       return serverObj ? serverObj.map((s) => `${s.address}:${s.port}`) : [];
     }
-    case Protocols.DNS: {
+    case isOutboundProtocol(o, Protocols.DNS): {
       const addr = (settings?.rewriteAddress as string) || (settings?.address as string) || '';
       const port =
         (settings?.rewritePort as string | number) || (settings?.port as string | number) || '';
       return addr || port ? [`${addr}:${port}`] : [];
     }
-    case Protocols.Wireguard:
+    case isOutboundProtocol(o, Protocols.Wireguard):
+    case isOutboundProtocol(o, Protocols.AmneziaWG):
       return ((settings?.peers as Array<{ endpoint?: string }>) || [])
         .map((p) => p.endpoint || '')
         .filter(Boolean);

+ 3 - 3
frontend/src/pages/xray/outbounds/useOutboundColumns.tsx

@@ -18,7 +18,7 @@ import type { ColumnsType } from 'antd/es/table';
 
 import { SizeFormatter } from '@/utils';
 import { activateOnKey } from '@/utils/a11y';
-import { OutboundProtocols as Protocols } from '@/schemas/primitives';
+import { isOutboundProtocol, OutboundProtocols as Protocols } from '@/schemas/primitives';
 import type {
   OutboundTestMode,
   OutboundTestState,
@@ -169,8 +169,8 @@ export function useOutboundColumns({
             </Tooltip>
             <div className="protocol-line">
               <Tag color="green">{record.protocol}</Tag>
-              {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(
-                record.protocol as never,
+              {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].some(
+                (id) => isOutboundProtocol(record, id),
               ) && (
                 <>
                   <Tag>{record.streamSettings?.network}</Tag>

+ 60 - 0
frontend/src/test/outbound-identity-tags-case.test.tsx

@@ -0,0 +1,60 @@
+import { describe, it, expect, vi } from 'vitest';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+import OutboundsTab from '@/pages/xray/outbounds/OutboundsTab';
+import type { XraySettingsValue } from '@/hooks/useXraySetting';
+
+import { renderWithProviders } from './test-utils';
+
+// The core lowercases the id, so a "VMess" row is a vmess outbound: its stream
+// tags must follow the same rule its address does.
+function settingsWithCapitalisedProtocol(): XraySettingsValue {
+  return {
+    outbounds: [
+      {
+        tag: 'proxy-a',
+        protocol: 'VMess',
+        settings: { vnext: [{ address: 'a.example.com', port: 443 }] },
+        streamSettings: { network: 'ws', security: 'tls' },
+      },
+    ],
+  } as unknown as XraySettingsValue;
+}
+
+function renderTab(settings: XraySettingsValue) {
+  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+  return renderWithProviders(
+    <QueryClientProvider client={queryClient}>
+      <OutboundsTab
+        templateSettings={settings}
+        setTemplateSettings={vi.fn()}
+        outboundsTraffic={[]}
+        outboundTestStates={{}}
+        subscriptionTestStates={{}}
+        testingAll={false}
+        inboundTags={[]}
+        isMobile={false}
+        onResetTraffic={vi.fn()}
+        onTest={vi.fn()}
+        onTestSubscription={vi.fn()}
+        onTestAll={vi.fn()}
+        onShowWarp={vi.fn()}
+        onShowNord={vi.fn()}
+        onShowPia={vi.fn()}
+      />
+    </QueryClientProvider>,
+  );
+}
+
+describe('OutboundsTab row for a case-variant protocol id', () => {
+  it('renders the stream tags and the address of a "VMess" row', () => {
+    renderTab(settingsWithCapitalisedProtocol());
+
+    const row = document.querySelector('.ant-table-tbody tr.ant-table-row');
+    const tags = Array.from(row?.querySelectorAll('.protocol-line .ant-tag') ?? []).map(
+      (el) => el.textContent,
+    );
+    expect(tags).toEqual(['VMess', 'ws', 'tls']);
+    expect(row?.textContent).toContain('a.example.com:443');
+  });
+});

+ 73 - 0
frontend/src/test/outbounds-addresses-case.test.ts

@@ -0,0 +1,73 @@
+import { describe, expect, it } from 'vitest';
+
+import { outboundAddresses } from '@/pages/xray/outbounds/outbounds-tab-helpers';
+import type { OutboundRow } from '@/pages/xray/outbounds/outbounds-tab-types';
+
+// The core lowercases a protocol id before resolving the handler, so a row
+// spelled "VMess" must still show the address its settings carry.
+const row = (protocol: string, settings: Record<string, unknown>): OutboundRow => ({
+  key: 0,
+  tag: 'p',
+  protocol,
+  settings,
+});
+
+const vnext = { vnext: [{ address: 'a.example.com', port: 443 }] };
+
+describe('outboundAddresses', () => {
+  it('reads a capitalised vmess id', () => {
+    expect(outboundAddresses(row('VMess', vnext))).toEqual(['a.example.com:443']);
+  });
+
+  it('reads a capitalised trojan id', () => {
+    expect(
+      outboundAddresses(row('Trojan', { servers: [{ address: 'b.example.com', port: 8443 }] })),
+    ).toEqual(['b.example.com:8443']);
+  });
+
+  it('reads a capitalised wireguard id', () => {
+    expect(
+      outboundAddresses(row('WireGuard', { peers: [{ endpoint: 'c.example.com:51820' }] })),
+    ).toEqual(['c.example.com:51820']);
+  });
+
+  it('reads a capitalised dns id', () => {
+    expect(outboundAddresses(row('DNS', { rewriteAddress: '1.1.1.1', rewritePort: 53 }))).toEqual([
+      '1.1.1.1:53',
+    ]);
+  });
+
+  it('reads the flat server of a capitalised vless id', () => {
+    expect(outboundAddresses(row('VLESS', { address: 'd.example.com', port: 443 }))).toEqual([
+      'd.example.com:443',
+    ]);
+  });
+
+  it('leaves a canonical id unchanged', () => {
+    expect(outboundAddresses(row('vmess', vnext))).toEqual(['a.example.com:443']);
+  });
+
+  it('still returns nothing for a protocol that carries no address', () => {
+    expect(outboundAddresses(row('freedom', {}))).toEqual([]);
+  });
+
+  it('reads the vnext server of a vless row', () => {
+    expect(outboundAddresses(row('VLESS', vnext))).toEqual(['a.example.com:443']);
+  });
+
+  it('returns no bare separator for a vless row that carries no server', () => {
+    expect(outboundAddresses(row('VLESS', {}))).toEqual([]);
+  });
+
+  it('reads the flat server of a hysteria id', () => {
+    expect(outboundAddresses(row('hysteria', { address: 'e.example.com', port: 443 }))).toEqual([
+      'e.example.com:443',
+    ]);
+  });
+
+  it('reads the peer endpoint of an amneziawg id', () => {
+    expect(
+      outboundAddresses(row('amneziawg', { peers: [{ endpoint: 'f.example.com:51820' }] })),
+    ).toEqual(['f.example.com:51820']);
+  });
+});