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:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -82,6 +82,22 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -255,22 +271,6 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
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]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mikrotik-btest"
|
name = "btest-rs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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"
|
license = "MIT"
|
||||||
|
repository = "https://github.com/samm-git/btest-opensource"
|
||||||
|
keywords = ["mikrotik", "bandwidth", "btest", "network", "benchmarking"]
|
||||||
|
categories = ["command-line-utilities", "network-programming"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "mikrotik_btest"
|
name = "btest_rs"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
24
LICENSE
Normal file
24
LICENSE
Normal 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
156
README.md
Normal 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.
|
||||||
@@ -12,10 +12,12 @@ use crate::protocol::*;
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "btest",
|
name = "btest",
|
||||||
about = "MikroTik Bandwidth Test (btest) - server and client",
|
about = "btest-rs: MikroTik Bandwidth Test server & client in Rust",
|
||||||
version,
|
version,
|
||||||
long_about = "Compatible bandwidth testing tool for MikroTik RouterOS devices.\n\
|
long_about = "btest-rs — A Rust reimplementation of the MikroTik Bandwidth Test protocol.\n\
|
||||||
Supports TCP and UDP modes with optional authentication."
|
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 {
|
struct Cli {
|
||||||
/// Run in server mode
|
/// Run in server mode
|
||||||
|
|||||||
@@ -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 user = auth_user.map(String::from);
|
||||||
let pass = auth_pass.map(String::from);
|
let pass = auth_pass.map(String::from);
|
||||||
tokio::spawn(async move {
|
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;
|
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]);
|
assert_eq!(buf, [0x01, 0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
// CMD_DIR_TX (0x02) = server should transmit data to us
|
// CMD_DIR_TX (0x02) = server should transmit data to us
|
||||||
let cmd = mikrotik_btest::protocol::Command::new(
|
let cmd = btest_rs::protocol::Command::new(
|
||||||
mikrotik_btest::protocol::CMD_PROTO_TCP,
|
btest_rs::protocol::CMD_PROTO_TCP,
|
||||||
mikrotik_btest::protocol::CMD_DIR_TX,
|
btest_rs::protocol::CMD_DIR_TX,
|
||||||
);
|
);
|
||||||
stream.write_all(&cmd.serialize()).await.unwrap();
|
stream.write_all(&cmd.serialize()).await.unwrap();
|
||||||
stream.flush().await.unwrap();
|
stream.flush().await.unwrap();
|
||||||
@@ -72,9 +72,9 @@ async fn test_server_auth_challenge() {
|
|||||||
assert_eq!(buf, [0x01, 0x00, 0x00, 0x00]);
|
assert_eq!(buf, [0x01, 0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
// CMD_DIR_TX = server transmits
|
// CMD_DIR_TX = server transmits
|
||||||
let cmd = mikrotik_btest::protocol::Command::new(
|
let cmd = btest_rs::protocol::Command::new(
|
||||||
mikrotik_btest::protocol::CMD_PROTO_TCP,
|
btest_rs::protocol::CMD_PROTO_TCP,
|
||||||
mikrotik_btest::protocol::CMD_DIR_TX,
|
btest_rs::protocol::CMD_DIR_TX,
|
||||||
);
|
);
|
||||||
stream.write_all(&cmd.serialize()).await.unwrap();
|
stream.write_all(&cmd.serialize()).await.unwrap();
|
||||||
stream.flush().await.unwrap();
|
stream.flush().await.unwrap();
|
||||||
@@ -85,7 +85,7 @@ async fn test_server_auth_challenge() {
|
|||||||
let mut challenge = [0u8; 16];
|
let mut challenge = [0u8; 16];
|
||||||
stream.read_exact(&mut challenge).await.unwrap();
|
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];
|
let mut response = [0u8; 48];
|
||||||
response[0..16].copy_from_slice(&hash);
|
response[0..16].copy_from_slice(&hash);
|
||||||
response[16..21].copy_from_slice(b"admin");
|
response[16..21].copy_from_slice(b"admin");
|
||||||
@@ -109,9 +109,9 @@ async fn test_server_auth_failure() {
|
|||||||
let mut buf = [0u8; 4];
|
let mut buf = [0u8; 4];
|
||||||
stream.read_exact(&mut buf).await.unwrap();
|
stream.read_exact(&mut buf).await.unwrap();
|
||||||
|
|
||||||
let cmd = mikrotik_btest::protocol::Command::new(
|
let cmd = btest_rs::protocol::Command::new(
|
||||||
mikrotik_btest::protocol::CMD_PROTO_TCP,
|
btest_rs::protocol::CMD_PROTO_TCP,
|
||||||
mikrotik_btest::protocol::CMD_DIR_TX,
|
btest_rs::protocol::CMD_DIR_TX,
|
||||||
);
|
);
|
||||||
stream.write_all(&cmd.serialize()).await.unwrap();
|
stream.write_all(&cmd.serialize()).await.unwrap();
|
||||||
stream.flush().await.unwrap();
|
stream.flush().await.unwrap();
|
||||||
@@ -122,7 +122,7 @@ async fn test_server_auth_failure() {
|
|||||||
let mut challenge = [0u8; 16];
|
let mut challenge = [0u8; 16];
|
||||||
stream.read_exact(&mut challenge).await.unwrap();
|
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];
|
let mut response = [0u8; 48];
|
||||||
response[0..16].copy_from_slice(&hash);
|
response[0..16].copy_from_slice(&hash);
|
||||||
response[16..21].copy_from_slice(b"admin");
|
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;
|
start_test_server(port, None, None).await;
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
mikrotik_btest::client::run_client(
|
btest_rs::client::run_client(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
port,
|
port,
|
||||||
mikrotik_btest::protocol::CMD_DIR_TX, // server TX = client RX
|
btest_rs::protocol::CMD_DIR_TX, // server TX = client RX
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -167,10 +167,10 @@ async fn test_loopback_tcp_tx() {
|
|||||||
start_test_server(port, None, None).await;
|
start_test_server(port, None, None).await;
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
mikrotik_btest::client::run_client(
|
btest_rs::client::run_client(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
port,
|
port,
|
||||||
mikrotik_btest::protocol::CMD_DIR_RX, // server RX = client TX
|
btest_rs::protocol::CMD_DIR_RX, // server RX = client TX
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -191,10 +191,10 @@ async fn test_loopback_tcp_both() {
|
|||||||
start_test_server(port, None, None).await;
|
start_test_server(port, None, None).await;
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
mikrotik_btest::client::run_client(
|
btest_rs::client::run_client(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
port,
|
port,
|
||||||
mikrotik_btest::protocol::CMD_DIR_BOTH,
|
btest_rs::protocol::CMD_DIR_BOTH,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -215,10 +215,10 @@ async fn test_loopback_tcp_with_auth() {
|
|||||||
start_test_server(port, Some("admin"), Some("secret")).await;
|
start_test_server(port, Some("admin"), Some("secret")).await;
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
mikrotik_btest::client::run_client(
|
btest_rs::client::run_client(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
port,
|
port,
|
||||||
mikrotik_btest::protocol::CMD_DIR_TX, // server TX = client RX
|
btest_rs::protocol::CMD_DIR_TX, // server TX = client RX
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|||||||
Reference in New Issue
Block a user