From d6cddaff12c54a0d3020700008ebd192ac087117 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 17 Jun 2026 17:07:10 +0200 Subject: [PATCH] fix(sub): emit JSON-subscription pinnedPeerCertSha256 as comma-separated string xray-core now parses tlsSettings.pinnedPeerCertSha256 as a comma-separated string rather than a []string array. The JSON subscription still emitted the array form, which current xray-core-backed v2ray clients reject on import. Join the panel's stored pins into the string form, matching the raw share-link path (pcs/pinSHA256). Fixes #5401. --- internal/sub/json_service.go | 6 ++++-- internal/sub/json_service_test.go | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 0cb943ac5..38c3b2dbc 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -297,8 +297,10 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any { if ech, ok := tlsClientSettings["echConfigList"].(string); ok && ech != "" { tlsData["echConfigList"] = ech } - if pins, ok := tlsClientSettings["pinnedPeerCertSha256"].([]any); ok && len(pins) > 0 { - tlsData["pinnedPeerCertSha256"] = pins + // xray-core now parses pinnedPeerCertSha256 as a comma-separated string, not + // an array; emit the joined form so v2ray clients can import the config (#5401). + if pins, ok := pinnedSha256List(tlsClientSettings); ok { + tlsData["pinnedPeerCertSha256"] = strings.Join(pins, ",") } return tlsData } diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index e7daf34d1..36b86bf4c 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -102,6 +102,22 @@ func TestSubJsonServiceNoFinalMaskWhenEmpty(t *testing.T) { } } +// xray-core parses tlsSettings.pinnedPeerCertSha256 as a comma-separated string; +// the JSON subscription must emit that form, not an array, or v2ray clients fail +// to import the config (#5401). +func TestSubJsonServicePinnedCertJoinedToString(t *testing.T) { + svc := NewSubJsonService("", "", "", nil) + stream := svc.streamData(`{"network":"tcp","security":"tls","tlsSettings":{"serverName":"a.example.com","settings":{"pinnedPeerCertSha256":["aa11","bb22"]}}}`) + + tls, _ := stream["tlsSettings"].(map[string]any) + if tls == nil { + t.Fatalf("tlsSettings missing: %#v", stream) + } + if got := tls["pinnedPeerCertSha256"]; got != "aa11,bb22" { + t.Fatalf("pinnedPeerCertSha256 = %#v, want comma-separated string \"aa11,bb22\"", got) + } +} + func TestSubJsonServiceVlessFlattened(t *testing.T) { inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`} client := model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}