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>
This commit is contained in:
264
docs/protocol.md
Normal file
264
docs/protocol.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 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](https://github.com/samm-git/btest-opensource).
|
||||
|
||||
## Connection Setup
|
||||
|
||||
All communication begins on **TCP port 2000**.
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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:
|
||||
|
||||
```mermaid
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user