mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-20 19:01:17 +00:00
fix(sub): give every host endpoint its own deep copy of the stream
cloneStreamForExternalProxy deep-copied only tlsSettings, so the hosts of one inbound shared realitySettings and wsSettings/xhttpSettings within a request. applyHostStreamOverrides writes SNI, fingerprint, Host and path into those maps in place, so a host that leaves one blank inherited the previous host's value in Clash and JSON while its vless:// link kept the inbound's. Deep-copy the whole stream per endpoint.
This commit is contained in:
parent
3660650c4b
commit
0aaba17d59
2 changed files with 136 additions and 14 deletions
|
|
@ -550,6 +550,114 @@ func TestSub_HostRealityFingerprintJSONAndClash(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Hosts of one inbound render from per-host copies of the same stream; a host
|
||||
// that leaves an override blank must get the inbound's value, not the previous host's.
|
||||
func TestSub_HostBlankOverridesDoNotLeakBetweenHosts(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
stream string
|
||||
first model.Host
|
||||
clashWant map[string]string
|
||||
jsonField func(stream map[string]any) any
|
||||
jsonWant any
|
||||
}{
|
||||
{
|
||||
name: "reality fingerprint and sni",
|
||||
stream: mihomoRealityStream,
|
||||
first: model.Host{Security: "reality", Sni: "a.reality.com", Fingerprint: "firefox"},
|
||||
clashWant: map[string]string{"client-fingerprint": "chrome", "servername": "base.reality.com"},
|
||||
jsonField: func(stream map[string]any) any {
|
||||
rs, _ := stream["realitySettings"].(map[string]any)
|
||||
return rs["fingerprint"]
|
||||
},
|
||||
jsonWant: "chrome",
|
||||
},
|
||||
{
|
||||
name: "ws host header and path",
|
||||
stream: wsTLSStream,
|
||||
first: model.Host{Security: "tls", HostHeader: "a.host", Path: "/a"},
|
||||
clashWant: map[string]string{"ws-opts.path": "/base", "ws-opts.headers.Host": "base.host"},
|
||||
jsonField: func(stream map[string]any) any {
|
||||
ws, _ := stream["wsSettings"].(map[string]any)
|
||||
return ws["path"]
|
||||
},
|
||||
jsonWant: "/base",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
ib := seedSubInbound(t, "s1", "lk", 4494, 1, tc.stream)
|
||||
first := tc.first
|
||||
first.InboundId, first.SortOrder, first.Remark, first.Address, first.Port = ib.Id, 0, "A", "a.cdn.com", 443
|
||||
seedHost(t, &first)
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "B", Address: "b.cdn.com", Port: 443, Security: tc.first.Security})
|
||||
|
||||
out, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
}
|
||||
second := clashProxyByServer(t, out, "b.cdn.com")
|
||||
for field, want := range tc.clashWant {
|
||||
if got := lookupPath(second, field); got != want {
|
||||
t.Errorf("host B clash %s = %v, want the inbound's %q", field, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
out, _, err = NewSubJsonService("", "", "", "", NewSubService("")).GetJson("s1", "req.example.com", false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetJson: %v", err)
|
||||
}
|
||||
var configs []map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &configs); err != nil || len(configs) != 2 {
|
||||
t.Fatalf("want one JSON config per host, got err=%v:\n%s", err, out)
|
||||
}
|
||||
if got := tc.jsonField(proxyStreamSettings(t, configs[1])); got != tc.jsonWant {
|
||||
t.Errorf("host B json value = %v, want the inbound's %v", got, tc.jsonWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func clashProxyByServer(t *testing.T, out, server string) map[string]any {
|
||||
t.Helper()
|
||||
var config struct {
|
||||
Proxies []map[string]any `yaml:"proxies"`
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(out), &config); err != nil {
|
||||
t.Fatalf("decode Clash YAML: %v\n%s", err, out)
|
||||
}
|
||||
for _, proxy := range config.Proxies {
|
||||
if proxy["server"] == server {
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
t.Fatalf("no clash proxy for %s in:\n%s", server, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupPath(m map[string]any, path string) any {
|
||||
var cur any = m
|
||||
for _, key := range strings.Split(path, ".") {
|
||||
next, _ := cur.(map[string]any)
|
||||
cur = next[key]
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
func proxyStreamSettings(t *testing.T, doc map[string]any) map[string]any {
|
||||
t.Helper()
|
||||
outbounds, _ := doc["outbounds"].([]any)
|
||||
for _, ob := range outbounds {
|
||||
outbound, _ := ob.(map[string]any)
|
||||
if outbound["tag"] == "proxy" {
|
||||
stream, _ := outbound["streamSettings"].(map[string]any)
|
||||
return stream
|
||||
}
|
||||
}
|
||||
t.Fatalf("no proxy outbound in %v", doc)
|
||||
return nil
|
||||
}
|
||||
|
||||
const mihomoRealityStream = `{"network":"tcp","security":"reality","tcpSettings":{"header":{"type":"none"}},"realitySettings":{"serverNames":["base.reality.com"],"shortIds":["abcd"],"settings":{"publicKey":"PBK","fingerprint":"chrome"}}}`
|
||||
|
||||
// seedMihomoX25519Hosts puts a default host and a "Mihomo X25519" host on one
|
||||
|
|
|
|||
|
|
@ -2118,24 +2118,38 @@ func applyExternalProxyHysteriaParams(ep map[string]any, params map[string]strin
|
|||
}
|
||||
}
|
||||
|
||||
// cloneStreamForExternalProxy returns a shallow clone of stream with
|
||||
// tlsSettings (and its nested settings map) deep-copied. The external
|
||||
// proxy loop mutates tlsSettings per iteration, so without isolating
|
||||
// those maps each proxy's SNI/fingerprint/ALPN would leak into the next.
|
||||
// cloneStreamForExternalProxy deep-copies stream: every endpoint overrides nested
|
||||
// maps in place, so a shared map leaks one host's SNI/fp/host/path into the next.
|
||||
func cloneStreamForExternalProxy(stream map[string]any) map[string]any {
|
||||
out := cloneMap(stream)
|
||||
ts, ok := out["tlsSettings"].(map[string]any)
|
||||
if !ok || ts == nil {
|
||||
return out
|
||||
}
|
||||
clonedTs := cloneMap(ts)
|
||||
if inner, ok := clonedTs["settings"].(map[string]any); ok && inner != nil {
|
||||
clonedTs["settings"] = cloneMap(inner)
|
||||
}
|
||||
out["tlsSettings"] = clonedTs
|
||||
out, _ := deepCopyJSONValue(stream).(map[string]any)
|
||||
return out
|
||||
}
|
||||
|
||||
func deepCopyJSONValue(v any) any {
|
||||
switch val := v.(type) {
|
||||
case map[string]any:
|
||||
if val == nil {
|
||||
return val
|
||||
}
|
||||
out := make(map[string]any, len(val))
|
||||
for k, item := range val {
|
||||
out[k] = deepCopyJSONValue(item)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
if val == nil {
|
||||
return val
|
||||
}
|
||||
out := make([]any, len(val))
|
||||
for i, item := range val {
|
||||
out[i] = deepCopyJSONValue(item)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func applyExternalProxyTLSToStream(ep map[string]any, stream map[string]any, security string) {
|
||||
if security != "tls" {
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue