mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +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:
parent
90e11dc0f6
commit
5a90f7e348
24 changed files with 171 additions and 528 deletions
|
|
@ -3025,7 +3025,7 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"Clients"
|
"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",
|
"operationId": "get_panel_api_clients_links_email",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { RandomUtil, Wireguard } from '@/utils';
|
import { RandomUtil, Wireguard } from '@/utils';
|
||||||
|
|
||||||
import type { HttpInboundSettings } from '@/schemas/protocols/inbound/http';
|
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 { HysteriaClient, HysteriaInboundSettings } from '@/schemas/protocols/inbound/hysteria';
|
||||||
import type { MixedInboundSettings } from '@/schemas/protocols/inbound/mixed';
|
import type { MixedInboundSettings } from '@/schemas/protocols/inbound/mixed';
|
||||||
import type { ShadowsocksClient, ShadowsocksInboundSettings } from '@/schemas/protocols/inbound/shadowsocks';
|
import type { ShadowsocksClient, ShadowsocksInboundSettings } from '@/schemas/protocols/inbound/shadowsocks';
|
||||||
import type { TrojanClient, TrojanInboundSettings } from '@/schemas/protocols/inbound/trojan';
|
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 { TunnelInboundSettings } from '@/schemas/protocols/inbound/tunnel';
|
||||||
import type { VlessClient, VlessInboundSettings } from '@/schemas/protocols/inbound/vless';
|
import type { VlessClient, VlessInboundSettings } from '@/schemas/protocols/inbound/vless';
|
||||||
import type { VmessClient, VmessInboundSettings } from '@/schemas/protocols/inbound/vmess';
|
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 {
|
export function createDefaultHttpInboundSettings(): HttpInboundSettings {
|
||||||
return { accounts: [], allowTransparent: false };
|
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 {
|
export interface WireguardInboundSeed {
|
||||||
mtu?: number;
|
mtu?: number;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
noKernelTun?: boolean;
|
noKernelTun?: boolean;
|
||||||
|
peerPrivateKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDefaultWireguardInboundSettings(
|
export function createDefaultWireguardInboundSettings(
|
||||||
seed: WireguardInboundSeed = {},
|
seed: WireguardInboundSeed = {},
|
||||||
): WireguardInboundSettings {
|
): WireguardInboundSettings {
|
||||||
|
const peerKp = seed.peerPrivateKey
|
||||||
|
? { privateKey: seed.peerPrivateKey, publicKey: Wireguard.generateKeypair(seed.peerPrivateKey).publicKey }
|
||||||
|
: Wireguard.generateKeypair();
|
||||||
return {
|
return {
|
||||||
mtu: seed.mtu ?? 1420,
|
mtu: seed.mtu ?? 1420,
|
||||||
secretKey: seed.secretKey ?? Wireguard.generateKeypair().privateKey,
|
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,
|
noKernelTun: seed.noKernelTun ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -237,9 +254,9 @@ export type AnyInboundSettings =
|
||||||
| TrojanInboundSettings
|
| TrojanInboundSettings
|
||||||
| ShadowsocksInboundSettings
|
| ShadowsocksInboundSettings
|
||||||
| HysteriaInboundSettings
|
| HysteriaInboundSettings
|
||||||
| Hysteria2InboundSettings
|
|
||||||
| HttpInboundSettings
|
| HttpInboundSettings
|
||||||
| MixedInboundSettings
|
| MixedInboundSettings
|
||||||
|
| TunInboundSettings
|
||||||
| TunnelInboundSettings
|
| TunnelInboundSettings
|
||||||
| WireguardInboundSettings;
|
| WireguardInboundSettings;
|
||||||
|
|
||||||
|
|
@ -250,10 +267,10 @@ export function createDefaultInboundSettings(protocol: string): AnyInboundSettin
|
||||||
case 'trojan': return createDefaultTrojanInboundSettings();
|
case 'trojan': return createDefaultTrojanInboundSettings();
|
||||||
case 'shadowsocks': return createDefaultShadowsocksInboundSettings();
|
case 'shadowsocks': return createDefaultShadowsocksInboundSettings();
|
||||||
case 'hysteria': return createDefaultHysteriaInboundSettings();
|
case 'hysteria': return createDefaultHysteriaInboundSettings();
|
||||||
case 'hysteria2': return createDefaultHysteria2InboundSettings();
|
|
||||||
case 'http': return createDefaultHttpInboundSettings();
|
case 'http': return createDefaultHttpInboundSettings();
|
||||||
case 'mixed': return createDefaultMixedInboundSettings();
|
case 'mixed': return createDefaultMixedInboundSettings();
|
||||||
case 'tunnel': return createDefaultTunnelInboundSettings();
|
case 'tunnel': return createDefaultTunnelInboundSettings();
|
||||||
|
case 'tun': return createDefaultTunInboundSettings();
|
||||||
case 'wireguard': return createDefaultWireguardInboundSettings();
|
case 'wireguard': return createDefaultWireguardInboundSettings();
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -572,7 +572,7 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string {
|
||||||
clientAuth,
|
clientAuth,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
if (inbound.protocol !== 'hysteria' && inbound.protocol !== 'hysteria2') return '';
|
if (inbound.protocol !== 'hysteria') return '';
|
||||||
const stream = inbound.streamSettings;
|
const stream = inbound.streamSettings;
|
||||||
if (!stream || stream.security !== 'tls') return '';
|
if (!stream || stream.security !== 'tls') return '';
|
||||||
|
|
||||||
|
|
@ -707,7 +707,6 @@ export function getInboundClients(inbound: Inbound): ClientShape[] | null {
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
return (inbound.settings.clients ?? []) as ClientShape[];
|
return (inbound.settings.clients ?? []) as ClientShape[];
|
||||||
case 'hysteria':
|
case 'hysteria':
|
||||||
case 'hysteria2':
|
|
||||||
return (inbound.settings.clients ?? []) as ClientShape[];
|
return (inbound.settings.clients ?? []) as ClientShape[];
|
||||||
case 'shadowsocks': {
|
case 'shadowsocks': {
|
||||||
const isMultiUser = inbound.settings.method !== '2022-blake3-chacha20-poly1305';
|
const isMultiUser = inbound.settings.method !== '2022-blake3-chacha20-poly1305';
|
||||||
|
|
@ -764,7 +763,6 @@ export function genLink(input: GenLinkInput): string {
|
||||||
externalProxy,
|
externalProxy,
|
||||||
});
|
});
|
||||||
case 'hysteria':
|
case 'hysteria':
|
||||||
case 'hysteria2':
|
|
||||||
return genHysteriaLink({
|
return genHysteriaLink({
|
||||||
inbound, address, port, remark,
|
inbound, address, port, remark,
|
||||||
clientAuth: client.auth ?? '',
|
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 { DNSOutboundSettings } from '@/schemas/protocols/outbound/dns';
|
||||||
import type { FreedomOutboundSettings } from '@/schemas/protocols/outbound/freedom';
|
import type { FreedomOutboundSettings } from '@/schemas/protocols/outbound/freedom';
|
||||||
import type { HttpOutboundSettings } from '@/schemas/protocols/outbound/http';
|
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 { HysteriaOutboundSettings } from '@/schemas/protocols/outbound/hysteria';
|
||||||
import type { LoopbackOutboundSettings } from '@/schemas/protocols/outbound/loopback';
|
import type { LoopbackOutboundSettings } from '@/schemas/protocols/outbound/loopback';
|
||||||
import type { ShadowsocksOutboundSettings } from '@/schemas/protocols/outbound/shadowsocks';
|
import type { ShadowsocksOutboundSettings } from '@/schemas/protocols/outbound/shadowsocks';
|
||||||
|
|
@ -126,17 +125,12 @@ export function createDefaultHysteriaOutboundSettings(): HysteriaOutboundSetting
|
||||||
return { address: '', port: 443, version: 2 };
|
return { address: '', port: 443, version: 2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDefaultHysteria2OutboundSettings(): Hysteria2OutboundSettings {
|
|
||||||
return { address: '', port: 443, version: 2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnyOutboundSettings =
|
export type AnyOutboundSettings =
|
||||||
| BlackholeOutboundSettings
|
| BlackholeOutboundSettings
|
||||||
| DNSOutboundSettings
|
| DNSOutboundSettings
|
||||||
| FreedomOutboundSettings
|
| FreedomOutboundSettings
|
||||||
| HttpOutboundSettings
|
| HttpOutboundSettings
|
||||||
| HysteriaOutboundSettings
|
| HysteriaOutboundSettings
|
||||||
| Hysteria2OutboundSettings
|
|
||||||
| LoopbackOutboundSettings
|
| LoopbackOutboundSettings
|
||||||
| ShadowsocksOutboundSettings
|
| ShadowsocksOutboundSettings
|
||||||
| SocksOutboundSettings
|
| SocksOutboundSettings
|
||||||
|
|
@ -167,7 +161,6 @@ export function createDefaultOutboundSettings(protocol: string): AnyOutboundSett
|
||||||
case 'http': return createDefaultHttpOutboundSettings();
|
case 'http': return createDefaultHttpOutboundSettings();
|
||||||
case 'wireguard': return createDefaultWireguardOutboundSettings();
|
case 'wireguard': return createDefaultWireguardOutboundSettings();
|
||||||
case 'hysteria': return createDefaultHysteriaOutboundSettings();
|
case 'hysteria': return createDefaultHysteriaOutboundSettings();
|
||||||
case 'hysteria2': return createDefaultHysteria2OutboundSettings();
|
|
||||||
case 'loopback': return createDefaultLoopbackOutboundSettings();
|
case 'loopback': return createDefaultLoopbackOutboundSettings();
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -363,10 +363,7 @@ export function parseHysteria2Link(link: string): Raw | null {
|
||||||
network: 'hysteria',
|
network: 'hysteria',
|
||||||
security: 'tls',
|
security: 'tls',
|
||||||
hysteriaSettings: {
|
hysteriaSettings: {
|
||||||
version: 2, auth, congestion: '', up: '0', down: '0',
|
version: 2, auth, udpIdleTimeout: 60,
|
||||||
initStreamReceiveWindow: 8388608, maxStreamReceiveWindow: 8388608,
|
|
||||||
initConnectionReceiveWindow: 20971520, maxConnectionReceiveWindow: 20971520,
|
|
||||||
maxIdleTimeout: 30, keepAlivePeriod: 2, disablePathMTUDiscovery: false,
|
|
||||||
},
|
},
|
||||||
tlsSettings: {
|
tlsSettings: {
|
||||||
serverName: params.get('sni') ?? '',
|
serverName: params.get('sni') ?? '',
|
||||||
|
|
|
||||||
|
|
@ -590,7 +590,7 @@ export const sections: readonly Section[] = [
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/clients/links/:email',
|
path: '/panel/api/clients/links/:email',
|
||||||
summary:
|
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: [
|
params: [
|
||||||
{ name: 'email', in: 'path', type: 'string', desc: 'Client email (unique identifier).' },
|
{ 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 JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
|
||||||
|
|
||||||
const MULTI_CLIENT_PROTOCOLS = new Set([
|
const MULTI_CLIENT_PROTOCOLS = new Set([
|
||||||
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2',
|
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface ClientBulkAddModalProps {
|
interface ClientBulkAddModalProps {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import './ClientFormModal.css';
|
||||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||||
|
|
||||||
const MULTI_CLIENT_PROTOCOLS = new Set([
|
const MULTI_CLIENT_PROTOCOLS = new Set([
|
||||||
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2',
|
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
interface ApiMsg<T = unknown> {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Divider,
|
||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
|
@ -61,6 +62,7 @@ import {
|
||||||
UTLS_FINGERPRINT,
|
UTLS_FINGERPRINT,
|
||||||
} from '@/schemas/primitives';
|
} from '@/schemas/primitives';
|
||||||
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
||||||
|
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
|
||||||
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
|
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
|
||||||
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
|
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
|
||||||
import { SniffingSchema } from '@/schemas/primitives/sniffing';
|
import { SniffingSchema } from '@/schemas/primitives/sniffing';
|
||||||
|
|
@ -494,14 +496,46 @@ export default function InboundFormModal({
|
||||||
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
|
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 current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
|
||||||
const cleaned: Record<string, unknown> = { ...current, security: next };
|
const cleaned: Record<string, unknown> = { ...current, security: next };
|
||||||
delete cleaned.tlsSettings;
|
delete cleaned.tlsSettings;
|
||||||
delete cleaned.realitySettings;
|
delete cleaned.realitySettings;
|
||||||
if (next === 'tls') cleaned.tlsSettings = TlsStreamSettingsSchema.parse({});
|
if (next === 'tls') {
|
||||||
if (next === 'reality') cleaned.realitySettings = RealityStreamSettingsSchema.parse({});
|
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);
|
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 xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
|
||||||
const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false;
|
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
|
// snap back to TCP so the standard network selector has a valid
|
||||||
// starting point.
|
// starting point.
|
||||||
if (next === Protocols.HYSTERIA) {
|
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', {
|
form.setFieldValue('streamSettings', {
|
||||||
network: 'hysteria',
|
network: 'hysteria',
|
||||||
security: 'tls',
|
security: 'tls',
|
||||||
hysteriaSettings: {
|
hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
|
||||||
version: 2,
|
tlsSettings: tls,
|
||||||
auth: '',
|
|
||||||
udpIdleTimeout: 60,
|
|
||||||
},
|
|
||||||
tlsSettings: {},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
|
const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
|
||||||
|
|
@ -705,6 +746,14 @@ export default function InboundFormModal({
|
||||||
|
|
||||||
const basicTab = (
|
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">
|
<Form.Item name="enable" label={t('enable')} valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -943,27 +992,34 @@ export default function InboundFormModal({
|
||||||
<Form.Item label="Peers">
|
<Form.Item label="Peers">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => add({
|
onClick={() => {
|
||||||
publicKey: '',
|
const kp = Wireguard.generateKeypair();
|
||||||
allowedIPs: [],
|
add({
|
||||||
})}
|
privateKey: kp.privateKey,
|
||||||
|
publicKey: kp.publicKey,
|
||||||
|
allowedIPs: ['10.0.0.2/32'],
|
||||||
|
keepAlive: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> Add peer
|
<PlusOutlined /> Add peer
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{fields.map((field, idx) => (
|
{fields.map((field, idx) => (
|
||||||
<div key={field.key} className="wg-peer">
|
<div key={field.key} className="wg-peer">
|
||||||
<Form.Item label={`Peer ${idx + 1}`}>
|
<Divider titlePlacement="center">
|
||||||
{fields.length > 1 && (
|
<Space>
|
||||||
<Button
|
<span>Peer {idx + 1}</span>
|
||||||
size="small"
|
{fields.length > 1 && (
|
||||||
danger
|
<Button
|
||||||
onClick={() => remove(field.name)}
|
size="small"
|
||||||
>
|
danger
|
||||||
<MinusOutlined />
|
icon={<MinusOutlined />}
|
||||||
</Button>
|
onClick={() => remove(field.name)}
|
||||||
)}
|
/>
|
||||||
</Form.Item>
|
)}
|
||||||
|
</Space>
|
||||||
|
</Divider>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[field.name, 'privateKey']}
|
name={[field.name, 'privateKey']}
|
||||||
label={
|
label={
|
||||||
|
|
@ -1118,35 +1174,9 @@ export default function InboundFormModal({
|
||||||
<Select.Option value="udp">UDP</Select.Option>
|
<Select.Option value="udp">UDP</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.List name={['settings', 'portMap']}>
|
<Form.Item label="Port map" name={['settings', 'portMap']}>
|
||||||
{(fields, { add, remove }) => (
|
<HeaderMapEditor mode="v1" />
|
||||||
<>
|
</Form.Item>
|
||||||
<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
|
<Form.Item
|
||||||
name={['settings', 'followRedirect']}
|
name={['settings', 'followRedirect']}
|
||||||
label="Follow redirect"
|
label="Follow redirect"
|
||||||
|
|
@ -1163,7 +1193,13 @@ export default function InboundFormModal({
|
||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
<Form.Item label="Accounts">
|
<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
|
<PlusOutlined /> Add
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -1373,12 +1409,6 @@ export default function InboundFormModal({
|
||||||
>
|
>
|
||||||
<InputNumber min={2} max={2} disabled />
|
<InputNumber min={2} max={2} disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
label="Auth password"
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'auth']}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="UDP idle timeout (s)"
|
label="UDP idle timeout (s)"
|
||||||
name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
|
name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
|
||||||
|
|
@ -1400,7 +1430,7 @@ export default function InboundFormModal({
|
||||||
['streamSettings', 'hysteriaSettings', 'masquerade'],
|
['streamSettings', 'hysteriaSettings', 'masquerade'],
|
||||||
checked
|
checked
|
||||||
? {
|
? {
|
||||||
type: 'proxy', dir: '', url: '',
|
type: '', dir: '', url: '',
|
||||||
rewriteHost: false, insecure: false,
|
rewriteHost: false, insecure: false,
|
||||||
content: '', headers: {}, statusCode: 0,
|
content: '', headers: {}, statusCode: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -1426,6 +1456,7 @@ export default function InboundFormModal({
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
|
{ value: '', label: 'default (404 page)' },
|
||||||
{ value: 'proxy', label: 'proxy (reverse proxy)' },
|
{ value: 'proxy', label: 'proxy (reverse proxy)' },
|
||||||
{ value: 'file', label: 'file (serve directory)' },
|
{ value: 'file', label: 'file (serve directory)' },
|
||||||
{ value: 'string', label: 'string (fixed body)' },
|
{ value: 'string', label: 'string (fixed body)' },
|
||||||
|
|
@ -2161,6 +2192,9 @@ export default function InboundFormModal({
|
||||||
|
|
||||||
const securityTab = (
|
const securityTab = (
|
||||||
<>
|
<>
|
||||||
|
<Form.Item name={['streamSettings', 'security']} hidden noStyle>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t('pages.inbounds.securityTab')}>
|
<Form.Item label={t('pages.inbounds.securityTab')}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
noStyle
|
noStyle
|
||||||
|
|
@ -2176,6 +2210,7 @@ export default function InboundFormModal({
|
||||||
const proto = getFieldValue('protocol') ?? '';
|
const proto = getFieldValue('protocol') ?? '';
|
||||||
const tlsOk = canEnableTls({ protocol: proto, streamSettings: { network: net, security: sec } });
|
const tlsOk = canEnableTls({ protocol: proto, streamSettings: { network: net, security: sec } });
|
||||||
const realityOk = canEnableReality({ protocol: proto, streamSettings: { network: net, security: sec } });
|
const realityOk = canEnableReality({ protocol: proto, streamSettings: { network: net, security: sec } });
|
||||||
|
const tlsOnly = proto === Protocols.HYSTERIA;
|
||||||
return (
|
return (
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
value={sec}
|
value={sec}
|
||||||
|
|
@ -2183,7 +2218,7 @@ export default function InboundFormModal({
|
||||||
disabled={!tlsOk}
|
disabled={!tlsOk}
|
||||||
onChange={(e) => onSecurityChange(e.target.value)}
|
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>
|
<Radio.Button value="tls">tls</Radio.Button>
|
||||||
{realityOk && <Radio.Button value="reality">reality</Radio.Button>}
|
{realityOk && <Radio.Button value="reality">reality</Radio.Button>}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|
|
||||||
|
|
@ -149,16 +149,7 @@ function newStreamSlice(network: string): Record<string, unknown> {
|
||||||
hysteriaSettings: {
|
hysteriaSettings: {
|
||||||
version: 2,
|
version: 2,
|
||||||
auth: '',
|
auth: '',
|
||||||
congestion: '',
|
udpIdleTimeout: 60,
|
||||||
up: '0',
|
|
||||||
down: '0',
|
|
||||||
initStreamReceiveWindow: 8388608,
|
|
||||||
maxStreamReceiveWindow: 8388608,
|
|
||||||
initConnectionReceiveWindow: 20971520,
|
|
||||||
maxConnectionReceiveWindow: 20971520,
|
|
||||||
maxIdleTimeout: 30,
|
|
||||||
keepAlivePeriod: 2,
|
|
||||||
disablePathMTUDiscovery: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
|
|
@ -1709,113 +1700,11 @@ export default function OutboundFormModal({
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Congestion"
|
label="UDP idle timeout (s)"
|
||||||
name={['streamSettings', 'hysteriaSettings', 'congestion']}
|
name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
|
||||||
>
|
>
|
||||||
<Select
|
<InputNumber min={1} style={{ width: '100%' }} />
|
||||||
options={[
|
|
||||||
{ value: '', label: 'BBR (auto)' },
|
|
||||||
{ value: 'brutal', label: 'Brutal' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</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',
|
'shadowsocks',
|
||||||
'wireguard',
|
'wireguard',
|
||||||
'hysteria',
|
'hysteria',
|
||||||
'hysteria2',
|
|
||||||
'http',
|
'http',
|
||||||
'mixed',
|
'mixed',
|
||||||
'tunnel',
|
'tunnel',
|
||||||
|
'tun',
|
||||||
]);
|
]);
|
||||||
export type Protocol = z.infer<typeof ProtocolSchema>;
|
export type Protocol = z.infer<typeof ProtocolSchema>;
|
||||||
|
|
||||||
|
|
@ -27,7 +27,6 @@ export const Protocols = Object.freeze({
|
||||||
SHADOWSOCKS: 'shadowsocks',
|
SHADOWSOCKS: 'shadowsocks',
|
||||||
WIREGUARD: 'wireguard',
|
WIREGUARD: 'wireguard',
|
||||||
HYSTERIA: 'hysteria',
|
HYSTERIA: 'hysteria',
|
||||||
HYSTERIA2: 'hysteria2',
|
|
||||||
HTTP: 'http',
|
HTTP: 'http',
|
||||||
MIXED: 'mixed',
|
MIXED: 'mixed',
|
||||||
TUNNEL: 'tunnel',
|
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 { z } from 'zod';
|
||||||
|
|
||||||
import { HttpInboundSettingsSchema } from './http';
|
import { HttpInboundSettingsSchema } from './http';
|
||||||
import { Hysteria2InboundSettingsSchema } from './hysteria2';
|
|
||||||
import { HysteriaInboundSettingsSchema } from './hysteria';
|
import { HysteriaInboundSettingsSchema } from './hysteria';
|
||||||
import { MixedInboundSettingsSchema } from './mixed';
|
import { MixedInboundSettingsSchema } from './mixed';
|
||||||
import { ShadowsocksInboundSettingsSchema } from './shadowsocks';
|
import { ShadowsocksInboundSettingsSchema } from './shadowsocks';
|
||||||
import { TrojanInboundSettingsSchema } from './trojan';
|
import { TrojanInboundSettingsSchema } from './trojan';
|
||||||
|
import { TunInboundSettingsSchema } from './tun';
|
||||||
import { TunnelInboundSettingsSchema } from './tunnel';
|
import { TunnelInboundSettingsSchema } from './tunnel';
|
||||||
import { VlessInboundSettingsSchema } from './vless';
|
import { VlessInboundSettingsSchema } from './vless';
|
||||||
import { VmessInboundSettingsSchema } from './vmess';
|
import { VmessInboundSettingsSchema } from './vmess';
|
||||||
|
|
@ -13,10 +13,10 @@ import { WireguardInboundSettingsSchema } from './wireguard';
|
||||||
|
|
||||||
export * from './http';
|
export * from './http';
|
||||||
export * from './hysteria';
|
export * from './hysteria';
|
||||||
export * from './hysteria2';
|
|
||||||
export * from './mixed';
|
export * from './mixed';
|
||||||
export * from './shadowsocks';
|
export * from './shadowsocks';
|
||||||
export * from './trojan';
|
export * from './trojan';
|
||||||
|
export * from './tun';
|
||||||
export * from './tunnel';
|
export * from './tunnel';
|
||||||
export * from './vless';
|
export * from './vless';
|
||||||
export * from './vmess';
|
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('shadowsocks'), settings: ShadowsocksInboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardInboundSettingsSchema }),
|
z.object({ protocol: z.literal('wireguard'), settings: WireguardInboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaInboundSettingsSchema }),
|
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('http'), settings: HttpInboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('mixed'), settings: MixedInboundSettingsSchema }),
|
z.object({ protocol: z.literal('mixed'), settings: MixedInboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('tunnel'), settings: TunnelInboundSettingsSchema }),
|
z.object({ protocol: z.literal('tunnel'), settings: TunnelInboundSettingsSchema }),
|
||||||
|
z.object({ protocol: z.literal('tun'), settings: TunInboundSettingsSchema }),
|
||||||
]);
|
]);
|
||||||
export type InboundSettings = z.infer<typeof InboundSettingsSchema>;
|
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 { DNSOutboundSettingsSchema } from './dns';
|
||||||
import { FreedomOutboundSettingsSchema } from './freedom';
|
import { FreedomOutboundSettingsSchema } from './freedom';
|
||||||
import { HttpOutboundSettingsSchema } from './http';
|
import { HttpOutboundSettingsSchema } from './http';
|
||||||
import { Hysteria2OutboundSettingsSchema } from './hysteria2';
|
|
||||||
import { HysteriaOutboundSettingsSchema } from './hysteria';
|
import { HysteriaOutboundSettingsSchema } from './hysteria';
|
||||||
import { LoopbackOutboundSettingsSchema } from './loopback';
|
import { LoopbackOutboundSettingsSchema } from './loopback';
|
||||||
import { ShadowsocksOutboundSettingsSchema } from './shadowsocks';
|
import { ShadowsocksOutboundSettingsSchema } from './shadowsocks';
|
||||||
|
|
@ -19,7 +18,6 @@ export * from './dns';
|
||||||
export * from './freedom';
|
export * from './freedom';
|
||||||
export * from './http';
|
export * from './http';
|
||||||
export * from './hysteria';
|
export * from './hysteria';
|
||||||
export * from './hysteria2';
|
|
||||||
export * from './loopback';
|
export * from './loopback';
|
||||||
export * from './shadowsocks';
|
export * from './shadowsocks';
|
||||||
export * from './socks';
|
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('shadowsocks'), settings: ShadowsocksOutboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardOutboundSettingsSchema }),
|
z.object({ protocol: z.literal('wireguard'), settings: WireguardOutboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaOutboundSettingsSchema }),
|
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('http'), settings: HttpOutboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('socks'), settings: SocksOutboundSettingsSchema }),
|
z.object({ protocol: z.literal('socks'), settings: SocksOutboundSettingsSchema }),
|
||||||
z.object({ protocol: z.literal('freedom'), settings: FreedomOutboundSettingsSchema }),
|
z.object({ protocol: z.literal('freedom'), settings: FreedomOutboundSettingsSchema }),
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,17 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Hysteria stream transport — the hysteria-specific knobs that ride
|
// Hysteria stream transport. Per Xray docs (transports/hysteria.html), the
|
||||||
// alongside the connect target on outbound (and the inbound side too,
|
// Xray implementation of Hysteria2's underlying QUIC transport keeps only
|
||||||
// where the listening peer needs matching auth / congestion / obfs).
|
// the essentials — version, auth, udpIdleTimeout, and masquerade. The
|
||||||
// Wire shape mirrors xray-core's HysteriaConfig, with udphop nested
|
// extended bandwidth/window/udphop knobs that earlier hysteria builds
|
||||||
// when port-hopping is on and omitted otherwise.
|
// exposed are not part of this transport's wire shape.
|
||||||
|
|
||||||
export const HysteriaUdphopSchema = z.object({
|
// Inbound masquerade — Xray's hysteria inbound can disguise itself as an
|
||||||
port: z.string().default(''),
|
// HTTP/3 server. `type` is the empty string by default (serves the default
|
||||||
intervalMin: z.number().int().min(1).default(30),
|
// 404 page), and per-type config keys are only honored when their type is
|
||||||
intervalMax: z.number().int().min(1).default(30),
|
// active.
|
||||||
});
|
|
||||||
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.
|
|
||||||
export const HysteriaMasqueradeSchema = z.object({
|
export const HysteriaMasqueradeSchema = z.object({
|
||||||
type: z.enum(['proxy', 'file', 'string']).default('proxy'),
|
type: z.enum(['', 'proxy', 'file', 'string']).default(''),
|
||||||
dir: z.string().default(''),
|
dir: z.string().default(''),
|
||||||
url: z.string().default(''),
|
url: z.string().default(''),
|
||||||
rewriteHost: z.boolean().default(false),
|
rewriteHost: z.boolean().default(false),
|
||||||
|
|
@ -35,30 +23,9 @@ export const HysteriaMasqueradeSchema = z.object({
|
||||||
export type HysteriaMasquerade = z.infer<typeof HysteriaMasqueradeSchema>;
|
export type HysteriaMasquerade = z.infer<typeof HysteriaMasqueradeSchema>;
|
||||||
|
|
||||||
export const HysteriaStreamSettingsSchema = z.object({
|
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),
|
version: z.literal(2).default(2),
|
||||||
auth: z.string().default(''),
|
auth: z.string().default(''),
|
||||||
congestion: HysteriaCongestionSchema.default(''),
|
udpIdleTimeout: z.number().int().min(1).default(60),
|
||||||
// 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(),
|
|
||||||
masquerade: HysteriaMasqueradeSchema.optional(),
|
masquerade: HysteriaMasqueradeSchema.optional(),
|
||||||
});
|
});
|
||||||
export type HysteriaStreamSettings = z.infer<typeof HysteriaStreamSettingsSchema>;
|
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`] = `
|
exports[`createDefault*InboundSettings factories > mixed 1`] = `
|
||||||
{
|
{
|
||||||
"accounts": [],
|
"accounts": [],
|
||||||
|
|
@ -74,7 +67,16 @@ exports[`createDefault*InboundSettings factories > wireguard 1`] = `
|
||||||
{
|
{
|
||||||
"mtu": 1420,
|
"mtu": 1420,
|
||||||
"noKernelTun": false,
|
"noKernelTun": false,
|
||||||
"peers": [],
|
"peers": [
|
||||||
|
{
|
||||||
|
"allowedIPs": [
|
||||||
|
"10.0.0.2/32",
|
||||||
|
],
|
||||||
|
"keepAlive": 0,
|
||||||
|
"privateKey": "cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==",
|
||||||
|
"publicKey": "RNa/H++60PStnhoiiU/vIuwFimZUBuIkLkbrmEoDz34=",
|
||||||
|
},
|
||||||
|
],
|
||||||
"secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
|
"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`] = `
|
exports[`protocol capability predicates > mixed-basic :: grpc/none 1`] = `
|
||||||
{
|
{
|
||||||
"canEnableReality": false,
|
"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`] = `
|
exports[`InboundSettingsSchema fixtures > parses mixed-basic byte-stably 1`] = `
|
||||||
{
|
{
|
||||||
"protocol": "mixed",
|
"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 {
|
import {
|
||||||
createDefaultHttpInboundSettings,
|
createDefaultHttpInboundSettings,
|
||||||
createDefaultHysteria2InboundSettings,
|
|
||||||
createDefaultHysteriaClient,
|
createDefaultHysteriaClient,
|
||||||
createDefaultHysteriaInboundSettings,
|
createDefaultHysteriaInboundSettings,
|
||||||
createDefaultMixedInboundSettings,
|
createDefaultMixedInboundSettings,
|
||||||
|
|
@ -18,7 +17,6 @@ import {
|
||||||
createDefaultWireguardInboundSettings,
|
createDefaultWireguardInboundSettings,
|
||||||
} from '@/lib/xray/inbound-defaults';
|
} from '@/lib/xray/inbound-defaults';
|
||||||
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
|
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
|
||||||
import { Hysteria2InboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria2';
|
|
||||||
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
|
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
|
||||||
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
|
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
|
||||||
import { ShadowsocksClientSchema, ShadowsocksInboundSettingsSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
import { ShadowsocksClientSchema, ShadowsocksInboundSettingsSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
||||||
|
|
@ -112,12 +110,6 @@ describe('createDefault*InboundSettings factories', () => {
|
||||||
expect(HysteriaInboundSettingsSchema.parse(s)).toEqual(s);
|
expect(HysteriaInboundSettingsSchema.parse(s)).toEqual(s);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hysteria2', () => {
|
|
||||||
const s = createDefaultHysteria2InboundSettings();
|
|
||||||
expect(s).toMatchSnapshot();
|
|
||||||
expect(Hysteria2InboundSettingsSchema.parse(s)).toEqual(s);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('http', () => {
|
it('http', () => {
|
||||||
const s = createDefaultHttpInboundSettings();
|
const s = createDefaultHttpInboundSettings();
|
||||||
expect(s).toMatchSnapshot();
|
expect(s).toMatchSnapshot();
|
||||||
|
|
@ -139,6 +131,7 @@ describe('createDefault*InboundSettings factories', () => {
|
||||||
it('wireguard', () => {
|
it('wireguard', () => {
|
||||||
const s = createDefaultWireguardInboundSettings({
|
const s = createDefaultWireguardInboundSettings({
|
||||||
secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
||||||
|
peerPrivateKey: 'cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==',
|
||||||
});
|
});
|
||||||
expect(s).toMatchSnapshot();
|
expect(s).toMatchSnapshot();
|
||||||
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
|
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
|
||||||
|
|
|
||||||
|
|
@ -213,12 +213,6 @@ describe('genInboundLinks orchestrator', () => {
|
||||||
.sort(([a], [b]) => a.localeCompare(b));
|
.sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
|
||||||
for (const [name, raw] of fixtures) {
|
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`, () => {
|
it(`${name}: byte-stable`, () => {
|
||||||
const typed = InboundSchema.parse(raw);
|
const typed = InboundSchema.parse(raw);
|
||||||
const block = genInboundLinks({
|
const block = genInboundLinks({
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
createDefaultDNSOutboundSettings,
|
createDefaultDNSOutboundSettings,
|
||||||
createDefaultFreedomOutboundSettings,
|
createDefaultFreedomOutboundSettings,
|
||||||
createDefaultHttpOutboundSettings,
|
createDefaultHttpOutboundSettings,
|
||||||
createDefaultHysteria2OutboundSettings,
|
|
||||||
createDefaultHysteriaOutboundSettings,
|
createDefaultHysteriaOutboundSettings,
|
||||||
createDefaultLoopbackOutboundSettings,
|
createDefaultLoopbackOutboundSettings,
|
||||||
createDefaultShadowsocksOutboundSettings,
|
createDefaultShadowsocksOutboundSettings,
|
||||||
|
|
@ -21,7 +20,6 @@ import {
|
||||||
DNSOutboundSettingsSchema,
|
DNSOutboundSettingsSchema,
|
||||||
FreedomOutboundSettingsSchema,
|
FreedomOutboundSettingsSchema,
|
||||||
HttpOutboundSettingsSchema,
|
HttpOutboundSettingsSchema,
|
||||||
Hysteria2OutboundSettingsSchema,
|
|
||||||
HysteriaOutboundSettingsSchema,
|
HysteriaOutboundSettingsSchema,
|
||||||
LoopbackOutboundSettingsSchema,
|
LoopbackOutboundSettingsSchema,
|
||||||
ShadowsocksOutboundSettingsSchema,
|
ShadowsocksOutboundSettingsSchema,
|
||||||
|
|
@ -132,12 +130,6 @@ describe('outbound default factories: shape snapshots', () => {
|
||||||
address: '', port: 443, version: 2,
|
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', () => {
|
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;
|
def.address = SAMPLE_ADDRESS;
|
||||||
expect(HysteriaOutboundSettingsSchema.safeParse(def).success).toBe(true);
|
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', () => {
|
describe('createDefaultOutboundSettings dispatcher', () => {
|
||||||
const PROTOCOLS = [
|
const PROTOCOLS = [
|
||||||
'freedom', 'blackhole', 'dns', 'vmess', 'vless', 'trojan', 'shadowsocks',
|
'freedom', 'blackhole', 'dns', 'vmess', 'vless', 'trojan', 'shadowsocks',
|
||||||
'socks', 'http', 'wireguard', 'hysteria', 'hysteria2', 'loopback',
|
'socks', 'http', 'wireguard', 'hysteria', 'loopback',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const protocol of PROTOCOLS) {
|
for (const protocol of PROTOCOLS) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue