fix(sub): bind JSON local inbounds to 127.0.0.1 and keep mux.cool off Vision outbounds (#6418)

* fix(sub): bind JSON local inbounds to 127.0.0.1 and keep mux.cool off Vision outbounds

The JSON subscription's local SOCKS/HTTP inbounds had no listen address, so
every client that runs the profile verbatim bound an unauthenticated proxy on
0.0.0.0, and iOS packet-tunnel clients could not reach it at all (Happ iOS:
CONNECTED with zero traffic, same symptom as #6379 — on the same device the
mixed inbound also worked once bound to 127.0.0.1). Bind both to loopback,
which is what every client's own generated config does.

The global subJsonMux was also applied to VLESS outbounds carrying
xtls-rprx-vision. XTLS flows do not support mux.cool: Xray answers the mux
handshake with "common/mux: unexpected network TCP" and the tunnel passes
nothing, on every platform (verified with Happ iOS/Android/macOS, V2Box iOS
and desktop Xray 26.6.27 against a 3x-ui 3.7.0 box with per-client traffic
counters). Skip the mux block whenever the outbound carries a flow.

Refs #6379

* fix(sub): keep XUDP settings when disabling TCP mux on Vision outbounds

Clearing the whole mux object also dropped xudpConcurrency, xudpProxyUDP443
and any per-host muxParams override. Xray reads those only under
mux.enabled, so set concurrency to -1 instead: TCP mux.cool (which XTLS flows
reject) is off, XUDP and the UDP/443 policy stay. The test now decodes each
outbound into a fresh map.

---------

Co-authored-by: Farhan Zare <farhan.zare@openscreen.com>
This commit is contained in:
Farhan Zare
2026-09-05 14:44:48 -04:00
committed by GitHub
parent ed6bc1d898
commit 0f6e1ae8d7
9 changed files with 79 additions and 6 deletions
+2
View File
@@ -12,6 +12,7 @@
},
"inbounds": [
{
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": {
@@ -31,6 +32,7 @@
"tag": "mixed"
},
{
"listen": "127.0.0.1",
"port": 10809,
"protocol": "http",
"settings": {
+19
View File
@@ -788,12 +788,31 @@ func (s *SubJsonService) genVless(subReq *SubService, inbound *model.Inbound, st
}
if client.Flow != "" && !inbound.DisableFlow {
settings["flow"] = client.Flow
outbound.Mux = muxWithoutTCP(mux)
}
outbound.Settings = settings
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
// XTLS flows reject TCP mux.cool ("unexpected network TCP"); concurrency -1
// turns only that off and keeps the XUDP keys (Xray reads them under enabled).
func muxWithoutTCP(mux string) json_util.RawMessage {
if mux == "" {
return nil
}
var m map[string]any
if err := json.Unmarshal([]byte(mux), &m); err != nil || m == nil {
return nil
}
m["concurrency"] = -1
out, err := json.Marshal(m)
if err != nil {
return nil
}
return json_util.RawMessage(out)
}
func (s *SubJsonService) genServer(subReq *SubService, inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
outbound := Outbound{}
+44
View File
@@ -66,11 +66,55 @@ func TestDefaultJSONUsesCompatibleLocalInbounds(t *testing.T) {
if settings == nil || settings["udp"] != true {
t.Fatalf("port 10808 settings = %#v, want udp enabled", socks["settings"])
}
if socks["listen"] != "127.0.0.1" {
t.Fatalf("port 10808 listen = %#v, want 127.0.0.1 (an unbound local inbound is exposed to the LAN and is not reachable by iOS packet tunnels)", socks["listen"])
}
http := byPort[10809]
if http == nil || http["protocol"] != "http" {
t.Fatalf("port 10809 inbound = %#v, want http protocol", http)
}
if http["listen"] != "127.0.0.1" {
t.Fatalf("port 10809 listen = %#v, want 127.0.0.1", http["listen"])
}
}
func TestSubJsonServiceVisionFlowDisablesTCPMuxOnly(t *testing.T) {
globalMux := `{"enabled":true,"concurrency":8,"xudpConcurrency":16,"xudpProxyUDP443":"reject"}`
svc := NewSubJsonService(globalMux, "", "", nil)
inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
decode := func(raw []byte) map[string]any {
t.Helper()
var ob map[string]any
if err := json.Unmarshal(raw, &ob); err != nil {
t.Fatalf("unmarshal outbound: %v", err)
}
return ob
}
vision := decode([]byte(svc.genVless(&SubService{}, inbound, nil, model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}, globalMux)))
mux, _ := vision["mux"].(map[string]any)
if mux == nil {
t.Fatalf("vision outbound must keep its mux object for the XUDP keys, got %#v", vision["mux"])
}
if mux["concurrency"] != float64(-1) {
t.Fatalf("vision outbound mux.concurrency = %v, want -1 (TCP mux.cool is rejected by XTLS flows)", mux["concurrency"])
}
if mux["enabled"] != true || mux["xudpConcurrency"] != float64(16) || mux["xudpProxyUDP443"] != "reject" {
t.Fatalf("vision outbound lost its XUDP settings: %#v", mux)
}
plain := decode([]byte(svc.genVless(&SubService{}, inbound, nil, model.Client{ID: "uuid-1"}, globalMux)))
mux, _ = plain["mux"].(map[string]any)
if mux == nil || mux["concurrency"] != float64(8) {
t.Fatalf("flow-less outbound must keep the global mux unchanged, got %#v", plain["mux"])
}
noMux := decode([]byte(svc.genVless(&SubService{}, inbound, nil, model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}, "")))
if _, has := noMux["mux"]; has {
t.Fatalf("no global mux must still mean no mux key, got %#v", noMux["mux"])
}
}
func TestSubJsonServiceInjectsGlobalFinalMask(t *testing.T) {