import { useTranslation } from 'react-i18next'; import { Alert, Button, Form, Input, InputNumber, Segmented, Select, Space, Switch } from 'antd'; import { Address_Port_Strategy, DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION, } from '@/schemas/primitives'; import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt'; // Transport key that carries its own acceptProxyProtocol field (mirrored // alongside the sockopt-level one so the PROXY preset never silently no-ops). const TRANSPORT_PROXY_FIELD: Record = { tcp: 'tcpSettings', ws: 'wsSettings', httpupgrade: 'httpupgradeSettings', }; // Transports on which xray-core honors sockopt.trustedXForwardedFor. const TRUSTED_HEADER_NETWORKS = ['ws', 'httpupgrade', 'xhttp']; type RealClientIpPreset = 'off' | 'cloudflare' | 'proxy'; export default function SockoptForm({ toggleSockopt, network, }: { toggleSockopt: (on: boolean) => void; network: string; }) { const { t } = useTranslation(); // Presets write the same sockopt fields the user could set by hand below, // picking the mechanism xray-core actually honors for the chosen transport: // CF-Connecting-IP via trustedXForwardedFor (ws/httpupgrade/xhttp) or the // PROXY-protocol header via acceptProxyProtocol (every transport but mKCP). const applyRealClientIpPreset = ( preset: RealClientIpPreset, getFieldValue: (name: (string | number)[]) => unknown, setFieldValue: (name: (string | number)[], value: unknown) => void, ) => { const sockopt = getFieldValue(['streamSettings', 'sockopt']); const sockoptOn = !!sockopt && typeof sockopt === 'object' && Object.keys(sockopt as object).length > 0; if (preset !== 'off' && !sockoptOn) { toggleSockopt(true); } const transportField = TRANSPORT_PROXY_FIELD[network]; if (preset === 'off') { setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], []); setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], false); if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], false); return; } if (preset === 'cloudflare') { const current = getFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor']); const list = Array.isArray(current) ? [...(current as string[])] : []; if (!list.includes('CF-Connecting-IP')) list.push('CF-Connecting-IP'); setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], list); setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], false); if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], false); return; } // proxy — clear trustedXForwardedFor so a lingering header can't override the // PROXY-recovered IP (xray reads the header last on ws/httpupgrade/xhttp). setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], []); setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], true); if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], true); }; return ( { const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt; const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt; return !!a !== !!b; }} > {({ getFieldValue }) => { const sock = getFieldValue(['streamSettings', 'sockopt']); const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0; return ( <> {on && ( <> { type ProxyWatch = { streamSettings?: { sockopt?: { trustedXForwardedFor?: unknown; acceptProxyProtocol?: unknown }; tcpSettings?: { acceptProxyProtocol?: unknown }; wsSettings?: { acceptProxyProtocol?: unknown }; httpupgradeSettings?: { acceptProxyProtocol?: unknown }; }; }; const pick = (v: ProxyWatch) => { const s = v.streamSettings; return JSON.stringify([ s?.sockopt?.trustedXForwardedFor, s?.sockopt?.acceptProxyProtocol, s?.tcpSettings?.acceptProxyProtocol, s?.wsSettings?.acceptProxyProtocol, s?.httpupgradeSettings?.acceptProxyProtocol, ]); }; return pick(prev as ProxyWatch) !== pick(curr as ProxyWatch); }} > {({ getFieldValue, setFieldValue }) => { const sockopt = (getFieldValue(['streamSettings', 'sockopt']) ?? {}) as Record< string, unknown >; const transportField = TRANSPORT_PROXY_FIELD[network]; const transportPP = transportField ? getFieldValue(['streamSettings', transportField, 'acceptProxyProtocol']) === true : false; const proxyOn = sockopt.acceptProxyProtocol === true || transportPP; const trusted = Array.isArray(sockopt.trustedXForwardedFor) ? (sockopt.trustedXForwardedFor as string[]) : []; const value: RealClientIpPreset = proxyOn ? 'proxy' : trusted.length > 0 ? 'cloudflare' : 'off'; const trustedMismatch = trusted.length > 0 && !TRUSTED_HEADER_NETWORKS.includes(network); const proxyMismatch = proxyOn && network === 'kcp'; return ( <> applyRealClientIpPreset(v as RealClientIpPreset, getFieldValue, setFieldValue) } options={[ { value: 'off', label: t('pages.inbounds.form.realClientIpPresetOff') }, { value: 'cloudflare', label: t('pages.inbounds.form.realClientIpPresetCloudflare') }, { value: 'proxy', label: t('pages.inbounds.form.realClientIpPresetProxyProtocol') }, ]} /> {trustedMismatch && ( )} {proxyMismatch && ( )} ); }} ({ value: c, label: c }))} /> ({ value: v, label: v }))} /> {({ getFieldValue, setFieldValue }) => { const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']); const hasHe = he != null; return ( <> { setFieldValue( ['streamSettings', 'sockopt', 'happyEyeballs'], v ? HappyEyeballsSchema.parse({}) : undefined, ); }} /> {hasHe && ( <> )} ); }} {(fields, { add, remove }) => ( <> {fields.map((field) => ( ))} )} )} ); }} ); }