@twinsearth/dsh-2xray 0.3.0 → 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 +8 -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,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.3.0 — 2026-10-29
|
|
4
12
|
|
|
5
13
|
### 更新中心
|
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.
|
|
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
|
+
})
|