mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-06 18:27:14 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user