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
This commit is contained in:
MHSanaei
2026-10-05 22:26:04 +02:00
parent dd9569478e
commit 66ef5bbc05
3 changed files with 64 additions and 11 deletions
+46
View File
@@ -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
View File
@@ -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 {
-10
View File
@@ -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}