Files
3x-ui/frontend/src/lib/xray/forms/fields/FinalMaskField.tsx
T
MHSanaei ec0a57fc4f fix(finalmask): hop client UDP through the udphop mask
xray-core 26.9.9 dropped finalmask.quicParams.udpHop for the client-only
"udphop" UDP mask {mode, interval, remoteIPs, remotePorts}. The panel kept
writing the old key, so hysteria/xhttp outbounds and JSON-subscription
clients silently stopped hopping ports.

An inbound keeps quicParams.udpHop: it is panel metadata advertised as the
link's mport, and a udphop mask on a listener fails with "udphop: client
only". The conversion (maskcompat.UpgradeLegacyUDPHop, intervalRemote mode)
runs only where a client config is produced:

- UDPHopClientMaskFix seeder over hosts, sub-JSON finalmask, cached
  outbound-subscription outbounds and template outbounds (never inbounds);
- GetXrayConfig heals template outbounds at build time;
- the JSON subscription upgrades each generated client stream;
- the Go and frontend link importers upgrade a legacy fm= param.

The finalmask editor gains a side prop: the client side lifts the dead key
on mount and offers a udphop mask editor (also for xhttp's HTTP/3 dial);
the inbound side keeps the UDP Hop switch, labelled as advertised.
2026-10-11 11:20:34 +02:00

65 lines
1.6 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { Form } from 'antd';
import { FinalMaskForm } from '@/lib/xray/forms/transport';
import type { FinalMaskStreamSettings } from '@/schemas/protocols/stream/finalmask';
interface FinalMaskFieldProps {
value?: FinalMaskStreamSettings;
onChange?: (next: FinalMaskStreamSettings) => void;
network: string;
protocol: string;
showAll?: boolean;
side?: 'server' | 'client';
}
const EMPTY: FinalMaskStreamSettings = { tcp: [], udp: [] };
export default function FinalMaskField({
value,
onChange,
network,
protocol,
showAll,
side,
}: FinalMaskFieldProps) {
const [form] = Form.useForm();
const [initial] = useState(() => value ?? EMPTY);
const onChangeRef = useRef(onChange);
useEffect(() => {
onChangeRef.current = onChange;
});
const lastEmitted = useRef(JSON.stringify(initial));
const finalmask = Form.useWatch('finalmask', form) as FinalMaskStreamSettings | undefined;
useEffect(() => {
if (finalmask === undefined) return;
const serialized = JSON.stringify(finalmask);
if (serialized === lastEmitted.current) return;
lastEmitted.current = serialized;
onChangeRef.current?.(finalmask);
}, [finalmask]);
return (
<Form
form={form}
component={false}
colon={false}
labelCol={{ sm: { span: 8 } }}
wrapperCol={{ sm: { span: 14 } }}
labelWrap
initialValues={{ finalmask: initial }}
>
<FinalMaskForm
name="finalmask"
network={network}
protocol={protocol}
form={form}
showAll={showAll}
side={side}
/>
</Form>
);
}