mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(frontend): inbound Hysteria stream sub-form (auth + udpIdleTimeout + masquerade)
Restore the inbound side of Hysteria stream configuration that was
previously hidden — the legacy modal exposed these knobs but the
Pattern A rewrite gated them out.
schemas/protocols/stream/hysteria.ts:
- HysteriaMasqueradeSchema covers the inbound-only masquerade wire
shape: type ('proxy'|'file'|'string'), dir, url, rewriteHost,
insecure, content, headers, statusCode. The three masquerade types
cover the spectrum: reverse-proxy upstream, serve static files, or
return a fixed string body.
- HysteriaStreamSettingsSchema gains 3 inbound-side optional fields:
protocol, udpIdleTimeout, masquerade. Outbound side is untouched
(the legacy class accepted both wire shapes via the same struct).
InboundFormModal.tsx:
- New hysteria stream sub-form section in streamTab, gated by
protocol === HYSTERIA. Fields: version (disabled, locked to 2),
auth, udpIdleTimeout, masquerade Switch + nested type-Select with
three conditional sub-blocks (proxy URL+rewriteHost+insecure,
file dir, string statusCode+body+headers).
- onValuesChange cascade: switching TO hysteria seeds streamSettings
with the hysteria branch (forcing network='hysteria' + TLS); switching
AWAY from hysteria snaps back to TCP so the standard network
selector has a valid starting point.
masquerade headers use the HeaderMapEditor v1 component.
This commit is contained in:
parent
9de527b35f
commit
5c902ca298
2 changed files with 183 additions and 0 deletions
|
|
@ -510,6 +510,28 @@ export default function InboundFormModal({
|
||||||
if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
|
if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
|
||||||
form.setFieldValue('nodeId', null);
|
form.setFieldValue('nodeId', null);
|
||||||
}
|
}
|
||||||
|
// Hysteria uses its dedicated transport — force the network branch
|
||||||
|
// so the stream tab renders the hysteria sub-form, not the leftover
|
||||||
|
// tcpSettings from the previous protocol. When leaving hysteria,
|
||||||
|
// snap back to TCP so the standard network selector has a valid
|
||||||
|
// starting point.
|
||||||
|
if (next === Protocols.HYSTERIA) {
|
||||||
|
form.setFieldValue('streamSettings', {
|
||||||
|
network: 'hysteria',
|
||||||
|
security: 'tls',
|
||||||
|
hysteriaSettings: {
|
||||||
|
version: 2,
|
||||||
|
auth: '',
|
||||||
|
udpIdleTimeout: 60,
|
||||||
|
},
|
||||||
|
tlsSettings: {},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
|
||||||
|
if (current?.network === 'hysteria') {
|
||||||
|
form.setFieldValue('streamSettings', { network: 'tcp', security: 'none', tcpSettings: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1198,6 +1220,141 @@ export default function InboundFormModal({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Inbound Hysteria stream sub-form. The transport for hysteria
|
||||||
|
isn't user-selectable (always 'hysteria'), so the network
|
||||||
|
dropdown is hidden above. Fields here mirror the legacy
|
||||||
|
HysteriaStreamSettings inbound class: version is locked to 2,
|
||||||
|
auth + udpIdleTimeout are required, masquerade is an optional
|
||||||
|
sub-object that lets xray-core disguise the listener as an
|
||||||
|
HTTP server when probed. */}
|
||||||
|
{protocol === Protocols.HYSTERIA && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Version"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'version']}
|
||||||
|
>
|
||||||
|
<InputNumber min={2} max={2} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Auth password"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'auth']}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="UDP idle timeout (s)"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Masquerade">
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{() => {
|
||||||
|
const m = form.getFieldValue([
|
||||||
|
'streamSettings', 'hysteriaSettings', 'masquerade',
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={!!m}
|
||||||
|
onChange={(checked) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
['streamSettings', 'hysteriaSettings', 'masquerade'],
|
||||||
|
checked
|
||||||
|
? {
|
||||||
|
type: 'proxy', dir: '', url: '',
|
||||||
|
rewriteHost: false, insecure: false,
|
||||||
|
content: '', headers: {}, statusCode: 0,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{() => {
|
||||||
|
const m = form.getFieldValue([
|
||||||
|
'streamSettings', 'hysteriaSettings', 'masquerade',
|
||||||
|
]) as { type?: string } | undefined;
|
||||||
|
if (!m) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Type"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'type']}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'proxy', label: 'proxy (reverse proxy)' },
|
||||||
|
{ value: 'file', label: 'file (serve directory)' },
|
||||||
|
{ value: 'string', label: 'string (fixed body)' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
{m.type === 'proxy' && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Upstream URL"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'url']}
|
||||||
|
>
|
||||||
|
<Input placeholder="https://www.example.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Rewrite Host"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'rewriteHost']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Skip TLS verify"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'insecure']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{m.type === 'file' && (
|
||||||
|
<Form.Item
|
||||||
|
label="Directory"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'dir']}
|
||||||
|
>
|
||||||
|
<Input placeholder="/var/www/html" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{m.type === 'string' && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Status code"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'statusCode']}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={599} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Body"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'content']}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={{ minRows: 3 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Headers"
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'headers']}
|
||||||
|
>
|
||||||
|
<HeaderMapEditor mode="v1" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{network === 'tcp' && (
|
{network === 'tcp' && (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,26 @@ export type HysteriaUdphop = z.infer<typeof HysteriaUdphopSchema>;
|
||||||
// missing are equivalent on the wire so we accept either.
|
// missing are equivalent on the wire so we accept either.
|
||||||
export const HysteriaCongestionSchema = z.union([z.literal(''), z.literal('brutal')]);
|
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({
|
||||||
|
type: z.enum(['proxy', 'file', 'string']).default('proxy'),
|
||||||
|
dir: z.string().default(''),
|
||||||
|
url: z.string().default(''),
|
||||||
|
rewriteHost: z.boolean().default(false),
|
||||||
|
insecure: z.boolean().default(false),
|
||||||
|
content: z.string().default(''),
|
||||||
|
headers: z.record(z.string(), z.string()).default({}),
|
||||||
|
statusCode: z.number().int().min(0).default(0),
|
||||||
|
});
|
||||||
|
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(''),
|
congestion: HysteriaCongestionSchema.default(''),
|
||||||
|
|
@ -34,5 +53,12 @@ export const HysteriaStreamSettingsSchema = z.object({
|
||||||
maxIdleTimeout: z.number().int().min(1).default(30),
|
maxIdleTimeout: z.number().int().min(1).default(30),
|
||||||
keepAlivePeriod: z.number().int().min(1).default(2),
|
keepAlivePeriod: z.number().int().min(1).default(2),
|
||||||
disablePathMTUDiscovery: z.boolean().default(false),
|
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(),
|
||||||
});
|
});
|
||||||
export type HysteriaStreamSettings = z.infer<typeof HysteriaStreamSettingsSchema>;
|
export type HysteriaStreamSettings = z.infer<typeof HysteriaStreamSettingsSchema>;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue