@twinsearth/dsh-2xray 0.0.0-stage → 0.1.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/bin/README.md +27 -0
  5. package/cordis.patch.yml +37 -0
  6. package/docs/AC-0.md +60 -0
  7. package/docs/EVIDENCE.md +135 -0
  8. package/examples/config.downgrade.json +20 -0
  9. package/examples/config.example.json +22 -0
  10. package/examples/config.multi.downgrade.json +39 -0
  11. package/examples/config.multi.json +46 -0
  12. package/icon.svg +9 -0
  13. package/index.js +89 -0
  14. package/lib/binary.js +133 -0
  15. package/lib/config.js +401 -0
  16. package/lib/control/client.js +100 -0
  17. package/lib/control/grpc.js +121 -0
  18. package/lib/control/protobuf.js +143 -0
  19. package/lib/health.js +109 -0
  20. package/lib/log.js +20 -0
  21. package/lib/paths.js +65 -0
  22. package/lib/process.js +154 -0
  23. package/lib/route.js +299 -0
  24. package/lib/share-link.js +132 -0
  25. package/lib/supervisor.js +687 -0
  26. package/lib/tools.js +106 -0
  27. package/lib/xray-config.js +225 -0
  28. package/locale/en.json +6 -0
  29. package/locale/zh.json +6 -0
  30. package/package.json +42 -4
  31. package/scripts/check-config.mjs +56 -0
  32. package/scripts/parse-share-link.mjs +23 -0
  33. package/scripts/probe-routing.mjs +66 -0
  34. package/scripts/release-check.mjs +104 -0
  35. package/test/binary.test.js +103 -0
  36. package/test/config.test.js +106 -0
  37. package/test/control.test.js +166 -0
  38. package/test/health.test.js +75 -0
  39. package/test/index.test.js +52 -0
  40. package/test/multi-node.test.js +111 -0
  41. package/test/route.test.js +158 -0
  42. package/test/share-link.test.js +113 -0
  43. package/test/supervisor.test.js +525 -0
  44. package/test/tools.test.js +88 -0
  45. 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
+ })