diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx
index dfc204f1..0333c19a 100644
--- a/frontend/src/pages/inbounds/InboundFormModal.tsx
+++ b/frontend/src/pages/inbounds/InboundFormModal.tsx
@@ -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({
)}
+ {/* 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 && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {() => {
+ const m = form.getFieldValue([
+ 'streamSettings', 'hysteriaSettings', 'masquerade',
+ ]);
+ return (
+
+ form.setFieldValue(
+ ['streamSettings', 'hysteriaSettings', 'masquerade'],
+ checked
+ ? {
+ type: 'proxy', dir: '', url: '',
+ rewriteHost: false, insecure: false,
+ content: '', headers: {}, statusCode: 0,
+ }
+ : undefined,
+ )
+ }
+ />
+ );
+ }}
+
+
+
+ {() => {
+ const m = form.getFieldValue([
+ 'streamSettings', 'hysteriaSettings', 'masquerade',
+ ]) as { type?: string } | undefined;
+ if (!m) return null;
+ return (
+ <>
+
+
+
+ {m.type === 'proxy' && (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {m.type === 'file' && (
+
+
+
+ )}
+ {m.type === 'string' && (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+ }}
+
+ >
+ )}
+
{network === 'tcp' && (
<>
;
// 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;
+
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;