dsh-ris-yds 0.0.0-stage → 1.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.
- package/LICENSE +21 -0
- package/README.md +160 -2
- package/assets/yds-chrome-resource-override.crx +0 -0
- package/bin/check-listing.mjs +247 -0
- package/bin/yds.js +19 -0
- package/cordis.patch.yml +13 -0
- package/lib/browser.js +428 -0
- package/lib/build.js +115 -0
- package/lib/cli.js +1130 -0
- package/lib/confcenter.js +377 -0
- package/lib/config.js +552 -0
- package/lib/fe-dev.js +206 -0
- package/lib/ide-proxy.js +906 -0
- package/lib/ide-watch.js +188 -0
- package/lib/ide.js +728 -0
- package/lib/index.js +267 -0
- package/lib/proc.js +270 -0
- package/lib/risd-peer.js +353 -0
- package/lib/rules.js +193 -0
- package/lib/socks.js +279 -0
- package/lib/token.js +36 -0
- package/lib/yms.js +336 -0
- package/lib/yoncode.js +249 -0
- package/native/yds-rust.darwin-arm64.node +0 -0
- package/native/yds-rust.darwin-x64.node +0 -0
- package/native/yds-rust.linux-arm64.node +0 -0
- package/native/yds-rust.linux-x64.node +0 -0
- package/native/yds-rust.win32-arm64.node +0 -0
- package/native/yds-rust.win32-x64.node +0 -0
- package/package.json +58 -4
- package/skills/dsh-ris-yds/SKILL.md +134 -0
- package/test/browser.test.mjs +442 -0
- package/test/cli-stop.test.mjs +40 -0
- package/test/cli.test.mjs +246 -0
- package/test/config.test.mjs +106 -0
- package/test/fe-dev.test.mjs +93 -0
- package/test/hints.test.mjs +13 -0
- package/test/ide-config.test.mjs +155 -0
- package/test/ide-proxy.test.mjs +256 -0
- package/test/ide-watch.test.mjs +79 -0
- package/test/launch-proxy.test.mjs +83 -0
- package/test/lease-release.test.mjs +76 -0
- package/test/mode-separation.test.mjs +147 -0
- package/test/pidfile.test.mjs +105 -0
- package/test/proc.test.mjs +172 -0
- package/test/proxy-standalone.test.mjs +121 -0
- package/test/risd-leases.test.mjs +111 -0
- package/test/risd-tier.test.mjs +361 -0
- package/test/rules-e2e.test.mjs +122 -0
- package/test/rules.test.mjs +154 -0
- package/test/yoncode.test.mjs +159 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// confcenter sync: pull the 4 YMS config files over YYCToken, cache the raw
|
|
2
|
+
// copies, and write the SOCKS-adapted (or direct) versions into yms-home/config.
|
|
3
|
+
// This replaces any bundled "golden config": a fresh environment pulls its own.
|
|
4
|
+
//
|
|
5
|
+
// Bump ADAPT_VERSION whenever the adaptation logic below changes shape: an
|
|
6
|
+
// existing yms-home/config written by an older adapter is then re-adapted from
|
|
7
|
+
// the cache on the next `yds start` (otherwise "yms_middleware.json exists"
|
|
8
|
+
// would short-circuit and old rewrites would stay live forever).
|
|
9
|
+
export const ADAPT_VERSION = '3'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Marker content for the current adaptation shape and mode.
|
|
13
|
+
* @param {boolean} rewrite - whether addresses were rewritten to 127.0.0.1.
|
|
14
|
+
* @returns {string} marker string compared by adaptIsCurrent().
|
|
15
|
+
*/
|
|
16
|
+
export function adaptMarker(rewrite) {
|
|
17
|
+
return `${ADAPT_VERSION}:${rewrite ? 'rewrite' : 'direct'}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Private IPv4 ranges (RFC1918). Hosts in these ranges are on the dev-machine
|
|
21
|
+
// side of the SOCKS tunnel and get rewritten to 127.0.0.1:<localPort>.
|
|
22
|
+
const PRIVATE_HOST = /^(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})$/
|
|
23
|
+
// Hostnames the SOCKS server cannot resolve/reach (k8s-internal DNS): left
|
|
24
|
+
// unrewritten so behavior is unchanged, with a loud warning.
|
|
25
|
+
const UNREACHABLE_HOST = /\.inner-yonbip-k8s\.com$/i
|
|
26
|
+
// host(:port) occurrences inside string values (urls, "h:p" lists, "h1:p1,h2:p2")
|
|
27
|
+
const HOSTPORT_RE = /\b((?:\d{1,3}\.){3}\d{1,3}|[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]):(\d{2,5})\b/g
|
|
28
|
+
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, renameSync } from 'node:fs'
|
|
30
|
+
import { join } from 'node:path'
|
|
31
|
+
import { getYycToken } from './token.js'
|
|
32
|
+
|
|
33
|
+
export const CONF_FILES = ['yonbip_config.json', 'yms_middleware.json', 'module_config.json', 'ip_node_mapping.json']
|
|
34
|
+
// ip_node_mapping.json is NOT served by this endpoint (it is generated locally
|
|
35
|
+
// by the YonCode sync step); the other three come from confcenter.
|
|
36
|
+
const PULLED_FILES = ['yonbip_config.json', 'yms_middleware.json', 'module_config.json']
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pull the confcenter bundle for one app.
|
|
40
|
+
* GET <workbench>/iuap-yms-manage/confcenter/web/v2/config/new/file
|
|
41
|
+
* ?env=<env>&version=2.0.0&c_isolate=default&apps=<module>
|
|
42
|
+
* Response JSON: { "<file name>": <parsed file content>, ... } — a top-level
|
|
43
|
+
* object keyed by file name (values are the parsed JSON of each file).
|
|
44
|
+
* Returns the files as JSON text (normalized for the cache + adapt step).
|
|
45
|
+
*/
|
|
46
|
+
export async function pullConfcenter(cfg, { signal } = {}) {
|
|
47
|
+
if (cfg.accessKey == null || cfg.accessSecret == null) throw new Error('缺少 accessKey/accessSecret,无法拉取配置')
|
|
48
|
+
if (cfg.module == null) throw new Error('未识别模块名,无法拉取配置')
|
|
49
|
+
const base = `${cfg.workbenchUrl}/iuap-yms-manage/confcenter/web/v2/config/new/file`
|
|
50
|
+
const url = `${base}?env=${encodeURIComponent(cfg.env)}&version=2.0.0&c_isolate=default&apps=${encodeURIComponent(cfg.module)}`
|
|
51
|
+
const token = await getYycToken(url, cfg.accessKey, cfg.accessSecret)
|
|
52
|
+
// timeout guard: hang DoS protection (fetch without a signal can hang forever)
|
|
53
|
+
const timeout = AbortSignal.timeout(60_000)
|
|
54
|
+
const signalOrTimeout = signal !== undefined ? AbortSignal.any([signal, timeout]) : timeout
|
|
55
|
+
const res = await fetch(url, { headers: { YYCToken: token }, signal: signalOrTimeout })
|
|
56
|
+
if (!res.ok) throw new Error(`confcenter HTTP ${res.status}`)
|
|
57
|
+
const body = await res.json()
|
|
58
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
59
|
+
throw new Error(`confcenter 响应形状异常: ${String(body).slice(0, 120)}`)
|
|
60
|
+
}
|
|
61
|
+
const files = {}
|
|
62
|
+
for (const [name, content] of Object.entries(body)) {
|
|
63
|
+
const fileName = String(name).split('/').pop()
|
|
64
|
+
if (!PULLED_FILES.includes(fileName)) continue
|
|
65
|
+
// accept either the parsed object or raw text
|
|
66
|
+
files[fileName] = typeof content === 'string' ? content : JSON.stringify(content, null, 2)
|
|
67
|
+
}
|
|
68
|
+
const missing = PULLED_FILES.filter((f) => files[f] === undefined)
|
|
69
|
+
if (missing.length > 0) throw new Error(`confcenter 响应缺文件: ${missing.join(', ')}`)
|
|
70
|
+
return files
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read the raw cache; used when the network is unreachable (offline start). */
|
|
74
|
+
export function readConfCache(cfg) {
|
|
75
|
+
const dir = join(cfg.stateDir, 'confcenter')
|
|
76
|
+
const files = {}
|
|
77
|
+
for (const name of CONF_FILES) {
|
|
78
|
+
const p = join(dir, name)
|
|
79
|
+
if (existsSync(p)) files[name] = readFileSync(p, 'utf8')
|
|
80
|
+
}
|
|
81
|
+
return files
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function writeConfCache(cfg, files) {
|
|
85
|
+
const dir = join(cfg.stateDir, 'confcenter')
|
|
86
|
+
// 0o700/0o600: the cache carries DB/Redis endpoints + credentials from
|
|
87
|
+
// confcenter — keep it owner-only.
|
|
88
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
89
|
+
for (const name of CONF_FILES) {
|
|
90
|
+
if (files[name] !== undefined) writeFileSync(join(dir, name), files[name], { encoding: 'utf8', mode: 0o600 })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Adapt + write the 4 files into yms-home/config.
|
|
96
|
+
* SOCKS mode rewrites Oracle/MySQL hosts to 127.0.0.1:<dbForwardPort> and
|
|
97
|
+
* Redis pools to 127.0.0.1:<redisForwardPort>; direct mode keeps original
|
|
98
|
+
* hosts. ip_node_mapping always points the module at the callback address
|
|
99
|
+
* (serverBindAddress:serverPort).
|
|
100
|
+
* Returns a summary of what was written.
|
|
101
|
+
*/
|
|
102
|
+
/** Atomic write: tmp file + rename, so a crash mid-write never leaves a truncated live config. */
|
|
103
|
+
function atomicWrite(path, text, mode) {
|
|
104
|
+
const tmp = `${path}.tmp-${process.pid}`
|
|
105
|
+
writeFileSync(tmp, text, { encoding: 'utf8', mode })
|
|
106
|
+
renameSync(tmp, path)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write the four YMS config files, adapting every internal endpoint to this
|
|
111
|
+
* machine.
|
|
112
|
+
*
|
|
113
|
+
* `rewrite` decides WHICH adaptation applies:
|
|
114
|
+
* - true (fallback): private endpoints are replaced with 127.0.0.1:<localPort>
|
|
115
|
+
* and the forwarder binds those ports, so a JDBC/Redis client that
|
|
116
|
+
* cannot speak SOCKS still reaches the real server.
|
|
117
|
+
* - false (interception): endpoints keep their real host:port because a
|
|
118
|
+
* system-level redirector (ProxyBridge) already sends the java
|
|
119
|
+
* process' connections through the tunnel. Nothing is bound locally.
|
|
120
|
+
*
|
|
121
|
+
* Targets are collected either way: the redirector profile needs them.
|
|
122
|
+
* @param {object} cfg - resolved plugin config.
|
|
123
|
+
* @param {Record<string,string>} files - confcenter cache contents.
|
|
124
|
+
* `dir` defaults to `<ymsHome>/config`; the direct-launch copy passes the
|
|
125
|
+
* sibling `config-direct` instead, so the two launch modes never overwrite each
|
|
126
|
+
* other's endpoint addresses.
|
|
127
|
+
* @param {{rewrite?: boolean, dir?: string|null}} [options] - rewrite=false keeps
|
|
128
|
+
* real addresses; dir overrides the output directory.
|
|
129
|
+
* @returns {{files: object, dbTargets: object[], rewrite: boolean}} outcome.
|
|
130
|
+
*/
|
|
131
|
+
export function adaptAndWriteConfig(cfg, files, { rewrite = true, dir = null } = {}) {
|
|
132
|
+
const cfgDir = dir ?? join(cfg.ymsHome, 'config')
|
|
133
|
+
mkdirSync(cfgDir, { recursive: true })
|
|
134
|
+
|
|
135
|
+
let mw
|
|
136
|
+
try {
|
|
137
|
+
mw = JSON.parse(files['yms_middleware.json'])
|
|
138
|
+
} catch (e) {
|
|
139
|
+
throw new Error(`yms_middleware.json 解析失败: ${e.message}——删除 .dsh-ris-yds/confcenter/ 缓存后重试(yds confcenter)`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const dbTargets = [] // { host, port, localPort? } original endpoints for the forwarder
|
|
143
|
+
if (cfg.socks !== null) {
|
|
144
|
+
for (const pool of Object.values(mw.connectionPools ?? {})) {
|
|
145
|
+
if (typeof pool?.url === 'string' && pool.url.startsWith('jdbc:')) {
|
|
146
|
+
const m = parseJdbcEndpoint(pool.url)
|
|
147
|
+
if (m !== null) {
|
|
148
|
+
dbTargets.push({ host: m.host, port: m.port })
|
|
149
|
+
if (rewrite) pool.url = m.rewrite(`127.0.0.1`, cfg.dbForwardPort)
|
|
150
|
+
} else {
|
|
151
|
+
console.warn(`[yds] 警告: 无法解析 JDBC url(未重写、未转发): ${pool.url}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const pool of Object.values(mw['redis.pools'] ?? {})) {
|
|
156
|
+
if (typeof pool?.host === 'string') {
|
|
157
|
+
dbTargets.push({ host: pool.host, port: Number(pool.port) || 6379 })
|
|
158
|
+
if (rewrite) {
|
|
159
|
+
pool.host = '127.0.0.1'
|
|
160
|
+
pool.port = cfg.redisForwardPort
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 0o600: yms_middleware.json carries DB/Redis credentials.
|
|
166
|
+
atomicWrite(join(cfgDir, 'yms_middleware.json'), JSON.stringify(mw, null, 2), 0o600)
|
|
167
|
+
|
|
168
|
+
// module_config / yonbip_config: verbatim (platform global options);
|
|
169
|
+
// in SOCKS mode yonbip_config additionally gets its internal endpoints
|
|
170
|
+
// (kafka / ES / license-server / ...) rewritten to the local tunnel ports.
|
|
171
|
+
for (const name of ['module_config.json', 'yonbip_config.json']) {
|
|
172
|
+
if (typeof files[name] !== 'string') {
|
|
173
|
+
throw new Error(`confcenter 缓存缺 ${name}——先执行 yds confcenter 在线拉取一次`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let yonbipText = files['yonbip_config.json']
|
|
177
|
+
if (cfg.socks !== null) {
|
|
178
|
+
let r
|
|
179
|
+
try {
|
|
180
|
+
r = rewriteYonbipInternal(yonbipText)
|
|
181
|
+
} catch (e) {
|
|
182
|
+
throw new Error(`yonbip_config.json 解析失败: ${e.message}——删除 .dsh-ris-yds/confcenter/ 缓存后重试(yds confcenter)`)
|
|
183
|
+
}
|
|
184
|
+
yonbipText = r.text
|
|
185
|
+
dbTargets.push(...r.targets)
|
|
186
|
+
}
|
|
187
|
+
atomicWrite(join(cfgDir, 'module_config.json'), files['module_config.json'], 0o600)
|
|
188
|
+
atomicWrite(join(cfgDir, 'yonbip_config.json'), yonbipText, 0o600)
|
|
189
|
+
|
|
190
|
+
// ip_node_mapping: route the module to our callback address. Merge with the
|
|
191
|
+
// existing file (YonCode sync may have written other modules' entries) so a
|
|
192
|
+
// shared workspace keeps working for both sides.
|
|
193
|
+
const mappingPath = join(cfgDir, 'ip_node_mapping.json')
|
|
194
|
+
let mapping = {}
|
|
195
|
+
let source = files['ip_node_mapping.json'] // may be undefined (not served)
|
|
196
|
+
if (source === undefined && existsSync(mappingPath)) source = readFileSync(mappingPath, 'utf8')
|
|
197
|
+
if (source !== undefined) {
|
|
198
|
+
try {
|
|
199
|
+
mapping = JSON.parse(source)
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// malformed existing mapping: back it up, start from an empty mapping,
|
|
202
|
+
// and say so — silently replacing it would wipe other modules' entries.
|
|
203
|
+
try {
|
|
204
|
+
renameSync(mappingPath, `${mappingPath}.bak-${Date.now()}`)
|
|
205
|
+
console.warn(`[yds] 警告: 现有 ip_node_mapping.json 解析失败(${e.message})——已备份为 .bak-* 并重建,其它模块条目可能丢失`)
|
|
206
|
+
} catch { /* ignore */ }
|
|
207
|
+
mapping = {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const entry = mapping[cfg.module] ?? {}
|
|
211
|
+
entry.context = cfg.context
|
|
212
|
+
entry.ip_nodes = [`${cfg.serverBindAddress}:${cfg.serverPort}`]
|
|
213
|
+
mapping[cfg.module] = entry
|
|
214
|
+
const mappingText = JSON.stringify(mapping, null, 2)
|
|
215
|
+
atomicWrite(mappingPath, mappingText, 0o600)
|
|
216
|
+
const filesOut = { ...files, 'ip_node_mapping.json': mappingText }
|
|
217
|
+
|
|
218
|
+
// adaptation stamp: prepareStart re-adapts from the cache when this is
|
|
219
|
+
// missing or stale (adapter logic upgrades must not leave old rewrites live).
|
|
220
|
+
writeFileSync(join(cfg.stateDir, 'adapt-version'), adaptMarker(rewrite), 'utf8')
|
|
221
|
+
|
|
222
|
+
// variables/default.properties: keep the build's copy; create a minimal one
|
|
223
|
+
// when the build did not ship it (kernel template normally does).
|
|
224
|
+
const variables = join(cfgDir, 'variables', 'default.properties')
|
|
225
|
+
if (!existsSync(variables)) {
|
|
226
|
+
mkdirSync(join(cfgDir, 'variables'), { recursive: true })
|
|
227
|
+
writeFileSync(variables, [
|
|
228
|
+
'# generated by dsh-ris-yds',
|
|
229
|
+
`yms.container.web.port=${cfg.ymsPort}`,
|
|
230
|
+
'yms.container.web.https.port=9443',
|
|
231
|
+
'yms.container.workerThreads=500',
|
|
232
|
+
'yms.container.maxPostSize=11534336',
|
|
233
|
+
'',
|
|
234
|
+
].join('\n'), 'utf8')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { mode: cfg.socks !== null ? 'socks' : 'direct', dbTargets, files: filesOut }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Parse host:port out of the common JDBC url forms so we can rewrite them
|
|
242
|
+
* for the SOCKS tunnel:
|
|
243
|
+
* jdbc:oracle:thin:@//host:port/service (Oracle service form)
|
|
244
|
+
* jdbc:oracle:thin:@host:port:SID (Oracle SID form)
|
|
245
|
+
* jdbc:mysql://host:port/db?params (MySQL)
|
|
246
|
+
* jdbc:mysql://host:port (MySQL no-db)
|
|
247
|
+
* Returns { host, port, rewrite(host, port) } or null (unknown form).
|
|
248
|
+
*/
|
|
249
|
+
export function parseJdbcEndpoint(url) {
|
|
250
|
+
let m = /^jdbc:[a-z0-9]+:thin:@\/\/([^/:]+):(\d+)(?=[/\s]|$)/.exec(url)
|
|
251
|
+
if (m !== null) {
|
|
252
|
+
return { host: m[1], port: Number(m[2]), rewrite: (h, p) => url.replace(/(@\/\/)[^/:]+:\d+/, `$1${h}:${p}`) }
|
|
253
|
+
}
|
|
254
|
+
m = /^jdbc:[a-z0-9]+:thin:@([^/:]+):(\d+):/.exec(url) // SID form
|
|
255
|
+
if (m !== null) {
|
|
256
|
+
return { host: m[1], port: Number(m[2]), rewrite: (h, p) => url.replace(/(thin:@)[^/:]+:\d+/, `$1${h}:${p}`) }
|
|
257
|
+
}
|
|
258
|
+
m = /^jdbc:mysql:\/\/([^/:]+):(\d+)(?=[/\s]|$)/.exec(url)
|
|
259
|
+
if (m !== null) {
|
|
260
|
+
return { host: m[1], port: Number(m[2]), rewrite: (h, p) => url.replace(/(jdbc:mysql:\/\/)[^/:]+:\d+/, `$1${h}:${p}`) }
|
|
261
|
+
}
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Rewrite the internal (RFC1918) endpoints inside yonbip_config.json to
|
|
267
|
+
* 127.0.0.1:<same port> so they traverse the SOCKS forwarder, and collect the
|
|
268
|
+
* tunnel targets. Rules (each backed by a probe of the real dev network):
|
|
269
|
+
* - host:port with a private IP host and port >= 1024 -> rewrite, tunnel
|
|
270
|
+
* (kafka 6880, ES 6888, license 30001, ...).
|
|
271
|
+
* - bare-IP values (e.g. middleware_es.elastic.host) -> 127.0.0.1, but only
|
|
272
|
+
* when that same host already has a tunneled port (the port comes from a
|
|
273
|
+
* sibling key).
|
|
274
|
+
* - *.inner-yonbip-k8s.com (e.g. domain.registry.url) -> NOT rewritten: the
|
|
275
|
+
* SOCKS server cannot resolve k8s-internal DNS, so rewriting would just
|
|
276
|
+
* move the failure; warn loudly instead.
|
|
277
|
+
* - port < 1024 (e.g. objectStorage SFTP :22) -> skipped, cannot bind
|
|
278
|
+
* privileged local ports; warn.
|
|
279
|
+
* - non-private hosts (${domain.url} placeholders, public names) -> verbatim.
|
|
280
|
+
* Returns { text, targets } where targets = [{ host, port, localPort }] and
|
|
281
|
+
* localPort === port (deterministic 1:1 mapping, readable in logs).
|
|
282
|
+
*/
|
|
283
|
+
export function rewriteYonbipInternal(yonbipText) {
|
|
284
|
+
const doc = JSON.parse(yonbipText) // caller handles parse errors
|
|
285
|
+
const targets = []
|
|
286
|
+
const seen = new Set()
|
|
287
|
+
const warned = new Set()
|
|
288
|
+
const warnOnce = (msg) => {
|
|
289
|
+
if (!warned.has(msg)) { warned.add(msg); console.warn(`[yds] ${msg}`) }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// pass 1: collect eligible host:port targets (also detects k8s-internal
|
|
293
|
+
// hostnames and privileged ports for warnings). Bare private-IP values are
|
|
294
|
+
// NOT collected — they only ride along once their host has a real tunnel.
|
|
295
|
+
const collect = (node) => {
|
|
296
|
+
if (typeof node === 'string') {
|
|
297
|
+
if (UNREACHABLE_HOST.test(node) || (node.includes('://') && UNREACHABLE_HOST.test(node.split('://')[1] ?? ''))) {
|
|
298
|
+
warnOnce(`内网域名 ${node} 无法经 SOCKS 解析/转发(k8s 内部 DNS),保持原值——相关功能(服务注册发现等)将直连失败,属已知限制`)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
for (const m of node.matchAll(HOSTPORT_RE)) {
|
|
302
|
+
const host = m[1]
|
|
303
|
+
const port = Number(m[2])
|
|
304
|
+
if (!PRIVATE_HOST.test(host)) continue
|
|
305
|
+
if (UNREACHABLE_HOST.test(host)) continue
|
|
306
|
+
if (port < 1024) {
|
|
307
|
+
warnOnce(`内网端口 ${host}:${port} < 1024,本地无法监听,未隧道化(保持原值直连)`)
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
const key = `${host}:${port}`
|
|
311
|
+
if (!seen.has(key)) { seen.add(key); targets.push({ host, port, localPort: port }) }
|
|
312
|
+
}
|
|
313
|
+
} else if (node !== null && typeof node === 'object') {
|
|
314
|
+
for (const v of Object.values(node)) collect(v)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// pass 2: rewrite (order-independent: targets are already known)
|
|
319
|
+
const rewrite = (node) => {
|
|
320
|
+
if (typeof node === 'string') {
|
|
321
|
+
if (PRIVATE_HOST.test(node)) {
|
|
322
|
+
// bare private IP (e.g. middleware_es.elastic.host): its port comes
|
|
323
|
+
// from a sibling key, so rewrite only when the host has a tunnel.
|
|
324
|
+
return seenHosts().has(node) ? '127.0.0.1' : node
|
|
325
|
+
}
|
|
326
|
+
return node.replace(HOSTPORT_RE, (full, host, portStr) => {
|
|
327
|
+
const port = Number(portStr)
|
|
328
|
+
if (!PRIVATE_HOST.test(host) || UNREACHABLE_HOST.test(host) || port < 1024) return full
|
|
329
|
+
if (!seen.has(`${host}:${port}`)) return full
|
|
330
|
+
return `127.0.0.1:${port}`
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
if (node !== null && typeof node === 'object') {
|
|
334
|
+
for (const k of Object.keys(node)) node[k] = rewrite(node[k])
|
|
335
|
+
}
|
|
336
|
+
return node
|
|
337
|
+
}
|
|
338
|
+
const hostSetCache = { set: null }
|
|
339
|
+
const seenHosts = () => {
|
|
340
|
+
if (hostSetCache.set === null) {
|
|
341
|
+
hostSetCache.set = new Set([...seen].map((k) => k.split(':')[0]))
|
|
342
|
+
}
|
|
343
|
+
return hostSetCache.set
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
collect(doc)
|
|
347
|
+
const text = JSON.stringify(rewrite(doc), null, 2)
|
|
348
|
+
return { text, targets }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Extract the internal tunnel targets from a CACHED (raw) yonbip_config.json
|
|
353
|
+
* without rewriting anything — used by the forwarder bootstrap when the
|
|
354
|
+
* yms-home copy is already adapted. Same eligibility rules as
|
|
355
|
+
* rewriteYonbipInternal (private host, port >= 1024, not k8s-internal DNS).
|
|
356
|
+
*/
|
|
357
|
+
export function extractYonbipTargets(yonbipText) {
|
|
358
|
+
const doc = JSON.parse(yonbipText)
|
|
359
|
+
const targets = []
|
|
360
|
+
const seen = new Set()
|
|
361
|
+
const walk = (node) => {
|
|
362
|
+
if (typeof node === 'string') {
|
|
363
|
+
for (const m of node.matchAll(HOSTPORT_RE)) {
|
|
364
|
+
const host = m[1]
|
|
365
|
+
const port = Number(m[2])
|
|
366
|
+
if (PRIVATE_HOST.test(host) && !UNREACHABLE_HOST.test(host) && port >= 1024) {
|
|
367
|
+
const key = `${host}:${port}`
|
|
368
|
+
if (!seen.has(key)) { seen.add(key); targets.push({ host, port, localPort: port }) }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} else if (node !== null && typeof node === 'object') {
|
|
372
|
+
for (const v of Object.values(node)) walk(v)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
walk(doc)
|
|
376
|
+
return targets
|
|
377
|
+
}
|