Initial commit: MikroTik btest server & client in Rust

Full reimplementation of the MikroTik Bandwidth Test protocol:
- Server mode: accepts connections from MikroTik devices on port 2000
- Client mode: connects to MikroTik btest servers
- TCP and UDP protocols with bidirectional support
- MD5 challenge-response authentication
- Dynamic speed adjustment (1.5x algorithm)
- Status exchange matching original C pselect() behavior
- Docker support with multi-stage build

Tested against MikroTik RouterOS achieving:
- 1.05 Gbps server RX (single connection)
- 530 Mbps client TCP download
- 840 Mbps client TCP upload
- 433 Mbps client UDP download

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-31 11:56:34 +04:00
commit d9007dc169
18 changed files with 2801 additions and 0 deletions

136
src/main.rs Normal file
View File

@@ -0,0 +1,136 @@
mod auth;
mod bandwidth;
mod client;
mod protocol;
mod server;
use clap::Parser;
use tracing_subscriber::EnvFilter;
use crate::protocol::*;
#[derive(Parser, Debug)]
#[command(
name = "btest",
about = "MikroTik Bandwidth Test (btest) - server and client",
version,
long_about = "Compatible bandwidth testing tool for MikroTik RouterOS devices.\n\
Supports TCP and UDP modes with optional authentication."
)]
struct Cli {
/// Run in server mode
#[arg(short = 's', long = "server", conflicts_with = "client")]
server: bool,
/// Run in client mode, connecting to the specified host
#[arg(short = 'c', long = "client", conflicts_with = "server")]
client: Option<String>,
/// Client transmits data (upload test)
#[arg(short = 't', long = "transmit")]
transmit: bool,
/// Client receives data (download test)
#[arg(short = 'r', long = "receive")]
receive: bool,
/// Use UDP instead of TCP
#[arg(short = 'u', long = "udp")]
udp: bool,
/// Target bandwidth (e.g., 100M, 1G, 500K)
#[arg(short = 'b', long = "bandwidth")]
bandwidth: Option<String>,
/// Listen/connect port (default: 2000)
#[arg(short = 'P', long = "port", default_value_t = BTEST_PORT)]
port: u16,
/// Authentication username
#[arg(short = 'a', long = "authuser")]
auth_user: Option<String>,
/// Authentication password
#[arg(short = 'p', long = "authpass")]
auth_pass: Option<String>,
/// NAT mode - send probe packet to open firewall
#[arg(short = 'n', long = "nat")]
nat: bool,
/// Verbose logging (repeat for more: -v, -vv, -vvv)
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbose: u8,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
// Set up logging based on verbosity
let filter = match cli.verbose {
0 => "info",
1 => "debug",
_ => "trace",
};
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)),
)
.with_target(false)
.init();
if cli.server {
// Server mode
tracing::info!("Starting btest server on port {}", cli.port);
server::run_server(cli.port, cli.auth_user, cli.auth_pass).await?;
} else if let Some(host) = cli.client {
// Client mode - must specify at least one direction
if !cli.transmit && !cli.receive {
eprintln!("Error: Client mode requires at least one of -t (transmit) or -r (receive)");
std::process::exit(1);
}
// Direction tells SERVER what to do (C client convention):
// client transmit → CMD_DIR_RX (server receives)
// client receive → CMD_DIR_TX (server transmits)
let direction = match (cli.transmit, cli.receive) {
(true, false) => CMD_DIR_RX,
(false, true) => CMD_DIR_TX,
(true, true) => CMD_DIR_BOTH,
_ => unreachable!(),
};
let bw = match &cli.bandwidth {
Some(b) => bandwidth::parse_bandwidth(b)?,
None => 0,
};
// For client: local_tx_speed controls upload, remote_tx_speed controls download
let (tx_speed, rx_speed) = match direction {
CMD_DIR_TX => (bw, 0),
CMD_DIR_RX => (0, bw),
CMD_DIR_BOTH => (bw, bw),
_ => (0, 0),
};
client::run_client(
&host,
cli.port,
direction,
cli.udp,
tx_speed,
rx_speed,
cli.auth_user,
cli.auth_pass,
cli.nat,
)
.await?;
} else {
eprintln!("Error: Must specify either -s (server) or -c <host> (client)");
eprintln!("Run with --help for usage information.");
std::process::exit(1);
}
Ok(())
}