mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 09:29:34 +00:00
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")
This commit is contained in:
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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') ?? '',
|
||||
|
||||
@@ -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).' },
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
]}
|
||||
/>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</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 />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>;
|
||||
@@ -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
frontend/src/schemas/protocols/inbound/tun.ts
Normal file
12
frontend/src/schemas/protocols/inbound/tun.ts
Normal file
@@ -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>;
|
||||
@@ -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>;
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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=",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user