mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-06 06:02:09 +03:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user