agentsclimarketplace

Routeros mac telnet

Skill tikoci/routeros-skills/routeros-mac-telnet

Custom instruction SKILL.md for MikroTik RouterOS v7

Install
npx -y skills add tikoci/routeros-skills --skill routeros-mac-telnet

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

MAC-Telnet protocol (MikroTik Layer-2 terminal/exec over UDP 20561) wire format, session handshake, and MD5 + MTWEI (EC-SRP) authentication. Use when: implementing or debugging a MAC-Telnet client/server, reaching a RouterOS device by MAC address without IP, parsing MAC-Telnet packets, understanding the WinBox-style L2 terminal, deciding between classic MD5 and modern MTWEI auth, or when the user mentions MAC-Telnet, mactelnet, mactelnetd, port 20561, or MTWEI/EC-SRP RouterOS login.

SKILL.md

25.0 KB, as published. Nobody here has run it

MAC-Telnet — MikroTik Layer-2 Terminal Protocol

MAC-Telnet lets you open an interactive RouterOS terminal (or run commands) addressing a device by its MAC address — you do not need to know or route to the target's IP. It works across a Layer-2 broadcast domain even when the target has no IP, a wrong IP, or an IP you cannot route to, which is why WISPs and provisioning tools rely on it. WinBox's "MAC Telnet" and the tools/mac-telnet CLI use this protocol family.

Why This Matters for Agents

  • It is the only way to get a shell on a freshly-unboxed or misconfigured RouterOS device that has no reachable IP.
  • The protocol carries its own 6+6-byte source/destination MAC addressing inside every packet, independent of the outer UDP/L2 delivery. This trips up implementers who assume normal UDP semantics.
  • Authentication has two incompatible modes — classic MD5 and modern MTWEI (EC-SRP over Curve25519). Current RouterOS 7.x defaults to MTWEI; getting the mode-detection wrong is the single most common failure.
  • It is simple enough to implement from scratch for the MD5 path; MTWEI needs an elliptic-curve library.

Protocol Basics

PropertyValue
TransportUDP
Port20561 (server listens; client may use an ephemeral or matching source port)
AddressingIn-packet 6-byte src + 6-byte dst MAC (the real addressing); outer UDP/IP is just transport
DeliveryLayer-2 — broadcast or unicast Ethernet frame to the target MAC; does not cross routers
ReliabilityApplication-level: byte-counter ACKs + timed retransmission (UDP gives none)
SessionStateful handshake → authenticated → raw terminal stream → teardown
AuthMD5 (legacy) or MTWEI / EC-SRP (current RouterOS 7.x default)
Max packet1500 bytes (MT_PACKET_LEN)

MAC-Telnet (UDP 20561) is a sibling of MNDP (UDP 5678) from the same MikroTik L2 toolset: MNDP discovers the device and its MAC; MAC-Telnet then connects to that MAC. See the routeros-mndp skill for discovery. They are otherwise independent wire formats — MNDP is a one-shot TLV announcement with no session, auth, control-block magic, or 00 15 client-type.

Packet Header (22 bytes)

Every SESSIONSTART / DATA / ACK / END session packet starts with a fixed 22-byte header. The session-key and client-type fields swap position by direction — this is the #1 footgun. (PING/PONG belong to the MAC-Ping tool and use an 18-byte variant; see Packet Types.)

Client → server layout:

Offset  Len  Field
0       1    version  = 0x01 (always 1)
1       1    packet type (see table below)
2       6    source MAC (the client's in-protocol address)
8       6    destination MAC (the target device)
14      2    session key   (uint16, BIG-endian)      ← client direction
16      2    client type   = 00 15                    ← client direction
18      4    counter       (uint32, BIG-endian)

Server → client layout is identical except the two middle fields swap:

14      2    client type   = 00 15                    ← server direction
16      2    session key   (uint16, BIG-endian)       ← server direction

So a parser must know the direction (or which MAC is "ours") to read the session key from the right offset. Reference: init_packet() in MAC-Telnet protocol.c keys both fields on mt_direction_fromserver.

  • Version is always 1. Reject other values as not-this-protocol.
  • Client type 00 15 is a fixed constant identifying a MAC-Telnet/WinBox client. Treat it as a magic constant; its internal meaning is not documented by MikroTik.
  • Counter is cumulative payload bytes, not a packet index — see below.

Packet Types (enum mt_ptype)

ValueNameDirectionPurpose
0SESSIONSTARTclient → serverOpen a session (header only, no payload)
1DATAbothCarries control blocks and/or terminal bytes; must be ACKed
2ACKbothAcknowledges received bytes (header only; counter = bytes seen)
4PINGeitherMAC-Ping liveness/latency probe — not the session keepalive (see below)
5PONGeitherReply to PING, echoing the PING's payload
255ENDeitherTear down the session

Note the gap: there is no type 3; PING/PONG are 4/5.

PING/PONG are the separate MAC-Ping tool, not session keepalive. In the canonical implementation a PING packet is 18 bytes, not 22: it is built like a normal header but bytes 14–17 (the swapped session-key/client-type fields) are zeroed and the 4-byte counter field at 18–21 is omitted (init_pingpacket). The responder echoes the payload back in a PONG. Do not use PING/PONG to keep a terminal session alive — that is done with empty ACKs (see Session Lifecycle and Reliability).

The Counter (ACK accounting — footgun)

The 32-bit counter is a running total of payload bytes, per direction — not a sequence/packet number.

  • SESSIONSTART and the first DATA use counter 0.
  • Each side advances its outbound counter by the number of payload bytes it sent (control-block bytes including their 9-byte headers, plus raw terminal bytes).
  • An ACK's counter = the received packet's counter + the length of that packet's payload (i.e. "I have now received this many bytes"). A peer keeps retransmitting a DATA packet until it sees an ACK whose counter reaches the end of that payload.
  • A DATA packet whose counter does not advance past what you've already processed is a retransmission: still ACK it (so the peer stops resending) but do not re-deliver its payload.

Getting this wrong produces a session that authenticates but then hangs on endless retransmits, or one that double-prints terminal output.

Control Blocks (the DATA payload format)

A DATA payload is zero or more control blocks, optionally followed/mixed with raw terminal data. Each control block:

Offset  Len  Field
0       4    magic  = 56 34 12 FF
4       1    control type (see table)
5       4    length (uint32, BIG-endian) — byte count of value
9       N    value
  • The 9-byte control header is MT_CPHEADER_LEN. The magic is 56 34 12 ff (mt_mactelnet_cpmagic).
  • Length is big-endian (htonl in the reference, even though it carries a 16-bit value).
  • PLAINDATA: any bytes in the payload that do not begin with the 4-byte magic are raw terminal data with no header. A parser reads blocks until the next 4 bytes aren't the magic, then treats the remainder as one PLAINDATA run. This is how keystrokes (client→server) and screen output (server→client) ride inside DATA packets. PLAINDATA is cptype -1 internally — never on the wire. Note there is no escaping: terminal bytes that happen to begin with 56 34 12 ff at a parse boundary would be misread as a control block. This is vanishingly rare for text terminals but matters for robust codec tests — the reference parser distinguishes PLAINDATA only by the absence of the magic at the current offset.

Control Types (enum mt_cptype)

ValueNameDirectionValue payload
0BEGINAUTHclient → serverempty (length 0) — starts auth
1PASSSALTboth (overloaded)server→client: salt (MD5) or pubkey+salt (MTWEI). client→server (MTWEI only): username\0 + client pubkey
2PASSWORDclient → serverMD5: 17 bytes. MTWEI: 32-byte EC-SRP proof
3USERNAMEclient → serverlogin name (byte string; commonly ASCII/UTF-8, no encoding is specified)
4TERM_TYPEclient → server$TERM (byte string, e.g. vt102, xterm)
5TERM_WIDTHclient → serveruint16 little-endian columns
6TERM_HEIGHTclient → serveruint16 little-endian rows
7PACKET_ERRORserver → clienterror string (byte string; auth failure, etc.)
9END_AUTHserver → clientempty — auth done, terminal mode begins

Note the gap: there is no control type 8; END_AUTH is 9.

Endianness trap: the control-block length is big-endian, but the terminal width/height values are little-endian uint16. Everything else multi-byte in the header (session key, counter) is big-endian.

Session Lifecycle

A successful client session, in order:

1. client → SESSIONSTART                     (counter 0, header only)
2. server → ACK
3. client → DATA: BEGINAUTH [+ MTWEI login block]   (see auth below)
4. server → DATA: PASSSALT  (16 bytes → MD5 mode; 49 bytes → MTWEI mode)
   client → ACK (always ACK inbound DATA)
5. client → DATA: PASSWORD + USERNAME + TERM_TYPE + TERM_WIDTH + TERM_HEIGHT
6. server → DATA: END_AUTH                    → terminal mode
7. ── interactive: server PLAINDATA (screen) / client PLAINDATA (keystrokes),
      each DATA ACKed; empty-ACK keepalive on idle (see below) ──
8. either → END                               → both close

Every inbound DATA is answered with an ACK (step counter = received counter + payload length). Keepalive is an empty ACK sent on idle: the reference client sends one after ~10 seconds of inactivity, and the server drops a session after ~15 seconds without traffic (MT_CONNECTION_TIMEOUT). END should be echoed/acknowledged, then the socket closed. (PING/PONG, despite the name, are the MAC-Ping tool — not part of this keepalive.)

Authentication

The server announces the mode by the length of the PASSSALT it sends:

Server PASSSALT lengthModeMeaning of bytes
16MD5 (legacy)16-byte random salt
49MTWEI / EC-SRP33-byte server public key ‖ 16-byte salt

Any other length is invalid — abort. This single check is the correct, robust way to detect the mode (do not rely on RouterOS version strings).

Classic MD5

Simple and library-free:

PASSWORD value = 0x00 ‖ MD5( 0x00 ‖ password ‖ salt )      // 17 bytes total
  • Leading 0x00, then the 16-byte MD5 digest of (0x00 + password bytes + 16-byte salt). The client sends only BEGINAUTH in step 3 and waits for the server's 16-byte salt.
  • Modern RouterOS no longer accepts MD5 in practice. Verified on stock CHR 7.23: the device offers a 16-byte salt to a classic client (one that sends only BEGINAUTH, with no MTWEI offer) but then rejects the MD5 proof even for credentials it accepts over REST/native-API ("login failure … via mac-telnet" in the device log). So a 16-byte salt does not mean MD5 will be honored — you must drive MTWEI to log in. MD5 stays useful only for genuinely legacy/downgraded gear.

MTWEI (EC-SRP over Curve25519) — current default

A zero-knowledge password proof: the password never crosses the wire and the exchange proves the client knows it without revealing it. It requires an elliptic-curve implementation. (Note: MTWEI authenticates the login but does not encrypt the subsequent terminal stream — MAC-Telnet has no transport cipher. Treat it as management-plane traffic on a trusted L2 segment.)

Wire-level flow (what differs from MD5):

  1. Client generates a keypair → 33-byte client public key. This is not the standard 32-byte X25519 wire format: MTWEI runs EC-SRP on a custom Curve25519-in-Weierstrass form and serializes the public point as a 32-byte big-endian X coordinate plus a 1-byte Y-parity flag (literal 0/1, not a SEC1 0x02/0x03 compressed-point tag). Use the reference MTWEI math, not an off-the-shelf X25519 API.
  2. In step 3 the client sends, alongside BEGINAUTH, a PASSSALT (type 1) block carrying username + 0x00 + the 33-byte client public key. (Yes — control type 1 is overloaded: server→client it is the salt; client→server in MTWEI it is the login+pubkey.)
  3. Server replies with a 49-byte PASSSALT = 33-byte server public key ‖ 16-byte salt.
  4. Client derives the SRP identity in two SHA-256 stages: v1 = SHA256(username ‖ ":" ‖ password), then id = SHA256(salt ‖ v1) (16-byte salt). It runs the EC-SRP exchange over both public keys using id to produce a 32-byte PASSWORD proof, sent with USERNAME + terminal blocks as in MD5.
  5. Server verifies; on success sends END_AUTH.

Username caveat: the C reference strips any +... console-parameter suffix from the login name only for the MTWEI identity hash (strsep(..., "+") before mtwei_id()), while still sending the original login string in the USERNAME control block. Implement this if you support RouterOS console parameters in the login field.

The cryptographic core (point arithmetic, Weierstrass↔Montgomery conversion, proof derivation) is non-trivial — implement against a reference rather than from this summary:

  • C: mtwei.c / mtwei.h in haakonnessjoen/MAC-Telnet (OpenSSL EC; EC-SRP per the IEEE P1363.2 draft, derived from Margin Research's PoC).
  • .NET: EcsrpEngine.cs / EcsrpMath.cs in KCTech-Lab/KC.MacTelnet.
  • m2ir: profiles/winbox-ipc/ec-srp5-handshake.yaml captures the related WinBox EC-SRP5 point encoding and is useful corroboration for the 32-byte-X + parity shape. Do not copy WinBox's later AES-128-CBC transport encryption into MAC-Telnet; MAC-Telnet's terminal stream is not encrypted.

Constants worth pinning in tests: client/server public key = 33 bytes (MTWEI_PUBKEY_LEN), validator/proof = 32 bytes (MTWEI_VALIDATOR_LEN).

Auth-mode pragmatics

To log into current RouterOS you must implement MTWEI — an MD5-only client gets a 16-byte salt but its proof is refused (see Classic MD5 above). A robust client offers MTWEI in BEGINAUTH by default and falls back to MD5 only when the device returns a 16-byte salt (legacy gear). tikoci/centrs does this — it implements both MD5 and MTWEI (src/protocols/mtwei.ts, a dependency-free BigInt port of mtwei.c) and is validated over real L2 against stock CHR 7.23.

END_AUTH does not mean the login succeeded. A failed login also sends END_AUTH, immediately followed by a PLAINDATA "Login failed, incorrect username or password" message and END. Confirm success only when real terminal output (a prompt/banner) arrives; treat END_AUTH → "Login failed" → END (or an END right after END_AUTH with no prompt) as auth failure. Grounded on CHR 7.23 via centrs.

After Login: The Interactive Console

END_AUTH drops you into RouterOS's normal readline console — the same shell you get over SSH/serial. These facts are grounded on stock CHR 7.23.1 (via tikoci/centrs's console reader) and matter whenever you scrape command output over MAC-Telnet rather than just relaying to a human's terminal:

  • Terminal-size probe. On connect the console runs a multi-step ANSI cursor probe to learn the terminal size: ESC[9999B / ESC[9999C (move the cursor far down / right), ESC[H (home), ESC D, ESC Z (DECID), mode sets (ESC[4l ESC[20l ESC[?7h ESC[?5l ESC[?25h), and repeated ESC[6n (DSR cursor-position report). Answer each ESC[6n with ESC[<row>;<col>R. Reporting a wide column makes the console use that width; ignore the probe and it falls back to 80 columns and wraps the echo, which breaks naive output scraping. The TERM_WIDTH/TERM_HEIGHT control blocks appear advisory — the ANSI probe wins.
  • ~10s negotiation stall. The MOTD/prompt does not appear until ~10 seconds after login, on every login. Answering the DSR sets the width but does not remove the stall — the probe seems to expect a real terminal's clamped cursor tracking across the down/up/right/home sequence, so a fixed DSR answer leaves it unsatisfied and the console waits out a timeout. A cursor-tracking emulator (track row/col in a rows×cols grid, answer each DSR with the real clamped position) is the way to avoid it. A human with a real terminal does not see this.
  • First-login license gate. A factory-fresh device prints the banner and Do you want to see the software license? [Y/n]: and blocks until answered — exactly the fresh/unconfigured gear MAC-Telnet exists for, so a robust client must detect and answer it (n). Later logins skip it.
  • Prompt, echo, and output shape. The prompt has two forms: [user@identity] > (root) and [user@identity] /path> (submenu), each with a trailing space; it is redrawn with CR + space-padding + CR (not ANSI erase). Keystrokes are echoed and the input line is redrawn. Command responses contain no ANSI — only CR, LF, spaces, and text — so a simple CR/LF terminal emulation (CR → column 0 with overwrite, LF → new line) reconstructs the screen. A response is: the echoed [prompt] > <command> line, then the output lines, then the trailing prompt; strip the first and last to get clean output. A successful write prints nothing (e.g. /ip/address/add … returns straight to the prompt — there is no .id on the console, unlike REST/the binary API).
  • Validate without running: :put [:parse "<command>"] over the console prints (evl …) for a good parse, syntax error (line N column M) for malformed CLI, and bad parameter <name> (line …) for an unknown attribute — so one console :parse checks both syntax and parameter validity. (Over REST the same string rides an HTTP-200 ret; over the binary API :put [:parse] returns an opaque object handle and reveals nothing.)

Reliability & Retransmission

UDP provides nothing, so the protocol layers on:

  • ACK-by-byte-counter (above) — a sender retransmits an unacknowledged DATA.
  • Timed retransmission schedule from the reference client, in milliseconds: {15, 20, 30, 50, 90, 170, 330, 660, 1000} (retransmit_intervals), i.e. a rough exponential backoff, up to 9 tries before giving up.
  • Empty-ACK keepalive so idle authenticated sessions are not dropped: the client sends an empty ACK after ~10s idle; the server times out a session after ~15s of silence. (PING/PONG is the MAC-Ping tool, not this keepalive.)

Transport & Socket Notes

  • Server: UDP 20561. To receive frames addressed to a device that has no IP, real clients often send to the broadcast address (or a raw Ethernet frame to the unicast target MAC) and rely on the in-packet destination MAC to let the right device claim the session. The device replies to the client MAC.
  • Because addressing is in-band, you generally do not need the target's IP — only its MAC (discovered via MNDP, ARP, or printed on the label).
  • A shared L2 segment means other devices' MAC-Telnet traffic can hit your socket. Filter rigorously: accept a packet only when version = 1, the session key matches, and the in-packet src/dst MACs mirror your own (their src = your dst, their dst = your src). Drop everything else.
  • Self-echo: broadcasting can loop your own packet back; ignore packets whose in-packet source MAC is yours.
  • Privilege: sending raw Ethernet frames (the most robust delivery) needs root / CAP_NET_RAW. A pure UDP-broadcast approach avoids that but is less universal.
  • Testing without a router: the codec and state machine are pure functions of bytes — drive them with a scripted in-memory peer (no L2 fabric needed), as centrs' unit tests do. Full end-to-end testing needs a real L2 segment (CHR-on-a-bridge or hardware), since broadcast/raw-frame delivery is the part a CI runner usually can't provide.

Security Considerations

  • MD5 auth is weak — salted MD5 of the password, no forward secrecy, and the 17-byte proof is replayable within a salt. Prefer MTWEI; disable legacy MD5 login on RouterOS unless required for old gear.
  • MTWEI does not transmit the password, but MAC-Telnet does not use MTWEI as a transport cipher for the terminal stream — treat it as a management-plane protocol for trusted L2 segments.
  • No transport authentication of the peer beyond the auth exchange; any device on the segment can attempt sessions. Restrict MAC-Telnet on RouterOS (/tool mac-server) to trusted interfaces.
  • Exposure: a reachable L2 segment + a weak password = shell. Lock down the MAC-server interface list the same way you would Telnet/SSH.

RouterOS-Side Surface

# Which interfaces accept incoming MAC-Telnet (server side)
/tool/mac-server/print
/tool/mac-server/set allowed-interface-list=LAN

# MAC WinBox (related L2 service) is configured separately
/tool/mac-server/mac-winbox/print

# From one RouterOS device to another, by MAC:
/tool/mac-telnet 64:D1:54:XX:XX:XX

Restricting allowed-interface-list to a trusted list is the primary hardening control; the default may allow all interfaces.

Reference Implementations

LangSourceNotes
Chaakonnessjoen/MAC-Telnet (protocol.c/.h, mactelnet.c, mactelnetd.c, mtwei.c/.h)Canonical ground truth. Header offsets, control magic, MD5 + MTWEI. MTWEI added 2022.
.NET 10KCTech-Lab/KC.MacTelnet (MacTelnetDriver/Proto/*, Proto/Auth/Ecsrp*)Modern client; implements MTWEI against current RouterOS 7.x; pluggable terminal engine.
TypeScripttikoci/centrs (src/protocols/mac-telnet.ts, src/protocols/mtwei.ts, src/protocols/mac-telnet-console.ts)Pure codec + injectable-sink session state machine with byte-counter retransmit + empty-ACK keepalive (a tick(now) driven by the console reader); MD5 + MTWEI (dependency-free BigInt EC-SRP port of mtwei.c); interactive-console reader (terminal-probe answering, license clear, prompt sync, CR/LF screen emulation, :parse gate) wired into an execute adapter; scripted-peer + real-byte unit tests and real-L2 CHR-7.23.1 integration (login, run, write, validate) via quickchr socket-connect.

Verified: the header layout (22 bytes, direction-swapped session-key/client- type), control-block magic 56 34 12 ff, big-endian length, little-endian terminal dimensions, packet/control type enums, the 17-byte MD5 proof, the 16-vs-49-byte PASSSALT mode detection, the MTWEI identity hash, and the 33-byte public-point encoding in this skill were cross-checked against MAC-Telnet's protocol.c / mactelnet.c / mtwei.c, the centrs codec + tests, and m2ir's related EC-SRP5 profile.

Provenance & Unknowns

Captured deliberately, with confidence noted:

  • High confidence (source-grounded): port 20561, the 22-byte header and its direction swap, control magic, packet/control type numbers, byte-counter ACK semantics, the empty-ACK keepalive (~10s client / ~15s server timeout), MD5 proof formula, the two-stage MTWEI identity hash, the 16-vs-49 PASSSALT detection, the MTWEI public-key (33) / proof (32) sizes, and the 32-byte-X + parity point encoding — all read directly from protocol.c, mactelnet.c, mactelnetd.c, and mtwei.c, with key codec behavior cross-checked against centrs and the point encoding corroborated by m2ir's WinBox EC-SRP5 profile.
  • C-reference-specific behavior: MTWEI strips a +... console-parameter suffix from the login name before hashing the SRP identity, but still sends the original login string in USERNAME. This is grounded in mactelnet.c; verify against your target implementation if you depend on console parameters.
  • Meaning of client type 00 15: treated as an opaque constant. MikroTik publishes no spec; do not attribute semantics to it.
  • MTWEI math details (curve constants, Weierstrass↔Montgomery conversion, proof derivation): not reproduced here on purpose — they are easy to get subtly wrong, and the 33-byte public key is not standard X25519. Implement against mtwei.c or EcsrpEngine.cs and pin the byte-length constants plus the 32-byte-X + parity encoding in tests.
  • PING/PONG (MAC-Ping): present in the enum (4/5) and used by the MAC-Ping latency tool with 18-byte packets; the exact MAC-Ping payload semantics are out of scope here and only sketched.
  • Exact UDP delivery (broadcast vs. raw unicast Ethernet, source port choice): varies by implementation and platform; the in-packet MAC addressing is the invariant. Verify delivery empirically on your target segment.
  • There is no official MikroTik protocol specification. All of the above is reverse-engineered and corroborated across the reference implementations cited.

Related Skills

  • routeros-mndp — discover the device and MAC to connect to (UDP 5678).
  • routeros-fundamentals — RouterOS CLI/REST basics for what you do once connected.
  • routeros-qemu-chr — boot CHR (e.g. bridged onto an L2 segment) to test MAC-Telnet without hardware.
  • routeros-sniffer — capture and inspect MAC-Telnet packets on the wire.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.