mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +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:
parent
7442486a58
commit
19204f9e04
3 changed files with 195 additions and 2 deletions
|
|
@ -99,6 +99,11 @@ const NETWORK_OPTIONS: { value: string; label: string }[] = [
|
||||||
{ value: 'xhttp', label: 'XHTTP' },
|
{ 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
|
// Per-network bootstrap. Mirrors the legacy class constructors so the
|
||||||
// initial state for each transport matches what xray-core expects.
|
// initial state for each transport matches what xray-core expects.
|
||||||
function newStreamSlice(network: string): Record<string, unknown> {
|
function newStreamSlice(network: string): Record<string, unknown> {
|
||||||
|
|
@ -136,6 +141,24 @@ function newStreamSlice(network: string): Record<string, unknown> {
|
||||||
xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
|
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:
|
default:
|
||||||
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
|
return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
|
||||||
}
|
}
|
||||||
|
|
@ -1053,7 +1076,11 @@ export default function OutboundFormModal({
|
||||||
<Select
|
<Select
|
||||||
value={network}
|
value={network}
|
||||||
onChange={onNetworkChange}
|
onChange={onNetworkChange}
|
||||||
options={NETWORK_OPTIONS}
|
options={
|
||||||
|
protocol === 'hysteria'
|
||||||
|
? [...NETWORK_OPTIONS, HYSTERIA_NETWORK_OPTION]
|
||||||
|
: NETWORK_OPTIONS
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|
@ -1286,6 +1313,125 @@ export default function OutboundFormModal({
|
||||||
</div>
|
</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 { FinalMaskStreamSettingsSchema } from './finalmask';
|
||||||
import { GrpcStreamSettingsSchema } from './grpc';
|
import { GrpcStreamSettingsSchema } from './grpc';
|
||||||
import { HttpUpgradeStreamSettingsSchema } from './httpupgrade';
|
import { HttpUpgradeStreamSettingsSchema } from './httpupgrade';
|
||||||
|
import { HysteriaStreamSettingsSchema } from './hysteria';
|
||||||
import { KcpStreamSettingsSchema } from './kcp';
|
import { KcpStreamSettingsSchema } from './kcp';
|
||||||
import { SockoptStreamSettingsSchema } from './sockopt';
|
import { SockoptStreamSettingsSchema } from './sockopt';
|
||||||
import { TcpStreamSettingsSchema } from './tcp';
|
import { TcpStreamSettingsSchema } from './tcp';
|
||||||
|
|
@ -14,13 +15,16 @@ export * from './external-proxy';
|
||||||
export * from './finalmask';
|
export * from './finalmask';
|
||||||
export * from './grpc';
|
export * from './grpc';
|
||||||
export * from './httpupgrade';
|
export * from './httpupgrade';
|
||||||
|
export * from './hysteria';
|
||||||
export * from './kcp';
|
export * from './kcp';
|
||||||
export * from './sockopt';
|
export * from './sockopt';
|
||||||
export * from './tcp';
|
export * from './tcp';
|
||||||
export * from './ws';
|
export * from './ws';
|
||||||
export * from './xhttp';
|
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>;
|
export type Network = z.infer<typeof NetworkSchema>;
|
||||||
|
|
||||||
// Tagged-wrapper DU on `network`. The wire shape uses an asymmetric per-
|
// 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
|
// `settings` object — same pattern Xray ships and the panel's StreamSettings
|
||||||
// class flattens via toJson. Each branch carries only the matching key so
|
// class flattens via toJson. Each branch carries only the matching key so
|
||||||
// fixtures round-trip byte-identical.
|
// 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', [
|
export const NetworkSettingsSchema = z.discriminatedUnion('network', [
|
||||||
z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
|
z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
|
||||||
z.object({ network: z.literal('kcp'), kcpSettings: KcpStreamSettingsSchema }),
|
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('grpc'), grpcSettings: GrpcStreamSettingsSchema }),
|
||||||
z.object({ network: z.literal('httpupgrade'), httpupgradeSettings: HttpUpgradeStreamSettingsSchema }),
|
z.object({ network: z.literal('httpupgrade'), httpupgradeSettings: HttpUpgradeStreamSettingsSchema }),
|
||||||
z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
|
z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
|
||||||
|
z.object({ network: z.literal('hysteria'), hysteriaSettings: HysteriaStreamSettingsSchema }),
|
||||||
]);
|
]);
|
||||||
export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
|
export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue