|
|
@@ -1,5 +1,5 @@
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
-import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
|
|
+import { Alert, Button, Form, Input, InputNumber, Segmented, Select, Space, Switch } from 'antd';
|
|
|
|
|
|
import {
|
|
|
Address_Port_Strategy,
|
|
|
@@ -8,12 +8,68 @@ import {
|
|
|
} 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<string, string> = {
|
|
|
+ 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 (
|
|
|
<Form.Item
|
|
|
noStyle
|
|
|
@@ -33,6 +89,89 @@ export default function SockoptForm({
|
|
|
</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 }}
|
|
|
+ message={t('pages.inbounds.form.realClientIpTrustedHeaderTransportWarn')}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {proxyMismatch && (
|
|
|
+ <Alert
|
|
|
+ type="warning"
|
|
|
+ showIcon
|
|
|
+ style={{ marginBottom: 16 }}
|
|
|
+ message={t('pages.inbounds.form.realClientIpProxyProtocolTransportWarn')}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ </Form.Item>
|
|
|
<Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
|
|
|
<InputNumber min={0} />
|
|
|
</Form.Item>
|
|
|
@@ -67,6 +206,7 @@ export default function SockoptForm({
|
|
|
<Form.Item
|
|
|
name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
|
|
|
label={t('pages.inbounds.form.proxyProtocol')}
|
|
|
+ tooltip={t('pages.inbounds.form.proxyProtocolHint')}
|
|
|
valuePropName="checked"
|
|
|
>
|
|
|
<Switch />
|
|
|
@@ -139,6 +279,7 @@ export default function SockoptForm({
|
|
|
<Form.Item
|
|
|
name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
|
|
|
label={t('pages.inbounds.form.trustedXForwardedFor')}
|
|
|
+ tooltip={t('pages.inbounds.form.trustedXForwardedForHint')}
|
|
|
>
|
|
|
<Select
|
|
|
mode="tags"
|