Rename to btest-rs, add LICENSE and README with full credits

- Rename package to btest-rs (Rust convention for reimplementations)
- MIT license matching the original btest-opensource license
- LICENSE explicitly credits Alex Samorukov's original work
- Comprehensive README with usage, performance numbers, and credits
- CLI --help references the original project

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-31 12:58:04 +04:00
parent e604fdb2e7
commit d71a3a4e71
6 changed files with 227 additions and 42 deletions

32
Cargo.lock generated
View File

@@ -82,6 +82,22 @@ dependencies = [
"generic-array",
]
[[package]]
name = "btest-rs"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"clap",
"md-5",
"rand",
"socket2 0.5.10",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "bytes"
version = "1.11.1"
@@ -255,22 +271,6 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mikrotik-btest"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"clap",
"md-5",
"rand",
"socket2 0.5.10",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "mio"
version = "1.2.0"

View File

@@ -1,12 +1,15 @@
[package]
name = "mikrotik-btest"
name = "btest-rs"
version = "0.1.0"
edition = "2021"
description = "MikroTik Bandwidth Test (btest) server and client implementation in Rust"
description = "MikroTik Bandwidth Test (btest) server and client — a Rust reimplementation"
license = "MIT"
repository = "https://github.com/samm-git/btest-opensource"
keywords = ["mikrotik", "bandwidth", "btest", "network", "benchmarking"]
categories = ["command-line-utilities", "network-programming"]
[lib]
name = "mikrotik_btest"
name = "btest_rs"
path = "src/lib.rs"
[[bin]]

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
MIT License
Copyright (c) 2026 btest-rs contributors
Based on btest-opensource by Alex Samorukov (https://github.com/samm-git/btest-opensource)
Original work Copyright (c) 2016 Alex Samorukov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

156
README.md Normal file
View File

@@ -0,0 +1,156 @@
# btest-rs
A Rust reimplementation of the [MikroTik Bandwidth Test (btest)](https://wiki.mikrotik.com/wiki/Manual:Tools/Bandwidth_Test) protocol. Both server and client modes, compatible with MikroTik RouterOS devices.
## Based on
This project is a clean-room Rust reimplementation based on the protocol reverse-engineering work done by **Alex Samorukov** in [btest-opensource](https://github.com/samm-git/btest-opensource). The original C implementation and protocol documentation were invaluable in making this project possible. Full credit to Alex and all contributors to that project.
The original `btest-opensource` project is included as a git submodule for reference and protocol documentation.
## Why Rust?
- **Single static binary** - 2 MB, zero dependencies, runs anywhere
- **Cross-platform** - macOS, Linux (x86_64, ARM64), Docker
- **Async I/O** - tokio-based, handles many concurrent connections efficiently
- **Memory safe** - no buffer overflows, no use-after-free, no data races
- **Easy deployment** - `scp` one file, done. Or use the systemd installer.
## Performance
Tested over WiFi 6E (MikroTik RouterOS <-> macOS):
| Mode | Protocol | Speed |
|------|----------|-------|
| Server RX (1 conn) | UDP | **1.05 Gbps** |
| Client TCP download | TCP | **530 Mbps** |
| Client TCP upload | TCP | **840 Mbps** |
| Client UDP download | UDP | **433 Mbps** |
| Client TCP bidirectional | TCP | **264/264 Mbps** |
| Server bidirectional | UDP | **280/393 Mbps** |
## Installation
### Pre-built binary
```bash
# Build for Linux x86_64 from macOS (requires Docker)
scripts/build-linux.sh
# Copy to server
scp dist/btest root@yourserver:/usr/local/bin/btest
```
### From source
```bash
cargo install --path .
```
### Docker
```bash
docker compose up -d # Server on port 2000
```
### systemd service
```bash
# On the target Linux server:
sudo ./scripts/install-service.sh
sudo ./scripts/install-service.sh --auth-user admin --auth-pass secret
```
## Usage
### Server mode
MikroTik devices connect to this server to run bandwidth tests.
```bash
# Basic server (no auth)
btest -s
# With authentication
btest -s -a admin -p password
# Custom port with verbose logging
btest -s -P 2000 -v
```
### Client mode
Connect to a MikroTik device's built-in btest server.
```bash
# TCP download test
btest -c 192.168.88.1 -r
# TCP upload test
btest -c 192.168.88.1 -t
# Bidirectional
btest -c 192.168.88.1 -t -r
# UDP with bandwidth limit
btest -c 192.168.88.1 -r -u -b 100M
# With authentication
btest -c 192.168.88.1 -r -a admin -p password
```
### Debug logging
```bash
btest -s -v # info + debug
btest -s -vv # info + debug + trace (hex dumps of status exchange)
```
## MikroTik Setup
### Enable btest server on MikroTik (for client mode)
```
/tool/bandwidth-server set enabled=yes
```
### Run btest from MikroTik (connecting to our server)
```
/tool/bandwidth-test address=<server-ip> direction=both protocol=udp user=admin password=password
```
## Protocol
The MikroTik btest protocol uses:
- **TCP port 2000** for control (handshake, auth, status exchange)
- **UDP ports 2001+** for data transfer
- **MD5 challenge-response** authentication (RouterOS < 6.43)
- **1-second status interval** with dynamic speed adjustment
See the [original protocol documentation](btest-opensource/README.md) for wire-format details.
## Known Limitations
- **EC-SRP5 authentication** (RouterOS >= 6.43) is not yet supported for client mode. Server mode works fine with MD5 auth. Disable auth on the MikroTik btest server as a workaround.
- **Multi-connection mode** (`Connection Count > 1` on MikroTik client) causes MikroTik's per-connection speed adaptation to throttle each stream independently, resulting in lower aggregate throughput. Use 1 connection for best results.
## Testing
```bash
cargo test # Unit + integration tests
scripts/test-local.sh # Loopback self-test
scripts/test-mikrotik.sh <ip> # Test against MikroTik device
scripts/test-docker.sh # Docker container test
```
## Credits
- **[btest-opensource](https://github.com/samm-git/btest-opensource)** by [Alex Samorukov](https://github.com/samm-git) - Original C implementation and protocol reverse-engineering that made this project possible. Licensed under MIT.
- **MikroTik** - Creator of the bandwidth test protocol and RouterOS.
## License
MIT License - see [LICENSE](LICENSE).
This project is derived from [btest-opensource](https://github.com/samm-git/btest-opensource) (MIT License, Copyright 2016 Alex Samorukov). The original license and copyright notice are preserved as required.

View File

@@ -12,10 +12,12 @@ use crate::protocol::*;
#[derive(Parser, Debug)]
#[command(
name = "btest",
about = "MikroTik Bandwidth Test (btest) - server and client",
about = "btest-rs: MikroTik Bandwidth Test server & client in Rust",
version,
long_about = "Compatible bandwidth testing tool for MikroTik RouterOS devices.\n\
Supports TCP and UDP modes with optional authentication."
long_about = "btest-rs — A Rust reimplementation of the MikroTik Bandwidth Test protocol.\n\
Compatible with MikroTik RouterOS devices. Supports TCP/UDP, bidirectional\n\
testing, and MD5 authentication.\n\n\
Based on btest-opensource by Alex Samorukov (https://github.com/samm-git/btest-opensource)"
)]
struct Cli {
/// Run in server mode

View File

@@ -8,7 +8,7 @@ async fn start_test_server(port: u16, auth_user: Option<&str>, auth_pass: Option
let user = auth_user.map(String::from);
let pass = auth_pass.map(String::from);
tokio::spawn(async move {
let _ = mikrotik_btest::server::run_server(port, user, pass).await;
let _ = btest_rs::server::run_server(port, user, pass).await;
});
tokio::time::sleep(Duration::from_millis(100)).await;
}
@@ -41,9 +41,9 @@ async fn test_server_command_and_noauth() {
assert_eq!(buf, [0x01, 0x00, 0x00, 0x00]);
// CMD_DIR_TX (0x02) = server should transmit data to us
let cmd = mikrotik_btest::protocol::Command::new(
mikrotik_btest::protocol::CMD_PROTO_TCP,
mikrotik_btest::protocol::CMD_DIR_TX,
let cmd = btest_rs::protocol::Command::new(
btest_rs::protocol::CMD_PROTO_TCP,
btest_rs::protocol::CMD_DIR_TX,
);
stream.write_all(&cmd.serialize()).await.unwrap();
stream.flush().await.unwrap();
@@ -72,9 +72,9 @@ async fn test_server_auth_challenge() {
assert_eq!(buf, [0x01, 0x00, 0x00, 0x00]);
// CMD_DIR_TX = server transmits
let cmd = mikrotik_btest::protocol::Command::new(
mikrotik_btest::protocol::CMD_PROTO_TCP,
mikrotik_btest::protocol::CMD_DIR_TX,
let cmd = btest_rs::protocol::Command::new(
btest_rs::protocol::CMD_PROTO_TCP,
btest_rs::protocol::CMD_DIR_TX,
);
stream.write_all(&cmd.serialize()).await.unwrap();
stream.flush().await.unwrap();
@@ -85,7 +85,7 @@ async fn test_server_auth_challenge() {
let mut challenge = [0u8; 16];
stream.read_exact(&mut challenge).await.unwrap();
let hash = mikrotik_btest::auth::compute_auth_hash("test", &challenge);
let hash = btest_rs::auth::compute_auth_hash("test", &challenge);
let mut response = [0u8; 48];
response[0..16].copy_from_slice(&hash);
response[16..21].copy_from_slice(b"admin");
@@ -109,9 +109,9 @@ async fn test_server_auth_failure() {
let mut buf = [0u8; 4];
stream.read_exact(&mut buf).await.unwrap();
let cmd = mikrotik_btest::protocol::Command::new(
mikrotik_btest::protocol::CMD_PROTO_TCP,
mikrotik_btest::protocol::CMD_DIR_TX,
let cmd = btest_rs::protocol::Command::new(
btest_rs::protocol::CMD_PROTO_TCP,
btest_rs::protocol::CMD_DIR_TX,
);
stream.write_all(&cmd.serialize()).await.unwrap();
stream.flush().await.unwrap();
@@ -122,7 +122,7 @@ async fn test_server_auth_failure() {
let mut challenge = [0u8; 16];
stream.read_exact(&mut challenge).await.unwrap();
let hash = mikrotik_btest::auth::compute_auth_hash("wrongpassword", &challenge);
let hash = btest_rs::auth::compute_auth_hash("wrongpassword", &challenge);
let mut response = [0u8; 48];
response[0..16].copy_from_slice(&hash);
response[16..21].copy_from_slice(b"admin");
@@ -143,10 +143,10 @@ async fn test_loopback_tcp_rx() {
start_test_server(port, None, None).await;
let handle = tokio::spawn(async move {
mikrotik_btest::client::run_client(
btest_rs::client::run_client(
"127.0.0.1",
port,
mikrotik_btest::protocol::CMD_DIR_TX, // server TX = client RX
btest_rs::protocol::CMD_DIR_TX, // server TX = client RX
false,
0,
0,
@@ -167,10 +167,10 @@ async fn test_loopback_tcp_tx() {
start_test_server(port, None, None).await;
let handle = tokio::spawn(async move {
mikrotik_btest::client::run_client(
btest_rs::client::run_client(
"127.0.0.1",
port,
mikrotik_btest::protocol::CMD_DIR_RX, // server RX = client TX
btest_rs::protocol::CMD_DIR_RX, // server RX = client TX
false,
0,
0,
@@ -191,10 +191,10 @@ async fn test_loopback_tcp_both() {
start_test_server(port, None, None).await;
let handle = tokio::spawn(async move {
mikrotik_btest::client::run_client(
btest_rs::client::run_client(
"127.0.0.1",
port,
mikrotik_btest::protocol::CMD_DIR_BOTH,
btest_rs::protocol::CMD_DIR_BOTH,
false,
0,
0,
@@ -215,10 +215,10 @@ async fn test_loopback_tcp_with_auth() {
start_test_server(port, Some("admin"), Some("secret")).await;
let handle = tokio::spawn(async move {
mikrotik_btest::client::run_client(
btest_rs::client::run_client(
"127.0.0.1",
port,
mikrotik_btest::protocol::CMD_DIR_TX, // server TX = client RX
btest_rs::protocol::CMD_DIR_TX, // server TX = client RX
false,
0,
0,