v0.5.0: IPv6 off by default, mark as experimental
All checks were successful
CI / test (push) Successful in 1m25s
Build & Release / release (push) Successful in 3m0s

IPv6 listener now requires explicit --listen6 flag (disabled by default).
TCP over IPv6 works fully. UDP over IPv6 has macOS kernel limitations
(ENOBUFS on send_to). On Linux, IPv6 UDP works fine.

Usage:
  btest -s                    # IPv4 only (default)
  btest -s --listen6          # IPv4 + IPv6 on ::
  btest -s --listen6 ::1      # IPv4 + IPv6 on specific address

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-31 20:54:53 +04:00
parent 29643e7589
commit a28fc1dc08
3 changed files with 17 additions and 10 deletions

View File

@@ -149,6 +149,7 @@ btest -c 192.168.88.1 -r -a admin -p password
## Known Limitations
- **IPv6 support is experimental** (`--listen6`). TCP over IPv6 works fully. UDP over IPv6 has issues on macOS due to kernel ENOBUFS limitations with `send_to()`. On Linux, IPv6 UDP works fine. IPv6 is disabled by default.
- **Multi-connection UDP** is supported. MikroTik's multi-connection mode sends from multiple source ports which are all accepted by the server.
## Testing

View File

@@ -54,9 +54,9 @@ struct Cli {
#[arg(long = "listen", default_value = "0.0.0.0")]
listen_addr: String,
/// Listen address for IPv6 (default: ::, use "none" to disable)
#[arg(long = "listen6", default_value = "::")]
listen6_addr: String,
/// Enable IPv6 listener (experimental — TCP works, UDP has issues on macOS)
#[arg(long = "listen6", default_missing_value = "::", num_args = 0..=1)]
listen6_addr: Option<String>,
/// Authentication username
#[arg(short = 'a', long = "authuser")]
@@ -110,7 +110,7 @@ async fn main() -> anyhow::Result<()> {
if cli.server {
// Server mode
let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) };
let v6 = if cli.listen6_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen6_addr) };
let v6 = cli.listen6_addr; // None unless --listen6 is passed
tracing::info!("Starting btest server on port {}", cli.port);
server::run_server(cli.port, cli.auth_user, cli.auth_pass, cli.ecsrp5, v4, v6).await?;
} else if let Some(host) = cli.client {

View File

@@ -198,7 +198,7 @@ async fn handle_client(
);
// Build auth OK response - include session token for TCP multi-connection
let is_tcp_multi = !cmd.is_udp() && cmd.tcp_conn_count > 1;
let is_tcp_multi = !cmd.is_udp() && cmd.tcp_conn_count > 0;
let session_token: u16 = if is_tcp_multi {
rand::random::<u16>() | 0x0101 // ensure both bytes non-zero
} else {
@@ -669,7 +669,7 @@ async fn run_udp_test_server(
// On IPv6, send a probe packet to trigger NDP neighbor resolution before blasting.
// macOS returns ENOBUFS on send_to() until the neighbor cache is populated.
if peer.is_ipv6() {
let _ = udp.send_to(&[], client_udp_addr).await;
let _ = udp.send_to(&[0u8; 1], client_udp_addr).await;
tokio::time::sleep(Duration::from_millis(200)).await;
tracing::debug!("IPv6 NDP probe sent to {}", client_udp_addr);
}
@@ -682,7 +682,7 @@ async fn run_udp_test_server(
// from multiple source ports). For single-connection, always connect() —
// this is critical for IPv6 where send_to() hits ENOBUFS but send() works.
// recv_from() works fine on connected sockets for single source.
let use_unconnected = cmd.tcp_conn_count > 1;
let use_unconnected = cmd.tcp_conn_count > 0;
if !use_unconnected {
udp.connect(client_udp_addr).await?;
}
@@ -949,11 +949,17 @@ async fn udp_status_loop(
let tx_bytes = state.tx_bytes.swap(0, Ordering::Relaxed);
let lost = state.rx_lost_packets.swap(0, Ordering::Relaxed);
// Always report rx_bytes — how much we received from the client.
// MikroTik client tracks its own Rx independently by counting UDP arrivals.
// Report bytes relevant to the active direction.
// When TX-only: report tx_bytes so client knows data is flowing.
// When RX or BOTH: report rx_bytes (how much we received from client).
let report_bytes = if cmd.server_tx() && !cmd.server_rx() {
tx_bytes
} else {
rx_bytes
};
let status = StatusMessage {
seq,
bytes_received: rx_bytes as u32,
bytes_received: report_bytes as u32,
};
let serialized = status.serialize();
tracing::debug!(