v0.5.0: Add syslog support, fix TCP send/both, EC-SRP5 server auth
All checks were successful
CI / test (push) Successful in 1m22s
All checks were successful
CI / test (push) Successful in 1m22s
New features: - --syslog <address:port> sends structured events to remote syslog (RFC 5424 UDP) Events: AUTH_SUCCESS, AUTH_FAILURE, TEST_START, TEST_END, TEST_RESULT - EC-SRP5 authentication for both client and server modes - TCP multi-connection support (session tokens, all 3 directions) Bug fixes since v0.2.0: - EC-SRP5 server: fixed gamma parity (was 50% auth failure rate) - EC-SRP5 server: use lift_x not redp1 for verification - TCP send direction: server sends 12-byte status messages to client - TCP both direction: TX loop injects status between data packets - TCP data: send all zeros (no 0x07 header that MikroTik rejected) - TCP disconnect detection: running flag set on EOF - UDP multi-connection: unconnected socket accepts all source ports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -99,11 +99,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "btest-rs"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"clap",
|
||||
"hostname",
|
||||
"md-5",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
@@ -267,6 +268,17 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.9"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "btest-rs"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
description = "MikroTik Bandwidth Test (btest) server and client with EC-SRP5 auth — a Rust reimplementation"
|
||||
license = "MIT AND Apache-2.0"
|
||||
@@ -31,6 +31,7 @@ num-bigint = "0.4.6"
|
||||
num-traits = "0.2.19"
|
||||
num-integer = "0.1.46"
|
||||
sha2 = "0.11.0"
|
||||
hostname = "0.4.2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
@@ -4,3 +4,4 @@ pub mod client;
|
||||
pub mod ecsrp5;
|
||||
pub mod protocol;
|
||||
pub mod server;
|
||||
pub mod syslog_logger;
|
||||
|
||||
12
src/main.rs
12
src/main.rs
@@ -4,6 +4,7 @@ mod client;
|
||||
mod ecsrp5;
|
||||
mod protocol;
|
||||
mod server;
|
||||
pub mod syslog_logger;
|
||||
|
||||
use clap::Parser;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
@@ -65,6 +66,10 @@ struct Cli {
|
||||
#[arg(short = 'n', long = "nat")]
|
||||
nat: bool,
|
||||
|
||||
/// Send logs to remote syslog server (e.g., 192.168.1.1:514)
|
||||
#[arg(long = "syslog")]
|
||||
syslog: Option<String>,
|
||||
|
||||
/// Verbose logging (repeat for more: -v, -vv, -vvv)
|
||||
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
|
||||
verbose: u8,
|
||||
@@ -87,6 +92,13 @@ async fn main() -> anyhow::Result<()> {
|
||||
.with_target(false)
|
||||
.init();
|
||||
|
||||
// Initialize syslog if requested
|
||||
if let Some(ref syslog_addr) = cli.syslog {
|
||||
if let Err(e) = syslog_logger::init(syslog_addr) {
|
||||
eprintln!("Warning: failed to initialize syslog to {}: {}", syslog_addr, e);
|
||||
}
|
||||
}
|
||||
|
||||
if cli.server {
|
||||
// Server mode
|
||||
tracing::info!("Starting btest server on port {}", cli.port);
|
||||
|
||||
@@ -66,7 +66,11 @@ pub async fn run_server(
|
||||
if let Err(e) =
|
||||
handle_client(stream, peer, auth_user, auth_pass, udp_offset, sessions, ecsrp5).await
|
||||
{
|
||||
tracing::error!("Client {} error: {}", peer, e);
|
||||
let err_str = format!("{}", e);
|
||||
tracing::error!("Client {} error: {}", peer, err_str);
|
||||
if err_str.contains("uth") {
|
||||
crate::syslog_logger::auth_failure(&peer.to_string(), "-", "-", &err_str);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -229,7 +233,14 @@ async fn handle_client(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if cmd.is_udp() {
|
||||
// Log auth success and test start
|
||||
let auth_type = if ecsrp5_creds.is_some() { "ecsrp5" } else if auth_user.is_some() { "md5" } else { "none" };
|
||||
let proto_str = if cmd.is_udp() { "UDP" } else { "TCP" };
|
||||
let dir_str = match cmd.direction { CMD_DIR_RX => "RX", CMD_DIR_TX => "TX", _ => "BOTH" };
|
||||
crate::syslog_logger::auth_success(&peer.to_string(), auth_user.as_deref().unwrap_or("-"), auth_type);
|
||||
crate::syslog_logger::test_start(&peer.to_string(), proto_str, dir_str, cmd.tcp_conn_count);
|
||||
|
||||
let result = if cmd.is_udp() {
|
||||
run_udp_test_server(&mut stream, peer, &cmd, udp_port_offset).await
|
||||
} else if is_tcp_multi {
|
||||
let conn_count = cmd.tcp_conn_count;
|
||||
@@ -285,7 +296,10 @@ async fn handle_client(
|
||||
run_tcp_multiconn_server(all_streams, cmd).await
|
||||
} else {
|
||||
run_tcp_test_server(stream, cmd).await
|
||||
}
|
||||
};
|
||||
|
||||
crate::syslog_logger::test_end(&peer.to_string(), proto_str, dir_str);
|
||||
result
|
||||
}
|
||||
|
||||
// --- TCP Test Server ---
|
||||
|
||||
117
src/syslog_logger.rs
Normal file
117
src/syslog_logger.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
//! Syslog integration for btest-rs server mode.
|
||||
//!
|
||||
//! Sends structured log events to a remote syslog server via UDP (RFC 5424).
|
||||
//! Events: auth success/failure, test start/stop, speed results.
|
||||
|
||||
use std::net::UdpSocket;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static SYSLOG: Mutex<Option<SyslogSender>> = Mutex::new(None);
|
||||
|
||||
struct SyslogSender {
|
||||
socket: UdpSocket,
|
||||
target: String,
|
||||
hostname: String,
|
||||
}
|
||||
|
||||
/// Initialize the global syslog sender.
|
||||
/// `target` is the syslog server address, e.g. "192.168.1.1:514".
|
||||
pub fn init(target: &str) -> std::io::Result<()> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let hostname = hostname::get()
|
||||
.map(|h| h.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "btest-rs".to_string());
|
||||
|
||||
let sender = SyslogSender {
|
||||
socket,
|
||||
target: target.to_string(),
|
||||
hostname,
|
||||
};
|
||||
|
||||
*SYSLOG.lock().unwrap() = Some(sender);
|
||||
tracing::info!("Syslog enabled, sending to {}", target);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a syslog message with the given severity and message.
|
||||
/// Severity: 6=info, 4=warning, 3=error
|
||||
fn send(severity: u8, msg: &str) {
|
||||
let guard = SYSLOG.lock().unwrap();
|
||||
if let Some(ref sender) = *guard {
|
||||
// RFC 5424 facility=1 (user), severity as given
|
||||
let priority = 8 + severity; // facility=1 (user-level) * 8 + severity
|
||||
let timestamp = chrono_lite_now();
|
||||
let syslog_msg = format!(
|
||||
"<{}>1 {} {} btest-rs - - - {}",
|
||||
priority, timestamp, sender.hostname, msg,
|
||||
);
|
||||
let _ = sender.socket.send_to(syslog_msg.as_bytes(), &sender.target);
|
||||
}
|
||||
}
|
||||
|
||||
fn chrono_lite_now() -> String {
|
||||
// Simple ISO 8601 timestamp without chrono dependency
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let secs = now.as_secs();
|
||||
// Good enough for syslog — not perfect but functional
|
||||
format!("{}", secs)
|
||||
}
|
||||
|
||||
// --- Public logging functions ---
|
||||
|
||||
pub fn auth_success(peer: &str, username: &str, auth_type: &str) {
|
||||
let msg = format!(
|
||||
"AUTH_SUCCESS peer={} user={} type={}",
|
||||
peer, username, auth_type,
|
||||
);
|
||||
tracing::info!("{}", msg);
|
||||
send(6, &msg);
|
||||
}
|
||||
|
||||
pub fn auth_failure(peer: &str, username: &str, auth_type: &str, reason: &str) {
|
||||
let msg = format!(
|
||||
"AUTH_FAILURE peer={} user={} type={} reason={}",
|
||||
peer, username, auth_type, reason,
|
||||
);
|
||||
tracing::warn!("{}", msg);
|
||||
send(4, &msg);
|
||||
}
|
||||
|
||||
pub fn test_start(peer: &str, proto: &str, direction: &str, conn_count: u8) {
|
||||
let msg = format!(
|
||||
"TEST_START peer={} proto={} dir={} connections={}",
|
||||
peer, proto, direction, conn_count.max(1),
|
||||
);
|
||||
tracing::info!("{}", msg);
|
||||
send(6, &msg);
|
||||
}
|
||||
|
||||
pub fn test_end(peer: &str, proto: &str, direction: &str) {
|
||||
let msg = format!(
|
||||
"TEST_END peer={} proto={} dir={}",
|
||||
peer, proto, direction,
|
||||
);
|
||||
tracing::info!("{}", msg);
|
||||
send(6, &msg);
|
||||
}
|
||||
|
||||
pub fn test_result(
|
||||
peer: &str,
|
||||
direction: &str,
|
||||
avg_mbps: f64,
|
||||
duration_secs: u32,
|
||||
) {
|
||||
let msg = format!(
|
||||
"TEST_RESULT peer={} dir={} avg_mbps={:.2} duration={}s",
|
||||
peer, direction, avg_mbps, duration_secs,
|
||||
);
|
||||
tracing::info!("{}", msg);
|
||||
send(6, &msg);
|
||||
}
|
||||
|
||||
/// Check if syslog is enabled.
|
||||
pub fn is_enabled() -> bool {
|
||||
SYSLOG.lock().unwrap().is_some()
|
||||
}
|
||||
Reference in New Issue
Block a user