Add client syslog events, fix client UDP TX error threshold
All checks were successful
CI / test (push) Successful in 1m26s

- Client mode now emits TEST_START and TEST_END syslog events
- Client UDP TX threshold raised from 1000 to 50000 with adaptive backoff
  (matching server behavior) — prevents premature TX death on macOS
- Updated all docs (README, user-guide, architecture, protocol, docker)
- Added results.csv to gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-01 09:40:52 +04:00
parent 751a9d5f13
commit 949c4908ad
9 changed files with 1143 additions and 195 deletions

View File

@@ -1,6 +1,6 @@
# 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).
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) and [Margin Research](https://github.com/MarginResearch/mikrotik_authentication).
## Connection Setup
@@ -24,7 +24,11 @@ sequenceDiagram
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
C->>S: MSG1 [len][username\0][client_pubkey:32][parity:1]
S->>C: MSG2 [len][server_pubkey:32][parity:1][salt:16]
C->>S: MSG3 [len][client_confirmation:32]
S->>C: MSG4 [len][server_confirmation:32]
S->>C: OK [01 00 00 00]
end
Note over C,S: Data transfer begins
@@ -32,11 +36,11 @@ sequenceDiagram
## Command Structure (16 bytes)
Sent by client after receiving HELLO.
Sent by the 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
@@ -58,8 +62,8 @@ Direction bits describe what the **server** should do:
| 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)
- Client selects "transmit" -> sends `0x01` (server should receive)
- Client selects "receive" -> sends `0x02` (server should transmit)
### Default TX Sizes
@@ -124,6 +128,184 @@ Challenge: ad32d6f94d28161625f2f390bb895637 (hex)
Expected: 3c968565bc0314f281a6da1571cf7255 (hex)
```
## EC-SRP5 Authentication
EC-SRP5 (Elliptic Curve Secure Remote Password) is used by RouterOS >= 6.43. It provides zero-knowledge password proof using Curve25519 in Weierstrass form.
### Auth Trigger
After the standard btest handshake (HELLO + Command), the server responds with one of:
```
01 00 00 00 -> No auth required
02 00 00 00 -> MD5 challenge-response (RouterOS < 6.43)
03 00 00 00 -> EC-SRP5 (RouterOS >= 6.43)
```
### Message Framing
Unlike Winbox (port 8291) which uses `[len:1][0x06][payload]`, the btest protocol uses a simpler framing:
```
[len:1][payload]
```
The `0x06` handler byte is omitted because the authentication context is implicit after receiving `03 00 00 00`.
| Protocol | Message framing |
|----------|----------------|
| Winbox (port 8291) | `[len:1][0x06][payload]` |
| **btest (port 2000)** | **`[len:1][payload]`** |
### EC-SRP5 Handshake (4 messages)
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
Note over S: Server sent 03 00 00 00
C->>S: MSG1: [len][username\0][client_pubkey:32][parity:1]
Note over C: len = username_len + 1 + 32 + 1
S->>C: MSG2: [len][server_pubkey:32][parity:1][salt:16]
Note over S: len = 49 (0x31)
C->>S: MSG3: [len][client_confirmation:32]
Note over C: len = 32 (0x20)
S->>C: MSG4: [len][server_confirmation:32]
Note over S: len = 32 (0x20)
Note over S: Then continues with normal btest flow:
S->>C: AUTH_OK [01 00 00 00]
S->>C: UDP port [2 bytes BE] (if UDP mode)
```
### Elliptic Curve: Curve25519 in Weierstrass Form
MikroTik's EC-SRP5 uses Curve25519 parameters but operates entirely in Weierstrass form, not the more common Montgomery or Edwards representations.
```
Prime field: p = 2^255 - 19
Curve order: r = 2^252 + 27742317777372353535851937790883648493
Montgomery A: 486662
Weierstrass parameters (converted from Montgomery):
a = 0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144
b = 0x7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864
Generator: lift_x(9) in Montgomery, converted to Weierstrass
Cofactor: 8
```
Public keys are transmitted as Montgomery x-coordinates (32 bytes big-endian) plus a 1-byte y-parity flag.
### Key Derivation
```
inner = SHA256(username + ":" + password)
salt = 16 random bytes (generated by server)
validator_priv (i) = SHA256(salt || inner)
validator_pub (x_gamma) = i * G
```
The server stores `salt` and `x_gamma` (the validator public key) for each user. In btest-rs, these are derived from the username and password at startup.
### Shared Secret Computation
**Client side (ECPESVDP-SRP-A):**
```
v = redp1(x_gamma, parity=1) # hash-to-curve of validator pubkey
w_b = lift_x(server_pubkey) + v # undo verifier blinding
j = SHA256(client_pubkey || server_pubkey)
scalar = (i * j + client_secret) mod r # combined scalar
Z = scalar * w_b # shared secret point
z = to_montgomery(Z).x # Montgomery x-coordinate
```
**Server side (ECPESVDP-SRP-B):**
```
gamma = redp1(x_gamma, parity=0)
w_a = lift_x(client_pubkey)
j = SHA256(client_pubkey || server_pubkey)
Z = server_secret * (w_a + j * gamma) # shared secret point
z = to_montgomery(Z).x
```
### Confirmation Codes
```
client_cc = SHA256(j || z)
server_cc = SHA256(j || client_cc || z)
```
Both sides verify the peer's confirmation code to ensure the shared secret matches. If either code is wrong, authentication fails.
### redp1 (Hash-to-Curve)
```
def redp1(x_bytes, parity):
x = SHA256(x_bytes)
while True:
x2 = SHA256(x)
point = lift_x(int(x2), parity)
if point is valid:
return point
x = (int(x) + 1).to_bytes(32)
```
This deterministically maps a byte string to a valid curve point by repeatedly hashing until a valid x-coordinate is found.
### Captured Exchange (from MITM analysis)
```
CLIENT -> SERVER (40 bytes):
27 61 6e 74 61 72 00 38 8a 37 36 52 6a 32 e9 87
4e 92 f8 c3 aa a1 18 da cd 71 b6 ab 76 fd 72 aa
c3 f6 6a 43 9b c8 a1 01
Decoded:
len=0x27 (39 bytes payload)
username="antar\0"
pubkey=388a373652...c8a1 (32 bytes)
parity=0x01
SERVER -> CLIENT (50 bytes):
31 6c c9 e3 1a 79 43 4a 40 51 de fd 55 cc 8d 6d
3c ec cd 73 19 1f a6 83 15 94 62 52 97 fe 5d 89
1a 00 3c ec 65 b8 34 28 0a 16 c5 48 0d 7b 50 00
e3 80
Decoded:
len=0x31 (49 bytes payload)
server_pubkey=6cc9e31a...5d891a (32 bytes)
parity=0x00
salt=3cec65b834280a16c5480d7b5000e380 (16 bytes)
CLIENT -> SERVER (33 bytes):
20 9b 1f 74 ec 40 31 2c ...
Decoded:
len=0x20 (32 bytes payload)
client_cc=9b1f74ec... (32 bytes, SHA256 proof)
SERVER -> CLIENT (33 bytes):
20 7d 59 b3 2e 28 6e 52 ...
Decoded:
len=0x20 (32 bytes payload)
server_cc=7d59b32e... (32 bytes, SHA256 proof)
POST-AUTH:
01 00 00 00 07 fa
Decoded:
AUTH_OK=01000000
UDP_port=0x07fa (2042)
```
## TCP Data Transfer
After handshake, data flows on the **same TCP connection** used for control.
@@ -163,7 +345,7 @@ graph LR
```
Offset Size Type Field
────── ──── ──── ─────
------ ---- ---- -----
0-3 4 uint32 BE sequence_number
4+ var bytes payload (zeros or random)
```
@@ -176,7 +358,7 @@ 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
@@ -208,11 +390,11 @@ sequenceDiagram
```
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
```
@@ -237,7 +419,7 @@ graph TD
For a target speed in bits/sec and packet size in bytes:
```
interval_ns = (1,000,000,000 × packet_size × 8) / target_speed_bps
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.
@@ -249,16 +431,19 @@ When `-n` / `--nat` flag is set, the client sends an empty UDP packet before sta
## 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
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]
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
STATUS_MSG_TYPE = 0x07
STATUS_MSG_SIZE = 12 bytes
DEFAULT_TCP_TX_SIZE = 32768 (0x8000)
DEFAULT_UDP_TX_SIZE = 1500 (0x05DC)
```