mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-14 09:05:01 +03:00
a0f4c13dc5
* fix(sockopt): honor trustedXForwardedFor on gRPC inbounds xray-core v26.6.22 (commit 711aea4) switched the gRPC server from reading the x-real-ip gRPC metadata to resolving the client IP from X-Forwarded-For via sockopt.trustedXForwardedFor, matching ws/httpupgrade/xhttp. The panel already exposed the trustedXForwardedFor field and wire output, but the per-transport gate (TRUSTED_HEADER_NETWORKS) still omitted grpc. On a gRPC inbound this raised a false "transport does not honor this header" warning and mis-flagged the Cloudflare real-client-IP preset. Add grpc to the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(i18n): note gRPC in trustedXForwardedFor hint (all locales) Follow-up to the gRPC gate fix: the trustedXForwardedForHint tooltip across all 13 locales said the header is honored "only on WebSocket, HTTPUpgrade and XHTTP". xray-core v26.6.22 added gRPC, so list it too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
278 lines
12 KiB
TypeScript
278 lines
12 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
import { Alert, Form, InputNumber, Segmented, Select, Switch } from 'antd';
|
|
|
|
import { CustomSockoptList } from '@/components/form';
|
|
import { TCP_CONGESTION_OPTION } from '@/schemas/primitives';
|
|
|
|
// 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<string, string> = {
|
|
tcp: 'tcpSettings',
|
|
ws: 'wsSettings',
|
|
httpupgrade: 'httpupgradeSettings',
|
|
};
|
|
// Transports on which xray-core honors sockopt.trustedXForwardedFor. gRPC joined
|
|
// in v26.6.22 (xray-core 711aea4): it now reads X-Forwarded-For via this option
|
|
// instead of the old x-real-ip gRPC metadata.
|
|
const TRUSTED_HEADER_NETWORKS = ['ws', 'httpupgrade', 'xhttp', 'grpc'];
|
|
|
|
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/grpc) 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/grpc).
|
|
setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], []);
|
|
setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], true);
|
|
if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], true);
|
|
};
|
|
|
|
return (
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prev, curr) => {
|
|
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 (
|
|
<>
|
|
<Form.Item label="Sockopt">
|
|
<Switch checked={on} onChange={toggleSockopt} />
|
|
</Form.Item>
|
|
{on && (
|
|
<>
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prev, curr) => {
|
|
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 (
|
|
<>
|
|
<Form.Item
|
|
label={t('pages.inbounds.form.realClientIp')}
|
|
tooltip={t('pages.inbounds.form.realClientIpHint')}
|
|
>
|
|
<Segmented
|
|
value={value}
|
|
onChange={(v) =>
|
|
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') },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
{trustedMismatch && (
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
style={{ marginBottom: 16 }}
|
|
title={t('pages.inbounds.form.realClientIpTrustedHeaderTransportWarn')}
|
|
/>
|
|
)}
|
|
{proxyMismatch && (
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
style={{ marginBottom: 16 }}
|
|
title={t('pages.inbounds.form.realClientIpProxyProtocolTransportWarn')}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
<Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
|
|
label={t('pages.inbounds.form.tcpKeepAliveInterval')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
|
|
label={t('pages.inbounds.form.tcpKeepAliveIdle')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label={t('pages.inbounds.form.tcpMaxSeg')}>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
|
|
label={t('pages.inbounds.form.tcpUserTimeout')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
|
|
label={t('pages.inbounds.form.tcpWindowClamp')}
|
|
tooltip={t('pages.inbounds.form.tcpWindowClampHint')}
|
|
>
|
|
<InputNumber min={0} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
|
|
label={t('pages.inbounds.form.proxyProtocol')}
|
|
tooltip={t('pages.inbounds.form.proxyProtocolHint')}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'tcpFastOpen']}
|
|
label={t('pages.inbounds.form.tcpFastOpen')}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'penetrate']}
|
|
label={t('pages.inbounds.form.penetrate')}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'V6Only']}
|
|
label={t('pages.inbounds.form.v6Only')}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'tcpcongestion']}
|
|
label={t('pages.inbounds.form.tcpCongestion')}
|
|
>
|
|
<Select
|
|
style={{ width: '50%' }}
|
|
options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
|
|
<Select
|
|
style={{ width: '50%' }}
|
|
options={[
|
|
{ value: 'off', label: 'Off' },
|
|
{ value: 'redirect', label: 'Redirect' },
|
|
{ value: 'tproxy', label: 'TProxy' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
|
|
label={t('pages.inbounds.form.trustedXForwardedFor')}
|
|
tooltip={t('pages.inbounds.form.trustedXForwardedForHint')}
|
|
>
|
|
<Select
|
|
mode="tags"
|
|
style={{ width: '100%' }}
|
|
tokenSeparators={[',']}
|
|
options={[
|
|
{ value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
|
|
{ value: 'X-Real-IP', label: 'X-Real-IP' },
|
|
{ value: 'True-Client-IP', label: 'True-Client-IP' },
|
|
{ value: 'X-Client-IP', label: 'X-Client-IP' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<CustomSockoptList />
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
);
|
|
}
|