fix(frontend): add the missing AmneziaWG config download on the sub page

The subscription page already gave WireGuard links their own "Config"
block (copy/download/QR of the actual .conf, via wireguardConfigFromLink
reversing the wireguard:// query params) but had no equivalent for
AmneziaWG's vpn:// links -- its isWireguardLink gate never matched them,
and no reverse-parse helper existed for this page specifically. Every
other surface (InboundInfoModal, ClientInfoModal, ClientQrModal) already
had this parity; this was the one page that didn't.

Fixed by adding amneziawgConfigFromLink (inbound-link.ts), simpler than
its WireGuard counterpart since a vpn:// payload already *is* the plain
.conf text -- just base64url-decode it, no query-param reconstruction
needed -- and wiring it into SubPage.tsx alongside the existing WireGuard
block, reusing the same pages.clients.amneziaWgConfig label the other
three surfaces already use.
This commit is contained in:
Kuzz007
2026-08-03 21:22:00 +03:00
parent 4a9c2e0b04
commit 966bab0d71
3 changed files with 63 additions and 1 deletions
+28
View File
@@ -1064,6 +1064,34 @@ export function wireguardConfigFromLink(link: string, fallbackRemark = ''): stri
return lines.join('\n');
}
// Reverse of toBase64Url above -- recovers a vpn:// link's plain .conf
// payload for display/copy/download/QR, the AmneziaWG counterpart of
// wireguardConfigFromLink. Simpler than that function: a vpn:// link's
// payload already *is* the .conf text (see genAmneziaWGLink's own doc
// comment), so there's nothing to reconstruct from query params -- just
// decode. Mirrors link-label.tsx's own private fromBase64Url (used there
// only to pull the remark/port back out for the tag label); duplicated
// rather than imported since both are tiny, self-contained, and each
// file already owns the matching encode or decode half of this pair.
function fromBase64Url(value: string): string {
const b64 = value.replace(/-/g, '+').replace(/_/g, '/');
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return new TextDecoder().decode(bytes);
}
export function amneziawgConfigFromLink(link: string): string {
const trimmed = link.trim();
if (!trimmed.startsWith('vpn://')) return '';
try {
return fromBase64Url(trimmed.slice('vpn://'.length));
} catch {
return '';
}
}
export type { WireguardInboundPeer };
function isUnixSocketListen(listen: string): boolean {
+11 -1
View File
@@ -33,7 +33,7 @@ import {
} from '@ant-design/icons';
import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils';
import { isPostQuantumLink, wireguardConfigFromLink } from '@/lib/xray/inbound-link';
import { amneziawgConfigFromLink, isPostQuantumLink, wireguardConfigFromLink } from '@/lib/xray/inbound-link';
import { LinkTags, parseLinkParts } from '@/lib/xray/link-label';
import ConfigBlock from '@/components/clients/ConfigBlock';
import { setMessageInstance } from '@/utils/messageBus';
@@ -451,6 +451,7 @@ export default function SubPage() {
const qrLabel = parts?.remark || rowTitle;
const canQr = !isPostQuantumLink(link);
const isWireguardLink = link.startsWith('wireguard://') || link.startsWith('wg://');
const isAmneziawgLink = link.startsWith('vpn://');
return (
<Fragment key={link}>
<div className="sub-link-row">
@@ -506,6 +507,15 @@ export default function SubPage() {
tagColor="cyan"
/>
)}
{isAmneziawgLink && (
<ConfigBlock
label={t('pages.clients.amneziaWgConfig')}
text={amneziawgConfigFromLink(link)}
fileName={`${rowTitle || 'peer'}.conf`}
qrRemark={rowTitle}
tagColor="purple"
/>
)}
</Fragment>
);
})}
+24
View File
@@ -2,6 +2,7 @@
import { describe, expect, it } from 'vitest';
import {
amneziawgConfigFromLink,
genAmneziaWGConfig,
genAmneziaWGLink,
genHysteriaLink,
@@ -409,6 +410,29 @@ describe('genAmneziaWGLink vpn:// scheme', () => {
it('returns an empty string when the peer index has no client', () => {
expect(genAmneziaWGLink({ ...input, peerIndex: 5 })).toBe('');
});
// The subscription page's own reverse of the above: recovers a vpn://
// link's .conf text for the same copy/download/QR "Config" block
// WireGuard already gets there (wireguardConfigFromLink's AmneziaWG
// counterpart) -- found missing from that page in production (no
// download-config affordance for AmneziaWG links, unlike WireGuard's),
// even though every other surface in the panel (InboundInfoModal,
// ClientInfoModal, ClientQrModal) already had parity.
it('amneziawgConfigFromLink round-trips genAmneziaWGLink byte-identical to genAmneziaWGConfig', () => {
const link = genAmneziaWGLink(input);
expect(amneziawgConfigFromLink(link)).toBe(genAmneziaWGConfig(input));
});
});
describe('amneziawgConfigFromLink edge cases', () => {
it('returns an empty string for a non-vpn:// link', () => {
expect(amneziawgConfigFromLink('wireguard://abc')).toBe('');
expect(amneziawgConfigFromLink('')).toBe('');
});
it('returns an empty string for an unparseable vpn:// payload', () => {
expect(amneziawgConfigFromLink('vpn://not-valid-base64url!!!')).toBe('');
});
});
describe('resolveAddr precedence', () => {