@usex/mikrotik-mcp 3.54.0 → 3.56.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.
@@ -337,7 +337,8 @@ var DeviceConfigSchema = z.object({
337
337
  sourceMac: z.string().optional(),
338
338
  macHost: z.string().optional(),
339
339
  macPort: z.coerce.number().int().positive().optional(),
340
- description: z.string().optional()
340
+ description: z.string().optional(),
341
+ disabled: z.boolean().optional()
341
342
  });
342
343
  var S3ConfigSchema = z.object({
343
344
  accessKeyId: z.string().optional(),
@@ -581,13 +582,17 @@ function setConfig(cfg) {
581
582
  function getConfig() {
582
583
  return active;
583
584
  }
585
+ function isEnabled(dc) {
586
+ return !dc.disabled;
587
+ }
584
588
  function listDevices() {
585
- return { names: Object.keys(active.devices), default: active.defaultDevice };
589
+ const names = Object.entries(active.devices).filter(([, dc]) => isEnabled(dc)).map(([k]) => k);
590
+ return { names, default: active.defaultDevice };
586
591
  }
587
592
  function deviceKeyForLabel(name) {
588
593
  const target = name.trim().toLowerCase();
589
594
  for (const [key, dc] of Object.entries(active.devices)) {
590
- if (dc.description && dc.description.trim().toLowerCase() === target)
595
+ if (isEnabled(dc) && dc.description && dc.description.trim().toLowerCase() === target)
591
596
  return key;
592
597
  }
593
598
  return;
@@ -596,6 +601,8 @@ function deviceLabels() {
596
601
  const seen = new Set;
597
602
  const out = [];
598
603
  for (const [key, dc] of Object.entries(active.devices)) {
604
+ if (!isEnabled(dc))
605
+ continue;
599
606
  const label = dc.description?.trim();
600
607
  if (label && label !== key && !(label in active.devices) && !seen.has(label)) {
601
608
  seen.add(label);
@@ -606,13 +613,17 @@ function deviceLabels() {
606
613
  }
607
614
  function resolveDeviceName(name) {
608
615
  if (name) {
609
- if (name in active.devices)
616
+ if (name in active.devices && isEnabled(active.devices[name]))
610
617
  return name;
611
618
  const byLabel = deviceKeyForLabel(name);
612
619
  if (byLabel)
613
620
  return byLabel;
614
621
  }
615
- return active.defaultDevice in active.devices ? active.defaultDevice : Object.keys(active.devices)[0] ?? active.defaultDevice;
622
+ if (active.defaultDevice in active.devices && isEnabled(active.devices[active.defaultDevice])) {
623
+ return active.defaultDevice;
624
+ }
625
+ const firstEnabled = Object.entries(active.devices).find(([, dc]) => isEnabled(dc));
626
+ return firstEnabled ? firstEnabled[0] : active.defaultDevice;
616
627
  }
617
628
  function deviceTarget(dc) {
618
629
  if (!dc)
@@ -620,7 +631,7 @@ function deviceTarget(dc) {
620
631
  return dc.mac ? `MAC ${dc.mac}` : `${dc.host}:${dc.port ?? 22}`;
621
632
  }
622
633
  function deviceDirectory() {
623
- return Object.entries(active.devices).map(([key, dc]) => ({
634
+ return Object.entries(active.devices).filter(([, dc]) => isEnabled(dc)).map(([key, dc]) => ({
624
635
  key,
625
636
  label: dc.description?.trim() || undefined,
626
637
  target: deviceTarget(dc),
@@ -640,6 +651,9 @@ function getDevice(name) {
640
651
  const dc = active.devices[key];
641
652
  if (!dc)
642
653
  throw new Error(`No device configuration available for '${key}'.`);
654
+ if (dc.disabled) {
655
+ throw new Error(`Device '${key}' is disabled. Enable it from the dashboard or config file.`);
656
+ }
643
657
  return dc;
644
658
  }
645
659
 
@@ -1743,7 +1757,7 @@ function parseKvTokens(chunk) {
1743
1757
  return out;
1744
1758
  }
1745
1759
  var INDEX_LINE = /^\s*(\d+)\s+(.*)$/;
1746
- var LEADING_FLAGS = /^([A-Z]+)(?=\s|$)/;
1760
+ var LEADING_FLAGS = /^([A-Z][A-Za-z]*)(?=\s|$)/;
1747
1761
  function unionColumns(rows) {
1748
1762
  const seen = new Set;
1749
1763
  const cols = [];
@@ -8243,7 +8257,7 @@ var cache = null;
8243
8257
  async function gateway() {
8244
8258
  if (cache)
8245
8259
  return cache;
8246
- const { moduleCatalog } = await import("./library-84y0nvhr.js");
8260
+ const { moduleCatalog } = await import("./library-jve65pzw.js");
8247
8261
  const forIndex = [];
8248
8262
  const byName = new Map;
8249
8263
  for (const mod of moduleCatalog) {
@@ -9268,10 +9282,44 @@ function covers(key, aVal, bVal) {
9268
9282
  return cidrContains(aVal, bVal);
9269
9283
  return false;
9270
9284
  }
9285
+ var INTERFACE_LIST_PAIRS = [
9286
+ ["in-interface-list", "in-interface"],
9287
+ ["out-interface-list", "out-interface"]
9288
+ ];
9289
+ var _ifaceLists;
9290
+ function interfaceListCovers(aKey, aVal, bKey, bVal) {
9291
+ if (!_ifaceLists)
9292
+ return;
9293
+ for (const [listKey, ifaceKey] of INTERFACE_LIST_PAIRS) {
9294
+ if (aKey === listKey && bKey === ifaceKey) {
9295
+ const negated = aVal.startsWith("!");
9296
+ const listName = negated ? aVal.slice(1) : aVal;
9297
+ const members = _ifaceLists.get(listName);
9298
+ if (!members)
9299
+ return;
9300
+ const isMember = members.has(bVal);
9301
+ return negated ? !isMember : isMember;
9302
+ }
9303
+ }
9304
+ return;
9305
+ }
9271
9306
  function aCoversB(a, b) {
9272
9307
  for (const [k, v] of Object.entries(a.match)) {
9273
9308
  const bv = b.match[k];
9274
- if (bv === undefined || !covers(k, v, bv))
9309
+ if (bv !== undefined) {
9310
+ if (!covers(k, v, bv))
9311
+ return false;
9312
+ continue;
9313
+ }
9314
+ let crossCovered = false;
9315
+ for (const [bk, bval] of Object.entries(b.match)) {
9316
+ const result = interfaceListCovers(k, v, bk, bval);
9317
+ if (result === true) {
9318
+ crossCovered = true;
9319
+ break;
9320
+ }
9321
+ }
9322
+ if (!crossCovered)
9275
9323
  return false;
9276
9324
  }
9277
9325
  return true;
@@ -9448,6 +9496,7 @@ function grade2(score) {
9448
9496
  return "critical";
9449
9497
  }
9450
9498
  function auditFirewall(input) {
9499
+ _ifaceLists = input.interfaceLists;
9451
9500
  const findings = [];
9452
9501
  if (input.filter)
9453
9502
  findings.push(...auditFilter(input.filter));
@@ -9496,6 +9545,25 @@ async function fetchRules(path, ctx) {
9496
9545
  return [];
9497
9546
  return parseRecords(out).rows;
9498
9547
  }
9548
+ async function fetchInterfaceListMembers(ctx) {
9549
+ const out = await executeMikrotikCommand("/interface list member print detail", ctx);
9550
+ const members = new Map;
9551
+ if (looksLikeError(out) || isEmpty(out))
9552
+ return members;
9553
+ for (const row of parseRecords(out).rows) {
9554
+ const list = row.list;
9555
+ const iface = row.interface;
9556
+ if (!list || !iface)
9557
+ continue;
9558
+ let set = members.get(list);
9559
+ if (!set) {
9560
+ set = new Set;
9561
+ members.set(list, set);
9562
+ }
9563
+ set.add(iface);
9564
+ }
9565
+ return members;
9566
+ }
9499
9567
  var firewallAuditTools = [
9500
9568
  defineTool({
9501
9569
  name: "firewall_audit",
@@ -9510,10 +9578,16 @@ var firewallAuditTools = [
9510
9578
  async handler(a, ctx) {
9511
9579
  const device = resolveDeviceName(ctx.device);
9512
9580
  ctx.info(`Auditing firewall for '${device}'`);
9513
- const filter = rulesFromRows(await fetchRules("/ip firewall filter", ctx));
9514
- const nat = a.include_nat ? rulesFromRows(await fetchRules("/ip firewall nat", ctx)) : undefined;
9515
- const mangle = a.include_mangle ? rulesFromRows(await fetchRules("/ip firewall mangle", ctx)) : undefined;
9516
- const report = auditFirewall({ filter, nat, mangle });
9581
+ const [filterRows, natRows, mangleRows, interfaceLists] = await Promise.all([
9582
+ fetchRules("/ip firewall filter", ctx),
9583
+ a.include_nat ? fetchRules("/ip firewall nat", ctx) : Promise.resolve(undefined),
9584
+ a.include_mangle ? fetchRules("/ip firewall mangle", ctx) : Promise.resolve(undefined),
9585
+ fetchInterfaceListMembers(ctx)
9586
+ ]);
9587
+ const filter = rulesFromRows(filterRows);
9588
+ const nat = natRows ? rulesFromRows(natRows) : undefined;
9589
+ const mangle = mangleRows ? rulesFromRows(mangleRows) : undefined;
9590
+ const report = auditFirewall({ filter, nat, mangle, interfaceLists });
9517
9591
  const structuredContent = {
9518
9592
  __mikrotikView: "firewall-audit",
9519
9593
  device,
@@ -9978,6 +10052,22 @@ Management access may be partially restricted \u2014 review immediately.`;
9978
10052
  // src/tools/firewall-filter.ts
9979
10053
  import { z as z28 } from "zod";
9980
10054
  var isDigits = (s) => /^\d+$/.test(s);
10055
+ async function resolveFilterRuleId(ruleId, ctx) {
10056
+ if (/^\d+$/.test(ruleId)) {
10057
+ const id = `*${ruleId}`;
10058
+ const byId = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10059
+ if (byId.trim() !== "0")
10060
+ return id;
10061
+ const idsRaw = await executeMikrotikCommand(`:foreach i in=[/ip firewall filter find] do={:put $i}`, ctx);
10062
+ if (isEmpty(idsRaw))
10063
+ return null;
10064
+ const ids = idsRaw.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
10065
+ const pos = Number.parseInt(ruleId, 10);
10066
+ return pos >= 0 && pos < ids.length ? ids[pos] : null;
10067
+ }
10068
+ const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${ruleId}`, ctx);
10069
+ return count.trim() !== "0" ? ruleId : null;
10070
+ }
9981
10071
  async function updateFilterRule(a, ctx) {
9982
10072
  ctx.info(`Updating firewall filter rule: rule_id=${a.rule_id}`);
9983
10073
  const updates = [];
@@ -10046,7 +10136,9 @@ async function updateFilterRule(a, ctx) {
10046
10136
  }
10047
10137
  if (updates.length === 0)
10048
10138
  return "No updates specified.";
10049
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10139
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10140
+ if (!id)
10141
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10050
10142
  const cmd = `/ip firewall filter set ${id} ${updates.join(" ")}`;
10051
10143
  const result = await executeMikrotikCommand(cmd, ctx);
10052
10144
  if (looksLikeError(result))
@@ -10190,7 +10282,7 @@ ${details}`;
10190
10282
  filters.push("invalid=yes");
10191
10283
  if (a.dynamic_only)
10192
10284
  filters.push("dynamic=yes");
10193
- const result = await executeMikrotikCommand(`/ip firewall filter print${whereClause(filters)}`, ctx);
10285
+ const result = await executeMikrotikCommand(`/ip firewall filter print detail${whereClause(filters)}`, ctx);
10194
10286
  return isEmpty(result) ? "No firewall filter rules found matching the criteria." : `FIREWALL FILTER RULES:
10195
10287
 
10196
10288
  ${result}`;
@@ -10200,15 +10292,17 @@ ${result}`;
10200
10292
  name: "get_filter_rule",
10201
10293
  title: "Get Firewall Filter Rule",
10202
10294
  annotations: READ,
10203
- description: "Gets the full detail of one IPv4 firewall FILTER rule (`/ip firewall filter`) by id \u2014 " + "every matcher, action, counter and flag. For IPv6 use get_ipv6_filter_rule. " + 'rule_id: the `.id` from list_filter_rules e.g. "*1" or "0".',
10295
+ description: "Gets the full detail of one IPv4 firewall FILTER rule (`/ip firewall filter`) by id \u2014 " + "every matcher, action, counter and flag. For IPv6 use get_ipv6_filter_rule. " + 'rule_id: preferably the `.id` from list_filter_rules e.g. "*1F". A bare number like "3" ' + "is tried as `.id=*3` first, then as the positional row index if no `.id` matches.",
10204
10296
  inputSchema: {
10205
- rule_id: z28.string().describe('Rule ID from list output e.g. "*1" or "0"')
10297
+ rule_id: z28.string().describe('Rule .id e.g. "*1F", or bare position number e.g. "3"')
10206
10298
  },
10207
10299
  async handler(a, ctx) {
10208
10300
  ctx.info(`Getting firewall filter rule details: rule_id=${a.rule_id}`);
10209
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10301
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10302
+ if (!id)
10303
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10210
10304
  const result = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
10211
- return isEmpty(result) ? `Firewall filter rule with ID '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10305
+ return isEmpty(result) ? `Firewall filter rule '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10212
10306
 
10213
10307
  ${result}`;
10214
10308
  }
@@ -10283,14 +10377,13 @@ ${result}`;
10283
10377
  inputSchema: { rule_id: z28.string() },
10284
10378
  async handler(a, ctx) {
10285
10379
  ctx.info(`Removing firewall filter rule: rule_id=${a.rule_id}`);
10286
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10287
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10288
- if (count.trim() === "0")
10289
- return `Firewall filter rule with ID '${a.rule_id}' not found.`;
10380
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10381
+ if (!id)
10382
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10290
10383
  const result = await executeMikrotikCommand(`/ip firewall filter remove ${id}`, ctx);
10291
10384
  if (looksLikeError(result))
10292
10385
  return `Failed to remove firewall filter rule: ${result}`;
10293
- return `Firewall filter rule with ID '${a.rule_id}' removed successfully.`;
10386
+ return `Firewall filter rule '${a.rule_id}' (${id}) removed successfully.`;
10294
10387
  }
10295
10388
  }),
10296
10389
  defineTool({
@@ -10304,14 +10397,13 @@ ${result}`;
10304
10397
  },
10305
10398
  async handler(a, ctx) {
10306
10399
  ctx.info(`Moving firewall filter rule: rule_id=${a.rule_id} to position ${a.destination}`);
10307
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10308
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10309
- if (count.trim() === "0")
10310
- return `Firewall filter rule with ID '${a.rule_id}' not found.`;
10400
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10401
+ if (!id)
10402
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10311
10403
  const result = await executeMikrotikCommand(`/ip firewall filter move ${id} destination=${a.destination}`, ctx);
10312
10404
  if (looksLikeError(result))
10313
10405
  return `Failed to move firewall filter rule: ${result}`;
10314
- return `Firewall filter rule with ID '${a.rule_id}' moved to position ${a.destination}.`;
10406
+ return `Firewall filter rule '${a.rule_id}' (${id}) moved to position ${a.destination}.`;
10315
10407
  }
10316
10408
  }),
10317
10409
  defineTool({
@@ -20587,13 +20679,27 @@ function analyzeRootCause(data) {
20587
20679
  }
20588
20680
  }
20589
20681
  if (downInterfaces.length === 0 && errorInterfaces.length === 0) {
20682
+ if (data.interfaces.length === 0) {
20683
+ evidence.push({
20684
+ dimension: "interfaces",
20685
+ severity: "warning",
20686
+ summary: "Interface data unavailable \u2014 the device returned no parseable interface records. " + "Run `/interface print detail` manually to verify."
20687
+ });
20688
+ } else {
20689
+ evidence.push({
20690
+ dimension: "interfaces",
20691
+ severity: "ok",
20692
+ summary: `All ${data.interfaces.length} interfaces healthy`
20693
+ });
20694
+ }
20695
+ }
20696
+ if (data.routeCount === 0) {
20590
20697
  evidence.push({
20591
- dimension: "interfaces",
20592
- severity: "ok",
20593
- summary: `All ${data.interfaces.length} interfaces healthy`
20698
+ dimension: "routing",
20699
+ severity: "warning",
20700
+ summary: "Route data unavailable \u2014 the device returned no parseable route records. " + "Run `/ip route print detail` manually to verify."
20594
20701
  });
20595
- }
20596
- if (!data.defaultRouteExists) {
20702
+ } else if (!data.defaultRouteExists) {
20597
20703
  evidence.push({
20598
20704
  dimension: "routing",
20599
20705
  severity: "critical",
@@ -20606,7 +20712,7 @@ function analyzeRootCause(data) {
20606
20712
  summary: `Default route present, ${data.routeCount} total routes`
20607
20713
  });
20608
20714
  }
20609
- const ospfDown = data.ospfNeighbors.filter((n) => n.state.toLowerCase() !== "full");
20715
+ const ospfDown = data.ospfNeighbors.filter((n) => (n.id || n.address) && n.state.toLowerCase() !== "full");
20610
20716
  for (const n of ospfDown) {
20611
20717
  evidence.push({
20612
20718
  dimension: "routing",
@@ -20615,7 +20721,7 @@ function analyzeRootCause(data) {
20615
20721
  reference: n.id
20616
20722
  });
20617
20723
  }
20618
- const bgpDown = data.bgpPeers.filter((p) => !p.state.toLowerCase().includes("established"));
20724
+ const bgpDown = data.bgpPeers.filter((p) => (p.id || p.address) && !p.state.toLowerCase().includes("established"));
20619
20725
  for (const p of bgpDown) {
20620
20726
  evidence.push({
20621
20727
  dimension: "routing",
@@ -20718,7 +20824,13 @@ function analyzeRootCause(data) {
20718
20824
  summary: "No DNS servers configured"
20719
20825
  });
20720
20826
  }
20721
- if (data.cpuLoad > 90) {
20827
+ if (data.cpuLoad === 0 && data.memoryUsedPct === 0 && !data.rosVersion && !data.uptime) {
20828
+ evidence.push({
20829
+ dimension: "resources",
20830
+ severity: "warning",
20831
+ summary: "System resource data unavailable \u2014 CPU 0%, memory 0%, no version or uptime. " + "The `/system resource print` output may not have been parsed correctly."
20832
+ });
20833
+ } else if (data.cpuLoad > 90) {
20722
20834
  evidence.push({
20723
20835
  dimension: "resources",
20724
20836
  severity: "critical",
@@ -20737,24 +20849,26 @@ function analyzeRootCause(data) {
20737
20849
  summary: `CPU load: ${data.cpuLoad}%`
20738
20850
  });
20739
20851
  }
20740
- if (data.memoryUsedPct > 90) {
20741
- evidence.push({
20742
- dimension: "resources",
20743
- severity: "critical",
20744
- summary: `Memory critically low: ${data.memoryUsedPct}% used`
20745
- });
20746
- } else if (data.memoryUsedPct > 75) {
20747
- evidence.push({
20748
- dimension: "resources",
20749
- severity: "warning",
20750
- summary: `Memory pressure: ${data.memoryUsedPct}% used`
20751
- });
20752
- } else {
20753
- evidence.push({
20754
- dimension: "resources",
20755
- severity: "ok",
20756
- summary: `Memory: ${data.memoryUsedPct}% used`
20757
- });
20852
+ if (data.rosVersion || data.uptime || data.cpuLoad > 0 || data.memoryUsedPct > 0) {
20853
+ if (data.memoryUsedPct > 90) {
20854
+ evidence.push({
20855
+ dimension: "resources",
20856
+ severity: "critical",
20857
+ summary: `Memory critically low: ${data.memoryUsedPct}% used`
20858
+ });
20859
+ } else if (data.memoryUsedPct > 75) {
20860
+ evidence.push({
20861
+ dimension: "resources",
20862
+ severity: "warning",
20863
+ summary: `Memory pressure: ${data.memoryUsedPct}% used`
20864
+ });
20865
+ } else {
20866
+ evidence.push({
20867
+ dimension: "resources",
20868
+ severity: "ok",
20869
+ summary: `Memory: ${data.memoryUsedPct}% used`
20870
+ });
20871
+ }
20758
20872
  }
20759
20873
  const errorLogs = data.relevantLogs.filter((l) => l.topics.includes("error") || l.topics.includes("critical") || l.topics.includes("warning"));
20760
20874
  const firewallLogs = data.relevantLogs.filter((l) => l.topics.includes("firewall"));
@@ -20791,14 +20905,24 @@ function analyzeRootCause(data) {
20791
20905
  summary: "No concerning log entries in the last 10 minutes"
20792
20906
  });
20793
20907
  }
20908
+ const isServerBinding = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
20794
20909
  const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
20795
20910
  for (const t of downTunnels) {
20796
- evidence.push({
20797
- dimension: "vpn",
20798
- severity: "critical",
20799
- summary: `Tunnel ${t.name} (${t.type}) is down`,
20800
- reference: t.name
20801
- });
20911
+ if (isServerBinding(t)) {
20912
+ evidence.push({
20913
+ dimension: "vpn",
20914
+ severity: "info",
20915
+ summary: `Server binding ${t.name} (${t.type}) is idle \u2014 no active client session`,
20916
+ reference: t.name
20917
+ });
20918
+ } else {
20919
+ evidence.push({
20920
+ dimension: "vpn",
20921
+ severity: "critical",
20922
+ summary: `Tunnel ${t.name} (${t.type}) is down`,
20923
+ reference: t.name
20924
+ });
20925
+ }
20802
20926
  }
20803
20927
  if (data.tunnelInterfaces.length > 0 && downTunnels.length === 0) {
20804
20928
  evidence.push({
@@ -20808,6 +20932,19 @@ function analyzeRootCause(data) {
20808
20932
  });
20809
20933
  }
20810
20934
  correlateRootCauses(data, evidence, rootCauses);
20935
+ if (data.ping && data.ping.lossPct === 0) {
20936
+ const contradicted = new Set([
20937
+ "Missing default route",
20938
+ "Missing source NAT / masquerade",
20939
+ "Interface link failure"
20940
+ ]);
20941
+ for (const rc of rootCauses) {
20942
+ if (contradicted.has(rc.cause) && rc.confidence !== "low") {
20943
+ rc.confidence = "low";
20944
+ rc.explanation += " (Note: ping to the target succeeded with 0% loss, which contradicts this hypothesis.)";
20945
+ }
20946
+ }
20947
+ }
20811
20948
  rootCauses.sort((a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence]);
20812
20949
  const dimensionSummary = ALL_DIMENSIONS.map((dim) => {
20813
20950
  const dimEvidence = evidence.filter((e) => e.dimension === dim);
@@ -20946,14 +21083,15 @@ function correlateRootCauses(data, evidence, causes) {
20946
21083
  });
20947
21084
  }
20948
21085
  }
20949
- const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
20950
- if (downTunnels.length > 0) {
21086
+ const isServerTunnel = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
21087
+ const realDownTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled && !isServerTunnel(t));
21088
+ if (realDownTunnels.length > 0) {
20951
21089
  causes.push({
20952
21090
  cause: "VPN/tunnel interface down",
20953
- explanation: `Tunnel(s) ${downTunnels.map((t) => `${t.name} (${t.type})`).join(", ")} are down. ` + "Traffic destined for remote networks over these tunnels will be black-holed.",
21091
+ explanation: `Tunnel(s) ${realDownTunnels.map((t) => `${t.name} (${t.type})`).join(", ")} are down. ` + "Traffic destined for remote networks over these tunnels will be black-holed.",
20954
21092
  confidence: "high",
20955
21093
  evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "critical"),
20956
- fixes: downTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
21094
+ fixes: realDownTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
20957
21095
  dimensions: ["vpn"]
20958
21096
  });
20959
21097
  }
@@ -21182,7 +21320,7 @@ async function collectRoutes(ctx) {
21182
21320
  }
21183
21321
  async function collectOspfNeighbors(ctx) {
21184
21322
  const raw = await safe("/routing ospf neighbor print detail", ctx);
21185
- if (!raw)
21323
+ if (!raw || isEmpty(raw))
21186
21324
  return [];
21187
21325
  return parseRecords(raw).rows.map((r) => ({
21188
21326
  id: r["neighbor-id"] ?? r.router ?? "",
@@ -21190,13 +21328,13 @@ async function collectOspfNeighbors(ctx) {
21190
21328
  state: r.state ?? "",
21191
21329
  interface: r.interface ?? "",
21192
21330
  uptime: r.uptime
21193
- }));
21331
+ })).filter((n) => n.id || n.address);
21194
21332
  }
21195
21333
  async function collectBgpPeers(ctx) {
21196
21334
  const raw = await safe("/routing bgp session print detail", ctx);
21197
- if (!raw) {
21335
+ if (!raw || isEmpty(raw)) {
21198
21336
  const raw2 = await safe("/routing bgp peer print detail", ctx);
21199
- if (!raw2)
21337
+ if (!raw2 || isEmpty(raw2))
21200
21338
  return [];
21201
21339
  return parseRecords(raw2).rows.map((r) => ({
21202
21340
  id: r.name ?? "",
@@ -21204,7 +21342,7 @@ async function collectBgpPeers(ctx) {
21204
21342
  state: r.state ?? "",
21205
21343
  interface: r.interface ?? "",
21206
21344
  uptime: r.uptime
21207
- }));
21345
+ })).filter((p) => p.id || p.address);
21208
21346
  }
21209
21347
  return parseRecords(raw).rows.map((r) => ({
21210
21348
  id: r.name ?? r["remote.address"] ?? "",
@@ -21212,7 +21350,7 @@ async function collectBgpPeers(ctx) {
21212
21350
  state: r.state ?? r.established ?? "",
21213
21351
  interface: r.interface ?? "",
21214
21352
  uptime: r.uptime
21215
- }));
21353
+ })).filter((p) => p.id || p.address);
21216
21354
  }
21217
21355
  async function collectFirewallRules(target, ctx) {
21218
21356
  const raw = await safe("/ip firewall filter print detail", ctx);
@@ -21312,7 +21450,7 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21312
21450
  arpEntries,
21313
21451
  dhcpLeases,
21314
21452
  dnsResult,
21315
- dnsSettingsRaw,
21453
+ dnsSettingsParts,
21316
21454
  resourceRaw,
21317
21455
  logs,
21318
21456
  tunnels
@@ -21328,13 +21466,17 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21328
21466
  dims.has("arp_dhcp") ? collectArp(ctx) : Promise.resolve([]),
21329
21467
  dims.has("arp_dhcp") ? collectDhcpLeases(ctx) : Promise.resolve([]),
21330
21468
  dims.has("dns") && !isIpLike(target) ? safe(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
21331
- dims.has("dns") ? safe("/ip dns print", ctx) : Promise.resolve(""),
21469
+ dims.has("dns") ? Promise.all([
21470
+ safe(":put [/ip dns get servers]", ctx),
21471
+ safe(":put [/ip dns get dynamic-servers]", ctx),
21472
+ safe(":put [/ip dns get allow-remote-requests]", ctx)
21473
+ ]) : Promise.resolve(["", "", ""]),
21332
21474
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
21333
21475
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
21334
21476
  dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
21335
21477
  ]);
21336
21478
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
21337
- const dnsKv = parseKeyValues(dnsSettingsRaw);
21479
+ const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
21338
21480
  const resKv = parseKeyValues(resourceRaw);
21339
21481
  const totalMem = parseSize(resKv["total-memory"]) ?? 0;
21340
21482
  const freeMem = parseSize(resKv["free-memory"]) ?? 0;
@@ -21356,8 +21498,8 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21356
21498
  arpEntries,
21357
21499
  dhcpLeases,
21358
21500
  dnsResolveResult: dnsResult ?? undefined,
21359
- dnsServers: [dnsKv.servers, dnsKv["dynamic-servers"]].filter(Boolean).join(",") || "",
21360
- dnsAllowRemote: (dnsKv["allow-remote-requests"] ?? "").toLowerCase() === "yes",
21501
+ dnsServers: [dnsServersRaw.trim(), dnsDynamicRaw.trim()].filter(Boolean).join(",") || "",
21502
+ dnsAllowRemote: dnsAllowRemoteRaw.trim().toLowerCase() === "yes" || dnsAllowRemoteRaw.trim().toLowerCase() === "true",
21361
21503
  cpuLoad: parsePercent(resKv["cpu-load"]) ?? 0,
21362
21504
  memoryUsedPct: memUsedPct,
21363
21505
  uptime: resKv.uptime ?? "",