feat(inbound): Advanced XHTTP and external TLS proxy settings (#4491)

*  Introduce extended XHTTP and external proxy settings

*  Add custom SNI for proxy

*  Add previous changes into React version of app

* fix(sub): isolate per-proxy tlsSettings during external-proxy iteration

cloneMap (Clash) is shallow and `newStream := stream` (JSON) is an alias,
so tlsSettings was shared across iterations. The new applyExternalProxyTLSToStream
mutates it, leaking one proxy's serverName/fingerprint/alpn into the next
(only overwritten when the next proxy explicitly sets the same field).

Add cloneStreamForExternalProxy: shallow clones the top-level stream plus
deep clones tlsSettings and tlsSettings.settings. Regression test locks
in that proxy B does not inherit proxy A's fingerprint/alpn when B leaves
them unset.
This commit is contained in:
Maksim Alekseev
2026-05-24 22:54:26 +03:00
committed by GitHub
parent cfe1b25ca0
commit 1f90d2a6ee
9 changed files with 553 additions and 224 deletions
+18 -2
View File
@@ -122,7 +122,8 @@ func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client
defaultDest = host
}
externalProxies, ok := stream["externalProxy"].([]any)
if !ok || len(externalProxies) == 0 {
hasExternalProxy := ok && len(externalProxies) > 0
if !hasExternalProxy {
externalProxies = []any{map[string]any{
"forceTls": "same",
"dest": defaultDest,
@@ -138,7 +139,7 @@ func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client
workingInbound := *inbound
workingInbound.Listen = extPrxy["dest"].(string)
workingInbound.Port = int(extPrxy["port"].(float64))
workingStream := cloneMap(stream)
workingStream := cloneStreamForExternalProxy(stream)
switch extPrxy["forceTls"].(string) {
case "tls":
@@ -153,6 +154,10 @@ func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client
delete(workingStream, "realitySettings")
}
}
security, _ := workingStream["security"].(string)
if hasExternalProxy {
applyExternalProxyTLSToStream(extPrxy, workingStream, security)
}
proxy := s.buildProxy(&workingInbound, client, workingStream, extPrxy["remark"].(string))
if len(proxy) > 0 {
@@ -383,6 +388,17 @@ func (s *SubClashService) applySecurity(proxy map[string]any, security string, s
if fingerprint, ok := tlsSettings["fingerprint"].(string); ok && fingerprint != "" {
proxy["client-fingerprint"] = fingerprint
}
if alpn, ok := externalProxyALPNList(tlsSettings["alpn"]); ok {
out := make([]string, 0, len(alpn))
for _, item := range alpn {
if s, ok := item.(string); ok && s != "" {
out = append(out, s)
}
}
if len(out) > 0 {
proxy["alpn"] = out
}
}
}
return true
case "reality":