@usex/mikrotik-mcp 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -230,7 +230,7 @@ import {
230
230
  validatePolicyText,
231
231
  worstSeverity,
232
232
  writeBackup
233
- } from "./shared/cli-mqb3rqfy.js";
233
+ } from "./shared/cli-54yv0kd3.js";
234
234
 
235
235
  // src/cli.ts
236
236
  import { existsSync as existsSync2 } from "fs";
@@ -3267,6 +3267,11 @@ function configPayload() {
3267
3267
  s3: cfg.s3,
3268
3268
  dashboard: cfg.dashboard,
3269
3269
  ssh: cfg.ssh,
3270
+ alerts: cfg.alerts,
3271
+ flows: cfg.flows,
3272
+ policy: cfg.policy,
3273
+ schedules: cfg.schedules,
3274
+ attacks: cfg.attacks,
3270
3275
  readOnly: cfg.readOnly,
3271
3276
  tools: cfg.tools,
3272
3277
  memory: cfg.memory,
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  selectToolModules,
30
30
  setConfig,
31
31
  updateSummaryLine
32
- } from "./shared/library-tq8phch8.js";
32
+ } from "./shared/library-s7s3s14v.js";
33
33
  // src/server.ts
34
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
35
35
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -10832,7 +10832,7 @@ var cache2 = null;
10832
10832
  async function gateway() {
10833
10833
  if (cache2)
10834
10834
  return cache2;
10835
- const { moduleCatalog } = await import("./cli-s9fc4c7j.js");
10835
+ const { moduleCatalog } = await import("./cli-f80vccvs.js");
10836
10836
  const forIndex = [];
10837
10837
  const byName = new Map;
10838
10838
  for (const mod of moduleCatalog) {
@@ -28428,6 +28428,8 @@ function analyzeRootCause(data) {
28428
28428
  summary: `All ${data.tunnelInterfaces.length} tunnel(s) running`
28429
28429
  });
28430
28430
  }
28431
+ const mtuFindings = analyzeTunnelMtu(data);
28432
+ evidence.push(...mtuFindings.evidence);
28431
28433
  correlateRootCauses(data, evidence, rootCauses);
28432
28434
  if (data.ping && data.ping.lossPct === 0) {
28433
28435
  const contradicted = new Set([
@@ -28462,6 +28464,105 @@ function analyzeRootCause(data) {
28462
28464
  dimensionSummary
28463
28465
  };
28464
28466
  }
28467
+ var TUNNEL_OVERHEAD = [
28468
+ { match: "gre", bytes: 24, label: "GRE", path: "/interface gre" },
28469
+ { match: "ipip", bytes: 20, label: "IPIP", path: "/interface ipip" },
28470
+ { match: "eoip", bytes: 42, label: "EoIP", path: "/interface eoip" },
28471
+ { match: "vxlan", bytes: 50, label: "VXLAN", path: "/interface vxlan" },
28472
+ { match: "wg", bytes: 60, label: "WireGuard", path: "/interface wireguard" },
28473
+ { match: "l2tp", bytes: 100, label: "L2TP" },
28474
+ { match: "pptp", bytes: 100, label: "PPTP" },
28475
+ { match: "sstp", bytes: 100, label: "SSTP" },
28476
+ { match: "ovpn", bytes: 100, label: "OpenVPN" }
28477
+ ];
28478
+ var PPP_FAMILY = /l2tp|pptp|sstp|ovpn|pppoe/;
28479
+ function tunnelOverhead(type) {
28480
+ const t = type.toLowerCase();
28481
+ return TUNNEL_OVERHEAD.find((o) => t.includes(o.match));
28482
+ }
28483
+ function analyzeTunnelMtu(data) {
28484
+ const evidence = [];
28485
+ const unclamped = [];
28486
+ const oversizedMtu = [];
28487
+ const tunnels = data.tunnelInterfaces.filter((t) => !t.disabled);
28488
+ const clampRules = data.mangleRules.filter((r) => r.action === "change-mss" && !r.disabled);
28489
+ const globalClamp = clampRules.some((r) => r.chain === "forward" || r.chain === "postrouting" || r.chain === "output");
28490
+ const strandedClamp = clampRules.length > 0 && !globalClamp;
28491
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28492
+ const pathBudget = pppoeWan ? 1492 : 1500;
28493
+ const pppClampedEverywhere = data.pppProfilesMissingMssClamp.length === 0;
28494
+ for (const t of tunnels) {
28495
+ const overhead = tunnelOverhead(t.type);
28496
+ if (!overhead)
28497
+ continue;
28498
+ const effective = t.actualMtu ?? t.mtu;
28499
+ const budget = pathBudget - overhead.bytes;
28500
+ if (effective > budget) {
28501
+ oversizedMtu.push({ iface: t, effective, budget, label: overhead.label });
28502
+ evidence.push({
28503
+ dimension: "vpn",
28504
+ severity: "warning",
28505
+ summary: `Tunnel ${t.name} MTU ${effective} exceeds the ${budget}-byte budget for ` + `${overhead.label} (${overhead.bytes} B overhead${pppoeWan ? " + PPPoE 8 B" : ""})`,
28506
+ detail: "Packets at this size need fragmentation the transit path will not perform, " + "so they are dropped silently.",
28507
+ reference: t.name
28508
+ });
28509
+ }
28510
+ const covered = t.clampTcpMss === true || globalClamp || PPP_FAMILY.test(t.type.toLowerCase()) && pppClampedEverywhere;
28511
+ if (!covered) {
28512
+ unclamped.push(t);
28513
+ evidence.push({
28514
+ dimension: "vpn",
28515
+ severity: "warning",
28516
+ summary: `Tunnel ${t.name} (${overhead.label}) has no TCP MSS clamp`,
28517
+ detail: "Hosts derive their MSS from their own LAN MTU and set DF, so full-size " + "segments enter the tunnel and are dropped in transit. Small flows (ping, " + "logins) work; large ones (file transfer, photo/video upload, TLS) stall.",
28518
+ reference: t.name
28519
+ });
28520
+ }
28521
+ }
28522
+ if (strandedClamp) {
28523
+ evidence.push({
28524
+ dimension: "vpn",
28525
+ severity: "warning",
28526
+ summary: `A change-mss rule exists but only in chain '${clampRules[0].chain}' \u2014 ` + "transit traffic is clamped in forward/postrouting, not there"
28527
+ });
28528
+ }
28529
+ if (data.pppProfilesMissingMssClamp.length > 0 && tunnels.some((t) => PPP_FAMILY.test(t.type))) {
28530
+ evidence.push({
28531
+ dimension: "vpn",
28532
+ severity: "warning",
28533
+ summary: `PPP profile(s) without change-tcp-mss: ${data.pppProfilesMissingMssClamp.join(", ")}`,
28534
+ detail: "PPP-family tunnels (L2TP/PPTP/SSTP/OpenVPN/PPPoE) clamp MSS through their " + "profile. Sessions using these profiles are unprotected."
28535
+ });
28536
+ }
28537
+ const keepaliveOff = data.wireguardPeers.filter((p) => !p.disabled && p.persistentKeepalive <= 0);
28538
+ if (keepaliveOff.length > 0) {
28539
+ evidence.push({
28540
+ dimension: "vpn",
28541
+ severity: "warning",
28542
+ summary: `${keepaliveOff.length} WireGuard peer(s) have persistent-keepalive off`,
28543
+ detail: "NAT and stateful firewalls drop idle UDP mappings after ~30 s. Without " + "keepalive the peer becomes unreachable from this side until it transmits.",
28544
+ reference: keepaliveOff[0].interface
28545
+ });
28546
+ }
28547
+ if (tunnels.length > 0 && unclamped.length === 0 && oversizedMtu.length === 0 && keepaliveOff.length === 0) {
28548
+ evidence.push({
28549
+ dimension: "vpn",
28550
+ severity: "ok",
28551
+ summary: `MTU/MSS sane on all ${tunnels.length} tunnel(s)`
28552
+ });
28553
+ }
28554
+ return { evidence, unclamped, oversizedMtu, keepaliveOff };
28555
+ }
28556
+ function suggestedMtu(type, pppoeWan) {
28557
+ const o = tunnelOverhead(type);
28558
+ return o ? (pppoeWan ? 1492 : 1500) - o.bytes : undefined;
28559
+ }
28560
+ function mtuFixCommand(iface, mtu) {
28561
+ const o = tunnelOverhead(iface.type);
28562
+ if (o?.path)
28563
+ return `${o.path} set [find name="${iface.name}"] mtu=${mtu}`;
28564
+ return `# ${iface.name} (${o?.label ?? iface.type}): set max-mtu=${mtu} max-mru=${mtu} on the ` + "client interface, or on the server settings for an inbound session";
28565
+ }
28465
28566
  function correlateRootCauses(data, evidence, causes) {
28466
28567
  const criticals = evidence.filter((e) => e.severity === "critical");
28467
28568
  const warnings = evidence.filter((e) => e.severity === "warning");
@@ -28592,6 +28693,41 @@ function correlateRootCauses(data, evidence, causes) {
28592
28693
  dimensions: ["vpn"]
28593
28694
  });
28594
28695
  }
28696
+ const mtu = analyzeTunnelMtu(data);
28697
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28698
+ if (mtu.unclamped.length > 0 || mtu.oversizedMtu.length > 0) {
28699
+ const names = [
28700
+ ...new Set([...mtu.unclamped, ...mtu.oversizedMtu.map((o) => o.iface)].map((t) => t.name))
28701
+ ];
28702
+ const bothProblems = mtu.unclamped.length > 0 && mtu.oversizedMtu.length > 0;
28703
+ const pingOk = data.ping?.lossPct === 0;
28704
+ causes.push({
28705
+ cause: "MTU/MSS black hole on tunnel",
28706
+ explanation: `Tunnel(s) ${names.join(", ")} carry traffic larger than the path can deliver. ` + "Encapsulation shrinks the usable MTU, but hosts still negotiate a TCP MSS from " + "their own LAN MTU and set the don't-fragment bit; transit routers drop the " + "oversized packets and the ICMP 'fragmentation needed' reply is commonly filtered, " + "so the sender never learns to back off. The result is a tunnel that pings clean " + "and passes logins while file transfers, media uploads and some HTTPS sites hang.",
28707
+ confidence: bothProblems || pingOk ? "high" : "medium",
28708
+ evidence: mtu.evidence.filter((e) => e.severity === "warning"),
28709
+ fixes: [
28710
+ ...mtu.oversizedMtu.map((o) => mtuFixCommand(o.iface, suggestedMtu(o.iface.type, pppoeWan) ?? o.budget)),
28711
+ ...mtu.unclamped.length > 0 ? [
28712
+ "# Clamp TCP MSS for every forwarded flow (covers all tunnel types):",
28713
+ "/ip firewall mangle add chain=forward protocol=tcp tcp-flags=syn " + 'tcp-mss=1400-65535 action=change-mss new-mss=clamp-to-pmtu comment="clamp MSS to PMTU"',
28714
+ "# PPP-family tunnels (L2TP/PPTP/SSTP/OVPN) can clamp via their profile instead:",
28715
+ "/ppp profile set [find] change-tcp-mss=yes"
28716
+ ] : []
28717
+ ],
28718
+ dimensions: ["vpn", "firewall"]
28719
+ });
28720
+ }
28721
+ if (mtu.keepaliveOff.length > 0) {
28722
+ causes.push({
28723
+ cause: "WireGuard peer unreachable after idle (no persistent-keepalive)",
28724
+ explanation: `${mtu.keepaliveOff.length} enabled peer(s) have persistent-keepalive off: ` + `${mtu.keepaliveOff.map((p) => `${p.interface}/${p.publicKey.slice(0, 12)}\u2026`).join(", ")}. ` + "WireGuard is silent when idle, so the NAT/firewall mapping the peer punched " + "expires (typically ~30 s) and this side can no longer initiate. The tunnel " + "appears to work whenever the peer starts the traffic and to be down otherwise.",
28725
+ confidence: "medium",
28726
+ evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "warning"),
28727
+ fixes: mtu.keepaliveOff.map((p) => `/interface wireguard peers set [find public-key="${p.publicKey}"] persistent-keepalive=25s`),
28728
+ dimensions: ["vpn"]
28729
+ });
28730
+ }
28595
28731
  const flapping = data.interfaces.filter((i) => i.linkDowns > 5 && i.running);
28596
28732
  if (flapping.length > 0 && data.ping && data.ping.lossPct > 0 && data.ping.lossPct < 100) {
28597
28733
  causes.push({
@@ -28797,7 +28933,9 @@ async function collectInterfaces(ctx) {
28797
28933
  rxErrors: num(r["rx-error"]),
28798
28934
  linkDowns: num(r["link-downs"]),
28799
28935
  lastLinkDownTime: r["last-link-down-time"],
28800
- mtu: num(r.mtu) || 1500
28936
+ mtu: num(r.mtu) || 1500,
28937
+ actualMtu: num(r["actual-mtu"]) || undefined,
28938
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
28801
28939
  }));
28802
28940
  }
28803
28941
  async function collectRoutes(ctx) {
@@ -28930,9 +29068,35 @@ async function collectTunnels(ctx) {
28930
29068
  txErrors: num(r["tx-error"]),
28931
29069
  rxErrors: num(r["rx-error"]),
28932
29070
  linkDowns: num(r["link-downs"]),
28933
- mtu: num(r.mtu) || 1500
29071
+ mtu: num(r.mtu) || 1500,
29072
+ actualMtu: num(r["actual-mtu"]) || undefined,
29073
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
29074
+ }));
29075
+ }
29076
+ async function collectMangleRules(ctx) {
29077
+ const raw = await safe("/ip firewall mangle print detail", ctx);
29078
+ if (!raw)
29079
+ return [];
29080
+ return parseRecords(raw).rows.map(parseFirewallRow);
29081
+ }
29082
+ async function collectWireguardPeers(ctx) {
29083
+ const raw = await safe("/interface wireguard peers print detail", ctx);
29084
+ if (!raw || isEmpty(raw))
29085
+ return [];
29086
+ return parseRecords(raw).rows.map((r) => ({
29087
+ interface: r.interface ?? "",
29088
+ publicKey: r["public-key"] ?? "",
29089
+ endpoint: r["endpoint-address"] || undefined,
29090
+ persistentKeepalive: num(r["persistent-keepalive"]),
29091
+ disabled: (r.flags ?? "").includes("X") || r.disabled === "true"
28934
29092
  }));
28935
29093
  }
29094
+ async function collectPppProfilesMissingMssClamp(ctx) {
29095
+ const raw = await safe("/ppp profile print detail", ctx);
29096
+ if (!raw || isEmpty(raw))
29097
+ return [];
29098
+ return parseRecords(raw).rows.filter((r) => !isYes(r["change-tcp-mss"])).map((r) => r.name ?? "").filter(Boolean);
29099
+ }
28936
29100
  async function collectDiagnosticData(target, ctx, dimensions) {
28937
29101
  const dims = new Set(dimensions ?? ALL_DIMENSIONS);
28938
29102
  const [
@@ -28950,7 +29114,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28950
29114
  dnsSettingsParts,
28951
29115
  resourceRaw,
28952
29116
  logs,
28953
- tunnels
29117
+ tunnels,
29118
+ mangleRules,
29119
+ wireguardPeers,
29120
+ pppProfilesMissingMssClamp
28954
29121
  ] = await Promise.all([
28955
29122
  dims.has("connectivity") ? safe(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
28956
29123
  dims.has("interfaces") ? collectInterfaces(ctx) : Promise.resolve([]),
@@ -28970,7 +29137,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28970
29137
  ]) : Promise.resolve(["", "", ""]),
28971
29138
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
28972
29139
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
28973
- dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
29140
+ dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([]),
29141
+ dims.has("vpn") || dims.has("firewall") ? collectMangleRules(ctx) : Promise.resolve([]),
29142
+ dims.has("vpn") ? collectWireguardPeers(ctx) : Promise.resolve([]),
29143
+ dims.has("vpn") ? collectPppProfilesMissingMssClamp(ctx) : Promise.resolve([])
28974
29144
  ]);
28975
29145
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
28976
29146
  const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
@@ -29002,7 +29172,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
29002
29172
  uptime: resKv.uptime ?? "",
29003
29173
  rosVersion: resKv.version ?? "",
29004
29174
  relevantLogs: logs,
29005
- tunnelInterfaces: tunnels
29175
+ tunnelInterfaces: tunnels,
29176
+ mangleRules,
29177
+ wireguardPeers,
29178
+ pppProfilesMissingMssClamp
29006
29179
  };
29007
29180
  }
29008
29181
  var dimensionEnum = z94.enum(ALL_DIMENSIONS).describe("Diagnostic dimension to investigate.");
@@ -29011,7 +29184,7 @@ var rootCauseTools = [
29011
29184
  name: "diagnose",
29012
29185
  title: "Intelligent Root-Cause Diagnosis",
29013
29186
  annotations: READ,
29014
- description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state. " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
29187
+ description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state \u2014 " + "including the tunnel MTU / TCP-MSS-clamp / WireGuard-keepalive checks that catch a " + "PMTU black hole (tunnel pings fine but large transfers, uploads and some HTTPS hang). " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
29015
29188
  inputSchema: {
29016
29189
  target: z94.string().describe("The target to investigate \u2014 an IP address (e.g. '8.8.8.8'), hostname " + "(e.g. 'google.com'), or network/subnet (e.g. '10.0.0.0/24')."),
29017
29190
  dimensions: z94.array(dimensionEnum).optional().describe(`Limit investigation to specific dimensions. ` + `Omit to check all: ${ALL_DIMENSIONS.join(", ")}`),
@@ -34834,7 +35007,7 @@ var riskByTool = null;
34834
35007
  async function riskIndex() {
34835
35008
  if (riskByTool)
34836
35009
  return riskByTool;
34837
- const { moduleCatalog } = await import("./cli-s9fc4c7j.js");
35010
+ const { moduleCatalog } = await import("./cli-f80vccvs.js");
34838
35011
  const index = new Map;
34839
35012
  for (const mod of moduleCatalog) {
34840
35013
  for (const tool of mod.tools)
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-mqb3rqfy.js";
7
+ } from "./cli-54yv0kd3.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-tq8phch8.js";
7
+ } from "./library-s7s3s14v.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -10607,7 +10607,7 @@ var cache2 = null;
10607
10607
  async function gateway() {
10608
10608
  if (cache2)
10609
10609
  return cache2;
10610
- const { moduleCatalog } = await import("./library-xgy70fsf.js");
10610
+ const { moduleCatalog } = await import("./library-4a4mfnxf.js");
10611
10611
  const forIndex = [];
10612
10612
  const byName = new Map;
10613
10613
  for (const mod of moduleCatalog) {
@@ -28176,6 +28176,8 @@ function analyzeRootCause(data) {
28176
28176
  summary: `All ${data.tunnelInterfaces.length} tunnel(s) running`
28177
28177
  });
28178
28178
  }
28179
+ const mtuFindings = analyzeTunnelMtu(data);
28180
+ evidence.push(...mtuFindings.evidence);
28179
28181
  correlateRootCauses(data, evidence, rootCauses);
28180
28182
  if (data.ping && data.ping.lossPct === 0) {
28181
28183
  const contradicted = new Set([
@@ -28210,6 +28212,105 @@ function analyzeRootCause(data) {
28210
28212
  dimensionSummary
28211
28213
  };
28212
28214
  }
28215
+ var TUNNEL_OVERHEAD = [
28216
+ { match: "gre", bytes: 24, label: "GRE", path: "/interface gre" },
28217
+ { match: "ipip", bytes: 20, label: "IPIP", path: "/interface ipip" },
28218
+ { match: "eoip", bytes: 42, label: "EoIP", path: "/interface eoip" },
28219
+ { match: "vxlan", bytes: 50, label: "VXLAN", path: "/interface vxlan" },
28220
+ { match: "wg", bytes: 60, label: "WireGuard", path: "/interface wireguard" },
28221
+ { match: "l2tp", bytes: 100, label: "L2TP" },
28222
+ { match: "pptp", bytes: 100, label: "PPTP" },
28223
+ { match: "sstp", bytes: 100, label: "SSTP" },
28224
+ { match: "ovpn", bytes: 100, label: "OpenVPN" }
28225
+ ];
28226
+ var PPP_FAMILY = /l2tp|pptp|sstp|ovpn|pppoe/;
28227
+ function tunnelOverhead(type) {
28228
+ const t = type.toLowerCase();
28229
+ return TUNNEL_OVERHEAD.find((o) => t.includes(o.match));
28230
+ }
28231
+ function analyzeTunnelMtu(data) {
28232
+ const evidence = [];
28233
+ const unclamped = [];
28234
+ const oversizedMtu = [];
28235
+ const tunnels = data.tunnelInterfaces.filter((t) => !t.disabled);
28236
+ const clampRules = data.mangleRules.filter((r) => r.action === "change-mss" && !r.disabled);
28237
+ const globalClamp = clampRules.some((r) => r.chain === "forward" || r.chain === "postrouting" || r.chain === "output");
28238
+ const strandedClamp = clampRules.length > 0 && !globalClamp;
28239
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28240
+ const pathBudget = pppoeWan ? 1492 : 1500;
28241
+ const pppClampedEverywhere = data.pppProfilesMissingMssClamp.length === 0;
28242
+ for (const t of tunnels) {
28243
+ const overhead = tunnelOverhead(t.type);
28244
+ if (!overhead)
28245
+ continue;
28246
+ const effective = t.actualMtu ?? t.mtu;
28247
+ const budget = pathBudget - overhead.bytes;
28248
+ if (effective > budget) {
28249
+ oversizedMtu.push({ iface: t, effective, budget, label: overhead.label });
28250
+ evidence.push({
28251
+ dimension: "vpn",
28252
+ severity: "warning",
28253
+ summary: `Tunnel ${t.name} MTU ${effective} exceeds the ${budget}-byte budget for ` + `${overhead.label} (${overhead.bytes} B overhead${pppoeWan ? " + PPPoE 8 B" : ""})`,
28254
+ detail: "Packets at this size need fragmentation the transit path will not perform, " + "so they are dropped silently.",
28255
+ reference: t.name
28256
+ });
28257
+ }
28258
+ const covered = t.clampTcpMss === true || globalClamp || PPP_FAMILY.test(t.type.toLowerCase()) && pppClampedEverywhere;
28259
+ if (!covered) {
28260
+ unclamped.push(t);
28261
+ evidence.push({
28262
+ dimension: "vpn",
28263
+ severity: "warning",
28264
+ summary: `Tunnel ${t.name} (${overhead.label}) has no TCP MSS clamp`,
28265
+ detail: "Hosts derive their MSS from their own LAN MTU and set DF, so full-size " + "segments enter the tunnel and are dropped in transit. Small flows (ping, " + "logins) work; large ones (file transfer, photo/video upload, TLS) stall.",
28266
+ reference: t.name
28267
+ });
28268
+ }
28269
+ }
28270
+ if (strandedClamp) {
28271
+ evidence.push({
28272
+ dimension: "vpn",
28273
+ severity: "warning",
28274
+ summary: `A change-mss rule exists but only in chain '${clampRules[0].chain}' \u2014 ` + "transit traffic is clamped in forward/postrouting, not there"
28275
+ });
28276
+ }
28277
+ if (data.pppProfilesMissingMssClamp.length > 0 && tunnels.some((t) => PPP_FAMILY.test(t.type))) {
28278
+ evidence.push({
28279
+ dimension: "vpn",
28280
+ severity: "warning",
28281
+ summary: `PPP profile(s) without change-tcp-mss: ${data.pppProfilesMissingMssClamp.join(", ")}`,
28282
+ detail: "PPP-family tunnels (L2TP/PPTP/SSTP/OpenVPN/PPPoE) clamp MSS through their " + "profile. Sessions using these profiles are unprotected."
28283
+ });
28284
+ }
28285
+ const keepaliveOff = data.wireguardPeers.filter((p) => !p.disabled && p.persistentKeepalive <= 0);
28286
+ if (keepaliveOff.length > 0) {
28287
+ evidence.push({
28288
+ dimension: "vpn",
28289
+ severity: "warning",
28290
+ summary: `${keepaliveOff.length} WireGuard peer(s) have persistent-keepalive off`,
28291
+ detail: "NAT and stateful firewalls drop idle UDP mappings after ~30 s. Without " + "keepalive the peer becomes unreachable from this side until it transmits.",
28292
+ reference: keepaliveOff[0].interface
28293
+ });
28294
+ }
28295
+ if (tunnels.length > 0 && unclamped.length === 0 && oversizedMtu.length === 0 && keepaliveOff.length === 0) {
28296
+ evidence.push({
28297
+ dimension: "vpn",
28298
+ severity: "ok",
28299
+ summary: `MTU/MSS sane on all ${tunnels.length} tunnel(s)`
28300
+ });
28301
+ }
28302
+ return { evidence, unclamped, oversizedMtu, keepaliveOff };
28303
+ }
28304
+ function suggestedMtu(type, pppoeWan) {
28305
+ const o = tunnelOverhead(type);
28306
+ return o ? (pppoeWan ? 1492 : 1500) - o.bytes : undefined;
28307
+ }
28308
+ function mtuFixCommand(iface, mtu) {
28309
+ const o = tunnelOverhead(iface.type);
28310
+ if (o?.path)
28311
+ return `${o.path} set [find name="${iface.name}"] mtu=${mtu}`;
28312
+ return `# ${iface.name} (${o?.label ?? iface.type}): set max-mtu=${mtu} max-mru=${mtu} on the ` + "client interface, or on the server settings for an inbound session";
28313
+ }
28213
28314
  function correlateRootCauses(data, evidence, causes) {
28214
28315
  const criticals = evidence.filter((e) => e.severity === "critical");
28215
28316
  const warnings = evidence.filter((e) => e.severity === "warning");
@@ -28340,6 +28441,41 @@ function correlateRootCauses(data, evidence, causes) {
28340
28441
  dimensions: ["vpn"]
28341
28442
  });
28342
28443
  }
28444
+ const mtu = analyzeTunnelMtu(data);
28445
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28446
+ if (mtu.unclamped.length > 0 || mtu.oversizedMtu.length > 0) {
28447
+ const names = [
28448
+ ...new Set([...mtu.unclamped, ...mtu.oversizedMtu.map((o) => o.iface)].map((t) => t.name))
28449
+ ];
28450
+ const bothProblems = mtu.unclamped.length > 0 && mtu.oversizedMtu.length > 0;
28451
+ const pingOk = data.ping?.lossPct === 0;
28452
+ causes.push({
28453
+ cause: "MTU/MSS black hole on tunnel",
28454
+ explanation: `Tunnel(s) ${names.join(", ")} carry traffic larger than the path can deliver. ` + "Encapsulation shrinks the usable MTU, but hosts still negotiate a TCP MSS from " + "their own LAN MTU and set the don't-fragment bit; transit routers drop the " + "oversized packets and the ICMP 'fragmentation needed' reply is commonly filtered, " + "so the sender never learns to back off. The result is a tunnel that pings clean " + "and passes logins while file transfers, media uploads and some HTTPS sites hang.",
28455
+ confidence: bothProblems || pingOk ? "high" : "medium",
28456
+ evidence: mtu.evidence.filter((e) => e.severity === "warning"),
28457
+ fixes: [
28458
+ ...mtu.oversizedMtu.map((o) => mtuFixCommand(o.iface, suggestedMtu(o.iface.type, pppoeWan) ?? o.budget)),
28459
+ ...mtu.unclamped.length > 0 ? [
28460
+ "# Clamp TCP MSS for every forwarded flow (covers all tunnel types):",
28461
+ "/ip firewall mangle add chain=forward protocol=tcp tcp-flags=syn " + 'tcp-mss=1400-65535 action=change-mss new-mss=clamp-to-pmtu comment="clamp MSS to PMTU"',
28462
+ "# PPP-family tunnels (L2TP/PPTP/SSTP/OVPN) can clamp via their profile instead:",
28463
+ "/ppp profile set [find] change-tcp-mss=yes"
28464
+ ] : []
28465
+ ],
28466
+ dimensions: ["vpn", "firewall"]
28467
+ });
28468
+ }
28469
+ if (mtu.keepaliveOff.length > 0) {
28470
+ causes.push({
28471
+ cause: "WireGuard peer unreachable after idle (no persistent-keepalive)",
28472
+ explanation: `${mtu.keepaliveOff.length} enabled peer(s) have persistent-keepalive off: ` + `${mtu.keepaliveOff.map((p) => `${p.interface}/${p.publicKey.slice(0, 12)}\u2026`).join(", ")}. ` + "WireGuard is silent when idle, so the NAT/firewall mapping the peer punched " + "expires (typically ~30 s) and this side can no longer initiate. The tunnel " + "appears to work whenever the peer starts the traffic and to be down otherwise.",
28473
+ confidence: "medium",
28474
+ evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "warning"),
28475
+ fixes: mtu.keepaliveOff.map((p) => `/interface wireguard peers set [find public-key="${p.publicKey}"] persistent-keepalive=25s`),
28476
+ dimensions: ["vpn"]
28477
+ });
28478
+ }
28343
28479
  const flapping = data.interfaces.filter((i) => i.linkDowns > 5 && i.running);
28344
28480
  if (flapping.length > 0 && data.ping && data.ping.lossPct > 0 && data.ping.lossPct < 100) {
28345
28481
  causes.push({
@@ -28545,7 +28681,9 @@ async function collectInterfaces(ctx) {
28545
28681
  rxErrors: num(r["rx-error"]),
28546
28682
  linkDowns: num(r["link-downs"]),
28547
28683
  lastLinkDownTime: r["last-link-down-time"],
28548
- mtu: num(r.mtu) || 1500
28684
+ mtu: num(r.mtu) || 1500,
28685
+ actualMtu: num(r["actual-mtu"]) || undefined,
28686
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
28549
28687
  }));
28550
28688
  }
28551
28689
  async function collectRoutes(ctx) {
@@ -28678,9 +28816,35 @@ async function collectTunnels(ctx) {
28678
28816
  txErrors: num(r["tx-error"]),
28679
28817
  rxErrors: num(r["rx-error"]),
28680
28818
  linkDowns: num(r["link-downs"]),
28681
- mtu: num(r.mtu) || 1500
28819
+ mtu: num(r.mtu) || 1500,
28820
+ actualMtu: num(r["actual-mtu"]) || undefined,
28821
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
28822
+ }));
28823
+ }
28824
+ async function collectMangleRules(ctx) {
28825
+ const raw = await safe("/ip firewall mangle print detail", ctx);
28826
+ if (!raw)
28827
+ return [];
28828
+ return parseRecords(raw).rows.map(parseFirewallRow);
28829
+ }
28830
+ async function collectWireguardPeers(ctx) {
28831
+ const raw = await safe("/interface wireguard peers print detail", ctx);
28832
+ if (!raw || isEmpty(raw))
28833
+ return [];
28834
+ return parseRecords(raw).rows.map((r) => ({
28835
+ interface: r.interface ?? "",
28836
+ publicKey: r["public-key"] ?? "",
28837
+ endpoint: r["endpoint-address"] || undefined,
28838
+ persistentKeepalive: num(r["persistent-keepalive"]),
28839
+ disabled: (r.flags ?? "").includes("X") || r.disabled === "true"
28682
28840
  }));
28683
28841
  }
28842
+ async function collectPppProfilesMissingMssClamp(ctx) {
28843
+ const raw = await safe("/ppp profile print detail", ctx);
28844
+ if (!raw || isEmpty(raw))
28845
+ return [];
28846
+ return parseRecords(raw).rows.filter((r) => !isYes(r["change-tcp-mss"])).map((r) => r.name ?? "").filter(Boolean);
28847
+ }
28684
28848
  async function collectDiagnosticData(target, ctx, dimensions) {
28685
28849
  const dims = new Set(dimensions ?? ALL_DIMENSIONS);
28686
28850
  const [
@@ -28698,7 +28862,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28698
28862
  dnsSettingsParts,
28699
28863
  resourceRaw,
28700
28864
  logs,
28701
- tunnels
28865
+ tunnels,
28866
+ mangleRules,
28867
+ wireguardPeers,
28868
+ pppProfilesMissingMssClamp
28702
28869
  ] = await Promise.all([
28703
28870
  dims.has("connectivity") ? safe(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
28704
28871
  dims.has("interfaces") ? collectInterfaces(ctx) : Promise.resolve([]),
@@ -28718,7 +28885,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28718
28885
  ]) : Promise.resolve(["", "", ""]),
28719
28886
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
28720
28887
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
28721
- dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
28888
+ dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([]),
28889
+ dims.has("vpn") || dims.has("firewall") ? collectMangleRules(ctx) : Promise.resolve([]),
28890
+ dims.has("vpn") ? collectWireguardPeers(ctx) : Promise.resolve([]),
28891
+ dims.has("vpn") ? collectPppProfilesMissingMssClamp(ctx) : Promise.resolve([])
28722
28892
  ]);
28723
28893
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
28724
28894
  const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
@@ -28750,7 +28920,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28750
28920
  uptime: resKv.uptime ?? "",
28751
28921
  rosVersion: resKv.version ?? "",
28752
28922
  relevantLogs: logs,
28753
- tunnelInterfaces: tunnels
28923
+ tunnelInterfaces: tunnels,
28924
+ mangleRules,
28925
+ wireguardPeers,
28926
+ pppProfilesMissingMssClamp
28754
28927
  };
28755
28928
  }
28756
28929
  var dimensionEnum = z94.enum(ALL_DIMENSIONS).describe("Diagnostic dimension to investigate.");
@@ -28759,7 +28932,7 @@ var rootCauseTools = [
28759
28932
  name: "diagnose",
28760
28933
  title: "Intelligent Root-Cause Diagnosis",
28761
28934
  annotations: READ,
28762
- description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state. " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
28935
+ description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state \u2014 " + "including the tunnel MTU / TCP-MSS-clamp / WireGuard-keepalive checks that catch a " + "PMTU black hole (tunnel pings fine but large transfers, uploads and some HTTPS hang). " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
28763
28936
  inputSchema: {
28764
28937
  target: z94.string().describe("The target to investigate \u2014 an IP address (e.g. '8.8.8.8'), hostname " + "(e.g. 'google.com'), or network/subnet (e.g. '10.0.0.0/24')."),
28765
28938
  dimensions: z94.array(dimensionEnum).optional().describe(`Limit investigation to specific dimensions. ` + `Omit to check all: ${ALL_DIMENSIONS.join(", ")}`),
@@ -34334,7 +34507,7 @@ var riskByTool = null;
34334
34507
  async function riskIndex() {
34335
34508
  if (riskByTool)
34336
34509
  return riskByTool;
34337
- const { moduleCatalog } = await import("./library-xgy70fsf.js");
34510
+ const { moduleCatalog } = await import("./library-4a4mfnxf.js");
34338
34511
  const index = new Map;
34339
34512
  for (const mod of moduleCatalog) {
34340
34513
  for (const tool of mod.tools)