@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 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 readFileSync3 } from "fs";
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 = readFileSync3(this.opts.keyFilename);
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 mod2 of modules) {
2354
- for (const tool of mod2) {
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 readFileSync4 } from "fs";
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 = readFileSync4(file, "utf8");
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 readFileSync5 } from "fs";
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 readFileSync5(join4(UI_DIST_DIR, "observability.html"), "utf8");
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 readFileSync6, readdirSync } from "fs";
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(readFileSync6(join5(PROMPTS_DIR, file), "utf8"));
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.2.0",
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 mod2 of allToolModules) {
19529
- for (const t of mod2) {
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
  `);