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:
@@ -1,6 +1,7 @@
|
||||
services:
|
||||
btest-server:
|
||||
build: .
|
||||
image: git.manko.yoga/manawenuz/btest-rs:latest
|
||||
container_name: btest-server
|
||||
ports:
|
||||
- "2000:2000/tcp"
|
||||
@@ -12,6 +13,7 @@ services:
|
||||
# Server with authentication enabled
|
||||
btest-server-auth:
|
||||
build: .
|
||||
image: git.manko.yoga/manawenuz/btest-rs:latest
|
||||
container_name: btest-server-auth
|
||||
ports:
|
||||
- "2010:2000/tcp"
|
||||
|
||||
203
docs/architecture.md
Normal file
203
docs/architecture.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# btest-rs Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
btest-rs is a Rust reimplementation of the MikroTik Bandwidth Test protocol. It operates in two modes: **server** (accepts connections from MikroTik devices) and **client** (connects to MikroTik btest servers).
|
||||
|
||||
## Module Structure
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
main["main.rs<br/>CLI parsing (clap)"]
|
||||
server["server.rs<br/>Server mode"]
|
||||
client["client.rs<br/>Client mode"]
|
||||
protocol["protocol.rs<br/>Wire protocol types"]
|
||||
auth["auth.rs<br/>MD5 authentication"]
|
||||
bandwidth["bandwidth.rs<br/>Rate control & reporting"]
|
||||
lib["lib.rs<br/>Public API for tests"]
|
||||
|
||||
main --> server
|
||||
main --> client
|
||||
main --> bandwidth
|
||||
server --> protocol
|
||||
server --> auth
|
||||
server --> bandwidth
|
||||
client --> protocol
|
||||
client --> auth
|
||||
client --> bandwidth
|
||||
lib --> server
|
||||
lib --> client
|
||||
lib --> protocol
|
||||
lib --> auth
|
||||
lib --> bandwidth
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Server Mode (MikroTik connects to us)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MK as MikroTik Client
|
||||
participant TCP as TCP Control<br/>(port 2000)
|
||||
participant SRV as btest-rs Server
|
||||
participant UDP as UDP Data<br/>(port 2001+)
|
||||
|
||||
MK->>TCP: Connect
|
||||
SRV->>TCP: HELLO [01 00 00 00]
|
||||
MK->>TCP: Command [16 bytes]
|
||||
Note over SRV: Parse proto, direction,<br/>tx_size, speeds
|
||||
|
||||
alt No auth configured
|
||||
SRV->>TCP: AUTH_OK [01 00 00 00]
|
||||
else MD5 auth
|
||||
SRV->>TCP: AUTH_REQUIRED [02 00 00 00]
|
||||
SRV->>TCP: Challenge [16 random bytes]
|
||||
MK->>TCP: Response [16 hash + 32 username]
|
||||
Note over SRV: Verify MD5(pass + MD5(pass + challenge))
|
||||
SRV->>TCP: AUTH_OK or AUTH_FAILED
|
||||
end
|
||||
|
||||
alt TCP mode
|
||||
Note over SRV,MK: Data flows on same TCP connection
|
||||
loop Every second
|
||||
SRV-->>SRV: Print bandwidth stats
|
||||
end
|
||||
else UDP mode
|
||||
SRV->>TCP: UDP port [2 bytes BE]
|
||||
Note over SRV: Bind UDP socket
|
||||
par TX Thread (if server transmits)
|
||||
loop Continuous
|
||||
SRV->>UDP: Data packets [seq + payload]
|
||||
end
|
||||
and RX Thread (if server receives)
|
||||
loop Continuous
|
||||
UDP->>SRV: Data packets [seq + payload]
|
||||
end
|
||||
and Status Loop (TCP control)
|
||||
loop Every 1 second
|
||||
MK->>TCP: Status [12 bytes]
|
||||
SRV->>TCP: Status [12 bytes]
|
||||
Note over SRV: Adjust TX speed<br/>based on client feedback
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Client Mode (we connect to MikroTik)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as btest-rs Client
|
||||
participant TCP as TCP Control
|
||||
participant MK as MikroTik Server
|
||||
|
||||
CLI->>TCP: Connect to MikroTik:2000
|
||||
MK->>TCP: HELLO
|
||||
CLI->>TCP: Command [16 bytes]
|
||||
Note over CLI: direction bits tell server<br/>what to do (TX/RX/BOTH)
|
||||
|
||||
alt Auth response 01
|
||||
Note over CLI: No auth, proceed
|
||||
else Auth response 02 (MD5)
|
||||
MK->>TCP: Challenge
|
||||
CLI->>TCP: MD5 response
|
||||
MK->>TCP: AUTH_OK
|
||||
else Auth response 03 (EC-SRP5)
|
||||
Note over CLI: Not supported yet
|
||||
end
|
||||
|
||||
Note over CLI,MK: Data transfer begins<br/>(TCP or UDP, same as server)
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Server Process"
|
||||
LISTEN["Main Loop<br/>Accept connections"]
|
||||
LISTEN -->|spawn per client| HANDLER
|
||||
|
||||
subgraph "Per-Client Tasks (tokio)"
|
||||
HANDLER["Connection Handler<br/>Handshake + Auth"]
|
||||
HANDLER --> TX["TX Task<br/>Send data packets"]
|
||||
HANDLER --> RX["RX Task<br/>Receive data packets"]
|
||||
HANDLER --> STATUS["Status Loop<br/>Exchange stats every 1s"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph "Shared State (Arc + Atomics)"
|
||||
STATE["BandwidthState"]
|
||||
TX_BYTES["tx_bytes: AtomicU64"]
|
||||
RX_BYTES["rx_bytes: AtomicU64"]
|
||||
TX_SPEED["tx_speed: AtomicU32"]
|
||||
RUNNING["running: AtomicBool"]
|
||||
end
|
||||
|
||||
TX --> TX_BYTES
|
||||
RX --> RX_BYTES
|
||||
STATUS --> TX_BYTES
|
||||
STATUS --> RX_BYTES
|
||||
STATUS --> TX_SPEED
|
||||
TX --> TX_SPEED
|
||||
TX --> RUNNING
|
||||
RX --> RUNNING
|
||||
STATUS --> RUNNING
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Tokio async runtime
|
||||
All I/O is async via tokio. Each client connection spawns independent tasks for TX, RX, and status exchange. This allows handling hundreds of concurrent connections on a single thread pool.
|
||||
|
||||
### 2. Lock-free shared state
|
||||
TX/RX threads and the status loop share bandwidth counters via `AtomicU64`. No mutexes needed — `swap(0)` atomically reads and resets counters each interval.
|
||||
|
||||
### 3. Sequential status loop (matching C pselect)
|
||||
The UDP status exchange uses a sequential timeout-read-then-send pattern rather than `tokio::select!`. This ensures our status messages are sent exactly every 1 second, preventing MikroTik's speed adaptation from seeing irregular feedback.
|
||||
|
||||
### 4. Direction bits from server perspective
|
||||
The direction byte in the protocol means what the **server** should do:
|
||||
- `0x01` (CMD_DIR_RX) = server receives
|
||||
- `0x02` (CMD_DIR_TX) = server transmits
|
||||
- `0x03` (CMD_DIR_BOTH) = bidirectional
|
||||
|
||||
The client inverts before sending: client "transmit" → `CMD_DIR_RX` (telling server to receive).
|
||||
|
||||
### 5. TCP socket half keepalive
|
||||
When only one direction is active (e.g., TX only), the unused socket half is kept alive. Dropping `OwnedWriteHalf` sends a TCP FIN, which MikroTik interprets as disconnection.
|
||||
|
||||
### 6. Static musl binary
|
||||
Release builds use musl for a fully static binary with zero runtime dependencies. The binary is 2 MB and runs on any Linux.
|
||||
|
||||
## File Layout
|
||||
|
||||
```
|
||||
btest-rs/
|
||||
├── src/
|
||||
│ ├── main.rs # CLI entry point, argument parsing
|
||||
│ ├── lib.rs # Public API (used by integration tests)
|
||||
│ ├── protocol.rs # Wire format: Command, StatusMessage, constants
|
||||
│ ├── auth.rs # MD5 challenge-response authentication
|
||||
│ ├── server.rs # Server mode: listener, TCP/UDP handlers
|
||||
│ ├── client.rs # Client mode: connector, TCP/UDP handlers
|
||||
│ └── bandwidth.rs # Rate limiting, formatting, shared state
|
||||
├── tests/
|
||||
│ └── integration_test.rs # End-to-end server/client tests
|
||||
├── scripts/
|
||||
│ ├── build-linux.sh # Cross-compile for x86_64 Linux
|
||||
│ ├── install-service.sh # systemd service installer
|
||||
│ ├── test-local.sh # Loopback self-test
|
||||
│ ├── test-mikrotik.sh # Test against MikroTik device
|
||||
│ └── test-docker.sh # Docker container test
|
||||
├── docs/
|
||||
│ ├── architecture.md # This file
|
||||
│ ├── protocol.md # Protocol specification
|
||||
│ ├── user-guide.md # Usage documentation
|
||||
│ └── docker.md # Docker & deployment guide
|
||||
├── Dockerfile # Production Docker image
|
||||
├── Dockerfile.cross # Cross-compilation for Linux x86_64
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── Cargo.toml
|
||||
└── btest-opensource/ # Original C implementation (git submodule)
|
||||
```
|
||||
222
docs/docker.md
Normal file
222
docs/docker.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Docker & Deployment Guide
|
||||
|
||||
## Container Registry
|
||||
|
||||
Images are published to:
|
||||
```
|
||||
git.manko.yoga/manawenuz/btest-rs
|
||||
```
|
||||
|
||||
## Quick Run (Ephemeral)
|
||||
|
||||
### Server (one-liner)
|
||||
|
||||
```bash
|
||||
# Build and run server directly
|
||||
docker build -t btest-rs . && \
|
||||
docker run --rm -it \
|
||||
-p 2000:2000/tcp \
|
||||
-p 2001-2100:2001-2100/udp \
|
||||
-p 2257-2356:2257-2356/udp \
|
||||
btest-rs -s -v
|
||||
|
||||
# With authentication
|
||||
docker run --rm -it \
|
||||
-p 2000:2000/tcp \
|
||||
-p 2001-2100:2001-2100/udp \
|
||||
-p 2257-2356:2257-2356/udp \
|
||||
btest-rs -s -a admin -p password -v
|
||||
```
|
||||
|
||||
### Client (one-liner)
|
||||
|
||||
```bash
|
||||
# TCP download test against MikroTik
|
||||
docker run --rm -it btest-rs -c 192.168.88.1 -r
|
||||
|
||||
# UDP bidirectional
|
||||
docker run --rm -it btest-rs -c 192.168.88.1 -t -r -u
|
||||
```
|
||||
|
||||
### Using pre-built image from registry
|
||||
|
||||
```bash
|
||||
# Pull from Gitea registry
|
||||
docker pull git.manko.yoga/manawenuz/btest-rs:latest
|
||||
|
||||
# Run server
|
||||
docker run --rm -it \
|
||||
-p 2000:2000/tcp \
|
||||
-p 2001-2100:2001-2100/udp \
|
||||
-p 2257-2356:2257-2356/udp \
|
||||
git.manko.yoga/manawenuz/btest-rs:latest -s -v
|
||||
```
|
||||
|
||||
## Docker Compose
|
||||
|
||||
### Basic server
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Server with authentication
|
||||
|
||||
```bash
|
||||
docker compose --profile auth up -d
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
btest-server:
|
||||
build: .
|
||||
image: git.manko.yoga/manawenuz/btest-rs:latest
|
||||
container_name: btest-server
|
||||
ports:
|
||||
- "2000:2000/tcp"
|
||||
- "2001-2100:2001-2100/udp"
|
||||
- "2257-2356:2257-2356/udp"
|
||||
command: ["-s", "-v"]
|
||||
restart: unless-stopped
|
||||
|
||||
btest-server-auth:
|
||||
build: .
|
||||
image: git.manko.yoga/manawenuz/btest-rs:latest
|
||||
container_name: btest-server-auth
|
||||
ports:
|
||||
- "2010:2000/tcp"
|
||||
- "2101-2200:2001-2100/udp"
|
||||
command: ["-s", "-a", "admin", "-p", "password", "-v"]
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- auth
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Local build (native)
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
# Binary at: target/release/btest
|
||||
```
|
||||
|
||||
### Cross-compile for Linux x86_64 (from macOS)
|
||||
|
||||
```bash
|
||||
scripts/build-linux.sh
|
||||
# Binary at: dist/btest (static musl, 2 MB)
|
||||
```
|
||||
|
||||
### Docker image build
|
||||
|
||||
```bash
|
||||
# Production image (for running)
|
||||
docker build -t btest-rs .
|
||||
|
||||
# With custom tag
|
||||
docker build -t git.manko.yoga/manawenuz/btest-rs:latest .
|
||||
docker build -t git.manko.yoga/manawenuz/btest-rs:0.1.0 .
|
||||
```
|
||||
|
||||
### Multi-platform build
|
||||
|
||||
```bash
|
||||
# Build for both ARM64 and x86_64
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t git.manko.yoga/manawenuz/btest-rs:latest \
|
||||
--push .
|
||||
```
|
||||
|
||||
## Push to Registry
|
||||
|
||||
```bash
|
||||
# Login to Gitea registry
|
||||
docker login git.manko.yoga
|
||||
|
||||
# Tag and push
|
||||
docker build -t git.manko.yoga/manawenuz/btest-rs:latest .
|
||||
docker push git.manko.yoga/manawenuz/btest-rs:latest
|
||||
|
||||
# Also tag with version
|
||||
docker tag git.manko.yoga/manawenuz/btest-rs:latest \
|
||||
git.manko.yoga/manawenuz/btest-rs:0.1.0
|
||||
docker push git.manko.yoga/manawenuz/btest-rs:0.1.0
|
||||
```
|
||||
|
||||
## Deployment on Linux Server
|
||||
|
||||
### Option 1: Docker
|
||||
|
||||
```bash
|
||||
docker run -d --name btest-server \
|
||||
--restart unless-stopped \
|
||||
-p 2000:2000/tcp \
|
||||
-p 2001-2100:2001-2100/udp \
|
||||
-p 2257-2356:2257-2356/udp \
|
||||
git.manko.yoga/manawenuz/btest-rs:latest \
|
||||
-s -a admin -p password -v
|
||||
```
|
||||
|
||||
### Option 2: Static binary + systemd
|
||||
|
||||
```bash
|
||||
# Copy binary to server
|
||||
scp dist/btest root@server:/usr/local/bin/btest
|
||||
|
||||
# Copy and run installer
|
||||
scp scripts/install-service.sh root@server:/tmp/
|
||||
ssh root@server "bash /tmp/install-service.sh --auth-user admin --auth-pass password"
|
||||
```
|
||||
|
||||
### Option 3: Docker Compose on server
|
||||
|
||||
```bash
|
||||
scp docker-compose.yml root@server:/opt/btest-rs/
|
||||
ssh root@server "cd /opt/btest-rs && docker compose up -d"
|
||||
```
|
||||
|
||||
## Port Reference
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| 2000 | TCP | Control channel (handshake, auth, status) |
|
||||
| 2001-2100 | UDP | Server-side data ports |
|
||||
| 2257-2356 | UDP | Client-side data ports (2001+256) |
|
||||
|
||||
### Firewall rules (iptables)
|
||||
|
||||
```bash
|
||||
iptables -A INPUT -p tcp --dport 2000 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 2001:2100 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 2257:2356 -j ACCEPT
|
||||
```
|
||||
|
||||
### Firewall rules (ufw)
|
||||
|
||||
```bash
|
||||
ufw allow 2000/tcp
|
||||
ufw allow 2001:2100/udp
|
||||
ufw allow 2257:2356/udp
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
```bash
|
||||
# Check if server is responding
|
||||
nc -zv <server-ip> 2000
|
||||
|
||||
# Check Docker container
|
||||
docker logs btest-server
|
||||
docker exec btest-server ps aux
|
||||
```
|
||||
|
||||
## Resource Usage
|
||||
|
||||
- **Memory**: ~5 MB base, +1 MB per active connection
|
||||
- **CPU**: Minimal when idle, scales with bandwidth
|
||||
- **Binary size**: 2 MB (static musl build)
|
||||
- **Docker image**: ~80 MB (Debian slim + binary)
|
||||
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
|
||||
```
|
||||
186
docs/user-guide.md
Normal file
186
docs/user-guide.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# btest-rs User Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Server mode (MikroTik connects to you)
|
||||
btest -s
|
||||
|
||||
# Client mode (you connect to MikroTik)
|
||||
btest -c 192.168.88.1 -r
|
||||
```
|
||||
|
||||
## Server Mode
|
||||
|
||||
Run btest-rs as a server and let MikroTik devices connect for bandwidth testing.
|
||||
|
||||
### Basic Server
|
||||
|
||||
```bash
|
||||
btest -s
|
||||
```
|
||||
|
||||
Listens on TCP port 2000 (default). Any MikroTik device can connect without authentication.
|
||||
|
||||
### Server with Authentication
|
||||
|
||||
```bash
|
||||
btest -s -a admin -p mysecretpassword
|
||||
```
|
||||
|
||||
MikroTik devices must provide matching credentials. Uses MD5 challenge-response authentication.
|
||||
|
||||
### Custom Port
|
||||
|
||||
```bash
|
||||
btest -s -P 3000
|
||||
```
|
||||
|
||||
### Verbose/Debug Output
|
||||
|
||||
```bash
|
||||
btest -s -v # Show connection info and debug messages
|
||||
btest -s -vv # Show hex dumps of status exchange (for debugging)
|
||||
```
|
||||
|
||||
### MikroTik Configuration (connecting to our server)
|
||||
|
||||
On the MikroTik device (WinBox or CLI):
|
||||
|
||||
```
|
||||
# CLI
|
||||
/tool/bandwidth-test address=<server-ip> direction=both protocol=udp user=admin password=mysecretpassword
|
||||
|
||||
# For best results, use 1 connection
|
||||
/tool/bandwidth-test address=<server-ip> direction=both protocol=udp connection-count=1
|
||||
```
|
||||
|
||||
Or via WinBox: **Tools → Bandwidth Test**, enter server address, credentials, and click Start.
|
||||
|
||||
## Client Mode
|
||||
|
||||
Connect to a MikroTik device's built-in bandwidth test server.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Enable btest server on MikroTik:
|
||||
```
|
||||
/tool/bandwidth-server set enabled=yes
|
||||
```
|
||||
|
||||
**Note**: If the MikroTik uses RouterOS >= 6.43 with authentication enabled, you'll need to either disable auth or use credentials. EC-SRP5 auth is not yet supported; MD5 auth works on older RouterOS versions.
|
||||
|
||||
### Download Test (receive)
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r
|
||||
```
|
||||
|
||||
Measures download speed from MikroTik to your machine.
|
||||
|
||||
### Upload Test (transmit)
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -t
|
||||
```
|
||||
|
||||
Measures upload speed from your machine to MikroTik.
|
||||
|
||||
### Bidirectional Test
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -t -r
|
||||
```
|
||||
|
||||
Tests both directions simultaneously.
|
||||
|
||||
### UDP Mode
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -u # UDP download
|
||||
btest -c 192.168.88.1 -t -u # UDP upload
|
||||
btest -c 192.168.88.1 -t -r -u # UDP bidirectional
|
||||
```
|
||||
|
||||
### Bandwidth Limiting
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -b 100M # Limit to 100 Mbps
|
||||
btest -c 192.168.88.1 -t -b 1G # Limit to 1 Gbps
|
||||
btest -c 192.168.88.1 -r -b 500K # Limit to 500 Kbps
|
||||
```
|
||||
|
||||
### NAT Traversal
|
||||
|
||||
If you're behind NAT and need to receive UDP data:
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -u -n
|
||||
```
|
||||
|
||||
The `-n` flag sends a probe packet to open the NAT firewall hole.
|
||||
|
||||
### With Authentication
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -a admin -p password
|
||||
```
|
||||
|
||||
## Reading the Output
|
||||
|
||||
```
|
||||
[ 1] TX 264.50 Mbps (33062912 bytes)
|
||||
[ 2] TX 263.98 Mbps (32997376 bytes)
|
||||
[ 2] RX 263.98 Mbps (32997012 bytes)
|
||||
[ 3] RX 430.51 Mbps (53813376 bytes) lost: 5
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `[ N]` | Interval number (1 per second) |
|
||||
| `TX` | Data we sent (upload) |
|
||||
| `RX` | Data we received (download) |
|
||||
| `Mbps` | Megabits per second |
|
||||
| `bytes` | Raw bytes transferred in this interval |
|
||||
| `lost: N` | UDP packets lost (UDP mode only) |
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
btest-rs — MikroTik Bandwidth Test server & client in Rust
|
||||
|
||||
Usage: btest [OPTIONS]
|
||||
|
||||
Options:
|
||||
-s, --server Run in server mode
|
||||
-c, --client <HOST> Run in client mode, connect to HOST
|
||||
-t, --transmit Client: upload test
|
||||
-r, --receive Client: download test
|
||||
-u, --udp Use UDP instead of TCP
|
||||
-b, --bandwidth <BW> Bandwidth limit (e.g., 100M, 1G, 500K)
|
||||
-P, --port <PORT> Port number [default: 2000]
|
||||
-a, --authuser <USER> Authentication username
|
||||
-p, --authpass <PASS> Authentication password
|
||||
-n, --nat NAT traversal mode
|
||||
-v, --verbose Increase log verbosity (-v, -vv)
|
||||
-h, --help Show help
|
||||
-V, --version Show version
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Use 1 connection** when MikroTik connects to your server. Multi-connection mode causes MikroTik's per-connection speed adaptation to throttle.
|
||||
- **TCP mode** generally gives more stable results than UDP due to TCP flow control.
|
||||
- **UDP mode** is better for measuring raw link capacity without TCP overhead.
|
||||
- **First interval** may show higher or lower numbers as the connection stabilizes. Look at intervals 3+ for steady-state throughput.
|
||||
- **WiFi testing**: bidirectional tests on WiFi will show lower per-direction speeds because WiFi is half-duplex at the MAC layer.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| `EC-SRP5 authentication not supported` | Disable auth on MikroTik btest server, or use older RouterOS |
|
||||
| `Connection refused` | Check port 2000 is open, firewall allows it |
|
||||
| Server shows 0 RX | Check MikroTik is actually sending (direction setting) |
|
||||
| Speed drops over time (server mode) | MikroTik client behavior — use 1 connection, or use our client mode instead |
|
||||
| UDP `lost` packets high | Network congestion or MTU issues, try reducing bandwidth with `-b` |
|
||||
Reference in New Issue
Block a user