signalk-edge-link 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +3 -2
- package/lib/bonding.js +4 -1
- package/lib/connection-config.js +18 -0
- package/lib/crypto.js +49 -20
- package/lib/index.js +6 -454
- package/lib/instance.js +3 -4
- package/lib/packet.js +14 -8
- package/lib/pipeline-v2-client.js +22 -9
- package/lib/pipeline-v2-server.js +27 -3
- package/lib/pipeline.js +6 -2
- package/lib/routes/monitoring.js +3 -9
- package/lib/shared/connection-schema.js +539 -0
- package/lib/shared/crypto-constants.js +18 -0
- package/package.json +6 -3
- package/public/{277.509a7bfc11fac344f433.js → 277.99e19dcb5b778c964ace.js} +4 -4
- package/public/277.99e19dcb5b778c964ace.js.map +1 -0
- package/public/982.b207a377ed6542e2fb4a.js +2 -0
- package/public/982.b207a377ed6542e2fb4a.js.map +1 -0
- package/public/index.html +1 -1
- package/public/{main.3576323fe7e587bd7c5e.js → main.f1780db6593b0c07a48c.js} +2 -2
- package/public/{main.3576323fe7e587bd7c5e.js.map → main.f1780db6593b0c07a48c.js.map} +1 -1
- package/public/remoteEntry.js +1 -1
- package/public/remoteEntry.js.map +1 -1
- package/public/11.413e152fbef6e7dfd2f8.js +0 -2
- package/public/11.413e152fbef6e7dfd2f8.js.map +0 -1
- package/public/277.509a7bfc11fac344f433.js.map +0 -1
- package/schemas/config.schema.json +0 -104
- /package/public/{277.509a7bfc11fac344f433.js.LICENSE.txt → 277.99e19dcb5b778c964ace.js.LICENSE.txt} +0 -0
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const createRoutes = require("./routes");
|
|
4
4
|
const instance_1 = require("./instance");
|
|
5
5
|
const connection_config_1 = require("./connection-config");
|
|
6
|
+
const connection_schema_1 = require("./shared/connection-schema");
|
|
6
7
|
const pkg = require("../package.json");
|
|
7
8
|
module.exports = function createPlugin(app) {
|
|
8
9
|
const plugin = {};
|
|
@@ -200,460 +201,11 @@ module.exports = function createPlugin(app) {
|
|
|
200
201
|
// ── Schema (array-based) ─────────────────────────────────────────────────
|
|
201
202
|
//
|
|
202
203
|
// Each item in the `connections` array is a full connection configuration.
|
|
203
|
-
// The
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
const connectionItemSchema = {
|
|
209
|
-
type: "object",
|
|
210
|
-
title: "Connection",
|
|
211
|
-
required: ["serverType", "udpPort", "secretKey"],
|
|
212
|
-
properties: {
|
|
213
|
-
name: {
|
|
214
|
-
type: "string",
|
|
215
|
-
title: "Connection Name",
|
|
216
|
-
description: "Human-readable label for this connection (e.g. 'Shore Server', 'Sat Client'). Used to namespace config files and Signal K metrics paths.",
|
|
217
|
-
default: "connection",
|
|
218
|
-
maxLength: 40
|
|
219
|
-
},
|
|
220
|
-
serverType: {
|
|
221
|
-
type: "string",
|
|
222
|
-
title: "Operation Mode",
|
|
223
|
-
description: "Select Server to receive data, or Client to send data",
|
|
224
|
-
default: "client",
|
|
225
|
-
oneOf: [
|
|
226
|
-
{ const: "server", title: "Server Mode – Receive Data" },
|
|
227
|
-
{ const: "client", title: "Client Mode – Send Data" }
|
|
228
|
-
]
|
|
229
|
-
},
|
|
230
|
-
udpPort: {
|
|
231
|
-
type: "number",
|
|
232
|
-
title: "UDP Port",
|
|
233
|
-
description: "UDP port for data transmission (must match on both ends)",
|
|
234
|
-
default: 4446,
|
|
235
|
-
minimum: 1024,
|
|
236
|
-
maximum: 65535
|
|
237
|
-
},
|
|
238
|
-
secretKey: {
|
|
239
|
-
type: "string",
|
|
240
|
-
title: "Encryption Key",
|
|
241
|
-
description: "32-byte secret key: 32-character ASCII, 64-character hex, or 44-character base64",
|
|
242
|
-
minLength: 32,
|
|
243
|
-
maxLength: 64,
|
|
244
|
-
pattern: "^(?:.{32}|[0-9a-fA-F]{64}|[A-Za-z0-9+/]{43}=?)$"
|
|
245
|
-
},
|
|
246
|
-
useMsgpack: {
|
|
247
|
-
type: "boolean",
|
|
248
|
-
title: "Use MessagePack",
|
|
249
|
-
description: "Binary serialization for smaller payloads (must match on both ends)",
|
|
250
|
-
default: false
|
|
251
|
-
},
|
|
252
|
-
usePathDictionary: {
|
|
253
|
-
type: "boolean",
|
|
254
|
-
title: "Use Path Dictionary",
|
|
255
|
-
description: "Encode paths as numeric IDs for bandwidth savings (must match on both ends)",
|
|
256
|
-
default: false
|
|
257
|
-
},
|
|
258
|
-
protocolVersion: {
|
|
259
|
-
type: "number",
|
|
260
|
-
title: "Protocol Version",
|
|
261
|
-
description: "v1: encrypted UDP. v2 adds reliable delivery and metrics. v3 keeps the v2 data path and authenticates control packets (ACK/NAK/HEARTBEAT/HELLO). Must match on both ends.",
|
|
262
|
-
default: 1,
|
|
263
|
-
oneOf: [
|
|
264
|
-
{ const: 1, title: "v1 – Standard encrypted UDP" },
|
|
265
|
-
{ const: 2, title: "v2 – Reliability, congestion control, bonding, metrics" },
|
|
266
|
-
{ const: 3, title: "v3 - v2 features with authenticated control packets" }
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
dependencies: {
|
|
271
|
-
serverType: {
|
|
272
|
-
oneOf: [
|
|
273
|
-
{
|
|
274
|
-
properties: {
|
|
275
|
-
serverType: { enum: ["server"] },
|
|
276
|
-
reliability: {
|
|
277
|
-
type: "object",
|
|
278
|
-
title: "Reliability Settings (v2/v3 only)",
|
|
279
|
-
description: "Requires Protocol v2 or v3. Controls ACK/NAK timing for reliable delivery",
|
|
280
|
-
properties: {
|
|
281
|
-
ackInterval: {
|
|
282
|
-
type: "number",
|
|
283
|
-
title: "ACK Interval (ms)",
|
|
284
|
-
description: "How often server sends cumulative ACK updates",
|
|
285
|
-
default: 100,
|
|
286
|
-
minimum: 20,
|
|
287
|
-
maximum: 5000
|
|
288
|
-
},
|
|
289
|
-
ackResendInterval: {
|
|
290
|
-
type: "number",
|
|
291
|
-
title: "ACK Resend Interval (ms)",
|
|
292
|
-
description: "Re-send duplicate ACK periodically to recover from lost ACK packets",
|
|
293
|
-
default: 1000,
|
|
294
|
-
minimum: 100,
|
|
295
|
-
maximum: 10000
|
|
296
|
-
},
|
|
297
|
-
nakTimeout: {
|
|
298
|
-
type: "number",
|
|
299
|
-
title: "NAK Timeout (ms)",
|
|
300
|
-
description: "Delay before requesting retransmission for missing sequence numbers",
|
|
301
|
-
default: 100,
|
|
302
|
-
minimum: 20,
|
|
303
|
-
maximum: 5000
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
properties: {
|
|
311
|
-
serverType: { enum: ["client"] },
|
|
312
|
-
udpAddress: {
|
|
313
|
-
type: "string",
|
|
314
|
-
title: "Server Address",
|
|
315
|
-
description: "IP address or hostname of the SignalK server",
|
|
316
|
-
default: "127.0.0.1"
|
|
317
|
-
},
|
|
318
|
-
helloMessageSender: {
|
|
319
|
-
type: "integer",
|
|
320
|
-
title: "Heartbeat Interval (seconds)",
|
|
321
|
-
description: "How often to send heartbeat messages",
|
|
322
|
-
default: 60,
|
|
323
|
-
minimum: 10,
|
|
324
|
-
maximum: 3600
|
|
325
|
-
},
|
|
326
|
-
testAddress: {
|
|
327
|
-
type: "string",
|
|
328
|
-
title: "Connectivity Test Address",
|
|
329
|
-
description: "Address to ping for network testing (e.g., 8.8.8.8)",
|
|
330
|
-
default: "127.0.0.1"
|
|
331
|
-
},
|
|
332
|
-
testPort: {
|
|
333
|
-
type: "number",
|
|
334
|
-
title: "Connectivity Test Port",
|
|
335
|
-
description: "Port for connectivity test (80, 443, 53)",
|
|
336
|
-
default: 80,
|
|
337
|
-
minimum: 1,
|
|
338
|
-
maximum: 65535
|
|
339
|
-
},
|
|
340
|
-
pingIntervalTime: {
|
|
341
|
-
type: "number",
|
|
342
|
-
title: "Check Interval (minutes)",
|
|
343
|
-
description: "How often to test network connectivity",
|
|
344
|
-
default: 1,
|
|
345
|
-
minimum: 0.1,
|
|
346
|
-
maximum: 60
|
|
347
|
-
},
|
|
348
|
-
heartbeatInterval: {
|
|
349
|
-
type: "number",
|
|
350
|
-
title: "NAT Keepalive Heartbeat Interval (ms)",
|
|
351
|
-
description: "v2/v3 only. How often to send UDP heartbeat packets for NAT traversal. Typical NAT timeouts range from 30s to 120s.",
|
|
352
|
-
default: 25000,
|
|
353
|
-
minimum: 5000,
|
|
354
|
-
maximum: 120000
|
|
355
|
-
},
|
|
356
|
-
reliability: {
|
|
357
|
-
type: "object",
|
|
358
|
-
title: "Reliability Settings (v2/v3 only)",
|
|
359
|
-
description: "Requires Protocol v2 or v3. Controls retransmit queue behavior and packet retry limits",
|
|
360
|
-
properties: {
|
|
361
|
-
retransmitQueueSize: {
|
|
362
|
-
type: "number",
|
|
363
|
-
title: "Retransmit Queue Size",
|
|
364
|
-
description: "Maximum number of sent packets stored for potential retransmission",
|
|
365
|
-
default: 5000,
|
|
366
|
-
minimum: 100,
|
|
367
|
-
maximum: 50000
|
|
368
|
-
},
|
|
369
|
-
maxRetransmits: {
|
|
370
|
-
type: "number",
|
|
371
|
-
title: "Max Retransmit Attempts",
|
|
372
|
-
description: "Maximum resend attempts before a packet is dropped from the retransmit queue",
|
|
373
|
-
default: 3,
|
|
374
|
-
minimum: 1,
|
|
375
|
-
maximum: 20
|
|
376
|
-
},
|
|
377
|
-
retransmitMaxAge: {
|
|
378
|
-
type: "number",
|
|
379
|
-
title: "Retransmit Max Age (ms)",
|
|
380
|
-
description: "Expire stale unacknowledged packets older than this age",
|
|
381
|
-
default: 120000,
|
|
382
|
-
minimum: 1000,
|
|
383
|
-
maximum: 300000
|
|
384
|
-
},
|
|
385
|
-
retransmitMinAge: {
|
|
386
|
-
type: "number",
|
|
387
|
-
title: "Retransmit Min Age (ms)",
|
|
388
|
-
description: "Minimum packet age before expiration is allowed",
|
|
389
|
-
default: 10000,
|
|
390
|
-
minimum: 200,
|
|
391
|
-
maximum: 30000
|
|
392
|
-
},
|
|
393
|
-
retransmitRttMultiplier: {
|
|
394
|
-
type: "number",
|
|
395
|
-
title: "RTT Expiry Multiplier",
|
|
396
|
-
description: "Dynamic expiry age is adjusted to RTT x this multiplier",
|
|
397
|
-
default: 12,
|
|
398
|
-
minimum: 2,
|
|
399
|
-
maximum: 20
|
|
400
|
-
},
|
|
401
|
-
ackIdleDrainAge: {
|
|
402
|
-
type: "number",
|
|
403
|
-
title: "ACK Idle Drain Age (ms)",
|
|
404
|
-
description: "If ACKs are idle longer than this, expiry becomes more aggressive",
|
|
405
|
-
default: 20000,
|
|
406
|
-
minimum: 500,
|
|
407
|
-
maximum: 30000
|
|
408
|
-
},
|
|
409
|
-
forceDrainAfterAckIdle: {
|
|
410
|
-
type: "boolean",
|
|
411
|
-
title: "Force Drain After ACK Idle",
|
|
412
|
-
description: "When enabled, clear retransmit queue if no ACKs arrive for too long",
|
|
413
|
-
default: false
|
|
414
|
-
},
|
|
415
|
-
forceDrainAfterMs: {
|
|
416
|
-
type: "number",
|
|
417
|
-
title: "Force Drain Timeout (ms)",
|
|
418
|
-
description: "ACK idle duration before force-draining retransmit queue to zero",
|
|
419
|
-
default: 45000,
|
|
420
|
-
minimum: 2000,
|
|
421
|
-
maximum: 120000
|
|
422
|
-
},
|
|
423
|
-
recoveryBurstEnabled: {
|
|
424
|
-
type: "boolean",
|
|
425
|
-
title: "Recovery Burst Enabled",
|
|
426
|
-
description: "When ACKs return after outage, rapidly retransmit queued packets to catch up",
|
|
427
|
-
default: true
|
|
428
|
-
},
|
|
429
|
-
recoveryBurstSize: {
|
|
430
|
-
type: "number",
|
|
431
|
-
title: "Recovery Burst Size",
|
|
432
|
-
description: "Max queued packets to retransmit per recovery burst cycle",
|
|
433
|
-
default: 100,
|
|
434
|
-
minimum: 10,
|
|
435
|
-
maximum: 1000
|
|
436
|
-
},
|
|
437
|
-
recoveryBurstIntervalMs: {
|
|
438
|
-
type: "number",
|
|
439
|
-
title: "Recovery Burst Interval (ms)",
|
|
440
|
-
description: "Interval between recovery burst cycles while backlog exists",
|
|
441
|
-
default: 200,
|
|
442
|
-
minimum: 50,
|
|
443
|
-
maximum: 5000
|
|
444
|
-
},
|
|
445
|
-
recoveryAckGapMs: {
|
|
446
|
-
type: "number",
|
|
447
|
-
title: "Recovery ACK Gap (ms)",
|
|
448
|
-
description: "Minimum ACK silence before triggering fast recovery bursts",
|
|
449
|
-
default: 4000,
|
|
450
|
-
minimum: 500,
|
|
451
|
-
maximum: 120000
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
congestionControl: {
|
|
456
|
-
type: "object",
|
|
457
|
-
title: "Dynamic Congestion Control (v2/v3 only)",
|
|
458
|
-
description: "Requires Protocol v2 or v3. AIMD algorithm to dynamically adjust send rate based on network conditions",
|
|
459
|
-
properties: {
|
|
460
|
-
enabled: {
|
|
461
|
-
type: "boolean",
|
|
462
|
-
title: "Enable Congestion Control",
|
|
463
|
-
description: "Automatically adjust delta timer based on RTT and packet loss",
|
|
464
|
-
default: false
|
|
465
|
-
},
|
|
466
|
-
targetRTT: {
|
|
467
|
-
type: "number",
|
|
468
|
-
title: "Target RTT (ms)",
|
|
469
|
-
description: "RTT threshold above which send rate is reduced",
|
|
470
|
-
default: 200,
|
|
471
|
-
minimum: 50,
|
|
472
|
-
maximum: 2000
|
|
473
|
-
},
|
|
474
|
-
nominalDeltaTimer: {
|
|
475
|
-
type: "number",
|
|
476
|
-
title: "Nominal Delta Timer (ms)",
|
|
477
|
-
description: "Preferred steady-state send interval",
|
|
478
|
-
default: 1000,
|
|
479
|
-
minimum: 100,
|
|
480
|
-
maximum: 10000
|
|
481
|
-
},
|
|
482
|
-
minDeltaTimer: {
|
|
483
|
-
type: "number",
|
|
484
|
-
title: "Minimum Delta Timer (ms)",
|
|
485
|
-
description: "Fastest allowed send interval",
|
|
486
|
-
default: 100,
|
|
487
|
-
minimum: 50,
|
|
488
|
-
maximum: 1000
|
|
489
|
-
},
|
|
490
|
-
maxDeltaTimer: {
|
|
491
|
-
type: "number",
|
|
492
|
-
title: "Maximum Delta Timer (ms)",
|
|
493
|
-
description: "Slowest allowed send interval",
|
|
494
|
-
default: 5000,
|
|
495
|
-
minimum: 1000,
|
|
496
|
-
maximum: 30000
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
bonding: {
|
|
501
|
-
type: "object",
|
|
502
|
-
title: "Connection Bonding (v2/v3 only)",
|
|
503
|
-
description: "Requires Protocol v2 or v3. Dual-link bonding with automatic failover between primary and backup connections",
|
|
504
|
-
properties: {
|
|
505
|
-
enabled: {
|
|
506
|
-
type: "boolean",
|
|
507
|
-
title: "Enable Connection Bonding",
|
|
508
|
-
description: "Enable dual-link bonding with automatic failover",
|
|
509
|
-
default: false
|
|
510
|
-
},
|
|
511
|
-
mode: {
|
|
512
|
-
type: "string",
|
|
513
|
-
title: "Bonding Mode",
|
|
514
|
-
description: "Bonding operating mode",
|
|
515
|
-
default: "main-backup",
|
|
516
|
-
oneOf: [
|
|
517
|
-
{
|
|
518
|
-
const: "main-backup",
|
|
519
|
-
title: "Main/Backup – Failover to backup when primary degrades"
|
|
520
|
-
}
|
|
521
|
-
]
|
|
522
|
-
},
|
|
523
|
-
primary: {
|
|
524
|
-
type: "object",
|
|
525
|
-
title: "Primary Link",
|
|
526
|
-
description: "Primary connection (e.g., LTE modem)",
|
|
527
|
-
properties: {
|
|
528
|
-
address: { type: "string", title: "Server Address", default: "127.0.0.1" },
|
|
529
|
-
port: {
|
|
530
|
-
type: "number",
|
|
531
|
-
title: "UDP Port",
|
|
532
|
-
default: 4446,
|
|
533
|
-
minimum: 1024,
|
|
534
|
-
maximum: 65535
|
|
535
|
-
},
|
|
536
|
-
interface: { type: "string", title: "Bind Interface (optional)" }
|
|
537
|
-
}
|
|
538
|
-
},
|
|
539
|
-
backup: {
|
|
540
|
-
type: "object",
|
|
541
|
-
title: "Backup Link",
|
|
542
|
-
description: "Backup connection (e.g., Starlink, satellite)",
|
|
543
|
-
properties: {
|
|
544
|
-
address: { type: "string", title: "Server Address", default: "127.0.0.1" },
|
|
545
|
-
port: {
|
|
546
|
-
type: "number",
|
|
547
|
-
title: "UDP Port",
|
|
548
|
-
default: 4447,
|
|
549
|
-
minimum: 1024,
|
|
550
|
-
maximum: 65535
|
|
551
|
-
},
|
|
552
|
-
interface: { type: "string", title: "Bind Interface (optional)" }
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
failover: {
|
|
556
|
-
type: "object",
|
|
557
|
-
title: "Failover Thresholds",
|
|
558
|
-
description: "Configure when failover is triggered",
|
|
559
|
-
properties: {
|
|
560
|
-
rttThreshold: {
|
|
561
|
-
type: "number",
|
|
562
|
-
title: "RTT Threshold (ms)",
|
|
563
|
-
default: 500,
|
|
564
|
-
minimum: 100,
|
|
565
|
-
maximum: 5000
|
|
566
|
-
},
|
|
567
|
-
lossThreshold: {
|
|
568
|
-
type: "number",
|
|
569
|
-
title: "Packet Loss Threshold",
|
|
570
|
-
default: 0.1,
|
|
571
|
-
minimum: 0.01,
|
|
572
|
-
maximum: 0.5
|
|
573
|
-
},
|
|
574
|
-
healthCheckInterval: {
|
|
575
|
-
type: "number",
|
|
576
|
-
title: "Health Check Interval (ms)",
|
|
577
|
-
default: 1000,
|
|
578
|
-
minimum: 500,
|
|
579
|
-
maximum: 10000
|
|
580
|
-
},
|
|
581
|
-
failbackDelay: {
|
|
582
|
-
type: "number",
|
|
583
|
-
title: "Failback Delay (ms)",
|
|
584
|
-
default: 30000,
|
|
585
|
-
minimum: 5000,
|
|
586
|
-
maximum: 300000
|
|
587
|
-
},
|
|
588
|
-
heartbeatTimeout: {
|
|
589
|
-
type: "number",
|
|
590
|
-
title: "Heartbeat Timeout (ms)",
|
|
591
|
-
default: 5000,
|
|
592
|
-
minimum: 1000,
|
|
593
|
-
maximum: 30000
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
},
|
|
599
|
-
alertThresholds: {
|
|
600
|
-
type: "object",
|
|
601
|
-
title: "Monitoring Alert Thresholds (v2/v3 only)",
|
|
602
|
-
description: "Customize warning/critical thresholds for network monitoring alerts",
|
|
603
|
-
properties: {
|
|
604
|
-
rtt: {
|
|
605
|
-
type: "object",
|
|
606
|
-
title: "RTT Thresholds",
|
|
607
|
-
properties: {
|
|
608
|
-
warning: { type: "number", title: "Warning RTT (ms)", default: 300 },
|
|
609
|
-
critical: { type: "number", title: "Critical RTT (ms)", default: 800 }
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
packetLoss: {
|
|
613
|
-
type: "object",
|
|
614
|
-
title: "Packet Loss Thresholds",
|
|
615
|
-
properties: {
|
|
616
|
-
warning: { type: "number", title: "Warning Loss Ratio", default: 0.03 },
|
|
617
|
-
critical: { type: "number", title: "Critical Loss Ratio", default: 0.1 }
|
|
618
|
-
}
|
|
619
|
-
},
|
|
620
|
-
retransmitRate: {
|
|
621
|
-
type: "object",
|
|
622
|
-
title: "Retransmit Rate Thresholds",
|
|
623
|
-
properties: {
|
|
624
|
-
warning: { type: "number", title: "Warning Retransmit Ratio", default: 0.05 },
|
|
625
|
-
critical: {
|
|
626
|
-
type: "number",
|
|
627
|
-
title: "Critical Retransmit Ratio",
|
|
628
|
-
default: 0.15
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
jitter: {
|
|
633
|
-
type: "object",
|
|
634
|
-
title: "Jitter Thresholds",
|
|
635
|
-
properties: {
|
|
636
|
-
warning: { type: "number", title: "Warning Jitter (ms)", default: 100 },
|
|
637
|
-
critical: { type: "number", title: "Critical Jitter (ms)", default: 300 }
|
|
638
|
-
}
|
|
639
|
-
},
|
|
640
|
-
queueDepth: {
|
|
641
|
-
type: "object",
|
|
642
|
-
title: "Queue Depth Thresholds",
|
|
643
|
-
properties: {
|
|
644
|
-
warning: { type: "number", title: "Warning Queue Depth", default: 100 },
|
|
645
|
-
critical: { type: "number", title: "Critical Queue Depth", default: 500 }
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
},
|
|
651
|
-
required: ["udpAddress", "testAddress", "testPort"]
|
|
652
|
-
}
|
|
653
|
-
]
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
};
|
|
204
|
+
// The field definitions live in `src/shared/connection-schema.ts` and are
|
|
205
|
+
// consumed unchanged here and by the webapp RJSF form in
|
|
206
|
+
// `src/webapp/components/PluginConfigurationPanel.tsx` — there is a single
|
|
207
|
+
// schema source.
|
|
208
|
+
const connectionItemSchema = (0, connection_schema_1.buildConnectionItemSchema)();
|
|
657
209
|
plugin.schema = {
|
|
658
210
|
type: "object",
|
|
659
211
|
title: "SignalK Edge Link",
|
package/lib/instance.js
CHANGED
|
@@ -93,7 +93,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
93
93
|
pipelineServer: null,
|
|
94
94
|
heartbeatHandle: null,
|
|
95
95
|
monitoring: null,
|
|
96
|
-
networkSimulator: null,
|
|
97
96
|
configDebounceTimers: {},
|
|
98
97
|
configContentHashes: {},
|
|
99
98
|
configWatcherObjects: [],
|
|
@@ -269,7 +268,7 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
269
268
|
}
|
|
270
269
|
finally {
|
|
271
270
|
state.batchSendInFlight = false;
|
|
272
|
-
if (state.deltas.length > 0 && !state.pendingRetry) {
|
|
271
|
+
if (state.deltas.length > 0 && !state.pendingRetry && !state.stopped) {
|
|
273
272
|
setImmediate(() => {
|
|
274
273
|
flushDeltaBatch();
|
|
275
274
|
});
|
|
@@ -780,7 +779,8 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
780
779
|
failover: options.bonding.failover || {},
|
|
781
780
|
instanceId: state.instanceId,
|
|
782
781
|
notificationsEnabled: options.enableNotifications === true,
|
|
783
|
-
secretKey: options.secretKey
|
|
782
|
+
secretKey: options.secretKey,
|
|
783
|
+
stretchAsciiKey: !!options.stretchAsciiKey
|
|
784
784
|
};
|
|
785
785
|
try {
|
|
786
786
|
await v2Pipeline.initBonding(bondingConfig);
|
|
@@ -884,7 +884,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
884
884
|
}
|
|
885
885
|
state.monitoring = null;
|
|
886
886
|
}
|
|
887
|
-
state.networkSimulator = null;
|
|
888
887
|
// Stop ping monitor
|
|
889
888
|
if (state.pingMonitor) {
|
|
890
889
|
state.pingMonitor.stop();
|
package/lib/packet.js
CHANGED
|
@@ -120,11 +120,14 @@ class PacketBuilder {
|
|
|
120
120
|
/**
|
|
121
121
|
* @param {Object} [config]
|
|
122
122
|
* @param {number} [config.initialSequence=0] - Starting sequence number
|
|
123
|
+
* @param {boolean} [config.stretchAsciiKey=false] - When true, 32-char
|
|
124
|
+
* ASCII keys are stretched via PBKDF2 before use. Both ends must agree.
|
|
123
125
|
*/
|
|
124
126
|
constructor(config = {}) {
|
|
125
127
|
this._sequence = config.initialSequence ?? 0;
|
|
126
128
|
this._protocolVersion = normalizeProtocolVersion(config.protocolVersion);
|
|
127
129
|
this._secretKey = config.secretKey || null;
|
|
130
|
+
this._stretchAsciiKey = !!config.stretchAsciiKey;
|
|
128
131
|
}
|
|
129
132
|
/**
|
|
130
133
|
* Build a DATA packet
|
|
@@ -258,7 +261,7 @@ class PacketBuilder {
|
|
|
258
261
|
throw new Error("Protocol v3 control packets require a secretKey");
|
|
259
262
|
}
|
|
260
263
|
header.writeUInt32BE(payloadBuffer.length + crypto_1.CONTROL_AUTH_TAG_LENGTH, 9);
|
|
261
|
-
const authTag = (0, crypto_1.createControlPacketAuthTag)(header.subarray(0, 13), payloadBuffer, secretKey);
|
|
264
|
+
const authTag = (0, crypto_1.createControlPacketAuthTag)(header.subarray(0, 13), payloadBuffer, secretKey, { stretchAsciiKey: this._stretchAsciiKey });
|
|
262
265
|
finalPayload = Buffer.concat([payloadBuffer, authTag]);
|
|
263
266
|
}
|
|
264
267
|
else if (payloadBuffer.length > 0) {
|
|
@@ -290,13 +293,15 @@ exports.PacketBuilder = PacketBuilder;
|
|
|
290
293
|
class PacketParser {
|
|
291
294
|
constructor(config = {}) {
|
|
292
295
|
this._secretKey = config.secretKey || null;
|
|
296
|
+
this._stretchAsciiKey = !!config.stretchAsciiKey;
|
|
293
297
|
}
|
|
294
298
|
/**
|
|
295
299
|
* Parse a packet header
|
|
296
300
|
* @param {Buffer} packet - Raw packet data
|
|
297
301
|
* @param {Object} [options]
|
|
298
302
|
* @param {string} [options.secretKey] - Required for v3 control packets
|
|
299
|
-
* @param {boolean} [options.
|
|
303
|
+
* @param {boolean} [options.stretchAsciiKey] - Override the parser's
|
|
304
|
+
* constructor-time setting; both ends must agree
|
|
300
305
|
* @returns {Object} Parsed packet information
|
|
301
306
|
* @throws {Error} If packet is invalid
|
|
302
307
|
*/
|
|
@@ -353,13 +358,14 @@ class PacketParser {
|
|
|
353
358
|
}
|
|
354
359
|
const payloadData = payload.subarray(0, payload.length - crypto_1.CONTROL_AUTH_TAG_LENGTH);
|
|
355
360
|
const authTag = payload.subarray(payload.length - crypto_1.CONTROL_AUTH_TAG_LENGTH);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
throw new Error("Control packet authentication requires secretKey");
|
|
360
|
-
}
|
|
361
|
-
(0, crypto_1.verifyControlPacketAuthTag)(packet.subarray(0, 13), payloadData, authTag, secretKey);
|
|
361
|
+
const secretKey = options.secretKey || this._secretKey;
|
|
362
|
+
if (!secretKey) {
|
|
363
|
+
throw new Error("Control packet authentication requires secretKey");
|
|
362
364
|
}
|
|
365
|
+
const stretchAsciiKey = options.stretchAsciiKey ?? this._stretchAsciiKey;
|
|
366
|
+
(0, crypto_1.verifyControlPacketAuthTag)(packet.subarray(0, 13), payloadData, authTag, secretKey, {
|
|
367
|
+
stretchAsciiKey
|
|
368
|
+
});
|
|
363
369
|
payload = payloadData;
|
|
364
370
|
}
|
|
365
371
|
else {
|
|
@@ -37,12 +37,15 @@ function createPipelineV2Client(app, state, metricsApi) {
|
|
|
37
37
|
const { metrics, recordError, trackPathStats, updateBandwidthRates } = metricsApi;
|
|
38
38
|
const setStatus = app.setPluginStatus || app.setProviderStatus || (() => { });
|
|
39
39
|
const protocolVersion = state.options && state.options.protocolVersion === 3 ? 3 : 2;
|
|
40
|
+
const stretchAsciiKey = !!state.options?.stretchAsciiKey;
|
|
40
41
|
const packetBuilder = new packet_1.PacketBuilder({
|
|
41
42
|
protocolVersion,
|
|
42
|
-
secretKey: state.options?.secretKey ?? undefined
|
|
43
|
+
secretKey: state.options?.secretKey ?? undefined,
|
|
44
|
+
stretchAsciiKey
|
|
43
45
|
});
|
|
44
46
|
const packetParser = new packet_1.PacketParser({
|
|
45
|
-
secretKey: state.options?.secretKey ?? undefined
|
|
47
|
+
secretKey: state.options?.secretKey ?? undefined,
|
|
48
|
+
stretchAsciiKey
|
|
46
49
|
});
|
|
47
50
|
const clientTelemetrySource = "signalk-edge-link-client-telemetry";
|
|
48
51
|
// Reliability: extract config once to avoid repetitive deep-access chains
|
|
@@ -140,8 +143,11 @@ function createPipelineV2Client(app, state, metricsApi) {
|
|
|
140
143
|
}
|
|
141
144
|
function _effectiveRetransmitAge() {
|
|
142
145
|
let maxAge = retransmitMaxAge;
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
// Use the congestion controller's smoothed RTT (EMA) instead of the raw
|
|
147
|
+
// latest sample to avoid volatile timeout swings from single RTT spikes.
|
|
148
|
+
const smoothedRtt = congestionControl.getAvgRTT();
|
|
149
|
+
if (smoothedRtt > 0) {
|
|
150
|
+
const rttBasedAge = Math.round(smoothedRtt * retransmitRttMultiplier);
|
|
145
151
|
maxAge = Math.min(maxAge, Math.max(retransmitMinAge, rttBasedAge));
|
|
146
152
|
}
|
|
147
153
|
const ackIdleMs = Date.now() - lastAckAt;
|
|
@@ -266,7 +272,9 @@ function createPipelineV2Client(app, state, metricsApi) {
|
|
|
266
272
|
// Compress
|
|
267
273
|
const compressed = await (0, pipeline_utils_1.compressPayload)(serialized, state.options?.useMsgpack ?? false);
|
|
268
274
|
// Encrypt
|
|
269
|
-
const encrypted = (0, crypto_1.encryptBinary)(compressed, secretKey
|
|
275
|
+
const encrypted = (0, crypto_1.encryptBinary)(compressed, secretKey, {
|
|
276
|
+
stretchAsciiKey
|
|
277
|
+
});
|
|
270
278
|
// Capture sequence before building (buildDataPacket advances it)
|
|
271
279
|
const seq = packetBuilder.getCurrentSequence();
|
|
272
280
|
// Build v2 packet with header
|
|
@@ -349,10 +357,12 @@ function createPipelineV2Client(app, state, metricsApi) {
|
|
|
349
357
|
const ackedSeq = packetParser.parseACKPayload(parsed.payload);
|
|
350
358
|
const now = Date.now();
|
|
351
359
|
let rttSample = null;
|
|
352
|
-
//
|
|
360
|
+
// Only sample RTT from packets that were NOT retransmitted (Karn's algorithm).
|
|
361
|
+
// When a retransmitted packet is ACKed, the measurement is ambiguous — the ACK
|
|
362
|
+
// could be for the original or the retransmit — so we skip it entirely.
|
|
353
363
|
const entry = retransmitQueue.get(ackedSeq);
|
|
354
|
-
if (entry &&
|
|
355
|
-
rttSample = Math.max(0, now -
|
|
364
|
+
if (entry && entry.attempts === 0) {
|
|
365
|
+
rttSample = Math.max(0, now - entry.originalTimestamp);
|
|
356
366
|
}
|
|
357
367
|
if (rttSample !== null) {
|
|
358
368
|
metrics.rtt = rttSample;
|
|
@@ -376,10 +386,13 @@ function createPipelineV2Client(app, state, metricsApi) {
|
|
|
376
386
|
lastAckAt = now;
|
|
377
387
|
lastAckRinfo = rinfo ? { address: rinfo.address, port: rinfo.port } : lastAckRinfo;
|
|
378
388
|
// Update congestion control with latest network metrics.
|
|
389
|
+
// Only feed RTT when we have a fresh sample; passing -1 causes the
|
|
390
|
+
// congestion controller's >= 0 guard to skip the RTT EMA update,
|
|
391
|
+
// preventing stale values from being repeatedly folded into the average.
|
|
379
392
|
// Clamp packetLoss to [0, 1] as a defensive measure against any future
|
|
380
393
|
// changes to _calculatePacketLoss that could produce out-of-range values.
|
|
381
394
|
congestionControl.updateMetrics({
|
|
382
|
-
rtt:
|
|
395
|
+
rtt: rttSample ?? -1,
|
|
383
396
|
packetLoss: Math.min(1, Math.max(0, _calculatePacketLoss()))
|
|
384
397
|
});
|
|
385
398
|
app.debug(`ACK received: seq=${ackedSeq}, removed=${removed}, queueDepth=${retransmitQueue.getSize()}, rtt=${metrics.rtt}ms`);
|