diff --git a/docker-compose.yml b/docker-compose.yml index a28b61e..4e61634 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..56c1ebd --- /dev/null +++ b/docs/architecture.md @@ -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
CLI parsing (clap)"] + server["server.rs
Server mode"] + client["client.rs
Client mode"] + protocol["protocol.rs
Wire protocol types"] + auth["auth.rs
MD5 authentication"] + bandwidth["bandwidth.rs
Rate control & reporting"] + lib["lib.rs
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
(port 2000) + participant SRV as btest-rs Server + participant UDP as UDP Data
(port 2001+) + + MK->>TCP: Connect + SRV->>TCP: HELLO [01 00 00 00] + MK->>TCP: Command [16 bytes] + Note over SRV: Parse proto, direction,
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
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
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
(TCP or UDP, same as server) +``` + +## Threading Model + +```mermaid +graph TB + subgraph "Server Process" + LISTEN["Main Loop
Accept connections"] + LISTEN -->|spawn per client| HANDLER + + subgraph "Per-Client Tasks (tokio)" + HANDLER["Connection Handler
Handshake + Auth"] + HANDLER --> TX["TX Task
Send data packets"] + HANDLER --> RX["RX Task
Receive data packets"] + HANDLER --> STATUS["Status Loop
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) +``` diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..2b44610 --- /dev/null +++ b/docs/docker.md @@ -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 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) diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000..56c51af --- /dev/null +++ b/docs/protocol.md @@ -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
UDP 2001"] + C["Client binds
UDP 2257
(2001 + 256)"] + S <-->|"Data"| C + end + TCP["TCP 2000
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:
new_tx_speed = client_bytes * 8 * 1.5 + Note over C: If transmitting:
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 +``` diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..0435ae2 --- /dev/null +++ b/docs/user-guide.md @@ -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= direction=both protocol=udp user=admin password=mysecretpassword + +# For best results, use 1 connection +/tool/bandwidth-test address= 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 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 Bandwidth limit (e.g., 100M, 1G, 500K) + -P, --port Port number [default: 2000] + -a, --authuser Authentication username + -p, --authpass 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` |