From ad3d3937b056a346fad9f9243ecbc90f166a2df4 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 12:37:44 +0200 Subject: [PATCH] feat(frontend): OutboundFormModal deferred features (Vision seed / TCP host+path / WG pubKey derive) Three small wins from the post-atomic-swap deferred list: - VLESS Vision testpre + testseed: shown only when flow === 'xtls-rprx-vision' (mirrors the legacy canEnableVisionSeed gate). testseed binds to a Select mode='tags' with a normalize() that coerces strings to positive integers and drops invalid entries. - TCP HTTP camouflage host + path: when the TCP HTTP camouflage Switch is on, surface two inputs that read/write directly into streamSettings.tcpSettings.header.request.headers.Host and .path. Both fields are string[] on the wire; normalize + getValueProps translate to/from comma-joined strings in the UI (one entry per host or path the user wants camouflaged). - Wireguard pubKey auto-derive: Form.useWatch on settings.secretKey + useEffect that runs Wireguard.generateKeypair(secret).publicKey on every change and writes the result into the disabled pubKey display field. Matches the legacy modal's per-keystroke derive. --- frontend/src/pages/xray/OutboundFormModal.tsx | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/xray/OutboundFormModal.tsx b/frontend/src/pages/xray/OutboundFormModal.tsx index fc9b6dfe..fffd5e15 100644 --- a/frontend/src/pages/xray/OutboundFormModal.tsx +++ b/frontend/src/pages/xray/OutboundFormModal.tsx @@ -203,6 +203,26 @@ export default function OutboundFormModal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [streamAllowed, network]); + // Wireguard pubKey is a UI-only field derived from secretKey on every + // edit. The legacy modal did the same on every keystroke. We re-derive + // here so paste-in secret keys immediately surface the matching pub. + const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined; + useEffect(() => { + if (protocol !== 'wireguard') return; + const sk = (wgSecretKey ?? '').trim(); + if (!sk) { + form.setFieldValue(['settings', 'pubKey'], ''); + return; + } + try { + const { publicKey } = Wireguard.generateKeypair(sk); + form.setFieldValue(['settings', 'pubKey'], publicKey); + } catch { + form.setFieldValue(['settings', 'pubKey'], ''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [protocol, wgSecretKey]); + // Switching protocol resets the settings sub-object to fresh defaults // so leftover fields from the previous protocol do not bleed through. // The adapter's rawOutboundToFormValues seeds whatever the new protocol @@ -1054,12 +1074,72 @@ export default function OutboundFormModal({ form.setFieldValue( ['streamSettings', 'tcpSettings', 'header'], checked - ? { type: 'http', request: undefined, response: undefined } + ? { + type: 'http', + request: { + version: '1.1', + method: 'GET', + path: ['/'], + headers: {}, + }, + response: undefined, + } : { type: 'none' }, ) } /> + {type === 'http' && ( + <> + {/* Host is stored as a string[] on the + wire (V2 header map: { Host: [...] }). + The form-level normalize/getValueProps + translate to/from a comma-joined input + so the user types one Host:contentReference[oaicite:0]{index=0} value per + server they want camouflaged. */} + + typeof v === 'string' + ? v.split(',').map((s) => s.trim()).filter(Boolean) + : Array.isArray(v) ? v : [] + } + getValueProps={(v: unknown) => ({ + value: Array.isArray(v) ? v.join(',') : '', + })} + > + + + + typeof v === 'string' + ? v.split(',').map((s) => s.trim()).filter(Boolean) + : Array.isArray(v) ? v : ['/'] + } + getValueProps={(v: unknown) => ({ + value: Array.isArray(v) ? v.join(',') : '/', + })} + > + + + + )} ); }} @@ -1205,6 +1285,42 @@ export default function OutboundFormModal({ )} + {/* Vision seed knobs only meaningful for the exact + xtls-rprx-vision flow, on TCP+(tls|reality). The + legacy class gated this on `canEnableVisionSeed()` + — same condition encoded inline here. */} + + {() => { + const flow = + (form.getFieldValue(['settings', 'flow']) ?? '') as string; + if (!(tlsFlowAllowed && flow === 'xtls-rprx-vision')) return null; + return ( + <> + + + + + Array.isArray(v) + ? v + .map((x) => Number(x)) + .filter((n) => Number.isInteger(n) && n > 0) + : [] + } + > +