From 82073c10c92094eced427ca49ecaa04f212a9851 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 15 Jul 2026 02:41:49 +0200 Subject: [PATCH] fix(sub): emit the pinned peer cert sha256 in Clash subscriptions The Clash stream builder computed tlsSettings["pin-sha256"] from the inbound's pinnedPeerCertSha256, but applySecurity's tls case never copied it onto the proxy, so it was written with no reader and silently dropped. Clash subscribers lost certificate pinning while JSON subscribers kept it. Surface pin-sha256 on the proxy in the tls case, matching the JSON emitter. --- internal/sub/clash_service.go | 3 +++ internal/sub/sub_panic_test.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/internal/sub/clash_service.go b/internal/sub/clash_service.go index 8147930e3..bff4c088e 100644 --- a/internal/sub/clash_service.go +++ b/internal/sub/clash_service.go @@ -684,6 +684,9 @@ func (s *SubClashService) applySecurity(proxy map[string]any, security string, s proxy["skip-cert-verify"] = true } } + if pins, ok := tlsSettings["pin-sha256"].([]any); ok && len(pins) > 0 { + proxy["pin-sha256"] = pins + } } return true case "reality": diff --git a/internal/sub/sub_panic_test.go b/internal/sub/sub_panic_test.go index e7b16fade..8eb9ccb39 100644 --- a/internal/sub/sub_panic_test.go +++ b/internal/sub/sub_panic_test.go @@ -2,6 +2,7 @@ package sub import ( "fmt" + "strings" "testing" "github.com/gin-gonic/gin" @@ -81,3 +82,21 @@ func TestGetJsonToleratesHysteriaWithoutHysteriaSettings(t *testing.T) { t.Fatal("GetJson returned empty for a hysteria inbound without hysteriaSettings") } } + +// A Clash subscription must carry the pinned peer certificate SHA-256 when the +// inbound configures one, matching the JSON subscription; dropping it silently +// downgrades certificate pinning for Clash subscribers. +func TestGetClashEmitsPinnedCertSha256(t *testing.T) { + seedSubDB(t) + const pin = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + stream := `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"pin.sni","settings":{"pinnedPeerCertSha256":["` + pin + `"]}}}` + seedSubInbound(t, "pin1", "pin", 46300, 1, stream) + + out, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("pin1", "sub.example.com") + if err != nil { + t.Fatalf("GetClash: %v", err) + } + if !strings.Contains(out, "pin-sha256") { + t.Fatalf("Clash proxy dropped the pinned cert sha256:\n%s", out) + } +}