Procházet zdrojové kódy

refactor(frontend): align hysteria with new docs + drop hysteria2 protocol

Phase 2 smoke fixes on the Inbound add flow surfaced that hysteria2 was
modeled as a separate top-level protocol when it's really just hysteria
v2. The xray transports/hysteria.html docs also pin the hysteria stream
to a minimal shape (version/auth/udpIdleTimeout/masquerade) — the
previous schema carried legacy congestion/up/down/udphop/window knobs
that aren't part of the wire contract.

Hysteria2 removal:
- Drop 'hysteria2' from ProtocolSchema enum and Protocols const
- Drop hysteria2 branches from inbound/outbound discriminated unions
- Drop createDefaultHysteria2InboundSettings / OutboundSettings
- Delete schemas/protocols/inbound/hysteria2.ts and outbound/hysteria2.ts
- Drop hysteria2 case in getInboundClients / genLink (fell through to
  the hysteria handler anyway)
- Update client form modals' MULTI_CLIENT_PROTOCOLS sets
- Remove hysteria2-basic fixture + snapshot entries (14 capability
  cases, 1 protocols fixture, 1 inbound-defaults factory)
- Keep parseHysteria2Link() outbound parser since hysteria2:// is the
  share-link URI prefix for hysteria v2

Hysteria stream alignment with xtls docs:
- HysteriaStreamSettingsSchema reduced to version/auth/udpIdleTimeout/
  masquerade per transports/hysteria.html
- Masquerade type adds '' (default 404 page) and defaults to it
- Outbound form drops Congestion/Upload/Download/UDP hop/Max idle/
  Keep alive/Disable Path MTU controls and the receive-window note
- newStreamSlice('hysteria') in OutboundFormModal mirrors the trimmed
  shape; outbound-link-parser emits the trimmed shape too
- InboundFormModal Masquerade Select gains the default option

New TUN inbound schema:
- Add schemas/protocols/inbound/tun.ts with name/mtu/gateway/dns/
  userLevel/autoSystemRoutingTable/autoOutboundsInterface
- Wire into ProtocolSchema enum, InboundSettingsSchema discriminated
  union, createDefaultInboundSettings dispatcher

Other Phase 2 smoke fixes folded in:
- Tunnel portMap UI swaps Form.List for HeaderMapEditor v1 — wire
  shape is Record<string,string> and the List was producing arrays
- Hysteria onValuesChange seeds full TLS schema defaults + one
  empty certificate row (Cipher Suites/Min/Max Version/uTLS/ALPN
  were undefined before)
- HTTP/Mixed accounts Add button auto-fills user/pass with
  RandomUtil.randomLowerAndNum
- Hysteria security tab gates the 'none' radio out — TLS only
- Hysteria stream tab drops the inbound Auth password field (xray
  inbound auth is per-user via 'users', not stream-level)
- Reality onSecurityChange auto-randomizes target/serverNames/
  shortIds and fetches an X25519 keypair
- Tag and DB-side fields (up/down/total/expiryTime/
  lastTrafficResetTime/clientStats/security) gain hidden Form.Items
  so validateFields keeps them in the wire payload (rc-component
  form strips unregistered fields)
- WireGuard inbound auto-seeds one peer with generated keypair,
  allowedIPs ['10.0.0.2/32'], keepAlive 0 — matches legacy
- WireGuard peer rows separated by Divider with the Peer N title
  and a small inline remove button (titlePlacement="center")
MHSanaei před 1 dnem
rodič
revize
5a90f7e348

+ 1 - 1
frontend/public/openapi.json

@@ -3025,7 +3025,7 @@
         "tags": [
           "Clients"
         ],
-        "summary": "Return every URL for one client across all attached inbounds — the same strings the Copy URL button copies in the panel UI. Supported protocols: vmess, vless, trojan, shadowsocks, hysteria, hysteria2. If streamSettings.externalProxy is set, returns one URL per external proxy. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing.",
+        "summary": "Return every URL for one client across all attached inbounds — the same strings the Copy URL button copies in the panel UI. Supported protocols: vmess, vless, trojan, shadowsocks, hysteria. If streamSettings.externalProxy is set, returns one URL per external proxy. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing.",
         "operationId": "get_panel_api_clients_links_email",
         "parameters": [
           {

+ 25 - 8
frontend/src/lib/xray/inbound-defaults.ts

@@ -1,11 +1,11 @@
 import { RandomUtil, Wireguard } from '@/utils';
 
 import type { HttpInboundSettings } from '@/schemas/protocols/inbound/http';
-import type { Hysteria2InboundSettings } from '@/schemas/protocols/inbound/hysteria2';
 import type { HysteriaClient, HysteriaInboundSettings } from '@/schemas/protocols/inbound/hysteria';
 import type { MixedInboundSettings } from '@/schemas/protocols/inbound/mixed';
 import type { ShadowsocksClient, ShadowsocksInboundSettings } from '@/schemas/protocols/inbound/shadowsocks';
 import type { TrojanClient, TrojanInboundSettings } from '@/schemas/protocols/inbound/trojan';
+import type { TunInboundSettings } from '@/schemas/protocols/inbound/tun';
 import type { TunnelInboundSettings } from '@/schemas/protocols/inbound/tunnel';
 import type { VlessClient, VlessInboundSettings } from '@/schemas/protocols/inbound/vless';
 import type { VmessClient, VmessInboundSettings } from '@/schemas/protocols/inbound/vmess';
@@ -184,10 +184,6 @@ export function createDefaultHysteriaInboundSettings(
   };
 }
 
-export function createDefaultHysteria2InboundSettings(): Hysteria2InboundSettings {
-  return { version: 2, clients: [] };
-}
-
 export function createDefaultHttpInboundSettings(): HttpInboundSettings {
   return { accounts: [], allowTransparent: false };
 }
@@ -209,19 +205,40 @@ export function createDefaultTunnelInboundSettings(): TunnelInboundSettings {
   };
 }
 
+export function createDefaultTunInboundSettings(): TunInboundSettings {
+  return {
+    name: 'xray0',
+    mtu: 1500,
+    gateway: [],
+    dns: [],
+    userLevel: 0,
+    autoSystemRoutingTable: [],
+    autoOutboundsInterface: 'auto',
+  };
+}
+
 export interface WireguardInboundSeed {
   mtu?: number;
   secretKey?: string;
   noKernelTun?: boolean;
+  peerPrivateKey?: string;
 }
 
 export function createDefaultWireguardInboundSettings(
   seed: WireguardInboundSeed = {},
 ): WireguardInboundSettings {
+  const peerKp = seed.peerPrivateKey
+    ? { privateKey: seed.peerPrivateKey, publicKey: Wireguard.generateKeypair(seed.peerPrivateKey).publicKey }
+    : Wireguard.generateKeypair();
   return {
     mtu: seed.mtu ?? 1420,
     secretKey: seed.secretKey ?? Wireguard.generateKeypair().privateKey,
-    peers: [],
+    peers: [{
+      privateKey: peerKp.privateKey,
+      publicKey: peerKp.publicKey,
+      allowedIPs: ['10.0.0.2/32'],
+      keepAlive: 0,
+    }],
     noKernelTun: seed.noKernelTun ?? false,
   };
 }
@@ -237,9 +254,9 @@ export type AnyInboundSettings =
   | TrojanInboundSettings
   | ShadowsocksInboundSettings
   | HysteriaInboundSettings
-  | Hysteria2InboundSettings
   | HttpInboundSettings
   | MixedInboundSettings
+  | TunInboundSettings
   | TunnelInboundSettings
   | WireguardInboundSettings;
 
@@ -250,10 +267,10 @@ export function createDefaultInboundSettings(protocol: string): AnyInboundSettin
     case 'trojan':      return createDefaultTrojanInboundSettings();
     case 'shadowsocks': return createDefaultShadowsocksInboundSettings();
     case 'hysteria':    return createDefaultHysteriaInboundSettings();
-    case 'hysteria2':   return createDefaultHysteria2InboundSettings();
     case 'http':        return createDefaultHttpInboundSettings();
     case 'mixed':       return createDefaultMixedInboundSettings();
     case 'tunnel':      return createDefaultTunnelInboundSettings();
+    case 'tun':         return createDefaultTunInboundSettings();
     case 'wireguard':   return createDefaultWireguardInboundSettings();
     default:            return null;
   }

+ 1 - 3
frontend/src/lib/xray/inbound-link.ts

@@ -572,7 +572,7 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string {
     clientAuth,
   } = input;
 
-  if (inbound.protocol !== 'hysteria' && inbound.protocol !== 'hysteria2') return '';
+  if (inbound.protocol !== 'hysteria') return '';
   const stream = inbound.streamSettings;
   if (!stream || stream.security !== 'tls') return '';
 
@@ -707,7 +707,6 @@ export function getInboundClients(inbound: Inbound): ClientShape[] | null {
     case 'trojan':
       return (inbound.settings.clients ?? []) as ClientShape[];
     case 'hysteria':
-    case 'hysteria2':
       return (inbound.settings.clients ?? []) as ClientShape[];
     case 'shadowsocks': {
       const isMultiUser = inbound.settings.method !== '2022-blake3-chacha20-poly1305';
@@ -764,7 +763,6 @@ export function genLink(input: GenLinkInput): string {
         externalProxy,
       });
     case 'hysteria':
-    case 'hysteria2':
       return genHysteriaLink({
         inbound, address, port, remark,
         clientAuth: client.auth ?? '',

+ 0 - 7
frontend/src/lib/xray/outbound-defaults.ts

@@ -4,7 +4,6 @@ import type { BlackholeOutboundSettings } from '@/schemas/protocols/outbound/bla
 import type { DNSOutboundSettings } from '@/schemas/protocols/outbound/dns';
 import type { FreedomOutboundSettings } from '@/schemas/protocols/outbound/freedom';
 import type { HttpOutboundSettings } from '@/schemas/protocols/outbound/http';
-import type { Hysteria2OutboundSettings } from '@/schemas/protocols/outbound/hysteria2';
 import type { HysteriaOutboundSettings } from '@/schemas/protocols/outbound/hysteria';
 import type { LoopbackOutboundSettings } from '@/schemas/protocols/outbound/loopback';
 import type { ShadowsocksOutboundSettings } from '@/schemas/protocols/outbound/shadowsocks';
@@ -126,17 +125,12 @@ export function createDefaultHysteriaOutboundSettings(): HysteriaOutboundSetting
   return { address: '', port: 443, version: 2 };
 }
 
-export function createDefaultHysteria2OutboundSettings(): Hysteria2OutboundSettings {
-  return { address: '', port: 443, version: 2 };
-}
-
 export type AnyOutboundSettings =
   | BlackholeOutboundSettings
   | DNSOutboundSettings
   | FreedomOutboundSettings
   | HttpOutboundSettings
   | HysteriaOutboundSettings
-  | Hysteria2OutboundSettings
   | LoopbackOutboundSettings
   | ShadowsocksOutboundSettings
   | SocksOutboundSettings
@@ -167,7 +161,6 @@ export function createDefaultOutboundSettings(protocol: string): AnyOutboundSett
     case 'http':        return createDefaultHttpOutboundSettings();
     case 'wireguard':   return createDefaultWireguardOutboundSettings();
     case 'hysteria':    return createDefaultHysteriaOutboundSettings();
-    case 'hysteria2':   return createDefaultHysteria2OutboundSettings();
     case 'loopback':    return createDefaultLoopbackOutboundSettings();
     default:            return null;
   }

+ 1 - 4
frontend/src/lib/xray/outbound-link-parser.ts

@@ -363,10 +363,7 @@ export function parseHysteria2Link(link: string): Raw | null {
     network: 'hysteria',
     security: 'tls',
     hysteriaSettings: {
-      version: 2, auth, congestion: '', up: '0', down: '0',
-      initStreamReceiveWindow: 8388608, maxStreamReceiveWindow: 8388608,
-      initConnectionReceiveWindow: 20971520, maxConnectionReceiveWindow: 20971520,
-      maxIdleTimeout: 30, keepAlivePeriod: 2, disablePathMTUDiscovery: false,
+      version: 2, auth, udpIdleTimeout: 60,
     },
     tlsSettings: {
       serverName: params.get('sni') ?? '',

+ 1 - 1
frontend/src/pages/api-docs/endpoints.ts

@@ -590,7 +590,7 @@ export const sections: readonly Section[] = [
         method: 'GET',
         path: '/panel/api/clients/links/:email',
         summary:
-          "Return every URL for one client across all attached inbounds — the same strings the Copy URL button copies in the panel UI. Supported protocols: vmess, vless, trojan, shadowsocks, hysteria, hysteria2. If streamSettings.externalProxy is set, returns one URL per external proxy. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing.",
+          "Return every URL for one client across all attached inbounds — the same strings the Copy URL button copies in the panel UI. Supported protocols: vmess, vless, trojan, shadowsocks, hysteria. If streamSettings.externalProxy is set, returns one URL per external proxy. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing.",
         params: [
           { name: 'email', in: 'path', type: 'string', desc: 'Client email (unique identifier).' },
         ],

+ 1 - 1
frontend/src/pages/clients/ClientBulkAddModal.tsx

@@ -15,7 +15,7 @@ const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
 const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
 
 const MULTI_CLIENT_PROTOCOLS = new Set([
-  'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2',
+  'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria',
 ]);
 
 interface ClientBulkAddModalProps {

+ 1 - 1
frontend/src/pages/clients/ClientFormModal.tsx

@@ -27,7 +27,7 @@ import './ClientFormModal.css';
 const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
 
 const MULTI_CLIENT_PROTOCOLS = new Set([
-  'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2',
+  'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria',
 ]);
 
 interface ApiMsg<T = unknown> {

+ 97 - 62
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -5,6 +5,7 @@ import {
   Button,
   Card,
   Checkbox,
+  Divider,
   Empty,
   Form,
   Input,
@@ -61,6 +62,7 @@ import {
   UTLS_FINGERPRINT,
 } from '@/schemas/primitives';
 import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
+import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
 import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
 import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
 import { SniffingSchema } from '@/schemas/primitives/sniffing';
@@ -494,14 +496,46 @@ export default function InboundFormModal({
     form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
   };
 
-  const onSecurityChange = (next: string) => {
+  const onSecurityChange = async (next: string) => {
     const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
     const cleaned: Record<string, unknown> = { ...current, security: next };
     delete cleaned.tlsSettings;
     delete cleaned.realitySettings;
-    if (next === 'tls') cleaned.tlsSettings = TlsStreamSettingsSchema.parse({});
-    if (next === 'reality') cleaned.realitySettings = RealityStreamSettingsSchema.parse({});
+    if (next === 'tls') {
+      const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
+      tls.certificates = [{
+        useFile: true,
+        certificateFile: '',
+        keyFile: '',
+        certificate: [],
+        key: [],
+        oneTimeLoading: false,
+        usage: 'encipherment',
+        buildChain: false,
+      }];
+      cleaned.tlsSettings = tls;
+    }
+    if (next === 'reality') {
+      const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
+      const tgt = getRandomRealityTarget() as { target: string; sni: string };
+      reality.target = tgt.target;
+      reality.serverNames = tgt.sni.split(',').map((s) => s.trim()).filter(Boolean);
+      reality.shortIds = RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean);
+      cleaned.realitySettings = reality;
+    }
     form.setFieldValue('streamSettings', cleaned);
+    if (next === 'reality') {
+      try {
+        const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
+        if (msg?.success) {
+          const obj = msg.obj as { privateKey: string; publicKey: string };
+          form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
+          form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
+        }
+      } catch {
+        // best-effort: leave keypair fields empty if server call fails
+      }
+    }
   };
   const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
   const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false;
@@ -636,15 +670,22 @@ export default function InboundFormModal({
       // snap back to TCP so the standard network selector has a valid
       // starting point.
       if (next === Protocols.HYSTERIA) {
+        const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
+        tls.certificates = [{
+          useFile: true,
+          certificateFile: '',
+          keyFile: '',
+          certificate: [],
+          key: [],
+          oneTimeLoading: false,
+          usage: 'encipherment',
+          buildChain: false,
+        }];
         form.setFieldValue('streamSettings', {
           network: 'hysteria',
           security: 'tls',
-          hysteriaSettings: {
-            version: 2,
-            auth: '',
-            udpIdleTimeout: 60,
-          },
-          tlsSettings: {},
+          hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
+          tlsSettings: tls,
         });
       } else {
         const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
@@ -705,6 +746,14 @@ export default function InboundFormModal({
 
   const basicTab = (
     <>
+      <Form.Item name="tag" hidden noStyle><Input /></Form.Item>
+      <Form.Item name="up" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="down" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="total" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="expiryTime" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="lastTrafficResetTime" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="clientStats" hidden noStyle><Input /></Form.Item>
+
       <Form.Item name="enable" label={t('enable')} valuePropName="checked">
         <Switch />
       </Form.Item>
@@ -943,27 +992,34 @@ export default function InboundFormModal({
                 <Form.Item label="Peers">
                   <Button
                     size="small"
-                    onClick={() => add({
-                      publicKey: '',
-                      allowedIPs: [],
-                    })}
+                    onClick={() => {
+                      const kp = Wireguard.generateKeypair();
+                      add({
+                        privateKey: kp.privateKey,
+                        publicKey: kp.publicKey,
+                        allowedIPs: ['10.0.0.2/32'],
+                        keepAlive: 0,
+                      });
+                    }}
                   >
                     <PlusOutlined /> Add peer
                   </Button>
                 </Form.Item>
                 {fields.map((field, idx) => (
                   <div key={field.key} className="wg-peer">
-                    <Form.Item label={`Peer ${idx + 1}`}>
-                      {fields.length > 1 && (
-                        <Button
-                          size="small"
-                          danger
-                          onClick={() => remove(field.name)}
-                        >
-                          <MinusOutlined />
-                        </Button>
-                      )}
-                    </Form.Item>
+                    <Divider titlePlacement="center">
+                      <Space>
+                        <span>Peer {idx + 1}</span>
+                        {fields.length > 1 && (
+                          <Button
+                            size="small"
+                            danger
+                            icon={<MinusOutlined />}
+                            onClick={() => remove(field.name)}
+                          />
+                        )}
+                      </Space>
+                    </Divider>
                     <Form.Item
                       name={[field.name, 'privateKey']}
                       label={
@@ -1118,35 +1174,9 @@ export default function InboundFormModal({
               <Select.Option value="udp">UDP</Select.Option>
             </Select>
           </Form.Item>
-          <Form.List name={['settings', 'portMap']}>
-            {(fields, { add, remove }) => (
-              <>
-                <Form.Item label="Port map">
-                  <Button size="small" onClick={() => add({ name: '', value: '' })}>
-                    <PlusOutlined />
-                  </Button>
-                </Form.Item>
-                {fields.length > 0 && (
-                  <Form.Item wrapperCol={{ span: 24 }}>
-                    {fields.map((field, idx) => (
-                      <Space.Compact key={field.key} className="mb-8" block>
-                        <InputAddon>{String(idx + 1)}</InputAddon>
-                        <Form.Item name={[field.name, 'name']} noStyle>
-                          <Input placeholder="5555" />
-                        </Form.Item>
-                        <Form.Item name={[field.name, 'value']} noStyle>
-                          <Input placeholder="1.1.1.1:7777" />
-                        </Form.Item>
-                        <Button onClick={() => remove(field.name)}>
-                          <MinusOutlined />
-                        </Button>
-                      </Space.Compact>
-                    ))}
-                  </Form.Item>
-                )}
-              </>
-            )}
-          </Form.List>
+          <Form.Item label="Port map" name={['settings', 'portMap']}>
+            <HeaderMapEditor mode="v1" />
+          </Form.Item>
           <Form.Item
             name={['settings', 'followRedirect']}
             label="Follow redirect"
@@ -1163,7 +1193,13 @@ export default function InboundFormModal({
             {(fields, { add, remove }) => (
               <>
                 <Form.Item label="Accounts">
-                  <Button size="small" onClick={() => add({ user: '', pass: '' })}>
+                  <Button
+                    size="small"
+                    onClick={() => add({
+                      user: RandomUtil.randomLowerAndNum(8),
+                      pass: RandomUtil.randomLowerAndNum(12),
+                    })}
+                  >
                     <PlusOutlined /> Add
                   </Button>
                 </Form.Item>
@@ -1373,12 +1409,6 @@ export default function InboundFormModal({
           >
             <InputNumber min={2} max={2} disabled />
           </Form.Item>
-          <Form.Item
-            label="Auth password"
-            name={['streamSettings', 'hysteriaSettings', 'auth']}
-          >
-            <Input />
-          </Form.Item>
           <Form.Item
             label="UDP idle timeout (s)"
             name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
@@ -1400,7 +1430,7 @@ export default function InboundFormModal({
                         ['streamSettings', 'hysteriaSettings', 'masquerade'],
                         checked
                           ? {
-                              type: 'proxy', dir: '', url: '',
+                              type: '', dir: '', url: '',
                               rewriteHost: false, insecure: false,
                               content: '', headers: {}, statusCode: 0,
                             }
@@ -1426,6 +1456,7 @@ export default function InboundFormModal({
                   >
                     <Select
                       options={[
+                        { value: '', label: 'default (404 page)' },
                         { value: 'proxy', label: 'proxy (reverse proxy)' },
                         { value: 'file', label: 'file (serve directory)' },
                         { value: 'string', label: 'string (fixed body)' },
@@ -2161,6 +2192,9 @@ export default function InboundFormModal({
 
   const securityTab = (
     <>
+      <Form.Item name={['streamSettings', 'security']} hidden noStyle>
+        <Input />
+      </Form.Item>
       <Form.Item label={t('pages.inbounds.securityTab')}>
         <Form.Item
           noStyle
@@ -2176,6 +2210,7 @@ export default function InboundFormModal({
             const proto = getFieldValue('protocol') ?? '';
             const tlsOk = canEnableTls({ protocol: proto, streamSettings: { network: net, security: sec } });
             const realityOk = canEnableReality({ protocol: proto, streamSettings: { network: net, security: sec } });
+            const tlsOnly = proto === Protocols.HYSTERIA;
             return (
               <Radio.Group
                 value={sec}
@@ -2183,7 +2218,7 @@ export default function InboundFormModal({
                 disabled={!tlsOk}
                 onChange={(e) => onSecurityChange(e.target.value)}
               >
-                <Radio.Button value="none">none</Radio.Button>
+                {!tlsOnly && <Radio.Button value="none">none</Radio.Button>}
                 <Radio.Button value="tls">tls</Radio.Button>
                 {realityOk && <Radio.Button value="reality">reality</Radio.Button>}
               </Radio.Group>

+ 4 - 115
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -149,16 +149,7 @@ function newStreamSlice(network: string): Record<string, unknown> {
         hysteriaSettings: {
           version: 2,
           auth: '',
-          congestion: '',
-          up: '0',
-          down: '0',
-          initStreamReceiveWindow: 8388608,
-          maxStreamReceiveWindow: 8388608,
-          initConnectionReceiveWindow: 20971520,
-          maxConnectionReceiveWindow: 20971520,
-          maxIdleTimeout: 30,
-          keepAlivePeriod: 2,
-          disablePathMTUDiscovery: false,
+          udpIdleTimeout: 60,
         },
       };
     default:
@@ -1709,113 +1700,11 @@ export default function OutboundFormModal({
                               <Input />
                             </Form.Item>
                             <Form.Item
-                              label="Congestion"
-                              name={['streamSettings', 'hysteriaSettings', 'congestion']}
+                              label="UDP idle timeout (s)"
+                              name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
                             >
-                              <Select
-                                options={[
-                                  { value: '', label: 'BBR (auto)' },
-                                  { value: 'brutal', label: 'Brutal' },
-                                ]}
-                              />
-                            </Form.Item>
-                            <Form.Item
-                              label="Upload"
-                              name={['streamSettings', 'hysteriaSettings', 'up']}
-                            >
-                              <Input placeholder="100 mbps" />
-                            </Form.Item>
-                            <Form.Item
-                              label="Download"
-                              name={['streamSettings', 'hysteriaSettings', 'down']}
-                            >
-                              <Input placeholder="100 mbps" />
-                            </Form.Item>
-                            <Form.Item label="UDP hop">
-                              <Form.Item
-                                shouldUpdate
-                                noStyle
-                              >
-                                {() => {
-                                  const udphop = form.getFieldValue([
-                                    'streamSettings', 'hysteriaSettings', 'udphop',
-                                  ]) as { port?: string } | undefined;
-                                  return (
-                                    <Switch
-                                      checked={!!udphop}
-                                      onChange={(checked) =>
-                                        form.setFieldValue(
-                                          ['streamSettings', 'hysteriaSettings', 'udphop'],
-                                          checked
-                                            ? { port: '', intervalMin: 30, intervalMax: 30 }
-                                            : undefined,
-                                        )
-                                      }
-                                    />
-                                  );
-                                }}
-                              </Form.Item>
-                            </Form.Item>
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                const udphop = form.getFieldValue([
-                                  'streamSettings', 'hysteriaSettings', 'udphop',
-                                ]) as { port?: string } | undefined;
-                                if (!udphop) return null;
-                                return (
-                                  <>
-                                    <Form.Item
-                                      label="UDP hop port"
-                                      name={['streamSettings', 'hysteriaSettings', 'udphop', 'port']}
-                                    >
-                                      <Input placeholder="1145-1919" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label="UDP hop interval min (s)"
-                                      name={[
-                                        'streamSettings', 'hysteriaSettings',
-                                        'udphop', 'intervalMin',
-                                      ]}
-                                    >
-                                      <InputNumber min={1} />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label="UDP hop interval max (s)"
-                                      name={[
-                                        'streamSettings', 'hysteriaSettings',
-                                        'udphop', 'intervalMax',
-                                      ]}
-                                    >
-                                      <InputNumber min={1} />
-                                    </Form.Item>
-                                  </>
-                                );
-                              }}
-                            </Form.Item>
-                            <Form.Item
-                              label="Max idle (s)"
-                              name={['streamSettings', 'hysteriaSettings', 'maxIdleTimeout']}
-                            >
-                              <InputNumber min={1} />
-                            </Form.Item>
-                            <Form.Item
-                              label="Keep alive (s)"
-                              name={['streamSettings', 'hysteriaSettings', 'keepAlivePeriod']}
-                            >
-                              <InputNumber min={1} />
-                            </Form.Item>
-                            <Form.Item
-                              label="Disable Path MTU"
-                              name={['streamSettings', 'hysteriaSettings', 'disablePathMTUDiscovery']}
-                              valuePropName="checked"
-                            >
-                              <Switch />
+                              <InputNumber min={1} style={{ width: '100%' }} />
                             </Form.Item>
-                            <div style={{ marginTop: 4, opacity: 0.6, fontStyle: 'italic' }}>
-                              Receive-window tuning (init/maxStreamReceiveWindow,
-                              init/maxConnectionReceiveWindow) is rarely changed
-                              — edit via the JSON tab if needed.
-                            </div>
                           </>
                         )}
                       </>

+ 1 - 2
frontend/src/schemas/primitives/protocol.ts

@@ -7,10 +7,10 @@ export const ProtocolSchema = z.enum([
   'shadowsocks',
   'wireguard',
   'hysteria',
-  'hysteria2',
   'http',
   'mixed',
   'tunnel',
+  'tun',
 ]);
 export type Protocol = z.infer<typeof ProtocolSchema>;
 
@@ -27,7 +27,6 @@ export const Protocols = Object.freeze({
   SHADOWSOCKS: 'shadowsocks',
   WIREGUARD: 'wireguard',
   HYSTERIA: 'hysteria',
-  HYSTERIA2: 'hysteria2',
   HTTP: 'http',
   MIXED: 'mixed',
   TUNNEL: 'tunnel',

+ 0 - 13
frontend/src/schemas/protocols/inbound/hysteria2.ts

@@ -1,13 +0,0 @@
-import { z } from 'zod';
-
-import { HysteriaClientSchema } from '@/schemas/protocols/inbound/hysteria';
-
-// hysteria2 is wire-distinct from hysteria (different parent protocol literal,
-// different Go validate tag) but the panel's settings payload is structurally
-// identical — same client shape, same auth-based clients. We pin `version` to
-// the literal 2 here so a hysteria2 inbound can never silently downgrade.
-export const Hysteria2InboundSettingsSchema = z.object({
-  version: z.literal(2).default(2),
-  clients: z.array(HysteriaClientSchema).default([]),
-});
-export type Hysteria2InboundSettings = z.infer<typeof Hysteria2InboundSettingsSchema>;

+ 3 - 3
frontend/src/schemas/protocols/inbound/index.ts

@@ -1,11 +1,11 @@
 import { z } from 'zod';
 
 import { HttpInboundSettingsSchema } from './http';
-import { Hysteria2InboundSettingsSchema } from './hysteria2';
 import { HysteriaInboundSettingsSchema } from './hysteria';
 import { MixedInboundSettingsSchema } from './mixed';
 import { ShadowsocksInboundSettingsSchema } from './shadowsocks';
 import { TrojanInboundSettingsSchema } from './trojan';
+import { TunInboundSettingsSchema } from './tun';
 import { TunnelInboundSettingsSchema } from './tunnel';
 import { VlessInboundSettingsSchema } from './vless';
 import { VmessInboundSettingsSchema } from './vmess';
@@ -13,10 +13,10 @@ import { WireguardInboundSettingsSchema } from './wireguard';
 
 export * from './http';
 export * from './hysteria';
-export * from './hysteria2';
 export * from './mixed';
 export * from './shadowsocks';
 export * from './trojan';
+export * from './tun';
 export * from './tunnel';
 export * from './vless';
 export * from './vmess';
@@ -34,9 +34,9 @@ export const InboundSettingsSchema = z.discriminatedUnion('protocol', [
   z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksInboundSettingsSchema }),
   z.object({ protocol: z.literal('wireguard'),   settings: WireguardInboundSettingsSchema }),
   z.object({ protocol: z.literal('hysteria'),    settings: HysteriaInboundSettingsSchema }),
-  z.object({ protocol: z.literal('hysteria2'),   settings: Hysteria2InboundSettingsSchema }),
   z.object({ protocol: z.literal('http'),        settings: HttpInboundSettingsSchema }),
   z.object({ protocol: z.literal('mixed'),       settings: MixedInboundSettingsSchema }),
   z.object({ protocol: z.literal('tunnel'),      settings: TunnelInboundSettingsSchema }),
+  z.object({ protocol: z.literal('tun'),         settings: TunInboundSettingsSchema }),
 ]);
 export type InboundSettings = z.infer<typeof InboundSettingsSchema>;

+ 12 - 0
frontend/src/schemas/protocols/inbound/tun.ts

@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+export const TunInboundSettingsSchema = z.object({
+  name: z.string().default('xray0'),
+  mtu: z.number().int().min(0).default(1500),
+  gateway: z.array(z.string()).default([]),
+  dns: z.array(z.string()).default([]),
+  userLevel: z.number().int().min(0).default(0),
+  autoSystemRoutingTable: z.array(z.string()).default([]),
+  autoOutboundsInterface: z.string().default('auto'),
+});
+export type TunInboundSettings = z.infer<typeof TunInboundSettingsSchema>;

+ 0 - 12
frontend/src/schemas/protocols/outbound/hysteria2.ts

@@ -1,12 +0,0 @@
-import { z } from 'zod';
-
-import { PortSchema } from '@/schemas/primitives';
-
-// Outbound counterpart to hysteria2 — same {address, port} connect descriptor
-// as hysteria, but version locked to 2.
-export const Hysteria2OutboundSettingsSchema = z.object({
-  address: z.string().min(1),
-  port: PortSchema,
-  version: z.literal(2).default(2),
-});
-export type Hysteria2OutboundSettings = z.infer<typeof Hysteria2OutboundSettingsSchema>;

+ 0 - 3
frontend/src/schemas/protocols/outbound/index.ts

@@ -4,7 +4,6 @@ import { BlackholeOutboundSettingsSchema } from './blackhole';
 import { DNSOutboundSettingsSchema } from './dns';
 import { FreedomOutboundSettingsSchema } from './freedom';
 import { HttpOutboundSettingsSchema } from './http';
-import { Hysteria2OutboundSettingsSchema } from './hysteria2';
 import { HysteriaOutboundSettingsSchema } from './hysteria';
 import { LoopbackOutboundSettingsSchema } from './loopback';
 import { ShadowsocksOutboundSettingsSchema } from './shadowsocks';
@@ -19,7 +18,6 @@ export * from './dns';
 export * from './freedom';
 export * from './http';
 export * from './hysteria';
-export * from './hysteria2';
 export * from './loopback';
 export * from './shadowsocks';
 export * from './socks';
@@ -39,7 +37,6 @@ export const OutboundSettingsSchema = z.discriminatedUnion('protocol', [
   z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksOutboundSettingsSchema }),
   z.object({ protocol: z.literal('wireguard'),   settings: WireguardOutboundSettingsSchema }),
   z.object({ protocol: z.literal('hysteria'),    settings: HysteriaOutboundSettingsSchema }),
-  z.object({ protocol: z.literal('hysteria2'),   settings: Hysteria2OutboundSettingsSchema }),
   z.object({ protocol: z.literal('http'),        settings: HttpOutboundSettingsSchema }),
   z.object({ protocol: z.literal('socks'),       settings: SocksOutboundSettingsSchema }),
   z.object({ protocol: z.literal('freedom'),     settings: FreedomOutboundSettingsSchema }),

+ 11 - 44
frontend/src/schemas/protocols/stream/hysteria.ts

@@ -1,29 +1,17 @@
 import { z } from 'zod';
 
-// Hysteria stream transport — the hysteria-specific knobs that ride
-// alongside the connect target on outbound (and the inbound side too,
-// where the listening peer needs matching auth / congestion / obfs).
-// Wire shape mirrors xray-core's HysteriaConfig, with udphop nested
-// when port-hopping is on and omitted otherwise.
+// Hysteria stream transport. Per Xray docs (transports/hysteria.html), the
+// Xray implementation of Hysteria2's underlying QUIC transport keeps only
+// the essentials — version, auth, udpIdleTimeout, and masquerade. The
+// extended bandwidth/window/udphop knobs that earlier hysteria builds
+// exposed are not part of this transport's wire shape.
 
-export const HysteriaUdphopSchema = z.object({
-  port: z.string().default(''),
-  intervalMin: z.number().int().min(1).default(30),
-  intervalMax: z.number().int().min(1).default(30),
-});
-export type HysteriaUdphop = z.infer<typeof HysteriaUdphopSchema>;
-
-// `congestion` is `''` (BBR, the default) or `'brutal'`. Both empty and
-// missing are equivalent on the wire so we accept either.
-export const HysteriaCongestionSchema = z.union([z.literal(''), z.literal('brutal')]);
-
-// Inbound-only masquerade sub-object. Xray's hysteria inbound can disguise
-// itself as an HTTP server by serving static files (`type: 'file'`),
-// reverse-proxying upstream traffic (`type: 'proxy'`), or returning a
-// fixed string body (`type: 'string'`). Fields are loose-typed strings
-// because the panel writes them as free-form input.
+// Inbound masquerade — Xray's hysteria inbound can disguise itself as an
+// HTTP/3 server. `type` is the empty string by default (serves the default
+// 404 page), and per-type config keys are only honored when their type is
+// active.
 export const HysteriaMasqueradeSchema = z.object({
-  type: z.enum(['proxy', 'file', 'string']).default('proxy'),
+  type: z.enum(['', 'proxy', 'file', 'string']).default(''),
   dir: z.string().default(''),
   url: z.string().default(''),
   rewriteHost: z.boolean().default(false),
@@ -35,30 +23,9 @@ export const HysteriaMasqueradeSchema = z.object({
 export type HysteriaMasquerade = z.infer<typeof HysteriaMasqueradeSchema>;
 
 export const HysteriaStreamSettingsSchema = z.object({
-  // Outbound-side fields. The version field is shared with inbound and
-  // typically locked to 2.
   version: z.literal(2).default(2),
   auth: z.string().default(''),
-  congestion: HysteriaCongestionSchema.default(''),
-  // up / down are dash-separated bandwidth strings like '100 mbps' / '1 gbps'.
-  // The panel stores them as free-form strings and Xray parses on the
-  // server side; no client-side validation.
-  up: z.string().default('0'),
-  down: z.string().default('0'),
-  udphop: HysteriaUdphopSchema.optional(),
-  initStreamReceiveWindow: z.number().int().min(0).default(8388608),
-  maxStreamReceiveWindow: z.number().int().min(0).default(8388608),
-  initConnectionReceiveWindow: z.number().int().min(0).default(20971520),
-  maxConnectionReceiveWindow: z.number().int().min(0).default(20971520),
-  maxIdleTimeout: z.number().int().min(1).default(30),
-  keepAlivePeriod: z.number().int().min(1).default(2),
-  disablePathMTUDiscovery: z.boolean().default(false),
-  // Inbound-side fields. xray-core's HysteriaConfig accepts both sets in
-  // the same struct; outbound emits the bandwidth/udphop block, inbound
-  // emits the protocol/udpIdleTimeout/masquerade block. The panel can
-  // round-trip both shapes through this single schema.
-  protocol: z.string().optional(),
-  udpIdleTimeout: z.number().int().min(1).optional(),
+  udpIdleTimeout: z.number().int().min(1).default(60),
   masquerade: HysteriaMasqueradeSchema.optional(),
 });
 export type HysteriaStreamSettings = z.infer<typeof HysteriaStreamSettingsSchema>;

+ 10 - 8
frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap

@@ -14,13 +14,6 @@ exports[`createDefault*InboundSettings factories > hysteria (v1, defaults to v2
 }
 `;
 
-exports[`createDefault*InboundSettings factories > hysteria2 1`] = `
-{
-  "clients": [],
-  "version": 2,
-}
-`;
-
 exports[`createDefault*InboundSettings factories > mixed 1`] = `
 {
   "accounts": [],
@@ -74,7 +67,16 @@ exports[`createDefault*InboundSettings factories > wireguard 1`] = `
 {
   "mtu": 1420,
   "noKernelTun": false,
-  "peers": [],
+  "peers": [
+    {
+      "allowedIPs": [
+        "10.0.0.2/32",
+      ],
+      "keepAlive": 0,
+      "privateKey": "cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==",
+      "publicKey": "RNa/H++60PStnhoiiU/vIuwFimZUBuIkLkbrmEoDz34=",
+    },
+  ],
   "secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
 }
 `;

+ 0 - 168
frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap

@@ -336,174 +336,6 @@ exports[`protocol capability predicates > hysteria-basic :: xhttp/tls 1`] = `
 }
 `;
 
-exports[`protocol capability predicates > hysteria2-basic :: grpc/none 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: grpc/reality 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: grpc/tls 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: httpupgrade/none 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: httpupgrade/tls 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: kcp/none 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: tcp/none 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: tcp/reality 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: tcp/tls 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: ws/none 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: ws/tls 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: xhttp/none 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: xhttp/reality 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
-exports[`protocol capability predicates > hysteria2-basic :: xhttp/tls 1`] = `
-{
-  "canEnableReality": false,
-  "canEnableStream": false,
-  "canEnableTls": false,
-  "canEnableTlsFlow": false,
-  "canEnableVisionSeed": false,
-  "isSS2022": false,
-  "isSSMultiUser": true,
-}
-`;
-
 exports[`protocol capability predicates > mixed-basic :: grpc/none 1`] = `
 {
   "canEnableReality": false,

+ 0 - 23
frontend/src/test/__snapshots__/protocols.test.ts.snap

@@ -42,29 +42,6 @@ exports[`InboundSettingsSchema fixtures > parses hysteria-basic byte-stably 1`]
 }
 `;
 
-exports[`InboundSettingsSchema fixtures > parses hysteria2-basic byte-stably 1`] = `
-{
-  "protocol": "hysteria2",
-  "settings": {
-    "clients": [
-      {
-        "auth": "hyst3ria2-auth-token-XYZ",
-        "comment": "",
-        "email": "[email protected]",
-        "enable": true,
-        "expiryTime": 0,
-        "limitIp": 0,
-        "reset": 0,
-        "subId": "hy2-001",
-        "tgId": 0,
-        "totalGB": 0,
-      },
-    ],
-    "version": 2,
-  },
-}
-`;
-
 exports[`InboundSettingsSchema fixtures > parses mixed-basic byte-stably 1`] = `
 {
   "protocol": "mixed",

+ 0 - 20
frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json

@@ -1,20 +0,0 @@
-{
-  "protocol": "hysteria2",
-  "settings": {
-    "version": 2,
-    "clients": [
-      {
-        "auth": "hyst3ria2-auth-token-XYZ",
-        "email": "[email protected]",
-        "limitIp": 0,
-        "totalGB": 0,
-        "expiryTime": 0,
-        "enable": true,
-        "tgId": 0,
-        "subId": "hy2-001",
-        "comment": "",
-        "reset": 0
-      }
-    ]
-  }
-}

+ 1 - 8
frontend/src/test/inbound-defaults.test.ts

@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
 
 import {
   createDefaultHttpInboundSettings,
-  createDefaultHysteria2InboundSettings,
   createDefaultHysteriaClient,
   createDefaultHysteriaInboundSettings,
   createDefaultMixedInboundSettings,
@@ -18,7 +17,6 @@ import {
   createDefaultWireguardInboundSettings,
 } from '@/lib/xray/inbound-defaults';
 import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
-import { Hysteria2InboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria2';
 import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
 import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
 import { ShadowsocksClientSchema, ShadowsocksInboundSettingsSchema } from '@/schemas/protocols/inbound/shadowsocks';
@@ -112,12 +110,6 @@ describe('createDefault*InboundSettings factories', () => {
     expect(HysteriaInboundSettingsSchema.parse(s)).toEqual(s);
   });
 
-  it('hysteria2', () => {
-    const s = createDefaultHysteria2InboundSettings();
-    expect(s).toMatchSnapshot();
-    expect(Hysteria2InboundSettingsSchema.parse(s)).toEqual(s);
-  });
-
   it('http', () => {
     const s = createDefaultHttpInboundSettings();
     expect(s).toMatchSnapshot();
@@ -139,6 +131,7 @@ describe('createDefault*InboundSettings factories', () => {
   it('wireguard', () => {
     const s = createDefaultWireguardInboundSettings({
       secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
+      peerPrivateKey: 'cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==',
     });
     expect(s).toMatchSnapshot();
     expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);

+ 0 - 6
frontend/src/test/inbound-link.test.ts

@@ -213,12 +213,6 @@ describe('genInboundLinks orchestrator', () => {
     .sort(([a], [b]) => a.localeCompare(b));
 
   for (const [name, raw] of fixtures) {
-    const protocol = (raw as { protocol?: string }).protocol;
-    // Skip hysteria2 — the legacy class had no dispatch case at the time
-    // the baseline was locked, so no snapshot exists. The new orchestrator
-    // covers it via its own logic and the genHysteriaLink unit test.
-    if (protocol === 'hysteria2') continue;
-
     it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       const block = genInboundLinks({

+ 1 - 15
frontend/src/test/outbound-defaults.test.ts

@@ -5,7 +5,6 @@ import {
   createDefaultDNSOutboundSettings,
   createDefaultFreedomOutboundSettings,
   createDefaultHttpOutboundSettings,
-  createDefaultHysteria2OutboundSettings,
   createDefaultHysteriaOutboundSettings,
   createDefaultLoopbackOutboundSettings,
   createDefaultShadowsocksOutboundSettings,
@@ -21,7 +20,6 @@ import {
   DNSOutboundSettingsSchema,
   FreedomOutboundSettingsSchema,
   HttpOutboundSettingsSchema,
-  Hysteria2OutboundSettingsSchema,
   HysteriaOutboundSettingsSchema,
   LoopbackOutboundSettingsSchema,
   ShadowsocksOutboundSettingsSchema,
@@ -132,12 +130,6 @@ describe('outbound default factories: shape snapshots', () => {
       address: '', port: 443, version: 2,
     });
   });
-
-  it('hysteria2 mirrors hysteria with literal version 2', () => {
-    expect(createDefaultHysteria2OutboundSettings()).toEqual({
-      address: '', port: 443, version: 2,
-    });
-  });
 });
 
 describe('outbound default factories: schema acceptance after stub fill-in', () => {
@@ -219,18 +211,12 @@ describe('outbound default factories: schema acceptance after stub fill-in', ()
     def.address = SAMPLE_ADDRESS;
     expect(HysteriaOutboundSettingsSchema.safeParse(def).success).toBe(true);
   });
-
-  it('hysteria2 parses once address is filled', () => {
-    const def = createDefaultHysteria2OutboundSettings();
-    def.address = SAMPLE_ADDRESS;
-    expect(Hysteria2OutboundSettingsSchema.safeParse(def).success).toBe(true);
-  });
 });
 
 describe('createDefaultOutboundSettings dispatcher', () => {
   const PROTOCOLS = [
     'freedom', 'blackhole', 'dns', 'vmess', 'vless', 'trojan', 'shadowsocks',
-    'socks', 'http', 'wireguard', 'hysteria', 'hysteria2', 'loopback',
+    'socks', 'http', 'wireguard', 'hysteria', 'loopback',
   ];
 
   for (const protocol of PROTOCOLS) {