From 89391e178176ff7fe746c33b308717b1a0c2ccd0 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Wed, 1 Apr 2026 14:44:57 +0400 Subject: [PATCH] Add OpenWrt ipk packaging + split client/server binaries OpenWrt package (deploy/openwrt/): - build-ipk.sh: creates .ipk from pre-built binary (no SDK needed) - Makefile: for OpenWrt SDK integration - ProCD init script with UCI config - Supports all architectures (x86_64, aarch64, mipsel, mips) Split binaries for embedded (src/bin/): - btest-client: client-only, no server/syslog/csv - btest-server: server-only, no client - release-small profile: opt-level=z + panic=abort Sizes (compressed .tar.gz): Full btest: ~1 MB btest-client: ~500 KB (release-small) btest-server: ~550 KB (release-small) Install on OpenWrt: opkg install btest-rs_0.6.0-1_x86_64.ipk Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 14 ++++ deploy/openwrt/Makefile | 57 ++++++++++++++ deploy/openwrt/build-ipk.sh | 117 +++++++++++++++++++++++++++ deploy/openwrt/files/btest.config | 7 ++ deploy/openwrt/files/btest.init | 34 ++++++++ src/bin/client_only.rs | 127 ++++++++++++++++++++++++++++++ src/bin/server_only.rs | 62 +++++++++++++++ 7 files changed, 418 insertions(+) create mode 100644 deploy/openwrt/Makefile create mode 100755 deploy/openwrt/build-ipk.sh create mode 100644 deploy/openwrt/files/btest.config create mode 100755 deploy/openwrt/files/btest.init create mode 100644 src/bin/client_only.rs create mode 100644 src/bin/server_only.rs diff --git a/Cargo.toml b/Cargo.toml index f81188a..4425008 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,14 @@ path = "src/lib.rs" name = "btest" path = "src/main.rs" +[[bin]] +name = "btest-client" +path = "src/bin/client_only.rs" + +[[bin]] +name = "btest-server" +path = "src/bin/server_only.rs" + [dependencies] tokio = { version = "1", features = ["full"] } clap = { version = "4", features = ["derive"] } @@ -38,3 +46,9 @@ opt-level = 3 lto = true strip = true codegen-units = 1 + +# Minimal size profile for embedded/OpenWrt targets +[profile.release-small] +inherits = "release" +opt-level = "z" +panic = "abort" diff --git a/deploy/openwrt/Makefile b/deploy/openwrt/Makefile new file mode 100644 index 0000000..308e7b4 --- /dev/null +++ b/deploy/openwrt/Makefile @@ -0,0 +1,57 @@ +# OpenWrt package Makefile for btest-rs +# +# To build: +# 1. Clone the OpenWrt SDK for your target +# 2. Copy this directory to package/btest-rs/ in the SDK +# 3. Run: make package/btest-rs/compile V=s +# +# Or use the pre-built binary approach (see build-ipk.sh) + +include $(TOPDIR)/rules.mk + +PKG_NAME:=btest-rs +PKG_VERSION:=0.6.0 +PKG_RELEASE:=1 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://github.com/manawenuz/btest-rs/archive/refs/tags/v$(PKG_VERSION).tar.gz +PKG_HASH:=skip + +PKG_BUILD_DEPENDS:=rust/host +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) + +include $(INCLUDE_DIR)/package.mk + +define Package/btest-rs + SECTION:=net + CATEGORY:=Network + TITLE:=MikroTik Bandwidth Test server and client + URL:=https://github.com/manawenuz/btest-rs + DEPENDS:= + PKGARCH:=$(ARCH) +endef + +define Package/btest-rs/description + A Rust reimplementation of the MikroTik Bandwidth Test (btest) protocol. + Supports TCP/UDP, IPv4/IPv6, EC-SRP5 and MD5 authentication, + multi-connection, syslog, CSV output, and CPU monitoring. +endef + +define Build/Compile + cd $(PKG_BUILD_DIR) && \ + CARGO_TARGET_DIR=$(PKG_BUILD_DIR)/target \ + cargo build --release --target $(RUSTC_TARGET) +endef + +define Package/btest-rs/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET)/release/btest $(1)/usr/bin/btest + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/btest.init $(1)/etc/init.d/btest + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/btest.config $(1)/etc/config/btest +endef + +$(eval $(call BuildPackage,btest-rs)) diff --git a/deploy/openwrt/build-ipk.sh b/deploy/openwrt/build-ipk.sh new file mode 100755 index 0000000..5690a96 --- /dev/null +++ b/deploy/openwrt/build-ipk.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Build an OpenWrt .ipk package from a pre-built static binary. +# No OpenWrt SDK needed — just packages the binary with metadata. +# +# Usage: +# ./deploy/openwrt/build-ipk.sh [binary-path] +# +# Examples: +# ./deploy/openwrt/build-ipk.sh x86_64 dist/btest # from cross-compiled binary +# ./deploy/openwrt/build-ipk.sh aarch64 dist/btest # for RPi/ARM64 routers +# ./deploy/openwrt/build-ipk.sh mipsel target/release/btest # for MIPS little-endian +# +# Supported architectures: x86_64, aarch64, arm_cortex-a7, mipsel_24kc, mips_24kc +set -euo pipefail + +cd "$(dirname "$0")/../.." + +ARCH="${1:?Usage: $0 [binary-path]}" +BINARY="${2:-dist/btest}" +VERSION="0.6.0" +PKG_NAME="btest-rs" +OUTPUT_DIR="dist" + +if [ ! -f "$BINARY" ]; then + echo "Error: binary not found at $BINARY" + echo "Build it first: cargo build --release --target " + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" +WORKDIR=$(mktemp -d) +trap "rm -rf $WORKDIR" EXIT + +echo "=== Building ${PKG_NAME}_${VERSION}_${ARCH}.ipk ===" + +# Create package structure +mkdir -p "$WORKDIR/data/usr/bin" +mkdir -p "$WORKDIR/data/etc/init.d" +mkdir -p "$WORKDIR/data/etc/config" +mkdir -p "$WORKDIR/control" + +# Install files +cp "$BINARY" "$WORKDIR/data/usr/bin/btest" +chmod 755 "$WORKDIR/data/usr/bin/btest" +cp deploy/openwrt/files/btest.init "$WORKDIR/data/etc/init.d/btest" +chmod 755 "$WORKDIR/data/etc/init.d/btest" +cp deploy/openwrt/files/btest.config "$WORKDIR/data/etc/config/btest" + +# Calculate installed size +INSTALLED_SIZE=$(du -sk "$WORKDIR/data" | awk '{print $1}') + +# Control file +cat > "$WORKDIR/control/control" << EOF +Package: ${PKG_NAME} +Version: ${VERSION}-1 +Depends: libc +Source: https://github.com/manawenuz/btest-rs +License: MIT AND Apache-2.0 +Section: net +SourceName: ${PKG_NAME} +Maintainer: Siavash Sameni +Architecture: ${ARCH} +Installed-Size: ${INSTALLED_SIZE} +Description: MikroTik Bandwidth Test server and client + A Rust reimplementation of the MikroTik btest protocol. + Supports TCP/UDP, EC-SRP5 and MD5 auth, IPv4/IPv6. +EOF + +# Post-install script +cat > "$WORKDIR/control/postinst" << 'EOF' +#!/bin/sh +[ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 +/etc/init.d/btest enable 2>/dev/null || true +exit 0 +EOF +chmod 755 "$WORKDIR/control/postinst" + +# Pre-remove script +cat > "$WORKDIR/control/prerm" << 'EOF' +#!/bin/sh +/etc/init.d/btest stop 2>/dev/null || true +/etc/init.d/btest disable 2>/dev/null || true +exit 0 +EOF +chmod 755 "$WORKDIR/control/prerm" + +# Conffiles +cat > "$WORKDIR/control/conffiles" << EOF +/etc/config/btest +EOF + +# Build the .ipk (it's just a tar.gz of tar.gz's) +cd "$WORKDIR" + +# Create data.tar.gz +(cd data && tar czf ../data.tar.gz .) + +# Create control.tar.gz +(cd control && tar czf ../control.tar.gz .) + +# Create debian-binary +echo "2.0" > debian-binary + +# Package it all +tar czf "${PKG_NAME}_${VERSION}-1_${ARCH}.ipk" debian-binary control.tar.gz data.tar.gz + +cd - +cp "$WORKDIR/${PKG_NAME}_${VERSION}-1_${ARCH}.ipk" "$OUTPUT_DIR/" + +echo "" +echo "Package: $OUTPUT_DIR/${PKG_NAME}_${VERSION}-1_${ARCH}.ipk" +ls -lh "$OUTPUT_DIR/${PKG_NAME}_${VERSION}-1_${ARCH}.ipk" +echo "" +echo "Install on OpenWrt:" +echo " scp $OUTPUT_DIR/${PKG_NAME}_${VERSION}-1_${ARCH}.ipk root@router:/tmp/" +echo " ssh root@router 'opkg install /tmp/${PKG_NAME}_${VERSION}-1_${ARCH}.ipk'" +echo " ssh root@router '/etc/init.d/btest enable && /etc/init.d/btest start'" diff --git a/deploy/openwrt/files/btest.config b/deploy/openwrt/files/btest.config new file mode 100644 index 0000000..766365f --- /dev/null +++ b/deploy/openwrt/files/btest.config @@ -0,0 +1,7 @@ +config server + option enabled '0' + option port '2000' + option auth_user '' + option auth_pass '' + option ecsrp5 '0' + option syslog '' diff --git a/deploy/openwrt/files/btest.init b/deploy/openwrt/files/btest.init new file mode 100755 index 0000000..c1b7ae7 --- /dev/null +++ b/deploy/openwrt/files/btest.init @@ -0,0 +1,34 @@ +#!/bin/sh /etc/rc.common +# btest-rs OpenWrt init script + +START=90 +STOP=10 + +USE_PROCD=1 + +start_service() { + local enabled port auth_user auth_pass ecsrp5 syslog + + config_load btest + config_get_bool enabled server enabled 0 + [ "$enabled" -eq 0 ] && return + + config_get port server port 2000 + config_get auth_user server auth_user '' + config_get auth_pass server auth_pass '' + config_get_bool ecsrp5 server ecsrp5 0 + config_get syslog server syslog '' + + procd_open_instance + procd_set_param command /usr/bin/btest -s -P "$port" + + [ -n "$auth_user" ] && procd_append_param command -a "$auth_user" + [ -n "$auth_pass" ] && procd_append_param command -p "$auth_pass" + [ "$ecsrp5" -eq 1 ] && procd_append_param command --ecsrp5 + [ -n "$syslog" ] && procd_append_param command --syslog "$syslog" + + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} diff --git a/src/bin/client_only.rs b/src/bin/client_only.rs new file mode 100644 index 0000000..ed238e3 --- /dev/null +++ b/src/bin/client_only.rs @@ -0,0 +1,127 @@ +//! btest-client: minimal bandwidth test client for embedded/OpenWrt systems. +//! +//! Stripped-down client that connects to MikroTik btest servers. +//! No server mode, no syslog, smaller binary footprint. +//! +//! Build: cargo build --profile release-small --bin btest-client + +use clap::Parser; +use std::sync::atomic::Ordering; + +#[derive(Parser)] +#[command(name = "btest-client", about = "MikroTik Bandwidth Test client", version)] +struct Cli { + /// Server address to connect to + #[arg(short = 'c', long = "client", required = true)] + host: String, + + /// Transmit data (upload) + #[arg(short = 't', long = "transmit")] + transmit: bool, + + /// Receive data (download) + #[arg(short = 'r', long = "receive")] + receive: bool, + + /// Use UDP + #[arg(short = 'u', long = "udp")] + udp: bool, + + /// Bandwidth limit (e.g., 100M) + #[arg(short = 'b', long = "bandwidth")] + bandwidth: Option, + + /// Port + #[arg(short = 'P', long = "port", default_value_t = 2000)] + port: u16, + + /// Username + #[arg(short = 'a', long = "authuser")] + auth_user: Option, + + /// Password + #[arg(short = 'p', long = "authpass")] + auth_pass: Option, + + /// NAT mode + #[arg(short = 'n', long = "nat")] + nat: bool, + + /// Duration in seconds (0=unlimited) + #[arg(short = 'd', long = "duration", default_value_t = 0)] + duration: u64, + + /// Verbose + #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)] + verbose: u8, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let filter = match cli.verbose { + 0 => "info", + 1 => "debug", + _ => "trace", + }; + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)), + ) + .with_target(false) + .init(); + + btest_rs::cpu::start_sampler(); + + if !cli.transmit && !cli.receive { + eprintln!("Error: specify -t (transmit) and/or -r (receive)"); + std::process::exit(1); + } + + let direction = match (cli.transmit, cli.receive) { + (true, false) => btest_rs::protocol::CMD_DIR_RX, + (false, true) => btest_rs::protocol::CMD_DIR_TX, + (true, true) => btest_rs::protocol::CMD_DIR_BOTH, + _ => unreachable!(), + }; + + let bw = match &cli.bandwidth { + Some(b) => btest_rs::bandwidth::parse_bandwidth(b)?, + None => 0, + }; + + let (tx_speed, rx_speed) = match direction { + btest_rs::protocol::CMD_DIR_TX => (bw, 0), + btest_rs::protocol::CMD_DIR_RX => (0, bw), + _ => (bw, bw), + }; + + let state = btest_rs::bandwidth::BandwidthState::new(); + let state_clone = state.clone(); + + let host = cli.host.clone(); + let client_fut = btest_rs::client::run_client( + &host, cli.port, direction, cli.udp, + tx_speed, rx_speed, + cli.auth_user, cli.auth_pass, cli.nat, + state_clone, + ); + + if cli.duration > 0 { + match tokio::time::timeout( + std::time::Duration::from_secs(cli.duration), + client_fut, + ).await { + Ok(r) => { let _ = r?; } + Err(_) => { + state.running.store(false, Ordering::SeqCst); + } + } + } else { + let _ = client_fut.await?; + } + + Ok(()) +} diff --git a/src/bin/server_only.rs b/src/bin/server_only.rs new file mode 100644 index 0000000..c0fbe21 --- /dev/null +++ b/src/bin/server_only.rs @@ -0,0 +1,62 @@ +//! btest-server: minimal bandwidth test server for embedded/OpenWrt systems. +//! +//! Stripped-down server that accepts MikroTik client connections. +//! No client mode, no syslog, no CSV, smaller binary footprint. +//! +//! Build: cargo build --profile release-small --bin btest-server + +use clap::Parser; + +#[derive(Parser)] +#[command(name = "btest-server", about = "MikroTik Bandwidth Test server", version)] +struct Cli { + /// Port + #[arg(short = 'P', long = "port", default_value_t = 2000)] + port: u16, + + /// IPv4 listen address + #[arg(long = "listen", default_value = "0.0.0.0")] + listen_addr: String, + + /// Username + #[arg(short = 'a', long = "authuser")] + auth_user: Option, + + /// Password + #[arg(short = 'p', long = "authpass")] + auth_pass: Option, + + /// Use EC-SRP5 authentication + #[arg(long = "ecsrp5")] + ecsrp5: bool, + + /// Verbose + #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)] + verbose: u8, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let filter = match cli.verbose { + 0 => "info", + 1 => "debug", + _ => "trace", + }; + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)), + ) + .with_target(false) + .init(); + + btest_rs::cpu::start_sampler(); + + let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) }; + + tracing::info!("btest-server starting on port {}", cli.port); + btest_rs::server::run_server(cli.port, cli.auth_user, cli.auth_pass, cli.ecsrp5, v4, None).await?; + Ok(()) +}