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:
Siavash Sameni
2026-03-31 13:06:14 +04:00
parent d71a3a4e71
commit 6a70e05454
5 changed files with 877 additions and 0 deletions

264
docs/protocol.md Normal file
View 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
```