Parent: #108
Problem
crates/relay/src/main.rs:127-147:
tokio::spawn(async move {
loop {
if let Ok((stream, _)) = bootstrap_listener.accept().await {
let id = Arc::clone(&id_for_handler);
tokio::spawn(async move {
let (mut reader, mut writer) = stream.into_split();
let mut buf = [0u8; 1024];
let _ = tokio::io::AsyncReadExt::read(&mut reader, &mut buf).await;
// ... format response and write_all ...
let _ = tokio::io::AsyncWriteExt::write_all(&mut writer, response.as_bytes()).await;
});
}
}
});
Issues:
- No I/O timeout. A client can hold the connection open for minutes sending one byte at a time.
- No concurrent-connection cap.
accept() spawns an unbounded task per connection; thousands of slow clients exhaust file descriptors and memory.
- No
Connection: close header. Clients can hold sockets open even after a successful response.
A single attacker can trivially DoS the relay's bootstrap endpoint with a Slowloris-style attack.
Fix
- Wrap
read and write_all in tokio::time::timeout(Duration::from_secs(5), ...).
- Gate the accept loop with a
tokio::sync::Semaphore::new(1024) permit, held for the lifetime of the per-connection task.
- Add
Connection: close\r\n to the HTTP response headers.
- Consider extracting the handler into a small
axum or hyper server if the hand-rolled path keeps growing.
Suggested skeleton
use tokio::sync::Semaphore;
use tokio::time::{timeout, Duration};
const MAX_CONCURRENT: usize = 1024;
const IO_TIMEOUT: Duration = Duration::from_secs(5);
let sem = Arc::new(Semaphore::new(MAX_CONCURRENT));
tokio::spawn(async move {
loop {
let Ok((stream, _)) = bootstrap_listener.accept().await else { continue };
let permit = match Arc::clone(&sem).try_acquire_owned() {
Ok(p) => p,
Err(_) => { tracing::warn!("bootstrap endpoint at capacity"); continue; }
};
let id = Arc::clone(&id_for_handler);
tokio::spawn(async move {
let _permit = permit;
let (mut reader, mut writer) = stream.into_split();
let mut buf = [0u8; 1024];
let _ = timeout(IO_TIMEOUT, tokio::io::AsyncReadExt::read(&mut reader, &mut buf)).await;
let body = id.as_str();
let response = format!(
"HTTP/1.1 200 OK\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{}",
body.len(), body
);
let _ = timeout(IO_TIMEOUT, tokio::io::AsyncWriteExt::write_all(&mut writer, response.as_bytes())).await;
});
}
});
Test
Add an integration test (the relay crate currently has zero tests):
- Start the relay on a random port.
- Open 2000 TCP connections to the bootstrap endpoint without writing anything.
- Assert a normal client can still get a response within 5 seconds.
- Assert sending one byte per second is timed out rather than held open indefinitely.
Parent: #108
Problem
crates/relay/src/main.rs:127-147:Issues:
accept()spawns an unbounded task per connection; thousands of slow clients exhaust file descriptors and memory.Connection: closeheader. Clients can hold sockets open even after a successful response.A single attacker can trivially DoS the relay's bootstrap endpoint with a Slowloris-style attack.
Fix
readandwrite_allintokio::time::timeout(Duration::from_secs(5), ...).tokio::sync::Semaphore::new(1024)permit, held for the lifetime of the per-connection task.Connection: close\r\nto the HTTP response headers.axumorhyperserver if the hand-rolled path keeps growing.Suggested skeleton
Test
Add an integration test (the relay crate currently has zero tests):