@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/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
|
|
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 =
|
|
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
|
|
2322
|
-
for (const tool of
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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.
|
|
17714
|
+
version: "2.3.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
|
},
|