mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-29 16:39:35 +00:00
feat(frontend): Hysteria stream sub-form (schema branch + outbound UI)
Add the 7th branch to NetworkSettingsSchema for Hysteria transport.
schemas/protocols/stream/hysteria.ts:
- HysteriaStreamSettingsSchema covers the full wire shape: version=2,
auth, congestion (''|'brutal'), up/down bandwidth strings, optional
udphop sub-object for port-hopping, receive-window tuning fields,
maxIdleTimeout, keepAlivePeriod, disablePathMTUDiscovery.
schemas/protocols/stream/index.ts:
- NetworkSchema gains 'hysteria'.
- NetworkSettingsSchema gains the 7th branch
{ network: 'hysteria', hysteriaSettings: HysteriaStreamSettingsSchema }.
OutboundFormModal.tsx:
- NETWORK_OPTIONS keeps the 6 standard transports for non-hysteria
protocols; when protocol === 'hysteria', a 7th option is appended
(matches the legacy [...NETWORKS, 'hysteria'] gate).
- newStreamSlice handles the 'hysteria' case with sensible defaults
matching the legacy HysteriaStreamSettings constructor.
- New sub-form when network === 'hysteria': 8 common fields (auth,
congestion, up, down, udphop Switch + 3 nested fields when on,
maxIdleTimeout, keepAlivePeriod, disablePathMTUDiscovery).
- Receive-window tuning fields are still edit-via-JSON (rarely
touched + would clutter the form).
This commit is contained in:
@@ -99,6 +99,11 @@ const NETWORK_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: 'xhttp', label: 'XHTTP' },
|
||||
];
|
||||
|
||||
// Hysteria appends an extra `hysteria` network branch to the selector
|
||||
// — only when the parent protocol is hysteria. Wire-side this matches
|
||||
// the legacy modal's `isHysteria ? [...NETWORKS, 'hysteria'] : NETWORKS`.
|
||||
const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' };
|
||||
|
||||
// Per-network bootstrap. Mirrors the legacy class constructors so the
|
||||
// initial state for each transport matches what xray-core expects.
|
||||
function newStreamSlice(network: string): Record<string, unknown> {
|
||||
@@ -136,6 +141,24 @@ function newStreamSlice(network: string): Record<string, unknown> {
|
||||
xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
|
||||
},
|
||||
};
|
||||
case 'hysteria':
|
||||
return {
|
||||
network: 'hysteria',
|
||||
hysteriaSettings: {
|
||||
version: 2,
|
||||
auth: '',
|
||||
congestion: '',
|
||||
up: '0',
|
||||
down: '0',
|
||||
initStreamReceiveWindow: 8388608,
|
||||
maxStreamReceiveWindow: 8388608,
|
||||
initConnectionReceiveWindow: 20971520,
|
||||
maxConnectionReceiveWindow: 20971520,
|
||||
maxIdleTimeout: 30,
|
||||
keepAlivePeriod: 2,
|
||||
disablePathMTUDiscovery: false,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
|
||||
}
|
||||
@@ -1053,7 +1076,11 @@ export default function OutboundFormModal({
|
||||
<Select
|
||||
value={network}
|
||||
onChange={onNetworkChange}
|
||||
options={NETWORK_OPTIONS}
|
||||
options={
|
||||
protocol === 'hysteria'
|
||||
? [...NETWORK_OPTIONS, HYSTERIA_NETWORK_OPTION]
|
||||
: NETWORK_OPTIONS
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1286,6 +1313,125 @@ export default function OutboundFormModal({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{network === 'hysteria' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Auth password"
|
||||
name={['streamSettings', 'hysteriaSettings', 'auth']}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Congestion"
|
||||
name={['streamSettings', 'hysteriaSettings', 'congestion']}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'BBR (auto)' },
|
||||
{ value: 'brutal', label: 'Brutal' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Upload"
|
||||
name={['streamSettings', 'hysteriaSettings', 'up']}
|
||||
>
|
||||
<Input placeholder="100 mbps" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Download"
|
||||
name={['streamSettings', 'hysteriaSettings', 'down']}
|
||||
>
|
||||
<Input placeholder="100 mbps" />
|
||||
</Form.Item>
|
||||
<Form.Item label="UDP hop">
|
||||
<Form.Item
|
||||
shouldUpdate
|
||||
noStyle
|
||||
>
|
||||
{() => {
|
||||
const udphop = form.getFieldValue([
|
||||
'streamSettings', 'hysteriaSettings', 'udphop',
|
||||
]) as { port?: string } | undefined;
|
||||
return (
|
||||
<Switch
|
||||
checked={!!udphop}
|
||||
onChange={(checked) =>
|
||||
form.setFieldValue(
|
||||
['streamSettings', 'hysteriaSettings', 'udphop'],
|
||||
checked
|
||||
? { port: '', intervalMin: 30, intervalMax: 30 }
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const udphop = form.getFieldValue([
|
||||
'streamSettings', 'hysteriaSettings', 'udphop',
|
||||
]) as { port?: string } | undefined;
|
||||
if (!udphop) return null;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="UDP hop port"
|
||||
name={['streamSettings', 'hysteriaSettings', 'udphop', 'port']}
|
||||
>
|
||||
<Input placeholder="1145-1919" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="UDP hop interval min (s)"
|
||||
name={[
|
||||
'streamSettings', 'hysteriaSettings',
|
||||
'udphop', 'intervalMin',
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="UDP hop interval max (s)"
|
||||
name={[
|
||||
'streamSettings', 'hysteriaSettings',
|
||||
'udphop', 'intervalMax',
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max idle (s)"
|
||||
name={['streamSettings', 'hysteriaSettings', 'maxIdleTimeout']}
|
||||
>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Keep alive (s)"
|
||||
name={['streamSettings', 'hysteriaSettings', 'keepAlivePeriod']}
|
||||
>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Disable Path MTU"
|
||||
name={['streamSettings', 'hysteriaSettings', 'disablePathMTUDiscovery']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
38
frontend/src/schemas/protocols/stream/hysteria.ts
Normal file
38
frontend/src/schemas/protocols/stream/hysteria.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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.
|
||||
|
||||
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')]);
|
||||
|
||||
export const HysteriaStreamSettingsSchema = z.object({
|
||||
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),
|
||||
});
|
||||
export type HysteriaStreamSettings = z.infer<typeof HysteriaStreamSettingsSchema>;
|
||||
@@ -4,6 +4,7 @@ import { ExternalProxyEntrySchema } from './external-proxy';
|
||||
import { FinalMaskStreamSettingsSchema } from './finalmask';
|
||||
import { GrpcStreamSettingsSchema } from './grpc';
|
||||
import { HttpUpgradeStreamSettingsSchema } from './httpupgrade';
|
||||
import { HysteriaStreamSettingsSchema } from './hysteria';
|
||||
import { KcpStreamSettingsSchema } from './kcp';
|
||||
import { SockoptStreamSettingsSchema } from './sockopt';
|
||||
import { TcpStreamSettingsSchema } from './tcp';
|
||||
@@ -14,13 +15,16 @@ export * from './external-proxy';
|
||||
export * from './finalmask';
|
||||
export * from './grpc';
|
||||
export * from './httpupgrade';
|
||||
export * from './hysteria';
|
||||
export * from './kcp';
|
||||
export * from './sockopt';
|
||||
export * from './tcp';
|
||||
export * from './ws';
|
||||
export * from './xhttp';
|
||||
|
||||
export const NetworkSchema = z.enum(['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']);
|
||||
export const NetworkSchema = z.enum([
|
||||
'tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp', 'hysteria',
|
||||
]);
|
||||
export type Network = z.infer<typeof NetworkSchema>;
|
||||
|
||||
// Tagged-wrapper DU on `network`. The wire shape uses an asymmetric per-
|
||||
@@ -28,6 +32,10 @@ export type Network = z.infer<typeof NetworkSchema>;
|
||||
// `settings` object — same pattern Xray ships and the panel's StreamSettings
|
||||
// class flattens via toJson. Each branch carries only the matching key so
|
||||
// fixtures round-trip byte-identical.
|
||||
//
|
||||
// `hysteria` is only valid when the parent protocol is hysteria — the
|
||||
// network selector hides it for other protocols. xray-core enforces
|
||||
// the constraint server-side too.
|
||||
export const NetworkSettingsSchema = z.discriminatedUnion('network', [
|
||||
z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('kcp'), kcpSettings: KcpStreamSettingsSchema }),
|
||||
@@ -35,6 +43,7 @@ export const NetworkSettingsSchema = z.discriminatedUnion('network', [
|
||||
z.object({ network: z.literal('grpc'), grpcSettings: GrpcStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('httpupgrade'), httpupgradeSettings: HttpUpgradeStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('hysteria'), hysteriaSettings: HysteriaStreamSettingsSchema }),
|
||||
]);
|
||||
export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user