@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 +6 -1
- package/dist/index.js +1 -1
- package/dist/shared/{cli-mqb3rqfy.js → cli-54yv0kd3.js} +181 -8
- package/dist/shared/{cli-s9fc4c7j.js → cli-f80vccvs.js} +1 -1
- package/dist/shared/{library-xgy70fsf.js → library-4a4mfnxf.js} +1 -1
- package/dist/shared/{library-tq8phch8.js → library-s7s3s14v.js} +181 -8
- package/dist/ui/observability.html +59 -59
- package/package.json +7 -4
- package/prompts/setup-ipsec-site-to-site.md +14 -2
- package/prompts/setup-l2tp-ipsec-roadwarrior.md +13 -1
- package/prompts/setup-wireguard-tunnel-between-sites.md +7 -4
- package/prompts/setup-wireguard-vpn.md +22 -1
- package/schemas/tool-catalog.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -230,7 +230,7 @@ import {
|
|
|
230
230
|
validatePolicyText,
|
|
231
231
|
worstSeverity,
|
|
232
232
|
writeBackup
|
|
233
|
-
} from "./shared/cli-
|
|
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-
|
|
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-
|
|
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-
|
|
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)
|
|
@@ -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-
|
|
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-
|
|
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)
|