diodejs 0.2.2 → 0.4.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/README.md +56 -5
- package/bindPort.js +446 -66
- package/clientManager.js +435 -0
- package/connection.js +186 -72
- 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 +8 -1
- package/logger.js +10 -5
- package/nativeCrypto.js +321 -0
- package/package.json +6 -8
- package/publishPort.js +575 -94
- package/rpc.js +161 -41
- package/testServers/udpTest.js +1 -2
- package/utils.js +42 -9
package/bindPort.js
CHANGED
|
@@ -2,14 +2,16 @@ const net = require('net');
|
|
|
2
2
|
const tls = require('tls');
|
|
3
3
|
const dgram = require('dgram');
|
|
4
4
|
const { Buffer } = require('buffer');
|
|
5
|
+
const { toBufferView } = require('./utils');
|
|
5
6
|
const { Duplex } = require('stream');
|
|
6
7
|
const DiodeRPC = require('./rpc');
|
|
8
|
+
const nativeCrypto = require('./nativeCrypto');
|
|
7
9
|
const logger = require('./logger');
|
|
8
10
|
|
|
9
11
|
// Custom Duplex stream to handle the Diode connection
|
|
10
12
|
class DiodeSocket extends Duplex {
|
|
11
13
|
constructor(ref, rpc) {
|
|
12
|
-
super();
|
|
14
|
+
super({ readableHighWaterMark: 256 * 1024, writableHighWaterMark: 256 * 1024, allowHalfOpen: false });
|
|
13
15
|
this.ref = ref;
|
|
14
16
|
this.rpc = rpc;
|
|
15
17
|
this.destroyed = false;
|
|
@@ -50,7 +52,8 @@ class BindPort {
|
|
|
50
52
|
[localPortOrPortsConfig]: {
|
|
51
53
|
targetPort,
|
|
52
54
|
deviceIdHex: this._stripHexPrefix(deviceIdHex),
|
|
53
|
-
protocol: 'tls' // Default protocol is tls
|
|
55
|
+
protocol: 'tls', // Default protocol is tls
|
|
56
|
+
transport: 'api'
|
|
54
57
|
}
|
|
55
58
|
};
|
|
56
59
|
} else {
|
|
@@ -67,13 +70,21 @@ class BindPort {
|
|
|
67
70
|
if (!this.portsConfig[port].protocol) {
|
|
68
71
|
this.portsConfig[port].protocol = 'tls';
|
|
69
72
|
}
|
|
70
|
-
// Ensure protocol is
|
|
73
|
+
// Ensure protocol is lowercase
|
|
71
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);
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
this.servers = new Map(); // Track server instances by localPort
|
|
76
|
-
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;
|
|
77
88
|
|
|
78
89
|
// Set up listener for unsolicited messages once
|
|
79
90
|
this._setupMessageListener();
|
|
@@ -86,23 +97,159 @@ class BindPort {
|
|
|
86
97
|
}
|
|
87
98
|
return hexString;
|
|
88
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
|
+
}
|
|
89
231
|
|
|
90
232
|
_setupMessageListener() {
|
|
91
233
|
// Listen for data events from the device
|
|
92
|
-
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
|
+
}
|
|
93
240
|
const [messageIdRaw, messageContent] = message;
|
|
94
241
|
const messageTypeRaw = messageContent[0];
|
|
95
|
-
const messageType =
|
|
242
|
+
const messageType = toBufferView(messageTypeRaw).toString('utf8');
|
|
96
243
|
|
|
97
244
|
if (messageType === 'data' || messageType === 'portsend') {
|
|
98
245
|
const refRaw = messageContent[1];
|
|
99
246
|
const dataRaw = messageContent[2];
|
|
100
247
|
|
|
101
|
-
const dataRef =
|
|
102
|
-
const data =
|
|
248
|
+
const dataRef = toBufferView(refRaw);
|
|
249
|
+
const data = toBufferView(dataRaw);
|
|
103
250
|
|
|
104
251
|
// Find the associated client socket from connection
|
|
105
|
-
const clientSocket =
|
|
252
|
+
const clientSocket = connection.getClientSocket(dataRef);
|
|
106
253
|
if (clientSocket) {
|
|
107
254
|
if (clientSocket.diodeSocket) {
|
|
108
255
|
// If it's a DiodeSocket, push data to it so tls can process
|
|
@@ -112,57 +259,58 @@ class BindPort {
|
|
|
112
259
|
clientSocket.write(data);
|
|
113
260
|
}
|
|
114
261
|
} else {
|
|
115
|
-
const connectionInfo =
|
|
262
|
+
const connectionInfo = connection.getConnection(dataRef);
|
|
116
263
|
if (connectionInfo) {
|
|
117
|
-
logger.debug(`No client socket found for ref: ${dataRef.toString('hex')}, but connection exists for ${connectionInfo.host}:${connectionInfo.port}`);
|
|
264
|
+
logger.debug(() => `No client socket found for ref: ${dataRef.toString('hex')}, but connection exists for ${connectionInfo.host}:${connectionInfo.port}`);
|
|
118
265
|
} else {
|
|
119
|
-
logger.warn(`No client socket found for ref: ${dataRef.toString('hex')}`);
|
|
266
|
+
logger.warn(() => `No client socket found for ref: ${dataRef.toString('hex')}`);
|
|
120
267
|
}
|
|
121
268
|
}
|
|
122
269
|
} else if (messageType === 'portclose') {
|
|
123
270
|
const refRaw = messageContent[1];
|
|
124
|
-
const dataRef =
|
|
271
|
+
const dataRef = toBufferView(refRaw);
|
|
125
272
|
|
|
126
273
|
// Close the associated client socket
|
|
127
|
-
const clientSocket =
|
|
274
|
+
const clientSocket = connection.getClientSocket(dataRef);
|
|
128
275
|
if (clientSocket) {
|
|
129
276
|
if (clientSocket.diodeSocket) {
|
|
130
277
|
clientSocket.diodeSocket._destroy(null, () => {});
|
|
131
278
|
}
|
|
132
279
|
clientSocket.end();
|
|
133
|
-
|
|
134
|
-
logger.info(`Port closed for ref: ${dataRef.toString('hex')}`);
|
|
280
|
+
connection.deleteClientSocket(dataRef);
|
|
281
|
+
logger.info(() => `Port closed for ref: ${dataRef.toString('hex')}`);
|
|
135
282
|
}
|
|
136
283
|
} else {
|
|
137
|
-
if (messageType != 'portopen') {
|
|
138
|
-
logger.warn(`Unknown unsolicited message type: ${messageType}`);
|
|
284
|
+
if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request') {
|
|
285
|
+
logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
|
|
139
286
|
}
|
|
140
287
|
}
|
|
141
288
|
});
|
|
142
289
|
|
|
143
290
|
// Handle device disconnect
|
|
144
291
|
this.connection.on('end', () => {
|
|
145
|
-
logger.info('Disconnected from Diode.io server');
|
|
292
|
+
logger.info(() => 'Disconnected from Diode.io server');
|
|
146
293
|
this.closeAllServers();
|
|
147
294
|
});
|
|
148
295
|
|
|
149
296
|
// Handle connection errors
|
|
150
297
|
this.connection.on('error', (err) => {
|
|
151
|
-
logger.error(`Connection error: ${err}`);
|
|
298
|
+
logger.error(() => `Connection error: ${err}`);
|
|
152
299
|
this.closeAllServers();
|
|
153
300
|
});
|
|
154
301
|
}
|
|
155
302
|
|
|
156
|
-
addPort(localPort, targetPort, deviceIdHex, protocol = 'tls') {
|
|
303
|
+
addPort(localPort, targetPort, deviceIdHex, protocol = 'tls', transport = undefined) {
|
|
157
304
|
if (this.servers.has(localPort)) {
|
|
158
|
-
logger.warn(`Port ${localPort} is already bound`);
|
|
305
|
+
logger.warn(() => `Port ${localPort} is already bound`);
|
|
159
306
|
return false;
|
|
160
307
|
}
|
|
161
308
|
|
|
162
309
|
this.portsConfig[localPort] = {
|
|
163
310
|
targetPort,
|
|
164
311
|
deviceIdHex: this._stripHexPrefix(deviceIdHex),
|
|
165
|
-
protocol: protocol.toLowerCase()
|
|
312
|
+
protocol: protocol.toLowerCase(),
|
|
313
|
+
transport: this._normalizeTransport(transport)
|
|
166
314
|
};
|
|
167
315
|
|
|
168
316
|
this.bindSinglePort(localPort);
|
|
@@ -172,7 +320,7 @@ class BindPort {
|
|
|
172
320
|
|
|
173
321
|
removePort(localPort) {
|
|
174
322
|
if (!this.portsConfig[localPort]) {
|
|
175
|
-
logger.warn(`Port ${localPort} is not configured`);
|
|
323
|
+
logger.warn(() => `Port ${localPort} is not configured`);
|
|
176
324
|
return false;
|
|
177
325
|
}
|
|
178
326
|
|
|
@@ -180,7 +328,7 @@ class BindPort {
|
|
|
180
328
|
if (this.servers.has(localPort)) {
|
|
181
329
|
const server = this.servers.get(localPort);
|
|
182
330
|
server.close(() => {
|
|
183
|
-
logger.info(`Server on port ${localPort} closed`);
|
|
331
|
+
logger.info(() => `Server on port ${localPort} closed`);
|
|
184
332
|
});
|
|
185
333
|
this.servers.delete(localPort);
|
|
186
334
|
}
|
|
@@ -193,7 +341,7 @@ class BindPort {
|
|
|
193
341
|
closeAllServers() {
|
|
194
342
|
for (const [localPort, server] of this.servers.entries()) {
|
|
195
343
|
server.close();
|
|
196
|
-
logger.info(`Server on port ${localPort} closed`);
|
|
344
|
+
logger.info(() => `Server on port ${localPort} closed`);
|
|
197
345
|
}
|
|
198
346
|
this.servers.clear();
|
|
199
347
|
}
|
|
@@ -201,43 +349,153 @@ class BindPort {
|
|
|
201
349
|
bindSinglePort(localPort) {
|
|
202
350
|
const config = this.portsConfig[localPort];
|
|
203
351
|
if (!config) {
|
|
204
|
-
logger.error(`No configuration found for port ${localPort}`);
|
|
352
|
+
logger.error(() => `No configuration found for port ${localPort}`);
|
|
205
353
|
return false;
|
|
206
354
|
}
|
|
207
355
|
|
|
208
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
|
+
}
|
|
209
362
|
const deviceId = Buffer.from(deviceIdHex, 'hex');
|
|
210
363
|
|
|
211
364
|
// Format the target port with protocol prefix for the remote connection
|
|
212
365
|
const formattedTargetPort = `${protocol}:${targetPort}`;
|
|
213
|
-
logger.info(`Binding local port ${localPort} to remote ${formattedTargetPort}`);
|
|
366
|
+
logger.info(() => `Binding local port ${localPort} to remote ${formattedTargetPort}`);
|
|
214
367
|
|
|
215
368
|
// For udp protocol, use udp server
|
|
216
369
|
if (protocol === 'udp') {
|
|
217
370
|
const server = dgram.createSocket('udp4');
|
|
218
371
|
|
|
219
372
|
server.on('listening', () => {
|
|
220
|
-
logger.info(`udp server listening on port ${localPort} forwarding to device port ${targetPort}`);
|
|
373
|
+
logger.info(() => `udp server listening on port ${localPort} forwarding to device port ${targetPort}`);
|
|
221
374
|
});
|
|
222
375
|
|
|
223
376
|
server.on('message', async (data, rinfo) => {
|
|
224
|
-
// Open a new port on the device if this is a new client
|
|
225
377
|
const clientKey = `${rinfo.address}:${rinfo.port}`;
|
|
226
|
-
|
|
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];
|
|
227
472
|
|
|
228
|
-
if (!
|
|
473
|
+
if (!entry) {
|
|
474
|
+
let connection;
|
|
229
475
|
try {
|
|
230
|
-
|
|
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');
|
|
231
488
|
if (!ref) {
|
|
232
|
-
logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
489
|
+
logger.error(() => `Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
233
490
|
return;
|
|
234
491
|
} else {
|
|
235
|
-
logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
|
|
492
|
+
logger.info(() => `Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
|
|
236
493
|
if (!server.clientRefs) server.clientRefs = {};
|
|
237
|
-
|
|
494
|
+
entry = { ref, connection };
|
|
495
|
+
server.clientRefs[clientKey] = entry;
|
|
238
496
|
|
|
239
497
|
// Store the client info
|
|
240
|
-
|
|
498
|
+
connection.addClientSocket(ref, {
|
|
241
499
|
address: rinfo.address,
|
|
242
500
|
port: rinfo.port,
|
|
243
501
|
protocol: 'udp',
|
|
@@ -247,43 +505,164 @@ class BindPort {
|
|
|
247
505
|
});
|
|
248
506
|
}
|
|
249
507
|
} catch (error) {
|
|
250
|
-
logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
|
|
508
|
+
logger.error(() => `Error opening port ${formattedTargetPort} on device: ${error}`);
|
|
251
509
|
return;
|
|
252
510
|
}
|
|
253
511
|
}
|
|
254
512
|
|
|
255
513
|
// Send data to the device
|
|
256
514
|
try {
|
|
257
|
-
|
|
515
|
+
const rpc = this._getRpcFor(entry.connection);
|
|
516
|
+
await rpc.portSend(entry.ref, data);
|
|
258
517
|
} catch (error) {
|
|
259
|
-
logger.error(`Error sending udp data to device: ${error}`);
|
|
518
|
+
logger.error(() => `Error sending udp data to device: ${error}`);
|
|
260
519
|
}
|
|
261
520
|
});
|
|
262
521
|
|
|
263
522
|
server.on('error', (err) => {
|
|
264
|
-
logger.error(`udp Server error: ${err}`);
|
|
523
|
+
logger.error(() => `udp Server error: ${err}`);
|
|
265
524
|
});
|
|
266
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
|
+
|
|
267
535
|
server.bind(localPort);
|
|
268
536
|
this.servers.set(parseInt(localPort), server);
|
|
269
537
|
} else {
|
|
270
538
|
// For TCP and tls protocols, use TCP server locally
|
|
271
539
|
const server = net.createServer(async (clientSocket) => {
|
|
272
|
-
logger.info(`Client connected to local server on port ${localPort}`);
|
|
540
|
+
logger.info(() => `Client connected to local server on port ${localPort}`);
|
|
541
|
+
clientSocket.setNoDelay(true);
|
|
542
|
+
|
|
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);
|
|
273
557
|
|
|
274
|
-
// Open a new port on the device for this client
|
|
275
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
|
|
276
655
|
try {
|
|
277
|
-
ref = await
|
|
656
|
+
ref = await rpc.portOpen(deviceId, formattedTargetPort, 'rw');
|
|
278
657
|
if (!ref) {
|
|
279
|
-
logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
658
|
+
logger.error(() => `Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
|
|
280
659
|
clientSocket.destroy();
|
|
281
660
|
return;
|
|
282
661
|
} else {
|
|
283
|
-
logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for client`);
|
|
662
|
+
logger.info(() => `Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for client`);
|
|
284
663
|
}
|
|
285
664
|
} catch (error) {
|
|
286
|
-
logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
|
|
665
|
+
logger.error(() => `Error opening port ${formattedTargetPort} on device: ${error}`);
|
|
287
666
|
clientSocket.destroy();
|
|
288
667
|
return;
|
|
289
668
|
}
|
|
@@ -292,10 +671,10 @@ class BindPort {
|
|
|
292
671
|
// For tls protocol, create a proper tls connection
|
|
293
672
|
try {
|
|
294
673
|
// Create a DiodeSocket to handle communication with the device
|
|
295
|
-
const diodeSocket = new DiodeSocket(ref,
|
|
674
|
+
const diodeSocket = new DiodeSocket(ref, rpc);
|
|
296
675
|
|
|
297
676
|
// Get the device certificate for tls
|
|
298
|
-
const certPem =
|
|
677
|
+
const certPem = connection.getDeviceCertificate();
|
|
299
678
|
if (!certPem) {
|
|
300
679
|
throw new Error('No device certificate available');
|
|
301
680
|
}
|
|
@@ -316,15 +695,16 @@ class BindPort {
|
|
|
316
695
|
socket: diodeSocket,
|
|
317
696
|
...tlsOptions
|
|
318
697
|
}, () => {
|
|
319
|
-
logger.info(`tls connection established to device ${deviceIdHex}`);
|
|
698
|
+
logger.info(() => `tls connection established to device ${deviceIdHex}`);
|
|
320
699
|
});
|
|
700
|
+
tlsSocket.setNoDelay(true);
|
|
321
701
|
|
|
322
702
|
// Pipe data between the client socket and the tls socket
|
|
323
703
|
tlsSocket.pipe(clientSocket).pipe(tlsSocket);
|
|
324
704
|
|
|
325
705
|
// Handle tls socket errors
|
|
326
706
|
tlsSocket.on('error', (err) => {
|
|
327
|
-
logger.error(`tls Socket error: ${err}`);
|
|
707
|
+
logger.error(() => `tls Socket error: ${err}`);
|
|
328
708
|
clientSocket.destroy();
|
|
329
709
|
});
|
|
330
710
|
|
|
@@ -339,23 +719,23 @@ class BindPort {
|
|
|
339
719
|
};
|
|
340
720
|
|
|
341
721
|
// Store the socket wrapper
|
|
342
|
-
|
|
722
|
+
connection.addClientSocket(ref, socketWrapper);
|
|
343
723
|
|
|
344
724
|
} catch (error) {
|
|
345
|
-
logger.error(`Error setting up tls connection: ${error}`);
|
|
725
|
+
logger.error(() => `Error setting up tls connection: ${error}`);
|
|
346
726
|
clientSocket.destroy();
|
|
347
727
|
return;
|
|
348
728
|
}
|
|
349
729
|
} else {
|
|
350
730
|
// For TCP protocol, just use the raw socket
|
|
351
|
-
|
|
731
|
+
connection.addClientSocket(ref, clientSocket);
|
|
352
732
|
|
|
353
733
|
// Handle data from client to device
|
|
354
734
|
clientSocket.on('data', async (data) => {
|
|
355
735
|
try {
|
|
356
|
-
await
|
|
736
|
+
await rpc.portSend(ref, data);
|
|
357
737
|
} catch (error) {
|
|
358
|
-
logger.error(`Error sending data to device: ${error}`);
|
|
738
|
+
logger.error(() => `Error sending data to device: ${error}`);
|
|
359
739
|
clientSocket.destroy();
|
|
360
740
|
}
|
|
361
741
|
});
|
|
@@ -363,28 +743,28 @@ class BindPort {
|
|
|
363
743
|
|
|
364
744
|
// Handle client socket closure (common for all protocols)
|
|
365
745
|
clientSocket.on('end', async () => {
|
|
366
|
-
logger.info('Client disconnected');
|
|
367
|
-
if (ref &&
|
|
746
|
+
logger.info(() => 'Client disconnected');
|
|
747
|
+
if (ref && connection.hasClientSocket(ref)) {
|
|
368
748
|
try {
|
|
369
|
-
await
|
|
370
|
-
logger.info(`Port closed on device for ref: ${ref.toString('hex')}`);
|
|
371
|
-
|
|
749
|
+
await rpc.portClose(ref);
|
|
750
|
+
logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
|
|
751
|
+
connection.deleteClientSocket(ref);
|
|
372
752
|
} catch (error) {
|
|
373
|
-
logger.error(`Error closing port on device: ${error}`);
|
|
753
|
+
logger.error(() => `Error closing port on device: ${error}`);
|
|
374
754
|
}
|
|
375
755
|
} else {
|
|
376
|
-
logger.warn('Ref is invalid or no longer in clientSockets.');
|
|
756
|
+
logger.warn(() => 'Ref is invalid or no longer in clientSockets.');
|
|
377
757
|
}
|
|
378
758
|
});
|
|
379
759
|
|
|
380
760
|
// Handle client socket errors
|
|
381
761
|
clientSocket.on('error', (err) => {
|
|
382
|
-
logger.error(`Client socket error: ${err}`);
|
|
762
|
+
logger.error(() => `Client socket error: ${err}`);
|
|
383
763
|
});
|
|
384
764
|
});
|
|
385
765
|
|
|
386
766
|
server.listen(localPort, () => {
|
|
387
|
-
logger.info(`Local server listening on port ${localPort} forwarding to device ${protocol} port ${targetPort}`);
|
|
767
|
+
logger.info(() => `Local server listening on port ${localPort} forwarding to device ${protocol} port ${targetPort}`);
|
|
388
768
|
});
|
|
389
769
|
|
|
390
770
|
this.servers.set(parseInt(localPort), server);
|
|
@@ -404,4 +784,4 @@ class BindPort {
|
|
|
404
784
|
}
|
|
405
785
|
}
|
|
406
786
|
|
|
407
|
-
module.exports = BindPort;
|
|
787
|
+
module.exports = BindPort;
|