feat(wireguard): client config UX, collapsible config card, configurable DNS

Land the WireGuard client-config UX work on main (the upstream PR #5642
branch could not be pushed to).

- Reusable collapsible ConfigBlock (copy/download/QR, actions aligned right)
  for the client .conf, used by client info and the public sub page.
- Correct .conf: canonical PresharedKey casing and DNS sourced from the inbound
  (configurable per-inbound, default 1.1.1.1, 1.0.0.1).
- Configurable per-inbound DNS for WireGuard (schema + form + backend hint via
  InboundOption.WgDns); inert at the Xray layer.
- Public sub page now shows the WireGuard config, rebuilt from the share link;
  the Go wireguard:// link carries dns/presharedkey/keepalive for completeness.
- QR enabled for the wireguard:// link; link rows are compact like other protocols.
- Client information order is subscription, copy URL, WireGuard config; the
  redundant config tab is removed from the add/edit client modal.
- Drop the Inbound Information and QR Code row actions for WireGuard inbounds.
This commit is contained in:
MHSanaei
2026-06-29 00:50:34 +02:00
parent 60c54827aa
commit a329882e0e
26 changed files with 509 additions and 57 deletions
@@ -25,6 +25,9 @@ export default function WireguardFields({ wgPubKey, regenInboundWg }: WireguardF
<Form.Item name={['settings', 'mtu']} label="MTU">
<InputNumber />
</Form.Item>
<Form.Item name={['settings', 'dns']} label={t('pages.inbounds.info.dns')}>
<Input placeholder="1.1.1.1, 1.0.0.1" />
</Form.Item>
<Form.Item
name={['settings', 'noKernelTun']}
label={t('pages.inbounds.info.noKernelTun')}
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils } from '@/utils';
import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils, Wireguard } from '@/utils';
import { Protocols } from '@/schemas/primitives';
import { InfinityIcon } from '@/components/ui';
import { useDatepicker } from '@/hooks/useDatepicker';
@@ -208,6 +208,11 @@ export default function InboundInfoModal({
return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
}, [clientStats, clientSettings]);
const wgPubKey = useMemo(() => {
if (!dbInbound?.isWireguard || !inbound?.settings?.secretKey) return '';
return Wireguard.generateKeypair(inbound.settings.secretKey as string).publicKey;
}, [dbInbound?.isWireguard, inbound?.settings?.secretKey]);
const formatLastOnline = useCallback(
(email: string) => {
const ts = lastOnlineMap[email];
@@ -791,7 +796,7 @@ export default function InboundInfoModal({
</div>
<div className="info-row">
<dt>{t('pages.xray.wireguard.publicKey')}</dt>
<dd><Tag className="value-tag">{inbound.settings.pubKey as string}</Tag></dd>
<dd><Tag className="value-tag">{wgPubKey}</Tag></dd>
</div>
<div className="info-row">
<dt>{t('pages.inbounds.info.mtu')}</dt>
@@ -43,7 +43,7 @@ export function buildRowActionsMenu({ record, subEnable, t, isMobile, hasClients
label: `${t('pages.inbounds.export')}${t('pages.settings.subSettings')}`,
});
}
} else {
} else if (!record.isWireguard) {
items.push({ key: 'showInfo', icon: <InfoCircleOutlined />, label: t('pages.inbounds.inboundInfo') });
}
items.push({ key: 'clipboard', icon: <CopyOutlined />, label: t('pages.inbounds.exportInbound') });
@@ -81,7 +81,6 @@ export function isInboundMultiUser(record: { protocol: string; settings: unknown
}
export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
if (dbInbound.isWireguard) return true;
if (dbInbound.isSS) {
return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
}
@@ -7,6 +7,7 @@ import { SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
import { InfinityIcon } from '@/components/ui';
import { useDatepicker } from '@/hooks/useDatepicker';
import type { NodeRecord } from '@/api/queries/useNodesQuery';
import { coerceInboundJsonField } from '@/models/dbinbound';
import { RowActionsCell } from './RowActions';
import { InboundSpeedTag, isActiveSpeed } from './InboundSpeedTag';
@@ -51,6 +52,28 @@ export function useInboundColumns({
const { datepicker } = useDatepicker();
return useMemo(() => {
const fallbackClientCount = (record: DBInboundRecord): ClientCountEntry | null => {
const settings = coerceInboundJsonField(record.settings) as {
clients?: { email?: string; enable?: boolean }[];
};
const clients = Array.isArray(settings.clients) ? settings.clients : [];
if (clients.length === 0) return null;
const active = clients
.filter((client) => client.email && client.enable !== false)
.map((client) => client.email!);
const deactive = clients
.filter((client) => client.email && client.enable === false)
.map((client) => client.email!);
return {
clients: clients.length,
active,
deactive,
depleted: [],
expiring: [],
online: [],
};
};
const cols: TableColumnType<DBInboundRecord>[] = [
{
title: 'ID',
@@ -174,14 +197,14 @@ export function useInboundColumns({
align: 'left',
width: 200,
render: (_, record) => {
const cc = clientCount[record.id];
const cc = clientCount[record.id] || fallbackClientCount(record);
if (!cc) return null;
return (
<>
<Tag className="client-count-tag" style={{ margin: 0, marginRight: 4, padding: '0 2px' }}>
<TeamOutlined /> {cc.clients}
</Tag>
{cc.active.length > 0 && (
{cc.active.length > 0 ? (
<Popover
title={t('subscription.active')}
content={(
@@ -192,6 +215,8 @@ export function useInboundColumns({
>
<Tag color="green" className="client-count-tag" style={{ margin: 0, marginRight: 4, padding: '0 2px' }}>{cc.active.length}</Tag>
</Popover>
) : (
<Tag color="green" className="client-count-tag" style={{ margin: 0, marginRight: 4, padding: '0 2px' }}>0</Tag>
)}
{cc.deactive.length > 0 && (
<Popover
@@ -35,6 +35,7 @@ interface QrItem {
header: string;
value: string;
downloadName?: string;
showQr?: boolean;
}
export default function QrCodeModal({
@@ -122,7 +123,7 @@ export default function QrCodeModal({
downloadName: `peer-${idx + 1}.conf`,
});
if (wireguardLinks[idx]) {
items.push({ key: `wl${idx}`, header: `Peer ${idx + 1} link`, value: wireguardLinks[idx] });
items.push({ key: `wl${idx}`, header: `Peer ${idx + 1} link`, value: wireguardLinks[idx], showQr: false });
}
});
return items;
@@ -137,7 +138,7 @@ export default function QrCodeModal({
value={item.value}
remark={item.header}
downloadName={item.downloadName || ''}
showQr={!isPostQuantumLink(item.value)}
showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
/>
),
})),