fix(frontend): recognize AmneziaWG's vpn:// scheme in share-link labels

The shared link-tag/label helper (used by the client info modal, QR
modal, and subscription page) had no entry for the vpn:// scheme
AmneziaWG links use, so it fell through to the generic fallback: a
plain "Vpn" tag with no color, and an empty remark/port that made the
row's title fall back to "Link N" instead of the inbound's actual
name:port — unlike every other protocol, which shows its real tag and
label.

vpn:// links are base64url of a plain .conf text (matching the real
AmneziaVPN app's own share-link format), not a structured URL, so
there's no query string or #hash to read a remark/port from. Decode
the payload and pull the remark/endpoint back out of the .conf text
directly instead.
This commit is contained in:
Kuzz007
2026-08-01 23:51:05 +03:00
parent 0ea0e4512d
commit fb93ed772e
2 changed files with 62 additions and 0 deletions
+26
View File
@@ -26,6 +26,7 @@ const PROTOCOL_LABELS: Record<string, string> = {
wireguard: 'WireGuard',
wg: 'WireGuard',
tg: 'MTProto',
vpn: 'AmneziaWG',
};
const PROTOCOL_COLORS: Record<string, string> = {
@@ -37,6 +38,7 @@ const PROTOCOL_COLORS: Record<string, string> = {
Hysteria2: 'magenta',
WireGuard: 'cyan',
MTProto: 'blue',
AmneziaWG: 'yellow',
};
const SECURITY_COLORS: Record<string, string> = {
@@ -50,6 +52,18 @@ const TRANSPORT_COLOR = 'gold';
const TAG_STYLE = { marginInlineEnd: 0, fontWeight: 600, letterSpacing: '0.3px' };
// Reverse of inbound-link.ts's own toBase64Url — base64url (RFC 4648 §5, no
// padding) back to the original unicode text, needed to read the remark/
// endpoint back out of a vpn:// link's opaque payload below.
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);
}
/* Pull protocol, transport, security plus the remark and port out of a share
link. vless/trojan carry network+security as `type`/`security` query params
and the remark in the URL hash; vmess packs them into the base64 JSON as
@@ -81,6 +95,18 @@ export function parseLinkParts(link: string): LinkParts | null {
remark = typeof json.ps === 'string' ? json.ps : '';
port = json.port != null ? String(json.port) : '';
} catch { /* unparseable payload, fall back to protocol only */ }
} else if (scheme === 'vpn') {
/* AmneziaWG's vpn:// links are base64url of a plain .conf text (matching
the real AmneziaVPN app's own share-link scheme), not a structured URL
— there's no query string or #hash to read a remark/port from without
corrupting the payload the app itself needs to decode. The remark and
endpoint are still in there as plain .conf lines, though, so pull them
back out directly. */
try {
const cfgText = fromBase64Url(trimmed.slice('vpn://'.length));
remark = /^#\s?(.*)$/m.exec(cfgText)?.[1]?.trim() ?? '';
port = /^Endpoint\s*=\s*.+:(\d+)\s*$/m.exec(cfgText)?.[1] ?? '';
} catch { /* unparseable payload, fall back to protocol only */ }
} else {
try {
const url = new URL(trimmed);
+36
View File
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest';
import { parseLinkParts, linkMetaText } from '@/lib/xray/link-label';
import { genAmneziaWGLink } from '@/lib/xray/inbound-link';
import type { AmneziawgInboundSettings } from '@/schemas/protocols/inbound/amneziawg';
// The panel shows the subscription's remark verbatim. Per-client traffic/expiry
// info is rendered only into the body a client app imports (backend, first link
@@ -40,4 +42,38 @@ describe('link-label parseLinkParts', () => {
expect(parts?.remark).toBe('mt-inbound');
expect(parts && linkMetaText(parts)).toBe('mt-inbound:8443');
});
// AmneziaWG's vpn:// links are base64url of a plain .conf text, not a
// structured URL (see inbound-link.ts's genAmneziaWGLink) -- there's no
// query string or #hash available, so the remark/port have to be read back
// out of the decoded .conf body instead. Regression test for a real report:
// these links were showing a generic "Vpn" tag and falling back to "Link N"
// instead of "AmneziaWG" + the actual remark:port, unlike every other
// protocol's link row.
it('labels an AmneziaWG vpn:// link with its decoded remark and endpoint port', () => {
const settings = {
server: {
publicKey: 'serverPubKey==',
jc: 5, jmin: 10, jmax: 50, s1: 30, s2: 45, s3: 10, s4: 5,
h1: '', h2: '', h3: '', h4: '', i1: '',
},
clients: [{ email: 'peer-1', privateKey: 'clientPrivKey==', allowedIPs: ['10.8.1.2/32'] }],
} as unknown as AmneziawgInboundSettings;
// Cyrillic remark on purpose -- matches the real report, and exercises
// the unicode round-trip through base64url (not just plain ASCII).
const link = genAmneziaWGLink({
settings,
address: 'awg.example.test',
port: 36541,
remark: 'wg-Майфун',
peerIndex: 0,
});
const parts = parseLinkParts(link);
expect(parts?.protocol).toBe('AmneziaWG');
expect(parts?.remark).toBe('wg-Майфун');
expect(parts?.port).toBe('36541');
expect(parts && linkMetaText(parts)).toBe('wg-Майфун:36541');
});
});