@usex/mikrotik-mcp 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -251,880 +251,8 @@ var logger = {
251
251
  error: (m) => emit("error", m)
252
252
  };
253
253
 
254
- // src/mac-telnet/protocol.ts
255
- import { createHash as createHash2, randomBytes as randomBytes2, randomInt } from "crypto";
256
- import { createSocket } from "dgram";
257
- import { readFileSync as readFileSync2 } from "fs";
258
- import { networkInterfaces } from "os";
259
-
260
- // src/mac-telnet/ec-srp5.ts
261
- import { createHash, randomBytes } from "crypto";
262
- var EC_SRP5_PUBKEY_LEN = 33;
263
- var P = BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed");
264
- var A = BigInt("0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144");
265
- var B = BigInt("0x7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864");
266
- var N = BigInt("0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed");
267
- var GX = BigInt("0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a");
268
- var GY = BigInt("0x5f51e65e475f794b1fe122d388b72eb36dc2b28192839e4dd6163a5d81312c14");
269
- var W2M = BigInt("0x555555555555555555555555555555555555555555555555555555555552db9c");
270
- var M2W = BigInt("0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2451");
271
- var SQRT_M1 = modPow(2n, (P - 1n) / 4n, P);
272
- var G = { x: GX, y: GY };
273
- function mod(value, m) {
274
- const r = value % m;
275
- return r < 0n ? r + m : r;
276
- }
277
- function modPow(base, exp, m) {
278
- let result = 1n;
279
- let b = mod(base, m);
280
- let e = exp;
281
- while (e > 0n) {
282
- if (e & 1n)
283
- result = result * b % m;
284
- b = b * b % m;
285
- e >>= 1n;
286
- }
287
- return result;
288
- }
289
- function modInv(value) {
290
- return modPow(value, P - 2n, P);
291
- }
292
- function modSqrt(a) {
293
- const aa = mod(a, P);
294
- if (aa === 0n)
295
- return 0n;
296
- let x = modPow(aa, (P + 3n) / 8n, P);
297
- if (x * x % P === aa)
298
- return x;
299
- x = mod(x * SQRT_M1, P);
300
- if (x * x % P === aa)
301
- return x;
302
- return null;
303
- }
304
- function bytesToBigIntBE(bytes) {
305
- let value = 0n;
306
- for (const byte of bytes)
307
- value = value << 8n | BigInt(byte);
308
- return value;
309
- }
310
- function bigIntToBytesBE(value, length) {
311
- const out = new Uint8Array(length);
312
- let v = value;
313
- for (let i = length - 1;i >= 0; i -= 1) {
314
- out[i] = Number(v & 0xffn);
315
- v >>= 8n;
316
- }
317
- return out;
318
- }
319
- function sha256(...chunks) {
320
- const hash = createHash("sha256");
321
- for (const chunk of chunks)
322
- hash.update(chunk);
323
- return new Uint8Array(hash.digest());
324
- }
325
- function concatBytes(parts) {
326
- const total = parts.reduce((sum, part) => sum + part.length, 0);
327
- const out = new Uint8Array(total);
328
- let cursor = 0;
329
- for (const part of parts) {
330
- out.set(part, cursor);
331
- cursor += part.length;
332
- }
333
- return out;
334
- }
335
- function pointAdd(p, q) {
336
- if (p === null)
337
- return q;
338
- if (q === null)
339
- return p;
340
- if (p.x === q.x) {
341
- if (mod(p.y + q.y, P) === 0n)
342
- return null;
343
- return pointDouble(p);
344
- }
345
- const slope = mod((q.y - p.y) * modInv(mod(q.x - p.x, P)), P);
346
- const x = mod(slope * slope - p.x - q.x, P);
347
- const y = mod(slope * (p.x - x) - p.y, P);
348
- return { x, y };
349
- }
350
- function pointDouble(p) {
351
- if (p === null)
352
- return null;
353
- if (p.y === 0n)
354
- return null;
355
- const slope = mod((3n * p.x * p.x + A) * modInv(mod(2n * p.y, P)), P);
356
- const x = mod(slope * slope - 2n * p.x, P);
357
- const y = mod(slope * (p.x - x) - p.y, P);
358
- return { x, y };
359
- }
360
- function scalarMul(scalar, point) {
361
- let result = null;
362
- let addend = point;
363
- let k = scalar;
364
- while (k > 0n) {
365
- if (k & 1n)
366
- result = pointAdd(result, addend);
367
- addend = pointDouble(addend);
368
- k >>= 1n;
369
- }
370
- return result;
371
- }
372
- function liftX(xWeier, parity) {
373
- const x = mod(xWeier, P);
374
- const rhs = mod(x * x * x + A * x + B, P);
375
- const y0 = modSqrt(rhs);
376
- if (y0 === null)
377
- return null;
378
- const y = Number(y0 & 1n) === (parity & 1) ? y0 : mod(-y0, P);
379
- return { x, y };
380
- }
381
- function encodePoint(point) {
382
- if (point === null) {
383
- throw new Error("Cannot encode the EC-SRP5 point at infinity.");
384
- }
385
- const out = new Uint8Array(EC_SRP5_PUBKEY_LEN);
386
- out.set(bigIntToBytesBE(mod(point.x + W2M, P), 32), 0);
387
- out[32] = Number(point.y & 1n);
388
- return out;
389
- }
390
- function decodePoint(key) {
391
- if (key.length !== EC_SRP5_PUBKEY_LEN) {
392
- throw new Error(`EC-SRP5 public key must be ${EC_SRP5_PUBKEY_LEN} bytes (got ${key.length}).`);
393
- }
394
- const xWeier = mod(bytesToBigIntBE(key.subarray(0, 32)) + M2W, P);
395
- const point = liftX(xWeier, key[32]);
396
- if (point === null) {
397
- throw new Error("EC-SRP5 public key X is not a valid curve point.");
398
- }
399
- return point;
400
- }
401
- function ecSrp5Keygen(privBytes) {
402
- const priv = privBytes ? Uint8Array.from(privBytes) : new Uint8Array(randomBytes(32));
403
- if (priv.length !== 32) {
404
- throw new Error("EC-SRP5 private key seed must be 32 bytes.");
405
- }
406
- priv[0] = priv[0] & 248;
407
- priv[31] = priv[31] & 127;
408
- priv[31] = priv[31] | 64;
409
- const privateKey = bytesToBigIntBE(priv);
410
- return { privateKey, publicKey: encodePoint(scalarMul(privateKey, G)) };
411
- }
412
- function ecSrp5Id(username, password, salt) {
413
- const inner = sha256(new TextEncoder().encode(`${username}:${password}`));
414
- return sha256(salt, inner);
415
- }
416
- function redp1(montgomeryX, parity) {
417
- let seed = bytesToBigIntBE(sha256(montgomeryX));
418
- for (;; ) {
419
- const candidate = sha256(bigIntToBytesBE(seed, 32));
420
- const xWeier = mod(bytesToBigIntBE(candidate) + M2W, P);
421
- const point = liftX(xWeier, parity);
422
- if (point !== null)
423
- return point;
424
- seed = mod(seed + 1n, 1n << 256n);
425
- }
426
- }
427
- function ecSrp5ClientShared(privateKey, serverKey, clientKey, validator) {
428
- const serverPoint = decodePoint(serverKey);
429
- const v = bytesToBigIntBE(validator);
430
- const vG = scalarMul(v, G);
431
- if (vG === null) {
432
- throw new Error("EC-SRP5 validator produced the point at infinity.");
433
- }
434
- const gamma = redp1(bigIntToBytesBE(mod(vG.x + W2M, P), 32), 1);
435
- const wB = pointAdd(serverPoint, gamma);
436
- const j = sha256(clientKey.subarray(0, 32), serverKey.subarray(0, 32));
437
- const scalar = mod(v * bytesToBigIntBE(j) + privateKey, N);
438
- const pt = scalarMul(scalar, wB);
439
- if (pt === null) {
440
- throw new Error("EC-SRP5 shared point computed as infinity.");
441
- }
442
- const z2 = bigIntToBytesBE(mod(pt.x + W2M, P), 32);
443
- return { j, z: z2 };
444
- }
445
- function ecSrp5ClientProof(privateKey, serverKey, clientKey, validator) {
446
- const { j, z: z2 } = ecSrp5ClientShared(privateKey, serverKey, clientKey, validator);
447
- return sha256(j, z2);
448
- }
449
-
450
- // src/mac-telnet/mtwei.ts
451
- function mtweiOfferValue(username, publicKey) {
452
- return concatBytes([new TextEncoder().encode(username), Uint8Array.of(0), publicKey]);
453
- }
454
-
455
- // src/mac-telnet/protocol.ts
456
- var MAC_TELNET_PORT = 20561;
457
- var MAC_TELNET_HEADER_LEN = 22;
458
- var MAC_TELNET_CONTROL_HEADER_LEN = 9;
459
- var MAC_TELNET_CONTROL_MAGIC = Uint8Array.of(86, 52, 18, 255);
460
- var MAC_TELNET_CLIENT_TYPE = Uint8Array.of(0, 21);
461
- var MAC_TELNET_RETRANSMIT_SCHEDULE_MS = [
462
- 15,
463
- 20,
464
- 30,
465
- 50,
466
- 90,
467
- 170,
468
- 330,
469
- 660,
470
- 1000
471
- ];
472
- var MAC_TELNET_KEEPALIVE_IDLE_MS = 8000;
473
- var MacTelnetPacketType = {
474
- sessionStart: 0,
475
- data: 1,
476
- ack: 2,
477
- ping: 4,
478
- pong: 5,
479
- end: 255
480
- };
481
- var MacTelnetControlType = {
482
- beginAuth: 0,
483
- passwordSalt: 1,
484
- password: 2,
485
- username: 3,
486
- terminalType: 4,
487
- terminalWidth: 5,
488
- terminalHeight: 6,
489
- packetError: 7,
490
- endAuth: 9
491
- };
492
- function parseMac(value) {
493
- const parts = value.trim().split(/[:\-.]/);
494
- if (parts.length !== 6) {
495
- throw new Error(`"${value}" is not a 6-octet MAC address. Provide a MAC like aa:bb:cc:dd:ee:ff.`);
496
- }
497
- const octets = new Uint8Array(6);
498
- for (let index = 0;index < 6; index += 1) {
499
- const octet = Number.parseInt(parts[index], 16);
500
- if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
501
- throw new Error(`"${value}" has an invalid octet "${parts[index]}". Each octet must be a two-digit hex value (00\u2013ff).`);
502
- }
503
- octets[index] = octet;
504
- }
505
- return octets;
506
- }
507
- function macEquals(a, b) {
508
- if (a.length !== b.length)
509
- return false;
510
- for (let index = 0;index < a.length; index += 1) {
511
- if (a[index] !== b[index])
512
- return false;
513
- }
514
- return true;
515
- }
516
- function encodeHeader(options) {
517
- const data = new Uint8Array(MAC_TELNET_HEADER_LEN);
518
- const view = new DataView(data.buffer);
519
- data[0] = 1;
520
- data[1] = options.type;
521
- data.set(options.sourceMac, 2);
522
- data.set(options.destinationMac, 8);
523
- const sessionKeyOffset = options.fromServer ? 16 : 14;
524
- const clientTypeOffset = options.fromServer ? 14 : 16;
525
- view.setUint16(sessionKeyOffset, options.sessionKey & 65535, false);
526
- data.set(MAC_TELNET_CLIENT_TYPE, clientTypeOffset);
527
- view.setUint32(18, options.counter >>> 0, false);
528
- return data;
529
- }
530
- function decodeHeader(bytes, options = {}) {
531
- if (bytes.length < MAC_TELNET_HEADER_LEN) {
532
- throw new Error(`MAC-Telnet packet is too short (${bytes.length} bytes, need at least ${MAC_TELNET_HEADER_LEN}).`);
533
- }
534
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
535
- const fromServer = options.fromServer ?? true;
536
- const sessionKeyOffset = fromServer ? 16 : 14;
537
- return {
538
- version: bytes[0],
539
- type: bytes[1],
540
- sourceMac: bytes.slice(2, 8),
541
- destinationMac: bytes.slice(8, 14),
542
- sessionKey: view.getUint16(sessionKeyOffset, false),
543
- counter: view.getUint32(18, false)
544
- };
545
- }
546
- function encodeControlBlock(type, value = new Uint8Array(0)) {
547
- const block = new Uint8Array(MAC_TELNET_CONTROL_HEADER_LEN + value.length);
548
- block.set(MAC_TELNET_CONTROL_MAGIC, 0);
549
- block[4] = type;
550
- new DataView(block.buffer).setUint32(5, value.length >>> 0, false);
551
- block.set(value, MAC_TELNET_CONTROL_HEADER_LEN);
552
- return block;
553
- }
554
- function parseControlBlocks(payload) {
555
- const blocks = [];
556
- let offset = 0;
557
- while (offset < payload.length) {
558
- const remaining = payload.length - offset;
559
- 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];
560
- if (!hasMagic) {
561
- blocks.push({ type: "plaindata", value: payload.slice(offset) });
562
- break;
563
- }
564
- const type = payload[offset + 4];
565
- const length = new DataView(payload.buffer, payload.byteOffset + offset + 5, 4).getUint32(0, false);
566
- const valueStart = offset + MAC_TELNET_CONTROL_HEADER_LEN;
567
- const valueEnd = valueStart + length;
568
- if (valueEnd > payload.length) {
569
- throw new Error(`MAC-Telnet control block claims ${length} bytes but only ${payload.length - valueStart} remain.`);
570
- }
571
- blocks.push({ type, value: payload.slice(valueStart, valueEnd) });
572
- offset = valueEnd;
573
- }
574
- return blocks;
575
- }
576
- function macTelnetPasswordHash(password, salt) {
577
- const digest = createHash2("md5").update(Buffer.from([0])).update(Buffer.from(password, "utf8")).update(salt).digest();
578
- const out = new Uint8Array(17);
579
- out[0] = 0;
580
- out.set(digest, 1);
581
- return out;
582
- }
583
- function encodeTerminalDimension(value) {
584
- const out = new Uint8Array(2);
585
- new DataView(out.buffer).setUint16(0, value & 65535, true);
586
- return out;
587
- }
588
- function concatBytes2(parts) {
589
- const total = parts.reduce((sum, part) => sum + part.length, 0);
590
- const out = new Uint8Array(total);
591
- let cursor = 0;
592
- for (const part of parts) {
593
- out.set(part, cursor);
594
- cursor += part.length;
595
- }
596
- return out;
597
- }
598
- function buildPacket(options) {
599
- const header = encodeHeader({
600
- type: options.type,
601
- sourceMac: options.sourceMac,
602
- destinationMac: options.destinationMac,
603
- sessionKey: options.sessionKey,
604
- counter: options.counter
605
- });
606
- if (!options.payload || options.payload.length === 0)
607
- return header;
608
- return concatBytes2([header, options.payload]);
609
- }
610
- function createUdpMacTelnetTransport(options) {
611
- const socket = createSocket({ type: "udp4", reuseAddr: true });
612
- let handler;
613
- socket.on("message", (message) => {
614
- handler?.(new Uint8Array(message));
615
- });
616
- const readyPromise = new Promise((resolve, reject) => {
617
- socket.once("error", reject);
618
- socket.bind(0, () => {
619
- if (options.broadcast) {
620
- try {
621
- socket.setBroadcast(true);
622
- } catch {}
623
- }
624
- resolve();
625
- });
626
- });
627
- return {
628
- send(bytes) {
629
- socket.send(bytes, options.port, options.host);
630
- },
631
- close() {
632
- try {
633
- socket.close();
634
- } catch {}
635
- },
636
- onMessage(next) {
637
- handler = next;
638
- },
639
- ready() {
640
- return readyPromise;
641
- }
642
- };
643
- }
644
- function isZeroMac(mac) {
645
- return mac.every((octet) => octet === 0);
646
- }
647
- function tryParseMac(value) {
648
- if (!value)
649
- return;
650
- try {
651
- return parseMac(value);
652
- } catch {
653
- return;
654
- }
655
- }
656
- function hardwareMacOf(name) {
657
- try {
658
- const sys = readFileSync2(`/sys/class/net/${name}/address`, "utf8").trim();
659
- const mac = tryParseMac(sys);
660
- if (mac && !isZeroMac(mac))
661
- return mac;
662
- } catch {}
663
- return;
664
- }
665
- function ipv4ToInt(addr) {
666
- const parts = addr.split(".");
667
- if (parts.length !== 4)
668
- return;
669
- let value = 0;
670
- for (const part of parts) {
671
- const octet = Number(part);
672
- if (!Number.isInteger(octet) || octet < 0 || octet > 255)
673
- return;
674
- value = value << 8 | octet;
675
- }
676
- return value >>> 0;
677
- }
678
- function intToIpv4(value) {
679
- return [24, 16, 8, 0].map((shift) => value >>> shift & 255).join(".");
680
- }
681
- function directedBroadcast(address, netmask) {
682
- const addr = ipv4ToInt(address);
683
- const mask = ipv4ToInt(netmask);
684
- if (addr === undefined || mask === undefined)
685
- return;
686
- return intToIpv4((addr & mask | ~mask >>> 0) >>> 0);
687
- }
688
- function listBroadcastInterfaces() {
689
- const out = [];
690
- for (const [name, infos] of Object.entries(networkInterfaces())) {
691
- for (const info of infos ?? []) {
692
- if (info.family !== "IPv4" || info.internal)
693
- continue;
694
- let mac = tryParseMac(info.mac);
695
- if (!mac || isZeroMac(mac))
696
- mac = hardwareMacOf(name);
697
- if (!mac || isZeroMac(mac))
698
- continue;
699
- const broadcast = directedBroadcast(info.address, info.netmask);
700
- if (!broadcast)
701
- continue;
702
- out.push({ name, address: info.address, broadcast, mac });
703
- }
704
- }
705
- return out;
706
- }
707
- async function egressAddressFor(host, port) {
708
- return await new Promise((resolve) => {
709
- const probe = createSocket("udp4");
710
- const done = (addr) => {
711
- try {
712
- probe.close();
713
- } catch {}
714
- resolve(addr);
715
- };
716
- probe.on("error", () => done(undefined));
717
- try {
718
- probe.connect(port, host, () => {
719
- try {
720
- done(probe.address().address);
721
- } catch {
722
- done(undefined);
723
- }
724
- });
725
- } catch {
726
- done(undefined);
727
- }
728
- });
729
- }
730
- async function resolveEgressMac(host, port) {
731
- const localAddr = await egressAddressFor(host, port);
732
- if (!localAddr)
733
- return;
734
- for (const ifc of listBroadcastInterfaces()) {
735
- if (ifc.address === localAddr)
736
- return ifc.mac;
737
- }
738
- return;
739
- }
740
- async function discoverMacTelnetRoute(opts) {
741
- const interfaces = listBroadcastInterfaces();
742
- if (interfaces.length === 0)
743
- return;
744
- const sessionKey = opts.sessionKey ?? randomInt(65536);
745
- return await new Promise((resolve) => {
746
- const socket = createSocket({ type: "udp4", reuseAddr: true });
747
- let settled = false;
748
- const timers = [];
749
- const finish = (route) => {
750
- if (settled)
751
- return;
752
- settled = true;
753
- for (const t of timers)
754
- clearTimeout(t);
755
- try {
756
- socket.close();
757
- } catch {}
758
- resolve(route);
759
- };
760
- socket.on("error", () => finish(undefined));
761
- socket.on("message", (message) => {
762
- let header;
763
- try {
764
- header = decodeHeader(new Uint8Array(message), { fromServer: true });
765
- } catch {
766
- return;
767
- }
768
- if (header.version !== 1 || header.sessionKey !== sessionKey)
769
- return;
770
- if (header.type !== MacTelnetPacketType.ack && header.type !== MacTelnetPacketType.data) {
771
- return;
772
- }
773
- if (!macEquals(header.sourceMac, opts.destinationMac))
774
- return;
775
- const winner = interfaces.find((ifc) => macEquals(ifc.mac, header.destinationMac));
776
- if (winner)
777
- finish({ sourceMac: winner.mac, host: winner.broadcast });
778
- });
779
- const spray = () => {
780
- for (const ifc of interfaces) {
781
- const packet = buildPacket({
782
- type: MacTelnetPacketType.sessionStart,
783
- sourceMac: ifc.mac,
784
- destinationMac: opts.destinationMac,
785
- sessionKey,
786
- counter: 0
787
- });
788
- try {
789
- socket.send(packet, opts.port, ifc.broadcast);
790
- } catch {}
791
- }
792
- };
793
- socket.bind(0, () => {
794
- try {
795
- socket.setBroadcast(true);
796
- } catch {}
797
- spray();
798
- timers.push(setTimeout(spray, 250));
799
- timers.push(setTimeout(spray, 700));
800
- timers.push(setTimeout(() => finish(undefined), opts.timeoutMs));
801
- });
802
- });
803
- }
804
- var DEFAULT_MAC_TELNET_BROADCAST = "255.255.255.255";
805
- function randomLocalMac() {
806
- const octets = new Uint8Array(randomBytes2(6));
807
- octets[0] = octets[0] & 254 | 2;
808
- return octets;
809
- }
810
- function isBroadcastHost(host) {
811
- return host === DEFAULT_MAC_TELNET_BROADCAST || host.endsWith(".255");
812
- }
813
- async function resolveMacTelnetRoute(config) {
814
- if (config.explicitSourceMac) {
815
- return { sourceMac: config.explicitSourceMac, host: config.host };
816
- }
817
- if (config.host === DEFAULT_MAC_TELNET_BROADCAST) {
818
- const route = await discoverMacTelnetRoute({
819
- destinationMac: config.destinationMac,
820
- port: config.port,
821
- timeoutMs: Math.min(config.timeoutMs, 5000)
822
- });
823
- if (route)
824
- return route;
825
- }
826
- const sourceMac = await resolveEgressMac(config.host, config.port) ?? randomLocalMac();
827
- return { sourceMac, host: config.host };
828
- }
829
-
830
- class MacTelnetSession {
831
- options;
832
- sessionKey;
833
- state = "init";
834
- outCounter = 0;
835
- lastInCounter = null;
836
- mtweiKeypair;
837
- lastAckCounter = 0;
838
- maxOutAck = -1;
839
- pending;
840
- activitySinceTick = false;
841
- idleMs = 0;
842
- lastTickMs;
843
- constructor(options) {
844
- this.options = options;
845
- this.sessionKey = options.sessionKey ?? randomInt(65536);
846
- }
847
- get key() {
848
- return this.sessionKey;
849
- }
850
- start() {
851
- if (this.state !== "init")
852
- return;
853
- const bytes = this.sendPacket(MacTelnetPacketType.sessionStart);
854
- this.pending = { bytes, ackTarget: 0, attempts: 0, elapsedMs: 0 };
855
- this.state = "session-start-sent";
856
- }
857
- handlePacket(bytes) {
858
- if (this.state === "closed")
859
- return;
860
- let header;
861
- try {
862
- header = decodeHeader(bytes, { fromServer: true });
863
- } catch {
864
- return;
865
- }
866
- switch (header.type) {
867
- case MacTelnetPacketType.ack:
868
- if (this.matchesSession(header))
869
- this.onAck(header);
870
- return;
871
- case MacTelnetPacketType.data:
872
- if (this.matchesSession(header))
873
- this.onDataPacket(header, bytes);
874
- return;
875
- case MacTelnetPacketType.end:
876
- if (this.matchesSession(header))
877
- this.onEnd(header);
878
- return;
879
- case MacTelnetPacketType.ping:
880
- if (header.version === 1 && macEquals(header.destinationMac, this.options.sourceMac)) {
881
- this.onPing(header);
882
- }
883
- }
884
- }
885
- matchesSession(header) {
886
- return header.version === 1 && header.sessionKey === this.sessionKey && macEquals(header.sourceMac, this.options.destinationMac) && macEquals(header.destinationMac, this.options.sourceMac);
887
- }
888
- onPing(header) {
889
- this.activitySinceTick = true;
890
- this.emit(encodeHeader({
891
- type: MacTelnetPacketType.pong,
892
- sourceMac: this.options.sourceMac,
893
- destinationMac: this.options.destinationMac,
894
- sessionKey: header.sessionKey,
895
- counter: header.counter
896
- }));
897
- }
898
- onAck(header) {
899
- this.activitySinceTick = true;
900
- this.maxOutAck = Math.max(this.maxOutAck, header.counter);
901
- if (this.pending && this.maxOutAck >= this.pending.ackTarget) {
902
- this.pending = undefined;
903
- }
904
- if (this.state !== "session-start-sent")
905
- return;
906
- const blocks = [encodeControlBlock(MacTelnetControlType.beginAuth)];
907
- if (this.options.offerMtwei !== false) {
908
- this.mtweiKeypair = ecSrp5Keygen();
909
- blocks.push(encodeControlBlock(MacTelnetControlType.passwordSalt, mtweiOfferValue(this.identityUsername(), this.mtweiKeypair.publicKey)));
910
- }
911
- this.sendData(blocks);
912
- this.state = "auth-begin-sent";
913
- }
914
- identityUsername() {
915
- return this.options.username.split("+", 1)[0] ?? this.options.username;
916
- }
917
- onDataPacket(header, bytes) {
918
- this.activitySinceTick = true;
919
- const payload = bytes.subarray(MAC_TELNET_HEADER_LEN);
920
- this.acknowledge(header.counter, payload.length);
921
- if (!this.acceptCounter(header.counter))
922
- return;
923
- let blocks;
924
- try {
925
- blocks = parseControlBlocks(payload);
926
- } catch (error) {
927
- this.fail(error);
928
- return;
929
- }
930
- for (const block of blocks)
931
- this.handleControlBlock(block);
932
- }
933
- handleControlBlock(block) {
934
- switch (block.type) {
935
- case MacTelnetControlType.passwordSalt:
936
- this.handlePasswordSalt(block.value);
937
- return;
938
- case MacTelnetControlType.endAuth:
939
- if (this.state === "auth-sent") {
940
- this.state = "auth-complete";
941
- }
942
- return;
943
- case "plaindata":
944
- this.handleTerminalData(block.value);
945
- return;
946
- case MacTelnetControlType.packetError:
947
- 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."));
948
- }
949
- }
950
- handleTerminalData(data) {
951
- if (this.state === "auth-complete") {
952
- if (/login failed/i.test(new TextDecoder().decode(data))) {
953
- this.fail(new Error("MAC-Telnet login failed: incorrect username or password."));
954
- return;
955
- }
956
- this.state = "ready";
957
- this.options.onReady?.();
958
- if (data.length > 0)
959
- this.options.onData?.(data);
960
- return;
961
- }
962
- if (this.state === "ready" && data.length > 0)
963
- this.options.onData?.(data);
964
- }
965
- handlePasswordSalt(salt) {
966
- if (this.state !== "auth-begin-sent")
967
- return;
968
- let passwordValue;
969
- if (salt.length === 16) {
970
- passwordValue = macTelnetPasswordHash(this.options.password, salt);
971
- } else if (salt.length === EC_SRP5_PUBKEY_LEN + 16) {
972
- if (!this.mtweiKeypair) {
973
- 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."));
974
- return;
975
- }
976
- const serverKey = salt.subarray(0, EC_SRP5_PUBKEY_LEN);
977
- const mtweiSalt = salt.subarray(EC_SRP5_PUBKEY_LEN);
978
- const validator = ecSrp5Id(this.identityUsername(), this.options.password, mtweiSalt);
979
- passwordValue = ecSrp5ClientProof(this.mtweiKeypair.privateKey, serverKey, this.mtweiKeypair.publicKey, validator);
980
- } else {
981
- 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."));
982
- return;
983
- }
984
- const username = new TextEncoder().encode(this.options.username);
985
- const terminalType = new TextEncoder().encode(this.options.terminalType ?? "vt102");
986
- const blocks = [
987
- encodeControlBlock(MacTelnetControlType.password, passwordValue),
988
- encodeControlBlock(MacTelnetControlType.username, username),
989
- encodeControlBlock(MacTelnetControlType.terminalType, terminalType),
990
- encodeControlBlock(MacTelnetControlType.terminalWidth, encodeTerminalDimension(this.options.terminalWidth ?? 80)),
991
- encodeControlBlock(MacTelnetControlType.terminalHeight, encodeTerminalDimension(this.options.terminalHeight ?? 24))
992
- ];
993
- this.sendData(blocks);
994
- this.state = "auth-sent";
995
- }
996
- onEnd(header) {
997
- this.emit(encodeHeader({
998
- type: MacTelnetPacketType.end,
999
- sourceMac: this.options.sourceMac,
1000
- destinationMac: this.options.destinationMac,
1001
- sessionKey: header.sessionKey,
1002
- counter: 0
1003
- }));
1004
- if (this.state === "auth-complete") {
1005
- this.fail(new Error("MAC-Telnet login failed: the device closed the session after authentication. " + "Check the credentials configured for this device."));
1006
- return;
1007
- }
1008
- this.close();
1009
- }
1010
- sendInput(bytes) {
1011
- if (this.state !== "ready") {
1012
- throw new Error("Cannot send input before the MAC-Telnet session is ready.");
1013
- }
1014
- this.sendData([bytes]);
1015
- }
1016
- end() {
1017
- if (this.state === "closed")
1018
- return;
1019
- this.emit(encodeHeader({
1020
- type: MacTelnetPacketType.end,
1021
- sourceMac: this.options.sourceMac,
1022
- destinationMac: this.options.destinationMac,
1023
- sessionKey: this.sessionKey,
1024
- counter: this.outCounter
1025
- }));
1026
- this.close();
1027
- }
1028
- close(error) {
1029
- if (this.state === "closed")
1030
- return;
1031
- this.state = "closed";
1032
- this.pending = undefined;
1033
- this.options.sink.close();
1034
- this.options.onClose?.(error);
1035
- }
1036
- fail(error) {
1037
- this.close(error);
1038
- }
1039
- emit(bytes) {
1040
- try {
1041
- this.options.sink.send(bytes);
1042
- } catch {}
1043
- }
1044
- acceptCounter(counter) {
1045
- if (this.lastInCounter === null) {
1046
- this.lastInCounter = counter;
1047
- return true;
1048
- }
1049
- if (counter > this.lastInCounter) {
1050
- this.lastInCounter = counter;
1051
- return true;
1052
- }
1053
- return false;
1054
- }
1055
- acknowledge(counter, payloadLength) {
1056
- this.lastAckCounter = counter + payloadLength >>> 0;
1057
- this.activitySinceTick = true;
1058
- this.emit(encodeHeader({
1059
- type: MacTelnetPacketType.ack,
1060
- sourceMac: this.options.sourceMac,
1061
- destinationMac: this.options.destinationMac,
1062
- sessionKey: this.sessionKey,
1063
- counter: this.lastAckCounter
1064
- }));
1065
- }
1066
- sendPacket(type, payload) {
1067
- const bytes = buildPacket({
1068
- type,
1069
- sourceMac: this.options.sourceMac,
1070
- destinationMac: this.options.destinationMac,
1071
- sessionKey: this.sessionKey,
1072
- counter: this.outCounter,
1073
- payload
1074
- });
1075
- this.activitySinceTick = true;
1076
- this.emit(bytes);
1077
- return bytes;
1078
- }
1079
- sendData(parts) {
1080
- const payload = concatBytes2(parts);
1081
- const bytes = this.sendPacket(MacTelnetPacketType.data, payload);
1082
- this.outCounter = this.outCounter + payload.length >>> 0;
1083
- this.pending = {
1084
- bytes,
1085
- ackTarget: this.outCounter,
1086
- attempts: 0,
1087
- elapsedMs: 0
1088
- };
1089
- }
1090
- tick(nowMs) {
1091
- if (this.state === "closed")
1092
- return;
1093
- const delta = this.lastTickMs === undefined ? 0 : Math.max(0, nowMs - this.lastTickMs);
1094
- this.lastTickMs = nowMs;
1095
- if (this.activitySinceTick) {
1096
- this.idleMs = 0;
1097
- this.activitySinceTick = false;
1098
- } else {
1099
- this.idleMs += delta;
1100
- }
1101
- if (this.idleMs >= MAC_TELNET_KEEPALIVE_IDLE_MS) {
1102
- this.sendKeepalive();
1103
- this.idleMs = 0;
1104
- }
1105
- const pending = this.pending;
1106
- if (pending && pending.attempts < MAC_TELNET_RETRANSMIT_SCHEDULE_MS.length) {
1107
- pending.elapsedMs += delta;
1108
- const wait = MAC_TELNET_RETRANSMIT_SCHEDULE_MS[pending.attempts];
1109
- if (pending.elapsedMs >= wait) {
1110
- this.emit(pending.bytes);
1111
- pending.attempts += 1;
1112
- pending.elapsedMs = 0;
1113
- }
1114
- }
1115
- }
1116
- sendKeepalive() {
1117
- this.emit(encodeHeader({
1118
- type: MacTelnetPacketType.ack,
1119
- sourceMac: this.options.sourceMac,
1120
- destinationMac: this.options.destinationMac,
1121
- sessionKey: this.sessionKey,
1122
- counter: this.lastAckCounter
1123
- }));
1124
- }
1125
- }
1126
-
1127
254
  // src/mac-telnet/console.ts
255
+ import { MacTelnetSession } from "@tikoci/centrs/protocols";
1128
256
  var ESC = "\x1B";
1129
257
  var enc = new TextEncoder;
1130
258
  var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
@@ -1377,6 +505,15 @@ class MacTelnetConsole {
1377
505
  }
1378
506
 
1379
507
  // src/mac-telnet/client.ts
508
+ import {
509
+ DEFAULT_MAC_TELNET_BROADCAST,
510
+ isBroadcastHost,
511
+ MAC_TELNET_PORT,
512
+ createUdpMacTelnetTransport,
513
+ parseMac,
514
+ resolveMacTelnetRoute
515
+ } from "@tikoci/centrs/protocols";
516
+
1380
517
  class MikroTikMacTelnetClient {
1381
518
  transport = null;
1382
519
  console = null;
@@ -1449,7 +586,7 @@ class MikroTikMacTelnetClient {
1449
586
  }
1450
587
 
1451
588
  // src/ssh/client.ts
1452
- import { readFileSync as readFileSync3 } from "fs";
589
+ import { readFileSync as readFileSync2 } from "fs";
1453
590
  import { Client } from "ssh2";
1454
591
  function decodeOutput(data) {
1455
592
  if (!data || data.length === 0)
@@ -1484,7 +621,7 @@ class MikroTikSSHClient {
1484
621
  cfg.privateKey = this.opts.privateKey;
1485
622
  } else if (this.opts.keyFilename) {
1486
623
  try {
1487
- cfg.privateKey = readFileSync3(this.opts.keyFilename);
624
+ cfg.privateKey = readFileSync2(this.opts.keyFilename);
1488
625
  } catch (e) {
1489
626
  this.lastError = `could not read key file ${this.opts.keyFilename}: ${e instanceof Error ? e.message : String(e)}`;
1490
627
  logger.error(`Failed to read SSH key file ${this.opts.keyFilename}: ${String(e)}`);
@@ -2318,8 +1455,8 @@ function defineTool(def) {
2318
1455
  function registerTools(server, modules, opts = {}) {
2319
1456
  let count = 0;
2320
1457
  const seen = new Set;
2321
- for (const mod2 of modules) {
2322
- for (const tool of mod2) {
1458
+ for (const mod of modules) {
1459
+ for (const tool of mod) {
2323
1460
  if (seen.has(tool.name)) {
2324
1461
  throw new Error(`Duplicate tool name registered: ${tool.name}`);
2325
1462
  }
@@ -2336,7 +1473,7 @@ function registerTools(server, modules, opts = {}) {
2336
1473
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2337
1474
 
2338
1475
  // src/core/ui-resources.ts
2339
- import { readFileSync as readFileSync4 } from "fs";
1476
+ import { readFileSync as readFileSync3 } from "fs";
2340
1477
  import { join as join3 } from "path";
2341
1478
  import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
2342
1479
 
@@ -2383,7 +1520,7 @@ function registerUiResources(server) {
2383
1520
  registerAppResource(server, view.name, uri, { description: view.description, mimeType: RESOURCE_MIME_TYPE }, () => {
2384
1521
  let html;
2385
1522
  try {
2386
- html = readFileSync4(file, "utf8");
1523
+ html = readFileSync3(file, "utf8");
2387
1524
  } catch {
2388
1525
  html = placeholderHtml(view);
2389
1526
  }
@@ -2394,7 +1531,7 @@ function registerUiResources(server) {
2394
1531
  }
2395
1532
 
2396
1533
  // src/prompts/index.ts
2397
- import { readFileSync as readFileSync5, readdirSync } from "fs";
1534
+ import { readFileSync as readFileSync4, readdirSync } from "fs";
2398
1535
  import { join as join4 } from "path";
2399
1536
  import { z as z3 } from "zod";
2400
1537
  function parseFrontmatter(raw) {
@@ -2466,7 +1603,7 @@ function registerPrompts(server) {
2466
1603
  for (const file of files) {
2467
1604
  let parsed;
2468
1605
  try {
2469
- parsed = parseFrontmatter(readFileSync5(join4(PROMPTS_DIR, file), "utf8"));
1606
+ parsed = parseFrontmatter(readFileSync4(join4(PROMPTS_DIR, file), "utf8"));
2470
1607
  } catch (e) {
2471
1608
  logger.warn(`Skipping prompt ${file}: ${String(e)}`);
2472
1609
  continue;
@@ -18574,7 +17711,7 @@ var allToolModules = moduleCatalog.map((m) => m.tools);
18574
17711
  // package.json
18575
17712
  var package_default = {
18576
17713
  name: "@usex/mikrotik-mcp",
18577
- version: "2.2.0",
17714
+ version: "2.4.0",
18578
17715
  description: "Bun-native MCP server for MikroTik RouterOS \u2014 200+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
18579
17716
  keywords: [
18580
17717
  "ai",
@@ -18648,6 +17785,7 @@ var package_default = {
18648
17785
  dependencies: {
18649
17786
  "@modelcontextprotocol/ext-apps": "^1.7.4",
18650
17787
  "@modelcontextprotocol/sdk": "^1.29.0",
17788
+ "@tikoci/centrs": "^0.1.0",
18651
17789
  ssh2: "^1.17.0",
18652
17790
  zod: "^4.4.3"
18653
17791
  },