Files
btest-rs/docs/protocol.md
Siavash Sameni 6a70e05454 Add comprehensive documentation
- docs/architecture.md: module structure, data flow, threading model (Mermaid diagrams)
- docs/protocol.md: complete wire protocol specification with packet formats
- docs/user-guide.md: server & client usage, CLI reference, troubleshooting
- docs/docker.md: Docker, Compose, registry push, deployment options
- Update docker-compose.yml with Gitea registry image tags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:06:14 +04:00

8.0 KiB
Raw Blame History

MikroTik Bandwidth Test Protocol Specification

This document describes the MikroTik btest wire protocol as reverse-engineered from RouterOS traffic captures. Based on the work of Alex Samorukov.

Connection Setup

All communication begins on TCP port 2000.

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: TCP connect to port 2000
    S->>C: HELLO [01 00 00 00]
    C->>S: Command [16 bytes]

    alt No authentication
        S->>C: OK [01 00 00 00]
    else MD5 authentication (RouterOS < 6.43)
        S->>C: AUTH_REQUIRED [02 00 00 00]
        S->>C: Challenge [16 random bytes]
        C->>S: Auth response [48 bytes]
        S->>C: OK [01 00 00 00] or FAILED [00 00 00 00]
    else EC-SRP5 authentication (RouterOS >= 6.43)
        S->>C: EC_SRP5 [03 00 00 00]
        Note over C,S: Not yet implemented
    end

    Note over C,S: Data transfer begins

Command Structure (16 bytes)

Sent by client after receiving HELLO.

Offset  Size  Type       Field              Description
──────  ────  ────       ─────              ───────────
0       1     uint8      protocol           0x00=UDP, 0x01=TCP
1       1     uint8      direction          Bit flags (server perspective)
2       1     uint8      random_data        0x00=random, 0x01=zeros
3       1     uint8      tcp_conn_count     Number of parallel TCP connections
4-5     2     uint16 LE  tx_size            Bytes per packet
6-7     2     uint16 LE  client_buf_size    Client buffer size (0=default)
8-11    4     uint32 LE  remote_tx_speed    Remote TX speed (bits/sec, 0=unlimited)
12-15   4     uint32 LE  local_tx_speed     Local TX speed (bits/sec, 0=unlimited)

Direction Flags

Direction bits describe what the server should do:

Value Name Server action Client action
0x01 DIR_RX Server receives Client transmits
0x02 DIR_TX Server transmits Client receives
0x03 DIR_BOTH Both directions Both directions

Important: The client inverts when constructing the command:

  • Client selects "transmit" → sends 0x01 (server should receive)
  • Client selects "receive" → sends 0x02 (server should transmit)

Default TX Sizes

Protocol Default tx_size
TCP 32768 (0x8000)
UDP 1500 (0x05DC)

Example Commands

TCP transmit:            01 01 01 00 00 80 00 00 00 00 00 00 00 00 00 00
TCP receive:             01 02 01 00 00 80 00 00 00 00 00 00 00 00 00 00
TCP both:                01 03 01 00 00 80 00 00 00 00 00 00 00 00 00 00
UDP transmit:            00 01 01 00 DC 05 00 00 00 00 00 00 00 00 00 00
UDP receive:             00 02 01 00 DC 05 00 00 00 00 00 00 00 00 00 00
UDP both:                00 03 01 00 DC 05 00 00 00 00 00 00 00 00 00 00

MD5 Authentication

Challenge-Response Flow

sequenceDiagram
    participant C as Client
    participant S as Server

    S->>C: [02 00 00 00] (auth required)
    S->>C: challenge [16 random bytes]

    Note over C: hash1 = MD5(password + challenge)
    Note over C: hash2 = MD5(password + hash1)
    Note over C: response = hash2[16] + username[32]

    C->>S: response [48 bytes]

    Note over S: Verify hash matches
    alt Valid
        S->>C: [01 00 00 00]
    else Invalid
        S->>C: [00 00 00 00]
    end

Hash Computation (Double MD5)

hash1 = MD5(password_bytes + challenge_16_bytes)
hash2 = MD5(password_bytes + hash1_16_bytes)

The 48-byte response is:

  • Bytes 0-15: hash2
  • Bytes 16-47: username, null-padded to 32 bytes

Known Test Vector

Password:  "test"
Challenge: ad32d6f94d28161625f2f390bb895637 (hex)
Expected:  3c968565bc0314f281a6da1571cf7255 (hex)

TCP Data Transfer

After handshake, data flows on the same TCP connection used for control.

graph LR
    subgraph "TCP Connection (port 2000)"
        H["Handshake"] --> D["Data Stream"]
    end
  • Packets are tx_size bytes (default 32768)
  • First byte is 0x07 (status message type marker)
  • No separate status exchange for TCP mode
  • Speed is limited by TCP flow control

UDP Data Transfer

Port Assignment

graph LR
    subgraph "Port Allocation"
        S["Server binds<br/>UDP 2001"]
        C["Client binds<br/>UDP 2257<br/>(2001 + 256)"]
        S <-->|"Data"| C
    end
    TCP["TCP 2000<br/>Status exchange"] -.->|"Port negotiation"| S
  1. Server selects port: 2001 + offset (increments per connection)
  2. Server sends port to client over TCP (2 bytes, big-endian)
  3. Client binds to server_port + 256
  4. Both sides connect() their UDP sockets to the peer

UDP Packet Format

Offset  Size  Type       Field
──────  ────  ────       ─────
0-3     4     uint32 BE  sequence_number
4+      var   bytes      payload (zeros or random)

Total packet size = tx_size from command (default 1500 bytes for UDP).

Status Message (12 bytes)

Exchanged every 1 second over the TCP control channel during UDP tests.

Offset  Size  Type       Field              Byte Order
──────  ────  ────       ─────              ──────────
0       1     uint8      msg_type           Always 0x07
1-4     4     uint32 BE  seq_number         Big-endian
5-7     3     bytes      padding            Always 00 00 00
8-11    4     uint32 LE  bytes_received     Little-endian

Status Exchange Pattern

sequenceDiagram
    participant S as Server
    participant C as Client

    Note over S,C: Every 1 second:

    C->>S: Status (bytes client received from server)
    S->>C: Status (bytes server received from client)

    Note over S: If transmitting:<br/>new_tx_speed = client_bytes * 8 * 1.5
    Note over C: If transmitting:<br/>new_tx_speed = server_bytes * 8 * 1.5

Key rules:

  • Status is always sent regardless of direction (unconditional)
  • Speed adjustment only applies when the sender is active
  • The 1.5x multiplier provides overshoot to converge quickly

Example Status Messages

Server sends: 07 00 00 00 01 00 00 00 C0 2D B4 02
              ── ─────────── ──────── ───────────
              type  seq=1    padding  bytes=45,362,624

Client sends: 07 D9 00 00 01 00 00 00 00 00 00 00
              ── ─────────── ──────── ───────────
              type  seq      padding  bytes=0

Speed Adjustment Algorithm

The dynamic speed adjustment uses a simple feedback loop:

graph TD
    A["Sender transmits at current rate"] --> B["Receiver counts bytes per second"]
    B --> C["Receiver sends byte count in status"]
    C --> D["Sender reads status"]
    D --> E{"bytes_received > 0?"}
    E -->|Yes| F["new_speed = bytes * 8 * 1.5"]
    E -->|No| G["Keep current speed"]
    F --> A
    G --> A

Interval Calculation

For a target speed in bits/sec and packet size in bytes:

interval_ns = (1,000,000,000 × packet_size × 8) / target_speed_bps

Special case: If interval > 500ms, clamp to exactly 1 second. This replicates a MikroTik behavior where very slow speeds get normalized to 1 packet/second.

NAT Mode

When -n / --nat flag is set, the client sends an empty UDP packet before starting the receive thread. This opens a hole in NAT firewalls to allow the server's UDP packets through.

Protocol Constants

BTEST_PORT              = 2000    TCP control port
BTEST_UDP_PORT_START    = 2001    First UDP data port
BTEST_PORT_CLIENT_OFFSET = 256   Client UDP port offset

HELLO                   = [01 00 00 00]
AUTH_OK                 = [01 00 00 00]
AUTH_REQUIRED           = [02 00 00 00]
AUTH_EC_SRP5            = [03 00 00 00]
AUTH_FAILED             = [00 00 00 00]

STATUS_MSG_TYPE         = 0x07
STATUS_MSG_SIZE         = 12 bytes