diff --git a/docs/content/docs/en/config/subscription.mdx b/docs/content/docs/en/config/subscription.mdx index 77679d95f..2be1c3f03 100644 --- a/docs/content/docs/en/config/subscription.mdx +++ b/docs/content/docs/en/config/subscription.mdx @@ -57,7 +57,7 @@ page instead of the raw body. The **Base64** body is just the newline-joined share links, standard-base64 encoded (toggle with `subEncrypt`). The **JSON** body wraps each client in a -complete Xray client config — a fixed skeleton (local SOCKS/HTTP inbounds, DNS, +complete Xray client config — a fixed skeleton (local SOCKS/HTTP inbounds bound to 127.0.0.1, DNS, routing, policy) plus a `proxy` outbound pointing at the inbound. 3x-ui emits a **single config object for one client and an array for several**, uses the flat outbound `settings` form (`address`/`port`/`id`, `level: 8`), and strips diff --git a/docs/content/docs/fa/config/subscription.mdx b/docs/content/docs/fa/config/subscription.mdx index 2269c5d91..3bc494169 100644 --- a/docs/content/docs/fa/config/subscription.mdx +++ b/docs/content/docs/fa/config/subscription.mdx @@ -58,7 +58,7 @@ https://:// بدنه‌ی **Base64** صرفاً همان لینک‌های اشتراک‌گذاری است که با خط جدید به هم پیوسته و با standard-base64 رمزگذاری شده‌اند (با `subEncrypt` قابل تغییر است). بدنه‌ی **JSON** هر کلاینت را در یک پیکربندی کامل کلاینت Xray می‌پیچد — یک اسکلت ثابت (ورودی‌های محلی -SOCKS/HTTP، DNS، مسیریابی، policy) به‌علاوه‌ی یک outbound از نوع `proxy` که به ورودی +SOCKS/HTTP روی 127.0.0.1، DNS، مسیریابی، policy) به‌علاوه‌ی یک outbound از نوع `proxy` که به ورودی اشاره می‌کند. 3x-ui **برای یک کلاینت یک شیء پیکربندی واحد و برای چند کلاینت یک آرایه** تولید می‌کند، از فرم تخت `settings` در outbound استفاده می‌کند (`address`/`port`/`id`، `level: 8`) و `sockopt` را از `streamSettings` حذف می‌کند. diff --git a/docs/content/docs/ru/config/subscription.mdx b/docs/content/docs/ru/config/subscription.mdx index 9c05dd33d..025151014 100644 --- a/docs/content/docs/ru/config/subscription.mdx +++ b/docs/content/docs/ru/config/subscription.mdx @@ -59,7 +59,7 @@ https://:// Тело **Base64** — это просто ссылки для обмена, объединённые через перевод строки и закодированные в стандартный base64 (переключается через `subEncrypt`). Тело **JSON** оборачивает каждого клиента в полную клиентскую конфигурацию -Xray — фиксированный каркас (локальные входящие SOCKS/HTTP, DNS, маршрутизация, +Xray — фиксированный каркас (локальные входящие SOCKS/HTTP на 127.0.0.1, DNS, маршрутизация, policy) плюс исходящее соединение `proxy`, указывающее на входящее. 3x-ui выдаёт **единый объект конфигурации для одного клиента и массив для нескольких**, использует плоскую форму `settings` исходящего соединения diff --git a/docs/content/docs/zh/config/subscription.mdx b/docs/content/docs/zh/config/subscription.mdx index 380a77bb1..b5bbf811d 100644 --- a/docs/content/docs/zh/config/subscription.mdx +++ b/docs/content/docs/zh/config/subscription.mdx @@ -46,7 +46,7 @@ https://:// ### Base64 与 JSON -**Base64** 内容只是用换行符连接的分享链接,经标准 base64 编码(通过 `subEncrypt` 开关控制)。**JSON** 内容则将每个客户端包装为一份完整的 Xray 客户端配置 —— 一套固定的骨架(本地 SOCKS/HTTP 入站、DNS、路由、策略)加上一个指向该入站的 `proxy` 出站。3x-ui **对单个客户端输出单个配置对象,对多个客户端输出数组**,使用扁平的出站 `settings` 形式(`address`/`port`/`id`,`level: 8`),并从 `streamSettings` 中剥离 `sockopt`。 +**Base64** 内容只是用换行符连接的分享链接,经标准 base64 编码(通过 `subEncrypt` 开关控制)。**JSON** 内容则将每个客户端包装为一份完整的 Xray 客户端配置 —— 一套固定的骨架(绑定到 127.0.0.1 的本地 SOCKS/HTTP 入站、DNS、路由、策略)加上一个指向该入站的 `proxy` 出站。3x-ui **对单个客户端输出单个配置对象,对多个客户端输出数组**,使用扁平的出站 `settings` 形式(`address`/`port`/`id`,`level: 8`),并从 `streamSettings` 中剥离 `sockopt`。 ## 响应头 diff --git a/docs/lib/xray/subscription.test.ts b/docs/lib/xray/subscription.test.ts index d0d0c128b..a79f78705 100644 --- a/docs/lib/xray/subscription.test.ts +++ b/docs/lib/xray/subscription.test.ts @@ -126,11 +126,12 @@ describe('buildJsonSubscription', () => { const http = cfg.inbounds.find((inbound: { port: number }) => inbound.port === 10809); expect(socks).toMatchObject({ + listen: '127.0.0.1', protocol: 'socks', tag: 'mixed', settings: { udp: true }, }); - expect(http).toMatchObject({ protocol: 'http' }); + expect(http).toMatchObject({ listen: '127.0.0.1', protocol: 'http' }); }); it('trojan uses servers[] with a password and no method', () => { diff --git a/docs/lib/xray/subscription.ts b/docs/lib/xray/subscription.ts index 0237dd935..c06337a78 100644 --- a/docs/lib/xray/subscription.ts +++ b/docs/lib/xray/subscription.ts @@ -145,13 +145,20 @@ function subJsonSkeleton(): Record { }, inbounds: [ { + listen: '127.0.0.1', port: 10808, protocol: 'socks', settings: { auth: 'noauth', udp: true, userLevel: 8 }, sniffing: { destOverride: ['http', 'tls', 'quic', 'fakedns'], enabled: true }, tag: 'mixed', }, - { port: 10809, protocol: 'http', settings: { userLevel: 8 }, tag: 'http' }, + { + listen: '127.0.0.1', + port: 10809, + protocol: 'http', + settings: { userLevel: 8 }, + tag: 'http', + }, ], log: { loglevel: 'warning' }, policy: { diff --git a/internal/sub/default.json b/internal/sub/default.json index f16de25b0..1203eb218 100644 --- a/internal/sub/default.json +++ b/internal/sub/default.json @@ -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": { diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index e2d866aaa..48ec3a133 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -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{} diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index 1e142750c..58e431707 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -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) {