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) + } +}