fix(links): use configured domain for panel copy/QR links on loopback

The panel's copy/QR share links are built client-side and fell back to window.location.hostname, so reaching the panel over an SSH tunnel (127.0.0.1/localhost) leaked localhost into the links - unlike the backend subscription path, which falls back to the configured Sub/Web Domain (issue #4829).

Expose webDomain/subDomain via /defaultSettings and add preferPublicHost: when the browser host is loopback, prefer the configured Sub Domain (then Web Domain) for share/QR links. An explicit node override or per-inbound listen still wins; a routable browser host is kept as-is.

Closes #4829
This commit is contained in:
MHSanaei
2026-06-02 22:52:44 +02:00
parent fcc6787a64
commit 6ee462ac8e
8 changed files with 66 additions and 8 deletions
+5 -5
View File
@@ -23,7 +23,7 @@ import {
import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils';
import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
import { genInboundLinks } from '@/lib/xray/inbound-link';
import { genInboundLinks, preferPublicHost } from '@/lib/xray/inbound-link';
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
import { useTheme } from '@/hooks/useTheme';
@@ -260,11 +260,11 @@ export default function InboundsPage() {
remark: projected.remark,
remarkModel,
hostOverride: hostOverrideFor(dbInbound),
fallbackHostname: window.location.hostname,
fallbackHostname: preferPublicHost(window.location.hostname, subSettings.publicHost),
}),
fileName: projected.remark || 'inbound',
});
}, [checkFallback, remarkModel, hostOverrideFor, openText, t]);
}, [checkFallback, remarkModel, hostOverrideFor, subSettings.publicHost, openText, t]);
const exportInboundClipboard = useCallback((dbInbound: DBInbound) => {
openText({ title: t('pages.inbounds.inboundJsonTitle'), content: JSON.stringify(dbInbound, null, 2) });
@@ -298,11 +298,11 @@ export default function InboundsPage() {
remark: projected.remark,
remarkModel,
hostOverride: hostOverrideFor(ib),
fallbackHostname: window.location.hostname,
fallbackHostname: preferPublicHost(window.location.hostname, subSettings.publicHost),
}));
}
openText({ title: t('pages.inbounds.exportAllLinksTitle'), content: out.join('\r\n'), fileName: t('pages.inbounds.exportAllLinksFileName') });
}, [dbInbounds, hydrateInbound, checkFallback, remarkModel, hostOverrideFor, openText, t]);
}, [dbInbounds, hydrateInbound, checkFallback, remarkModel, hostOverrideFor, subSettings.publicHost, openText, t]);
const exportAllSubs = useCallback(async () => {
const hydrated = await Promise.all(
@@ -11,6 +11,7 @@ import {
genAllLinks,
genWireguardConfigs,
genWireguardLinks,
preferPublicHost,
} from '@/lib/xray/inbound-link';
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
@@ -113,7 +114,7 @@ export default function InboundInfoModal({
setClientStats(stats);
const inboundForLinks = inboundFromDb(dbInbound);
const fallbackHostname = window.location.hostname;
const fallbackHostname = preferPublicHost(window.location.hostname, subSettings?.publicHost ?? '');
if (info.protocol === Protocols.WIREGUARD) {
setWireguardConfigs(
genWireguardConfigs({
@@ -9,6 +9,7 @@ import {
genWireguardConfigs,
genWireguardLinks,
isPostQuantumLink,
preferPublicHost,
} from '@/lib/xray/inbound-link';
import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
import QrPanel from './QrPanel';
@@ -57,7 +58,7 @@ export default function QrCodeModal({
useEffect(() => {
if (!open || !dbInbound) return;
const inbound = inboundFromDb(dbInbound);
const fallbackHostname = window.location.hostname;
const fallbackHostname = preferPublicHost(window.location.hostname, subSettings?.publicHost ?? '');
if (inbound.protocol === Protocols.WIREGUARD) {
const peerRemark = client?.email
? `${dbInbound.remark}-${client.email}`
+6 -1
View File
@@ -18,6 +18,10 @@ export interface SubSettings {
subURI: string;
subJsonURI: string;
subJsonEnable: boolean;
// Configured public host (Sub Domain, else Web Domain) used as the share/QR
// link host when the panel is reached on a loopback address. Empty if neither
// is set.
publicHost: string;
}
type DBInboundInstance = InstanceType<typeof DBInbound>;
@@ -135,7 +139,8 @@ export function useInbounds() {
subURI: defaults.subURI || '',
subJsonURI: defaults.subJsonURI || '',
subJsonEnable: !!defaults.subJsonEnable,
}), [defaults.subEnable, defaults.subTitle, defaults.subURI, defaults.subJsonURI, defaults.subJsonEnable]);
publicHost: defaults.subDomain || defaults.webDomain || '',
}), [defaults.subEnable, defaults.subTitle, defaults.subURI, defaults.subJsonURI, defaults.subJsonEnable, defaults.subDomain, defaults.webDomain]);
useEffect(() => {
if (defaults.datepicker) setDatepicker(datepicker);