fix(clients): render all tunnel configs for multi-inbound client (#6346) (#6349)

When a client belongs to multiple AmneziaWG or WireGuard inbounds (e.g. across
remote nodes), findAmneziaWGInbounds and findWireguardInbounds only returned the
first matching inbound. Consequently, ClientInfoModal and ClientQrModal rendered
only one config block, making other inbounds' configs unreachable.

- Add findAmneziaWGInbounds and findWireguardInbounds returning all matching inbounds
- Add formatTunnelConfigMeta helper to unify label, fileName, and qrRemark resolution
- Support addressOverride in buildWireguardClientConfig from tunnelAllowedIPs
- Render all tunnel configs in ClientInfoModal and ClientQrModal with node remarks
- Distinguish download filenames with inbound remark suffix to avoid collisions
- Add component integration tests covering multi-inbound modal rendering
This commit is contained in:
MRVX
2026-09-03 18:33:48 +03:30
committed by GitHub
parent de18c5a006
commit 47964afbc5
6 changed files with 349 additions and 105 deletions
+67 -41
View File
@@ -10,7 +10,7 @@ import {
} from '@ant-design/icons';
import { ClipboardManager, FileManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { formatInboundLabel, formatTunnelConfigMeta } from '@/lib/inbounds/label';
import { normalizeClientIps, type ClientIpInfo } from '@/lib/clients/ip-log';
import { useDatepicker } from '@/hooks/useDatepicker';
import { useClientHwids } from '@/hooks/useClientHwids';
@@ -22,12 +22,12 @@ import ClientHwidListModal from '@/components/clients/ClientHwidList';
import ConfigBlock from '@/components/clients/ConfigBlock';
import {
buildWireguardClientConfig,
findWireguardInbound,
findWireguardInbounds,
isWireguardClient,
} from './wireguardConfig';
import {
buildAmneziaWGClientConfig,
findAmneziaWGInbound,
findAmneziaWGInbounds,
isAmneziaWGClient,
} from './amneziawgConfig';
import './ClientInfoModal.css';
@@ -180,35 +180,47 @@ export default function ClientInfoModal({
: '';
const showSubscription = !!(subSettings?.enable && client?.subId);
const wgInbound = useMemo(
() => findWireguardInbound(client, inboundsById),
const wgInbounds = useMemo(
() => findWireguardInbounds(client, inboundsById),
[client, inboundsById],
);
const wgConfigText = useMemo(() => {
if (!client || !wgInbound || !isWireguardClient(client)) return '';
return buildWireguardClientConfig(
client,
wgInbound,
window.location.hostname,
subSettings?.publicHost ?? '',
);
}, [client, wgInbound, subSettings?.publicHost]);
const wgConfigs = useMemo(() => {
if (!client || !isWireguardClient(client)) return [];
return wgInbounds
.map((ib) => {
const address = tunnelAllowedIPs?.[ib.id] ?? '';
const text = buildWireguardClientConfig(
client,
ib,
window.location.hostname,
subSettings?.publicHost ?? '',
address,
);
return { inbound: ib, text };
})
.filter((c) => !!c.text);
}, [client, wgInbounds, tunnelAllowedIPs, subSettings?.publicHost]);
const awgInbound = useMemo(
() => findAmneziaWGInbound(client, inboundsById),
const awgInbounds = useMemo(
() => findAmneziaWGInbounds(client, inboundsById),
[client, inboundsById],
);
const awgConfigText = useMemo(() => {
if (!client || !awgInbound || !isAmneziaWGClient(client)) return '';
const address = awgInbound ? (tunnelAllowedIPs?.[awgInbound.id] ?? '') : '';
return buildAmneziaWGClientConfig(
client,
awgInbound,
window.location.hostname,
subSettings?.publicHost ?? '',
address,
);
}, [client, awgInbound, tunnelAllowedIPs, subSettings?.publicHost]);
const awgConfigs = useMemo(() => {
if (!client || !isAmneziaWGClient(client)) return [];
return awgInbounds
.map((ib) => {
const address = tunnelAllowedIPs?.[ib.id] ?? '';
const text = buildAmneziaWGClientConfig(
client,
ib,
window.location.hostname,
subSettings?.publicHost ?? '',
address,
);
return { inbound: ib, text };
})
.filter((c) => !!c.text);
}, [client, awgInbounds, tunnelAllowedIPs, subSettings?.publicHost]);
async function copyValue(text: string) {
if (!text) return;
@@ -779,27 +791,41 @@ export default function ClientInfoModal({
</>
)}
{wgConfigText && client && (
{wgConfigs.length > 0 && client && (
<>
<Divider>{t('pages.clients.wireguardConfig')}</Divider>
<ConfigBlock
label={t('pages.clients.config')}
text={wgConfigText}
fileName={`${client.email}.conf`}
qrRemark={client.email || 'peer'}
/>
{wgConfigs.map(({ inbound, text }) => {
const meta = formatTunnelConfigMeta(inbound, client.email, wgConfigs.length);
return (
<ConfigBlock
key={`wg-${inbound.id}`}
label={meta.label || t('pages.clients.config')}
text={text}
fileName={meta.fileName}
qrRemark={meta.qrRemark}
tagColor="cyan"
/>
);
})}
</>
)}
{awgConfigText && client && (
{awgConfigs.length > 0 && client && (
<>
<Divider>{t('pages.clients.amneziaWgConfig')}</Divider>
<ConfigBlock
label={t('pages.clients.config')}
text={awgConfigText}
fileName={`${client.email}.conf`}
qrRemark={client.email || 'peer'}
/>
{awgConfigs.map(({ inbound, text }) => {
const meta = formatTunnelConfigMeta(inbound, client.email, awgConfigs.length);
return (
<ConfigBlock
key={`awg-${inbound.id}`}
label={meta.label || t('pages.clients.config')}
text={text}
fileName={meta.fileName}
qrRemark={meta.qrRemark}
tagColor="purple"
/>
);
})}
</>
)}
</>