routeros-mndp について
routeros-mndpは、MikroTik RouterOSの近隣探索プロトコルMNDPを解説する技術スキルです。エージェントや開発者がLAN探索やデバイス通知の解析を実装し、プロトコルの挙動とセキュリティ上の注意点を把握できます。
主な機能
- 01MNDPパケットとTLV形式の解説
- 02デバイス識別情報とインターフェース項目
- 03IPv4ブロードキャストとIPv6マルチキャスト
- 04応答時間と複数インターフェースの挙動
- 05ソケットと解析の手引き
活用シーン
- 01LAN上のMikroTikデバイス探索を実装
- 02RouterOSの近隣通知を解析
- 03信頼できないインターフェースでのMNDP公開を確認
MCP 対応
RouterOSドキュメントの検索には、routeros_searchやrouteros_get_pageを提供するrosetta MCPサーバーを案内しています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tikoci/ routeros-skills --skill routeros-mndpインストール後、Claude Code で/routeros-mndpを入力して読み込まれたか確認します。
詳細
MNDP — MikroTik Neighbor Discovery Protocol
MNDP は、RouterOS がローカルネットワーク上のデバイスを自動検出するために使用する UDP ブロードキャスト/マルチキャストプロトコルです。WinBox がルーターの検出に使用するものと同じプロトコルです。すべての RouterOS デバイスは、デフォルトでこのプロトコルに参加します。
エージェントにとって重要な理由
- RouterOS の
/ip/neighborは、MNDP の結果を取得するための CLI/REST インターフェースです - WinBox の「Neighbors」タブは、MNDP リスナーです
- MikroTik 機器のデバイス検出を実装するエージェントには、MNDP が必要です
- このプロトコルは一から実装できるほどシンプルで、ライブラリは必要ありません
プロトコルの基本
| プロパティ | 値 |
|---|---|
| トランスポート | UDP |
| ポート | 5678 |
| IPv4 | 255.255.255.255:5678 へのブロードキャスト |
| IPv6 | ff02::1(全ノードのリンクローカル)へのマルチキャスト |
| 方向 | 双方向 — 送受信に同じポートを使用 |
| 認証 | なし — 読み取り専用の検出で、認証情報は不要 |
| 範囲 | レイヤー 2 のブロードキャストドメイン(ルーターを越えません) |
検出の仕組み
- リスナーがリフレッシュパケットを送信します — 255.255.255.255:5678 宛ての小さな UDP データグラムです(MAC-Telnet は最小サイズの 4 バイトのゼロ化ヘッダーを送信します。9 バイト形式も使用できます。以下を参照してください)
- LAN 上のすべての RouterOS デバイスが応答します — 各デバイスは、識別情報、バージョン、ボード、MAC、IP、稼働時間などを含む TLV エンコードされたアナウンスを送信します
- 応答は非同期に届きます — ネットワークの状態に応じて、デバイスは数ミリ秒から数秒以内に応答します
- 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(Type-Length-Value)レコード:
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 タイプのリファレンス
| タイプ | Hex | 名前 | 値の形式 | 備考 |
|---|---|---|---|---|
| 1 | 0x0001 | MAC アドレス | 6 バイトの生データ(ネットワークバイトオーダー) | インターフェースごとの MAC アドレスであり、シャーシの MAC アドレスではありません |
| 5 | 0x0005 | ID | UTF-8 文字列 | ホスト名 — すべてのインターフェースで共通です |
| 7 | 0x0007 | バージョン | UTF-8 文字列 | 例:7.18 (stable)、7.22rc1 |
| 8 | 0x0008 | プラットフォーム | UTF-8 文字列 | 通常は MikroTik です |
| 10 | 0x000a | 稼働時間 | 4 バイトの LE uint32 | 起動後の秒数 — リトルエンディアンの値はこれだけです |
| 11 | 0x000b | ソフトウェア ID | UTF-8 文字列 | ライセンス識別子 |
| 12 | 0x000c | ボード | UTF-8 文字列 | 例:RB4011iGS+5HacQ2HnD、CHR |
| 14 | 0x000e | 展開 | 1 バイト | ファームウェアの圧縮フラグ(IPv6 の有無を示すフラグとして扱うパーサーもあります) |
| 15 | 0x000f | IPv6 アドレス | 16 バイトの生データ | リンクローカルまたはグローバル IPv6 |
| 16 | 0x0010 | インターフェース名 | UTF-8 文字列 | ルーター上の送信インターフェース(例:ether1) |
| 17 | 0x0011 | IPv4 アドレス | 4 バイトの生データ(ネットワークバイトオーダー) | 送信インターフェースの IP |
信頼できる情報源: これらのタイプ ID は、正規の MAC-Telnet protocol.c
(MT_MNDPTYPE_* enum)および Wireshark の MNDP ディセクターの値テーブルと完全に一致します。特に、稼働時間はタイプ 10、ソフトウェア ID はタイプ 11、ボードはタイプ 12 です。
これらを 1 つずつずらしてしまう(稼働時間=11/ボード=13)のはよくある間違いで、実際の RouterOS パケットを気付かないまま誤解析します。
パッキング TLV(タイプ 2、3、9): /ip のパッキング/圧縮に関連します。安全にスキップできます。
複数インターフェースの動作
N 個のアクティブなインターフェースを持つ RouterOS デバイスは、インターフェースごとに 1 つ、合計 N 個の個別の MNDP アナウンスを送信します。各アナウンスには次の情報が含まれます。
- 異なる MAC アドレス(そのインターフェース固有の MAC アドレス)
- 異なるインターフェース名(TLV 16)
- 異なる IP アドレス(割り当てられている場合)
- 同じ ID(ホスト名)
これは想定された動作であり、バグではありません。結果を表示する際は、同じルーターが N 回表示されないよう、ID でグループ化してください。ID が工場出荷時のデフォルト(MikroTik)の場合は、MAC アドレスを使って区別してください。
タイミングと信頼性
| シナリオ | 想定される応答時間 |
|---|---|
| ローカル LAN(有線) | 1~3 秒 |
| Wi-Fi /混雑したネットワーク | 3~10 秒 |
| ZeroTier /トンネルオーバーレイ | 5~20 秒 |
| 衛星通信/高遅延 | 10~30 秒 |
ベストプラクティス: リッスン時間中に、5 秒ごとに複数回リフレッシュパケットを送信してください(WinBox の動作に合わせます)。パケット損失、Wi-Fi の省電力機能、またはトンネルリレーの遅延により最初のブロードキャストを受信できなかったデバイスも、その後のリフレッシュに応答します。
デバイスが見つからなくても、オフラインと判断してはいけません。 短いスキャン時間では結果が不完全になります。デバイスに到達できないと結論付ける前に、タイムアウトを延長してください。
RouterOS /ip/neighbor
/ip/neighbor は、MNDP(および CDP/LLDP)の検出結果を RouterOS 側から確認するビューです。ルーターに直接接続されたネットワーク上の他のデバイスから、ルーターが受信した情報が表示されます。
# 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 からは、ID(ホスト名)、バージョン、ボードモデル、IP アドレス、MAC アドレス、稼働時間が分かります
- この情報は攻撃者にとって有用です — MNDP は公開された SNMP コミュニティと同様に扱ってください
ソケット実装に関する注意事項
ポート共有(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 にブロードキャストを送信すると、OS はそのコピーを自身のソケットに返します。自身の 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;
}
リファレンス実装
| 言語 | ソース | 備考 |
|---|---|---|
| C | github.com/haakonnessjoen/MAC-Telnet(protocol.c、protocol.h) | 正準となる基準実装です。 TLV の列挙型、ntohs によるビッグエンディアン TLV、le32toh による uptime |
| C(ディセクター) | Wireshark epan/dissectors/packet-mndp.c | 公式プロトコルディセクターです。タイプ ID と ENC_BIG_ENDIAN TLV を確認できます |
| Go | github.com/middelink/mikrotik-fwupdate | プロトコルの照合に使用した補助実装 |
| Elixir | hex.pm mndp package | TLV のタイプ ID を確認できます |
| Swift | オープンソースの macOS 実装 | 生バイトによる MAC エンコードを確認できます |
検証済み: このスキルの TLV タイプ ID とバイトオーダーは、MAC-Telnet の protocol.c および Wireshark のディセクターと照合済みです。両方とも、TLV のタイプと長さはビッグエンディアンであり、uptime(タイプ 10)は唯一のリトルエンディアン値であることを確認しています。
関連スキル
- RouterOS の CLI/REST の基本については、
routeros-fundamentalsスキルを参照してください /console/inspectコマンドツリーについては、routeros-command-treeスキルを参照してください- パケットキャプチャと TZSP ストリーミングについては、
routeros-snifferスキルを参照してください - QEMU CHR のセットアップ(ハードウェアを使わないテスト)については、
routeros-qemu-chrスキルを参照してください
関連 MCP ツール
- RouterOS ドキュメントの検索には、
rosettaMCP サーバーのツール(routeros_search、routeros_get_page)を使用してください