--- name: routeros-mndp description: "MNDP (MikroTik Neighbor Discovery Protocol) wire format, behavior, and RouterOS /ip/neighbor integration. Use when: implementing MNDP discovery, parsing MNDP packets, working with /ip/neighbor, understanding WinBox device discovery, debugging why a router doesn't appear in neighbor lists, or when the user mentions MNDP, neighbor discovery, WinBox discovery, or /ip/neighbor." --- # MNDP — MikroTik Neighbor Discovery Protocol 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. ## Why This Matters for Agents - **`/ip/neighbor`** on RouterOS is the CLI/REST surface for MNDP results - WinBox's "Neighbors" tab is an MNDP listener - Any agent implementing device discovery for MikroTik equipment needs MNDP - The protocol is simple enough to implement from scratch — no library needed ## Protocol Basics | 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) | ## How Discovery Works 1. **Listener sends a refresh packet** — a 9-byte UDP datagram to 255.255.255.255:5678 2. **All RouterOS devices on the LAN reply** — each sends a TLV-encoded announcement with identity, version, board, MAC, IP, uptime, etc. 3. **Replies arrive asynchronously** — devices respond within milliseconds to seconds depending on network conditions 4. **RouterOS devices also announce periodically** (~60s cycle) without being prompted — passive listening works but is slow to populate ### Refresh Packet (Discovery Request) A 9-byte packet that triggers immediate replies from all RouterOS devices on the broadcast domain: ``` Offset Length Value Field 0 2 0x0000 type (MNDP) 2 2 0x0000 sequence number (0 for request) 4 2 0x0000 TLV type (none) 6 2 0x0000 TLV length (none) 8 1 0x00 padding ``` As raw bytes: `00 00 00 00 00 00 00 00 00` ### Response Packet ``` Offset Length Field 0 2 type = 0x0000 (MNDP) 2 2 sequence number (monotonically increasing per device) 4+ TLV[] zero or more TLV records (see below) ``` ## TLV Format Each TLV (Type-Length-Value) record in the response: ``` Offset Length Field 0 2 type (little-endian uint16) 2 2 length (little-endian uint16) — byte count of value 4 N value (raw bytes — interpretation depends on type) ``` **Byte order:** TLV type and length are little-endian. Value encoding varies by type. ### TLV Type Reference | Type | Hex | Name | Value Format | Notes | |------|--------|----------------|-------------|-------| | 1 | 0x0001 | MAC Address | 6 bytes, big-endian | 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 | Board | UTF-8 string | e.g. `RB4011iGS+5HacQ2HnD`, `CHR` | | 11 | 0x000b | Uptime | 4 bytes LE uint32 | Seconds since boot | | 12 | 0x000c | Software ID | UTF-8 string | License identifier | | 13 | 0x000d | Board (alt) | UTF-8 string | Some firmware uses type 13 instead of 10 | | 14 | 0x000e | Unpack | 1 byte | Firmware compression flag | | 15 | 0x000f | IPv6 Address | 16 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 bytes, big-endian | IP of the sending interface | **Board name:** Some firmware versions use TLV type 10, others use type 13. Parsers should handle both — prefer type 10 if both are present. ## Multi-Interface Behavior A RouterOS device with N active interfaces sends **N separate MNDP announcements** — one per interface. Each announcement has: - A **different MAC address** (the interface's own MAC) - A **different interface name** (TLV 16) - A **different IP address** (if assigned) - The **same identity** (hostname) 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`). ## Timing and Reliability | 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. ## RouterOS /ip/neighbor `/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. ```routeros # Print discovered neighbors /ip/neighbor/print # Columns: interface, address, mac-address, identity, platform, version, board ``` ### /ip/neighbor/discovery-settings Controls which interfaces participate in neighbor discovery: ```routeros # 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 ``` ### REST API Access ```sh # List neighbors via REST curl -u admin: http:///rest/ip/neighbor # Response is JSON array with the same fields as CLI print ``` ### Security Considerations - MNDP has **no authentication** — any device on the broadcast domain can discover routers - Disable MNDP on untrusted interfaces (public-facing, guest networks) - RouterOS defaults to discovery on all interfaces — review and restrict - MNDP reveals: identity (hostname), version, board model, IPs, MACs, uptime - This information is useful for attackers — treat MNDP like an open SNMP community ## Socket Implementation Notes ### Port Sharing (SO_REUSEPORT) MNDP 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: ```typescript // 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 // 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). ### Self-Echo Filtering 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 Multicast IPv6 MNDP uses `ff02::1` (all-nodes multicast). Less commonly used than IPv4 broadcast but supported: ```typescript 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. ## Parsing Example (TypeScript) ```typescript function parseMndpResponse(buf: Buffer): Record { const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); let offset = 4; // skip 2-byte type + 2-byte sequence const fields: Record = {}; while (offset + 4 <= buf.length) { const tlvType = view.getUint16(offset, true); // little-endian const tlvLen = view.getUint16(offset + 2, true); // little-endian offset += 4; if (offset + tlvLen > buf.length) break; // malformed const value = buf.subarray(offset, offset + tlvLen); switch (tlvType) { case 1: // MAC Address — 6 bytes big-endian 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: // Board (primary) case 13: // Board (alternate — some firmware) if (!fields.board) fields.board = value.toString("utf8"); break; case 11: // Uptime — 4 bytes LE uint32 (seconds) fields.uptime = view.getUint32(offset - tlvLen, true); break; case 12: fields.softwareId = value.toString("utf8"); break; case 15: // IPv6 — 16 bytes fields.ipv6 = formatIpv6(value); break; case 16: fields.interfaceName = value.toString("utf8"); break; case 17: // IPv4 — 4 bytes big-endian fields.ipv4 = `${value[0]}.${value[1]}.${value[2]}.${value[3]}`; break; } offset += tlvLen; } return fields; } ``` ## Reference Implementations | Language | Source | Notes | |----------|--------|-------| | Go | github.com/middelink/mikrotik-fwupdate | Used as protocol ground truth during original research | | Elixir | hex.pm mndp package | Confirms TLV type IDs | | C | Various open-source MNDP clients | Direct setsockopt for REUSEPORT | | Swift | Open-source macOS implementations | Confirms big-endian MAC encoding | ## Related Skills - For RouterOS CLI/REST basics: see `routeros-fundamentals` skill - For the `/console/inspect` command tree: see `routeros-command-tree` skill - For packet capture and TZSP streaming: see `routeros-sniffer` skill - For QEMU CHR setup (testing without hardware): see `routeros-qemu-chr` skill ## Related MCP Tools - For RouterOS docs lookup: use the `rosetta` MCP server tools (`routeros_search`, `routeros_get_page`)