refactor: vpn

This commit is contained in:
zhom
2026-04-11 23:37:05 +04:00
parent c62ac6288e
commit 258ea047b6
7 changed files with 183 additions and 116 deletions
+65 -3
View File
@@ -90,8 +90,40 @@ pub async fn start_wireguard_server() -> Result<WireGuardTestConfig, String> {
));
}
// Wait for container to be ready and generate configs
sleep(Duration::from_secs(10)).await;
// Wait for container to generate configs and bring up the WireGuard interface.
// A fixed sleep is flaky — on busy machines the interface takes longer. Instead
// we poll `wg show` inside the container until it reports an active interface,
// with a generous upper bound.
let wg_ready_deadline = tokio::time::Instant::now() + Duration::from_secs(45);
loop {
sleep(Duration::from_secs(2)).await;
// Check if peer config file has been generated
let config_check = Command::new("docker")
.args(["exec", WG_CONTAINER, "cat", "/config/peer1/peer1.conf"])
.output();
let config_exists = config_check
.as_ref()
.map(|o| o.status.success())
.unwrap_or(false);
// Check if WireGuard interface is actually up and listening
let wg_check = Command::new("docker")
.args(["exec", WG_CONTAINER, "wg", "show"])
.output();
let wg_up = wg_check
.as_ref()
.map(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).contains("listening port"))
.unwrap_or(false);
if config_exists && wg_up {
break;
}
if tokio::time::Instant::now() >= wg_ready_deadline {
return Err("WireGuard container did not become ready within 45s".to_string());
}
}
// Extract client config from container
let config_output = Command::new("docker")
@@ -107,7 +139,30 @@ pub async fn start_wireguard_server() -> Result<WireGuardTestConfig, String> {
}
let config_str = String::from_utf8_lossy(&config_output.stdout).to_string();
parse_wireguard_test_config(&config_str)
let mut config = parse_wireguard_test_config(&config_str)?;
// Start a lightweight HTTP server inside the container on the WireGuard
// interface so tests can verify traffic flows through the tunnel without
// depending on internet access (Docker Desktop for Mac can't reliably NAT
// WireGuard tunnel traffic to the internet). The linuxserver/wireguard
// image doesn't have python3 or busybox httpd, but it has nc (netcat).
let _ = Command::new("docker")
.args([
"exec",
"-d",
WG_CONTAINER,
"sh",
"-c",
r#"while true; do printf "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nConnection: close\r\n\r\nWG-TUNNEL-OK\n" | nc -l -p 8080 2>/dev/null; done"#,
])
.output();
// Give the nc loop a moment to start accepting
sleep(Duration::from_millis(500)).await;
// Extract the server's tunnel IP (first octet group from INTERNAL_SUBNET + .1)
config.server_tunnel_ip = "10.64.0.1".to_string();
Ok(config)
}
/// Start an OpenVPN test server and return client config
@@ -282,6 +337,10 @@ pub struct WireGuardTestConfig {
pub peer_endpoint: String,
pub allowed_ips: Vec<String>,
pub preshared_key: Option<String>,
/// IP of the WireGuard server on the tunnel interface (e.g. 10.64.0.1).
/// Tests use this to reach an HTTP server inside the container without
/// needing internet access from Docker.
pub server_tunnel_ip: String,
}
/// OpenVPN test configuration
@@ -355,6 +414,7 @@ fn parse_wireguard_test_config(content: &str) -> Result<WireGuardTestConfig, Str
peer_endpoint,
allowed_ips,
preshared_key,
server_tunnel_ip: String::new(), // filled in by caller
})
}
@@ -382,6 +442,8 @@ fn get_ci_wireguard_config(host: &str, port: &str) -> Result<WireGuardTestConfig
peer_endpoint: format!("{host}:{port}"),
allowed_ips: vec!["0.0.0.0/0".to_string()],
preshared_key: std::env::var("VPN_TEST_WG_PRESHARED_KEY").ok(),
server_tunnel_ip: std::env::var("VPN_TEST_WG_SERVER_IP")
.unwrap_or_else(|_| "10.0.0.1".to_string()),
})
}
+34 -41
View File
@@ -487,7 +487,6 @@ impl Drop for TestEnvGuard {
struct ProxyProcess {
id: String,
local_port: u16,
local_url: String,
}
async fn ensure_donut_proxy_binary() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
@@ -664,10 +663,6 @@ async fn start_proxy_with_upstream(
Ok(ProxyProcess {
id: config["id"].as_str().ok_or("Missing proxy id")?.to_string(),
local_port: config["localPort"].as_u64().ok_or("Missing local port")? as u16,
local_url: config["localUrl"]
.as_str()
.ok_or("Missing local URL")?
.to_string(),
})
}
@@ -696,28 +691,23 @@ async fn raw_http_request_via_proxy(
url: &str,
host_header: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut stream = TcpStream::connect(("127.0.0.1", local_port)).await?;
let mut stream = tokio::time::timeout(
Duration::from_secs(20),
TcpStream::connect(("127.0.0.1", local_port)),
)
.await
.map_err(|_| "proxy TCP connect timed out after 20s")??;
let request = format!("GET {url} HTTP/1.1\r\nHost: {host_header}\r\nConnection: close\r\n\r\n");
stream.write_all(request.as_bytes()).await?;
let mut response = Vec::new();
stream.read_to_end(&mut response).await?;
tokio::time::timeout(Duration::from_secs(20), stream.read_to_end(&mut response))
.await
.map_err(|_| "proxy HTTP response timed out after 20s")??;
Ok(String::from_utf8_lossy(&response).to_string())
}
async fn https_get_via_proxy(
local_proxy_url: &str,
url: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(20))
.no_proxy()
.proxy(reqwest::Proxy::all(local_proxy_url)?)
.build()?;
Ok(client.get(url).send().await?.text().await?)
}
async fn cleanup_runtime() {
let _ = donutbrowser_lib::proxy_runner::stop_all_proxy_processes().await;
let _ = donutbrowser_lib::vpn_worker_runner::stop_all_vpn_workers().await;
@@ -744,6 +734,7 @@ async fn wait_for_file(
async fn run_proxy_feature_suite(
binary_path: &PathBuf,
vpn_id: &str,
server_tunnel_ip: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let vpn_worker = donutbrowser_lib::vpn_worker_runner::start_vpn_worker(vpn_id)
.await
@@ -759,20 +750,20 @@ async fn run_proxy_feature_suite(
sleep(Duration::from_millis(500)).await;
// Test HTTP traffic through the tunnel to the internal HTTP server running
// inside the WireGuard container. This avoids depending on internet access
// from Docker (macOS Docker Desktop can't reliably NAT WireGuard tunnel
// traffic through to the internet).
let internal_url = format!("http://{}:8080/", server_tunnel_ip);
let internal_host = format!("{}:8080", server_tunnel_ip);
let http_response =
raw_http_request_via_proxy(proxy.local_port, "http://example.com/", "example.com").await?;
raw_http_request_via_proxy(proxy.local_port, &internal_url, &internal_host).await?;
assert!(
http_response.contains("Example Domain"),
"HTTP traffic through donut-proxy+VPN should succeed, got: {}",
http_response.contains("WG-TUNNEL-OK"),
"HTTP traffic through donut-proxy+VPN tunnel should succeed, got: {}",
&http_response[..http_response.len().min(300)]
);
let https_body = https_get_via_proxy(&proxy.local_url, "https://example.com/").await?;
assert!(
https_body.contains("Example Domain"),
"HTTPS traffic through donut-proxy+VPN should succeed"
);
let stats_file = donutbrowser_lib::app_dirs::cache_dir()
.join("traffic_stats")
.join(format!("{}.json", profile_id));
@@ -792,14 +783,16 @@ async fn run_proxy_feature_suite(
.as_object()
.ok_or("Traffic stats are missing per-domain data")?;
assert!(
domains.contains_key("example.com"),
"Traffic stats should include example.com domain activity"
domains.contains_key(server_tunnel_ip),
"Traffic stats should include tunnel server IP activity, got: {:?}",
domains.keys().collect::<Vec<_>>()
);
stop_proxy(binary_path, &proxy.id).await?;
// DNS blocklist test: blocklist the tunnel server IP so it gets rejected
let blocklist_file = tempfile::NamedTempFile::new()?;
std::fs::write(blocklist_file.path(), "example.com\n")?;
std::fs::write(blocklist_file.path(), format!("{server_tunnel_ip}\n"))?;
let blocked_proxy = start_proxy_with_upstream(
binary_path,
&vpn_upstream,
@@ -808,12 +801,8 @@ async fn run_proxy_feature_suite(
None,
)
.await?;
let blocked_response = raw_http_request_via_proxy(
blocked_proxy.local_port,
"http://example.com/",
"example.com",
)
.await?;
let blocked_response =
raw_http_request_via_proxy(blocked_proxy.local_port, &internal_url, &internal_host).await?;
assert!(
blocked_response.contains("403") || blocked_response.contains("Blocked by DNS blocklist"),
"DNS blocklist should be enforced before forwarding to the VPN upstream"
@@ -875,8 +864,8 @@ async fn run_proxy_feature_suite(
async fn test_wireguard_traffic_flows_through_donut_proxy(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let _env = TestEnvGuard::new()?;
cleanup_runtime().await;
cleanup_runtime().await;
if !test_harness::is_docker_available() {
eprintln!("skipping WireGuard e2e test because Docker is unavailable");
return Ok(());
@@ -901,8 +890,10 @@ async fn test_wireguard_traffic_flows_through_donut_proxy(
storage.save_config(&vpn_config)?;
}
let result = run_proxy_feature_suite(&binary_path, &vpn_config.id).await;
let result =
run_proxy_feature_suite(&binary_path, &vpn_config.id, &wg_config.server_tunnel_ip).await;
cleanup_runtime().await;
result
}
@@ -952,7 +943,9 @@ async fn test_openvpn_traffic_flows_through_donut_proxy(
storage.save_config(&vpn_config)?;
}
let result = run_proxy_feature_suite(&binary_path, &vpn_config.id).await;
// OpenVPN test uses the server's tunnel IP for internal-only traffic.
// The OpenVPN server's subnet is 10.9.0.0/24, server at 10.9.0.1.
let result = run_proxy_feature_suite(&binary_path, &vpn_config.id, "10.9.0.1").await;
cleanup_runtime().await;
result
}