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` |