Quellcode durchsuchen

fix(sub): emit host TLS verification at xray level in JSON subscription

The JSON subscription flattens tlsSettings into xray's client shape before
the per-host loop, then applyExternalProxyTLSToStream writes the host's
echConfigList, verifyPeerCertByName, pinnedPeerCertSha256 and
allowInsecure into the panel-only tlsSettings.settings map. Xray ignores
that map, so a host's ECH (and its pins / verify name) never reached the
client and connections through the host failed.

After the host overrides are applied, lift that map through the same
writer tlsData uses for the inbound's own TLS: fields land at the top of
tlsSettings, pins are joined into the comma string xray parses, and
allowInsecure is dropped exactly as it is for the inbound (removed from
xray). The Clash renderer still reads the nested map and is unchanged.

The helper-level subtest that pinned the nested location as the "json"
shape is replaced by an end-to-end GetJson test on a host-backed inbound.

Closes #6743
MHSanaei vor 1 Tag
Ursprung
Commit
66ef5bbc05
3 geänderte Dateien mit 64 neuen und 11 gelöschten Zeilen
  1. 46 0
      internal/sub/host_sub_test.go
  2. 18 1
      internal/sub/json_service.go
  3. 0 10
      internal/sub/service_test.go

+ 46 - 0
internal/sub/host_sub_test.go

@@ -618,3 +618,49 @@ func TestSub_HostCipherSuitesJSON(t *testing.T) {
 		t.Fatalf("a host with no cipher suites should inherit the inbound's:\n%s", out)
 	}
 }
+
+// Xray reads a client's TLS verification fields at the top of tlsSettings; a
+// nested "settings" map is panel-only shape and xray silently ignores it.
+func TestSub_HostTLSVerificationJSONAtXrayLevel(t *testing.T) {
+	seedSubDB(t)
+	ib := seedSubInbound(t, "s1", "ech", 4461, 1,
+		`{"network":"xhttp","security":"tls","xhttpSettings":{"path":"/"},"tlsSettings":{"serverName":"base.sni","settings":{"fingerprint":"chrome"}}}`)
+	seedHost(t, &model.Host{
+		InboundId: ib.Id, SortOrder: 0, Remark: "ECH", Address: "ech.cdn.com", Port: 443, Security: "tls",
+		EchConfigList: "cloudflare-ech.com+udp://1.1.1.1", VerifyPeerCertByName: "cert.example.com",
+		PinnedPeerCertSha256: []string{"aa11", "bb22"}, AllowInsecure: true,
+	})
+
+	out, _, err := NewSubJsonService("", "", "", "", NewSubService("")).GetJson("s1", "req.example.com", false)
+	if err != nil {
+		t.Fatalf("GetJson: %v", err)
+	}
+	var config map[string]any
+	if err := json.Unmarshal([]byte(out), &config); err != nil {
+		t.Fatalf("unmarshal JSON subscription: %v", err)
+	}
+	outbounds, _ := config["outbounds"].([]any)
+	if len(outbounds) == 0 {
+		t.Fatalf("JSON subscription has no outbounds: %s", out)
+	}
+	outbound, _ := outbounds[0].(map[string]any)
+	stream, _ := outbound["streamSettings"].(map[string]any)
+	tls, _ := stream["tlsSettings"].(map[string]any)
+	want := map[string]any{
+		"serverName":           "base.sni",
+		"fingerprint":          "chrome",
+		"echConfigList":        "cloudflare-ech.com+udp://1.1.1.1",
+		"verifyPeerCertByName": "cert.example.com",
+		"pinnedPeerCertSha256": "aa11,bb22",
+	}
+	for key, value := range want {
+		if tls[key] != value {
+			t.Errorf("tlsSettings.%s = %#v, want %#v", key, tls[key], value)
+		}
+	}
+	for _, key := range []string{"settings", "allowInsecure"} {
+		if _, ok := tls[key]; ok {
+			t.Errorf("tlsSettings.%s must not reach xray: %#v", key, tls)
+		}
+	}
+}

+ 18 - 1
internal/sub/json_service.go

@@ -634,6 +634,7 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
 		security, _ := newStream["security"].(string)
 		if hasExternalProxy {
 			applyExternalProxyTLSToStream(extPrxy, newStream, security)
+			liftHostTLSVerification(newStream)
 		}
 		applyHostStreamOverrides(extPrxy, newStream)
 		if finalmask, ok := newStream["finalmask"].(map[string]any); ok {
@@ -774,6 +775,23 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
 	if cs, ok := tData["cipherSuites"].(string); ok && cs != "" {
 		tlsData["cipherSuites"] = cs
 	}
+	putClientTLSVerification(tlsData, tlsClientSettings)
+	return tlsData
+}
+
+// liftHostTLSVerification moves the host overrides applyExternalProxyTLSToStream
+// wrote into the panel-shaped tlsSettings.settings up to where xray reads them.
+func liftHostTLSVerification(stream map[string]any) {
+	tlsSettings, _ := stream["tlsSettings"].(map[string]any)
+	inner, ok := tlsSettings["settings"].(map[string]any)
+	if !ok {
+		return
+	}
+	delete(tlsSettings, "settings")
+	putClientTLSVerification(tlsSettings, inner)
+}
+
+func putClientTLSVerification(tlsData map[string]any, tlsClientSettings map[string]any) {
 	if ech, ok := tlsClientSettings["echConfigList"].(string); ok && ech != "" {
 		tlsData["echConfigList"] = ech
 	}
@@ -785,7 +803,6 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
 	if pins, ok := pinnedSha256List(tlsClientSettings); ok {
 		tlsData["pinnedPeerCertSha256"] = strings.Join(pins, ",")
 	}
-	return tlsData
 }
 
 func (s *SubJsonService) realityData(rData map[string]any, clientKey string) map[string]any {

+ 0 - 10
internal/sub/service_test.go

@@ -744,16 +744,6 @@ func TestApplyExternalProxy_ECHPropagates(t *testing.T) {
 		}
 	})
 
-	t.Run("json stream settings", func(t *testing.T) {
-		stream := map[string]any{"security": "tls", "tlsSettings": map[string]any{}}
-		ep := map[string]any{"dest": "proxy.example.com", "echConfigList": ech}
-		applyExternalProxyTLSToStream(ep, stream, "tls")
-		settings, _ := stream["tlsSettings"].(map[string]any)["settings"].(map[string]any)
-		if settings["echConfigList"] != ech {
-			t.Fatalf("echConfigList = %v, want %q", settings["echConfigList"], ech)
-		}
-	})
-
 	t.Run("non-tls security drops ech", func(t *testing.T) {
 		params := map[string]string{}
 		ep := map[string]any{"echConfigList": ech}