fix(wireguard): check every line of generated client configs

The WireGuard and AmneziaWG .conf builders (subscription service, clients
page, inbound modal and subscription page) checked only a few of the values
they write. Each builder now assembles the file line by line and renders
nothing when a line holds a control character, so every value is covered
whatever stored it. A decoded vpn:// payload is refused when it carries a
wg-quick hook directive, which no panel builder writes, and the subscription
page hides a config block that rendered nothing.

The inbound form now validates a WireGuard/AmneziaWG client's allowedIPs on
create and on update, including the stored clients a protocol switch keeps,
as the client endpoints already did.
This commit is contained in:
MHSanaei
2026-10-24 01:10:09 +02:00
parent 57fd61790c
commit 0faad4f4f3
12 changed files with 285 additions and 222 deletions
@@ -2,6 +2,7 @@ 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';
import { joinConfLines } from '@/lib/xray/conf-lines';
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
// AmneziaWG clients are wire-identical to WireGuard clients (same
@@ -58,13 +59,7 @@ export function buildAmneziaWGClientConfig(
.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.
const privateKey = client.privateKey || client.password || '';
for (const v of [privateKey, server?.primaryDns ?? '', server?.secondaryDns ?? '', remark]) {
if (/[\r\n]/.test(v)) return '';
}
const dnsParts = [server?.primaryDns, server?.secondaryDns].filter((v) => !!v && v.trim() !== '');
const lines = ['[Interface]', `PrivateKey = ${privateKey}`, `Address = ${address}`];
if (dnsParts.length > 0) lines.push(`DNS = ${dnsParts.join(', ')}`);
@@ -109,5 +104,5 @@ export function buildAmneziaWGClientConfig(
lines.push('AllowedIPs = 0.0.0.0/0, ::/0', `Endpoint = ${endpoint}`);
if (client.keepAlive && client.keepAlive > 0)
lines.push(`PersistentKeepalive = ${client.keepAlive}`);
return lines.join('\n');
return joinConfLines(lines);
}
@@ -1,5 +1,6 @@
import type { HostEndpoint } from '@/lib/hosts/host-link';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { joinConfLines } from '@/lib/xray/conf-lines';
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
@@ -54,5 +55,5 @@ export function buildWireguardClientConfig(
lines.push('AllowedIPs = 0.0.0.0/0, ::/0', `Endpoint = ${endpoint}`);
if (client.keepAlive && client.keepAlive > 0)
lines.push(`PersistentKeepalive = ${client.keepAlive}`);
return lines.join('\n');
return joinConfLines(lines);
}
+6 -5
View File
@@ -34,7 +34,8 @@ export default function SubConfigsTab({ links, onCopy }: SubConfigsTabProps) {
const parts = parseLinkParts(link);
const rowTitle = parts?.remark || `Link ${idx + 1}`;
const isWireguardLink = link.startsWith('wireguard://') || link.startsWith('wg://');
const isAmneziawgLink = link.startsWith('vpn://');
const wireguardConfig = isWireguardLink ? wireguardConfigFromLink(link, rowTitle) : '';
const amneziawgConfig = link.startsWith('vpn://') ? amneziawgConfigFromLink(link) : '';
return (
<Fragment key={link}>
<div className="sub-row">
@@ -54,19 +55,19 @@ export default function SubConfigsTab({ links, onCopy }: SubConfigsTabProps) {
)}
</div>
</div>
{isWireguardLink && (
{wireguardConfig && (
<ConfigBlock
label={t('pages.clients.wireguardConfig')}
text={wireguardConfigFromLink(link, rowTitle)}
text={wireguardConfig}
fileName={`${rowTitle || 'peer'}.conf`}
qrRemark={rowTitle}
tagColor="cyan"
/>
)}
{isAmneziawgLink && (
{amneziawgConfig && (
<ConfigBlock
label={t('pages.clients.amneziaWgConfig')}
text={amneziawgConfigFromLink(link)}
text={amneziawgConfig}
fileName={`${rowTitle || 'peer'}.conf`}
qrRemark={rowTitle}
tagColor="purple"