From 5a90f7e3485a2e1eb2d8679e385ab81a99fc02c8 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 17:49:37 +0200 Subject: [PATCH] refactor(frontend): align hysteria with new docs + drop hysteria2 protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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") --- frontend/public/openapi.json | 2 +- frontend/src/lib/xray/inbound-defaults.ts | 33 +++- frontend/src/lib/xray/inbound-link.ts | 4 +- frontend/src/lib/xray/outbound-defaults.ts | 7 - frontend/src/lib/xray/outbound-link-parser.ts | 5 +- frontend/src/pages/api-docs/endpoints.ts | 2 +- .../src/pages/clients/ClientBulkAddModal.tsx | 2 +- .../src/pages/clients/ClientFormModal.tsx | 2 +- .../src/pages/inbounds/InboundFormModal.tsx | 159 ++++++++++------- frontend/src/pages/xray/OutboundFormModal.tsx | 119 +------------ frontend/src/schemas/primitives/protocol.ts | 3 +- .../schemas/protocols/inbound/hysteria2.ts | 13 -- .../src/schemas/protocols/inbound/index.ts | 6 +- frontend/src/schemas/protocols/inbound/tun.ts | 12 ++ .../schemas/protocols/outbound/hysteria2.ts | 12 -- .../src/schemas/protocols/outbound/index.ts | 3 - .../src/schemas/protocols/stream/hysteria.ts | 55 ++---- .../inbound-defaults.test.ts.snap | 18 +- .../protocol-capabilities.test.ts.snap | 168 ------------------ .../test/__snapshots__/protocols.test.ts.snap | 23 --- .../fixtures/inbound/hysteria2-basic.json | 20 --- frontend/src/test/inbound-defaults.test.ts | 9 +- frontend/src/test/inbound-link.test.ts | 6 - frontend/src/test/outbound-defaults.test.ts | 16 +- 24 files changed, 171 insertions(+), 528 deletions(-) delete mode 100644 frontend/src/schemas/protocols/inbound/hysteria2.ts create mode 100644 frontend/src/schemas/protocols/inbound/tun.ts delete mode 100644 frontend/src/schemas/protocols/outbound/hysteria2.ts delete mode 100644 frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index fedcded9..50617bee 100644 --- a/frontend/public/openapi.json +++ b/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": [ { diff --git a/frontend/src/lib/xray/inbound-defaults.ts b/frontend/src/lib/xray/inbound-defaults.ts index d658373f..4c78fcc8 100644 --- a/frontend/src/lib/xray/inbound-defaults.ts +++ b/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; } diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 4a1560b9..c89e42c8 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/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 ?? '', diff --git a/frontend/src/lib/xray/outbound-defaults.ts b/frontend/src/lib/xray/outbound-defaults.ts index c979eb26..9b4a35b0 100644 --- a/frontend/src/lib/xray/outbound-defaults.ts +++ b/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; } diff --git a/frontend/src/lib/xray/outbound-link-parser.ts b/frontend/src/lib/xray/outbound-link-parser.ts index fff79025..8785d4cd 100644 --- a/frontend/src/lib/xray/outbound-link-parser.ts +++ b/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') ?? '', diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index 6fc4e5a5..2f138ab7 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/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).' }, ], diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx index d4c9c537..67ce67f2 100644 --- a/frontend/src/pages/clients/ClientBulkAddModal.tsx +++ b/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 { diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index 1f2a4bdb..5a0f46dc 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/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 { diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx index 1b7a268e..80fb7ed8 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.tsx +++ b/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) ?? {}; const cleaned: Record = { ...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; + 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; + 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; + 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 = ( <> + + + + + + + + @@ -943,27 +992,34 @@ export default function InboundFormModal({ {fields.map((field, idx) => (
- - {fields.length > 1 && ( - - )} - + + + Peer {idx + 1} + {fields.length > 1 && ( + - - {fields.length > 0 && ( - - {fields.map((field, idx) => ( - - {String(idx + 1)} - - - - - - - - - ))} - - )} - - )} - + + + ( <> - @@ -1373,12 +1409,6 @@ export default function InboundFormModal({ > - - - + onSecurityChange(e.target.value)} > - none + {!tlsOnly && none} tls {realityOk && reality} diff --git a/frontend/src/pages/xray/OutboundFormModal.tsx b/frontend/src/pages/xray/OutboundFormModal.tsx index e0ec6977..b5185cac 100644 --- a/frontend/src/pages/xray/OutboundFormModal.tsx +++ b/frontend/src/pages/xray/OutboundFormModal.tsx @@ -149,16 +149,7 @@ function newStreamSlice(network: string): Record { 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({ - - - - - - - - {() => { - const udphop = form.getFieldValue([ - 'streamSettings', 'hysteriaSettings', 'udphop', - ]) as { port?: string } | undefined; - return ( - - form.setFieldValue( - ['streamSettings', 'hysteriaSettings', 'udphop'], - checked - ? { port: '', intervalMin: 30, intervalMax: 30 } - : undefined, - ) - } - /> - ); - }} - - - - {() => { - const udphop = form.getFieldValue([ - 'streamSettings', 'hysteriaSettings', 'udphop', - ]) as { port?: string } | undefined; - if (!udphop) return null; - return ( - <> - - - - - - - - - - - ); - }} - - - - - - - - - - -
- Receive-window tuning (init/maxStreamReceiveWindow, - init/maxConnectionReceiveWindow) is rarely changed - — edit via the JSON tab if needed. -
)} diff --git a/frontend/src/schemas/primitives/protocol.ts b/frontend/src/schemas/primitives/protocol.ts index 9b619680..07723055 100644 --- a/frontend/src/schemas/primitives/protocol.ts +++ b/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; @@ -27,7 +27,6 @@ export const Protocols = Object.freeze({ SHADOWSOCKS: 'shadowsocks', WIREGUARD: 'wireguard', HYSTERIA: 'hysteria', - HYSTERIA2: 'hysteria2', HTTP: 'http', MIXED: 'mixed', TUNNEL: 'tunnel', diff --git a/frontend/src/schemas/protocols/inbound/hysteria2.ts b/frontend/src/schemas/protocols/inbound/hysteria2.ts deleted file mode 100644 index 135bee03..00000000 --- a/frontend/src/schemas/protocols/inbound/hysteria2.ts +++ /dev/null @@ -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; diff --git a/frontend/src/schemas/protocols/inbound/index.ts b/frontend/src/schemas/protocols/inbound/index.ts index 984579c5..18a8f022 100644 --- a/frontend/src/schemas/protocols/inbound/index.ts +++ b/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; diff --git a/frontend/src/schemas/protocols/inbound/tun.ts b/frontend/src/schemas/protocols/inbound/tun.ts new file mode 100644 index 00000000..19084492 --- /dev/null +++ b/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; diff --git a/frontend/src/schemas/protocols/outbound/hysteria2.ts b/frontend/src/schemas/protocols/outbound/hysteria2.ts deleted file mode 100644 index aae86d87..00000000 --- a/frontend/src/schemas/protocols/outbound/hysteria2.ts +++ /dev/null @@ -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; diff --git a/frontend/src/schemas/protocols/outbound/index.ts b/frontend/src/schemas/protocols/outbound/index.ts index abe42752..0dd3af20 100644 --- a/frontend/src/schemas/protocols/outbound/index.ts +++ b/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 }), diff --git a/frontend/src/schemas/protocols/stream/hysteria.ts b/frontend/src/schemas/protocols/stream/hysteria.ts index 8876dc53..c1a6efc6 100644 --- a/frontend/src/schemas/protocols/stream/hysteria.ts +++ b/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; - -// `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; 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; diff --git a/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap b/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap index 6a973a42..daf1c4fe 100644 --- a/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap +++ b/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=", } `; diff --git a/frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap b/frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap index 7d8b1737..3a33292e 100644 --- a/frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap +++ b/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, diff --git a/frontend/src/test/__snapshots__/protocols.test.ts.snap b/frontend/src/test/__snapshots__/protocols.test.ts.snap index 5ac334f2..646f831a 100644 --- a/frontend/src/test/__snapshots__/protocols.test.ts.snap +++ b/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": "hy2-client@example.test", - "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", diff --git a/frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json b/frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json deleted file mode 100644 index 1339df5d..00000000 --- a/frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "protocol": "hysteria2", - "settings": { - "version": 2, - "clients": [ - { - "auth": "hyst3ria2-auth-token-XYZ", - "email": "hy2-client@example.test", - "limitIp": 0, - "totalGB": 0, - "expiryTime": 0, - "enable": true, - "tgId": 0, - "subId": "hy2-001", - "comment": "", - "reset": 0 - } - ] - } -} diff --git a/frontend/src/test/inbound-defaults.test.ts b/frontend/src/test/inbound-defaults.test.ts index 5501e7c4..5ca6d44d 100644 --- a/frontend/src/test/inbound-defaults.test.ts +++ b/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); diff --git a/frontend/src/test/inbound-link.test.ts b/frontend/src/test/inbound-link.test.ts index 6b34ef1e..4ac8f10e 100644 --- a/frontend/src/test/inbound-link.test.ts +++ b/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({ diff --git a/frontend/src/test/outbound-defaults.test.ts b/frontend/src/test/outbound-defaults.test.ts index b5d467e7..f5c5313d 100644 --- a/frontend/src/test/outbound-defaults.test.ts +++ b/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) {