mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 14:16:07 +00:00
feat: add inbound share address strategy (#5162)
* feat: add inbound share address strategy Allow node-managed inbounds to choose whether exported share links use the node address, routable listen address, or a custom endpoint. Preserve locally configured share address fields during remote node traffic sync. Refs #5161 Refs #4891 * fix: preserve inbound share address settings Forward share address fields to remote nodes, keep existing values when older update payloads omit them, align localhost handling between frontend and subscriptions, and preserve share address settings when cloning inbounds. * fix: keep share address strategy out of subscriptions Limit the new share address strategy to direct exported share links and QR codes. Restore subscription address resolution to the existing panel-owned behavior and update the UI help text accordingly. * fix: address share address review feedback * fix: validate custom share address * fix --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -457,6 +457,8 @@ export default function InboundsPage() {
|
||||
settings: clonedSettings,
|
||||
streamSettings: streamSettingsString,
|
||||
sniffing: sniffingString,
|
||||
shareAddrStrategy: dbInbound.shareAddrStrategy,
|
||||
shareAddr: dbInbound.shareAddr,
|
||||
};
|
||||
const msg = await HttpUtil.post('/panel/api/inbounds/add', data);
|
||||
if (msg?.success) await refresh();
|
||||
|
||||
@@ -84,6 +84,8 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||
|
||||
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
||||
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
||||
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
||||
const SHARE_ADDR_HOSTNAME_RE = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
|
||||
const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
|
||||
Protocols.VLESS,
|
||||
Protocols.VMESS,
|
||||
@@ -93,6 +95,30 @@ const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
|
||||
Protocols.WIREGUARD,
|
||||
]);
|
||||
|
||||
function isValidShareAddrInput(value: string): boolean {
|
||||
const v = value.trim();
|
||||
if (v.length === 0) return true;
|
||||
if (v.includes('://') || v.startsWith('//') || /[/?#@]/.test(v)) return false;
|
||||
if (v.startsWith('[')) {
|
||||
if (!v.endsWith(']')) return false;
|
||||
try {
|
||||
new URL(`http://${v}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (v.includes(':')) {
|
||||
try {
|
||||
new URL(`http://[${v}]`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return SHARE_ADDR_HOSTNAME_RE.test(v);
|
||||
}
|
||||
|
||||
interface InboundFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -176,6 +202,7 @@ export default function InboundFormModal({
|
||||
const wListen = (Form.useWatch('listen', form) ?? '') as string;
|
||||
const isUdsListen = wListen.startsWith('/');
|
||||
const wNodeId = Form.useWatch('nodeId', form) ?? null;
|
||||
const shareAddrStrategy = Form.useWatch('shareAddrStrategy', form) ?? 'node';
|
||||
const wTag = Form.useWatch('tag', form) ?? '';
|
||||
const wSsNetwork = Form.useWatch(['settings', 'network'], form);
|
||||
const wTunnelNetwork = Form.useWatch(['settings', 'allowedNetwork'], form);
|
||||
@@ -499,6 +526,36 @@ export default function InboundFormModal({
|
||||
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="shareAddrStrategy"
|
||||
label={t('pages.inbounds.form.shareAddrStrategy')}
|
||||
extra={t('pages.inbounds.form.shareAddrStrategyHelp')}
|
||||
>
|
||||
<Select
|
||||
options={SHARE_ADDR_STRATEGIES.map((strategy) => ({
|
||||
value: strategy,
|
||||
label: t(`pages.inbounds.form.shareAddrStrategyOptions.${strategy}`),
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{shareAddrStrategy === 'custom' && (
|
||||
<Form.Item
|
||||
name="shareAddr"
|
||||
label={t('pages.inbounds.form.shareAddr')}
|
||||
extra={t('pages.inbounds.form.shareAddrHelp')}
|
||||
rules={[{
|
||||
validator: (_, value) => (
|
||||
isValidShareAddrInput(String(value ?? ''))
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error(t('pages.inbounds.form.shareAddrHelp')))
|
||||
),
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="edge.example.com" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('pages.inbounds.port')}
|
||||
|
||||
Reference in New Issue
Block a user