Quellcode durchsuchen

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.
BlindMaster24 vor 7 Stunden
Ursprung
Commit
f3dba07e13

+ 5 - 0
frontend/src/lib/xray/outbound-link-parser.ts

@@ -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;

+ 28 - 0
frontend/src/test/outbound-link-parser.test.ts

@@ -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();
   });

+ 60 - 0
internal/sub/vmess_tls_import_test.go

@@ -0,0 +1,60 @@
+package sub
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/link"
+)
+
+// The vmess object carries the certificate checks the panel exported, so
+// importing that same link has to rebuild them instead of dropping them.
+func TestVmessTLSVerifyFieldsSurviveExportImport(t *testing.T) {
+	in := &model.Inbound{
+		Id: 940002, Listen: "203.0.113.1", Port: 8443, Protocol: model.VMESS,
+		Settings: `{"clients":[{"id":"11111111-2222-4333-8444-555555555555","email":"user"}]}`,
+		StreamSettings: `{"network":"tcp","security":"tls","tcpSettings":{"header":{"type":"none"}},` +
+			`"tlsSettings":{"serverName":"vmess.example.com","alpn":["h2"],"settings":{` +
+			`"fingerprint":"chrome","echConfigList":"AEX+DQBB","verifyPeerCertByName":"vcn.example.com",` +
+			`"pinnedPeerCertSha256":["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="]}}}`,
+	}
+	exported := (&SubService{}).genVmessLink(in, "user")
+	if exported == "" {
+		t.Fatal("genVmessLink produced nothing")
+	}
+
+	parsed, err := link.ParseLink(exported)
+	if err != nil {
+		t.Fatalf("ParseLink: %v", err)
+	}
+	raw, err := json.Marshal(parsed.Outbound["streamSettings"])
+	if err != nil {
+		t.Fatalf("marshal stream: %v", err)
+	}
+	var stream map[string]any
+	if err := json.Unmarshal(raw, &stream); err != nil {
+		t.Fatalf("stream json: %v", err)
+	}
+	tlsSettings, _ := stream["tlsSettings"].(map[string]any)
+	if tlsSettings == nil {
+		t.Fatalf("no tlsSettings: %s", raw)
+	}
+
+	// The core reads these joined strings in its config, as applySecurity writes them.
+	for field, want := range map[string]string{
+		"serverName":           "vmess.example.com",
+		"fingerprint":          "chrome",
+		"echConfigList":        "AEX+DQBB",
+		"verifyPeerCertByName": "vcn.example.com",
+		"pinnedPeerCertSha256": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+	} {
+		if tlsSettings[field] != want {
+			t.Errorf("tlsSettings[%q] = %v, want %q", field, tlsSettings[field], want)
+		}
+	}
+	alpn, _ := tlsSettings["alpn"].([]any)
+	if len(alpn) != 1 || alpn[0] != "h2" {
+		t.Errorf("alpn = %v, want [h2]", tlsSettings["alpn"])
+	}
+}

+ 5 - 0
internal/util/link/outbound.go

@@ -205,6 +205,11 @@ func parseVmess(link string) (*ParseResult, error) {
 		if alpn := getString(j, "alpn", ""); alpn != "" {
 			tls["alpn"] = splitComma(alpn)
 		}
+		// The vmess object names the certificate checks v2rayN does the same way
+		// the url-param protocols name them in applySecurity.
+		tls["echConfigList"] = getString(j, "ech", "")
+		tls["verifyPeerCertByName"] = getString(j, "vcn", "")
+		tls["pinnedPeerCertSha256"] = getString(j, "pcs", "")
 	}
 
 	port := num(j["port"])