From adad6afb8ea616258d012883b4cb936eef98f964 Mon Sep 17 00:00:00 2001 From: Toximiner Date: Mon, 12 Oct 2026 22:13:53 +0300 Subject: [PATCH] fix(sub): carry a reality host's fingerprint into Clash and JSON applyHostStreamOverrides copied a reality host's SNI into the client stream but not its fingerprint, so the vless:// link for a host said fp=firefox while the Clash proxy and JSON outbound for the same host kept the inbound's chrome. Apply the host fingerprint next to the SNI. --- internal/sub/host_sub.go | 3 +++ internal/sub/host_sub_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/internal/sub/host_sub.go b/internal/sub/host_sub.go index 6dabec503..a8976a865 100644 --- a/internal/sub/host_sub.go +++ b/internal/sub/host_sub.go @@ -170,6 +170,9 @@ func applyHostStreamOverrides(ep map[string]any, stream map[string]any) { if sni, ok := externalProxySNI(ep); ok { rs["serverName"] = sni } + if fp, ok := ep["fingerprint"].(string); ok && fp != "" { + rs["fingerprint"] = fp + } } } } diff --git a/internal/sub/host_sub_test.go b/internal/sub/host_sub_test.go index 6cd02ba0b..e26e1e85b 100644 --- a/internal/sub/host_sub_test.go +++ b/internal/sub/host_sub_test.go @@ -516,6 +516,38 @@ func TestSub_HostRealitySniJSONAndClash(t *testing.T) { } } +// A reality host's fingerprint must reach JSON and Clash like it reaches the raw +// link; otherwise one host gets fp=firefox in vless:// but chrome in Clash. +func TestSub_HostRealityFingerprintJSONAndClash(t *testing.T) { + seedSubDB(t) + realityStream := `{"network":"tcp","security":"reality","tcpSettings":{"header":{"type":"none"}},"realitySettings":{"serverNames":["base.reality.com"],"shortIds":["abcd"],"settings":{"publicKey":"PBK","fingerprint":"chrome"}}}` + ib := seedSubInbound(t, "s1", "rlf", 4492, 1, realityStream) + seedHost(t, &model.Host{ + InboundId: ib.Id, SortOrder: 0, Remark: "RLF", Address: "rl.cdn.com", Port: 8443, + Security: "reality", Sni: "host.reality.com", Fingerprint: "firefox", + }) + + out, _, err := NewSubJsonService("", "", "", "", NewSubService("")).GetJson("s1", "req.example.com", false) + if err != nil { + t.Fatalf("GetJson: %v", err) + } + var doc map[string]any + if err := json.Unmarshal([]byte(out), &doc); err != nil { + t.Fatalf("a single-config subscription should be one JSON object: %v\n%s", err, out) + } + if got := proxyRealitySettings(t, doc)["fingerprint"]; got != "firefox" { + t.Fatalf("json reality fingerprint = %v, want the host's firefox", got) + } + + yaml, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("s1", "req.example.com") + if err != nil { + t.Fatalf("GetClash: %v", err) + } + if !strings.Contains(yaml, "client-fingerprint: firefox") { + t.Fatalf("clash proxy should carry the host's fingerprint:\n%s", yaml) + } +} + func proxyRealitySettings(t *testing.T, doc map[string]any) map[string]any { t.Helper() outbounds, _ := doc["outbounds"].([]any)