mirror of
https://codeberg.org/gigirassy/the-pit
synced 2026-08-30 23:27:39 +00:00
Add src/main.rs
This commit is contained in:
+229
@@ -0,0 +1,229 @@
|
|||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use mimalloc::MiMalloc;
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
io,
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::Arc,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
sync::{OwnedSemaphorePermit, Semaphore},
|
||||||
|
time::{self, Duration, Instant, MissedTickBehavior},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static GLOBAL: MiMalloc = MiMalloc;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Config {
|
||||||
|
delay: Duration,
|
||||||
|
min_line_len: usize,
|
||||||
|
max_line_len: usize,
|
||||||
|
max_connections: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn from_env() -> Self {
|
||||||
|
let delay_ms = env_usize("TARPIT_DELAY_MS", 10_000);
|
||||||
|
let min_line_len = env_usize("TARPIT_MIN_LINE", 8);
|
||||||
|
let max_line_len = env_usize("TARPIT_MAX_LINE", 64);
|
||||||
|
let max_connections = env_usize("TARPIT_MAX_CONNECTIONS", 4096);
|
||||||
|
|
||||||
|
let min_line_len = min_line_len.max(1);
|
||||||
|
let max_line_len = max_line_len.max(min_line_len);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
delay: Duration::from_millis(delay_ms as u64),
|
||||||
|
min_line_len,
|
||||||
|
max_line_len,
|
||||||
|
max_connections: max_connections.max(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> io::Result<()> {
|
||||||
|
if self.min_line_len > self.max_line_len {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"TARPIT_MIN_LINE must be <= TARPIT_MAX_LINE",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.max_connections == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"TARPIT_MAX_CONNECTIONS must be > 0",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct XorShift64 {
|
||||||
|
state: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XorShift64 {
|
||||||
|
fn new(seed: u64) -> Self {
|
||||||
|
// Avoid the all-zero state.
|
||||||
|
Self {
|
||||||
|
state: if seed == 0 { 0x9e3779b97f4a7c15 } else { seed },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_u64(&mut self) -> u64 {
|
||||||
|
let mut x = self.state;
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
self.state = x;
|
||||||
|
x.wrapping_mul(0x2545F4914F6CDD1D)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_range_inclusive(&mut self, start: usize, end_inclusive: usize) -> usize {
|
||||||
|
debug_assert!(start <= end_inclusive);
|
||||||
|
let span = (end_inclusive - start + 1) as u64;
|
||||||
|
(start as u64 + (self.next_u64() % span)) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seed_for_peer(peer: SocketAddr) -> u64 {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos() as u64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
peer.hash(&mut hasher);
|
||||||
|
|
||||||
|
now ^ hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
let bind_addr = env::var("TARPIT_BIND").unwrap_or_else(|_| "0.0.0.0:2222".to_string());
|
||||||
|
let cfg = Config::from_env();
|
||||||
|
cfg.validate()?;
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(&bind_addr).await?;
|
||||||
|
let limiter = Arc::new(Semaphore::new(cfg.max_connections));
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"listening on {bind_addr}, delay={}ms, line={}..{} bytes, max_conn={}",
|
||||||
|
cfg.delay.as_millis(),
|
||||||
|
cfg.min_line_len,
|
||||||
|
cfg.max_line_len,
|
||||||
|
cfg.max_connections
|
||||||
|
);
|
||||||
|
|
||||||
|
accept_loop(listener, limiter, cfg).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn accept_loop(
|
||||||
|
listener: TcpListener,
|
||||||
|
limiter: Arc<Semaphore>,
|
||||||
|
cfg: Config,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let shutdown = shutdown_signal();
|
||||||
|
tokio::pin!(shutdown);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut shutdown => {
|
||||||
|
eprintln!("shutdown requested");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
accepted = listener.accept() => {
|
||||||
|
let (stream, peer) = match accepted {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("accept error: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let permit = match limiter.clone().acquire_owned().await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_client(stream, peer, cfg, permit).await {
|
||||||
|
eprintln!("{peer}: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_client(
|
||||||
|
mut stream: TcpStream,
|
||||||
|
_peer: SocketAddr,
|
||||||
|
cfg: Config,
|
||||||
|
_permit: OwnedSemaphorePermit,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let _ = stream.set_nodelay(true);
|
||||||
|
|
||||||
|
let mut rng = XorShift64::new(seed_for_peer(_peer));
|
||||||
|
let mut buf = vec![0u8; cfg.max_line_len + 2];
|
||||||
|
|
||||||
|
let mut interval = time::interval_at(Instant::now() + cfg.delay, cfg.delay);
|
||||||
|
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
let line_len = rng.gen_range_inclusive(cfg.min_line_len, cfg.max_line_len);
|
||||||
|
fill_line(&mut buf[..line_len], &mut rng);
|
||||||
|
buf[line_len] = b'\r';
|
||||||
|
buf[line_len + 1] = b'\n';
|
||||||
|
|
||||||
|
stream.write_all(&buf[..line_len + 2]).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_line(dst: &mut [u8], rng: &mut XorShift64) {
|
||||||
|
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
for byte in dst.iter_mut() {
|
||||||
|
let idx = rng.gen_range_inclusive(0, ALPHABET.len() - 1);
|
||||||
|
*byte = ALPHABET[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown_signal() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
|
|
||||||
|
let mut sigterm = signal(SignalKind::terminate())
|
||||||
|
.expect("failed to install SIGTERM handler");
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::signal::ctrl_c() => {}
|
||||||
|
_ = sigterm.recv() => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
let _ = tokio::signal::ctrl_c().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_usize(name: &str, default: usize) -> usize {
|
||||||
|
env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user