fix(link): read the vmess certificate checks on import (#6507)

* fix(link): read the vmess certificate checks on import

applyVmessTLSParams writes ech, vcn and pcs into the vmess share object, but
parseVmess only read sni, fp and alpn back. Importing a link the panel had just
exported therefore dropped all three: no pinned certificate, no verify-by-name,
no ECH. On a server whose certificate is only trusted through a pin, the
imported outbound falls back to public-CA verification against the system roots
and cannot connect to the inbound the link came from.

The url-param protocols already read the same three in applySecurity, and the
core takes pinnedPeerCertSha256 as one joined string there, so the vmess path
now fills them the same way.

* fix(frontend): read the vmess certificate checks on import

The panel parses share links twice: link.ParseLink in Go and
parseVmessLink in outbound-link-parser.ts, which is what the Add Outbound
button calls. Reading ech, vcn and pcs in Go alone would have made the two
sides disagree on one link, leaving the UI path — the one an operator uses by
hand — still dropping the pin the panel had just exported.
This commit is contained in:
BlindMaster24
2026-09-13 20:58:10 +03:00
committed by GitHub
parent 2fcd28c1bc
commit f3dba07e13
4 changed files with 98 additions and 0 deletions
@@ -458,6 +458,11 @@ export function parseVmessLink(link: string): Raw | null {
tls.serverName = json.sni ?? '';
tls.fingerprint = json.fp ?? '';
if (json.alpn) tls.alpn = (json.alpn as string).split(',');
// The vmess object names the certificate checks the url-param protocols
// pass through applySecurityParams, under the same short names.
if (typeof json.ech === 'string') tls.echConfigList = json.ech;
if (typeof json.vcn === 'string') tls.verifyPeerCertByName = json.vcn;
if (typeof json.pcs === 'string') tls.pinnedPeerCertSha256 = json.pcs;
}
const port = Number(json.port) || 443;
@@ -57,6 +57,34 @@ describe('parseVmessLink', () => {
expect((stream.tlsSettings as Record<string, unknown>).alpn).toEqual(['h2', 'http/1.1']);
});
// The exporter writes ech/vcn/pcs into the vmess object, so the importer has
// to read them instead of leaving the tls checks it seeded empty.
it('keeps the ech, vcn and pcs certificate checks', () => {
const json = {
v: '2',
ps: 'pinned-vmess',
add: '1.2.3.4',
port: 8443,
id: '11111111-2222-4333-8444-555555555555',
scy: 'auto',
net: 'tcp',
tls: 'tls',
sni: 'vmess.example.com',
fp: 'chrome',
ech: 'AEX+DQBB',
vcn: 'vcn.example.com',
pcs: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
};
const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
const out = parseVmessLink(link);
expect(out).not.toBeNull();
const stream = out?.streamSettings as Record<string, unknown>;
const tls = stream.tlsSettings as Record<string, unknown>;
expect(tls.echConfigList).toBe('AEX+DQBB');
expect(tls.verifyPeerCertByName).toBe('vcn.example.com');
expect(tls.pinnedPeerCertSha256).toBe('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=');
});
it('returns null for non-vmess links', () => {
expect(parseVmessLink('vless://x@y:1')).toBeNull();
});