@usex/mikrotik-mcp 2.2.0 → 2.3.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/dist/cli.js +38 -887
- package/dist/index.js +20 -882
- package/dist/ui/observability.html +2 -2
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -258,880 +258,8 @@ var logger = {
|
|
|
258
258
|
error: (m) => emit("error", m)
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
-
// src/mac-telnet/protocol.ts
|
|
262
|
-
import { createHash as createHash2, randomBytes as randomBytes2, randomInt } from "crypto";
|
|
263
|
-
import { createSocket } from "dgram";
|
|
264
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
265
|
-
import { networkInterfaces } from "os";
|
|
266
|
-
|
|
267
|
-
// src/mac-telnet/ec-srp5.ts
|
|
268
|
-
import { createHash, randomBytes } from "crypto";
|
|
269
|
-
var EC_SRP5_PUBKEY_LEN = 33;
|
|
270
|
-
var P = BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed");
|
|
271
|
-
var A = BigInt("0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144");
|
|
272
|
-
var B = BigInt("0x7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864");
|
|
273
|
-
var N = BigInt("0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed");
|
|
274
|
-
var GX = BigInt("0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a");
|
|
275
|
-
var GY = BigInt("0x5f51e65e475f794b1fe122d388b72eb36dc2b28192839e4dd6163a5d81312c14");
|
|
276
|
-
var W2M = BigInt("0x555555555555555555555555555555555555555555555555555555555552db9c");
|
|
277
|
-
var M2W = BigInt("0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2451");
|
|
278
|
-
var SQRT_M1 = modPow(2n, (P - 1n) / 4n, P);
|
|
279
|
-
var G = { x: GX, y: GY };
|
|
280
|
-
function mod(value, m) {
|
|
281
|
-
const r = value % m;
|
|
282
|
-
return r < 0n ? r + m : r;
|
|
283
|
-
}
|
|
284
|
-
function modPow(base, exp, m) {
|
|
285
|
-
let result = 1n;
|
|
286
|
-
let b = mod(base, m);
|
|
287
|
-
let e = exp;
|
|
288
|
-
while (e > 0n) {
|
|
289
|
-
if (e & 1n)
|
|
290
|
-
result = result * b % m;
|
|
291
|
-
b = b * b % m;
|
|
292
|
-
e >>= 1n;
|
|
293
|
-
}
|
|
294
|
-
return result;
|
|
295
|
-
}
|
|
296
|
-
function modInv(value) {
|
|
297
|
-
return modPow(value, P - 2n, P);
|
|
298
|
-
}
|
|
299
|
-
function modSqrt(a) {
|
|
300
|
-
const aa = mod(a, P);
|
|
301
|
-
if (aa === 0n)
|
|
302
|
-
return 0n;
|
|
303
|
-
let x = modPow(aa, (P + 3n) / 8n, P);
|
|
304
|
-
if (x * x % P === aa)
|
|
305
|
-
return x;
|
|
306
|
-
x = mod(x * SQRT_M1, P);
|
|
307
|
-
if (x * x % P === aa)
|
|
308
|
-
return x;
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
function bytesToBigIntBE(bytes) {
|
|
312
|
-
let value = 0n;
|
|
313
|
-
for (const byte of bytes)
|
|
314
|
-
value = value << 8n | BigInt(byte);
|
|
315
|
-
return value;
|
|
316
|
-
}
|
|
317
|
-
function bigIntToBytesBE(value, length) {
|
|
318
|
-
const out = new Uint8Array(length);
|
|
319
|
-
let v = value;
|
|
320
|
-
for (let i = length - 1;i >= 0; i -= 1) {
|
|
321
|
-
out[i] = Number(v & 0xffn);
|
|
322
|
-
v >>= 8n;
|
|
323
|
-
}
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
function sha256(...chunks) {
|
|
327
|
-
const hash = createHash("sha256");
|
|
328
|
-
for (const chunk of chunks)
|
|
329
|
-
hash.update(chunk);
|
|
330
|
-
return new Uint8Array(hash.digest());
|
|
331
|
-
}
|
|
332
|
-
function concatBytes(parts) {
|
|
333
|
-
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
334
|
-
const out = new Uint8Array(total);
|
|
335
|
-
let cursor = 0;
|
|
336
|
-
for (const part of parts) {
|
|
337
|
-
out.set(part, cursor);
|
|
338
|
-
cursor += part.length;
|
|
339
|
-
}
|
|
340
|
-
return out;
|
|
341
|
-
}
|
|
342
|
-
function pointAdd(p, q) {
|
|
343
|
-
if (p === null)
|
|
344
|
-
return q;
|
|
345
|
-
if (q === null)
|
|
346
|
-
return p;
|
|
347
|
-
if (p.x === q.x) {
|
|
348
|
-
if (mod(p.y + q.y, P) === 0n)
|
|
349
|
-
return null;
|
|
350
|
-
return pointDouble(p);
|
|
351
|
-
}
|
|
352
|
-
const slope = mod((q.y - p.y) * modInv(mod(q.x - p.x, P)), P);
|
|
353
|
-
const x = mod(slope * slope - p.x - q.x, P);
|
|
354
|
-
const y = mod(slope * (p.x - x) - p.y, P);
|
|
355
|
-
return { x, y };
|
|
356
|
-
}
|
|
357
|
-
function pointDouble(p) {
|
|
358
|
-
if (p === null)
|
|
359
|
-
return null;
|
|
360
|
-
if (p.y === 0n)
|
|
361
|
-
return null;
|
|
362
|
-
const slope = mod((3n * p.x * p.x + A) * modInv(mod(2n * p.y, P)), P);
|
|
363
|
-
const x = mod(slope * slope - 2n * p.x, P);
|
|
364
|
-
const y = mod(slope * (p.x - x) - p.y, P);
|
|
365
|
-
return { x, y };
|
|
366
|
-
}
|
|
367
|
-
function scalarMul(scalar, point) {
|
|
368
|
-
let result = null;
|
|
369
|
-
let addend = point;
|
|
370
|
-
let k = scalar;
|
|
371
|
-
while (k > 0n) {
|
|
372
|
-
if (k & 1n)
|
|
373
|
-
result = pointAdd(result, addend);
|
|
374
|
-
addend = pointDouble(addend);
|
|
375
|
-
k >>= 1n;
|
|
376
|
-
}
|
|
377
|
-
return result;
|
|
378
|
-
}
|
|
379
|
-
function liftX(xWeier, parity) {
|
|
380
|
-
const x = mod(xWeier, P);
|
|
381
|
-
const rhs = mod(x * x * x + A * x + B, P);
|
|
382
|
-
const y0 = modSqrt(rhs);
|
|
383
|
-
if (y0 === null)
|
|
384
|
-
return null;
|
|
385
|
-
const y = Number(y0 & 1n) === (parity & 1) ? y0 : mod(-y0, P);
|
|
386
|
-
return { x, y };
|
|
387
|
-
}
|
|
388
|
-
function encodePoint(point) {
|
|
389
|
-
if (point === null) {
|
|
390
|
-
throw new Error("Cannot encode the EC-SRP5 point at infinity.");
|
|
391
|
-
}
|
|
392
|
-
const out = new Uint8Array(EC_SRP5_PUBKEY_LEN);
|
|
393
|
-
out.set(bigIntToBytesBE(mod(point.x + W2M, P), 32), 0);
|
|
394
|
-
out[32] = Number(point.y & 1n);
|
|
395
|
-
return out;
|
|
396
|
-
}
|
|
397
|
-
function decodePoint(key) {
|
|
398
|
-
if (key.length !== EC_SRP5_PUBKEY_LEN) {
|
|
399
|
-
throw new Error(`EC-SRP5 public key must be ${EC_SRP5_PUBKEY_LEN} bytes (got ${key.length}).`);
|
|
400
|
-
}
|
|
401
|
-
const xWeier = mod(bytesToBigIntBE(key.subarray(0, 32)) + M2W, P);
|
|
402
|
-
const point = liftX(xWeier, key[32]);
|
|
403
|
-
if (point === null) {
|
|
404
|
-
throw new Error("EC-SRP5 public key X is not a valid curve point.");
|
|
405
|
-
}
|
|
406
|
-
return point;
|
|
407
|
-
}
|
|
408
|
-
function ecSrp5Keygen(privBytes) {
|
|
409
|
-
const priv = privBytes ? Uint8Array.from(privBytes) : new Uint8Array(randomBytes(32));
|
|
410
|
-
if (priv.length !== 32) {
|
|
411
|
-
throw new Error("EC-SRP5 private key seed must be 32 bytes.");
|
|
412
|
-
}
|
|
413
|
-
priv[0] = priv[0] & 248;
|
|
414
|
-
priv[31] = priv[31] & 127;
|
|
415
|
-
priv[31] = priv[31] | 64;
|
|
416
|
-
const privateKey = bytesToBigIntBE(priv);
|
|
417
|
-
return { privateKey, publicKey: encodePoint(scalarMul(privateKey, G)) };
|
|
418
|
-
}
|
|
419
|
-
function ecSrp5Id(username, password, salt) {
|
|
420
|
-
const inner = sha256(new TextEncoder().encode(`${username}:${password}`));
|
|
421
|
-
return sha256(salt, inner);
|
|
422
|
-
}
|
|
423
|
-
function redp1(montgomeryX, parity) {
|
|
424
|
-
let seed = bytesToBigIntBE(sha256(montgomeryX));
|
|
425
|
-
for (;; ) {
|
|
426
|
-
const candidate = sha256(bigIntToBytesBE(seed, 32));
|
|
427
|
-
const xWeier = mod(bytesToBigIntBE(candidate) + M2W, P);
|
|
428
|
-
const point = liftX(xWeier, parity);
|
|
429
|
-
if (point !== null)
|
|
430
|
-
return point;
|
|
431
|
-
seed = mod(seed + 1n, 1n << 256n);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
function ecSrp5ClientShared(privateKey, serverKey, clientKey, validator) {
|
|
435
|
-
const serverPoint = decodePoint(serverKey);
|
|
436
|
-
const v = bytesToBigIntBE(validator);
|
|
437
|
-
const vG = scalarMul(v, G);
|
|
438
|
-
if (vG === null) {
|
|
439
|
-
throw new Error("EC-SRP5 validator produced the point at infinity.");
|
|
440
|
-
}
|
|
441
|
-
const gamma = redp1(bigIntToBytesBE(mod(vG.x + W2M, P), 32), 1);
|
|
442
|
-
const wB = pointAdd(serverPoint, gamma);
|
|
443
|
-
const j = sha256(clientKey.subarray(0, 32), serverKey.subarray(0, 32));
|
|
444
|
-
const scalar = mod(v * bytesToBigIntBE(j) + privateKey, N);
|
|
445
|
-
const pt = scalarMul(scalar, wB);
|
|
446
|
-
if (pt === null) {
|
|
447
|
-
throw new Error("EC-SRP5 shared point computed as infinity.");
|
|
448
|
-
}
|
|
449
|
-
const z2 = bigIntToBytesBE(mod(pt.x + W2M, P), 32);
|
|
450
|
-
return { j, z: z2 };
|
|
451
|
-
}
|
|
452
|
-
function ecSrp5ClientProof(privateKey, serverKey, clientKey, validator) {
|
|
453
|
-
const { j, z: z2 } = ecSrp5ClientShared(privateKey, serverKey, clientKey, validator);
|
|
454
|
-
return sha256(j, z2);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// src/mac-telnet/mtwei.ts
|
|
458
|
-
function mtweiOfferValue(username, publicKey) {
|
|
459
|
-
return concatBytes([new TextEncoder().encode(username), Uint8Array.of(0), publicKey]);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// src/mac-telnet/protocol.ts
|
|
463
|
-
var MAC_TELNET_PORT = 20561;
|
|
464
|
-
var MAC_TELNET_HEADER_LEN = 22;
|
|
465
|
-
var MAC_TELNET_CONTROL_HEADER_LEN = 9;
|
|
466
|
-
var MAC_TELNET_CONTROL_MAGIC = Uint8Array.of(86, 52, 18, 255);
|
|
467
|
-
var MAC_TELNET_CLIENT_TYPE = Uint8Array.of(0, 21);
|
|
468
|
-
var MAC_TELNET_RETRANSMIT_SCHEDULE_MS = [
|
|
469
|
-
15,
|
|
470
|
-
20,
|
|
471
|
-
30,
|
|
472
|
-
50,
|
|
473
|
-
90,
|
|
474
|
-
170,
|
|
475
|
-
330,
|
|
476
|
-
660,
|
|
477
|
-
1000
|
|
478
|
-
];
|
|
479
|
-
var MAC_TELNET_KEEPALIVE_IDLE_MS = 8000;
|
|
480
|
-
var MacTelnetPacketType = {
|
|
481
|
-
sessionStart: 0,
|
|
482
|
-
data: 1,
|
|
483
|
-
ack: 2,
|
|
484
|
-
ping: 4,
|
|
485
|
-
pong: 5,
|
|
486
|
-
end: 255
|
|
487
|
-
};
|
|
488
|
-
var MacTelnetControlType = {
|
|
489
|
-
beginAuth: 0,
|
|
490
|
-
passwordSalt: 1,
|
|
491
|
-
password: 2,
|
|
492
|
-
username: 3,
|
|
493
|
-
terminalType: 4,
|
|
494
|
-
terminalWidth: 5,
|
|
495
|
-
terminalHeight: 6,
|
|
496
|
-
packetError: 7,
|
|
497
|
-
endAuth: 9
|
|
498
|
-
};
|
|
499
|
-
function parseMac(value) {
|
|
500
|
-
const parts = value.trim().split(/[:\-.]/);
|
|
501
|
-
if (parts.length !== 6) {
|
|
502
|
-
throw new Error(`"${value}" is not a 6-octet MAC address. Provide a MAC like aa:bb:cc:dd:ee:ff.`);
|
|
503
|
-
}
|
|
504
|
-
const octets = new Uint8Array(6);
|
|
505
|
-
for (let index = 0;index < 6; index += 1) {
|
|
506
|
-
const octet = Number.parseInt(parts[index], 16);
|
|
507
|
-
if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
|
|
508
|
-
throw new Error(`"${value}" has an invalid octet "${parts[index]}". Each octet must be a two-digit hex value (00\u2013ff).`);
|
|
509
|
-
}
|
|
510
|
-
octets[index] = octet;
|
|
511
|
-
}
|
|
512
|
-
return octets;
|
|
513
|
-
}
|
|
514
|
-
function macEquals(a, b) {
|
|
515
|
-
if (a.length !== b.length)
|
|
516
|
-
return false;
|
|
517
|
-
for (let index = 0;index < a.length; index += 1) {
|
|
518
|
-
if (a[index] !== b[index])
|
|
519
|
-
return false;
|
|
520
|
-
}
|
|
521
|
-
return true;
|
|
522
|
-
}
|
|
523
|
-
function encodeHeader(options) {
|
|
524
|
-
const data = new Uint8Array(MAC_TELNET_HEADER_LEN);
|
|
525
|
-
const view = new DataView(data.buffer);
|
|
526
|
-
data[0] = 1;
|
|
527
|
-
data[1] = options.type;
|
|
528
|
-
data.set(options.sourceMac, 2);
|
|
529
|
-
data.set(options.destinationMac, 8);
|
|
530
|
-
const sessionKeyOffset = options.fromServer ? 16 : 14;
|
|
531
|
-
const clientTypeOffset = options.fromServer ? 14 : 16;
|
|
532
|
-
view.setUint16(sessionKeyOffset, options.sessionKey & 65535, false);
|
|
533
|
-
data.set(MAC_TELNET_CLIENT_TYPE, clientTypeOffset);
|
|
534
|
-
view.setUint32(18, options.counter >>> 0, false);
|
|
535
|
-
return data;
|
|
536
|
-
}
|
|
537
|
-
function decodeHeader(bytes, options = {}) {
|
|
538
|
-
if (bytes.length < MAC_TELNET_HEADER_LEN) {
|
|
539
|
-
throw new Error(`MAC-Telnet packet is too short (${bytes.length} bytes, need at least ${MAC_TELNET_HEADER_LEN}).`);
|
|
540
|
-
}
|
|
541
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
542
|
-
const fromServer = options.fromServer ?? true;
|
|
543
|
-
const sessionKeyOffset = fromServer ? 16 : 14;
|
|
544
|
-
return {
|
|
545
|
-
version: bytes[0],
|
|
546
|
-
type: bytes[1],
|
|
547
|
-
sourceMac: bytes.slice(2, 8),
|
|
548
|
-
destinationMac: bytes.slice(8, 14),
|
|
549
|
-
sessionKey: view.getUint16(sessionKeyOffset, false),
|
|
550
|
-
counter: view.getUint32(18, false)
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
function encodeControlBlock(type, value = new Uint8Array(0)) {
|
|
554
|
-
const block = new Uint8Array(MAC_TELNET_CONTROL_HEADER_LEN + value.length);
|
|
555
|
-
block.set(MAC_TELNET_CONTROL_MAGIC, 0);
|
|
556
|
-
block[4] = type;
|
|
557
|
-
new DataView(block.buffer).setUint32(5, value.length >>> 0, false);
|
|
558
|
-
block.set(value, MAC_TELNET_CONTROL_HEADER_LEN);
|
|
559
|
-
return block;
|
|
560
|
-
}
|
|
561
|
-
function parseControlBlocks(payload) {
|
|
562
|
-
const blocks = [];
|
|
563
|
-
let offset = 0;
|
|
564
|
-
while (offset < payload.length) {
|
|
565
|
-
const remaining = payload.length - offset;
|
|
566
|
-
const hasMagic = remaining >= MAC_TELNET_CONTROL_HEADER_LEN && payload[offset] === MAC_TELNET_CONTROL_MAGIC[0] && payload[offset + 1] === MAC_TELNET_CONTROL_MAGIC[1] && payload[offset + 2] === MAC_TELNET_CONTROL_MAGIC[2] && payload[offset + 3] === MAC_TELNET_CONTROL_MAGIC[3];
|
|
567
|
-
if (!hasMagic) {
|
|
568
|
-
blocks.push({ type: "plaindata", value: payload.slice(offset) });
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
const type = payload[offset + 4];
|
|
572
|
-
const length = new DataView(payload.buffer, payload.byteOffset + offset + 5, 4).getUint32(0, false);
|
|
573
|
-
const valueStart = offset + MAC_TELNET_CONTROL_HEADER_LEN;
|
|
574
|
-
const valueEnd = valueStart + length;
|
|
575
|
-
if (valueEnd > payload.length) {
|
|
576
|
-
throw new Error(`MAC-Telnet control block claims ${length} bytes but only ${payload.length - valueStart} remain.`);
|
|
577
|
-
}
|
|
578
|
-
blocks.push({ type, value: payload.slice(valueStart, valueEnd) });
|
|
579
|
-
offset = valueEnd;
|
|
580
|
-
}
|
|
581
|
-
return blocks;
|
|
582
|
-
}
|
|
583
|
-
function macTelnetPasswordHash(password, salt) {
|
|
584
|
-
const digest = createHash2("md5").update(Buffer.from([0])).update(Buffer.from(password, "utf8")).update(salt).digest();
|
|
585
|
-
const out = new Uint8Array(17);
|
|
586
|
-
out[0] = 0;
|
|
587
|
-
out.set(digest, 1);
|
|
588
|
-
return out;
|
|
589
|
-
}
|
|
590
|
-
function encodeTerminalDimension(value) {
|
|
591
|
-
const out = new Uint8Array(2);
|
|
592
|
-
new DataView(out.buffer).setUint16(0, value & 65535, true);
|
|
593
|
-
return out;
|
|
594
|
-
}
|
|
595
|
-
function concatBytes2(parts) {
|
|
596
|
-
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
597
|
-
const out = new Uint8Array(total);
|
|
598
|
-
let cursor = 0;
|
|
599
|
-
for (const part of parts) {
|
|
600
|
-
out.set(part, cursor);
|
|
601
|
-
cursor += part.length;
|
|
602
|
-
}
|
|
603
|
-
return out;
|
|
604
|
-
}
|
|
605
|
-
function buildPacket(options) {
|
|
606
|
-
const header = encodeHeader({
|
|
607
|
-
type: options.type,
|
|
608
|
-
sourceMac: options.sourceMac,
|
|
609
|
-
destinationMac: options.destinationMac,
|
|
610
|
-
sessionKey: options.sessionKey,
|
|
611
|
-
counter: options.counter
|
|
612
|
-
});
|
|
613
|
-
if (!options.payload || options.payload.length === 0)
|
|
614
|
-
return header;
|
|
615
|
-
return concatBytes2([header, options.payload]);
|
|
616
|
-
}
|
|
617
|
-
function createUdpMacTelnetTransport(options) {
|
|
618
|
-
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
619
|
-
let handler;
|
|
620
|
-
socket.on("message", (message) => {
|
|
621
|
-
handler?.(new Uint8Array(message));
|
|
622
|
-
});
|
|
623
|
-
const readyPromise = new Promise((resolve, reject) => {
|
|
624
|
-
socket.once("error", reject);
|
|
625
|
-
socket.bind(0, () => {
|
|
626
|
-
if (options.broadcast) {
|
|
627
|
-
try {
|
|
628
|
-
socket.setBroadcast(true);
|
|
629
|
-
} catch {}
|
|
630
|
-
}
|
|
631
|
-
resolve();
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
return {
|
|
635
|
-
send(bytes) {
|
|
636
|
-
socket.send(bytes, options.port, options.host);
|
|
637
|
-
},
|
|
638
|
-
close() {
|
|
639
|
-
try {
|
|
640
|
-
socket.close();
|
|
641
|
-
} catch {}
|
|
642
|
-
},
|
|
643
|
-
onMessage(next) {
|
|
644
|
-
handler = next;
|
|
645
|
-
},
|
|
646
|
-
ready() {
|
|
647
|
-
return readyPromise;
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
function isZeroMac(mac) {
|
|
652
|
-
return mac.every((octet) => octet === 0);
|
|
653
|
-
}
|
|
654
|
-
function tryParseMac(value) {
|
|
655
|
-
if (!value)
|
|
656
|
-
return;
|
|
657
|
-
try {
|
|
658
|
-
return parseMac(value);
|
|
659
|
-
} catch {
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
function hardwareMacOf(name) {
|
|
664
|
-
try {
|
|
665
|
-
const sys = readFileSync2(`/sys/class/net/${name}/address`, "utf8").trim();
|
|
666
|
-
const mac = tryParseMac(sys);
|
|
667
|
-
if (mac && !isZeroMac(mac))
|
|
668
|
-
return mac;
|
|
669
|
-
} catch {}
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
function ipv4ToInt(addr) {
|
|
673
|
-
const parts = addr.split(".");
|
|
674
|
-
if (parts.length !== 4)
|
|
675
|
-
return;
|
|
676
|
-
let value = 0;
|
|
677
|
-
for (const part of parts) {
|
|
678
|
-
const octet = Number(part);
|
|
679
|
-
if (!Number.isInteger(octet) || octet < 0 || octet > 255)
|
|
680
|
-
return;
|
|
681
|
-
value = value << 8 | octet;
|
|
682
|
-
}
|
|
683
|
-
return value >>> 0;
|
|
684
|
-
}
|
|
685
|
-
function intToIpv4(value) {
|
|
686
|
-
return [24, 16, 8, 0].map((shift) => value >>> shift & 255).join(".");
|
|
687
|
-
}
|
|
688
|
-
function directedBroadcast(address, netmask) {
|
|
689
|
-
const addr = ipv4ToInt(address);
|
|
690
|
-
const mask = ipv4ToInt(netmask);
|
|
691
|
-
if (addr === undefined || mask === undefined)
|
|
692
|
-
return;
|
|
693
|
-
return intToIpv4((addr & mask | ~mask >>> 0) >>> 0);
|
|
694
|
-
}
|
|
695
|
-
function listBroadcastInterfaces() {
|
|
696
|
-
const out = [];
|
|
697
|
-
for (const [name, infos] of Object.entries(networkInterfaces())) {
|
|
698
|
-
for (const info of infos ?? []) {
|
|
699
|
-
if (info.family !== "IPv4" || info.internal)
|
|
700
|
-
continue;
|
|
701
|
-
let mac = tryParseMac(info.mac);
|
|
702
|
-
if (!mac || isZeroMac(mac))
|
|
703
|
-
mac = hardwareMacOf(name);
|
|
704
|
-
if (!mac || isZeroMac(mac))
|
|
705
|
-
continue;
|
|
706
|
-
const broadcast = directedBroadcast(info.address, info.netmask);
|
|
707
|
-
if (!broadcast)
|
|
708
|
-
continue;
|
|
709
|
-
out.push({ name, address: info.address, broadcast, mac });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
return out;
|
|
713
|
-
}
|
|
714
|
-
async function egressAddressFor(host, port) {
|
|
715
|
-
return await new Promise((resolve) => {
|
|
716
|
-
const probe = createSocket("udp4");
|
|
717
|
-
const done = (addr) => {
|
|
718
|
-
try {
|
|
719
|
-
probe.close();
|
|
720
|
-
} catch {}
|
|
721
|
-
resolve(addr);
|
|
722
|
-
};
|
|
723
|
-
probe.on("error", () => done(undefined));
|
|
724
|
-
try {
|
|
725
|
-
probe.connect(port, host, () => {
|
|
726
|
-
try {
|
|
727
|
-
done(probe.address().address);
|
|
728
|
-
} catch {
|
|
729
|
-
done(undefined);
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
} catch {
|
|
733
|
-
done(undefined);
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
async function resolveEgressMac(host, port) {
|
|
738
|
-
const localAddr = await egressAddressFor(host, port);
|
|
739
|
-
if (!localAddr)
|
|
740
|
-
return;
|
|
741
|
-
for (const ifc of listBroadcastInterfaces()) {
|
|
742
|
-
if (ifc.address === localAddr)
|
|
743
|
-
return ifc.mac;
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
async function discoverMacTelnetRoute(opts) {
|
|
748
|
-
const interfaces = listBroadcastInterfaces();
|
|
749
|
-
if (interfaces.length === 0)
|
|
750
|
-
return;
|
|
751
|
-
const sessionKey = opts.sessionKey ?? randomInt(65536);
|
|
752
|
-
return await new Promise((resolve) => {
|
|
753
|
-
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
754
|
-
let settled = false;
|
|
755
|
-
const timers = [];
|
|
756
|
-
const finish = (route) => {
|
|
757
|
-
if (settled)
|
|
758
|
-
return;
|
|
759
|
-
settled = true;
|
|
760
|
-
for (const t of timers)
|
|
761
|
-
clearTimeout(t);
|
|
762
|
-
try {
|
|
763
|
-
socket.close();
|
|
764
|
-
} catch {}
|
|
765
|
-
resolve(route);
|
|
766
|
-
};
|
|
767
|
-
socket.on("error", () => finish(undefined));
|
|
768
|
-
socket.on("message", (message) => {
|
|
769
|
-
let header;
|
|
770
|
-
try {
|
|
771
|
-
header = decodeHeader(new Uint8Array(message), { fromServer: true });
|
|
772
|
-
} catch {
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
if (header.version !== 1 || header.sessionKey !== sessionKey)
|
|
776
|
-
return;
|
|
777
|
-
if (header.type !== MacTelnetPacketType.ack && header.type !== MacTelnetPacketType.data) {
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (!macEquals(header.sourceMac, opts.destinationMac))
|
|
781
|
-
return;
|
|
782
|
-
const winner = interfaces.find((ifc) => macEquals(ifc.mac, header.destinationMac));
|
|
783
|
-
if (winner)
|
|
784
|
-
finish({ sourceMac: winner.mac, host: winner.broadcast });
|
|
785
|
-
});
|
|
786
|
-
const spray = () => {
|
|
787
|
-
for (const ifc of interfaces) {
|
|
788
|
-
const packet = buildPacket({
|
|
789
|
-
type: MacTelnetPacketType.sessionStart,
|
|
790
|
-
sourceMac: ifc.mac,
|
|
791
|
-
destinationMac: opts.destinationMac,
|
|
792
|
-
sessionKey,
|
|
793
|
-
counter: 0
|
|
794
|
-
});
|
|
795
|
-
try {
|
|
796
|
-
socket.send(packet, opts.port, ifc.broadcast);
|
|
797
|
-
} catch {}
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
socket.bind(0, () => {
|
|
801
|
-
try {
|
|
802
|
-
socket.setBroadcast(true);
|
|
803
|
-
} catch {}
|
|
804
|
-
spray();
|
|
805
|
-
timers.push(setTimeout(spray, 250));
|
|
806
|
-
timers.push(setTimeout(spray, 700));
|
|
807
|
-
timers.push(setTimeout(() => finish(undefined), opts.timeoutMs));
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
var DEFAULT_MAC_TELNET_BROADCAST = "255.255.255.255";
|
|
812
|
-
function randomLocalMac() {
|
|
813
|
-
const octets = new Uint8Array(randomBytes2(6));
|
|
814
|
-
octets[0] = octets[0] & 254 | 2;
|
|
815
|
-
return octets;
|
|
816
|
-
}
|
|
817
|
-
function isBroadcastHost(host) {
|
|
818
|
-
return host === DEFAULT_MAC_TELNET_BROADCAST || host.endsWith(".255");
|
|
819
|
-
}
|
|
820
|
-
async function resolveMacTelnetRoute(config) {
|
|
821
|
-
if (config.explicitSourceMac) {
|
|
822
|
-
return { sourceMac: config.explicitSourceMac, host: config.host };
|
|
823
|
-
}
|
|
824
|
-
if (config.host === DEFAULT_MAC_TELNET_BROADCAST) {
|
|
825
|
-
const route = await discoverMacTelnetRoute({
|
|
826
|
-
destinationMac: config.destinationMac,
|
|
827
|
-
port: config.port,
|
|
828
|
-
timeoutMs: Math.min(config.timeoutMs, 5000)
|
|
829
|
-
});
|
|
830
|
-
if (route)
|
|
831
|
-
return route;
|
|
832
|
-
}
|
|
833
|
-
const sourceMac = await resolveEgressMac(config.host, config.port) ?? randomLocalMac();
|
|
834
|
-
return { sourceMac, host: config.host };
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
class MacTelnetSession {
|
|
838
|
-
options;
|
|
839
|
-
sessionKey;
|
|
840
|
-
state = "init";
|
|
841
|
-
outCounter = 0;
|
|
842
|
-
lastInCounter = null;
|
|
843
|
-
mtweiKeypair;
|
|
844
|
-
lastAckCounter = 0;
|
|
845
|
-
maxOutAck = -1;
|
|
846
|
-
pending;
|
|
847
|
-
activitySinceTick = false;
|
|
848
|
-
idleMs = 0;
|
|
849
|
-
lastTickMs;
|
|
850
|
-
constructor(options) {
|
|
851
|
-
this.options = options;
|
|
852
|
-
this.sessionKey = options.sessionKey ?? randomInt(65536);
|
|
853
|
-
}
|
|
854
|
-
get key() {
|
|
855
|
-
return this.sessionKey;
|
|
856
|
-
}
|
|
857
|
-
start() {
|
|
858
|
-
if (this.state !== "init")
|
|
859
|
-
return;
|
|
860
|
-
const bytes = this.sendPacket(MacTelnetPacketType.sessionStart);
|
|
861
|
-
this.pending = { bytes, ackTarget: 0, attempts: 0, elapsedMs: 0 };
|
|
862
|
-
this.state = "session-start-sent";
|
|
863
|
-
}
|
|
864
|
-
handlePacket(bytes) {
|
|
865
|
-
if (this.state === "closed")
|
|
866
|
-
return;
|
|
867
|
-
let header;
|
|
868
|
-
try {
|
|
869
|
-
header = decodeHeader(bytes, { fromServer: true });
|
|
870
|
-
} catch {
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
switch (header.type) {
|
|
874
|
-
case MacTelnetPacketType.ack:
|
|
875
|
-
if (this.matchesSession(header))
|
|
876
|
-
this.onAck(header);
|
|
877
|
-
return;
|
|
878
|
-
case MacTelnetPacketType.data:
|
|
879
|
-
if (this.matchesSession(header))
|
|
880
|
-
this.onDataPacket(header, bytes);
|
|
881
|
-
return;
|
|
882
|
-
case MacTelnetPacketType.end:
|
|
883
|
-
if (this.matchesSession(header))
|
|
884
|
-
this.onEnd(header);
|
|
885
|
-
return;
|
|
886
|
-
case MacTelnetPacketType.ping:
|
|
887
|
-
if (header.version === 1 && macEquals(header.destinationMac, this.options.sourceMac)) {
|
|
888
|
-
this.onPing(header);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
matchesSession(header) {
|
|
893
|
-
return header.version === 1 && header.sessionKey === this.sessionKey && macEquals(header.sourceMac, this.options.destinationMac) && macEquals(header.destinationMac, this.options.sourceMac);
|
|
894
|
-
}
|
|
895
|
-
onPing(header) {
|
|
896
|
-
this.activitySinceTick = true;
|
|
897
|
-
this.emit(encodeHeader({
|
|
898
|
-
type: MacTelnetPacketType.pong,
|
|
899
|
-
sourceMac: this.options.sourceMac,
|
|
900
|
-
destinationMac: this.options.destinationMac,
|
|
901
|
-
sessionKey: header.sessionKey,
|
|
902
|
-
counter: header.counter
|
|
903
|
-
}));
|
|
904
|
-
}
|
|
905
|
-
onAck(header) {
|
|
906
|
-
this.activitySinceTick = true;
|
|
907
|
-
this.maxOutAck = Math.max(this.maxOutAck, header.counter);
|
|
908
|
-
if (this.pending && this.maxOutAck >= this.pending.ackTarget) {
|
|
909
|
-
this.pending = undefined;
|
|
910
|
-
}
|
|
911
|
-
if (this.state !== "session-start-sent")
|
|
912
|
-
return;
|
|
913
|
-
const blocks = [encodeControlBlock(MacTelnetControlType.beginAuth)];
|
|
914
|
-
if (this.options.offerMtwei !== false) {
|
|
915
|
-
this.mtweiKeypair = ecSrp5Keygen();
|
|
916
|
-
blocks.push(encodeControlBlock(MacTelnetControlType.passwordSalt, mtweiOfferValue(this.identityUsername(), this.mtweiKeypair.publicKey)));
|
|
917
|
-
}
|
|
918
|
-
this.sendData(blocks);
|
|
919
|
-
this.state = "auth-begin-sent";
|
|
920
|
-
}
|
|
921
|
-
identityUsername() {
|
|
922
|
-
return this.options.username.split("+", 1)[0] ?? this.options.username;
|
|
923
|
-
}
|
|
924
|
-
onDataPacket(header, bytes) {
|
|
925
|
-
this.activitySinceTick = true;
|
|
926
|
-
const payload = bytes.subarray(MAC_TELNET_HEADER_LEN);
|
|
927
|
-
this.acknowledge(header.counter, payload.length);
|
|
928
|
-
if (!this.acceptCounter(header.counter))
|
|
929
|
-
return;
|
|
930
|
-
let blocks;
|
|
931
|
-
try {
|
|
932
|
-
blocks = parseControlBlocks(payload);
|
|
933
|
-
} catch (error) {
|
|
934
|
-
this.fail(error);
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
for (const block of blocks)
|
|
938
|
-
this.handleControlBlock(block);
|
|
939
|
-
}
|
|
940
|
-
handleControlBlock(block) {
|
|
941
|
-
switch (block.type) {
|
|
942
|
-
case MacTelnetControlType.passwordSalt:
|
|
943
|
-
this.handlePasswordSalt(block.value);
|
|
944
|
-
return;
|
|
945
|
-
case MacTelnetControlType.endAuth:
|
|
946
|
-
if (this.state === "auth-sent") {
|
|
947
|
-
this.state = "auth-complete";
|
|
948
|
-
}
|
|
949
|
-
return;
|
|
950
|
-
case "plaindata":
|
|
951
|
-
this.handleTerminalData(block.value);
|
|
952
|
-
return;
|
|
953
|
-
case MacTelnetControlType.packetError:
|
|
954
|
-
this.fail(new Error(`The device reported a MAC-Telnet error: ${new TextDecoder().decode(block.value)}. ` + "Check the credentials and that MAC-Telnet is enabled on the device."));
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
handleTerminalData(data) {
|
|
958
|
-
if (this.state === "auth-complete") {
|
|
959
|
-
if (/login failed/i.test(new TextDecoder().decode(data))) {
|
|
960
|
-
this.fail(new Error("MAC-Telnet login failed: incorrect username or password."));
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
this.state = "ready";
|
|
964
|
-
this.options.onReady?.();
|
|
965
|
-
if (data.length > 0)
|
|
966
|
-
this.options.onData?.(data);
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
if (this.state === "ready" && data.length > 0)
|
|
970
|
-
this.options.onData?.(data);
|
|
971
|
-
}
|
|
972
|
-
handlePasswordSalt(salt) {
|
|
973
|
-
if (this.state !== "auth-begin-sent")
|
|
974
|
-
return;
|
|
975
|
-
let passwordValue;
|
|
976
|
-
if (salt.length === 16) {
|
|
977
|
-
passwordValue = macTelnetPasswordHash(this.options.password, salt);
|
|
978
|
-
} else if (salt.length === EC_SRP5_PUBKEY_LEN + 16) {
|
|
979
|
-
if (!this.mtweiKeypair) {
|
|
980
|
-
this.fail(new Error("The device requires MTWEI but this session did not offer it. " + "Leave MTWEI enabled (the default) so the client advertises a public key."));
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
const serverKey = salt.subarray(0, EC_SRP5_PUBKEY_LEN);
|
|
984
|
-
const mtweiSalt = salt.subarray(EC_SRP5_PUBKEY_LEN);
|
|
985
|
-
const validator = ecSrp5Id(this.identityUsername(), this.options.password, mtweiSalt);
|
|
986
|
-
passwordValue = ecSrp5ClientProof(this.mtweiKeypair.privateKey, serverKey, this.mtweiKeypair.publicKey, validator);
|
|
987
|
-
} else {
|
|
988
|
-
this.fail(new Error(`The device offered an unsupported MAC-Telnet salt length (${salt.length}). ` + "Expected 16 (MD5) or 49 (MTWEI); confirm the device and RouterOS version."));
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
const username = new TextEncoder().encode(this.options.username);
|
|
992
|
-
const terminalType = new TextEncoder().encode(this.options.terminalType ?? "vt102");
|
|
993
|
-
const blocks = [
|
|
994
|
-
encodeControlBlock(MacTelnetControlType.password, passwordValue),
|
|
995
|
-
encodeControlBlock(MacTelnetControlType.username, username),
|
|
996
|
-
encodeControlBlock(MacTelnetControlType.terminalType, terminalType),
|
|
997
|
-
encodeControlBlock(MacTelnetControlType.terminalWidth, encodeTerminalDimension(this.options.terminalWidth ?? 80)),
|
|
998
|
-
encodeControlBlock(MacTelnetControlType.terminalHeight, encodeTerminalDimension(this.options.terminalHeight ?? 24))
|
|
999
|
-
];
|
|
1000
|
-
this.sendData(blocks);
|
|
1001
|
-
this.state = "auth-sent";
|
|
1002
|
-
}
|
|
1003
|
-
onEnd(header) {
|
|
1004
|
-
this.emit(encodeHeader({
|
|
1005
|
-
type: MacTelnetPacketType.end,
|
|
1006
|
-
sourceMac: this.options.sourceMac,
|
|
1007
|
-
destinationMac: this.options.destinationMac,
|
|
1008
|
-
sessionKey: header.sessionKey,
|
|
1009
|
-
counter: 0
|
|
1010
|
-
}));
|
|
1011
|
-
if (this.state === "auth-complete") {
|
|
1012
|
-
this.fail(new Error("MAC-Telnet login failed: the device closed the session after authentication. " + "Check the credentials configured for this device."));
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
this.close();
|
|
1016
|
-
}
|
|
1017
|
-
sendInput(bytes) {
|
|
1018
|
-
if (this.state !== "ready") {
|
|
1019
|
-
throw new Error("Cannot send input before the MAC-Telnet session is ready.");
|
|
1020
|
-
}
|
|
1021
|
-
this.sendData([bytes]);
|
|
1022
|
-
}
|
|
1023
|
-
end() {
|
|
1024
|
-
if (this.state === "closed")
|
|
1025
|
-
return;
|
|
1026
|
-
this.emit(encodeHeader({
|
|
1027
|
-
type: MacTelnetPacketType.end,
|
|
1028
|
-
sourceMac: this.options.sourceMac,
|
|
1029
|
-
destinationMac: this.options.destinationMac,
|
|
1030
|
-
sessionKey: this.sessionKey,
|
|
1031
|
-
counter: this.outCounter
|
|
1032
|
-
}));
|
|
1033
|
-
this.close();
|
|
1034
|
-
}
|
|
1035
|
-
close(error) {
|
|
1036
|
-
if (this.state === "closed")
|
|
1037
|
-
return;
|
|
1038
|
-
this.state = "closed";
|
|
1039
|
-
this.pending = undefined;
|
|
1040
|
-
this.options.sink.close();
|
|
1041
|
-
this.options.onClose?.(error);
|
|
1042
|
-
}
|
|
1043
|
-
fail(error) {
|
|
1044
|
-
this.close(error);
|
|
1045
|
-
}
|
|
1046
|
-
emit(bytes) {
|
|
1047
|
-
try {
|
|
1048
|
-
this.options.sink.send(bytes);
|
|
1049
|
-
} catch {}
|
|
1050
|
-
}
|
|
1051
|
-
acceptCounter(counter) {
|
|
1052
|
-
if (this.lastInCounter === null) {
|
|
1053
|
-
this.lastInCounter = counter;
|
|
1054
|
-
return true;
|
|
1055
|
-
}
|
|
1056
|
-
if (counter > this.lastInCounter) {
|
|
1057
|
-
this.lastInCounter = counter;
|
|
1058
|
-
return true;
|
|
1059
|
-
}
|
|
1060
|
-
return false;
|
|
1061
|
-
}
|
|
1062
|
-
acknowledge(counter, payloadLength) {
|
|
1063
|
-
this.lastAckCounter = counter + payloadLength >>> 0;
|
|
1064
|
-
this.activitySinceTick = true;
|
|
1065
|
-
this.emit(encodeHeader({
|
|
1066
|
-
type: MacTelnetPacketType.ack,
|
|
1067
|
-
sourceMac: this.options.sourceMac,
|
|
1068
|
-
destinationMac: this.options.destinationMac,
|
|
1069
|
-
sessionKey: this.sessionKey,
|
|
1070
|
-
counter: this.lastAckCounter
|
|
1071
|
-
}));
|
|
1072
|
-
}
|
|
1073
|
-
sendPacket(type, payload) {
|
|
1074
|
-
const bytes = buildPacket({
|
|
1075
|
-
type,
|
|
1076
|
-
sourceMac: this.options.sourceMac,
|
|
1077
|
-
destinationMac: this.options.destinationMac,
|
|
1078
|
-
sessionKey: this.sessionKey,
|
|
1079
|
-
counter: this.outCounter,
|
|
1080
|
-
payload
|
|
1081
|
-
});
|
|
1082
|
-
this.activitySinceTick = true;
|
|
1083
|
-
this.emit(bytes);
|
|
1084
|
-
return bytes;
|
|
1085
|
-
}
|
|
1086
|
-
sendData(parts) {
|
|
1087
|
-
const payload = concatBytes2(parts);
|
|
1088
|
-
const bytes = this.sendPacket(MacTelnetPacketType.data, payload);
|
|
1089
|
-
this.outCounter = this.outCounter + payload.length >>> 0;
|
|
1090
|
-
this.pending = {
|
|
1091
|
-
bytes,
|
|
1092
|
-
ackTarget: this.outCounter,
|
|
1093
|
-
attempts: 0,
|
|
1094
|
-
elapsedMs: 0
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
tick(nowMs) {
|
|
1098
|
-
if (this.state === "closed")
|
|
1099
|
-
return;
|
|
1100
|
-
const delta = this.lastTickMs === undefined ? 0 : Math.max(0, nowMs - this.lastTickMs);
|
|
1101
|
-
this.lastTickMs = nowMs;
|
|
1102
|
-
if (this.activitySinceTick) {
|
|
1103
|
-
this.idleMs = 0;
|
|
1104
|
-
this.activitySinceTick = false;
|
|
1105
|
-
} else {
|
|
1106
|
-
this.idleMs += delta;
|
|
1107
|
-
}
|
|
1108
|
-
if (this.idleMs >= MAC_TELNET_KEEPALIVE_IDLE_MS) {
|
|
1109
|
-
this.sendKeepalive();
|
|
1110
|
-
this.idleMs = 0;
|
|
1111
|
-
}
|
|
1112
|
-
const pending = this.pending;
|
|
1113
|
-
if (pending && pending.attempts < MAC_TELNET_RETRANSMIT_SCHEDULE_MS.length) {
|
|
1114
|
-
pending.elapsedMs += delta;
|
|
1115
|
-
const wait = MAC_TELNET_RETRANSMIT_SCHEDULE_MS[pending.attempts];
|
|
1116
|
-
if (pending.elapsedMs >= wait) {
|
|
1117
|
-
this.emit(pending.bytes);
|
|
1118
|
-
pending.attempts += 1;
|
|
1119
|
-
pending.elapsedMs = 0;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
sendKeepalive() {
|
|
1124
|
-
this.emit(encodeHeader({
|
|
1125
|
-
type: MacTelnetPacketType.ack,
|
|
1126
|
-
sourceMac: this.options.sourceMac,
|
|
1127
|
-
destinationMac: this.options.destinationMac,
|
|
1128
|
-
sessionKey: this.sessionKey,
|
|
1129
|
-
counter: this.lastAckCounter
|
|
1130
|
-
}));
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
261
|
// src/mac-telnet/console.ts
|
|
262
|
+
import { MacTelnetSession } from "@tikoci/centrs/protocols";
|
|
1135
263
|
var ESC = "\x1B";
|
|
1136
264
|
var enc = new TextEncoder;
|
|
1137
265
|
var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
@@ -1384,6 +512,15 @@ class MacTelnetConsole {
|
|
|
1384
512
|
}
|
|
1385
513
|
|
|
1386
514
|
// src/mac-telnet/client.ts
|
|
515
|
+
import {
|
|
516
|
+
DEFAULT_MAC_TELNET_BROADCAST,
|
|
517
|
+
isBroadcastHost,
|
|
518
|
+
MAC_TELNET_PORT,
|
|
519
|
+
createUdpMacTelnetTransport,
|
|
520
|
+
parseMac,
|
|
521
|
+
resolveMacTelnetRoute
|
|
522
|
+
} from "@tikoci/centrs/protocols";
|
|
523
|
+
|
|
1387
524
|
class MikroTikMacTelnetClient {
|
|
1388
525
|
transport = null;
|
|
1389
526
|
console = null;
|
|
@@ -1456,7 +593,7 @@ class MikroTikMacTelnetClient {
|
|
|
1456
593
|
}
|
|
1457
594
|
|
|
1458
595
|
// src/ssh/client.ts
|
|
1459
|
-
import { readFileSync as
|
|
596
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1460
597
|
import { Client } from "ssh2";
|
|
1461
598
|
function decodeOutput(data) {
|
|
1462
599
|
if (!data || data.length === 0)
|
|
@@ -1491,7 +628,7 @@ class MikroTikSSHClient {
|
|
|
1491
628
|
cfg.privateKey = this.opts.privateKey;
|
|
1492
629
|
} else if (this.opts.keyFilename) {
|
|
1493
630
|
try {
|
|
1494
|
-
cfg.privateKey =
|
|
631
|
+
cfg.privateKey = readFileSync2(this.opts.keyFilename);
|
|
1495
632
|
} catch (e) {
|
|
1496
633
|
this.lastError = `could not read key file ${this.opts.keyFilename}: ${e instanceof Error ? e.message : String(e)}`;
|
|
1497
634
|
logger.error(`Failed to read SSH key file ${this.opts.keyFilename}: ${String(e)}`);
|
|
@@ -2350,8 +1487,8 @@ function defineTool(def) {
|
|
|
2350
1487
|
function registerTools(server, modules, opts = {}) {
|
|
2351
1488
|
let count = 0;
|
|
2352
1489
|
const seen = new Set;
|
|
2353
|
-
for (const
|
|
2354
|
-
for (const tool of
|
|
1490
|
+
for (const mod of modules) {
|
|
1491
|
+
for (const tool of mod) {
|
|
2355
1492
|
if (seen.has(tool.name)) {
|
|
2356
1493
|
throw new Error(`Duplicate tool name registered: ${tool.name}`);
|
|
2357
1494
|
}
|
|
@@ -2486,7 +1623,7 @@ ${result}`;
|
|
|
2486
1623
|
];
|
|
2487
1624
|
|
|
2488
1625
|
// src/core/ui-resources.ts
|
|
2489
|
-
import { readFileSync as
|
|
1626
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2490
1627
|
import { join as join3 } from "path";
|
|
2491
1628
|
import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
2492
1629
|
|
|
@@ -2533,7 +1670,7 @@ function registerUiResources(server) {
|
|
|
2533
1670
|
registerAppResource(server, view.name, uri, { description: view.description, mimeType: RESOURCE_MIME_TYPE }, () => {
|
|
2534
1671
|
let html;
|
|
2535
1672
|
try {
|
|
2536
|
-
html =
|
|
1673
|
+
html = readFileSync3(file, "utf8");
|
|
2537
1674
|
} catch {
|
|
2538
1675
|
html = placeholderHtml(view);
|
|
2539
1676
|
}
|
|
@@ -18499,7 +17636,7 @@ var moduleCatalog = [
|
|
|
18499
17636
|
var allToolModules = moduleCatalog.map((m) => m.tools);
|
|
18500
17637
|
|
|
18501
17638
|
// src/observability/dashboard.ts
|
|
18502
|
-
import { readFileSync as
|
|
17639
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
18503
17640
|
import { join as join4 } from "path";
|
|
18504
17641
|
var {serve } = globalThis.Bun;
|
|
18505
17642
|
|
|
@@ -18512,6 +17649,16 @@ function getDeviceStatus(name) {
|
|
|
18512
17649
|
return statuses.get(name) ?? UNKNOWN;
|
|
18513
17650
|
}
|
|
18514
17651
|
async function probeDevice(name, dc) {
|
|
17652
|
+
if (dc.mac) {
|
|
17653
|
+
const status2 = {
|
|
17654
|
+
reachable: null,
|
|
17655
|
+
checkedAt: Date.now(),
|
|
17656
|
+
latencyMs: null,
|
|
17657
|
+
error: "MAC-Telnet device \u2014 reachability is verified on tool use, not background-probed."
|
|
17658
|
+
};
|
|
17659
|
+
statuses.set(name, status2);
|
|
17660
|
+
return status2;
|
|
17661
|
+
}
|
|
18515
17662
|
const client = createDeviceClient({
|
|
18516
17663
|
...dc,
|
|
18517
17664
|
timeoutMs: Math.min(dc.timeoutMs ?? 1e4, 8000)
|
|
@@ -18819,7 +17966,7 @@ function json(body, status = 200) {
|
|
|
18819
17966
|
}
|
|
18820
17967
|
function dashboardHtml() {
|
|
18821
17968
|
try {
|
|
18822
|
-
return
|
|
17969
|
+
return readFileSync4(join4(UI_DIST_DIR, "observability.html"), "utf8");
|
|
18823
17970
|
} catch {
|
|
18824
17971
|
return `<!doctype html><meta charset=utf-8><body style="font:14px system-ui;padding:24px;background:#0b0d10;color:#e8eaed">
|
|
18825
17972
|
<h2>MikroTik MCP \u2014 Observability Dashboard</h2>
|
|
@@ -18892,8 +18039,11 @@ function devicesPayload(store2) {
|
|
|
18892
18039
|
name,
|
|
18893
18040
|
host: dc.host,
|
|
18894
18041
|
port: dc.port,
|
|
18042
|
+
mac: dc.mac,
|
|
18043
|
+
transport: dc.mac ? "mac-telnet" : "ssh",
|
|
18044
|
+
address: dc.mac ? dc.mac : `${dc.host}:${dc.port}`,
|
|
18895
18045
|
username: dc.username,
|
|
18896
|
-
authMode: dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
|
|
18046
|
+
authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
|
|
18897
18047
|
isDefault: name === cfg.defaultDevice,
|
|
18898
18048
|
description: dc.description,
|
|
18899
18049
|
status: getDeviceStatus(name),
|
|
@@ -19104,7 +18254,7 @@ function corsHeaders(origin, configured) {
|
|
|
19104
18254
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19105
18255
|
|
|
19106
18256
|
// src/prompts/index.ts
|
|
19107
|
-
import { readFileSync as
|
|
18257
|
+
import { readFileSync as readFileSync5, readdirSync } from "fs";
|
|
19108
18258
|
import { join as join5 } from "path";
|
|
19109
18259
|
import { z as z87 } from "zod";
|
|
19110
18260
|
function parseFrontmatter(raw) {
|
|
@@ -19176,7 +18326,7 @@ function registerPrompts(server) {
|
|
|
19176
18326
|
for (const file of files) {
|
|
19177
18327
|
let parsed;
|
|
19178
18328
|
try {
|
|
19179
|
-
parsed = parseFrontmatter(
|
|
18329
|
+
parsed = parseFrontmatter(readFileSync5(join5(PROMPTS_DIR, file), "utf8"));
|
|
19180
18330
|
} catch (e) {
|
|
19181
18331
|
logger.warn(`Skipping prompt ${file}: ${String(e)}`);
|
|
19182
18332
|
continue;
|
|
@@ -19208,7 +18358,7 @@ function registerPrompts(server) {
|
|
|
19208
18358
|
// package.json
|
|
19209
18359
|
var package_default = {
|
|
19210
18360
|
name: "@usex/mikrotik-mcp",
|
|
19211
|
-
version: "2.
|
|
18361
|
+
version: "2.3.0",
|
|
19212
18362
|
description: "Bun-native MCP server for MikroTik RouterOS \u2014 200+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
19213
18363
|
keywords: [
|
|
19214
18364
|
"ai",
|
|
@@ -19282,6 +18432,7 @@ var package_default = {
|
|
|
19282
18432
|
dependencies: {
|
|
19283
18433
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
19284
18434
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
18435
|
+
"@tikoci/centrs": "^0.1.0",
|
|
19285
18436
|
ssh2: "^1.17.0",
|
|
19286
18437
|
zod: "^4.4.3"
|
|
19287
18438
|
},
|
|
@@ -19525,8 +18676,8 @@ function warnIfPlaintextPasswordInContainer(anyPassword) {
|
|
|
19525
18676
|
function listTools() {
|
|
19526
18677
|
const risk = (a) => a.readOnlyHint ? "READ" : a.destructiveHint ? "DESTRUCTIVE" : "WRITE";
|
|
19527
18678
|
let total = 0;
|
|
19528
|
-
for (const
|
|
19529
|
-
for (const t of
|
|
18679
|
+
for (const mod of allToolModules) {
|
|
18680
|
+
for (const t of mod) {
|
|
19530
18681
|
total++;
|
|
19531
18682
|
process.stdout.write(`${risk(t.annotations).padEnd(12)} ${t.name.padEnd(34)} ${t.title}
|
|
19532
18683
|
`);
|