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)) {
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 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' && (
|
||||
<>
|
||||
<Form.Item
|
||||
|
|
|
|||
|
|
@ -17,7 +17,26 @@ export type HysteriaUdphop = z.infer<typeof HysteriaUdphopSchema>;
|
|||
// 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({
|
||||
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({
|
||||
// Outbound-side fields. The version field is shared with inbound and
|
||||
// typically locked to 2.
|
||||
version: z.literal(2).default(2),
|
||||
auth: z.string().default(''),
|
||||
congestion: HysteriaCongestionSchema.default(''),
|
||||
|
|
@ -34,5 +53,12 @@ export const HysteriaStreamSettingsSchema = z.object({
|
|||
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(),
|
||||
});
|
||||
export type HysteriaStreamSettings = z.infer<typeof HysteriaStreamSettingsSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue