use super::{Filter, ProxyHandle, Server};
use anyhow::{Context, Result};
use std::time::Duration;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct BuiltInProxy {
pub filter: Filter,
pub port: Option<u16>,
pub startup_timeout: Duration,
}
impl BuiltInProxy {
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 = Server::bind(self.port, self.filter.clone())
.await
.context("bind built-in proxy")?;
let port = server.port();
let task = tokio::spawn(server.serve());
debug!(
"built-in proxy spawned: port={} filter_size={}",
port,
self.filter.len()
);
let handle = ProxyHandle::from_task(port, task);
#[cfg(target_os = "linux")]
let handle = match attach_uds_bridge(handle, port).await {
Ok(h) => h,
Err((h, e)) => {
tracing::warn!(
"koda-sandbox: UDS bridge spawn failed: {e}. Linux kernel-enforced \
egress unavailable for this session; well-behaved clients still \
route through the env-var tier."
);
h
}
};
Ok(handle)
}
}
#[cfg(target_os = "linux")]
async fn attach_uds_bridge(
handle: ProxyHandle,
tcp_port: u16,
) -> std::result::Result<ProxyHandle, (ProxyHandle, anyhow::Error)> {
let pid = std::process::id();
let uds_path = crate::bwrap_proxy::proxy_uds_path(pid, tcp_port);
match crate::bwrap_proxy::spawn_uds_bridge(uds_path.clone(), tcp_port).await {
Ok(task) => Ok(handle.with_uds_bridge(uds_path, task)),
Err(e) => Err((handle, e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
async fn connect_and_read_status(port: u16, target: &str) -> String {
let mut sock = TcpStream::connect(("127.0.0.1", port)).await.unwrap();
let req = format!("CONNECT {target} HTTP/1.1\r\nHost: {target}\r\n\r\n");
sock.write_all(req.as_bytes()).await.unwrap();
let mut line = String::new();
BufReader::new(sock).read_line(&mut line).await.unwrap();
line
}
#[test]
fn new_sets_defaults() {
let p = BuiltInProxy::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 = BuiltInProxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
assert!(h.port > 0, "ephemeral port must be non-zero");
}
#[tokio::test]
async fn spawned_proxy_serves_403_for_disallowed_host() {
let p = BuiltInProxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
let status = connect_and_read_status(h.port, "evil.example.com:443").await;
assert!(status.starts_with("HTTP/1.1 403"), "got: {status:?}");
}
#[tokio::test]
async fn dropping_handle_stops_accepting() {
let p = BuiltInProxy::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
.expect("must accept while alive");
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),
tokio::io::AsyncReadExt::read(&mut sock, &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");
}
}
}
#[tokio::test]
async fn ca_bundle_is_none_for_builtin() {
let p = BuiltInProxy::new(Filter::default());
let h = p.spawn().await.unwrap();
assert!(h.ca_bundle().is_none());
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn linux_spawn_attaches_uds_bridge() {
let p = BuiltInProxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
let uds = h
.uds_path()
.expect("Linux built-in proxy must attach a UDS bridge");
assert!(
uds.exists(),
"UDS path {} should exist after bridge spawn",
uds.display()
);
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn linux_drop_removes_uds_path() {
let p = BuiltInProxy::new(Filter::new(["github.com"]).unwrap());
let h = p.spawn().await.unwrap();
let uds = h.uds_path().expect("bridge attached").to_owned();
assert!(uds.exists());
drop(h);
assert!(
!uds.exists(),
"UDS path {} must be unlinked after drop",
uds.display()
);
}
}