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",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -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
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)]
|
||||
#[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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user