mirror of
https://github.com/mrvasil/ctf-ad-mirror-tool-rust.git
synced 2026-10-23 14:31:02 +00:00
173 lines
5.6 KiB
Rust
173 lines
5.6 KiB
Rust
//! Reproducible loopback comparison. Not a claim about production networks.
|
|
use anyhow::Result;
|
|
use mirror_tool::{
|
|
config::{NetworkConfig, ReplicaPolicy, ServiceConfig, Team},
|
|
engine::Engine,
|
|
};
|
|
use serde_json::json;
|
|
use std::{net::SocketAddr, time::Instant};
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::{TcpListener, TcpStream},
|
|
};
|
|
|
|
async fn transfer(addr: SocketAddr, megabytes: usize) -> Result<f64> {
|
|
let stream = TcpStream::connect(addr).await?;
|
|
stream.set_nodelay(true)?;
|
|
let (mut read, mut write) = stream.into_split();
|
|
let start = Instant::now();
|
|
let sender = async {
|
|
let data = vec![0x5au8; 65536];
|
|
for _ in 0..megabytes * 16 {
|
|
write.write_all(&data).await?;
|
|
}
|
|
write.shutdown().await
|
|
};
|
|
let receiver = async {
|
|
let mut data = vec![0; 65536];
|
|
let mut total = 0;
|
|
loop {
|
|
let n = read.read(&mut data).await?;
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
anyhow::ensure!(data[..n].iter().all(|b| *b == 0x5a), "byte corruption");
|
|
total += n;
|
|
}
|
|
anyhow::ensure!(total == megabytes * 1048576, "truncated stream");
|
|
Ok::<_, anyhow::Error>(())
|
|
};
|
|
tokio::try_join!(
|
|
async {
|
|
sender.await?;
|
|
Ok::<_, anyhow::Error>(())
|
|
},
|
|
receiver
|
|
)?;
|
|
Ok((megabytes as f64 * 2.0) / start.elapsed().as_secs_f64())
|
|
}
|
|
async fn latency(addr: SocketAddr) -> Result<(f64, f64)> {
|
|
let mut stream = TcpStream::connect(addr).await?;
|
|
stream.set_nodelay(true)?;
|
|
let mut times = vec![];
|
|
let mut reply = [0u8; 32];
|
|
for _ in 0..500 {
|
|
let t = Instant::now();
|
|
stream.write_all(&[42u8; 32]).await?;
|
|
stream.read_exact(&mut reply).await?;
|
|
anyhow::ensure!(reply == [42; 32], "latency reply corruption");
|
|
times.push(t.elapsed().as_secs_f64() * 1e6);
|
|
}
|
|
stream.shutdown().await?;
|
|
times.sort_by(f64::total_cmp);
|
|
Ok((times[250], times[475]))
|
|
}
|
|
fn echo(listener: TcpListener) -> tokio::task::JoinHandle<()> {
|
|
tokio::spawn(async move {
|
|
loop {
|
|
let (stream, _) = listener.accept().await.unwrap();
|
|
stream.set_nodelay(true).unwrap();
|
|
tokio::spawn(async move {
|
|
let (mut read, mut write) = stream.into_split();
|
|
let _ = tokio::io::copy(&mut read, &mut write).await;
|
|
let _ = write.shutdown().await;
|
|
});
|
|
}
|
|
})
|
|
}
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let megabytes = std::env::args()
|
|
.nth(1)
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(64);
|
|
anyhow::ensure!(
|
|
(1..=4096).contains(&megabytes),
|
|
"payload must be 1..4096 MiB"
|
|
);
|
|
let mode = std::env::args().nth(2).unwrap_or_else(|| "none".into());
|
|
anyhow::ensure!(
|
|
matches!(mode.as_str(), "none" | "strict" | "isolate"),
|
|
"mode: none, strict or isolate"
|
|
);
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let direct = listener.local_addr()?;
|
|
let upstream = echo(listener);
|
|
let replica = if mode != "none" {
|
|
Some(echo(
|
|
TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, direct.port())).await?,
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
let mut teams = vec![Team {
|
|
hidden: false,
|
|
id: "local".into(),
|
|
name: "Loopback".into(),
|
|
address: direct.ip(),
|
|
}];
|
|
if replica.is_some() {
|
|
teams.push(Team {
|
|
hidden: false,
|
|
id: "replica".into(),
|
|
name: "Replica".into(),
|
|
address: std::net::Ipv6Addr::LOCALHOST.into(),
|
|
});
|
|
}
|
|
let reserve = TcpListener::bind("127.0.0.1:0").await?;
|
|
let proxy = reserve.local_addr()?;
|
|
drop(reserve);
|
|
let engine = Engine::new(NetworkConfig::default());
|
|
let service = ServiceConfig {
|
|
custom: false,
|
|
custom_ip: None,
|
|
id: "bench".into(),
|
|
name: "Benchmark".into(),
|
|
listen: proxy,
|
|
upstream_port: direct.port(),
|
|
primary: Some("local".into()),
|
|
replicas: if replica.is_some() {
|
|
vec!["replica".into()]
|
|
} else {
|
|
vec![]
|
|
},
|
|
enabled: true,
|
|
auto_switch: false,
|
|
replica_policy: if mode == "strict" {
|
|
ReplicaPolicy::Strict
|
|
} else {
|
|
ReplicaPolicy::Isolate
|
|
},
|
|
};
|
|
engine.upsert(&service, &teams).await?;
|
|
transfer(direct, 4).await?;
|
|
transfer(proxy, 4).await?;
|
|
let mut direct_runs = vec![];
|
|
let mut proxy_runs = vec![];
|
|
for round in 0..3 {
|
|
if round % 2 == 0 {
|
|
direct_runs.push(transfer(direct, megabytes).await?);
|
|
proxy_runs.push(transfer(proxy, megabytes).await?);
|
|
} else {
|
|
proxy_runs.push(transfer(proxy, megabytes).await?);
|
|
direct_runs.push(transfer(direct, megabytes).await?);
|
|
}
|
|
}
|
|
let direct_latency = latency(direct).await?;
|
|
let proxy_latency = latency(proxy).await?;
|
|
engine.shutdown().await;
|
|
upstream.abort();
|
|
let _ = upstream.await;
|
|
if let Some(replica) = replica {
|
|
replica.abort();
|
|
let _ = replica.await;
|
|
}
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(
|
|
&json!({"platform":std::env::consts::OS,"arch":std::env::consts::ARCH,"mode":mode,"payload_mib_per_run":megabytes,"throughput_unit":"MiB/s aggregate upload+echo download (replica bytes excluded)","direct":direct_runs,"proxy":proxy_runs,"direct_latency_us":{"p50":direct_latency.0,"p95":direct_latency.1},"proxy_latency_us":{"p50":proxy_latency.0,"p95":proxy_latency.1},"byte_integrity":"verified every primary reply byte", "stats":engine.snapshots()})
|
|
)?
|
|
);
|
|
Ok(())
|
|
}
|