mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-04 01:17:15 +00:00
fix(sub): restore compatible SOCKS subscription inbound (#6395)
This commit is contained in:
@@ -57,7 +57,7 @@ page instead of the raw body.
|
|||||||
|
|
||||||
The **Base64** body is just the newline-joined share links, standard-base64
|
The **Base64** body is just the newline-joined share links, standard-base64
|
||||||
encoded (toggle with `subEncrypt`). The **JSON** body wraps each client in a
|
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
|
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
|
**single config object for one client and an array for several**, uses the flat
|
||||||
outbound `settings` form (`address`/`port`/`id`, `level: 8`), and strips
|
outbound `settings` form (`address`/`port`/`id`, `level: 8`), and strips
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
|
|||||||
بدنهی **Base64** صرفاً همان لینکهای اشتراکگذاری است که با خط جدید به هم پیوسته و
|
بدنهی **Base64** صرفاً همان لینکهای اشتراکگذاری است که با خط جدید به هم پیوسته و
|
||||||
با standard-base64 رمزگذاری شدهاند (با `subEncrypt` قابل تغییر است). بدنهی **JSON**
|
با standard-base64 رمزگذاری شدهاند (با `subEncrypt` قابل تغییر است). بدنهی **JSON**
|
||||||
هر کلاینت را در یک پیکربندی کامل کلاینت Xray میپیچد — یک اسکلت ثابت (ورودیهای محلی
|
هر کلاینت را در یک پیکربندی کامل کلاینت Xray میپیچد — یک اسکلت ثابت (ورودیهای محلی
|
||||||
mixed/HTTP، DNS، مسیریابی، policy) بهعلاوهی یک outbound از نوع `proxy` که به ورودی
|
SOCKS/HTTP، DNS، مسیریابی، policy) بهعلاوهی یک outbound از نوع `proxy` که به ورودی
|
||||||
اشاره میکند. 3x-ui **برای یک کلاینت یک شیء پیکربندی واحد و برای چند کلاینت یک آرایه**
|
اشاره میکند. 3x-ui **برای یک کلاینت یک شیء پیکربندی واحد و برای چند کلاینت یک آرایه**
|
||||||
تولید میکند، از فرم تخت `settings` در outbound استفاده میکند
|
تولید میکند، از فرم تخت `settings` در outbound استفاده میکند
|
||||||
(`address`/`port`/`id`، `level: 8`) و `sockopt` را از `streamSettings` حذف میکند.
|
(`address`/`port`/`id`، `level: 8`) و `sockopt` را از `streamSettings` حذف میکند.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
|
|||||||
Тело **Base64** — это просто ссылки для обмена, объединённые через перевод
|
Тело **Base64** — это просто ссылки для обмена, объединённые через перевод
|
||||||
строки и закодированные в стандартный base64 (переключается через `subEncrypt`).
|
строки и закодированные в стандартный base64 (переключается через `subEncrypt`).
|
||||||
Тело **JSON** оборачивает каждого клиента в полную клиентскую конфигурацию
|
Тело **JSON** оборачивает каждого клиента в полную клиентскую конфигурацию
|
||||||
Xray — фиксированный каркас (локальные входящие mixed/HTTP, DNS, маршрутизация,
|
Xray — фиксированный каркас (локальные входящие SOCKS/HTTP, DNS, маршрутизация,
|
||||||
policy) плюс исходящее соединение `proxy`, указывающее на входящее. 3x-ui
|
policy) плюс исходящее соединение `proxy`, указывающее на входящее. 3x-ui
|
||||||
выдаёт **единый объект конфигурации для одного клиента и массив для
|
выдаёт **единый объект конфигурации для одного клиента и массив для
|
||||||
нескольких**, использует плоскую форму `settings` исходящего соединения
|
нескольких**, использует плоскую форму `settings` исходящего соединения
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
|
|||||||
|
|
||||||
### Base64 与 JSON
|
### 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`。
|
||||||
|
|
||||||
## 响应头
|
## 响应头
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,19 @@ describe('buildJsonSubscription', () => {
|
|||||||
expect(cfg.remarks).toBe('HK-01');
|
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', () => {
|
it('trojan uses servers[] with a password and no method', () => {
|
||||||
const trojan: SubClient = {
|
const trojan: SubClient = {
|
||||||
protocol: 'trojan',
|
protocol: 'trojan',
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function subJsonSkeleton(): Record<string, unknown> {
|
|||||||
inbounds: [
|
inbounds: [
|
||||||
{
|
{
|
||||||
port: 10808,
|
port: 10808,
|
||||||
protocol: 'mixed',
|
protocol: 'socks',
|
||||||
settings: { auth: 'noauth', udp: true, userLevel: 8 },
|
settings: { auth: 'noauth', udp: true, userLevel: 8 },
|
||||||
sniffing: { destOverride: ['http', 'tls', 'quic', 'fakedns'], enabled: true },
|
sniffing: { destOverride: ['http', 'tls', 'quic', 'fakedns'], enabled: true },
|
||||||
tag: 'mixed',
|
tag: 'mixed',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
"port": 10808,
|
"port": 10808,
|
||||||
"protocol": "mixed",
|
"protocol": "socks",
|
||||||
"settings": {
|
"settings": {
|
||||||
"auth": "noauth",
|
"auth": "noauth",
|
||||||
"udp": true,
|
"udp": true,
|
||||||
|
|||||||
@@ -35,6 +35,44 @@ func outboundSettings(t *testing.T, raw []byte) map[string]any {
|
|||||||
return settings
|
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) {
|
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"}}`
|
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)
|
svc := NewSubJsonService("", "", finalMask, nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user