routeros-mndp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited routeros-mndp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
MNDP is the UDP broadcast/multicast protocol that RouterOS uses for automatic device discovery on the local network. It is the same protocol WinBox uses to find routers. Every RouterOS device participates by default.
| Property | Value |
|---|---|
| Transport | UDP |
| Port | 5678 |
| IPv4 | Broadcast to 255.255.255.255:5678 |
| IPv6 | Multicast to ff02::1 (all-nodes link-local) |
| Direction | Bidirectional — same port for send and receive |
| Authentication | None — read-only discovery, no credentials |
| Scope | Layer 2 broadcast domain (does not cross routers) |
A short packet that triggers immediate replies from all RouterOS devices on the broadcast domain. The minimal form is just a zeroed 4-byte header — this is exactly what MAC-Telnet sends (unsigned int message = 0;):
Offset Length Value Field
0 1 0x00 header byte 0 (version per MAC-Telnet; "unknown" per Wireshark)
1 1 0x00 header byte 1 (ttl per MAC-Telnet)
2 2 0x0000 seqno / checksumAs raw bytes (minimal): 00 00 00 00
Some implementations append an explicit refresh TLV (type 6, length 0). The complete header + empty-TLV form is 8 bytes (00 00 00 00 00 06 00 00); some implementations add a trailing zero byte (non-semantic padding), giving 9 bytes. RouterOS accepts all forms:
Offset Length Value Field
0 4 00 00 00 00 header (2 header bytes + 2-byte seqno)
4 2 0x0006 TLV type = 6 (refresh) ← big-endian
6 2 0x0000 TLV length = 0
8 1 0x00 optional trailing pad (non-semantic)As raw bytes (9-byte form): 00 00 00 00 00 06 00 00 00
Offset Length Field
0 2 header bytes (version + ttl per MAC-Telnet; "unknown" per Wireshark)
2 2 sequence number (big-endian; per-device counter)
4+ TLV[] zero or more TLV records (see below)The first 4 bytes are a fixed header; parsers skip the first 2 bytes and read the sequence number as a big-endian uint16, then iterate TLVs from offset 4.
Each TLV (Type-Length-Value) record in the response:
Offset Length Field
0 2 type (big-endian uint16)
2 2 length (big-endian uint16) — byte count of value
4 N value (raw bytes — interpretation depends on type)Byte order: TLV type and length are big-endian (network byte order — ntohs). This is confirmed by the canonical reference implementations: MAC-Telnet's protocol.c (type = ntohs(type); len = ntohs(len);) and the official Wireshark MNDP dissector (packet-mndp.c, which reads both with ENC_BIG_ENDIAN).
Value encoding varies by type and is the most common footgun: string TLVs are UTF-8, IPv4/IPv6/MAC are raw network-order bytes, but TLV 10 (uptime) is a little-endian uint32 — the only little-endian value in the entire protocol. Everything else multi-byte is big-endian/raw.
| Type | Hex | Name | Value Format | Notes |
|---|---|---|---|---|
| 1 | 0x0001 | MAC Address | 6 raw bytes (network order) | Per-interface MAC, not chassis MAC |
| 5 | 0x0005 | Identity | UTF-8 string | Hostname — same across all interfaces |
| 7 | 0x0007 | Version | UTF-8 string | e.g. 7.18 (stable), 7.22rc1 |
| 8 | 0x0008 | Platform | UTF-8 string | Usually MikroTik |
| 10 | 0x000a | Uptime | 4 bytes LE uint32 | Seconds since boot — the only little-endian value |
| 11 | 0x000b | Software ID | UTF-8 string | License identifier |
| 12 | 0x000c | Board | UTF-8 string | e.g. RB4011iGS+5HacQ2HnD, CHR |
| 14 | 0x000e | Unpack | 1 byte | Firmware compression flag (some parsers treat as IPv6-present flag) |
| 15 | 0x000f | IPv6 Address | 16 raw bytes | Link-local or global IPv6 |
| 16 | 0x0010 | Interface Name | UTF-8 string | Sending interface on the router (e.g. ether1) |
| 17 | 0x0011 | IPv4 Address | 4 raw bytes (network order) | IP of the sending interface |
Source of truth: these type IDs match the canonical MAC-Telnet protocol.c (MT_MNDPTYPE_* enum) and the Wireshark MNDP dissector value table exactly. Note in particular that uptime is type 10, software-id is type 11, and board is type 12 — a common mistake is to off-by-one these (uptime=11/board=13), which silently misparses real RouterOS packets.
Packing TLVs (types 2, 3, 9): related to /ip packing/compression. Safe to skip.
A RouterOS device with N active interfaces sends N separate MNDP announcements — one per interface. Each announcement has:
This is expected behavior, not a bug. When displaying results, group by identity to avoid showing the same router N times. Use MAC address to disambiguate when the identity is the factory default (MikroTik).
| Scenario | Expected Response Time |
|---|---|
| Local LAN (wired) | 1-3 seconds |
| WiFi / congested network | 3-10 seconds |
| ZeroTier / tunnel overlay | 5-20 seconds |
| Satellite / high-latency | 10-30 seconds |
Best practice: Send multiple refresh packets during the listen window (every 5 seconds, matching WinBox behavior). Devices that miss the first broadcast due to packet loss, WiFi power-save, or tunnel relay latency will respond to subsequent refreshes.
Never interpret missing devices as offline. A short scan window produces partial results. Increase the timeout before concluding a device is unreachable.
/ip/neighbor is the RouterOS-side view of MNDP (and CDP/LLDP) discovery results. It shows what the router has heard from other devices on its directly-connected networks.
# Print discovered neighbors
/ip/neighbor/print
# Columns: interface, address, mac-address, identity, platform, version, boardControls which interfaces participate in neighbor discovery:
# Show current discovery settings
/ip/neighbor/discovery-settings/print
# Disable MNDP on a specific interface (security hardening)
/interface/list/member/add list=no-mndp interface=ether1
/ip/neighbor/discovery-settings/set discover-interface-list=!no-mndp
# Supported protocols (can be combined)
# cdp — Cisco Discovery Protocol
# lldp — Link Layer Discovery Protocol
# mndp — MikroTik Neighbor Discovery Protocol
/ip/neighbor/discovery-settings/set protocol=mndp,lldp# List neighbors via REST
curl -u admin: http://<router-ip>/rest/ip/neighbor
# Response is JSON array with the same fields as CLI printMNDP uses the same port (5678) for both sending and receiving. If another process (e.g., WinBox) already has UDP/5678 bound, your listener needs SO_REUSEPORT to coexist:
// Node.js / Bun dgram
import { createSocket } from "node:dgram";
const sock = createSocket({ type: "udp4", reuseAddr: true, reusePort: true });
sock.bind(5678, "0.0.0.0", () => {
sock.setBroadcast(true);
});// C / POSIX
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));Platform note: SO_REUSEPORT works on macOS and Linux. On Windows, SO_REUSEADDR alone may be sufficient. In Bun, reusePort: true requires Bun >= 1.3.11 (earlier versions silently ignored it on macOS).
When you send a broadcast to 255.255.255.255:5678, the OS delivers a copy back to your own socket. Filter out packets from your own IP addresses to avoid processing your own refresh as a neighbor response. The looped-back packet will have no MNDP TLVs (it's your 9-byte refresh, not a device announcement).
IPv6 MNDP uses ff02::1 (all-nodes multicast). Less commonly used than IPv4 broadcast but supported:
sock.addMembership("ff02::1");IPv6 link-local addresses include a zone ID (e.g., fe80::1%en0) that identifies the receiving interface — useful topology information not available in IPv4.
function parseMndpResponse(buf: Buffer): Record<string, string | number> {
let offset = 4; // skip 2-byte header (version, ttl) + 2-byte sequence
const fields: Record<string, string | number> = {};
while (offset + 4 <= buf.length) {
const tlvType = buf.readUInt16BE(offset); // big-endian
const tlvLen = buf.readUInt16BE(offset + 2); // big-endian
offset += 4;
if (offset + tlvLen > buf.length) break; // malformed / truncated
const value = buf.subarray(offset, offset + tlvLen);
switch (tlvType) {
case 1: // MAC Address — 6 raw bytes
fields.macAddress = [...value].map(b => b.toString(16).padStart(2, "0")).join(":");
break;
case 5: fields.identity = value.toString("utf8"); break;
case 7: fields.version = value.toString("utf8"); break;
case 8: fields.platform = value.toString("utf8"); break;
case 10: // Uptime — 4 bytes LITTLE-ENDIAN uint32 (the only LE value)
if (tlvLen === 4) fields.uptime = value.readUInt32LE(0);
break;
case 11: fields.softwareId = value.toString("utf8"); break;
case 12: fields.board = value.toString("utf8"); break;
case 15: // IPv6 — 16 bytes
if (tlvLen === 16) fields.ipv6 = formatIpv6(value);
break;
case 16: fields.interfaceName = value.toString("utf8"); break;
case 17: // IPv4 — 4 raw bytes
if (tlvLen === 4) fields.ipv4 = `${value[0]}.${value[1]}.${value[2]}.${value[3]}`;
break;
}
offset += tlvLen;
}
return fields;
}| Language | Source | Notes |
|---|---|---|
| C | github.com/haakonnessjoen/MAC-Telnet (protocol.c, protocol.h) | Canonical ground truth. TLV enum + ntohs big-endian TLVs + le32toh uptime |
| C (dissector) | Wireshark epan/dissectors/packet-mndp.c | Official protocol dissector — confirms type IDs and ENC_BIG_ENDIAN TLVs |
| Go | github.com/middelink/mikrotik-fwupdate | Secondary protocol cross-check |
| Elixir | hex.pm mndp package | Confirms TLV type IDs |
| Swift | Open-source macOS implementations | Confirms raw-byte MAC encoding |
Verified: The TLV type IDs and byte order in this skill were cross-checked against MAC-Telnet's protocol.c and the Wireshark dissector — both agree that TLV type/length are big-endian and that uptime (type 10) is the sole little-endian value.
routeros-fundamentals skill/console/inspect command tree: see routeros-command-tree skillrouteros-sniffer skillrouteros-qemu-chr skillrosetta MCP server tools (routeros_search, routeros_get_page)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.