diff --git a/internal/sub/host_sub_test.go b/internal/sub/host_sub_test.go index 55f2b7c4e..6cd02ba0b 100644 --- a/internal/sub/host_sub_test.go +++ b/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) + } + } +} diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 91f9d178e..fd615b2f8 100644 --- a/internal/sub/json_service.go +++ b/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 { diff --git a/internal/sub/service_test.go b/internal/sub/service_test.go index 38e2f5545..4036e9d91 100644 --- a/internal/sub/service_test.go +++ b/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}