@twinsearth/dsh-2xray 0.2.10 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/lib/assets.js +14 -1
- package/lib/config.js +11 -4
- package/lib/supervisor.js +24 -3
- package/package.json +1 -1
- package/test/assets.test.js +1 -1
- package/test/port-guard.test.js +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
|
|
7
|
+
- **入站端口护栏覆盖所有平台**:`inbound.port < 1024` 或与节点远端端口相同(如 556)时自动回落到 20808 并告警,避免把远端端口当本地监听。
|
|
8
|
+
- **geodata 发现扩大**:当内核来自平台包、旁边没有 `geoip.dat`/`geosite.dat` 时,会扫描本机 v2rayN 安装目录并复制到数据目录,再设置 `XRAY_LOCATION_ASSET`。
|
|
9
|
+
- **缺 geodata 不再 config-rejected**:cn-direct / geosite 规则缺数据文件时临时降级为 global,代理仍可用;状态新增 `routingFallback` 说明原因。
|
|
10
|
+
|
|
11
|
+
## 0.3.0 — 2026-10-29
|
|
12
|
+
|
|
13
|
+
### 更新中心
|
|
14
|
+
|
|
15
|
+
- 设置页「检测更新 / 立即更新 / 自动更新」:npm registry 检测、sha512 校验、tarball 原子替换、重启生效;自动更新默认关闭。
|
|
16
|
+
- Host 鉴权接口 `GET/POST /api/xray/update`,状态并入 `xray_status.update`。
|
|
17
|
+
|
|
18
|
+
### 分流与内核
|
|
19
|
+
|
|
20
|
+
- 路由模式 global / cn-direct / whitelist,cn-direct 使用 `IPIfNonMatch` + geodata 自动复制。
|
|
21
|
+
- 黑名单最优先并并入 `noProxy`;白名单仅 whitelist 模式生效。
|
|
22
|
+
- 按平台内核包(`optionalDependencies`,已发布 win32-x64)+ 官方 zip 构建脚本 + CI workflow。
|
|
23
|
+
- macOS 发现路径、特权端口回落、探测 URL 修复。
|
|
24
|
+
|
|
25
|
+
### 界面
|
|
26
|
+
|
|
27
|
+
- 三卡片面板、节点切换、日志默认展开、设置页 2Xray 标签页、VLESS 链接/本地二维码/屏幕扫码导入。
|
|
28
|
+
|
|
3
29
|
## 0.2.10 — 2026-10-29
|
|
4
30
|
|
|
5
31
|
### 更新
|
package/lib/assets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
+
import { defaultDiscoveryRoots, discoverXray } from './binary.js'
|
|
3
4
|
|
|
4
5
|
const FILES = ['geoip.dat', 'geosite.dat']
|
|
5
6
|
|
|
@@ -8,7 +9,7 @@ const FILES = ['geoip.dat', 'geosite.dat']
|
|
|
8
9
|
* XRAY_LOCATION_ASSET. A borrowed v2rayN binary keeps them one directory up,
|
|
9
10
|
* so copy them into the managed data dir and point xray there.
|
|
10
11
|
*/
|
|
11
|
-
export function ensureXrayAssets({ binaryPath, dataDir, logger } = {}) {
|
|
12
|
+
export function ensureXrayAssets({ binaryPath, dataDir, logger, discoveryRoots } = {}) {
|
|
12
13
|
if (!dataDir) return { dir: undefined, copied: [], missing: FILES }
|
|
13
14
|
try {
|
|
14
15
|
mkdirSync(dataDir, { recursive: true })
|
|
@@ -17,6 +18,18 @@ export function ensureXrayAssets({ binaryPath, dataDir, logger } = {}) {
|
|
|
17
18
|
return { dir: dataDir, copied: [], missing: FILES }
|
|
18
19
|
}
|
|
19
20
|
const candidates = [dataDir, dirname(binaryPath ?? ''), dirname(dirname(binaryPath ?? ''))]
|
|
21
|
+
const missingBefore = FILES.some((name) => !existsSync(join(dataDir, name)))
|
|
22
|
+
if (missingBefore) {
|
|
23
|
+
// The resolved core may come from a package with no geodata. A local
|
|
24
|
+
// v2rayN install usually has geoip.dat/geosite.dat next to its binaries.
|
|
25
|
+
try {
|
|
26
|
+
for (const discovered of discoverXray({ roots: discoveryRoots ?? defaultDiscoveryRoots(), timeBudgetMs: 1500, max: 4 })) {
|
|
27
|
+
candidates.push(dirname(discovered), dirname(dirname(discovered)))
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// discovery is best-effort; the managed dir may already have the files
|
|
31
|
+
}
|
|
32
|
+
}
|
|
20
33
|
const copied = []
|
|
21
34
|
const missing = []
|
|
22
35
|
for (const name of FILES) {
|
package/lib/config.js
CHANGED
|
@@ -392,11 +392,18 @@ export function normalizeConfig(raw) {
|
|
|
392
392
|
}
|
|
393
393
|
if (!Number.isInteger(inbound.port) || inbound.port < 1 || inbound.port > 65535) {
|
|
394
394
|
errors.push({ field: 'inbound.port', message: '入站端口必须是 1..65535 的整数' })
|
|
395
|
-
} else
|
|
395
|
+
} else {
|
|
396
396
|
// A remote node port (e.g. 556) must never become the local listener:
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
inbound.port
|
|
397
|
+
// <1024 needs root on macOS/Linux, and mirroring the server port makes the
|
|
398
|
+
// local proxy look like a second server. Always fall back to 20808.
|
|
399
|
+
if (inbound.port < 1024) {
|
|
400
|
+
warnings.push(`入站端口 ${inbound.port} 是特权端口;已改用 ${DEFAULT_CONFIG.inbound.port}`)
|
|
401
|
+
inbound.port = DEFAULT_CONFIG.inbound.port
|
|
402
|
+
}
|
|
403
|
+
if (nodes.some((node) => Number(node.port) === inbound.port)) {
|
|
404
|
+
warnings.push(`入站端口 ${inbound.port} 与节点远端端口相同;已改用 ${DEFAULT_CONFIG.inbound.port}`)
|
|
405
|
+
inbound.port = DEFAULT_CONFIG.inbound.port
|
|
406
|
+
}
|
|
400
407
|
}
|
|
401
408
|
if (socks.enabled) {
|
|
402
409
|
if (!LOOPBACK_HOSTS.has(socks.host)) {
|
package/lib/supervisor.js
CHANGED
|
@@ -88,6 +88,7 @@ export class Supervisor {
|
|
|
88
88
|
#control
|
|
89
89
|
#activeNode
|
|
90
90
|
#schemaLoaded = false
|
|
91
|
+
#routingFallback = null
|
|
91
92
|
#now
|
|
92
93
|
|
|
93
94
|
constructor({
|
|
@@ -223,12 +224,24 @@ export class Supervisor {
|
|
|
223
224
|
this.#binary = binary
|
|
224
225
|
if (signal.aborted) return
|
|
225
226
|
|
|
227
|
+
let effectiveConfig = this.config
|
|
228
|
+
this.#routingFallback = null
|
|
226
229
|
if (this.dataDir) {
|
|
227
230
|
const routing = this.config.routing ?? {}
|
|
228
231
|
if (routing.mode === 'cn-direct' || routing.geositeCn || routing.blockAds) {
|
|
229
232
|
const assets = ensureXrayAssets({ binaryPath: binary.path, dataDir: this.dataDir, logger: this.#logger })
|
|
230
233
|
process.env.XRAY_LOCATION_ASSET = this.dataDir
|
|
231
234
|
if (assets.copied.length > 0) this.#logger?.info?.(`已复制 xray 数据文件:${assets.copied.join(', ')}`)
|
|
235
|
+
if (assets.missing.length > 0) {
|
|
236
|
+
// Never let missing geodata turn into config-rejected: keep the
|
|
237
|
+
// tunnel usable with global routing and surface the degradation.
|
|
238
|
+
effectiveConfig = {
|
|
239
|
+
...this.config,
|
|
240
|
+
routing: { ...routing, mode: 'global', geositeCn: false, blockAds: false },
|
|
241
|
+
}
|
|
242
|
+
this.#routingFallback = `缺少 ${assets.missing.join(', ')},已临时降级为 global`
|
|
243
|
+
this.#logger?.warn?.(this.#routingFallback)
|
|
244
|
+
}
|
|
232
245
|
}
|
|
233
246
|
}
|
|
234
247
|
|
|
@@ -244,7 +257,7 @@ export class Supervisor {
|
|
|
244
257
|
this.#version = version.version
|
|
245
258
|
if (signal.aborted) return
|
|
246
259
|
|
|
247
|
-
const xrayConfig = buildXrayConfig(
|
|
260
|
+
const xrayConfig = buildXrayConfig(effectiveConfig)
|
|
248
261
|
const text = JSON.stringify(xrayConfig, null, 2)
|
|
249
262
|
this.#configHash = sha256(text)
|
|
250
263
|
const configPath = join(this.dataDir, 'conf', 'xray-config.json')
|
|
@@ -505,12 +518,19 @@ export class Supervisor {
|
|
|
505
518
|
}
|
|
506
519
|
}
|
|
507
520
|
|
|
521
|
+
let effectiveConfig = this.config
|
|
508
522
|
const routing = this.config.routing ?? {}
|
|
509
523
|
if (routing.mode === 'cn-direct' || routing.geositeCn || routing.blockAds) {
|
|
510
|
-
ensureXrayAssets({ binaryPath: binary.path, dataDir: this.dataDir, logger: this.#logger })
|
|
524
|
+
const assets = ensureXrayAssets({ binaryPath: binary.path, dataDir: this.dataDir, logger: this.#logger })
|
|
525
|
+
if (assets.missing.length > 0) {
|
|
526
|
+
effectiveConfig = {
|
|
527
|
+
...this.config,
|
|
528
|
+
routing: { ...routing, mode: 'global', geositeCn: false, blockAds: false },
|
|
529
|
+
}
|
|
530
|
+
}
|
|
511
531
|
}
|
|
512
532
|
|
|
513
|
-
const built = buildXrayConfig(
|
|
533
|
+
const built = buildXrayConfig(effectiveConfig)
|
|
514
534
|
const text = JSON.stringify(built, null, 2)
|
|
515
535
|
const file = join(this.dataDir, 'conf', `validate-${Date.now()}-${process.pid}.json`)
|
|
516
536
|
atomicWriteJson(file, built, 0o600)
|
|
@@ -711,6 +731,7 @@ export class Supervisor {
|
|
|
711
731
|
enabled: this.config.enabled,
|
|
712
732
|
takeover: this.config.takeover,
|
|
713
733
|
settingsSchema: this.#schemaLoaded,
|
|
734
|
+
routingFallback: this.#routingFallback,
|
|
714
735
|
nodes: this.config.nodes.map((node) => ({
|
|
715
736
|
id: node.id,
|
|
716
737
|
tag: node.tag,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/dsh-2xray",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "DSH-2Xray: supervises xray-core and routes DSH process-wide HTTP(S) traffic through its local inbound, with optional multi-node gRPC control plane",
|
|
6
6
|
"type": "module",
|
package/test/assets.test.js
CHANGED
|
@@ -26,7 +26,7 @@ test('geodata next to a v2rayN binary is copied into the managed data dir', () =
|
|
|
26
26
|
test('missing geodata is reported rather than silently ignored', () => {
|
|
27
27
|
const root = mkdtempSync(join(tmpdir(), 'dsh-xray-assets-'))
|
|
28
28
|
try {
|
|
29
|
-
const result = ensureXrayAssets({ binaryPath: join(root, 'xray.exe'), dataDir: join(root, 'data') })
|
|
29
|
+
const result = ensureXrayAssets({ binaryPath: join(root, 'xray.exe'), dataDir: join(root, 'data'), discoveryRoots: [] })
|
|
30
30
|
assert.deepEqual(result.copied, [])
|
|
31
31
|
assert.deepEqual(result.missing.sort(), ['geoip.dat', 'geosite.dat'])
|
|
32
32
|
} finally {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { normalizeConfig } from '../lib/config.js'
|
|
4
|
+
|
|
5
|
+
const base = (overrides = {}) => ({
|
|
6
|
+
enabled: true,
|
|
7
|
+
server: {
|
|
8
|
+
address: 'example.com',
|
|
9
|
+
port: 443,
|
|
10
|
+
uuid: '11111111-2222-4333-8444-555555555555',
|
|
11
|
+
serverName: 'www.example.com',
|
|
12
|
+
password: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
13
|
+
shortId: 'ab12',
|
|
14
|
+
},
|
|
15
|
+
...overrides,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('a remote node port is never mirrored as the local inbound port', () => {
|
|
19
|
+
const { config, warnings } = normalizeConfig(base({ server: { address: 'example.com', port: 556, uuid: '11111111-2222-4333-8444-555555555555' }, inbound: { port: 556 } }))
|
|
20
|
+
assert.equal(config.inbound.port, 20808)
|
|
21
|
+
assert.ok(warnings.some((warning) => warning.includes('privileged') || warning.includes('特权')))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('an inbound port equal to the node port falls back to 20808', () => {
|
|
25
|
+
const { config, warnings } = normalizeConfig(base({ server: { address: 'example.com', port: 8443, uuid: '11111111-2222-4333-8444-555555555555' }, inbound: { port: 8443 } }))
|
|
26
|
+
assert.equal(config.inbound.port, 20808)
|
|
27
|
+
assert.ok(warnings.some((warning) => warning.includes('remote') || warning.includes('远端')))
|
|
28
|
+
})
|