use super::{Filter, ProxyHandle, Socks5Server};
use anyhow::{Context, Result};
use std::time::Duration;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct BuiltInSocks5Proxy {
pub filter: Filter,
pub port: Option<u16>,
pub startup_timeout: Duration,
}
impl BuiltInSocks5Proxy {
pub fn new(filter: Filter) -> Self {
Self {
filter,
port: None,
startup_timeout: Duration::from_secs(5),
}
}
pub async fn spawn(&self) -> Result<ProxyHandle> {
let server = Socks5Server::bind(self.port, self.filter.clone())
.await
.context("bind built-in socks5 proxy")?;
let port = server.port();
let task = tokio::spawn(server.serve());
debug!(
"built-in socks5 proxy spawned: port={} filter_size={}",
port,
self.filter.len()
);
Ok(ProxyHandle::from_task(port, task))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
#[test]
fn new_sets_defaults() {
let p = BuiltInSocks5Proxy::new(Filter::default());
assert!(p.port.is_none());
assert_eq!(p.startup_timeout, Duration::from_secs(5));
assert!(p.filter.is_empty());
}
#[tokio::test]
async fn spawn_returns_handle_with_assigned_port() {
let p = BuiltInSocks5Proxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
assert!(h.port > 0);
}
#[tokio::test]
async fn spawned_proxy_serves_403_equivalent_for_disallowed_host() {
let p = BuiltInSocks5Proxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
let mut sock = TcpStream::connect(("127.0.0.1", h.port)).await.unwrap();
sock.write_all(&[0x05, 0x01, 0x00]).await.unwrap();
let mut greet = [0u8; 2];
sock.read_exact(&mut greet).await.unwrap();
assert_eq!(greet, [0x05, 0x00]);
let host = "evil.example.com";
let mut req = vec![0x05, 0x01, 0x00, 0x03, host.len() as u8];
req.extend_from_slice(host.as_bytes());
req.extend_from_slice(&443u16.to_be_bytes());
sock.write_all(&req).await.unwrap();
let mut reply = [0u8; 4];
sock.read_exact(&mut reply).await.unwrap();
assert_eq!(reply[1], 0x02);
}
#[tokio::test]
async fn dropping_handle_stops_accepting() {
let p = BuiltInSocks5Proxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
let port = h.port;
let _ = TcpStream::connect(("127.0.0.1", port)).await.unwrap();
drop(h);
tokio::time::sleep(Duration::from_millis(50)).await;
match TcpStream::connect(("127.0.0.1", port)).await {
Err(_) => {} Ok(mut sock) => {
let mut buf = [0u8; 16];
let n = tokio::time::timeout(Duration::from_millis(200), sock.read(&mut buf))
.await
.expect("read must not hang post-drop")
.expect("read must not error");
assert_eq!(n, 0, "post-drop socket must EOF immediately");
}
}
}
}