mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-12 00:45:01 +03:00
fix(hosts): advertise Hosts in every WireGuard, AmneziaWG and TUIC config
Invariant: an inbound's enabled Hosts are the endpoints every client config for it advertises, whichever surface renders that config. WireGuard and AmneziaWG broke it. Their raw generators ignored the externalProxy entries Hosts are injected as and always emitted resolveInboundAddress, so the raw subscription, the sub page .conf, the clients links API and "export all links" gave out the panel address while the JSON and Clash formats of the same inbound used the Host. advertisedEndpoints now states the fan-out once for mtproto, wireguard and amneziawg. The browser-built configs had the same gap. The Clients page WireGuard and AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the panel hostname next to server links that already used Hosts; the Inbounds page peer configs, QR and export ignored them too. withMtprotoHostEndpoints becomes withHostEndpoints over a shared hostEndpointsFor mirror of the backend, the tunnel fan-outs render one config per Host, and the clients page waits for the hosts list the way the inbounds page does, so an empty list means "no hosts" rather than "not loaded yet".
This commit is contained in:
@@ -30,6 +30,8 @@ import {
|
||||
findAmneziaWGInbounds,
|
||||
isAmneziaWGClient,
|
||||
} from './amneziawgConfig';
|
||||
import { tunnelConfigEndpoints, tunnelEndpointLabel } from './tunnelEndpoints';
|
||||
import type { HostRecord } from '@/schemas/api/host';
|
||||
import './ClientInfoModal.css';
|
||||
|
||||
const INBOUND_PROTOCOL_COLORS: Record<string, string> = {
|
||||
@@ -66,9 +68,12 @@ interface ClientInfoModalProps {
|
||||
tunnelAllowedIPs?: Record<number, string>;
|
||||
isOnline: boolean;
|
||||
subSettings?: SubSettings;
|
||||
hosts?: HostRecord[];
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const NO_HOSTS: HostRecord[] = [];
|
||||
|
||||
interface ApiMsg<T = unknown> {
|
||||
success?: boolean;
|
||||
obj?: T;
|
||||
@@ -97,6 +102,7 @@ export default function ClientInfoModal({
|
||||
tunnelAllowedIPs,
|
||||
isOnline,
|
||||
subSettings = DEFAULT_SUB,
|
||||
hosts = NO_HOSTS,
|
||||
onOpenChange,
|
||||
}: ClientInfoModalProps) {
|
||||
const { datepicker } = useDatepicker();
|
||||
@@ -187,20 +193,19 @@ export default function ClientInfoModal({
|
||||
);
|
||||
const wgConfigs = useMemo(() => {
|
||||
if (!client || !isWireguardClient(client)) return [];
|
||||
const host = window.location.hostname;
|
||||
const publicHost = subSettings?.publicHost ?? '';
|
||||
return wgInbounds
|
||||
.map((ib) => {
|
||||
.flatMap((ib) => {
|
||||
const address = tunnelAllowedIPs?.[ib.id] ?? '';
|
||||
const text = buildWireguardClientConfig(
|
||||
client,
|
||||
ib,
|
||||
window.location.hostname,
|
||||
subSettings?.publicHost ?? '',
|
||||
address,
|
||||
);
|
||||
return { inbound: ib, text };
|
||||
return tunnelConfigEndpoints(ib, hosts, host, publicHost).map((ep) => ({
|
||||
inbound: ib,
|
||||
endpoint: tunnelEndpointLabel(ep),
|
||||
text: buildWireguardClientConfig(client, ib, host, publicHost, address, ep),
|
||||
}));
|
||||
})
|
||||
.filter((c) => !!c.text);
|
||||
}, [client, wgInbounds, tunnelAllowedIPs, subSettings?.publicHost]);
|
||||
}, [client, wgInbounds, tunnelAllowedIPs, subSettings?.publicHost, hosts]);
|
||||
|
||||
const awgInbounds = useMemo(
|
||||
() => findAmneziaWGInbounds(client, inboundsById),
|
||||
@@ -208,20 +213,19 @@ export default function ClientInfoModal({
|
||||
);
|
||||
const awgConfigs = useMemo(() => {
|
||||
if (!client || !isAmneziaWGClient(client)) return [];
|
||||
const host = window.location.hostname;
|
||||
const publicHost = subSettings?.publicHost ?? '';
|
||||
return awgInbounds
|
||||
.map((ib) => {
|
||||
.flatMap((ib) => {
|
||||
const address = tunnelAllowedIPs?.[ib.id] ?? '';
|
||||
const text = buildAmneziaWGClientConfig(
|
||||
client,
|
||||
ib,
|
||||
window.location.hostname,
|
||||
subSettings?.publicHost ?? '',
|
||||
address,
|
||||
);
|
||||
return { inbound: ib, text };
|
||||
return tunnelConfigEndpoints(ib, hosts, host, publicHost).map((ep) => ({
|
||||
inbound: ib,
|
||||
endpoint: tunnelEndpointLabel(ep),
|
||||
text: buildAmneziaWGClientConfig(client, ib, host, publicHost, address, ep),
|
||||
}));
|
||||
})
|
||||
.filter((c) => !!c.text);
|
||||
}, [client, awgInbounds, tunnelAllowedIPs, subSettings?.publicHost]);
|
||||
}, [client, awgInbounds, tunnelAllowedIPs, subSettings?.publicHost, hosts]);
|
||||
|
||||
async function copyValue(text: string) {
|
||||
if (!text) return;
|
||||
@@ -795,11 +799,16 @@ export default function ClientInfoModal({
|
||||
{wgConfigs.length > 0 && client && (
|
||||
<>
|
||||
<Divider>{t('pages.clients.wireguardConfig')}</Divider>
|
||||
{wgConfigs.map(({ inbound, text }) => {
|
||||
const meta = formatTunnelConfigMeta(inbound, client.email, wgConfigs.length);
|
||||
{wgConfigs.map(({ inbound, endpoint, text }) => {
|
||||
const meta = formatTunnelConfigMeta(
|
||||
inbound,
|
||||
client.email,
|
||||
wgConfigs.length,
|
||||
endpoint,
|
||||
);
|
||||
return (
|
||||
<ConfigBlock
|
||||
key={`wg-${inbound.id}`}
|
||||
key={`wg-${inbound.id}-${endpoint}`}
|
||||
label={meta.label || t('pages.clients.config')}
|
||||
text={text}
|
||||
fileName={meta.fileName}
|
||||
@@ -814,11 +823,16 @@ export default function ClientInfoModal({
|
||||
{awgConfigs.length > 0 && client && (
|
||||
<>
|
||||
<Divider>{t('pages.clients.amneziaWgConfig')}</Divider>
|
||||
{awgConfigs.map(({ inbound, text }) => {
|
||||
const meta = formatTunnelConfigMeta(inbound, client.email, awgConfigs.length);
|
||||
{awgConfigs.map(({ inbound, endpoint, text }) => {
|
||||
const meta = formatTunnelConfigMeta(
|
||||
inbound,
|
||||
client.email,
|
||||
awgConfigs.length,
|
||||
endpoint,
|
||||
);
|
||||
return (
|
||||
<ConfigBlock
|
||||
key={`awg-${inbound.id}`}
|
||||
key={`awg-${inbound.id}-${endpoint}`}
|
||||
label={meta.label || t('pages.clients.config')}
|
||||
text={text}
|
||||
fileName={meta.fileName}
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
isAmneziaWGClient,
|
||||
} from './amneziawgConfig';
|
||||
import { buildTuicClientConfig, findTuicInbound, isTuicClient } from './tuicConfig';
|
||||
import { tunnelConfigEndpoints, tunnelEndpointLabel } from './tunnelEndpoints';
|
||||
import type { HostRecord } from '@/schemas/api/host';
|
||||
|
||||
interface SubSettings {
|
||||
enable: boolean;
|
||||
@@ -38,9 +40,12 @@ interface ClientQrModalProps {
|
||||
inboundsById: Record<number, InboundOption>;
|
||||
tunnelAllowedIPs?: Record<number, string>;
|
||||
subSettings?: SubSettings;
|
||||
hosts?: HostRecord[];
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const NO_HOSTS: HostRecord[] = [];
|
||||
|
||||
interface ApiMsg<T = unknown> {
|
||||
success?: boolean;
|
||||
obj?: T;
|
||||
@@ -227,6 +232,7 @@ function ClientQrModalContent({
|
||||
inboundsById,
|
||||
tunnelAllowedIPs,
|
||||
subSettings = DEFAULT_SUB,
|
||||
hosts = NO_HOSTS,
|
||||
onOpenChange,
|
||||
}: ClientQrModalProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -326,20 +332,19 @@ function ClientQrModalContent({
|
||||
);
|
||||
const wgConfigs = useMemo(() => {
|
||||
if (!client || !isWireguardClient(client)) return [];
|
||||
const host = window.location.hostname;
|
||||
const publicHost = subSettings.publicHost ?? '';
|
||||
return wgInbounds
|
||||
.map((ib) => {
|
||||
.flatMap((ib) => {
|
||||
const address = tunnelAllowedIPs?.[ib.id] ?? '';
|
||||
const text = buildWireguardClientConfig(
|
||||
client,
|
||||
ib,
|
||||
window.location.hostname,
|
||||
subSettings.publicHost ?? '',
|
||||
address,
|
||||
);
|
||||
return { inbound: ib, text };
|
||||
return tunnelConfigEndpoints(ib, hosts, host, publicHost).map((ep) => ({
|
||||
inbound: ib,
|
||||
endpoint: tunnelEndpointLabel(ep),
|
||||
text: buildWireguardClientConfig(client, ib, host, publicHost, address, ep),
|
||||
}));
|
||||
})
|
||||
.filter((c) => !!c.text);
|
||||
}, [client, wgInbounds, tunnelAllowedIPs, subSettings.publicHost]);
|
||||
}, [client, wgInbounds, tunnelAllowedIPs, subSettings.publicHost, hosts]);
|
||||
|
||||
const awgInbounds = useMemo(
|
||||
() => findAmneziaWGInbounds(client, inboundsById),
|
||||
@@ -347,38 +352,37 @@ function ClientQrModalContent({
|
||||
);
|
||||
const awgConfigs = useMemo(() => {
|
||||
if (!client || !isAmneziaWGClient(client)) return [];
|
||||
const host = window.location.hostname;
|
||||
const publicHost = subSettings.publicHost ?? '';
|
||||
return awgInbounds
|
||||
.map((ib) => {
|
||||
.flatMap((ib) => {
|
||||
const address = tunnelAllowedIPs?.[ib.id] ?? '';
|
||||
const text = buildAmneziaWGClientConfig(
|
||||
client,
|
||||
ib,
|
||||
window.location.hostname,
|
||||
subSettings.publicHost ?? '',
|
||||
address,
|
||||
);
|
||||
return { inbound: ib, text };
|
||||
return tunnelConfigEndpoints(ib, hosts, host, publicHost).map((ep) => ({
|
||||
inbound: ib,
|
||||
endpoint: tunnelEndpointLabel(ep),
|
||||
text: buildAmneziaWGClientConfig(client, ib, host, publicHost, address, ep),
|
||||
}));
|
||||
})
|
||||
.filter((c) => !!c.text);
|
||||
}, [client, awgInbounds, tunnelAllowedIPs, subSettings.publicHost]);
|
||||
}, [client, awgInbounds, tunnelAllowedIPs, subSettings.publicHost, hosts]);
|
||||
|
||||
const tuicInbound = useMemo(() => findTuicInbound(client, inboundsById), [client, inboundsById]);
|
||||
const tuicConfigText = useMemo(() => {
|
||||
if (!client || !tuicInbound || !isTuicClient(client)) return '';
|
||||
return buildTuicClientConfig(
|
||||
client,
|
||||
tuicInbound,
|
||||
window.location.hostname,
|
||||
subSettings.publicHost ?? '',
|
||||
);
|
||||
}, [client, tuicInbound, subSettings.publicHost]);
|
||||
const tuicConfigs = useMemo(() => {
|
||||
if (!client || !tuicInbound || !isTuicClient(client)) return [];
|
||||
const host = window.location.hostname;
|
||||
const publicHost = subSettings.publicHost ?? '';
|
||||
return tunnelConfigEndpoints(tuicInbound, hosts, host, publicHost, 'clash').map((ep) => ({
|
||||
endpoint: tunnelEndpointLabel(ep),
|
||||
text: buildTuicClientConfig(client, tuicInbound, host, publicHost, ep),
|
||||
}));
|
||||
}, [client, tuicInbound, subSettings.publicHost, hosts]);
|
||||
|
||||
const hasAnything =
|
||||
!!subLink ||
|
||||
!!subJsonLink ||
|
||||
wgConfigs.length > 0 ||
|
||||
awgConfigs.length > 0 ||
|
||||
!!tuicConfigText ||
|
||||
tuicConfigs.length > 0 ||
|
||||
links.length > 0;
|
||||
|
||||
// The reset runs during render so the effect only carries the request.
|
||||
@@ -468,8 +472,8 @@ function ClientQrModalContent({
|
||||
),
|
||||
});
|
||||
});
|
||||
wgConfigs.forEach(({ inbound, text }) => {
|
||||
const meta = formatTunnelConfigMeta(inbound, client?.email, wgConfigs.length);
|
||||
wgConfigs.forEach(({ inbound, endpoint, text }) => {
|
||||
const meta = formatTunnelConfigMeta(inbound, client?.email, wgConfigs.length, endpoint);
|
||||
const label = (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||
<Tag color="cyan" style={{ margin: 0 }}>
|
||||
@@ -479,13 +483,13 @@ function ClientQrModalContent({
|
||||
</span>
|
||||
);
|
||||
out.push({
|
||||
key: `wg-config-${inbound.id}`,
|
||||
key: `wg-config-${inbound.id}-${endpoint}`,
|
||||
label,
|
||||
children: <QrPanel value={text} remark={meta.qrRemark} downloadName={meta.fileName} />,
|
||||
});
|
||||
});
|
||||
awgConfigs.forEach(({ inbound, text }) => {
|
||||
const meta = formatTunnelConfigMeta(inbound, client?.email, awgConfigs.length);
|
||||
awgConfigs.forEach(({ inbound, endpoint, text }) => {
|
||||
const meta = formatTunnelConfigMeta(inbound, client?.email, awgConfigs.length, endpoint);
|
||||
const label = (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||
<Tag color="purple" style={{ margin: 0 }}>
|
||||
@@ -495,28 +499,33 @@ function ClientQrModalContent({
|
||||
</span>
|
||||
);
|
||||
out.push({
|
||||
key: `awg-config-${inbound.id}`,
|
||||
key: `awg-config-${inbound.id}-${endpoint}`,
|
||||
label,
|
||||
children: <QrPanel value={text} remark={meta.qrRemark} downloadName={meta.fileName} />,
|
||||
});
|
||||
});
|
||||
if (tuicConfigText) {
|
||||
tuicConfigs.forEach(({ endpoint, text }) => {
|
||||
const name = client?.email || 'tuic';
|
||||
const multi = tuicConfigs.length > 1 ? endpoint : '';
|
||||
out.push({
|
||||
key: 'tuic-config',
|
||||
key: `tuic-config-${endpoint}`,
|
||||
label: (
|
||||
<Tag color="orange" style={{ margin: 0 }}>
|
||||
{t('pages.clients.tuicConfig')}
|
||||
</Tag>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||
<Tag color="orange" style={{ margin: 0 }}>
|
||||
{t('pages.clients.tuicConfig')}
|
||||
</Tag>
|
||||
{multi && <span style={{ opacity: 0.85, fontSize: 12 }}>{multi}</span>}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<QrPanel
|
||||
value={tuicConfigText}
|
||||
remark={client?.email || 'tuic'}
|
||||
downloadName={`${client?.email || 'tuic'}.yaml`}
|
||||
value={text}
|
||||
remark={[name, multi].filter(Boolean).join(' - ')}
|
||||
downloadName={`${[name, multi.replace(/[^\w.-]+/g, '_')].filter(Boolean).join('-')}.yaml`}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}, [
|
||||
subLink,
|
||||
@@ -533,7 +542,7 @@ function ClientQrModalContent({
|
||||
selectVariant,
|
||||
regenerateHappLink,
|
||||
openHappSettings,
|
||||
tuicConfigText,
|
||||
tuicConfigs,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
import { useClients } from '@/hooks/useClients';
|
||||
import { useNodesQuery } from '@/api/queries/useNodesQuery';
|
||||
import { useHostsQuery } from '@/api/queries/useHostsQuery';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
import type {
|
||||
ClientRecord,
|
||||
@@ -381,6 +382,15 @@ export default function ClientsPage() {
|
||||
// Node list for the Nodes filter; the section only renders when the panel
|
||||
// actually manages nodes (#4997).
|
||||
const { nodes } = useNodesQuery();
|
||||
// Tunnel configs advertise these Hosts, so an empty list must mean "no hosts"
|
||||
// and not "not loaded yet" — the page gate waits for it.
|
||||
const {
|
||||
hosts,
|
||||
fetched: hostsFetched,
|
||||
fetchError: hostsFetchError,
|
||||
refetch: refetchHosts,
|
||||
} = useHostsQuery();
|
||||
const hostsError = hosts.length > 0 ? '' : hostsFetchError;
|
||||
|
||||
const [togglingEmail, setTogglingEmail] = useState<string | null>(null);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
@@ -769,11 +779,11 @@ export default function ClientsPage() {
|
||||
const onRefreshClick = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await refresh();
|
||||
await Promise.all([refresh(), refetchHosts()]);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [refresh]);
|
||||
}, [refresh, refetchHosts]);
|
||||
|
||||
const openText = useCallback((opts: { title: string; content: string; fileName?: string }) => {
|
||||
setTextTitle(opts.title);
|
||||
@@ -1289,14 +1299,19 @@ export default function ClientsPage() {
|
||||
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content id="content-layout" className="content-area">
|
||||
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
|
||||
{!fetched ? (
|
||||
<Spin
|
||||
spinning={!fetched || !hostsFetched}
|
||||
delay={200}
|
||||
description={t('loading')}
|
||||
size="large"
|
||||
>
|
||||
{!fetched || !hostsFetched ? (
|
||||
<div className="loading-spacer" />
|
||||
) : fetchError ? (
|
||||
) : fetchError || hostsError ? (
|
||||
<Result
|
||||
status="error"
|
||||
title={t('somethingWentWrong')}
|
||||
subTitle={fetchError}
|
||||
subTitle={fetchError || hostsError}
|
||||
extra={
|
||||
<Button type="primary" loading={refreshing} onClick={onRefreshClick}>
|
||||
{t('refresh')}
|
||||
@@ -1898,6 +1913,7 @@ export default function ClientsPage() {
|
||||
tunnelAllowedIPs={viewingTunnelAllowedIPs}
|
||||
isOnline={infoClient ? isOnline(infoClient.email) : false}
|
||||
subSettings={subSettings}
|
||||
hosts={hosts}
|
||||
onOpenChange={setInfoOpen}
|
||||
/>
|
||||
</LazyMount>
|
||||
@@ -1908,6 +1924,7 @@ export default function ClientsPage() {
|
||||
inboundsById={inboundsById}
|
||||
tunnelAllowedIPs={viewingTunnelAllowedIPs}
|
||||
subSettings={subSettings}
|
||||
hosts={hosts}
|
||||
onOpenChange={setQrOpen}
|
||||
/>
|
||||
</LazyMount>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HostEndpoint } from '@/lib/hosts/host-link';
|
||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
|
||||
import { effectiveMtu } from '@/lib/xray/amneziawg-obfuscation';
|
||||
@@ -44,17 +45,18 @@ export function buildAmneziaWGClientConfig(
|
||||
host = window.location.hostname,
|
||||
publicHost = '',
|
||||
addressOverride = '',
|
||||
hostEndpoint?: HostEndpoint,
|
||||
): string {
|
||||
const server = inbound?.awgServer;
|
||||
const endpointHost = resolveShareHost(
|
||||
inbound ?? {},
|
||||
inbound?.nodeAddress ?? '',
|
||||
preferPublicHost(host, publicHost),
|
||||
);
|
||||
const endpointHost =
|
||||
hostEndpoint?.dest ||
|
||||
resolveShareHost(inbound ?? {}, inbound?.nodeAddress ?? '', preferPublicHost(host, publicHost));
|
||||
const address = addressOverride || client.allowedIPs || '10.8.1.2/32';
|
||||
const endpoint = `${endpointHost}:${inbound?.port || ''}`;
|
||||
const endpoint = `${endpointHost}:${hostEndpoint?.port || inbound?.port || ''}`;
|
||||
const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';
|
||||
const remark = [inboundName, client.email, client.comment].filter(Boolean).join(' - ');
|
||||
const remark = [inboundName, hostEndpoint?.remark, client.email, client.comment]
|
||||
.filter(Boolean)
|
||||
.join(' - ');
|
||||
|
||||
// These land unescaped in [Interface]; a newline here would inject a
|
||||
// config line (e.g. a rogue PostUp) into the downloaded .conf.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HostEndpoint } from '@/lib/hosts/host-link';
|
||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
@@ -21,21 +22,23 @@ export function buildTuicClientConfig(
|
||||
inbound: InboundOption | undefined,
|
||||
host = window.location.hostname,
|
||||
publicHost = '',
|
||||
hostEndpoint?: HostEndpoint,
|
||||
): string {
|
||||
const endpointHost = resolveShareHost(
|
||||
inbound ?? {},
|
||||
inbound?.nodeAddress ?? '',
|
||||
preferPublicHost(host, publicHost),
|
||||
);
|
||||
const endpointHost =
|
||||
hostEndpoint?.dest ||
|
||||
resolveShareHost(inbound ?? {}, inbound?.nodeAddress ?? '', preferPublicHost(host, publicHost));
|
||||
const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';
|
||||
const remark = [inboundName, client.email].filter(Boolean).join(' - ') || 'tuic-client';
|
||||
const remark =
|
||||
[inboundName, hostEndpoint?.remark, client.email].filter(Boolean).join(' - ') || 'tuic-client';
|
||||
|
||||
// A Host's SNI/ALPN/insecure override the inbound's, as the backend buildTuicProxy does.
|
||||
const tuicServer = inbound?.tuicServer;
|
||||
const alpn =
|
||||
Array.isArray(tuicServer?.alpn) && tuicServer.alpn.length > 0
|
||||
const alpn = hostEndpoint?.alpn?.length
|
||||
? hostEndpoint.alpn
|
||||
: Array.isArray(tuicServer?.alpn) && tuicServer.alpn.length > 0
|
||||
? tuicServer.alpn
|
||||
: ['h3', 'spdy/3.1'];
|
||||
const sni = tuicServer?.sni || endpointHost;
|
||||
const sni = hostEndpoint?.sni || tuicServer?.sni || endpointHost;
|
||||
const cc = tuicServer?.congestion_control || 'bbr';
|
||||
const udpRelay = tuicServer?.udp_relay_mode || 'native';
|
||||
const reduceRtt = tuicServer?.zero_rtt_handshake ?? true;
|
||||
@@ -49,7 +52,7 @@ export function buildTuicClientConfig(
|
||||
` - name: ${yamlQuote(remark)}`,
|
||||
` type: tuic`,
|
||||
` server: ${endpointHost}`,
|
||||
` port: ${inbound?.port || 8443}`,
|
||||
` port: ${hostEndpoint?.port || inbound?.port || 8443}`,
|
||||
` uuid: ${client.uuid || ''}`,
|
||||
` password: ${yamlQuote(client.password || '')}`,
|
||||
` alpn:`,
|
||||
@@ -59,6 +62,7 @@ export function buildTuicClientConfig(
|
||||
` udp-relay-mode: ${udpRelay}`,
|
||||
` reduce-rtt: ${reduceRtt}`,
|
||||
];
|
||||
if (hostEndpoint?.allowInsecure) lines.push(' skip-cert-verify: true');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { hostEndpointsFor, type HostEndpoint } from '@/lib/hosts/host-link';
|
||||
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
|
||||
import type { InboundOption } from '@/hooks/useClients';
|
||||
import type { HostRecord } from '@/schemas/api/host';
|
||||
|
||||
// One client config per Host the subscription of that format advertises for the
|
||||
// inbound; `undefined` stands for the inbound's own address when no Host applies.
|
||||
export function tunnelConfigEndpoints(
|
||||
inbound: InboundOption,
|
||||
hosts: HostRecord[],
|
||||
host: string,
|
||||
publicHost: string,
|
||||
subType: 'raw' | 'clash' = 'raw',
|
||||
): (HostEndpoint | undefined)[] {
|
||||
const defaultDest = resolveShareHost(
|
||||
inbound,
|
||||
inbound.nodeAddress ?? '',
|
||||
preferPublicHost(host, publicHost),
|
||||
);
|
||||
const endpoints = hostEndpointsFor(hosts, inbound.id, inbound.port ?? 0, defaultDest, subType);
|
||||
return endpoints.length > 0 ? endpoints : [undefined];
|
||||
}
|
||||
|
||||
// A Host group shares one remark across its addresses, so only dest:port is unique.
|
||||
export function tunnelEndpointLabel(endpoint: HostEndpoint | undefined): string {
|
||||
return endpoint ? `${endpoint.dest}:${endpoint.port}` : '';
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HostEndpoint } from '@/lib/hosts/host-link';
|
||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
@@ -28,16 +29,17 @@ export function buildWireguardClientConfig(
|
||||
host = window.location.hostname,
|
||||
publicHost = '',
|
||||
addressOverride = '',
|
||||
hostEndpoint?: HostEndpoint,
|
||||
): string {
|
||||
const endpointHost = resolveShareHost(
|
||||
inbound ?? {},
|
||||
inbound?.nodeAddress ?? '',
|
||||
preferPublicHost(host, publicHost),
|
||||
);
|
||||
const endpointHost =
|
||||
hostEndpoint?.dest ||
|
||||
resolveShareHost(inbound ?? {}, inbound?.nodeAddress ?? '', preferPublicHost(host, publicHost));
|
||||
const address = addressOverride || client.allowedIPs || '10.0.0.2/32';
|
||||
const endpoint = `${endpointHost}:${inbound?.port || ''}`;
|
||||
const endpoint = `${endpointHost}:${hostEndpoint?.port || inbound?.port || ''}`;
|
||||
const inboundName = inbound ? formatInboundLabel(inbound.tag, inbound.remark) : '';
|
||||
const remark = [inboundName, client.email, client.comment].filter(Boolean).join(' - ');
|
||||
const remark = [inboundName, hostEndpoint?.remark, client.email, client.comment]
|
||||
.filter(Boolean)
|
||||
.join(' - ');
|
||||
const lines = [
|
||||
'[Interface]',
|
||||
`PrivateKey = ${client.privateKey || client.password || ''}`,
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
import { useNodesQuery } from '@/api/queries/useNodesQuery';
|
||||
import { useHostsQuery } from '@/api/queries/useHostsQuery';
|
||||
import { withMtprotoHostEndpoints } from '@/lib/hosts/host-link';
|
||||
import { withHostEndpoints } from '@/lib/hosts/host-link';
|
||||
import AppSidebar from '@/layouts/AppSidebar';
|
||||
const TextModal = lazy(() => import('@/components/feedback/TextModal'));
|
||||
import type { TextModalTab } from '@/components/feedback/TextModal';
|
||||
@@ -340,7 +340,7 @@ export default function InboundsPage() {
|
||||
const hostOverride = hostOverrideFor(dbInbound);
|
||||
const fallbackHostname = preferPublicHost(window.location.hostname, subSettings.publicHost);
|
||||
const genInput = {
|
||||
inbound: withMtprotoHostEndpoints(
|
||||
inbound: withHostEndpoints(
|
||||
inboundFromDb(projected),
|
||||
dbInbound.id,
|
||||
hosts,
|
||||
|
||||
@@ -10,14 +10,14 @@ import { InfinityIcon } from '@/components/ui';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
import {
|
||||
genAllLinks,
|
||||
genAmneziaWGConfigs,
|
||||
genAmneziaWGLinks,
|
||||
genWireguardConfigs,
|
||||
genWireguardLinks,
|
||||
genAmneziaWGPeerConfigs,
|
||||
genAmneziaWGPeerLinks,
|
||||
genWireguardPeerConfigs,
|
||||
genWireguardPeerLinks,
|
||||
preferPublicHost,
|
||||
} from '@/lib/xray/inbound-link';
|
||||
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
|
||||
import { withMtprotoHostEndpoints } from '@/lib/hosts/host-link';
|
||||
import { withHostEndpoints } from '@/lib/hosts/host-link';
|
||||
|
||||
import {
|
||||
buildInboundInfo,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
downloadText,
|
||||
formatIpInfo,
|
||||
hasShareLink,
|
||||
peerConfFileName,
|
||||
statsColor,
|
||||
} from './helpers';
|
||||
import type { ClientSetting, ClientStats, InboundInfo, InboundInfoModalProps } from './types';
|
||||
@@ -53,10 +54,10 @@ export default function InboundInfoModal({
|
||||
const [clientSettings, setClientSettings] = useState<ClientSetting | null>(null);
|
||||
const [clientStats, setClientStats] = useState<ClientStats | null>(null);
|
||||
const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
|
||||
const [wireguardConfigs, setWireguardConfigs] = useState<string[]>([]);
|
||||
const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
|
||||
const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[]>([]);
|
||||
const [amneziawgLinks, setAmneziawgLinks] = useState<string[]>([]);
|
||||
const [wireguardConfigs, setWireguardConfigs] = useState<string[][]>([]);
|
||||
const [wireguardLinks, setWireguardLinks] = useState<string[][]>([]);
|
||||
const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[][]>([]);
|
||||
const [amneziawgLinks, setAmneziawgLinks] = useState<string[][]>([]);
|
||||
const [subLink, setSubLink] = useState('');
|
||||
const [subJsonLink, setSubJsonLink] = useState('');
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -145,7 +146,7 @@ export default function InboundInfoModal({
|
||||
window.location.hostname,
|
||||
subSettings?.publicHost ?? '',
|
||||
);
|
||||
const inboundForLinks = withMtprotoHostEndpoints(
|
||||
const inboundForLinks = withHostEndpoints(
|
||||
inboundFromDb(dbInbound),
|
||||
dbInbound.id,
|
||||
hosts,
|
||||
@@ -154,40 +155,40 @@ export default function InboundInfoModal({
|
||||
);
|
||||
if (info.protocol === Protocols.WIREGUARD) {
|
||||
setWireguardConfigs(
|
||||
genWireguardConfigs({
|
||||
genWireguardPeerConfigs({
|
||||
inbound: inboundForLinks,
|
||||
remark: dbInbound.remark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setWireguardLinks(
|
||||
genWireguardLinks({
|
||||
genWireguardPeerLinks({
|
||||
inbound: inboundForLinks,
|
||||
remark: dbInbound.remark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setAmneziawgConfigs([]);
|
||||
setAmneziawgLinks([]);
|
||||
setLinks([]);
|
||||
} else if (info.protocol === Protocols.AMNEZIAWG) {
|
||||
setAmneziawgConfigs(
|
||||
genAmneziaWGConfigs({
|
||||
genAmneziaWGPeerConfigs({
|
||||
inbound: inboundForLinks,
|
||||
remark: dbInbound.remark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setAmneziawgLinks(
|
||||
genAmneziaWGLinks({
|
||||
genAmneziaWGPeerLinks({
|
||||
inbound: inboundForLinks,
|
||||
remark: dbInbound.remark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setWireguardConfigs([]);
|
||||
setWireguardLinks([]);
|
||||
@@ -1189,49 +1190,55 @@ export default function InboundInfoModal({
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{wireguardConfigs[idx] && (
|
||||
<div className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">
|
||||
{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}
|
||||
</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(wireguardConfigs[idx], t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
aria-label={t('download')}
|
||||
onClick={() =>
|
||||
downloadText(wireguardConfigs[idx], `peer-${idx + 1}.conf`)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{wireguardConfigs[idx]}</code>
|
||||
</div>
|
||||
{(wireguardConfigs[idx] ?? []).map(
|
||||
(cfg, j, all) =>
|
||||
cfg && (
|
||||
<div key={`wg-cfg-${j}`} className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">
|
||||
{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}
|
||||
</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(cfg, t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
aria-label={t('download')}
|
||||
onClick={() =>
|
||||
downloadText(cfg, peerConfFileName(idx, j, all.length))
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{cfg}</code>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{wireguardLinks[idx] && (
|
||||
<div className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">Peer {idx + 1} link</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(wireguardLinks[idx], t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{wireguardLinks[idx]}</code>
|
||||
</div>
|
||||
{(wireguardLinks[idx] ?? []).map(
|
||||
(link, j) =>
|
||||
link && (
|
||||
<div key={`wg-link-${j}`} className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">Peer {idx + 1} link</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(link, t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{link}</code>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
@@ -1241,49 +1248,55 @@ export default function InboundInfoModal({
|
||||
{inbound?.protocol === Protocols.AMNEZIAWG && amneziawgConfigs.length > 0 && (
|
||||
<>
|
||||
<Divider>{t('pages.inbounds.copyLink')}</Divider>
|
||||
{amneziawgConfigs.map((cfg, idx) => (
|
||||
{amneziawgConfigs.map((peerConfigs, idx) => (
|
||||
<Fragment key={idx}>
|
||||
{cfg && (
|
||||
<div className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">
|
||||
{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}
|
||||
</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(cfg, t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
aria-label={t('download')}
|
||||
onClick={() => downloadText(cfg, `peer-${idx + 1}.conf`)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{cfg}</code>
|
||||
</div>
|
||||
{peerConfigs.map(
|
||||
(cfg, j, all) =>
|
||||
cfg && (
|
||||
<div key={`awg-cfg-${j}`} className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">
|
||||
{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}
|
||||
</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(cfg, t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
aria-label={t('download')}
|
||||
onClick={() => downloadText(cfg, peerConfFileName(idx, j, all.length))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{cfg}</code>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{amneziawgLinks[idx] && (
|
||||
<div className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">Peer {idx + 1} link</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(amneziawgLinks[idx], t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{amneziawgLinks[idx]}</code>
|
||||
</div>
|
||||
{(amneziawgLinks[idx] ?? []).map(
|
||||
(link, j) =>
|
||||
link && (
|
||||
<div key={`awg-link-${j}`} className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">Peer {idx + 1} link</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
aria-label={t('copy')}
|
||||
onClick={() => copyText(link, t)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<code className="link-panel-text">{link}</code>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -162,6 +162,12 @@ export function downloadText(content: string, filename: string) {
|
||||
FileManager.downloadTextFile(content, filename);
|
||||
}
|
||||
|
||||
// One file per advertised Host, so a peer behind two Hosts gets two names.
|
||||
export function peerConfFileName(peerIndex: number, endpointIndex: number, endpointCount: number) {
|
||||
const suffix = endpointCount > 1 ? `-${endpointIndex + 1}` : '';
|
||||
return `peer-${peerIndex + 1}${suffix}.conf`;
|
||||
}
|
||||
|
||||
export function statsColor(stats: ClientStats, trafficDiff: number) {
|
||||
return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@ import type { CollapseProps } from 'antd';
|
||||
import { Protocols } from '@/schemas/primitives';
|
||||
import {
|
||||
genAllLinks,
|
||||
genAmneziaWGConfigs,
|
||||
genAmneziaWGLinks,
|
||||
genWireguardConfigs,
|
||||
genWireguardLinks,
|
||||
genAmneziaWGPeerConfigs,
|
||||
genAmneziaWGPeerLinks,
|
||||
genWireguardPeerConfigs,
|
||||
genWireguardPeerLinks,
|
||||
isPostQuantumLink,
|
||||
preferPublicHost,
|
||||
} from '@/lib/xray/inbound-link';
|
||||
import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
|
||||
import { withMtprotoHostEndpoints } from '@/lib/hosts/host-link';
|
||||
import { withHostEndpoints } from '@/lib/hosts/host-link';
|
||||
import type { HostRecord } from '@/schemas/api/host';
|
||||
import QrPanel from './QrPanel';
|
||||
import { peerConfFileName } from '../info/helpers';
|
||||
import type { SubSettings } from '../useInbounds';
|
||||
|
||||
interface ClientSetting {
|
||||
@@ -56,10 +57,10 @@ export default function QrCodeModal({
|
||||
}: QrCodeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
|
||||
const [wireguardConfigs, setWireguardConfigs] = useState<string[]>([]);
|
||||
const [wireguardLinks, setWireguardLinks] = useState<string[]>([]);
|
||||
const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[]>([]);
|
||||
const [amneziawgLinks, setAmneziawgLinks] = useState<string[]>([]);
|
||||
const [wireguardConfigs, setWireguardConfigs] = useState<string[][]>([]);
|
||||
const [wireguardLinks, setWireguardLinks] = useState<string[][]>([]);
|
||||
const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[][]>([]);
|
||||
const [amneziawgLinks, setAmneziawgLinks] = useState<string[][]>([]);
|
||||
const [subLink, setSubLink] = useState('');
|
||||
const [subJsonLink, setSubJsonLink] = useState('');
|
||||
const [activeKey, setActiveKey] = useState<string[]>([]);
|
||||
@@ -88,7 +89,7 @@ export default function QrCodeModal({
|
||||
window.location.hostname,
|
||||
subSettings?.publicHost ?? '',
|
||||
);
|
||||
const inbound = withMtprotoHostEndpoints(
|
||||
const inbound = withHostEndpoints(
|
||||
inboundFromDb(dbInbound),
|
||||
dbInbound.id,
|
||||
hosts,
|
||||
@@ -100,20 +101,20 @@ export default function QrCodeModal({
|
||||
? `${dbInbound.remark}-${client.email}`
|
||||
: dbInbound.remark || '';
|
||||
setWireguardConfigs(
|
||||
genWireguardConfigs({
|
||||
genWireguardPeerConfigs({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setWireguardLinks(
|
||||
genWireguardLinks({
|
||||
genWireguardPeerLinks({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setAmneziawgConfigs([]);
|
||||
setAmneziawgLinks([]);
|
||||
@@ -123,20 +124,20 @@ export default function QrCodeModal({
|
||||
? `${dbInbound.remark}-${client.email}`
|
||||
: dbInbound.remark || '';
|
||||
setAmneziawgConfigs(
|
||||
genAmneziaWGConfigs({
|
||||
genAmneziaWGPeerConfigs({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setAmneziawgLinks(
|
||||
genAmneziaWGLinks({
|
||||
genAmneziaWGPeerLinks({
|
||||
inbound,
|
||||
remark: peerRemark,
|
||||
hostOverride: nodeAddress,
|
||||
fallbackHostname,
|
||||
}).split('\r\n'),
|
||||
}),
|
||||
);
|
||||
setWireguardConfigs([]);
|
||||
setWireguardLinks([]);
|
||||
@@ -183,38 +184,30 @@ export default function QrCodeModal({
|
||||
links.forEach((link, idx) => {
|
||||
items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
|
||||
});
|
||||
wireguardConfigs.forEach((cfg, idx) => {
|
||||
items.push({
|
||||
key: `wc${idx}`,
|
||||
header: `Peer ${idx + 1} config`,
|
||||
value: cfg,
|
||||
downloadName: `peer-${idx + 1}.conf`,
|
||||
});
|
||||
if (wireguardLinks[idx]) {
|
||||
items.push({
|
||||
key: `wl${idx}`,
|
||||
header: `Peer ${idx + 1} link`,
|
||||
value: wireguardLinks[idx],
|
||||
showQr: false,
|
||||
const pushTunnelPeers = (prefix: string, configs: string[][], peerLinks: string[][]) => {
|
||||
configs.forEach((peerConfigs, idx) => {
|
||||
peerConfigs.forEach((cfg, j) => {
|
||||
const multi = peerConfigs.length > 1 ? ` #${j + 1}` : '';
|
||||
items.push({
|
||||
key: `${prefix}c${idx}-${j}`,
|
||||
header: `Peer ${idx + 1} config${multi}`,
|
||||
value: cfg,
|
||||
downloadName: peerConfFileName(idx, j, peerConfigs.length),
|
||||
});
|
||||
const link = peerLinks[idx]?.[j];
|
||||
if (link) {
|
||||
items.push({
|
||||
key: `${prefix}l${idx}-${j}`,
|
||||
header: `Peer ${idx + 1} link${multi}`,
|
||||
value: link,
|
||||
showQr: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
amneziawgConfigs.forEach((cfg, idx) => {
|
||||
items.push({
|
||||
key: `ac${idx}`,
|
||||
header: `Peer ${idx + 1} config`,
|
||||
value: cfg,
|
||||
downloadName: `peer-${idx + 1}.conf`,
|
||||
});
|
||||
if (amneziawgLinks[idx]) {
|
||||
items.push({
|
||||
key: `al${idx}`,
|
||||
header: `Peer ${idx + 1} link`,
|
||||
value: amneziawgLinks[idx],
|
||||
showQr: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
pushTunnelPeers('w', wireguardConfigs, wireguardLinks);
|
||||
pushTunnelPeers('a', amneziawgConfigs, amneziawgLinks);
|
||||
return items;
|
||||
}, [
|
||||
subLink,
|
||||
|
||||
Reference in New Issue
Block a user