@twinsearth/dsh-2xray 0.0.0-stage → 0.1.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 +35 -0
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/bin/README.md +27 -0
- package/cordis.patch.yml +37 -0
- package/docs/AC-0.md +60 -0
- package/docs/EVIDENCE.md +135 -0
- package/examples/config.downgrade.json +20 -0
- package/examples/config.example.json +22 -0
- package/examples/config.multi.downgrade.json +39 -0
- package/examples/config.multi.json +46 -0
- package/icon.svg +9 -0
- package/index.js +89 -0
- package/lib/binary.js +133 -0
- package/lib/config.js +401 -0
- package/lib/control/client.js +100 -0
- package/lib/control/grpc.js +121 -0
- package/lib/control/protobuf.js +143 -0
- package/lib/health.js +109 -0
- package/lib/log.js +20 -0
- package/lib/paths.js +65 -0
- package/lib/process.js +197 -0
- package/lib/route.js +299 -0
- package/lib/share-link.js +132 -0
- package/lib/supervisor.js +687 -0
- package/lib/tools.js +106 -0
- package/lib/xray-config.js +225 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +42 -4
- package/scripts/check-config.mjs +56 -0
- package/scripts/parse-share-link.mjs +23 -0
- package/scripts/probe-routing.mjs +66 -0
- package/scripts/release-check.mjs +104 -0
- package/test/binary.test.js +103 -0
- package/test/config.test.js +106 -0
- package/test/control.test.js +166 -0
- package/test/health.test.js +75 -0
- package/test/index.test.js +52 -0
- package/test/multi-node.test.js +111 -0
- package/test/process.test.js +49 -0
- package/test/route.test.js +158 -0
- package/test/share-link.test.js +113 -0
- package/test/supervisor.test.js +525 -0
- package/test/tools.test.js +88 -0
- package/test/xray-config.test.js +132 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { RouteManager, RouteError } from '../lib/route.js'
|
|
4
|
+
|
|
5
|
+
const logger = { info() {}, warn() {}, error() {}, debug() {} }
|
|
6
|
+
|
|
7
|
+
function makeFakeHarness({ effective = true, preinstalled = false } = {}) {
|
|
8
|
+
let active = preinstalled
|
|
9
|
+
let installCalls = 0
|
|
10
|
+
let disposerCalls = 0
|
|
11
|
+
const dispatcher = { kind: 'policy-dispatcher' }
|
|
12
|
+
const module = {
|
|
13
|
+
proxyRouteFor() {
|
|
14
|
+
return active
|
|
15
|
+
? { proxied: true, proxy: 'http://127.0.0.1:20808', dispatcher }
|
|
16
|
+
: { proxied: false }
|
|
17
|
+
},
|
|
18
|
+
async installProxyFromEnvironment() {
|
|
19
|
+
installCalls++
|
|
20
|
+
if (effective) active = true
|
|
21
|
+
return async () => {
|
|
22
|
+
disposerCalls++
|
|
23
|
+
active = false
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
module,
|
|
29
|
+
dispatcher,
|
|
30
|
+
stats: () => ({ installCalls, disposerCalls, active }),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFakeUndici() {
|
|
35
|
+
let current = { kind: 'previous' }
|
|
36
|
+
const agents = []
|
|
37
|
+
class Agent {
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.options = options
|
|
40
|
+
agents.push(this)
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
return Promise.resolve()
|
|
44
|
+
}
|
|
45
|
+
destroy() {}
|
|
46
|
+
}
|
|
47
|
+
class Pool {
|
|
48
|
+
constructor(origin, options) {
|
|
49
|
+
this.origin = origin
|
|
50
|
+
this.options = options
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class ProxyAgent {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = options
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
module: {
|
|
60
|
+
Agent,
|
|
61
|
+
Pool,
|
|
62
|
+
ProxyAgent,
|
|
63
|
+
getGlobalDispatcher: () => current,
|
|
64
|
+
setGlobalDispatcher: (agent) => {
|
|
65
|
+
current = agent
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
agents,
|
|
69
|
+
current: () => current,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('harness mode installs the DSH policy and is idempotent', async () => {
|
|
74
|
+
const harness = makeFakeHarness()
|
|
75
|
+
const route = new RouteManager({ logger, loadHarness: async () => ({ module: harness.module }) })
|
|
76
|
+
|
|
77
|
+
const first = await route.install({ proxyUrl: 'http://127.0.0.1:20808' })
|
|
78
|
+
assert.equal(first.installed, true)
|
|
79
|
+
assert.equal(first.mode, 'harness')
|
|
80
|
+
assert.equal(first.webFetchCovered, true)
|
|
81
|
+
assert.equal(harness.stats().installCalls, 1)
|
|
82
|
+
|
|
83
|
+
const second = await route.install({ proxyUrl: 'http://127.0.0.1:20808' })
|
|
84
|
+
assert.equal(second.reason, 'already-installed')
|
|
85
|
+
assert.equal(harness.stats().installCalls, 1)
|
|
86
|
+
|
|
87
|
+
await route.restore()
|
|
88
|
+
assert.equal(harness.stats().active, false)
|
|
89
|
+
assert.equal(harness.stats().disposerCalls, 1)
|
|
90
|
+
assert.equal(route.snapshot().installed, false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('an existing launcher policy is respected under takeover=auto', async () => {
|
|
94
|
+
const harness = makeFakeHarness({ preinstalled: true })
|
|
95
|
+
const route = new RouteManager({ logger, loadHarness: async () => ({ module: harness.module }) })
|
|
96
|
+
const result = await route.install({ proxyUrl: 'http://127.0.0.1:20808', force: false })
|
|
97
|
+
assert.equal(result.installed, false)
|
|
98
|
+
assert.equal(result.reason, 'external-proxy-active')
|
|
99
|
+
assert.equal(harness.stats().installCalls, 0)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('takeover=force overrides an existing policy', async () => {
|
|
103
|
+
const harness = makeFakeHarness({ preinstalled: true })
|
|
104
|
+
const route = new RouteManager({ logger, loadHarness: async () => ({ module: harness.module }) })
|
|
105
|
+
const result = await route.install({ proxyUrl: 'http://127.0.0.1:20808', force: true })
|
|
106
|
+
assert.equal(result.installed, true)
|
|
107
|
+
assert.equal(harness.stats().installCalls, 1)
|
|
108
|
+
await route.restore()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('a policy that does not take effect is rejected and rolled back', async () => {
|
|
112
|
+
const harness = makeFakeHarness({ effective: false })
|
|
113
|
+
const route = new RouteManager({ logger, loadHarness: async () => ({ module: harness.module }) })
|
|
114
|
+
await assert.rejects(
|
|
115
|
+
() => route.install({ proxyUrl: 'http://127.0.0.1:20808' }),
|
|
116
|
+
(error) => error instanceof RouteError && error.code === 'route-instance-mismatch',
|
|
117
|
+
)
|
|
118
|
+
assert.equal(harness.stats().disposerCalls, 1)
|
|
119
|
+
assert.equal(route.snapshot().installed, false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('builtin mode installs its own dispatcher when the harness package is missing', async () => {
|
|
123
|
+
const undici = makeFakeUndici()
|
|
124
|
+
const route = new RouteManager({
|
|
125
|
+
logger,
|
|
126
|
+
loadHarness: async () => undefined,
|
|
127
|
+
loadUndici: async () => undici.module,
|
|
128
|
+
})
|
|
129
|
+
const snapshot = await route.install({ proxyUrl: 'http://127.0.0.1:20808', noProxy: 'corp.test' })
|
|
130
|
+
assert.equal(snapshot.mode, 'builtin')
|
|
131
|
+
assert.equal(snapshot.webFetchCovered, false)
|
|
132
|
+
assert.equal(undici.current(), undici.agents[0])
|
|
133
|
+
|
|
134
|
+
const factory = undici.agents[0].options.factory
|
|
135
|
+
assert.ok(factory(new URL('https://api.deepseek.com'), {}) instanceof undici.module.ProxyAgent)
|
|
136
|
+
assert.ok(factory(new URL('http://127.0.0.1:53343'), {}) instanceof undici.module.Pool)
|
|
137
|
+
assert.ok(factory(new URL('https://git.corp.test'), {}) instanceof undici.module.Pool)
|
|
138
|
+
|
|
139
|
+
await route.restore()
|
|
140
|
+
assert.equal(undici.current().kind, 'previous')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('a socks inbound always takes the builtin path', async () => {
|
|
144
|
+
let harnessLoaded = false
|
|
145
|
+
const undici = makeFakeUndici()
|
|
146
|
+
const route = new RouteManager({
|
|
147
|
+
logger,
|
|
148
|
+
loadHarness: async () => {
|
|
149
|
+
harnessLoaded = true
|
|
150
|
+
return { module: makeFakeHarness().module }
|
|
151
|
+
},
|
|
152
|
+
loadUndici: async () => undici.module,
|
|
153
|
+
})
|
|
154
|
+
const snapshot = await route.install({ proxyUrl: 'socks5://127.0.0.1:20809' })
|
|
155
|
+
assert.equal(harnessLoaded, false)
|
|
156
|
+
assert.equal(snapshot.mode, 'builtin')
|
|
157
|
+
await route.restore()
|
|
158
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { parseShareLink, parseShareLinks, ShareLinkError } from '../lib/share-link.js'
|
|
4
|
+
import { normalizeConfig } from '../lib/config.js'
|
|
5
|
+
import { buildXrayConfig } from '../lib/xray-config.js'
|
|
6
|
+
|
|
7
|
+
const KEY = 'A'.repeat(43)
|
|
8
|
+
const REALITY =
|
|
9
|
+
`vless://11111111-2222-4333-8444-555555555555@hk.example.com:443` +
|
|
10
|
+
`?encryption=none&security=reality&sni=www.example.com&fp=chrome` +
|
|
11
|
+
`&pbk=${KEY}&sid=ab12&spx=%2F&flow=xtls-rprx-vision&type=tcp#Hong%20Kong`
|
|
12
|
+
|
|
13
|
+
test('a REALITY share link becomes a node', () => {
|
|
14
|
+
const node = parseShareLink(REALITY)
|
|
15
|
+
assert.equal(node.id, 'Hong Kong')
|
|
16
|
+
assert.equal(node.address, 'hk.example.com')
|
|
17
|
+
assert.equal(node.port, 443)
|
|
18
|
+
assert.equal(node.uuid, '11111111-2222-4333-8444-555555555555')
|
|
19
|
+
assert.equal(node.flow, 'xtls-rprx-vision')
|
|
20
|
+
assert.equal(node.serverName, 'www.example.com')
|
|
21
|
+
assert.equal(node.fingerprint, 'chrome')
|
|
22
|
+
assert.equal(node.password, KEY)
|
|
23
|
+
assert.equal(node.publicKey, KEY)
|
|
24
|
+
assert.equal(node.shortId, 'ab12')
|
|
25
|
+
assert.equal(node.spiderX, '/')
|
|
26
|
+
assert.equal(node.network, 'raw')
|
|
27
|
+
assert.equal(node.security, 'reality')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('defaults fill the fields a short link omits', () => {
|
|
31
|
+
const node = parseShareLink(`vless://uuid-here@example.com?security=reality&sni=s.example.com&pbk=${KEY}`)
|
|
32
|
+
assert.equal(node.port, 443)
|
|
33
|
+
assert.equal(node.fingerprint, 'chrome')
|
|
34
|
+
assert.equal(node.spiderX, '/')
|
|
35
|
+
assert.equal(node.shortId, '')
|
|
36
|
+
assert.equal(node.flow, 'xtls-rprx-vision')
|
|
37
|
+
assert.equal(node.id, 'link-1')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a v6 literal loses its brackets', () => {
|
|
41
|
+
const node = parseShareLink(`vless://uuid-here@[2001:db8::1]:8443?security=none#v6`)
|
|
42
|
+
assert.equal(node.address, '2001:db8::1')
|
|
43
|
+
assert.equal(node.port, 8443)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('security=none links clear REALITY-only fields', () => {
|
|
47
|
+
const node = parseShareLink(`vless://uuid-here@example.com:8443?security=none&type=tcp#plain`)
|
|
48
|
+
assert.equal(node.security, 'none')
|
|
49
|
+
assert.equal(node.flow, '')
|
|
50
|
+
assert.equal(node.serverName, '')
|
|
51
|
+
assert.equal(node.shortId, '')
|
|
52
|
+
assert.equal(node.password, '')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('non-VLESS schemes are rejected with a named reason', () => {
|
|
56
|
+
assert.throws(
|
|
57
|
+
() => parseShareLink('vmess://eyJhZGQiOiJleGFtcGxlLmNvbSJ9'),
|
|
58
|
+
(error) => error instanceof ShareLinkError && error.code === 'unsupported-scheme',
|
|
59
|
+
)
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => parseShareLink('not a link'),
|
|
62
|
+
(error) => error instanceof ShareLinkError && error.code === 'invalid-link',
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('parseShareLinks keeps the good entries and names bad indexes', () => {
|
|
67
|
+
const { nodes, errors } = parseShareLinks([REALITY, 'vmess://x', `vless://uuid-here@example.com:443?security=none`])
|
|
68
|
+
assert.equal(nodes.length, 2)
|
|
69
|
+
assert.equal(errors.length, 1)
|
|
70
|
+
assert.equal(errors[0].field, 'shareLinks[1]')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('share links feed nodes[], activeNode and the generated config', () => {
|
|
74
|
+
const { config, errors } = normalizeConfig({
|
|
75
|
+
enabled: true,
|
|
76
|
+
shareLinks: [REALITY, `vless://uuid-here@jp.example.com:443?security=none&type=tcp#Japan`],
|
|
77
|
+
})
|
|
78
|
+
assert.deepEqual(errors, [])
|
|
79
|
+
assert.deepEqual(config.nodes.map((node) => node.id), ['Hong Kong', 'Japan'])
|
|
80
|
+
assert.deepEqual(config.nodes.map((node) => node.tag), ['proxy-Hong_Kong', 'proxy-Japan'])
|
|
81
|
+
assert.equal(config.activeNode, 'proxy-Hong_Kong')
|
|
82
|
+
|
|
83
|
+
const built = buildXrayConfig(config)
|
|
84
|
+
assert.deepEqual(built.outbounds.map((outbound) => outbound.tag), ['proxy-Hong_Kong', 'proxy-Japan', 'direct', 'block'])
|
|
85
|
+
assert.equal(built.outbounds[0].streamSettings.realitySettings.serverName, 'www.example.com')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('the singular shareLink field also works', () => {
|
|
89
|
+
const { config, errors } = normalizeConfig({ enabled: true, shareLink: REALITY })
|
|
90
|
+
assert.deepEqual(errors, [])
|
|
91
|
+
assert.equal(config.nodes.length, 1)
|
|
92
|
+
assert.equal(config.nodes[0].id, 'Hong Kong')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('explicit nodes stay first and links are appended', () => {
|
|
96
|
+
const { config } = normalizeConfig({
|
|
97
|
+
enabled: true,
|
|
98
|
+
nodes: [
|
|
99
|
+
{
|
|
100
|
+
id: 'manual',
|
|
101
|
+
address: 'manual.example.com',
|
|
102
|
+
port: 443,
|
|
103
|
+
uuid: '11111111-2222-4333-8444-555555555555',
|
|
104
|
+
serverName: 'www.example.com',
|
|
105
|
+
password: KEY,
|
|
106
|
+
shortId: 'ab12',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
shareLinks: [REALITY],
|
|
110
|
+
})
|
|
111
|
+
assert.deepEqual(config.nodes.map((node) => node.id), ['manual', 'Hong Kong'])
|
|
112
|
+
assert.equal(config.activeNode, 'proxy-manual')
|
|
113
|
+
})
|