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/publishPort.js CHANGED
@@ -8,12 +8,39 @@ const { Buffer } = require('buffer');
8
8
  const EventEmitter = require('events');
9
9
  const { Duplex } = require('stream');
10
10
  const DiodeRPC = require('./rpc');
11
- const { makeReadable } = require('./utils');
11
+ const { makeReadable, parseUInt, toBufferView } = require('./utils');
12
+ const nativeCrypto = require('./nativeCrypto');
12
13
  const logger = require('./logger');
14
+ const secp256k1 = require('secp256k1');
15
+ const ethUtil = require('ethereumjs-util');
16
+
17
+ function normalizeDeviceId(raw) {
18
+ if (!raw) return '';
19
+ let buf = toBufferView(raw);
20
+ if (buf.length === 20) {
21
+ return `0x${buf.toString('hex')}`;
22
+ }
23
+ if (buf.length === 32) {
24
+ return `0x${buf.slice(12).toString('hex')}`;
25
+ }
26
+ if (buf.length === 33 || buf.length === 65) {
27
+ try {
28
+ const uncompressed = buf.length === 33 ? secp256k1.publicKeyConvert(buf, false) : buf;
29
+ const addr = ethUtil.pubToAddress(uncompressed, true);
30
+ return `0x${addr.toString('hex')}`;
31
+ } catch (_) {
32
+ // fallback below
33
+ }
34
+ }
35
+ if (buf.length > 20) {
36
+ return `0x${buf.slice(buf.length - 20).toString('hex')}`;
37
+ }
38
+ return '';
39
+ }
13
40
 
14
41
  class DiodeSocket extends Duplex {
15
42
  constructor(ref, rpc) {
16
- super();
43
+ super({ readableHighWaterMark: 256 * 1024, writableHighWaterMark: 256 * 1024, allowHalfOpen: false });
17
44
  this.ref = ref;
18
45
  this.rpc = rpc;
19
46
  }
@@ -39,7 +66,11 @@ class PublishPort extends EventEmitter {
39
66
  constructor(connection, publishedPorts, _certPath = null) {
40
67
  super();
41
68
  this.connection = connection;
42
- this.rpc = new DiodeRPC(connection);
69
+ this._rpcByConnection = new Map();
70
+ this.rpc = this._isManager() ? null : this._getRpcFor(connection);
71
+ this._listening = false; // ensure startListening is idempotent
72
+ this.nativeSessions = new Map();
73
+ this.handshakeTimeoutMs = parseInt(process.env.DIODE_NATIVE_HANDSHAKE_TIMEOUT_MS, 10) || 10000;
43
74
 
44
75
  // Convert publishedPorts to a Map with configurations
45
76
  this.publishedPorts = new Map();
@@ -51,9 +82,9 @@ class PublishPort extends EventEmitter {
51
82
 
52
83
  this.startListening();
53
84
  if (this.publishedPorts.size > 0) {
54
- logger.info(`Publishing ports: ${Array.from(this.publishedPorts.keys())}`);
85
+ logger.info(() => `Publishing ports: ${Array.from(this.publishedPorts.keys())}`);
55
86
  } else {
56
- logger.info("No ports published initially");
87
+ logger.info(() => "No ports published initially");
57
88
  }
58
89
  }
59
90
 
@@ -69,7 +100,7 @@ class PublishPort extends EventEmitter {
69
100
 
70
101
  // Add to map
71
102
  this.publishedPorts.set(portNum, portConfig);
72
- logger.info(`Added published port ${portNum} with mode: ${portConfig.mode}`);
103
+ logger.info(() => `Added published port ${portNum} with mode: ${portConfig.mode}`);
73
104
 
74
105
  return true;
75
106
  }
@@ -79,23 +110,34 @@ class PublishPort extends EventEmitter {
79
110
  const portNum = parseInt(port, 10);
80
111
 
81
112
  if (!this.publishedPorts.has(portNum)) {
82
- logger.warn(`Port ${portNum} is not published`);
113
+ logger.warn(() => `Port ${portNum} is not published`);
83
114
  return false;
84
115
  }
85
116
 
86
117
  // Close any active connections for this port
87
118
  // This could require tracking active connections by port
88
119
  // For now, let's log about active connections
89
- const activeConnections = Array.from(this.connection.connections.values())
90
- .filter(conn => conn.port === portNum);
120
+ let activeConnections = [];
121
+ if (this.connection && typeof this.connection.getConnections === 'function') {
122
+ for (const conn of this.connection.getConnections()) {
123
+ if (conn && conn.connections) {
124
+ activeConnections = activeConnections.concat(
125
+ Array.from(conn.connections.values()).filter(info => info.port === portNum)
126
+ );
127
+ }
128
+ }
129
+ } else if (this.connection && this.connection.connections) {
130
+ activeConnections = Array.from(this.connection.connections.values())
131
+ .filter(conn => conn.port === portNum);
132
+ }
91
133
 
92
134
  if (activeConnections.length > 0) {
93
- logger.warn(`Removing port ${portNum} with ${activeConnections.length} active connections`);
135
+ logger.warn(() => `Removing port ${portNum} with ${activeConnections.length} active connections`);
94
136
  // We could close these connections, but they'll be rejected naturally on next data transfer
95
137
  }
96
138
 
97
139
  this.publishedPorts.delete(portNum);
98
- logger.info(`Removed published port ${portNum}`);
140
+ logger.info(() => `Removed published port ${portNum}`);
99
141
 
100
142
  return true;
101
143
  }
@@ -126,43 +168,83 @@ class PublishPort extends EventEmitter {
126
168
  clearPorts() {
127
169
  const portCount = this.publishedPorts.size;
128
170
  this.publishedPorts.clear();
129
- logger.info(`Cleared ${portCount} published ports`);
171
+ logger.info(() => `Cleared ${portCount} published ports`);
130
172
  return portCount;
131
173
  }
132
174
 
175
+ _isManager() {
176
+ return this.connection && typeof this.connection.getConnections === 'function';
177
+ }
178
+
179
+ _getRpcFor(connection) {
180
+ if (!connection) return null;
181
+ let rpc = this._rpcByConnection.get(connection);
182
+ if (!rpc) {
183
+ rpc = connection.RPC || new DiodeRPC(connection);
184
+ this._rpcByConnection.set(connection, rpc);
185
+ }
186
+ return rpc;
187
+ }
188
+
133
189
  startListening() {
190
+ if (this._listening) return this; // idempotent
134
191
  // Listen for unsolicited messages from the connection
135
- this.connection.on('unsolicited', (message) => {
192
+ this._onUnsolicited = (message, sourceConnection) => {
193
+ const connection = sourceConnection || this.connection;
194
+ if (!connection) {
195
+ logger.warn(() => 'Received unsolicited message without a valid connection context');
196
+ return;
197
+ }
136
198
  const [sessionIdRaw, messageContent] = message;
137
199
  const messageTypeRaw = messageContent[0];
138
- const messageType = Buffer.from(messageTypeRaw).toString('utf8');
200
+ const messageType = toBufferView(messageTypeRaw).toString('utf8');
139
201
 
140
202
  if (messageType === 'portopen') {
141
- this.handlePortOpen(sessionIdRaw, messageContent);
142
- } else if (messageType === 'portsend') {
143
- this.handlePortSend(sessionIdRaw, messageContent);
203
+ this.handlePortOpen(sessionIdRaw, messageContent, connection);
204
+ } else if (messageType === 'portopen2') {
205
+ this.handlePortOpen2(sessionIdRaw, messageContent, connection);
206
+ } else if (messageType === 'portclose2') {
207
+ logger.debug(() => 'Received portclose2');
208
+ } else if (messageType === 'portsend' || messageType === 'data') {
209
+ // Accept both synonyms for payload delivery
210
+ this.handlePortSend(sessionIdRaw, messageContent, connection);
144
211
  } else if (messageType === 'portclose') {
145
- this.handlePortClose(sessionIdRaw, messageContent);
212
+ this.handlePortClose(sessionIdRaw, messageContent, connection);
146
213
  } else {
147
- if (messageType != 'data') {
148
- logger.warn(`Unknown unsolicited message type: ${messageType}`);
214
+ if (messageType !== 'ticket_request') {
215
+ logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
149
216
  }
150
217
  }
151
- });
218
+ };
219
+ this.connection.on('unsolicited', this._onUnsolicited);
220
+ this._listening = true;
221
+ return this;
222
+ }
223
+
224
+ stopListening() {
225
+ if (!this._listening) return this;
226
+ if (this._onUnsolicited) {
227
+ this.connection.off('unsolicited', this._onUnsolicited);
228
+ }
229
+ this._listening = false;
230
+ return this;
152
231
  }
153
232
 
154
- handlePortOpen(sessionIdRaw, messageContent) {
233
+ handlePortOpen(sessionIdRaw, messageContent, connection) {
234
+ const rpc = this._getRpcFor(connection);
155
235
  // messageContent: ['portopen', portString, ref, deviceId]
156
236
  const portStringRaw = messageContent[1];
157
237
  const refRaw = messageContent[2];
158
238
  const deviceIdRaw = messageContent[3];
159
239
 
160
- const sessionId = Buffer.from(sessionIdRaw);
240
+ const sessionId = toBufferView(sessionIdRaw);
161
241
  const portString = makeReadable(portStringRaw);
162
- const ref = Buffer.from(refRaw);
163
- const deviceId = `0x${Buffer.from(deviceIdRaw).toString('hex')}`;
242
+ const ref = toBufferView(refRaw);
243
+ const deviceId = normalizeDeviceId(deviceIdRaw);
164
244
 
165
- logger.info(`Received portopen request for portString ${portString} with ref ${ref.toString('hex')} from device ${deviceId}`);
245
+ logger.info(() => `Received portopen request for portString ${portString} with ref ${ref.toString('hex')} from device ${deviceId}`);
246
+
247
+ const isHandshake = typeof portString === 'string' && portString.includes('#hs');
166
248
 
167
249
  // Extract protocol and port number from portString
168
250
  var protocol = 'tcp';
@@ -180,9 +262,9 @@ class PublishPort extends EventEmitter {
180
262
 
181
263
  // Check if the port is published
182
264
  if (!this.publishedPorts.has(port)) {
183
- logger.warn(`Port ${port} is not published. Rejecting request.`);
265
+ logger.warn(() => `Port ${port} is not published. Rejecting request.`);
184
266
  // Send error response
185
- this.rpc.sendError(sessionId, ref, 'Port is not published');
267
+ rpc.sendError(sessionId, ref, 'Port is not published');
186
268
  return;
187
269
  }
188
270
 
@@ -190,71 +272,463 @@ class PublishPort extends EventEmitter {
190
272
  const portConfig = this.publishedPorts.get(port);
191
273
  if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
192
274
  if (!portConfig.whitelist.includes(deviceId)) {
193
- logger.warn(`Device ${deviceId} is not whitelisted for port ${port}. Rejecting request.`);
194
- this.rpc.sendError(sessionId, ref, 'Device not whitelisted');
275
+ logger.warn(() => `Device ${deviceId} is not whitelisted for port ${port}. Rejecting request.`);
276
+ rpc.sendError(sessionId, ref, 'Device not whitelisted');
195
277
  return;
196
278
  }
197
- logger.info(`Device ${deviceId} is whitelisted for port ${port}. Accepting request.`);
279
+ logger.info(() => `Device ${deviceId} is whitelisted for port ${port}. Accepting request.`);
198
280
  }
199
281
 
200
282
  // Handle based on protocol
201
283
  if (protocol === 'tcp') {
202
- this.handleTCPConnection(sessionId, ref, port, deviceId);
284
+ this.handleTCPConnection(sessionId, ref, port, deviceId, connection);
203
285
  } else if (protocol === 'tls') {
204
- this.handleTLSConnection(sessionId, ref, port, deviceId);
286
+ if (isHandshake) {
287
+ this.handleTLSHandshake(sessionId, ref, port, deviceId, connection);
288
+ } else {
289
+ this.handleTLSConnection(sessionId, ref, port, deviceId, connection);
290
+ }
205
291
  } else if (protocol === 'udp') {
206
- this.handleUDPConnection(sessionId, ref, port, deviceId);
292
+ this.handleUDPConnection(sessionId, ref, port, deviceId, connection);
293
+ } else {
294
+ logger.warn(() => `Unsupported protocol: ${protocol}`);
295
+ rpc.sendError(sessionId, ref, `Unsupported protocol: ${protocol}`);
296
+ }
297
+ }
298
+
299
+ handleTLSHandshake(sessionId, ref, port, deviceId, connection) {
300
+ const rpc = this._getRpcFor(connection);
301
+ rpc.sendResponse(sessionId, ref, 'ok');
302
+
303
+ const diodeSocket = new DiodeSocket(ref, rpc);
304
+ const certPem = connection.getDeviceCertificate();
305
+ if (!certPem) {
306
+ logger.error(() => 'No device certificate available for TLS handshake');
307
+ return;
308
+ }
309
+
310
+ const tlsOptions = {
311
+ cert: certPem,
312
+ key: certPem,
313
+ rejectUnauthorized: false,
314
+ ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
315
+ ecdhCurve: 'secp256k1',
316
+ minVersion: 'TLSv1.2',
317
+ maxVersion: 'TLSv1.2',
318
+ };
319
+
320
+ const tlsSocket = new tls.TLSSocket(diodeSocket, {
321
+ isServer: true,
322
+ ...tlsOptions,
323
+ });
324
+ tlsSocket.setNoDelay(true);
325
+
326
+ connection.addConnection(ref, {
327
+ diodeSocket,
328
+ tlsSocket,
329
+ protocol: 'tls',
330
+ port,
331
+ deviceId,
332
+ handshake: true,
333
+ });
334
+
335
+ (async () => {
336
+ let session = null;
337
+ try {
338
+ const peerMessage = await nativeCrypto.readHandshakeMessage(tlsSocket, this.handshakeTimeoutMs);
339
+ session = this.nativeSessions.get(Number(peerMessage.physicalPort));
340
+ if (!session) {
341
+ throw new Error(`No native session for physical port ${peerMessage.physicalPort}`);
342
+ }
343
+
344
+ const verification = nativeCrypto.verifyHandshakeMessage(peerMessage, {
345
+ expectedRole: 'bind',
346
+ expectedDeviceId: session.deviceId,
347
+ expectedPhysicalPort: session.physicalPort,
348
+ });
349
+ if (!verification.ok) {
350
+ throw new Error(`Handshake verification failed: ${verification.reason}`);
351
+ }
352
+
353
+ const localDeviceId = connection.getEthereumAddress().toLowerCase();
354
+ const { message, privKey, nonce } = nativeCrypto.createHandshakeMessage({
355
+ role: 'publish',
356
+ deviceId: localDeviceId,
357
+ physicalPort: session.physicalPort,
358
+ privateKey: connection.getPrivateKey()
359
+ });
360
+
361
+ nativeCrypto.writeHandshakeMessage(tlsSocket, message);
362
+
363
+ session.session = nativeCrypto.deriveSessionKeys({
364
+ role: 'publish',
365
+ localDeviceId,
366
+ remoteDeviceId: verification.deviceId,
367
+ localEphPriv: privKey,
368
+ remoteEphPub: verification.ephPub,
369
+ localNonce: nonce,
370
+ remoteNonce: verification.nonce,
371
+ physicalPort: session.physicalPort,
372
+ });
373
+ session.ready = true;
374
+ if (session.timer) {
375
+ clearTimeout(session.timer);
376
+ session.timer = null;
377
+ }
378
+
379
+ if (session.localSocket && typeof session.localSocket.resume === 'function') {
380
+ session.localSocket.resume();
381
+ }
382
+
383
+ if (session.protocol === 'udp' && session.relaySocket && session.session) {
384
+ const probe = nativeCrypto.createUdpPacket(session.session, Buffer.alloc(0));
385
+ session.relaySocket.send(probe);
386
+ }
387
+ } catch (error) {
388
+ logger.error(() => `TLS handshake failed: ${error}`);
389
+ if (session) {
390
+ session.error = error;
391
+ }
392
+ } finally {
393
+ try { tlsSocket.end(); } catch {}
394
+ try { rpc.portClose(ref); } catch {}
395
+ try { connection.deleteConnection(ref); } catch {}
396
+ }
397
+ })();
398
+ }
399
+
400
+ handlePortOpen2(sessionIdRaw, messageContent, connection) {
401
+ const rpc = this._getRpcFor(connection);
402
+ // messageContent: ['portopen2', portName, physicalPort, sourceDeviceAddress, flags]
403
+ const portNameRaw = messageContent[1];
404
+ const physicalPortRaw = messageContent[2];
405
+ const sourceDeviceRaw = messageContent[3];
406
+ const flagsRaw = messageContent[4];
407
+
408
+ const sessionId = toBufferView(sessionIdRaw);
409
+ const portName = makeReadable(portNameRaw);
410
+ const physicalPort = parseUInt(physicalPortRaw);
411
+ const physicalPortRef = Number.isFinite(physicalPort) ? physicalPort : physicalPortRaw;
412
+ const flags = flagsRaw ? makeReadable(flagsRaw) : '';
413
+ const deviceId = normalizeDeviceId(sourceDeviceRaw);
414
+
415
+ logger.info(() => `Received portopen2 request for ${portName} on relay port ${physicalPort} from device ${deviceId}`);
416
+
417
+ // Parse port and protocol from portName
418
+ let port = 0;
419
+ let protocolFromName = null;
420
+ if (typeof portName === 'number') {
421
+ port = portName;
422
+ } else if (typeof portName === 'string') {
423
+ const parts = portName.split(':');
424
+ if (parts.length === 2) {
425
+ protocolFromName = parts[0].toLowerCase();
426
+ port = parseInt(parts[1], 10);
427
+ } else {
428
+ port = parseInt(portName, 10);
429
+ }
430
+ }
431
+
432
+ if (!Number.isFinite(port) || port <= 0) {
433
+ logger.warn(() => `Invalid port in portopen2: ${portName}`);
434
+ rpc.sendError(sessionId, physicalPortRef, 'Invalid port');
435
+ return;
436
+ }
437
+
438
+ // Determine protocol
439
+ let protocol = (typeof flags === 'string' && flags.includes('u')) ? 'udp' : 'tcp';
440
+ if (protocolFromName === 'udp' || protocolFromName === 'tcp') {
441
+ protocol = protocolFromName;
442
+ }
443
+
444
+ // Check if the port is published
445
+ if (!this.publishedPorts.has(port)) {
446
+ logger.warn(() => `Port ${port} is not published. Rejecting request.`);
447
+ rpc.sendError(sessionId, physicalPortRef, 'Port is not published');
448
+ return;
449
+ }
450
+
451
+ // Get port configuration and check whitelist if in private mode
452
+ const portConfig = this.publishedPorts.get(port);
453
+ if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
454
+ if (!portConfig.whitelist.includes(deviceId)) {
455
+ logger.warn(() => `Device ${deviceId} is not whitelisted for port ${port}. Rejecting request.`);
456
+ rpc.sendError(sessionId, physicalPortRef, 'Device not whitelisted');
457
+ return;
458
+ }
459
+ logger.info(() => `Device ${deviceId} is whitelisted for port ${port}. Accepting request.`);
460
+ }
461
+
462
+ if (!physicalPort) {
463
+ logger.warn(() => `Invalid physical port in portopen2: ${physicalPortRaw}`);
464
+ rpc.sendError(sessionId, physicalPortRef, 'Invalid physical port');
465
+ return;
466
+ }
467
+
468
+ const existing = this.nativeSessions.get(physicalPort);
469
+ if (existing) {
470
+ this._cleanupNativeSession(existing);
471
+ }
472
+
473
+ const session = {
474
+ physicalPort,
475
+ port,
476
+ protocol,
477
+ deviceId: deviceId.toLowerCase(),
478
+ connection,
479
+ ready: false,
480
+ session: null,
481
+ relaySocket: null,
482
+ localSocket: null,
483
+ timer: null,
484
+ };
485
+ session.timer = setTimeout(() => {
486
+ if (!session.ready) {
487
+ logger.warn(() => `Handshake timeout for native session on physical port ${physicalPort}`);
488
+ this._cleanupNativeSession(session);
489
+ }
490
+ }, Math.max(15000, this.handshakeTimeoutMs * 2));
491
+
492
+ this.nativeSessions.set(physicalPort, session);
493
+
494
+ if (protocol === 'udp') {
495
+ this.handleNativeUDPRelay(sessionId, physicalPortRef, session, connection);
207
496
  } else {
208
- logger.warn(`Unsupported protocol: ${protocol}`);
209
- this.rpc.sendError(sessionId, ref, `Unsupported protocol: ${protocol}`);
497
+ this.handleNativeTCPRelay(sessionId, physicalPortRef, session, connection);
498
+ }
499
+ }
500
+
501
+ _cleanupNativeSession(session) {
502
+ if (!session) return;
503
+ if (session.timer) {
504
+ clearTimeout(session.timer);
505
+ session.timer = null;
506
+ }
507
+ if (session.relaySocket) {
508
+ try {
509
+ if (typeof session.relaySocket.close === 'function') {
510
+ session.relaySocket.close();
511
+ } else {
512
+ session.relaySocket.destroy();
513
+ }
514
+ } catch (_) {}
515
+ }
516
+ if (session.localSocket) {
517
+ try {
518
+ if (typeof session.localSocket.close === 'function') {
519
+ session.localSocket.close();
520
+ } else {
521
+ session.localSocket.destroy();
522
+ }
523
+ } catch (_) {}
210
524
  }
525
+ this.nativeSessions.delete(session.physicalPort);
526
+ }
527
+
528
+ handleNativeTCPRelay(sessionId, physicalPortRef, session, connection) {
529
+ const rpc = this._getRpcFor(connection);
530
+ const { physicalPort, port, deviceId } = session;
531
+ let responded = false;
532
+ const sendOk = () => {
533
+ if (responded) return;
534
+ responded = true;
535
+ rpc.sendResponse(sessionId, physicalPortRef, 'ok');
536
+ };
537
+ const sendError = (reason) => {
538
+ if (responded) return;
539
+ responded = true;
540
+ rpc.sendError(sessionId, physicalPortRef, reason);
541
+ };
542
+
543
+ const relayHost = connection.getServerRelayHost();
544
+ const relaySocket = net.connect({ host: relayHost, port: physicalPort }, () => {
545
+ relaySocket.setNoDelay(true);
546
+ });
547
+ const localSocket = net.connect({ port }, () => {
548
+ localSocket.setNoDelay(true);
549
+ });
550
+ localSocket.pause();
551
+
552
+ session.relaySocket = relaySocket;
553
+ session.localSocket = localSocket;
554
+
555
+ let relayReady = false;
556
+ let localReady = false;
557
+ const maybeReady = () => {
558
+ if (relayReady && localReady) {
559
+ sendOk();
560
+ }
561
+ };
562
+
563
+ relaySocket.on('connect', () => {
564
+ relayReady = true;
565
+ maybeReady();
566
+ });
567
+ localSocket.on('connect', () => {
568
+ localReady = true;
569
+ maybeReady();
570
+ });
571
+
572
+ const cleanup = () => {
573
+ if (!relaySocket.destroyed) relaySocket.destroy();
574
+ if (!localSocket.destroyed) localSocket.destroy();
575
+ this._cleanupNativeSession(session);
576
+ };
577
+
578
+ relaySocket.on('error', (err) => {
579
+ logger.error(() => `Relay socket error (${deviceId}): ${err}`);
580
+ sendError('Relay connection failed');
581
+ cleanup();
582
+ });
583
+ localSocket.on('error', (err) => {
584
+ logger.error(() => `Local TCP service error (${deviceId}): ${err}`);
585
+ sendError('Local service connection failed');
586
+ cleanup();
587
+ });
588
+
589
+ relaySocket.on('end', cleanup);
590
+ localSocket.on('end', cleanup);
591
+
592
+ relaySocket.on('data', (data) => {
593
+ if (!session.ready || !session.session) return;
594
+ try {
595
+ const messages = nativeCrypto.consumeTcpFrames(session.session, data);
596
+ for (const msg of messages) {
597
+ localSocket.write(msg);
598
+ }
599
+ } catch (error) {
600
+ logger.error(() => `TCP decrypt error (${deviceId}): ${error}`);
601
+ cleanup();
602
+ }
603
+ });
604
+
605
+ localSocket.on('data', (data) => {
606
+ if (!session.ready || !session.session) return;
607
+ try {
608
+ const frame = nativeCrypto.createTcpFrame(session.session, data);
609
+ relaySocket.write(frame);
610
+ } catch (error) {
611
+ logger.error(() => `TCP encrypt error (${deviceId}): ${error}`);
612
+ cleanup();
613
+ }
614
+ });
615
+ }
616
+
617
+ handleNativeUDPRelay(sessionId, physicalPortRef, session, connection) {
618
+ const rpc = this._getRpcFor(connection);
619
+ const { physicalPort, port, deviceId } = session;
620
+ let responded = false;
621
+ const sendOk = () => {
622
+ if (responded) return;
623
+ responded = true;
624
+ rpc.sendResponse(sessionId, physicalPortRef, 'ok');
625
+ };
626
+ const sendError = (reason) => {
627
+ if (responded) return;
628
+ responded = true;
629
+ rpc.sendError(sessionId, physicalPortRef, reason);
630
+ };
631
+
632
+ const relaySocket = dgram.createSocket('udp4');
633
+ const localSocket = dgram.createSocket('udp4');
634
+
635
+ let relayReady = false;
636
+ let localReady = false;
637
+ const maybeReady = () => {
638
+ if (relayReady && localReady) {
639
+ sendOk();
640
+ }
641
+ };
642
+
643
+ relaySocket.on('message', (msg) => {
644
+ if (!session.ready || !session.session) return;
645
+ const plaintext = nativeCrypto.parseUdpPacket(session.session, msg);
646
+ if (!plaintext) return;
647
+ localSocket.send(plaintext);
648
+ });
649
+ localSocket.on('message', (msg) => {
650
+ if (!session.ready || !session.session) return;
651
+ const packet = nativeCrypto.createUdpPacket(session.session, msg);
652
+ relaySocket.send(packet);
653
+ });
654
+
655
+ relaySocket.on('error', (err) => {
656
+ logger.error(() => `Relay UDP socket error (${deviceId}): ${err}`);
657
+ sendError('Relay UDP error');
658
+ try { relaySocket.close(); } catch {}
659
+ try { localSocket.close(); } catch {}
660
+ this._cleanupNativeSession(session);
661
+ });
662
+ localSocket.on('error', (err) => {
663
+ logger.error(() => `Local UDP service error (${deviceId}): ${err}`);
664
+ sendError('Local UDP error');
665
+ try { relaySocket.close(); } catch {}
666
+ try { localSocket.close(); } catch {}
667
+ this._cleanupNativeSession(session);
668
+ });
669
+
670
+ const relayHost = connection.getServerRelayHost();
671
+ relaySocket.connect(physicalPort, relayHost, () => {
672
+ relayReady = true;
673
+ maybeReady();
674
+ });
675
+ localSocket.connect(port, '127.0.0.1', () => {
676
+ localReady = true;
677
+ maybeReady();
678
+ });
679
+
680
+ session.relaySocket = relaySocket;
681
+ session.localSocket = localSocket;
211
682
  }
212
683
 
213
- setupLocalSocketHandlers(localSocket, ref, protocol) {
684
+ setupLocalSocketHandlers(localSocket, ref, protocol, rpc, connection) {
214
685
  if (protocol === 'udp') {
215
686
 
216
687
  } else {
217
688
  localSocket.on('data', (data) => {
218
689
  // When data is received from the local service, send it back via Diode
219
- this.rpc.portSend(ref, data);
690
+ rpc.portSend(ref, data);
220
691
  });
221
692
 
222
693
  localSocket.on('end', () => {
223
- logger.info(`Local service disconnected`);
694
+ logger.info(() => `Local service disconnected`);
224
695
  // Send portclose message to Diode
225
- this.rpc.portClose(ref);
226
- this.connection.deleteConnection(ref);
696
+ rpc.portClose(ref);
697
+ connection.deleteConnection(ref);
227
698
  });
228
699
 
229
700
  localSocket.on('error', (err) => {
230
- logger.error(`Error with local service: ${err}`);
701
+ logger.error(() => `Error with local service: ${err}`);
231
702
  // Send portclose message to Diode
232
- this.rpc.portClose(ref);
233
- this.connection.deleteConnection(ref);
703
+ rpc.portClose(ref);
704
+ connection.deleteConnection(ref);
234
705
  });
235
706
  }
236
707
  }
237
708
 
238
- handleTCPConnection(sessionId, ref, port, deviceId) {
709
+ handleTCPConnection(sessionId, ref, port, deviceId, connection) {
710
+ const rpc = this._getRpcFor(connection);
239
711
  // Create a TCP connection to the local service on the specified port
240
712
  const localSocket = net.connect({ port: port }, () => {
241
- logger.info(`Connected to local TCP service on port ${port}`);
713
+ localSocket.setNoDelay(true);
714
+ logger.info(() => `Connected to local TCP service on port ${port}`);
242
715
  // Send success response
243
- this.rpc.sendResponse(sessionId, ref, 'ok');
716
+ rpc.sendResponse(sessionId, ref, 'ok');
244
717
  });
245
718
 
246
719
  // Handle data, end, and error events
247
- this.setupLocalSocketHandlers(localSocket, ref, 'tcp');
720
+ this.setupLocalSocketHandlers(localSocket, ref, 'tcp', rpc, connection);
248
721
 
249
722
  // Store the local socket with the ref using connection's method
250
- this.connection.addConnection(ref, { socket: localSocket, protocol: 'tcp', port, deviceId });
723
+ connection.addConnection(ref, { socket: localSocket, protocol: 'tcp', port, deviceId });
251
724
  }
252
725
 
253
- handleTLSConnection(sessionId, ref, port, deviceId) {
726
+ handleTLSConnection(sessionId, ref, port, deviceId, connection) {
727
+ const rpc = this._getRpcFor(connection);
254
728
  // Create a DiodeSocket instance
255
- const diodeSocket = new DiodeSocket(ref, this.rpc);
729
+ const diodeSocket = new DiodeSocket(ref, rpc);
256
730
 
257
- const certPem = this.connection.getDeviceCertificate();
731
+ const certPem = connection.getDeviceCertificate();
258
732
 
259
733
  // TLS options with your server's certificate and key
260
734
  const tlsOptions = {
@@ -272,12 +746,12 @@ class PublishPort extends EventEmitter {
272
746
  isServer: true,
273
747
  ...tlsOptions,
274
748
  });
275
-
749
+ tlsSocket.setNoDelay(true);
276
750
  // Connect to the local service (TCP or TLS as needed)
277
751
  const localSocket = net.connect({ port: port }, () => {
278
- logger.info(`Connected to local TCP service on port ${port}`);
752
+ logger.info(() => `Connected to local TCP service on port ${port}`);
279
753
  // Send success response
280
- this.rpc.sendResponse(sessionId, ref, 'ok');
754
+ rpc.sendResponse(sessionId, ref, 'ok');
281
755
  });
282
756
 
283
757
  // Pipe data between the TLS socket and the local service
@@ -285,17 +759,17 @@ class PublishPort extends EventEmitter {
285
759
 
286
760
  // Handle errors and cleanup
287
761
  tlsSocket.on('error', (err) => {
288
- logger.error(`TLS Socket error: ${err}`);
289
- this.rpc.portClose(ref);
290
- this.connection.deleteConnection(ref);
762
+ logger.error(() => `TLS Socket error: ${err}`);
763
+ rpc.portClose(ref);
764
+ connection.deleteConnection(ref);
291
765
  });
292
766
 
293
767
  tlsSocket.on('close', () => {
294
- this.connection.deleteConnection(ref);
768
+ connection.deleteConnection(ref);
295
769
  });
296
770
 
297
771
  // Store the connection info using connection's method
298
- this.connection.addConnection(ref, {
772
+ connection.addConnection(ref, {
299
773
  diodeSocket,
300
774
  tlsSocket,
301
775
  localSocket,
@@ -305,18 +779,29 @@ class PublishPort extends EventEmitter {
305
779
  });
306
780
  }
307
781
 
308
- handleUDPConnection(sessionId, ref, port, deviceId) {
782
+ handleUDPConnection(sessionId, ref, port, deviceId, connection) {
783
+ const rpc = this._getRpcFor(connection);
309
784
  // Create a UDP socket
310
785
  const localSocket = dgram.createSocket('udp4');
311
786
 
787
+ // Try larger kernel buffers if available
788
+ localSocket.on('listening', () => {
789
+ try {
790
+ localSocket.setRecvBufferSize(1 << 20); // ~1MB
791
+ localSocket.setSendBufferSize(1 << 20);
792
+ } catch (e) {
793
+ logger.debug(() => `UDP buffer sizing not supported: ${e.message}`);
794
+ }
795
+ });
796
+
312
797
  // Store the remote address and port from the Diode client
313
798
  const remoteInfo = {port, address: '127.0.0.1'};
314
799
 
315
800
  // Send success response
316
- this.rpc.sendResponse(sessionId, ref, 'ok');
801
+ rpc.sendResponse(sessionId, ref, 'ok');
317
802
 
318
803
  // Store the connection info using connection's method
319
- this.connection.addConnection(ref, {
804
+ connection.addConnection(ref, {
320
805
  socket: localSocket,
321
806
  protocol: 'udp',
322
807
  remoteInfo,
@@ -324,51 +809,47 @@ class PublishPort extends EventEmitter {
324
809
  deviceId
325
810
  });
326
811
 
327
- logger.info(`UDP connection set up on port ${port}`);
812
+ logger.info(() => `UDP connection set up on port ${port}`);
328
813
 
329
814
  // Handle messages from the local UDP service
330
815
  localSocket.on('message', (msg, rinfo) => {
331
- //need to add 4 bytes of data length to the beginning of the message but it's Big Endian
332
- const dataLength = Buffer.alloc(4);
333
- dataLength.writeUInt32LE(msg.length, 0);
334
- const data = Buffer.concat([dataLength, msg]);
335
- // Send the data back to the Diode client via portSend
336
- this.rpc.portSend(ref, data);
816
+ rpc.portSend(ref, msg);
337
817
  });
338
818
 
339
819
  localSocket.on('error', (err) => {
340
- logger.error(`UDP Socket error: ${err}`);
341
- this.rpc.portClose(ref);
342
- this.connection.deleteConnection(ref);
820
+ logger.error(() => `UDP Socket error: ${err}`);
821
+ rpc.portClose(ref);
822
+ connection.deleteConnection(ref);
343
823
  });
344
824
  }
345
825
 
346
- handlePortSend(sessionIdRaw, messageContent) {
826
+ handlePortSend(sessionIdRaw, messageContent, connection) {
827
+ const rpc = this._getRpcFor(connection);
347
828
  const refRaw = messageContent[1];
348
829
  const dataRaw = messageContent[2];
349
830
 
350
- const sessionId = Buffer.from(sessionIdRaw);
351
- const ref = Buffer.from(refRaw);
352
- const data = Buffer.from(dataRaw)//.slice(4);
831
+ const sessionId = toBufferView(sessionIdRaw);
832
+ const ref = toBufferView(refRaw);
833
+ const data = toBufferView(dataRaw);//.slice(4);
353
834
 
354
- const connectionInfo = this.connection.getConnection(ref);
835
+ const connectionInfo = connection.getConnection(ref);
355
836
  // Check if the port is still open and address is still in whitelist
356
837
  if (connectionInfo) {
357
838
  const { socket: localSocket, protocol, remoteInfo, port, deviceId } = connectionInfo;
358
839
 
359
840
  if (!this.publishedPorts.has(port)) {
360
- logger.warn(`Port ${port} is not published. Sending portclose.`);
361
- this.rpc.portClose(ref);
362
- this.connection.deleteConnection(ref);
841
+ logger.warn(() => `Port ${port} is not published. Sending portclose.`);
842
+ rpc.portClose(ref);
843
+ connection.deleteConnection(ref);
363
844
  return;
364
845
  }
365
846
 
366
847
  const portConfig = this.publishedPorts.get(port);
367
848
  if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
368
849
  if (!portConfig.whitelist.includes(deviceId)) {
369
- logger.warn(`Device ${deviceId} is not whitelisted for port ${port}. Sending portclose.`);
370
- this.rpc.portClose(ref);
371
- this.connection.deleteConnection(ref);
850
+ logger.warn(() => `Device ${deviceId} is not whitelisted for port ${port}. Sending portclose.`);
851
+ rpc.portClose(ref);
852
+ connection.deleteConnection(ref);
372
853
  return;
373
854
  }
374
855
  }
@@ -396,24 +877,24 @@ class PublishPort extends EventEmitter {
396
877
  diodeSocket.pushData(data);
397
878
  }
398
879
  } else {
399
- const clientSocket = this.connection.getClientSocket(ref);
880
+ const clientSocket = connection.getClientSocket(ref);
400
881
  if (clientSocket) {
401
- logger.debug(`No local connection found for ref: ${ref.toString('hex')}, but client socket exists`);
882
+ logger.debug(() => `No local connection found for ref: ${ref.toString('hex')}, but client socket exists`);
402
883
  } else {
403
- logger.warn(`No local connection found for ref ${ref.toString('hex')}. Sending portclose.`);
404
- this.rpc.sendError(sessionId, ref, 'No local connection found');
884
+ logger.warn(() => `No local connection found for ref ${ref.toString('hex')}. Sending portclose.`);
885
+ rpc.sendError(sessionId, ref, 'No local connection found');
405
886
  }
406
887
  }
407
888
  }
408
889
 
409
- handlePortClose(sessionIdRaw, messageContent) {
890
+ handlePortClose(sessionIdRaw, messageContent, connection) {
410
891
  const refRaw = messageContent[1];
411
- const sessionId = Buffer.from(sessionIdRaw);
412
- const ref = Buffer.from(refRaw);
892
+ const sessionId = toBufferView(sessionIdRaw);
893
+ const ref = toBufferView(refRaw);
413
894
 
414
- logger.info(`Received portclose for ref ${ref.toString('hex')}`);
895
+ logger.info(() => `Received portclose for ref ${ref.toString('hex')}`);
415
896
 
416
- const connectionInfo = this.connection.getConnection(ref);
897
+ const connectionInfo = connection.getConnection(ref);
417
898
  if (connectionInfo) {
418
899
  const { diodeSocket, tlsSocket, socket: localSocket } = connectionInfo;
419
900
  // End all sockets
@@ -426,7 +907,7 @@ class PublishPort extends EventEmitter {
426
907
  localSocket.end();
427
908
  }
428
909
  }
429
- this.connection.deleteConnection(ref);
910
+ connection.deleteConnection(ref);
430
911
  }
431
912
  }
432
913
  }