diff --git a/docs/content/docs/en/config/subscription.mdx b/docs/content/docs/en/config/subscription.mdx index cf7d4968b..2e89452d9 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 mixed/HTTP inbounds, DNS, +complete Xray client config — a fixed skeleton (local SOCKS/HTTP inbounds, 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 c1da7f66a..b6a2f79a4 100644 --- a/docs/content/docs/fa/config/subscription.mdx +++ b/docs/content/docs/fa/config/subscription.mdx @@ -58,7 +58,7 @@ https://:/sub/ بدنه‌ی **Base64** صرفاً همان لینک‌های اشتراک‌گذاری است که با خط جدید به هم پیوسته و با standard-base64 رمزگذاری شده‌اند (با `subEncrypt` قابل تغییر است). بدنه‌ی **JSON** هر کلاینت را در یک پیکربندی کامل کلاینت Xray می‌پیچد — یک اسکلت ثابت (ورودی‌های محلی -mixed/HTTP، DNS، مسیریابی، policy) به‌علاوه‌ی یک outbound از نوع `proxy` که به ورودی +SOCKS/HTTP، 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 a0c53d5c7..05f2c0ca0 100644 --- a/docs/content/docs/ru/config/subscription.mdx +++ b/docs/content/docs/ru/config/subscription.mdx @@ -59,7 +59,7 @@ https://:/sub/ Тело **Base64** — это просто ссылки для обмена, объединённые через перевод строки и закодированные в стандартный base64 (переключается через `subEncrypt`). Тело **JSON** оборачивает каждого клиента в полную клиентскую конфигурацию -Xray — фиксированный каркас (локальные входящие mixed/HTTP, DNS, маршрутизация, +Xray — фиксированный каркас (локальные входящие SOCKS/HTTP, 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 0ddde982e..5a33a8441 100644 --- a/docs/content/docs/zh/config/subscription.mdx +++ b/docs/content/docs/zh/config/subscription.mdx @@ -46,7 +46,7 @@ https://:/sub/ ### Base64 与 JSON -**Base64** 内容只是用换行符连接的分享链接,经标准 base64 编码(通过 `subEncrypt` 开关控制)。**JSON** 内容则将每个客户端包装为一份完整的 Xray 客户端配置 —— 一套固定的骨架(本地 mixed/HTTP 入站、DNS、路由、策略)加上一个指向该入站的 `proxy` 出站。3x-ui **对单个客户端输出单个配置对象,对多个客户端输出数组**,使用扁平的出站 `settings` 形式(`address`/`port`/`id`,`level: 8`),并从 `streamSettings` 中剥离 `sockopt`。 +**Base64** 内容只是用换行符连接的分享链接,经标准 base64 编码(通过 `subEncrypt` 开关控制)。**JSON** 内容则将每个客户端包装为一份完整的 Xray 客户端配置 —— 一套固定的骨架(本地 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 804541641..d0d0c128b 100644 --- a/docs/lib/xray/subscription.test.ts +++ b/docs/lib/xray/subscription.test.ts @@ -120,6 +120,19 @@ describe('buildJsonSubscription', () => { expect(cfg.remarks).toBe('HK-01'); }); + it('uses the iOS-compatible SOCKS inbound while preserving the mixed tag and HTTP inbound', () => { + const cfg = JSON.parse(buildJsonSubscription([vlessClient])); + const socks = cfg.inbounds.find((inbound: { port: number }) => inbound.port === 10808); + const http = cfg.inbounds.find((inbound: { port: number }) => inbound.port === 10809); + + expect(socks).toMatchObject({ + protocol: 'socks', + tag: 'mixed', + settings: { udp: true }, + }); + expect(http).toMatchObject({ protocol: 'http' }); + }); + it('trojan uses servers[] with a password and no method', () => { const trojan: SubClient = { protocol: 'trojan', diff --git a/docs/lib/xray/subscription.ts b/docs/lib/xray/subscription.ts index 19c88e8a9..0237dd935 100644 --- a/docs/lib/xray/subscription.ts +++ b/docs/lib/xray/subscription.ts @@ -146,7 +146,7 @@ function subJsonSkeleton(): Record { inbounds: [ { port: 10808, - protocol: 'mixed', + protocol: 'socks', settings: { auth: 'noauth', udp: true, userLevel: 8 }, sniffing: { destOverride: ['http', 'tls', 'quic', 'fakedns'], enabled: true }, tag: 'mixed', diff --git a/internal/sub/default.json b/internal/sub/default.json index 59c8a5ce4..f16de25b0 100644 --- a/internal/sub/default.json +++ b/internal/sub/default.json @@ -13,7 +13,7 @@ "inbounds": [ { "port": 10808, - "protocol": "mixed", + "protocol": "socks", "settings": { "auth": "noauth", "udp": true, diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index c7c6a4890..1e142750c 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -35,6 +35,44 @@ func outboundSettings(t *testing.T, raw []byte) map[string]any { return settings } +func TestDefaultJSONUsesCompatibleLocalInbounds(t *testing.T) { + svc := NewSubJsonService("", "", "", nil) + inbounds, ok := svc.configJson["inbounds"].([]any) + if !ok { + t.Fatalf("default JSON inbounds = %#v, want array", svc.configJson["inbounds"]) + } + + byPort := make(map[float64]map[string]any, len(inbounds)) + for _, raw := range inbounds { + inbound, ok := raw.(map[string]any) + if !ok { + t.Fatalf("default JSON inbound = %#v, want object", raw) + } + port, ok := inbound["port"].(float64) + if !ok { + t.Fatalf("default JSON inbound port = %#v, want number", inbound["port"]) + } + byPort[port] = inbound + } + + socks := byPort[10808] + if socks == nil { + t.Fatal("default JSON is missing the local inbound on port 10808") + } + if socks["protocol"] != "socks" || socks["tag"] != "mixed" { + t.Fatalf("port 10808 protocol/tag = %v/%v, want socks/mixed", socks["protocol"], socks["tag"]) + } + settings, _ := socks["settings"].(map[string]any) + if settings == nil || settings["udp"] != true { + t.Fatalf("port 10808 settings = %#v, want udp enabled", socks["settings"]) + } + + http := byPort[10809] + if http == nil || http["protocol"] != "http" { + t.Fatalf("port 10809 inbound = %#v, want http protocol", http) + } +} + func TestSubJsonServiceInjectsGlobalFinalMask(t *testing.T) { finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","length":"100-200","delay":"10-20"}}],"udp":[{"type":"noise","settings":{"noise":[{"type":"base64","packet":"SGVsbG8="}]}}],"quicParams":{"congestion":"bbr"}}` svc := NewSubJsonService("", "", finalMask, nil)