mcpField
R
routeros-mndp

routeros-mndp

github.com/tikoci开发8346 次安装5 天前更新
代码仓库

关于 routeros-mndp

routeros-mndp 是介绍 MikroTik RouterOS 邻居发现协议 MNDP 的技术技能,帮助 Agent 和开发者实现局域网发现、解析设备通告,并了解协议行为与安全注意事项。

主要功能

  1. 01说明 MNDP 数据包与 TLV 格式
  2. 02列出设备身份及接口字段
  3. 03介绍 IPv4 广播与 IPv6 多播
  4. 04记录响应时间和多接口行为
  5. 05提供套接字与解析指南

适用场景

  1. 01实现局域网内的 MikroTik 设备发现
  2. 02解析 RouterOS 邻居通告
  3. 03检查不可信接口上的 MNDP 暴露

MCP 相关

该技能推荐使用 rosetta MCP 服务查询 RouterOS 文档,包括 routeros_search 和 routeros_get_page 工具。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tikoci/routeros-skills --skill routeros-mndp

安装后在 Claude Code 中输入/routeros-mndp确认已加载。

详细介绍

MNDP — MikroTik 邻居发现协议

MNDP 是 RouterOS 用于在本地网络中自动发现设备的 UDP 广播/组播协议。WinBox 使用的也是同一种协议来查找路由器。 每台 RouterOS 设备默认都会参与发现。

为什么这对 Agent 很重要

  • RouterOS 上的 /ip/neighbor 是查看 MNDP 结果的 CLI/REST 接口
  • WinBox 的“Neighbors”选项卡是 MNDP 监听器
  • 任何为 MikroTik 设备实现发现功能的 Agent 都需要支持 MNDP
  • 该协议足够简单,可以从头实现,无需使用库

协议基础

属性值
传输方式UDP
端口5678
IPv4向 255.255.255.255:5678 广播
IPv6向 ff02::1(所有节点链路本地地址)组播
方向双向——发送和接收使用同一端口
身份验证无——只读发现,无需凭据
范围二层广播域(不会穿过路由器)

发现工作原理

  1. 监听器发送刷新数据包——向 255.255.255.255:5678 发送一个简短的 UDP 数据报(MAC-Telnet 会发送最简的 4 字节全零报头;9 字节格式也有效——见下文)
  2. 局域网内所有 RouterOS 设备都会回复——每台设备都会发送一个以 TLV 编码的通告,其中包含身份、版本、板卡、MAC、IP、运行时间等信息
  3. 回复以异步方式到达——设备会在数毫秒到数秒内响应,具体取决于网络状况
  4. RouterOS 设备也会定期通告(约每 60 秒一次),无需触发——被动监听也可行,但发现信息填充较慢
刷新数据包(发现请求)

这个简短的数据包会触发广播域内所有 RouterOS 设备立即回复。最简格式仅包含一个全零的 4 字节报头——这正是 MAC-Telnet 发送的内容(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 / checksum

原始字节(最简格式):00 00 00 00

某些实现会附加一个明确的刷新 TLV(类型为 6,长度为 0)。完整的报头加空 TLV 格式为 8 字节(00 00 00 00 00 06 00 00);有些实现还会添加一个尾随零字节(无语义的填充),使其长度达到 9 字节。RouterOS 接受所有格式:

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)

原始字节(9 字节格式):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)

前 4 个字节是固定报头;解析器会跳过前 2 个字节,将序列号读取为大端 uint16,然后从偏移量 4 开始遍历 TLV。

TLV 格式

响应中的每条 TLV(类型-长度-值)记录:

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)

字节序:TLV 类型和长度采用大端序(网络字节序——ntohs)。 这已由权威参考实现证实:MAC-Telnet 的 protocol.c (type = ntohs(type); len = ntohs(len);)以及官方 Wireshark MNDP 解析器 (packet-mndp.c,其中两者均使用 ENC_BIG_ENDIAN 读取)。

**值的编码因类型而异,这是最常见的易错点:**字符串 TLV 使用 UTF-8, IPv4/IPv6/MAC 使用原始网络序字节,但 TLV 10(运行时间)采用小端 uint32——这是整个协议中唯一采用小端序的值。其他所有多字节值均采用大端序/原始格式。

TLV 类型参考
TypeHexNameValue FormatNotes
10x0001MAC 地址6 个原始字节(网络字节序)每个接口的 MAC,而非机箱 MAC
50x0005标识UTF-8 字符串主机名——所有接口相同
70x0007版本UTF-8 字符串例如 7.18 (stable)、7.22rc1
80x0008平台UTF-8 字符串通常为 MikroTik
100x000a运行时间4 字节 LE uint32自启动以来的秒数——唯一的小端值
110x000b软件 IDUTF-8 字符串许可证标识符
120x000c主板UTF-8 字符串例如 RB4011iGS+5HacQ2HnD、CHR
140x000e解包1 字节固件压缩标志(某些解析器将其视为 IPv6 存在标志)
150x000fIPv6 地址16 个原始字节链路本地或全局 IPv6
160x0010接口名称UTF-8 字符串路由器上发送数据的接口(例如 ether1)
170x0011IPv4 地址4 个原始字节(网络字节序)发送接口的 IP

**权威来源:**这些类型 ID 与规范 MAC-Telnet protocol.c (MT_MNDPTYPE_* 枚举)和 Wireshark MNDP 解剖器值表完全一致。请特别注意,运行时间的类型为 10,软件 ID 的类型为 11,主板的类型为 12——常见错误是将它们的编号各加 1(运行时间=11/主板=13),这会导致真实 RouterOS 数据包被静默地错误解析。

**打包 TLV(类型 2、3、9):**与 /ip 打包/压缩相关,可以安全跳过。

多接口行为

具有 N 个活动接口的 RouterOS 设备会发送 N 条独立的 MNDP 通告——每个接口一条。每条通告都包含:

  • 不同的 MAC 地址(该接口自身的 MAC)
  • 不同的接口名称(TLV 16)
  • 不同的 IP 地址(如果已分配)
  • 相同的标识(主机名)

这是预期行为,并非错误。显示结果时,请按标识分组,避免同一台路由器重复显示 N 次。如果标识仍为出厂默认值(MikroTik),请使用 MAC 地址来区分设备。

时序和可靠性

场景预期响应时间
本地 LAN(有线)1-3 秒
WiFi/网络拥塞3-10 秒
ZeroTier/隧道覆盖网络5-20 秒
卫星/高延迟网络10-30 秒

**最佳实践:**在监听窗口期间多次发送刷新数据包(每隔 5 秒一次,与 WinBox 行为一致)。因丢包、WiFi 省电模式或隧道中继延迟而错过首次广播的设备,会响应后续刷新。

**切勿将未发现设备解读为离线。**较短的扫描窗口只能获得部分结果。在得出设备无法访问的结论之前,请延长超时时间。

RouterOS /ip/neighbor

/ip/neighbor 是 RouterOS 侧的 MNDP(以及 CDP/LLDP)发现结果视图。它显示路由器从直接连接网络上的其他设备收到的信息。

# Print discovered neighbors
/ip/neighbor/print

# Columns: interface, address, mac-address, identity, platform, version, board
/ip/neighbor/discovery-settings

控制哪些接口参与邻居发现:

# 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 访问
# List neighbors via REST
curl -u admin: http://<router-ip>/rest/ip/neighbor

# Response is JSON array with the same fields as CLI print
安全注意事项
  • MNDP 没有身份验证——广播域中的任何设备都可以发现路由器
  • 在不可信接口(面向公网的接口、访客网络)上禁用 MNDP
  • RouterOS 默认在所有接口上启用发现——请检查并限制
  • MNDP 会泄露:标识(主机名)、版本、主板型号、IP、MAC、运行时间
  • 这些信息对攻击者有用——请像对待开放的 SNMP community 一样对待 MNDP

套接字实现说明

端口共享(SO_REUSEPORT)

MNDP 发送和接收使用相同的端口(5678)。如果另一个进程(例如 WinBox)已绑定 UDP/5678,你的监听器需要使用 SO_REUSEPORT 才能共存:

// 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));

平台说明:SO_REUSEPORT 在 macOS 和 Linux 上可用。在 Windows 上,单独使用 SO_REUSEADDR 可能就足够了。在 Bun 中,reusePort: true 要求 Bun >= 1.3.11(较早版本会在 macOS 上静默忽略此设置)。

自回显过滤

当你向 255.255.255.255:5678 发送广播时,操作系统会将副本发送回你自己的套接字。过滤掉来自你自己 IP 地址的数据包,以免将自己的刷新请求误认为邻居响应。回环的数据包不包含 MNDP TLV(它是你的 9 字节刷新请求,而不是设备通告)。

IPv6 多播

IPv6 MNDP 使用 ff02::1(所有节点多播)。这种方式不如 IPv4 广播常用,但也受支持:

sock.addMembership("ff02::1");

IPv6 链路本地地址包含一个区域 ID(例如 fe80::1%en0),用于标识接收接口——这是 IPv4 不具备的实用拓扑信息。

解析示例(TypeScript)

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;
}

参考实现

语言源码说明
Cgithub.com/haakonnessjoen/MAC-Telnet(protocol.c、protocol.h)权威基准。 TLV 枚举 + ntohs 大端序 TLV + le32toh 运行时间
C(解析器)Wireshark epan/dissectors/packet-mndp.c官方协议解析器——确认类型 ID 和 ENC_BIG_ENDIAN TLV
Gogithub.com/middelink/mikrotik-fwupdate辅助协议交叉核验
Elixirhex.pm mndp package确认 TLV 类型 ID
Swift开源 macOS 实现确认 MAC 地址采用原始字节编码

**已验证:**此技能中的 TLV 类型 ID 和字节序已与 MAC-Telnet 的 protocol.c 和 Wireshark 解析器交叉核验——两者均确认 TLV 类型和长度采用大端序,而运行时间(类型 10)是唯一的小端序值。

相关技能

  • RouterOS CLI/REST 基础知识:参见 routeros-fundamentals 技能
  • /console/inspect 命令树:参见 routeros-command-tree 技能
  • 抓包和 TZSP 流式传输:参见 routeros-sniffer 技能
  • QEMU CHR 设置(无需硬件即可测试):参见 routeros-qemu-chr 技能

相关 MCP 工具

  • 查询 RouterOS 文档:使用 rosetta MCP 服务工具(routeros_search、routeros_get_page)

评论

支持 Markdown