mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-08 19: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',
|
wireguard: 'WireGuard',
|
||||||
wg: 'WireGuard',
|
wg: 'WireGuard',
|
||||||
tg: 'MTProto',
|
tg: 'MTProto',
|
||||||
|
vpn: 'AmneziaWG',
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROTOCOL_COLORS: Record<string, string> = {
|
const PROTOCOL_COLORS: Record<string, string> = {
|
||||||
@@ -37,6 +38,7 @@ const PROTOCOL_COLORS: Record<string, string> = {
|
|||||||
Hysteria2: 'magenta',
|
Hysteria2: 'magenta',
|
||||||
WireGuard: 'cyan',
|
WireGuard: 'cyan',
|
||||||
MTProto: 'blue',
|
MTProto: 'blue',
|
||||||
|
AmneziaWG: 'yellow',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SECURITY_COLORS: Record<string, string> = {
|
const SECURITY_COLORS: Record<string, string> = {
|
||||||
@@ -50,6 +52,18 @@ const TRANSPORT_COLOR = 'gold';
|
|||||||
|
|
||||||
const TAG_STYLE = { marginInlineEnd: 0, fontWeight: 600, letterSpacing: '0.3px' };
|
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
|
/* Pull protocol, transport, security plus the remark and port out of a share
|
||||||
link. vless/trojan carry network+security as `type`/`security` query params
|
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
|
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 : '';
|
remark = typeof json.ps === 'string' ? json.ps : '';
|
||||||
port = json.port != null ? String(json.port) : '';
|
port = json.port != null ? String(json.port) : '';
|
||||||
} catch { /* unparseable payload, fall back to protocol only */ }
|
} 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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
const url = new URL(trimmed);
|
const url = new URL(trimmed);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import { parseLinkParts, linkMetaText } from '@/lib/xray/link-label';
|
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
|
// 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
|
// 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?.remark).toBe('mt-inbound');
|
||||||
expect(parts && linkMetaText(parts)).toBe('mt-inbound:8443');
|
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