diff --git a/Cargo.lock b/Cargo.lock index 6209c9a..b8ef6a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 0a248fc..bbfc787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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]] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3481e39 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..53432a5 --- /dev/null +++ b/README.md @@ -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= 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 # 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. diff --git a/src/main.rs b/src/main.rs index 381fa21..ce71e34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 diff --git a/tests/integration_test.rs b/tests/integration_test.rs index e02a13d..d5c6ce0 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -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,