diodejs 0.3.0 → 0.4.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/README.md +204 -62
- package/bindPort.js +405 -28
- package/clientManager.js +2014 -0
- package/connection.js +107 -14
- package/examples/RPCTest.js +9 -7
- package/examples/nativeBindTest.js +55 -0
- package/examples/nativeForwardTest.js +80 -0
- package/examples/nativeTcpClientTest.js +56 -0
- package/examples/nativeUdpClientTest.js +40 -0
- package/examples/portForwardTest.js +5 -7
- package/examples/publishAndBind.js +6 -10
- package/examples/publishPortTest.js +4 -6
- package/index.js +2 -1
- package/nativeCrypto.js +321 -0
- package/networkDiscoveryClient.js +89 -0
- package/package.json +4 -3
- package/publishPort.js +514 -52
- package/rpc.js +125 -1
- package/scripts/benchmark-relay-selection.js +476 -0
- package/test/clientManager.relaySelection.test.js +1376 -0
- package/test/fixtures/dio-network-snapshot.json +82 -0
- package/utils.js +23 -0
package/bindPort.js
CHANGED
|
@@ -5,6 +5,7 @@ const { Buffer } = require('buffer');
|
|
|
5
5
|
const { toBufferView } = require('./utils');
|
|
6
6
|
const { Duplex } = require('stream');
|
|
7
7
|
const DiodeRPC = require('./rpc');
|
|
8
|
+
const nativeCrypto = require('./nativeCrypto');
|
|
8
9
|
const logger = require('./logger');
|
|
9
10
|
|
|
10
11
|
// Custom Duplex stream to handle the Diode connection
|
|
@@ -51,7 +52,8 @@ class BindPort {
|
|
|
51
52
|
[localPortOrPortsConfig]: {
|
|
52
53
|
targetPort,
|
|
53
54
|
deviceIdHex: this._stripHexPrefix(deviceIdHex),
|
|
54
|
-
protocol: 'tls' // Default protocol is tls
|
|
55
|
+
protocol: 'tls', // Default protocol is tls
|
|
56
|
+
transport: 'api'
|
|
55
57
|
}
|
|
56
58
|
};
|
|
57
59
|
} else {
|
|
@@ -68,13 +70,21 @@ class BindPort {
|
|
|
68
70
|
if (!this.portsConfig[port].protocol) {
|
|
69
71
|
this.portsConfig[port].protocol = 'tls';
|
|
70
72
|
}
|
|
71
|
-
// Ensure protocol is
|
|
73
|
+
// Ensure protocol is lowercase
|
|
72
74
|
this.portsConfig[port].protocol = this.portsConfig[port].protocol.toLowerCase();
|
|
75
|
+
|
|
76
|
+
// Normalize transport (api or native)
|
|
77
|
+
const transportValue = this.portsConfig[port].transport !== undefined
|
|
78
|
+
? this.portsConfig[port].transport
|
|
79
|
+
: this.portsConfig[port].native;
|
|
80
|
+
this.portsConfig[port].transport = this._normalizeTransport(transportValue);
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
this.servers = new Map(); // Track server instances by localPort
|
|
77
|
-
this.
|
|
85
|
+
this._rpcByConnection = new Map();
|
|
86
|
+
this.rpc = this._isManager() ? null : this._getRpcFor(this.connection);
|
|
87
|
+
this.handshakeTimeoutMs = parseInt(process.env.DIODE_NATIVE_HANDSHAKE_TIMEOUT_MS, 10) || 10000;
|
|
78
88
|
|
|
79
89
|
// Set up listener for unsolicited messages once
|
|
80
90
|
this._setupMessageListener();
|
|
@@ -87,10 +97,146 @@ class BindPort {
|
|
|
87
97
|
}
|
|
88
98
|
return hexString;
|
|
89
99
|
}
|
|
100
|
+
|
|
101
|
+
_normalizeTransport(value) {
|
|
102
|
+
if (value === true) return 'native';
|
|
103
|
+
if (typeof value === 'string') {
|
|
104
|
+
const lower = value.toLowerCase();
|
|
105
|
+
if (lower === 'native') return 'native';
|
|
106
|
+
if (lower === 'api') return 'api';
|
|
107
|
+
}
|
|
108
|
+
return 'api';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_isManager() {
|
|
112
|
+
return this.connection && typeof this.connection.getConnectionForDevice === 'function';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_getRpcFor(connection) {
|
|
116
|
+
if (!connection) return null;
|
|
117
|
+
let rpc = this._rpcByConnection.get(connection);
|
|
118
|
+
if (!rpc) {
|
|
119
|
+
rpc = connection.RPC || new DiodeRPC(connection);
|
|
120
|
+
this._rpcByConnection.set(connection, rpc);
|
|
121
|
+
}
|
|
122
|
+
return rpc;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async _resolveConnectionForDevice(deviceId) {
|
|
126
|
+
if (this._isManager()) {
|
|
127
|
+
return this.connection.getConnectionForDevice(deviceId);
|
|
128
|
+
}
|
|
129
|
+
return this.connection;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async _openTlsHandshakeChannel(connection, rpc, ref) {
|
|
133
|
+
const diodeSocket = new DiodeSocket(ref, rpc);
|
|
134
|
+
const certPem = connection.getDeviceCertificate();
|
|
135
|
+
if (!certPem) {
|
|
136
|
+
throw new Error('No device certificate available');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tlsOptions = {
|
|
140
|
+
cert: certPem,
|
|
141
|
+
key: certPem,
|
|
142
|
+
rejectUnauthorized: false,
|
|
143
|
+
ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
|
|
144
|
+
ecdhCurve: 'secp256k1',
|
|
145
|
+
minVersion: 'TLSv1.2',
|
|
146
|
+
maxVersion: 'TLSv1.2',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const tlsSocket = tls.connect({
|
|
150
|
+
socket: diodeSocket,
|
|
151
|
+
...tlsOptions
|
|
152
|
+
});
|
|
153
|
+
tlsSocket.setNoDelay(true);
|
|
154
|
+
|
|
155
|
+
const socketWrapper = {
|
|
156
|
+
diodeSocket,
|
|
157
|
+
tlsSocket,
|
|
158
|
+
end: () => {
|
|
159
|
+
try { tlsSocket.end(); } catch {}
|
|
160
|
+
try { diodeSocket._destroy(null, () => {}); } catch {}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
connection.addClientSocket(ref, socketWrapper);
|
|
165
|
+
|
|
166
|
+
await new Promise((resolve, reject) => {
|
|
167
|
+
const timer = setTimeout(() => reject(new Error('TLS handshake timeout')), this.handshakeTimeoutMs);
|
|
168
|
+
tlsSocket.once('secureConnect', () => {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
tlsSocket.once('error', (err) => {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
reject(err);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return { tlsSocket, socketWrapper };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async _performNativeHandshake(connection, rpc, deviceId, targetPort, physicalPort) {
|
|
182
|
+
const handshakePort = `tls:${targetPort}#hs`;
|
|
183
|
+
const ref = await rpc.portOpen(deviceId, handshakePort, 'rw');
|
|
184
|
+
if (!ref) {
|
|
185
|
+
throw new Error('Handshake portopen failed');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let tlsSocket;
|
|
189
|
+
try {
|
|
190
|
+
({ tlsSocket } = await this._openTlsHandshakeChannel(connection, rpc, ref));
|
|
191
|
+
|
|
192
|
+
const localDeviceId = connection.getEthereumAddress().toLowerCase();
|
|
193
|
+
const remoteDeviceId = `0x${Buffer.from(deviceId).toString('hex')}`.toLowerCase();
|
|
194
|
+
const { message, privKey, nonce } = nativeCrypto.createHandshakeMessage({
|
|
195
|
+
role: 'bind',
|
|
196
|
+
deviceId: localDeviceId,
|
|
197
|
+
physicalPort,
|
|
198
|
+
privateKey: connection.getPrivateKey()
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
nativeCrypto.writeHandshakeMessage(tlsSocket, message);
|
|
202
|
+
|
|
203
|
+
const peerMessage = await nativeCrypto.readHandshakeMessage(tlsSocket, this.handshakeTimeoutMs);
|
|
204
|
+
const verification = nativeCrypto.verifyHandshakeMessage(peerMessage, {
|
|
205
|
+
expectedRole: 'publish',
|
|
206
|
+
expectedDeviceId: remoteDeviceId,
|
|
207
|
+
expectedPhysicalPort: physicalPort
|
|
208
|
+
});
|
|
209
|
+
if (!verification.ok) {
|
|
210
|
+
throw new Error(`Handshake verification failed: ${verification.reason}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const session = nativeCrypto.deriveSessionKeys({
|
|
214
|
+
role: 'bind',
|
|
215
|
+
localDeviceId,
|
|
216
|
+
remoteDeviceId,
|
|
217
|
+
localEphPriv: privKey,
|
|
218
|
+
remoteEphPub: verification.ephPub,
|
|
219
|
+
localNonce: nonce,
|
|
220
|
+
remoteNonce: verification.nonce,
|
|
221
|
+
physicalPort,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return session;
|
|
225
|
+
} finally {
|
|
226
|
+
try { if (tlsSocket) tlsSocket.end(); } catch {}
|
|
227
|
+
try { await rpc.portClose(ref); } catch {}
|
|
228
|
+
try { connection.deleteClientSocket(ref); } catch {}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
90
231
|
|
|
91
232
|
_setupMessageListener() {
|
|
92
233
|
// Listen for data events from the device
|
|
93
|
-
this.connection.on('unsolicited', (message) => {
|
|
234
|
+
this.connection.on('unsolicited', (message, sourceConnection) => {
|
|
235
|
+
const connection = sourceConnection || this.connection;
|
|
236
|
+
if (!connection || typeof connection.getClientSocket !== 'function') {
|
|
237
|
+
logger.warn(() => 'Received unsolicited message without a valid connection context');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
94
240
|
const [messageIdRaw, messageContent] = message;
|
|
95
241
|
const messageTypeRaw = messageContent[0];
|
|
96
242
|
const messageType = toBufferView(messageTypeRaw).toString('utf8');
|
|
@@ -103,7 +249,7 @@ class BindPort {
|
|
|
103
249
|
const data = toBufferView(dataRaw);
|
|
104
250
|
|
|
105
251
|
// Find the associated client socket from connection
|
|
106
|
-
const clientSocket =
|
|
252
|
+
const clientSocket = connection.getClientSocket(dataRef);
|
|
107
253
|
if (clientSocket) {
|
|
108
254
|
if (clientSocket.diodeSocket) {
|
|
109
255
|
// If it's a DiodeSocket, push data to it so tls can process
|
|
@@ -113,7 +259,7 @@ class BindPort {
|
|
|
113
259
|
clientSocket.write(data);
|
|
114
260
|
}
|
|
115
261
|
} else {
|
|
116
|
-
const connectionInfo =
|
|
262
|
+
const connectionInfo = connection.getConnection(dataRef);
|
|
117
263
|
if (connectionInfo) {
|
|
118
264
|
logger.debug(() => `No client socket found for ref: ${dataRef.toString('hex')}, but connection exists for ${connectionInfo.host}:${connectionInfo.port}`);
|
|
119
265
|
} else {
|
|
@@ -125,17 +271,17 @@ class BindPort {
|
|
|
125
271
|
const dataRef = toBufferView(refRaw);
|
|
126
272
|
|
|
127
273
|
// Close the associated client socket
|
|
128
|
-
const clientSocket =
|
|
274
|
+
const clientSocket = connection.getClientSocket(dataRef);
|
|
129
275
|
if (clientSocket) {
|
|
130
276
|
if (clientSocket.diodeSocket) {
|
|
131
277
|
clientSocket.diodeSocket._destroy(null, () => {});
|
|
132
278
|
}
|
|
133
279
|
clientSocket.end();
|
|
134
|
-
|
|
280
|
+
connection.deleteClientSocket(dataRef);
|
|
135
281
|
logger.info(() => `Port closed for ref: ${dataRef.toString('hex')}`);
|
|
136
282
|
}
|
|
137
283
|
} else {
|
|
138
|
-
if (messageType != 'portopen') {
|
|
284
|
+
if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request') {
|
|
139
285
|
logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
|
|
140
286
|
}
|
|
141
287
|
}
|
|
@@ -154,7 +300,7 @@ class BindPort {
|
|
|
154
300
|
});
|
|
155
301
|
}
|
|
156
302
|
|
|
157
|
-
addPort(localPort, targetPort, deviceIdHex, protocol = 'tls') {
|
|
303
|
+
addPort(localPort, targetPort, deviceIdHex, protocol = 'tls', transport = undefined) {
|
|
158
304
|
if (this.servers.has(localPort)) {
|
|
159
305
|
logger.warn(() => `Port ${localPort} is already bound`);
|
|
160
306
|
return false;
|
|
@@ -163,7 +309,8 @@ class BindPort {
|
|
|
163
309
|
this.portsConfig[localPort] = {
|
|
164
310
|
targetPort,
|
|
165
311
|
deviceIdHex: this._stripHexPrefix(deviceIdHex),
|
|
166
|
-
protocol: protocol.toLowerCase()
|
|
312
|
+
protocol: protocol.toLowerCase(),
|
|
313
|
+
transport: this._normalizeTransport(transport)
|
|
167
314
|
};
|
|
168
315
|
|
|
169
316
|
this.bindSinglePort(localPort);
|
|
@@ -207,6 +354,11 @@ class BindPort {
|
|
|
207
354
|
}
|
|
208
355
|
|
|
209
356
|
const { targetPort, deviceIdHex, protocol = 'tls' } = config;
|
|
357
|
+
const transport = config.transport || 'api';
|
|
358
|
+
const useNative = transport === 'native' && (protocol === 'tcp' || protocol === 'udp');
|
|
359
|
+
if (transport === 'native' && protocol === 'tls') {
|
|
360
|
+
logger.warn(() => `Native transport does not support TLS for port ${localPort}. Falling back to API relay.`);
|
|
361
|
+
}
|
|
210
362
|
const deviceId = Buffer.from(deviceIdHex, 'hex');
|
|
211
363
|
|
|
212
364
|
// Format the target port with protocol prefix for the remote connection
|
|
@@ -222,23 +374,128 @@ class BindPort {
|
|
|
222
374
|
});
|
|
223
375
|
|
|
224
376
|
server.on('message', async (data, rinfo) => {
|
|
225
|
-
// Open a new port on the device if this is a new client
|
|
226
377
|
const clientKey = `${rinfo.address}:${rinfo.port}`;
|
|
227
|
-
|
|
378
|
+
if (useNative) {
|
|
379
|
+
if (!server.nativeRelays) server.nativeRelays = {};
|
|
380
|
+
let relayInfo = server.nativeRelays[clientKey];
|
|
381
|
+
if (!relayInfo) {
|
|
382
|
+
let connection;
|
|
383
|
+
try {
|
|
384
|
+
connection = await this._resolveConnectionForDevice(deviceId);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
logger.error(() => `Error resolving relay for device ${deviceIdHex}: ${error}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!connection) {
|
|
390
|
+
logger.error(() => `No relay connection available for device ${deviceIdHex}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const rpc = this._getRpcFor(connection);
|
|
394
|
+
try {
|
|
395
|
+
const flags = config.flags || 'rwu';
|
|
396
|
+
const physicalPort = await rpc.portOpen2(deviceId, formattedTargetPort, flags);
|
|
397
|
+
if (!physicalPort) {
|
|
398
|
+
logger.error(() => `Error opening portopen2 ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const relaySocket = dgram.createSocket('udp4');
|
|
403
|
+
relaySocket.on('message', (msg) => {
|
|
404
|
+
if (!relayInfo || !relayInfo.session) return;
|
|
405
|
+
const plaintext = nativeCrypto.parseUdpPacket(relayInfo.session, msg);
|
|
406
|
+
if (!plaintext) return;
|
|
407
|
+
server.send(plaintext, rinfo.port, rinfo.address);
|
|
408
|
+
});
|
|
409
|
+
relaySocket.on('error', (err) => {
|
|
410
|
+
logger.error(() => `udp relay socket error: ${err}`);
|
|
411
|
+
try { relaySocket.close(); } catch {}
|
|
412
|
+
delete server.nativeRelays[clientKey];
|
|
413
|
+
});
|
|
414
|
+
relaySocket.bind(0);
|
|
415
|
+
|
|
416
|
+
relayInfo = {
|
|
417
|
+
socket: relaySocket,
|
|
418
|
+
physicalPort,
|
|
419
|
+
relayHost: connection.getServerRelayHost(),
|
|
420
|
+
connection,
|
|
421
|
+
session: null,
|
|
422
|
+
handshakePromise: null,
|
|
423
|
+
client: { address: rinfo.address, port: rinfo.port }
|
|
424
|
+
};
|
|
425
|
+
server.nativeRelays[clientKey] = relayInfo;
|
|
426
|
+
logger.info(() => `Portopen2 ${formattedTargetPort} opened with server port ${physicalPort} for udp client ${clientKey}`);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
logger.error(() => `Error opening portopen2 ${formattedTargetPort} on device: ${error}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!relayInfo.handshakePromise) {
|
|
434
|
+
const rpc = this._getRpcFor(relayInfo.connection);
|
|
435
|
+
relayInfo.handshakePromise = this._performNativeHandshake(
|
|
436
|
+
relayInfo.connection,
|
|
437
|
+
rpc,
|
|
438
|
+
deviceId,
|
|
439
|
+
targetPort,
|
|
440
|
+
relayInfo.physicalPort
|
|
441
|
+
).then((session) => {
|
|
442
|
+
relayInfo.session = session;
|
|
443
|
+
return session;
|
|
444
|
+
}).catch((error) => {
|
|
445
|
+
logger.error(() => `Native UDP handshake failed: ${error}`);
|
|
446
|
+
try { relayInfo.socket.close(); } catch {}
|
|
447
|
+
delete server.nativeRelays[clientKey];
|
|
448
|
+
throw error;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
await relayInfo.handshakePromise;
|
|
454
|
+
} catch (_) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!relayInfo.session) return;
|
|
459
|
+
|
|
460
|
+
// Send encrypted data to the server relay port
|
|
461
|
+
try {
|
|
462
|
+
const packet = nativeCrypto.createUdpPacket(relayInfo.session, data);
|
|
463
|
+
relayInfo.socket.send(packet, relayInfo.physicalPort, relayInfo.relayHost);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
logger.error(() => `Error sending udp data to relay: ${error}`);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Legacy API relay
|
|
471
|
+
let entry = server.clientRefs && server.clientRefs[clientKey];
|
|
228
472
|
|
|
229
|
-
if (!
|
|
473
|
+
if (!entry) {
|
|
474
|
+
let connection;
|
|
230
475
|
try {
|
|
231
|
-
|
|
476
|
+
connection = await this._resolveConnectionForDevice(deviceId);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
logger.error(() => `Error resolving relay for device ${deviceIdHex}: ${error}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!connection) {
|
|
482
|
+
logger.error(() => `No relay connection available for device ${deviceIdHex}`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const rpc = this._getRpcFor(connection);
|
|
486
|
+
try {
|
|
487
|
+
const ref = await rpc.portOpen(deviceId, formattedTargetPort, 'rw');
|
|
232
488
|
if (!ref) {
|
|
233
489
|
logger.error(() => `Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
234
490
|
return;
|
|
235
491
|
} else {
|
|
236
492
|
logger.info(() => `Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
|
|
237
493
|
if (!server.clientRefs) server.clientRefs = {};
|
|
238
|
-
|
|
494
|
+
entry = { ref, connection };
|
|
495
|
+
server.clientRefs[clientKey] = entry;
|
|
239
496
|
|
|
240
497
|
// Store the client info
|
|
241
|
-
|
|
498
|
+
connection.addClientSocket(ref, {
|
|
242
499
|
address: rinfo.address,
|
|
243
500
|
port: rinfo.port,
|
|
244
501
|
protocol: 'udp',
|
|
@@ -255,7 +512,8 @@ class BindPort {
|
|
|
255
512
|
|
|
256
513
|
// Send data to the device
|
|
257
514
|
try {
|
|
258
|
-
|
|
515
|
+
const rpc = this._getRpcFor(entry.connection);
|
|
516
|
+
await rpc.portSend(entry.ref, data);
|
|
259
517
|
} catch (error) {
|
|
260
518
|
logger.error(() => `Error sending udp data to device: ${error}`);
|
|
261
519
|
}
|
|
@@ -265,6 +523,15 @@ class BindPort {
|
|
|
265
523
|
logger.error(() => `udp Server error: ${err}`);
|
|
266
524
|
});
|
|
267
525
|
|
|
526
|
+
server.on('close', () => {
|
|
527
|
+
if (server.nativeRelays) {
|
|
528
|
+
for (const relayInfo of Object.values(server.nativeRelays)) {
|
|
529
|
+
try { relayInfo.socket.close(); } catch {}
|
|
530
|
+
}
|
|
531
|
+
server.nativeRelays = null;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
268
535
|
server.bind(localPort);
|
|
269
536
|
this.servers.set(parseInt(localPort), server);
|
|
270
537
|
} else {
|
|
@@ -273,10 +540,120 @@ class BindPort {
|
|
|
273
540
|
logger.info(() => `Client connected to local server on port ${localPort}`);
|
|
274
541
|
clientSocket.setNoDelay(true);
|
|
275
542
|
|
|
276
|
-
|
|
543
|
+
let connection;
|
|
544
|
+
try {
|
|
545
|
+
connection = await this._resolveConnectionForDevice(deviceId);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
logger.error(() => `Error resolving relay for device ${deviceIdHex}: ${error}`);
|
|
548
|
+
clientSocket.destroy();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (!connection) {
|
|
552
|
+
logger.error(() => `No relay connection available for device ${deviceIdHex}`);
|
|
553
|
+
clientSocket.destroy();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const rpc = this._getRpcFor(connection);
|
|
557
|
+
|
|
277
558
|
let ref;
|
|
559
|
+
if (useNative) {
|
|
560
|
+
// Open a new native relay port on the device for this client
|
|
561
|
+
let physicalPort;
|
|
562
|
+
try {
|
|
563
|
+
const flags = config.flags || 'rw';
|
|
564
|
+
physicalPort = await rpc.portOpen2(deviceId, formattedTargetPort, flags);
|
|
565
|
+
if (!physicalPort) {
|
|
566
|
+
logger.error(() => `Error opening portopen2 ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
567
|
+
clientSocket.destroy();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
logger.error(() => `Error opening portopen2 ${formattedTargetPort} on device: ${error}`);
|
|
572
|
+
clientSocket.destroy();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
let session;
|
|
577
|
+
try {
|
|
578
|
+
session = await this._performNativeHandshake(connection, rpc, deviceId, targetPort, physicalPort);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
logger.error(() => `Native TCP handshake failed: ${error}`);
|
|
581
|
+
clientSocket.destroy();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const relayHost = connection.getServerRelayHost();
|
|
586
|
+
const relaySocket = net.connect({ host: relayHost, port: physicalPort }, () => {
|
|
587
|
+
logger.info(() => `Connected to relay ${relayHost}:${physicalPort} for ${formattedTargetPort}`);
|
|
588
|
+
});
|
|
589
|
+
relaySocket.setNoDelay(true);
|
|
590
|
+
|
|
591
|
+
let relayReady = false;
|
|
592
|
+
const pendingChunks = [];
|
|
593
|
+
|
|
594
|
+
const cleanup = () => {
|
|
595
|
+
if (!clientSocket.destroyed) clientSocket.destroy();
|
|
596
|
+
if (!relaySocket.destroyed) relaySocket.destroy();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
relaySocket.on('connect', () => {
|
|
600
|
+
relayReady = true;
|
|
601
|
+
while (pendingChunks.length > 0) {
|
|
602
|
+
const chunk = pendingChunks.shift();
|
|
603
|
+
try {
|
|
604
|
+
const frame = nativeCrypto.createTcpFrame(session, chunk);
|
|
605
|
+
relaySocket.write(frame);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
logger.error(() => `Error sending TCP frame: ${error}`);
|
|
608
|
+
cleanup();
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
relaySocket.on('data', (data) => {
|
|
615
|
+
try {
|
|
616
|
+
const messages = nativeCrypto.consumeTcpFrames(session, data);
|
|
617
|
+
for (const msg of messages) {
|
|
618
|
+
clientSocket.write(msg);
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
logger.error(() => `TCP decrypt error: ${error}`);
|
|
622
|
+
cleanup();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
clientSocket.on('data', (data) => {
|
|
627
|
+
if (!relayReady) {
|
|
628
|
+
pendingChunks.push(data);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const frame = nativeCrypto.createTcpFrame(session, data);
|
|
633
|
+
relaySocket.write(frame);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
logger.error(() => `Error sending TCP frame: ${error}`);
|
|
636
|
+
cleanup();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
relaySocket.on('error', (err) => {
|
|
641
|
+
logger.error(() => `Relay socket error: ${err}`);
|
|
642
|
+
cleanup();
|
|
643
|
+
});
|
|
644
|
+
clientSocket.on('error', (err) => {
|
|
645
|
+
logger.error(() => `Client socket error: ${err}`);
|
|
646
|
+
cleanup();
|
|
647
|
+
});
|
|
648
|
+
clientSocket.on('end', cleanup);
|
|
649
|
+
relaySocket.on('end', cleanup);
|
|
650
|
+
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Legacy API relay
|
|
278
655
|
try {
|
|
279
|
-
ref = await
|
|
656
|
+
ref = await rpc.portOpen(deviceId, formattedTargetPort, 'rw');
|
|
280
657
|
if (!ref) {
|
|
281
658
|
logger.error(() => `Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
282
659
|
clientSocket.destroy();
|
|
@@ -294,10 +671,10 @@ class BindPort {
|
|
|
294
671
|
// For tls protocol, create a proper tls connection
|
|
295
672
|
try {
|
|
296
673
|
// Create a DiodeSocket to handle communication with the device
|
|
297
|
-
const diodeSocket = new DiodeSocket(ref,
|
|
674
|
+
const diodeSocket = new DiodeSocket(ref, rpc);
|
|
298
675
|
|
|
299
676
|
// Get the device certificate for tls
|
|
300
|
-
const certPem =
|
|
677
|
+
const certPem = connection.getDeviceCertificate();
|
|
301
678
|
if (!certPem) {
|
|
302
679
|
throw new Error('No device certificate available');
|
|
303
680
|
}
|
|
@@ -342,7 +719,7 @@ class BindPort {
|
|
|
342
719
|
};
|
|
343
720
|
|
|
344
721
|
// Store the socket wrapper
|
|
345
|
-
|
|
722
|
+
connection.addClientSocket(ref, socketWrapper);
|
|
346
723
|
|
|
347
724
|
} catch (error) {
|
|
348
725
|
logger.error(() => `Error setting up tls connection: ${error}`);
|
|
@@ -351,12 +728,12 @@ class BindPort {
|
|
|
351
728
|
}
|
|
352
729
|
} else {
|
|
353
730
|
// For TCP protocol, just use the raw socket
|
|
354
|
-
|
|
731
|
+
connection.addClientSocket(ref, clientSocket);
|
|
355
732
|
|
|
356
733
|
// Handle data from client to device
|
|
357
734
|
clientSocket.on('data', async (data) => {
|
|
358
735
|
try {
|
|
359
|
-
await
|
|
736
|
+
await rpc.portSend(ref, data);
|
|
360
737
|
} catch (error) {
|
|
361
738
|
logger.error(() => `Error sending data to device: ${error}`);
|
|
362
739
|
clientSocket.destroy();
|
|
@@ -367,11 +744,11 @@ class BindPort {
|
|
|
367
744
|
// Handle client socket closure (common for all protocols)
|
|
368
745
|
clientSocket.on('end', async () => {
|
|
369
746
|
logger.info(() => 'Client disconnected');
|
|
370
|
-
if (ref &&
|
|
747
|
+
if (ref && connection.hasClientSocket(ref)) {
|
|
371
748
|
try {
|
|
372
|
-
await
|
|
749
|
+
await rpc.portClose(ref);
|
|
373
750
|
logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
|
|
374
|
-
|
|
751
|
+
connection.deleteClientSocket(ref);
|
|
375
752
|
} catch (error) {
|
|
376
753
|
logger.error(() => `Error closing port on device: ${error}`);
|
|
377
754
|
}
|