Add client syslog events, fix client UDP TX error threshold
All checks were successful
CI / test (push) Successful in 1m26s
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:
@@ -13,22 +13,31 @@ graph TB
|
||||
client["client.rs<br/>Client mode"]
|
||||
protocol["protocol.rs<br/>Wire protocol types"]
|
||||
auth["auth.rs<br/>MD5 authentication"]
|
||||
ecsrp5["ecsrp5.rs<br/>EC-SRP5 authentication<br/>(Curve25519 Weierstrass)"]
|
||||
bandwidth["bandwidth.rs<br/>Rate control & reporting"]
|
||||
csv_output["csv_output.rs<br/>CSV result logging"]
|
||||
syslog["syslog_logger.rs<br/>Remote syslog (RFC 3164)"]
|
||||
lib["lib.rs<br/>Public API for tests"]
|
||||
|
||||
main --> server
|
||||
main --> client
|
||||
main --> bandwidth
|
||||
main --> csv_output
|
||||
main --> syslog
|
||||
server --> protocol
|
||||
server --> auth
|
||||
server --> ecsrp5
|
||||
server --> bandwidth
|
||||
server --> syslog
|
||||
client --> protocol
|
||||
client --> auth
|
||||
client --> ecsrp5
|
||||
client --> bandwidth
|
||||
lib --> server
|
||||
lib --> client
|
||||
lib --> protocol
|
||||
lib --> auth
|
||||
lib --> ecsrp5
|
||||
lib --> bandwidth
|
||||
```
|
||||
|
||||
@@ -60,8 +69,8 @@ sequenceDiagram
|
||||
SRV->>TCP: EC-SRP5 [03 00 00 00]
|
||||
MK->>TCP: [len][username\0][client_pubkey:32][parity:1]
|
||||
SRV->>TCP: [len][server_pubkey:32][parity:1][salt:16]
|
||||
MK->>TCP: [len][client_proof:32]
|
||||
SRV->>TCP: [len][server_proof:32]
|
||||
MK->>TCP: [len][client_confirmation:32]
|
||||
SRV->>TCP: [len][server_confirmation:32]
|
||||
Note over SRV: Curve25519 Weierstrass EC-SRP5<br/>See docs/ecsrp5-research.md
|
||||
SRV->>TCP: AUTH_OK [01 00 00 00]
|
||||
end
|
||||
@@ -105,14 +114,18 @@ sequenceDiagram
|
||||
CLI->>TCP: Command [16 bytes]
|
||||
Note over CLI: direction bits tell server<br/>what to do (TX/RX/BOTH)
|
||||
|
||||
alt Auth response 01
|
||||
alt Auth response 01 (no auth)
|
||||
Note over CLI: No auth, proceed
|
||||
else Auth response 02 (MD5)
|
||||
MK->>TCP: Challenge
|
||||
CLI->>TCP: MD5 response
|
||||
MK->>TCP: Challenge [16 random bytes]
|
||||
CLI->>TCP: MD5 response [48 bytes]
|
||||
MK->>TCP: AUTH_OK
|
||||
else Auth response 03 (EC-SRP5)
|
||||
Note over CLI: Not supported yet
|
||||
CLI->>TCP: [len][username\0][client_pubkey:32][parity:1]
|
||||
MK->>TCP: [len][server_pubkey:32][parity:1][salt:16]
|
||||
CLI->>TCP: [len][client_confirmation:32]
|
||||
MK->>TCP: [len][server_confirmation:32]
|
||||
MK->>TCP: AUTH_OK
|
||||
end
|
||||
|
||||
Note over CLI,MK: Data transfer begins<br/>(TCP or UDP, same as server)
|
||||
@@ -156,57 +169,81 @@ graph TB
|
||||
## 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.
|
||||
|
||||
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).
|
||||
The client inverts before sending: client "transmit" sends `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.
|
||||
|
||||
Release builds use musl for a fully static binary with zero runtime dependencies. The binary is approximately 2 MB and runs on any Linux distribution.
|
||||
|
||||
### 7. EC-SRP5 with big integer arithmetic
|
||||
|
||||
The EC-SRP5 implementation uses `num-bigint` for Curve25519 Weierstrass-form elliptic curve arithmetic. MikroTik's authentication uses the Weierstrass form (not the more common Montgomery or Edwards forms), requiring direct field arithmetic over the prime `2^255 - 19`. The implementation includes point multiplication, `lift_x`, `redp1` (hash-to-curve), and Montgomery coordinate conversion.
|
||||
|
||||
### 8. Global singletons for syslog and CSV
|
||||
|
||||
The syslog and CSV modules use `Mutex<Option<...>>` global statics. This avoids threading state through every function call while remaining safe. Both modules are initialized once at startup and used from any async task via their public API functions.
|
||||
|
||||
## 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
|
||||
│ ├── ecsrp5.rs # EC-SRP5 authentication (Curve25519 Weierstrass)
|
||||
│ ├── server.rs # Server mode: listener, TCP/UDP handlers
|
||||
│ ├── client.rs # Client mode: connector, TCP/UDP handlers
|
||||
│ └── bandwidth.rs # Rate limiting, formatting, shared state
|
||||
│ ├── main.rs # CLI entry point, argument parsing (clap)
|
||||
│ ├── lib.rs # Public API (used by integration tests)
|
||||
│ ├── protocol.rs # Wire format: Command, StatusMessage, constants
|
||||
│ ├── auth.rs # MD5 challenge-response authentication
|
||||
│ ├── ecsrp5.rs # EC-SRP5 authentication (Curve25519 Weierstrass)
|
||||
│ ├── server.rs # Server mode: listener, TCP/UDP handlers
|
||||
│ ├── client.rs # Client mode: connector, TCP/UDP handlers
|
||||
│ ├── bandwidth.rs # Rate limiting, formatting, shared state
|
||||
│ ├── csv_output.rs # CSV result logging (append-mode, auto-header)
|
||||
│ └── syslog_logger.rs # Remote syslog sender (RFC 3164 / BSD format)
|
||||
├── 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
|
||||
│ ├── build-linux.sh # Cross-compile for x86_64 Linux (musl)
|
||||
│ ├── build-macos-release.sh # macOS release build
|
||||
│ ├── install-service.sh # systemd service installer
|
||||
│ ├── push-docker.sh # Push Docker image to registry
|
||||
│ ├── test-local.sh # Loopback self-test
|
||||
│ ├── test-mikrotik.sh # Test against MikroTik device
|
||||
│ ├── test-docker.sh # Docker container test
|
||||
│ └── debug-capture.sh # Packet capture for debugging
|
||||
├── 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)
|
||||
│ ├── architecture.md # This file
|
||||
│ ├── protocol.md # Protocol specification
|
||||
│ ├── user-guide.md # Usage documentation
|
||||
│ ├── docker.md # Docker & deployment guide
|
||||
│ ├── ecsrp5-research.md # EC-SRP5 reverse-engineering notes
|
||||
│ └── man/
|
||||
│ └── btest.1 # Unix manual page (troff format)
|
||||
├── Dockerfile # Production Docker image (multi-stage)
|
||||
├── Dockerfile.cross # Cross-compilation for Linux x86_64
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── Cargo.toml # Rust package manifest
|
||||
├── Cargo.lock # Dependency lock file
|
||||
├── LICENSE # MIT License
|
||||
└── btest-opensource/ # Original C implementation (git submodule)
|
||||
```
|
||||
|
||||
154
docs/docker.md
154
docs/docker.md
@@ -1,26 +1,42 @@
|
||||
# Docker & Deployment Guide
|
||||
# Docker and Deployment Guide
|
||||
|
||||
## Container Registry
|
||||
|
||||
Images are published to:
|
||||
|
||||
```
|
||||
git.manko.yoga/manawenuz/btest-rs
|
||||
```
|
||||
|
||||
## Quick Run (Ephemeral)
|
||||
## Quick Start
|
||||
|
||||
### Server (one-liner)
|
||||
### Docker Compose (recommended)
|
||||
|
||||
```bash
|
||||
# Server with no authentication
|
||||
docker compose up -d
|
||||
|
||||
# Server with authentication
|
||||
docker compose --profile auth up -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
### One-liner server
|
||||
|
||||
```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
|
||||
### One-liner server with authentication
|
||||
|
||||
```bash
|
||||
docker run --rm -it \
|
||||
-p 2000:2000/tcp \
|
||||
-p 2001-2100:2001-2100/udp \
|
||||
@@ -28,7 +44,28 @@ docker run --rm -it \
|
||||
btest-rs -s -a admin -p password -v
|
||||
```
|
||||
|
||||
### Client (one-liner)
|
||||
### Server with EC-SRP5 authentication
|
||||
|
||||
```bash
|
||||
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 --ecsrp5 -v
|
||||
```
|
||||
|
||||
### Server with syslog and CSV
|
||||
|
||||
```bash
|
||||
docker run --rm -it \
|
||||
-p 2000:2000/tcp \
|
||||
-p 2001-2100:2001-2100/udp \
|
||||
-p 2257-2356:2257-2356/udp \
|
||||
-v /var/log/btest:/data \
|
||||
btest-rs -s -a admin -p password --syslog 192.168.1.1:514 --csv /data/results.csv -v
|
||||
```
|
||||
|
||||
### Client mode
|
||||
|
||||
```bash
|
||||
# TCP download test against MikroTik
|
||||
@@ -36,6 +73,14 @@ 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
|
||||
|
||||
# Timed test with CSV output
|
||||
docker run --rm -it \
|
||||
-v $(pwd):/data \
|
||||
btest-rs -c 192.168.88.1 -r -d 30 --csv /data/results.csv
|
||||
|
||||
# With authentication
|
||||
docker run --rm -it btest-rs -c 192.168.88.1 -r -a admin -p password
|
||||
```
|
||||
|
||||
### Using pre-built image from registry
|
||||
@@ -54,18 +99,24 @@ docker run --rm -it \
|
||||
|
||||
## Docker Compose
|
||||
|
||||
### Basic server
|
||||
The `docker-compose.yml` file provides two service profiles:
|
||||
|
||||
### Default profile (no auth)
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Server with authentication
|
||||
Starts a server on port 2000 with verbose logging and no authentication.
|
||||
|
||||
### Auth profile
|
||||
|
||||
```bash
|
||||
docker compose --profile auth up -d
|
||||
```
|
||||
|
||||
Starts an additional server on port 2010 with MD5 authentication (user: admin, password: password).
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
@@ -94,7 +145,23 @@ services:
|
||||
- auth
|
||||
```
|
||||
|
||||
## Building
|
||||
## Dockerfile
|
||||
|
||||
The production Dockerfile uses a multi-stage build:
|
||||
|
||||
1. **Build stage** -- Rust 1.86 slim image, compiles a release binary
|
||||
2. **Runtime stage** -- Debian Bookworm slim, copies only the binary
|
||||
|
||||
The resulting image is approximately 80 MB. The binary itself is about 2 MB.
|
||||
|
||||
Exposed ports:
|
||||
- `2000/tcp` -- control channel
|
||||
- `2001-2100/udp` -- server-side data ports
|
||||
- `2257-2356/udp` -- client-side data ports
|
||||
|
||||
Default entrypoint: `btest -s`
|
||||
|
||||
## Building Images
|
||||
|
||||
### Local build (native)
|
||||
|
||||
@@ -107,24 +174,23 @@ cargo build --release
|
||||
|
||||
```bash
|
||||
scripts/build-linux.sh
|
||||
# Binary at: dist/btest (static musl, 2 MB)
|
||||
# Binary at: dist/btest (static musl, ~2 MB)
|
||||
```
|
||||
|
||||
### Docker image build
|
||||
|
||||
```bash
|
||||
# Production image (for running)
|
||||
# Production image
|
||||
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 .
|
||||
docker build -t git.manko.yoga/manawenuz/btest-rs:0.5.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 \
|
||||
@@ -143,13 +209,13 @@ 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
|
||||
git.manko.yoga/manawenuz/btest-rs:0.5.0
|
||||
docker push git.manko.yoga/manawenuz/btest-rs:0.5.0
|
||||
```
|
||||
|
||||
## Deployment on Linux Server
|
||||
## Deployment Options
|
||||
|
||||
### Option 1: Docker
|
||||
### Option 1: Docker (single container)
|
||||
|
||||
```bash
|
||||
docker run -d --name btest-server \
|
||||
@@ -158,7 +224,7 @@ docker run -d --name btest-server \
|
||||
-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
|
||||
-s -a admin -p password --ecsrp5 -v
|
||||
```
|
||||
|
||||
### Option 2: Static binary + systemd
|
||||
@@ -167,11 +233,28 @@ docker run -d --name btest-server \
|
||||
# Copy binary to server
|
||||
scp dist/btest root@server:/usr/local/bin/btest
|
||||
|
||||
# Copy and run installer
|
||||
# Run the installer
|
||||
scp scripts/install-service.sh root@server:/tmp/
|
||||
ssh root@server "bash /tmp/install-service.sh --auth-user admin --auth-pass password"
|
||||
```
|
||||
|
||||
The installer script:
|
||||
- Creates a dedicated `btest` system user
|
||||
- Installs a hardened systemd unit with security options (NoNewPrivileges, ProtectSystem, PrivateTmp)
|
||||
- Grants `CAP_NET_BIND_SERVICE` for binding to ports below 1024
|
||||
- Enables and starts the service
|
||||
- Supports `--auth-user`, `--auth-pass`, and `--port` options
|
||||
|
||||
Useful systemd commands after installation:
|
||||
|
||||
```bash
|
||||
systemctl status btest # Check status
|
||||
systemctl stop btest # Stop the service
|
||||
systemctl restart btest # Restart
|
||||
journalctl -u btest -f # Follow logs
|
||||
systemctl disable btest # Disable autostart
|
||||
```
|
||||
|
||||
### Option 3: Docker Compose on server
|
||||
|
||||
```bash
|
||||
@@ -183,9 +266,9 @@ ssh root@server "cd /opt/btest-rs && docker compose up -d"
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| 2000 | TCP | Control channel (handshake, auth, status) |
|
||||
| 2000 | TCP | Control channel (handshake, auth, status exchange) |
|
||||
| 2001-2100 | UDP | Server-side data ports |
|
||||
| 2257-2356 | UDP | Client-side data ports (2001+256) |
|
||||
| 2257-2356 | UDP | Client-side data ports (server_port + 256) |
|
||||
|
||||
### Firewall rules (iptables)
|
||||
|
||||
@@ -203,20 +286,35 @@ ufw allow 2001:2100/udp
|
||||
ufw allow 2257:2356/udp
|
||||
```
|
||||
|
||||
### Firewall rules (nftables)
|
||||
|
||||
```bash
|
||||
nft add rule inet filter input tcp dport 2000 accept
|
||||
nft add rule inet filter input udp dport 2001-2100 accept
|
||||
nft add rule inet filter input udp dport 2257-2356 accept
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
```bash
|
||||
# Check if server is responding
|
||||
# Check if server is responding (TCP handshake)
|
||||
nc -zv <server-ip> 2000
|
||||
|
||||
# Check Docker container
|
||||
# Check Docker container status
|
||||
docker logs btest-server
|
||||
docker exec btest-server ps aux
|
||||
docker ps --filter name=btest-server
|
||||
|
||||
# Check systemd service
|
||||
systemctl status btest
|
||||
journalctl -u btest --since "5 minutes ago"
|
||||
```
|
||||
|
||||
## 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)
|
||||
| Resource | Value |
|
||||
|----------|-------|
|
||||
| Memory (idle) | ~5 MB |
|
||||
| Memory (per active connection) | +1 MB |
|
||||
| CPU | Minimal when idle, scales with bandwidth |
|
||||
| Binary size | ~2 MB (static musl build) |
|
||||
| Docker image | ~80 MB (Debian slim + binary) |
|
||||
|
||||
365
docs/man/btest.1
Normal file
365
docs/man/btest.1
Normal file
@@ -0,0 +1,365 @@
|
||||
.\" btest-rs manual page
|
||||
.\" Generated for btest-rs v0.5.0
|
||||
.TH BTEST 1 "2026-03-31" "btest-rs 0.5.0" "User Commands"
|
||||
.SH NAME
|
||||
btest \- MikroTik Bandwidth Test server and client
|
||||
.SH SYNOPSIS
|
||||
.B btest
|
||||
.B \-s
|
||||
.RI [ OPTIONS ]
|
||||
.br
|
||||
.B btest
|
||||
.B \-c
|
||||
.I HOST
|
||||
.RB { \-t | \-r }
|
||||
.RI [ OPTIONS ]
|
||||
.SH DESCRIPTION
|
||||
.B btest
|
||||
is a Rust reimplementation of the MikroTik Bandwidth Test (btest) protocol.
|
||||
It can operate as a server (accepting connections from MikroTik RouterOS
|
||||
devices or other btest clients) or as a client (connecting to a MikroTik
|
||||
device's built-in bandwidth test server).
|
||||
.PP
|
||||
The server listens on TCP port 2000 by default. MikroTik devices connect
|
||||
to this port for handshake, authentication, and status exchange. UDP data
|
||||
transfer uses ports 2001 and above.
|
||||
.PP
|
||||
Both MD5 challenge-response (RouterOS < 6.43) and EC-SRP5 Curve25519
|
||||
(RouterOS >= 6.43) authentication are supported.
|
||||
.SH OPTIONS
|
||||
.SS "Mode Selection"
|
||||
.TP
|
||||
.BR \-s ", " \-\-server
|
||||
Run in server mode. Listen for incoming connections from MikroTik devices
|
||||
or other btest clients. Conflicts with
|
||||
.BR \-c .
|
||||
.TP
|
||||
.BI \-c " HOST" "\fR, \fP" \-\-client " HOST"
|
||||
Run in client mode, connecting to the specified
|
||||
.IR HOST .
|
||||
The host can be an IPv4 address, IPv6 address, or hostname. Conflicts with
|
||||
.BR \-s .
|
||||
.SS "Test Direction (client mode)"
|
||||
.TP
|
||||
.BR \-t ", " \-\-transmit
|
||||
Client transmits data to the server (upload test). Can be combined with
|
||||
.B \-r
|
||||
for bidirectional testing.
|
||||
.TP
|
||||
.BR \-r ", " \-\-receive
|
||||
Client receives data from the server (download test). Can be combined with
|
||||
.B \-t
|
||||
for bidirectional testing.
|
||||
.SS "Protocol and Transfer"
|
||||
.TP
|
||||
.BR \-u ", " \-\-udp
|
||||
Use UDP instead of TCP for data transfer. UDP uses separate data ports
|
||||
(2001+ server side, 2257+ client side) and exchanges status messages
|
||||
over the TCP control channel every second.
|
||||
.TP
|
||||
.BI \-b " BW" "\fR, \fP" \-\-bandwidth " BW"
|
||||
Target bandwidth limit for the test. Accepts suffixes:
|
||||
.B K
|
||||
(kilobits/sec),
|
||||
.B M
|
||||
(megabits/sec),
|
||||
.B G
|
||||
(gigabits/sec). Examples:
|
||||
.BR 100M ", " 1G ", " 500K .
|
||||
Default is 0 (unlimited).
|
||||
.TP
|
||||
.BI \-P " PORT" "\fR, \fP" \-\-port " PORT"
|
||||
TCP port to listen on in server mode or connect to in client mode.
|
||||
Default: 2000.
|
||||
.SS "Network Binding (server mode)"
|
||||
.TP
|
||||
.BI \-\-listen " ADDR"
|
||||
IPv4 address to bind the server listener to. Use
|
||||
.B none
|
||||
to disable IPv4 listening entirely (useful with
|
||||
.B \-\-listen6
|
||||
for IPv6-only mode). Default: 0.0.0.0.
|
||||
.TP
|
||||
.BI \-\-listen6 " \fR[\fPADDR\fR]\fP"
|
||||
Enable the IPv6 listener. If no address is given, binds to
|
||||
.BR :: .
|
||||
Experimental: TCP over IPv6 works fully on all platforms. UDP over IPv6
|
||||
has issues on macOS due to kernel ENOBUFS limitations. On Linux, IPv6 UDP
|
||||
works correctly.
|
||||
.SS "Authentication"
|
||||
.TP
|
||||
.BI \-a " USER" "\fR, \fP" \-\-authuser " USER"
|
||||
Authentication username. In server mode, connecting clients must provide
|
||||
this username. In client mode, this username is sent to the server.
|
||||
.TP
|
||||
.BI \-p " PASS" "\fR, \fP" \-\-authpass " PASS"
|
||||
Authentication password. In server mode, connecting clients must provide
|
||||
a matching password. In client mode, this password is used to authenticate
|
||||
with the server.
|
||||
.TP
|
||||
.B \-\-ecsrp5
|
||||
Use EC-SRP5 authentication (Curve25519 Weierstrass). In server mode, this
|
||||
causes the server to advertise EC-SRP5 instead of MD5 to connecting clients.
|
||||
Required for RouterOS >= 6.43 devices. In client mode, the authentication
|
||||
type is auto-detected from the server's response and this flag is not needed.
|
||||
.SS "Test Control"
|
||||
.TP
|
||||
.BI \-d " SECS" "\fR, \fP" \-\-duration " SECS"
|
||||
Test duration in seconds (client mode only). The client exits cleanly after
|
||||
the specified number of seconds. A value of 0 means unlimited (run until
|
||||
interrupted with Ctrl-C). Default: 0.
|
||||
.TP
|
||||
.BR \-n ", " \-\-nat
|
||||
NAT traversal mode. Sends an empty UDP probe packet to the server before
|
||||
starting the receive thread, opening a hole in NAT firewalls. Only relevant
|
||||
for UDP receive tests when the client is behind NAT.
|
||||
.SS "Logging and Output"
|
||||
.TP
|
||||
.BI \-\-csv " FILE"
|
||||
Output test results to a CSV file. Appends a row for each completed test.
|
||||
Creates the file with a header row if it does not exist. Columns:
|
||||
timestamp, host, port, protocol, direction, duration_s, tx_avg_mbps,
|
||||
rx_avg_mbps, tx_bytes, rx_bytes, lost_packets, auth_type.
|
||||
.TP
|
||||
.BR \-q ", " \-\-quiet
|
||||
Suppress per-second bandwidth output to the terminal. Useful in combination
|
||||
with
|
||||
.B \-\-csv
|
||||
for machine-readable-only output, or when running as a background service.
|
||||
.TP
|
||||
.BI \-\-syslog " HOST:PORT"
|
||||
Send structured log events to a remote syslog server via UDP. Uses RFC 3164
|
||||
(BSD syslog) format with facility local0. Events include AUTH_SUCCESS,
|
||||
AUTH_FAILURE, TEST_START, and TEST_END with detailed metadata.
|
||||
Example:
|
||||
.BR \-\-syslog\ 192.168.1.1:514 .
|
||||
.TP
|
||||
.BR \-v ", " \-\-verbose
|
||||
Increase log verbosity. Can be repeated for more detail:
|
||||
.RS
|
||||
.TP
|
||||
.B \-v
|
||||
Debug messages (connection lifecycle, authentication steps).
|
||||
.TP
|
||||
.B \-vv
|
||||
Trace messages (hex dumps of protocol exchange).
|
||||
.TP
|
||||
.B \-vvv
|
||||
Maximum verbosity.
|
||||
.RE
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
Print help information and exit.
|
||||
.TP
|
||||
.BR \-V ", " \-\-version
|
||||
Print version information and exit.
|
||||
.SH EXAMPLES
|
||||
.SS "Server Mode"
|
||||
Start a basic server with no authentication:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -s
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Server with MD5 authentication:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -s -a admin -p password
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Server with EC-SRP5 authentication (RouterOS >= 6.43):
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -s -a admin -p password --ecsrp5
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Server with syslog and CSV logging:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -s -a admin -p password --syslog 10.0.0.1:514 --csv /var/log/btest.csv
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Server listening on IPv4 and IPv6:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -s --listen6
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Server on a custom port with debug output:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -s -P 3000 -v
|
||||
.fi
|
||||
.RE
|
||||
.SS "Client Mode"
|
||||
TCP download test:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -r
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
TCP upload test:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -t
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Bidirectional TCP test:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -t -r
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
UDP download test:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -r -u
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
UDP bidirectional with bandwidth limit:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -t -r -u -b 100M
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Timed test (30 seconds) with CSV output:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -r -d 30 --csv results.csv
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Quiet mode with CSV only:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -r -d 60 --csv results.csv -q
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
With authentication:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -r -a admin -p password
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
UDP receive through NAT:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
btest -c 192.168.88.1 -r -u -n
|
||||
.fi
|
||||
.RE
|
||||
.SH PORTS
|
||||
.TP
|
||||
.B 2000/tcp
|
||||
Control channel. Used for handshake, authentication, and status exchange.
|
||||
.TP
|
||||
.B 2001-2100/udp
|
||||
Server-side UDP data ports. Each connection uses the next available port
|
||||
starting from 2001.
|
||||
.TP
|
||||
.B 2257-2356/udp
|
||||
Client-side UDP data ports. Offset from server port by 256.
|
||||
.SH EXIT STATUS
|
||||
.TP
|
||||
.B 0
|
||||
Success. The test completed normally or the duration expired.
|
||||
.TP
|
||||
.B 1
|
||||
Error. Failed to connect, authentication failed, or invalid arguments.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B RUST_LOG
|
||||
Override the log filter. When set, takes precedence over the
|
||||
.B \-v
|
||||
flag. Example:
|
||||
.BR RUST_LOG=trace .
|
||||
.SH FILES
|
||||
.TP
|
||||
.I /usr/local/bin/btest
|
||||
Default installation path for the binary.
|
||||
.TP
|
||||
.I /etc/systemd/system/btest.service
|
||||
systemd unit file created by the install-service.sh script.
|
||||
.SH AUTHENTICATION
|
||||
.B btest
|
||||
supports two authentication schemes:
|
||||
.TP
|
||||
.B MD5 (legacy)
|
||||
Double MD5 challenge-response. Compatible with RouterOS versions before 6.43.
|
||||
The server sends a 16-byte random challenge. The client responds with
|
||||
MD5(password + MD5(password + challenge)) and the username.
|
||||
.TP
|
||||
.B EC-SRP5 (modern)
|
||||
Elliptic Curve Secure Remote Password using Curve25519 in Weierstrass form.
|
||||
Used by RouterOS >= 6.43. Provides zero-knowledge password proof. Enable on
|
||||
the server with
|
||||
.BR \-\-ecsrp5 .
|
||||
Clients auto-detect the authentication type.
|
||||
.SH MIKROTIK CONFIGURATION
|
||||
Enable the bandwidth test server on MikroTik for client mode:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
/tool/bandwidth-server set enabled=yes
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Run a test from MikroTik connecting to a btest-rs server:
|
||||
.PP
|
||||
.RS
|
||||
.nf
|
||||
/tool/bandwidth-test address=<server-ip> direction=both \\
|
||||
protocol=udp user=admin password=password
|
||||
.fi
|
||||
.RE
|
||||
.SH SEE ALSO
|
||||
.BR iperf3 (1),
|
||||
.BR netperf (1)
|
||||
.PP
|
||||
Project documentation:
|
||||
.I https://github.com/samm-git/btest-opensource
|
||||
.SH CREDITS
|
||||
.B btest-opensource
|
||||
by Alex Samorukov \(em original C implementation and protocol
|
||||
reverse-engineering (MIT License).
|
||||
.PP
|
||||
.B Margin Research
|
||||
\(em EC-SRP5 authentication reverse-engineering for MikroTik RouterOS
|
||||
(Apache License 2.0).
|
||||
.PP
|
||||
.B MikroTik
|
||||
\(em creator of the bandwidth test protocol and RouterOS.
|
||||
.SH LICENSE
|
||||
MIT License. See the LICENSE file in the source distribution.
|
||||
.PP
|
||||
This project is derived from btest-opensource (MIT License, Copyright 2016
|
||||
Alex Samorukov). The EC-SRP5 implementation is based on research by Margin
|
||||
Research (Apache License 2.0).
|
||||
.SH AUTHORS
|
||||
btest-rs contributors.
|
||||
227
docs/protocol.md
227
docs/protocol.md
@@ -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)
|
||||
```
|
||||
|
||||
@@ -14,21 +14,29 @@ btest -c 192.168.88.1 -r
|
||||
|
||||
Run btest-rs as a server and let MikroTik devices connect for bandwidth testing.
|
||||
|
||||
### Basic Server
|
||||
### Basic Server (No Authentication)
|
||||
|
||||
```bash
|
||||
btest -s
|
||||
```
|
||||
|
||||
Listens on TCP port 2000 (default). Any MikroTik device can connect without authentication.
|
||||
Listens on all IPv4 interfaces, TCP port 2000. Any MikroTik device can connect without credentials.
|
||||
|
||||
### Server with Authentication
|
||||
### Server with MD5 Authentication
|
||||
|
||||
```bash
|
||||
btest -s -a admin -p mysecretpassword
|
||||
```
|
||||
|
||||
MikroTik devices must provide matching credentials. Uses MD5 challenge-response authentication.
|
||||
Requires connecting devices to provide matching credentials. Uses MD5 double-hash challenge-response authentication, compatible with RouterOS versions before 6.43.
|
||||
|
||||
### Server with EC-SRP5 Authentication
|
||||
|
||||
```bash
|
||||
btest -s -a admin -p mysecretpassword --ecsrp5
|
||||
```
|
||||
|
||||
Advertises EC-SRP5 (Curve25519 Weierstrass) authentication to connecting clients. Required for RouterOS >= 6.43 devices that use the modern authentication protocol.
|
||||
|
||||
### Custom Port
|
||||
|
||||
@@ -36,25 +44,90 @@ MikroTik devices must provide matching credentials. Uses MD5 challenge-response
|
||||
btest -s -P 3000
|
||||
```
|
||||
|
||||
### Custom Listen Address
|
||||
|
||||
```bash
|
||||
# Listen only on a specific interface
|
||||
btest -s --listen 10.0.0.1
|
||||
|
||||
# Disable IPv4, listen only on IPv6
|
||||
btest -s --listen none --listen6
|
||||
|
||||
# Listen on both IPv4 and IPv6
|
||||
btest -s --listen6
|
||||
```
|
||||
|
||||
### IPv6 Listener (Experimental)
|
||||
|
||||
```bash
|
||||
# IPv6 on default address (::)
|
||||
btest -s --listen6
|
||||
|
||||
# IPv6 on a specific address
|
||||
btest -s --listen6 fd00::1
|
||||
```
|
||||
|
||||
TCP over IPv6 works fully on all platforms. UDP over IPv6 has issues on macOS due to kernel ENOBUFS limitations with `send_to()`. On Linux, IPv6 UDP works correctly.
|
||||
|
||||
### Syslog Integration
|
||||
|
||||
```bash
|
||||
btest -s --syslog 192.168.1.1:514
|
||||
```
|
||||
|
||||
Sends structured log events to a remote syslog server via UDP (RFC 3164 / BSD syslog format, facility local0). Events include:
|
||||
|
||||
- `AUTH_SUCCESS` -- successful authentication with peer address, username, and auth type
|
||||
- `AUTH_FAILURE` -- failed authentication with peer address, username, auth type, and reason
|
||||
- `TEST_START` -- test initiated with peer address, protocol, direction, and connection count
|
||||
- `TEST_END` -- test completed with peer address, protocol, direction, duration, average speeds, bytes transferred, and lost packets
|
||||
|
||||
### CSV Output
|
||||
|
||||
```bash
|
||||
btest -s --csv /var/log/btest-results.csv
|
||||
```
|
||||
|
||||
Appends a row for each completed test to the specified CSV file. Creates the file with headers if it does not exist. CSV columns:
|
||||
|
||||
```
|
||||
timestamp,host,port,protocol,direction,duration_s,tx_avg_mbps,rx_avg_mbps,tx_bytes,rx_bytes,lost_packets,auth_type
|
||||
```
|
||||
|
||||
### Quiet Mode
|
||||
|
||||
```bash
|
||||
btest -s --csv /var/log/btest.csv -q
|
||||
```
|
||||
|
||||
Suppresses per-second terminal output. Useful when running as a background service with CSV or syslog logging only.
|
||||
|
||||
### Verbose/Debug Output
|
||||
|
||||
```bash
|
||||
btest -s -v # Show connection info and debug messages
|
||||
btest -s -vv # Show hex dumps of status exchange (for debugging)
|
||||
btest -s -v # Debug messages (connection lifecycle, auth steps)
|
||||
btest -s -vv # Trace messages (hex dumps of status exchange)
|
||||
btest -s -vvv # Maximum verbosity
|
||||
```
|
||||
|
||||
### MikroTik Configuration (connecting to our server)
|
||||
### Combined Example
|
||||
|
||||
**Important: Always set Connection Count to 1.** Multi-connection mode is not supported and will cause severely degraded speeds.
|
||||
```bash
|
||||
btest -s -a admin -p secret --ecsrp5 --syslog 10.0.0.1:514 --csv /var/log/btest.csv -v
|
||||
```
|
||||
|
||||
This runs a server with EC-SRP5 authentication, sends events to syslog, logs results to CSV, and prints debug output to the terminal.
|
||||
|
||||
### MikroTik Configuration (Connecting to Our Server)
|
||||
|
||||
On the MikroTik device (WinBox or CLI):
|
||||
|
||||
```
|
||||
# CLI — always include connection-count=1
|
||||
/tool/bandwidth-test address=<server-ip> direction=both protocol=udp user=admin password=mysecretpassword connection-count=1
|
||||
/tool/bandwidth-test address=<server-ip> direction=both protocol=udp \
|
||||
user=admin password=mysecretpassword
|
||||
```
|
||||
|
||||
Or via WinBox: **Tools → Bandwidth Test**, enter server address, credentials, **set Connection Count to 1**, and click Start.
|
||||
Or via WinBox: **Tools > Bandwidth Test**, enter the server address and credentials, and click Start.
|
||||
|
||||
## Client Mode
|
||||
|
||||
@@ -62,28 +135,27 @@ Connect to a MikroTik device's built-in bandwidth test server.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Enable btest server on MikroTik:
|
||||
Enable the btest server on the MikroTik device:
|
||||
|
||||
```
|
||||
/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)
|
||||
### Download Test (Receive)
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r
|
||||
```
|
||||
|
||||
Measures download speed from MikroTik to your machine.
|
||||
Measures download speed from the MikroTik device to your machine. The server transmits, the client receives.
|
||||
|
||||
### Upload Test (transmit)
|
||||
### Upload Test (Transmit)
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -t
|
||||
```
|
||||
|
||||
Measures upload speed from your machine to MikroTik.
|
||||
Measures upload speed from your machine to the MikroTik device. The client transmits, the server receives.
|
||||
|
||||
### Bidirectional Test
|
||||
|
||||
@@ -101,6 +173,8 @@ btest -c 192.168.88.1 -t -u # UDP upload
|
||||
btest -c 192.168.88.1 -t -r -u # UDP bidirectional
|
||||
```
|
||||
|
||||
UDP mode uses separate data ports (2001+ on the server side, 2257+ on the client side) and exchanges status messages every second over the TCP control channel.
|
||||
|
||||
### Bandwidth Limiting
|
||||
|
||||
```bash
|
||||
@@ -109,15 +183,7 @@ 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.
|
||||
Suffixes: `K` (kilobits/sec), `M` (megabits/sec), `G` (gigabits/sec). Values are in bits per second.
|
||||
|
||||
### With Authentication
|
||||
|
||||
@@ -125,6 +191,47 @@ The `-n` flag sends a probe packet to open the NAT firewall hole.
|
||||
btest -c 192.168.88.1 -r -a admin -p password
|
||||
```
|
||||
|
||||
The client auto-detects the authentication type (MD5 or EC-SRP5) from the server's response and handles it accordingly.
|
||||
|
||||
### NAT Traversal
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -u -n
|
||||
```
|
||||
|
||||
The `-n` flag sends an empty UDP probe packet before starting the receive thread. This opens a hole in NAT firewalls so the server's UDP data packets can reach the client.
|
||||
|
||||
### Timed Tests
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -d 30 # Run for 30 seconds, then stop
|
||||
btest -c 192.168.88.1 -t -r -d 60 # 60-second bidirectional test
|
||||
```
|
||||
|
||||
The default duration is 0 (unlimited). When the duration expires, the client exits cleanly.
|
||||
|
||||
### CSV Output (Client Mode)
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -d 30 --csv results.csv
|
||||
```
|
||||
|
||||
Appends a summary row after the test completes with the host, port, protocol, direction, duration, and auth type.
|
||||
|
||||
### Quiet Mode (Client)
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -d 10 --csv results.csv -q
|
||||
```
|
||||
|
||||
Suppresses per-second bandwidth output to the terminal. Useful for scripted or automated testing where only the CSV file matters.
|
||||
|
||||
### Custom Port
|
||||
|
||||
```bash
|
||||
btest -c 192.168.88.1 -r -P 3000
|
||||
```
|
||||
|
||||
## Reading the Output
|
||||
|
||||
```
|
||||
@@ -137,50 +244,131 @@ btest -c 192.168.88.1 -r -a admin -p password
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `[ N]` | Interval number (1 per second) |
|
||||
| `TX` | Data we sent (upload) |
|
||||
| `RX` | Data we received (download) |
|
||||
| `TX` | Data sent (upload from your perspective) |
|
||||
| `RX` | Data received (download from your perspective) |
|
||||
| `Mbps` | Megabits per second |
|
||||
| `bytes` | Raw bytes transferred in this interval |
|
||||
| `lost: N` | UDP packets lost (UDP mode only) |
|
||||
| `lost: N` | UDP packets lost in this interval (UDP mode only) |
|
||||
|
||||
## CLI Reference
|
||||
## Complete CLI Reference
|
||||
|
||||
```
|
||||
btest-rs — MikroTik Bandwidth Test server & client in Rust
|
||||
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
|
||||
-s, --server
|
||||
Run in server mode. Listens for incoming connections from MikroTik
|
||||
devices or other btest clients. Conflicts with -c.
|
||||
|
||||
-c, --client <HOST>
|
||||
Run in client mode, connecting to the specified host. The host can be
|
||||
an IPv4 address, IPv6 address, or hostname. Conflicts with -s.
|
||||
|
||||
-t, --transmit
|
||||
Client transmits data (upload test). Tells the server to receive.
|
||||
Can be combined with -r for bidirectional testing.
|
||||
|
||||
-r, --receive
|
||||
Client receives data (download test). Tells the server to transmit.
|
||||
Can be combined with -t for bidirectional testing.
|
||||
|
||||
-u, --udp
|
||||
Use UDP instead of TCP for the data transfer. UDP uses separate data
|
||||
ports (2001+ server side, 2257+ client side) and exchanges status
|
||||
messages over the TCP control channel every second.
|
||||
|
||||
-b, --bandwidth <BW>
|
||||
Target bandwidth limit for the test. Accepts suffixes: K (kilobits),
|
||||
M (megabits), G (gigabits). Examples: 100M, 1G, 500K. Default is 0
|
||||
(unlimited).
|
||||
|
||||
-P, --port <PORT>
|
||||
TCP port to listen on (server mode) or connect to (client mode).
|
||||
[default: 2000]
|
||||
|
||||
--listen <ADDR>
|
||||
IPv4 address to bind the server listener to. Use "none" to disable
|
||||
IPv4 listening entirely (useful with --listen6 for IPv6-only mode).
|
||||
[default: 0.0.0.0]
|
||||
|
||||
--listen6 [<ADDR>]
|
||||
Enable the IPv6 listener. If no address is given, binds to [::].
|
||||
Experimental: TCP over IPv6 works fully on all platforms. UDP over
|
||||
IPv6 has issues on macOS due to kernel ENOBUFS limitations.
|
||||
|
||||
-a, --authuser <USER>
|
||||
Authentication username. In server mode, clients must provide this
|
||||
username. In client mode, this is sent to the server.
|
||||
|
||||
-p, --authpass <PASS>
|
||||
Authentication password. In server mode, clients must provide a
|
||||
matching password. In client mode, this is used to authenticate.
|
||||
|
||||
--ecsrp5
|
||||
Use EC-SRP5 authentication (Curve25519 Weierstrass). In server mode,
|
||||
this advertises EC-SRP5 instead of MD5 to connecting clients.
|
||||
Required for RouterOS >= 6.43. In client mode, auth type is
|
||||
auto-detected and this flag is not needed.
|
||||
|
||||
-n, --nat
|
||||
NAT traversal mode. Sends an empty UDP probe packet to the server
|
||||
before starting the receive thread, opening a hole in NAT firewalls.
|
||||
Only relevant for UDP receive tests behind NAT.
|
||||
|
||||
-d, --duration <SECS>
|
||||
Test duration in seconds (client mode only). The client exits cleanly
|
||||
after the specified time. A value of 0 means unlimited (run until
|
||||
interrupted with Ctrl-C). [default: 0]
|
||||
|
||||
--csv <FILE>
|
||||
Output test results to a CSV file. Appends a row per completed test.
|
||||
Creates the file with a header row if it does not exist. Columns:
|
||||
timestamp, host, port, protocol, direction, duration_s, tx_avg_mbps,
|
||||
rx_avg_mbps, tx_bytes, rx_bytes, lost_packets, auth_type.
|
||||
|
||||
-q, --quiet
|
||||
Suppress per-second bandwidth output to the terminal. Useful in
|
||||
combination with --csv for machine-readable-only output, or when
|
||||
running as a background service.
|
||||
|
||||
--syslog <HOST:PORT>
|
||||
Send structured log events to a remote syslog server via UDP. Uses
|
||||
RFC 3164 (BSD syslog) format with facility local0. Events include
|
||||
AUTH_SUCCESS, AUTH_FAILURE, TEST_START, and TEST_END with detailed
|
||||
metadata. Example: --syslog 192.168.1.1:514
|
||||
|
||||
-v, --verbose...
|
||||
Increase log verbosity. Can be repeated:
|
||||
-v debug messages (connection lifecycle, auth steps)
|
||||
-vv trace messages (hex dumps of protocol exchange)
|
||||
-vvv maximum verbosity
|
||||
|
||||
-h, --help
|
||||
Print help information
|
||||
|
||||
-V, --version
|
||||
Print version information
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Connection Count MUST be 1** when MikroTik connects to your server. Multi-connection mode is not supported and will cause speeds to drop to near zero. Single-connection performance is excellent (1+ Gbps).
|
||||
- **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.
|
||||
- **Bandwidth limiting** applies to the direction you specify. In bidirectional mode with `-b 100M`, both directions are limited to 100 Mbps each.
|
||||
|
||||
## 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) | Set Connection Count to 1 on MikroTik. Multi-connection is not supported |
|
||||
| Very low speed with multiple connections | Multi-connection mode is broken — set Connection Count to 1 |
|
||||
| UDP `lost` packets high | Network congestion or MTU issues, try reducing bandwidth with `-b` |
|
||||
| Connection refused | Check that port 2000 is open and the server is running |
|
||||
| Auth failure with EC-SRP5 | Ensure `--ecsrp5` is set on the server if the MikroTik client uses RouterOS >= 6.43 |
|
||||
| Auth failure with MD5 | Verify username and password match exactly (case-sensitive) |
|
||||
| Server shows 0 RX | Check that the MikroTik direction setting includes sending to the server |
|
||||
| Very low UDP speed | Network congestion or MTU issues; try reducing bandwidth with `-b` |
|
||||
| IPv6 UDP fails on macOS | Known macOS kernel limitation; use Linux for IPv6 UDP tests |
|
||||
| Syslog messages not arriving | Verify the syslog server address and port, and check firewall rules for UDP 514 |
|
||||
| CSV file not created | Check write permissions on the directory; the file is created on first use |
|
||||
|
||||
Reference in New Issue
Block a user