dshost-plugin 0.1.15 → 0.1.16
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/lib/core.js +393 -3
- package/package.json +1 -1
package/lib/core.js
CHANGED
|
@@ -19,8 +19,93 @@ const WS_SEND_HIGH_WATER = parseInt(process.env.WS_SEND_HIGH_WATER || String(8 *
|
|
|
19
19
|
const RECONNECT_BASE_MS = 1000;
|
|
20
20
|
const RECONNECT_MAX_MS = 60000;
|
|
21
21
|
|
|
22
|
+
const IGNORE_IFACE_RE = /^(lo|docker\d*|br-[0-9a-f]+|veth[0-9a-f]+|virbr\d*|cni\d*|flannel\d*)$/i;
|
|
23
|
+
const TRANSPARENT_GIF = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enumerate and classify all candidate IPv4 addresses for intranet direct probing.
|
|
27
|
+
* Excludes loopback, internal interfaces, Docker bridges, container veths,
|
|
28
|
+
* sing-box/clash virtual tun interfaces (e.g. 172.18.0.1), and public WAN IPs.
|
|
29
|
+
* Categorizes EasyTier (10.144.x.x), Tailscale (100.64-127.x.x), and LAN (192.168.x.x, 10.x.x.x).
|
|
30
|
+
* @returns {Array<{ip: string, name: string, type: 'lan'|'vpn', desc: string, prio: number}>}
|
|
31
|
+
*/
|
|
32
|
+
export function collectCandidateIpv4s() {
|
|
33
|
+
const list = [];
|
|
34
|
+
const seenIps = new Set();
|
|
35
|
+
try {
|
|
36
|
+
const ifaces = os.networkInterfaces();
|
|
37
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
38
|
+
if (IGNORE_IFACE_RE.test(name)) continue;
|
|
39
|
+
for (const a of addrs || []) {
|
|
40
|
+
if (a.family !== 'IPv4' || a.internal) continue;
|
|
41
|
+
const ip = a.address;
|
|
42
|
+
if (!ip || ip.startsWith('127.') || ip.startsWith('169.254.') || ip === '0.0.0.0') continue;
|
|
43
|
+
if (seenIps.has(ip)) continue;
|
|
44
|
+
|
|
45
|
+
// 1. Identify virtual tun/tap interfaces
|
|
46
|
+
const isTunTap = /^(tun|tap|utun)/i.test(name);
|
|
47
|
+
const isEasyTier = ip.startsWith('10.144.') || /easytier/i.test(name);
|
|
48
|
+
const isTailscale = /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip) || /tailscale/i.test(name);
|
|
49
|
+
|
|
50
|
+
// If it's a tun/tap interface but NOT a recognized mesh VPN, it is a local proxy
|
|
51
|
+
// (e.g. sing-box/clash fake-ip or 172.18.0.1 tun) -> STRICTLY EXCLUDE.
|
|
52
|
+
if (isTunTap && !isEasyTier && !isTailscale) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Filter out 172.16.0.0/12 (Docker networks, sing-box tun endpoints)
|
|
57
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let type = null;
|
|
62
|
+
let desc = name;
|
|
63
|
+
let prio = 99;
|
|
64
|
+
|
|
65
|
+
if (isEasyTier) {
|
|
66
|
+
type = 'vpn';
|
|
67
|
+
desc = 'EasyTier 专网';
|
|
68
|
+
prio = 1; // Prioritize mesh VPN across heterogeneous networks
|
|
69
|
+
} else if (isTailscale) {
|
|
70
|
+
type = 'vpn';
|
|
71
|
+
desc = 'Tailscale 专网';
|
|
72
|
+
prio = 1;
|
|
73
|
+
} else if (ip.startsWith('192.168.')) {
|
|
74
|
+
type = 'lan';
|
|
75
|
+
desc = '物理局域网 (192.168.x)';
|
|
76
|
+
prio = 2;
|
|
77
|
+
} else if (ip.startsWith('10.')) {
|
|
78
|
+
type = 'lan';
|
|
79
|
+
desc = '私有内网 (10.x)';
|
|
80
|
+
prio = 3;
|
|
81
|
+
} else {
|
|
82
|
+
// Public WAN IPs MUST NEVER be reported as intranet direct probe candidates!
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
seenIps.add(ip);
|
|
87
|
+
list.push({ ip, name, type, desc, prio });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
|
|
92
|
+
// Explicit override or additional candidate IPs via env var
|
|
93
|
+
const envCandidates = (process.env.DSH_CANDIDATE_IPS || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
94
|
+
for (const raw of envCandidates) {
|
|
95
|
+
if (!seenIps.has(raw) && /^(\d{1,3}\.){3}\d{1,3}$/.test(raw)) {
|
|
96
|
+
seenIps.add(raw);
|
|
97
|
+
list.unshift({ ip: raw, name: 'env', type: 'lan', desc: '指定内网地址', prio: 0 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
list.sort((a, b) => a.prio - b.prio);
|
|
102
|
+
return list;
|
|
103
|
+
}
|
|
104
|
+
|
|
22
105
|
/** First non-internal IPv4 of this host (for the dashboard), or null. */
|
|
23
106
|
function localIPv4() {
|
|
107
|
+
const c = collectCandidateIpv4s();
|
|
108
|
+
if (c.length > 0) return c[0].ip;
|
|
24
109
|
try {
|
|
25
110
|
for (const addrs of Object.values(os.networkInterfaces())) {
|
|
26
111
|
for (const a of addrs || []) {
|
|
@@ -242,20 +327,28 @@ function detectDshMetadata() {
|
|
|
242
327
|
}
|
|
243
328
|
|
|
244
329
|
/** Collect local system info for the relay dashboard. */
|
|
245
|
-
export function collectSystemInfo(defaultVersion) {
|
|
330
|
+
export function collectSystemInfo(defaultVersion, lanBridgeInfo = null) {
|
|
246
331
|
const disks = collectDisks();
|
|
247
332
|
const diskTotal = disks.reduce((a, d) => a + d.total, 0);
|
|
248
333
|
const diskUsed = disks.reduce((a, d) => a + d.used, 0);
|
|
249
334
|
const diskFree = disks.reduce((a, d) => a + d.free, 0);
|
|
250
335
|
const cpus = os.cpus();
|
|
251
336
|
const meta = detectDshMetadata();
|
|
337
|
+
const candidates = collectCandidateIpv4s();
|
|
338
|
+
const bridgePort = lanBridgeInfo?.port || parseInt(process.env.LAN_BRIDGE_PORT || '3088', 10);
|
|
339
|
+
const bridgeEnabled = lanBridgeInfo ? lanBridgeInfo.enabled : process.env.LAN_BRIDGE_ENABLED !== '0';
|
|
252
340
|
return {
|
|
253
341
|
instanceId: getEffectiveInstanceId(),
|
|
254
342
|
hostname: os.hostname(),
|
|
255
343
|
platform: os.platform(),
|
|
256
344
|
release: os.release(),
|
|
257
345
|
arch: os.arch(),
|
|
258
|
-
ip: localIPv4(),
|
|
346
|
+
ip: candidates[0]?.ip || localIPv4(),
|
|
347
|
+
network: {
|
|
348
|
+
bridgePort,
|
|
349
|
+
bridgeEnabled,
|
|
350
|
+
candidates,
|
|
351
|
+
},
|
|
259
352
|
cpuModel: cpus[0]?.model || '',
|
|
260
353
|
cpuCores: cpus.length,
|
|
261
354
|
loadAvg: os.loadavg(),
|
|
@@ -347,6 +440,244 @@ export function normalizeServices(raw, log) {
|
|
|
347
440
|
return out;
|
|
348
441
|
}
|
|
349
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Start lightweight LAN bridge listener for browser probing and direct LAN offloading.
|
|
445
|
+
* - Handles /__dsh_probe.gif and /__dsh_probe with CORS + Private Network Access headers.
|
|
446
|
+
* - Reverse proxies all other requests to local DSH (127.0.0.1:3080) rewriting Host/Origin.
|
|
447
|
+
* - Forwards WebSocket upgrades cleanly.
|
|
448
|
+
*/
|
|
449
|
+
export function startLanBridge({
|
|
450
|
+
port = 3088,
|
|
451
|
+
dshHost = '127.0.0.1',
|
|
452
|
+
dshPort = 3080,
|
|
453
|
+
getInstanceId: getInstId = getInstanceId,
|
|
454
|
+
getWebToken = null,
|
|
455
|
+
getAuthCookie = null,
|
|
456
|
+
mintAuthCookie = null,
|
|
457
|
+
log = console,
|
|
458
|
+
}) {
|
|
459
|
+
let isListening = false;
|
|
460
|
+
// Track authenticated client IPs with expiration (7 days)
|
|
461
|
+
const authenticatedClients = new Map();
|
|
462
|
+
|
|
463
|
+
function isClientAuthed(ip, cookieHeader = '') {
|
|
464
|
+
if (cookieHeader.includes('dsh-auth-') || cookieHeader.includes('dsh_lan_auth=1')) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
const exp = authenticatedClients.get(ip);
|
|
468
|
+
if (exp && exp > Date.now()) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function markClientAuthed(ip) {
|
|
475
|
+
if (ip) {
|
|
476
|
+
authenticatedClients.set(ip, Date.now() + 7 * 86400000);
|
|
477
|
+
if (authenticatedClients.size > 1000) {
|
|
478
|
+
const now = Date.now();
|
|
479
|
+
for (const [k, v] of authenticatedClients) {
|
|
480
|
+
if (v <= now) authenticatedClients.delete(k);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const server = http.createServer((req, res) => {
|
|
487
|
+
// CORS + PNA headers for modern browser cross-network access
|
|
488
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
489
|
+
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
490
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, HEAD');
|
|
491
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
492
|
+
|
|
493
|
+
if (req.method === 'OPTIONS') {
|
|
494
|
+
res.writeHead(204);
|
|
495
|
+
res.end();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const rawUrl = req.url || '/';
|
|
500
|
+
const pathOnly = rawUrl.split('?')[0];
|
|
501
|
+
|
|
502
|
+
// 1. Lightweight probe image for PNA / Mixed Content bypass
|
|
503
|
+
if (pathOnly === '/__dsh_probe.gif') {
|
|
504
|
+
res.setHeader('Content-Type', 'image/gif');
|
|
505
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
506
|
+
res.writeHead(200);
|
|
507
|
+
res.end(TRANSPARENT_GIF);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 2. JSON probe endpoint
|
|
512
|
+
if (pathOnly === '/__dsh_probe') {
|
|
513
|
+
res.setHeader('Content-Type', 'application/json');
|
|
514
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
515
|
+
res.writeHead(200);
|
|
516
|
+
res.end(JSON.stringify({
|
|
517
|
+
ok: true,
|
|
518
|
+
instanceId: getInstId ? getInstId() : null,
|
|
519
|
+
hostname: os.hostname(),
|
|
520
|
+
ts: Date.now(),
|
|
521
|
+
}));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const clientIp = req.socket?.remoteAddress;
|
|
526
|
+
const incomingCookie = req.headers.cookie || '';
|
|
527
|
+
|
|
528
|
+
// Check if request query contains a launch/direct token
|
|
529
|
+
let hasValidTokenInQuery = false;
|
|
530
|
+
let queryToken = null;
|
|
531
|
+
try {
|
|
532
|
+
const u = new URL(rawUrl, 'http://127.0.0.1');
|
|
533
|
+
queryToken = u.searchParams.get('token');
|
|
534
|
+
const activeToken = getWebToken ? getWebToken() : null;
|
|
535
|
+
if (queryToken && activeToken && queryToken === activeToken) {
|
|
536
|
+
hasValidTokenInQuery = true;
|
|
537
|
+
markClientAuthed(clientIp);
|
|
538
|
+
}
|
|
539
|
+
} catch {}
|
|
540
|
+
|
|
541
|
+
const clientAuthed = hasValidTokenInQuery || isClientAuthed(clientIp, incomingCookie);
|
|
542
|
+
|
|
543
|
+
// 3. Reverse proxy to local dsh on 127.0.0.1:3080
|
|
544
|
+
const dshHeaders = { ...req.headers };
|
|
545
|
+
dshHeaders.host = `${dshHost}:${dshPort}`;
|
|
546
|
+
if (dshHeaders.origin) {
|
|
547
|
+
dshHeaders.origin = `http://${dshHost}:${dshPort}`;
|
|
548
|
+
}
|
|
549
|
+
if (clientIp) {
|
|
550
|
+
dshHeaders['x-forwarded-for'] = clientIp;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Auth injection: If request lacks dsh-auth- cookie, but client is authenticated
|
|
554
|
+
// (via token exchange, dsh_lan_auth cookie, or client IP match),
|
|
555
|
+
// inject active authCookie so local DSH never 401s on browser cross-origin redirects
|
|
556
|
+
const currentAuthCookie = getAuthCookie ? getAuthCookie() : null;
|
|
557
|
+
if (!incomingCookie.includes('dsh-auth-') && clientAuthed && currentAuthCookie) {
|
|
558
|
+
dshHeaders.cookie = incomingCookie ? `${incomingCookie}; ${currentAuthCookie}` : currentAuthCookie;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const dshReq = http.request(
|
|
562
|
+
`http://${dshHost}:${dshPort}${rawUrl}`,
|
|
563
|
+
{
|
|
564
|
+
method: req.method,
|
|
565
|
+
headers: dshHeaders,
|
|
566
|
+
},
|
|
567
|
+
(dshRes) => {
|
|
568
|
+
const resHeaders = { ...dshRes.headers };
|
|
569
|
+
|
|
570
|
+
// Rewrite Set-Cookie: SameSite=Strict -> SameSite=Lax
|
|
571
|
+
// Also attach dsh_lan_auth cookie if client is authenticated
|
|
572
|
+
const sc = resHeaders['set-cookie'];
|
|
573
|
+
if (sc) {
|
|
574
|
+
const list = Array.isArray(sc) ? sc : [sc];
|
|
575
|
+
const rewritten = list.map((c) => c.replace(/;\s*SameSite=Strict/i, '; SameSite=Lax'));
|
|
576
|
+
if (clientAuthed) {
|
|
577
|
+
rewritten.push('dsh_lan_auth=1; Path=/; SameSite=Lax; Max-Age=2592000');
|
|
578
|
+
}
|
|
579
|
+
resHeaders['set-cookie'] = rewritten;
|
|
580
|
+
} else if (hasValidTokenInQuery || clientAuthed) {
|
|
581
|
+
resHeaders['set-cookie'] = ['dsh_lan_auth=1; Path=/; SameSite=Lax; Max-Age=2592000'];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (dshRes.statusCode === 401 && mintAuthCookie) {
|
|
585
|
+
mintAuthCookie().catch(() => {});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
res.writeHead(dshRes.statusCode, resHeaders);
|
|
589
|
+
dshRes.pipe(res);
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
dshReq.on('error', (err) => {
|
|
594
|
+
if (!res.headersSent) {
|
|
595
|
+
res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
596
|
+
res.end(`LAN Bridge Error: unable to connect to local DSH at ${dshHost}:${dshPort} (${err.message})`);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
req.pipe(dshReq);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Handle WebSocket Upgrade (e.g. terminals, RPC streams)
|
|
604
|
+
server.on('upgrade', (req, socket, head) => {
|
|
605
|
+
const clientIp = req.socket?.remoteAddress;
|
|
606
|
+
const incomingCookie = req.headers.cookie || '';
|
|
607
|
+
const dshHeaders = { ...req.headers };
|
|
608
|
+
dshHeaders.host = `${dshHost}:${dshPort}`;
|
|
609
|
+
if (dshHeaders.origin) {
|
|
610
|
+
dshHeaders.origin = `http://${dshHost}:${dshPort}`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const currentAuthCookie = getAuthCookie ? getAuthCookie() : null;
|
|
614
|
+
const clientAuthed = isClientAuthed(clientIp, incomingCookie);
|
|
615
|
+
if (!incomingCookie.includes('dsh-auth-') && (clientAuthed || currentAuthCookie) && currentAuthCookie) {
|
|
616
|
+
dshHeaders.cookie = incomingCookie ? `${incomingCookie}; ${currentAuthCookie}` : currentAuthCookie;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const proxyReq = http.request({
|
|
620
|
+
hostname: dshHost,
|
|
621
|
+
port: dshPort,
|
|
622
|
+
path: req.url,
|
|
623
|
+
method: req.method,
|
|
624
|
+
headers: dshHeaders,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
|
|
628
|
+
let rawHeaders = `HTTP/1.1 101 Switching Protocols\r\n`;
|
|
629
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
630
|
+
if (Array.isArray(v)) {
|
|
631
|
+
for (const item of v) rawHeaders += `${k}: ${item}\r\n`;
|
|
632
|
+
} else {
|
|
633
|
+
rawHeaders += `${k}: ${v}\r\n`;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
rawHeaders += '\r\n';
|
|
637
|
+
socket.write(rawHeaders);
|
|
638
|
+
|
|
639
|
+
if (proxyHead && proxyHead.length) socket.write(proxyHead);
|
|
640
|
+
if (head && head.length) proxySocket.write(head);
|
|
641
|
+
|
|
642
|
+
proxySocket.pipe(socket);
|
|
643
|
+
socket.pipe(proxySocket);
|
|
644
|
+
|
|
645
|
+
proxySocket.on('error', () => socket.destroy());
|
|
646
|
+
socket.on('error', () => proxySocket.destroy());
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
proxyReq.on('error', (err) => {
|
|
650
|
+
log.warn?.(`[lan-bridge] WS upgrade to ${dshHost}:${dshPort} error: ${err.message}`);
|
|
651
|
+
socket.destroy();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
proxyReq.end();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
server.on('error', (err) => {
|
|
658
|
+
isListening = false;
|
|
659
|
+
log.warn?.(`[lan-bridge] Failed to bind 0.0.0.0:${port}: ${err.message}`);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
server.listen(port, '0.0.0.0', () => {
|
|
664
|
+
isListening = true;
|
|
665
|
+
log.log?.(`[lan-bridge] Listening on 0.0.0.0:${port} -> proxying to ${dshHost}:${dshPort}`);
|
|
666
|
+
});
|
|
667
|
+
} catch (err) {
|
|
668
|
+
log.warn?.(`[lan-bridge] Failed to start listener: ${err.message}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
server,
|
|
673
|
+
port,
|
|
674
|
+
get enabled() { return isListening && server.listening; },
|
|
675
|
+
close() {
|
|
676
|
+
try { server.close(); } catch {}
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
350
681
|
/**
|
|
351
682
|
* Start the agent connection loop.
|
|
352
683
|
* @param {object} opts
|
|
@@ -498,8 +829,43 @@ export function startAgent(opts) {
|
|
|
498
829
|
mintAuthCookie();
|
|
499
830
|
}
|
|
500
831
|
|
|
832
|
+
function getActiveWebToken() {
|
|
833
|
+
if (webToken) return webToken;
|
|
834
|
+
try {
|
|
835
|
+
if (fs.existsSync('/var/log/dsh-web.log')) {
|
|
836
|
+
const content = fs.readFileSync('/var/log/dsh-web.log', 'utf8');
|
|
837
|
+
const matches = [...content.matchAll(/\?token=([A-Za-z0-9_-]+)/g)];
|
|
838
|
+
if (matches.length > 0) {
|
|
839
|
+
return matches[matches.length - 1][1];
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} catch {}
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
|
|
501
846
|
if (webToken) mintAuthCookie();
|
|
502
847
|
|
|
848
|
+
// ── LAN Bridge for local/intranet direct offload & timing-gap probing ──
|
|
849
|
+
const lanBridgePort = parseInt(process.env.LAN_BRIDGE_PORT || '3088', 10);
|
|
850
|
+
const lanBridgeEnabled = process.env.LAN_BRIDGE_ENABLED !== '0' && process.env.DSH_SERVICES_ONLY !== '1';
|
|
851
|
+
let lanBridge = null;
|
|
852
|
+
if (lanBridgeEnabled) {
|
|
853
|
+
try {
|
|
854
|
+
lanBridge = startLanBridge({
|
|
855
|
+
port: lanBridgePort,
|
|
856
|
+
dshHost,
|
|
857
|
+
dshPort,
|
|
858
|
+
getInstanceId: getEffectiveInstanceId,
|
|
859
|
+
getWebToken: getActiveWebToken,
|
|
860
|
+
getAuthCookie: () => authCookie,
|
|
861
|
+
mintAuthCookie,
|
|
862
|
+
log,
|
|
863
|
+
});
|
|
864
|
+
} catch (e) {
|
|
865
|
+
log.warn?.(`[agent] Failed to initialize LAN bridge: ${e.message}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
503
869
|
const streams = new Map();
|
|
504
870
|
let registered = false;
|
|
505
871
|
let reconnectTimer = null;
|
|
@@ -541,7 +907,13 @@ export function startAgent(opts) {
|
|
|
541
907
|
reconnectAttempt = 0;
|
|
542
908
|
relayUsername = msg.username || null;
|
|
543
909
|
log.log?.(`[agent] Registered as ${msg.username} (session ${msg.sessionId || ''})`);
|
|
544
|
-
send(ws, {
|
|
910
|
+
send(ws, {
|
|
911
|
+
type: 'system-info',
|
|
912
|
+
info: collectSystemInfo(dshVersion, {
|
|
913
|
+
port: lanBridgePort,
|
|
914
|
+
enabled: lanBridge ? lanBridge.enabled : false,
|
|
915
|
+
}),
|
|
916
|
+
});
|
|
545
917
|
// 插件工作区:上报本实例声明的服务清单(relay 存实例维度,供 /p 路由校验)
|
|
546
918
|
// DSH_SERVICES_ONLY=1:独立 services agent,relay 不注册主实例(服务旁挂到同机 dsh 实例)
|
|
547
919
|
send(ws, servicesFrame());
|
|
@@ -551,6 +923,23 @@ export function startAgent(opts) {
|
|
|
551
923
|
|
|
552
924
|
if (msg.type === 'heartbeat') return;
|
|
553
925
|
|
|
926
|
+
// 直连免密换票请求响应
|
|
927
|
+
if (msg.type === 'lan-ticket:request') {
|
|
928
|
+
const activeToken = getActiveWebToken();
|
|
929
|
+
if (!authCookie && activeToken) {
|
|
930
|
+
mintAuthCookie().catch(() => {});
|
|
931
|
+
}
|
|
932
|
+
send(ws, {
|
|
933
|
+
type: 'lan-ticket:response',
|
|
934
|
+
reqId: msg.reqId,
|
|
935
|
+
ok: true,
|
|
936
|
+
token: activeToken || '',
|
|
937
|
+
bridgePort: lanBridgePort,
|
|
938
|
+
bridgeEnabled: lanBridge ? lanBridge.enabled : false,
|
|
939
|
+
});
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
554
943
|
// 门户托管业务下发(仅宿主开启 remoteServices 时接受)
|
|
555
944
|
if (msg.type === 'services:config') {
|
|
556
945
|
if (!remoteServices) { log.warn?.('[agent] services:config ignored (remoteServices disabled)'); return; }
|
|
@@ -1005,6 +1394,7 @@ export function startAgent(opts) {
|
|
|
1005
1394
|
stop() {
|
|
1006
1395
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
1007
1396
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1397
|
+
if (lanBridge) lanBridge.close();
|
|
1008
1398
|
},
|
|
1009
1399
|
};
|
|
1010
1400
|
}
|
package/package.json
CHANGED