use std::{net::SocketAddr, sync::Arc};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
net::{TcpListener, TcpStream},
sync::watch,
};
use tracing::{debug, info, warn};
use crate::{allowlist::DomainAllowlist, error::ProxyError};
pub struct ProxyServer {
listener: TcpListener,
allowlist: Arc<DomainAllowlist>,
shutdown_rx: watch::Receiver<bool>,
}
impl ProxyServer {
pub async fn bind(
allowlist: DomainAllowlist,
shutdown_rx: watch::Receiver<bool>,
) -> Result<(Self, u16), ProxyError> {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.map_err(ProxyError::Bind)?;
let port = listener.local_addr().map_err(ProxyError::Bind)?.port();
info!(port, "sbe proxy listening");
Ok((
Self {
listener,
allowlist: Arc::new(allowlist),
shutdown_rx,
},
port,
))
}
pub async fn run(self) -> Result<(), ProxyError> {
let mut shutdown = self.shutdown_rx;
loop {
tokio::select! {
result = self.listener.accept() => {
let (stream, addr): (TcpStream, SocketAddr) = result.map_err(ProxyError::Accept)?;
let allowlist = Arc::clone(&self.allowlist);
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, addr, &allowlist).await {
debug!(error = %e, "proxy connection error");
}
});
}
_ = shutdown.changed() => {
info!("sbe proxy shutting down");
break;
}
}
}
Ok(())
}
}
async fn handle_connection(
client: TcpStream,
addr: SocketAddr,
allowlist: &DomainAllowlist,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = BufReader::new(client);
let mut request_line = String::new();
client.read_line(&mut request_line).await?;
let request_line = request_line.trim().to_owned();
if request_line.is_empty() {
return Ok(());
}
let parts: Vec<&str> = request_line.split_whitespace().collect();
if parts.len() < 2 {
client
.write_all(b"HTTP/1.1 400 Bad Request\r\n\r\n")
.await?;
return Ok(());
}
let method = parts[0].to_uppercase();
let target = parts[1].to_owned();
if method == "CONNECT" {
handle_connect(client, addr, &target, allowlist).await
} else {
client
.write_all(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n")
.await?;
warn!(method = %method, addr = %addr, "rejected non-CONNECT request");
Ok(())
}
}
async fn handle_connect(
mut client: BufReader<TcpStream>,
addr: SocketAddr,
target: &str,
allowlist: &DomainAllowlist,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (host, port) = parse_host_port(target)?;
let mut header_line = String::new();
loop {
header_line.clear();
client.read_line(&mut header_line).await?;
if header_line.trim().is_empty() {
break;
}
}
if !allowlist.is_allowed(&host) {
warn!(
host = %host,
port = port,
client = %addr,
"blocked connection to non-allowed domain"
);
let response = format!(
"HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nsbe: \
domain '{host}' is not in the allowlist\n"
);
client.write_all(response.as_bytes()).await?;
return Ok(());
}
let upstream_addr = format!("{host}:{port}");
let mut upstream = TcpStream::connect(&upstream_addr).await.map_err(|e| {
Box::new(ProxyError::UpstreamConnect {
host: host.clone(),
port,
source: e,
})
})?;
client
.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
.await?;
client.flush().await?;
debug!(host = %host, port = port, client = %addr, "tunnel established");
let buffered = client.buffer().to_vec();
if !buffered.is_empty() {
upstream.write_all(&buffered).await?;
}
let mut client_stream = client.into_inner();
let _ = tokio::io::copy_bidirectional(&mut client_stream, &mut upstream).await;
Ok(())
}
fn parse_host_port(
target: &str,
) -> Result<(String, u16), Box<dyn std::error::Error + Send + Sync>> {
if let Some(bracket_end) = target.find("]:") {
let host = target[1..bracket_end].to_owned();
let port: u16 = target[bracket_end + 2..].parse()?;
return Ok((host, port));
}
let mut parts = target.rsplitn(2, ':');
let port_str = parts.next().ok_or("missing port")?;
let host = parts.next().ok_or("missing host")?;
let port: u16 = port_str.parse()?;
Ok((host.to_owned(), port))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_parse_host_port() {
let (host, port) = parse_host_port("registry.npmjs.org:443").unwrap();
assert_eq!(host, "registry.npmjs.org");
assert_eq!(port, 443);
}
#[test]
fn test_should_parse_host_port_8000() {
let (host, port) = parse_host_port("evil.com:8000").unwrap();
assert_eq!(host, "evil.com");
assert_eq!(port, 8000);
}
}