refactor: more robust vpn handling

This commit is contained in:
zhom
2026-04-04 03:16:04 +04:00
parent ac5d975e5b
commit 48883ddd03
19 changed files with 1936 additions and 203 deletions
+172 -29
View File
@@ -16,12 +16,21 @@ const WIREGUARD_IMAGE: &str = "linuxserver/wireguard:latest";
const OPENVPN_IMAGE: &str = "kylemanna/openvpn:latest";
const WG_CONTAINER: &str = "donut-wg-test";
const OVPN_CONTAINER: &str = "donut-ovpn-test";
const OVPN_VOLUME: &str = "donut-ovpn-test-data";
/// Check if running in CI environment
pub fn is_ci() -> bool {
std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok()
}
fn has_external_wireguard_service() -> bool {
std::env::var("VPN_TEST_WG_HOST").is_ok()
}
fn has_external_openvpn_service() -> bool {
std::env::var("VPN_TEST_OVPN_HOST").is_ok()
}
/// Check if Docker is available
pub fn is_docker_available() -> bool {
Command::new("docker")
@@ -33,14 +42,10 @@ pub fn is_docker_available() -> bool {
/// Start a WireGuard test server and return client config
pub async fn start_wireguard_server() -> Result<WireGuardTestConfig, String> {
if is_ci() {
// In CI, use the service container configured in workflow
if has_external_wireguard_service() {
let host = std::env::var("VPN_TEST_WG_HOST").unwrap_or_else(|_| "localhost".into());
let port = std::env::var("VPN_TEST_WG_PORT").unwrap_or_else(|_| "51820".into());
// Wait for service to be ready
wait_for_service(&host, port.parse().unwrap_or(51820)).await?;
return get_ci_wireguard_config(&host, &port);
}
@@ -71,6 +76,8 @@ pub async fn start_wireguard_server() -> Result<WireGuardTestConfig, String> {
"SERVERPORT=51820",
"-e",
"PEERDNS=auto",
"-e",
"INTERNAL_SUBNET=10.64.0.0",
WIREGUARD_IMAGE,
])
.output()
@@ -105,14 +112,10 @@ pub async fn start_wireguard_server() -> Result<WireGuardTestConfig, String> {
/// Start an OpenVPN test server and return client config
pub async fn start_openvpn_server() -> Result<OpenVpnTestConfig, String> {
if is_ci() {
// In CI, use the service container configured in workflow
if has_external_openvpn_service() {
let host = std::env::var("VPN_TEST_OVPN_HOST").unwrap_or_else(|_| "localhost".into());
let port = std::env::var("VPN_TEST_OVPN_PORT").unwrap_or_else(|_| "1194".into());
// Wait for service to be ready
wait_for_service(&host, port.parse().unwrap_or(1194)).await?;
return get_ci_openvpn_config(&host, &port);
}
@@ -125,9 +128,139 @@ pub async fn start_openvpn_server() -> Result<OpenVpnTestConfig, String> {
.args(["rm", "-f", OVPN_CONTAINER])
.output();
// For OpenVPN, we need to initialize PKI first, which is complex
// For simplicity in tests, we'll use a pre-configured test config
Err("OpenVPN container setup requires pre-configured PKI. Use test fixtures instead.".to_string())
let _ = Command::new("docker")
.args(["volume", "rm", "-f", OVPN_VOLUME])
.output();
let create_volume = Command::new("docker")
.args(["volume", "create", OVPN_VOLUME])
.output()
.map_err(|e| format!("Failed to create OpenVPN test volume: {e}"))?;
if !create_volume.status.success() {
return Err(format!(
"Failed to create OpenVPN test volume: {}",
String::from_utf8_lossy(&create_volume.stderr)
));
}
let genconfig = Command::new("docker")
.args([
"run",
"--rm",
"-v",
&format!("{OVPN_VOLUME}:/etc/openvpn"),
"-e",
"EASYRSA_BATCH=1",
OPENVPN_IMAGE,
"ovpn_genconfig",
"-u",
"udp://127.0.0.1",
"-s",
"10.9.0.0/24",
])
.output()
.map_err(|e| format!("Failed to generate OpenVPN config: {e}"))?;
if !genconfig.status.success() {
return Err(format!(
"OpenVPN config generation failed: {}",
String::from_utf8_lossy(&genconfig.stderr)
));
}
let init_pki = Command::new("docker")
.args([
"run",
"--rm",
"-v",
&format!("{OVPN_VOLUME}:/etc/openvpn"),
"-e",
"EASYRSA_BATCH=1",
OPENVPN_IMAGE,
"ovpn_initpki",
"nopass",
])
.output()
.map_err(|e| format!("Failed to initialize OpenVPN PKI: {e}"))?;
if !init_pki.status.success() {
return Err(format!(
"OpenVPN PKI initialization failed: {}",
String::from_utf8_lossy(&init_pki.stderr)
));
}
let build_client = Command::new("docker")
.args([
"run",
"--rm",
"-v",
&format!("{OVPN_VOLUME}:/etc/openvpn"),
"-e",
"EASYRSA_BATCH=1",
OPENVPN_IMAGE,
"easyrsa",
"build-client-full",
"donut-test-client",
"nopass",
])
.output()
.map_err(|e| format!("Failed to build OpenVPN client certificate: {e}"))?;
if !build_client.status.success() {
return Err(format!(
"OpenVPN client certificate build failed: {}",
String::from_utf8_lossy(&build_client.stderr)
));
}
let start_server = Command::new("docker")
.args([
"run",
"-d",
"--name",
OVPN_CONTAINER,
"--cap-add=NET_ADMIN",
"-p",
"1194:1194/udp",
"-v",
&format!("{OVPN_VOLUME}:/etc/openvpn"),
OPENVPN_IMAGE,
])
.output()
.map_err(|e| format!("Failed to start OpenVPN container: {e}"))?;
if !start_server.status.success() {
return Err(format!(
"OpenVPN container start failed: {}",
String::from_utf8_lossy(&start_server.stderr)
));
}
sleep(Duration::from_secs(10)).await;
let client_config = Command::new("docker")
.args([
"run",
"--rm",
"-v",
&format!("{OVPN_VOLUME}:/etc/openvpn"),
OPENVPN_IMAGE,
"ovpn_getclient",
"donut-test-client",
])
.output()
.map_err(|e| format!("Failed to fetch OpenVPN client config: {e}"))?;
if !client_config.status.success() {
return Err(format!(
"Failed to read OpenVPN client config: {}",
String::from_utf8_lossy(&client_config.stderr)
));
}
let raw_config = String::from_utf8_lossy(&client_config.stdout).to_string();
Ok(OpenVpnTestConfig {
raw_config,
remote_host: "127.0.0.1".to_string(),
remote_port: 1194,
protocol: "udp".to_string(),
})
}
/// Stop all VPN test servers
@@ -135,21 +268,9 @@ pub async fn stop_vpn_servers() {
let _ = Command::new("docker")
.args(["rm", "-f", WG_CONTAINER, OVPN_CONTAINER])
.output();
}
/// Wait for a network service to be ready
async fn wait_for_service(host: &str, port: u16) -> Result<(), String> {
let timeout = Duration::from_secs(30);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if std::net::TcpStream::connect(format!("{host}:{port}")).is_ok() {
return Ok(());
}
sleep(Duration::from_millis(500)).await;
}
Err(format!("Timeout waiting for service at {host}:{port}"))
let _ = Command::new("docker")
.args(["volume", "rm", "-f", OVPN_VOLUME])
.output();
}
/// WireGuard test configuration
@@ -160,6 +281,7 @@ pub struct WireGuardTestConfig {
pub peer_public_key: String,
pub peer_endpoint: String,
pub allowed_ips: Vec<String>,
pub preshared_key: Option<String>,
}
/// OpenVPN test configuration
@@ -178,6 +300,7 @@ fn parse_wireguard_test_config(content: &str) -> Result<WireGuardTestConfig, Str
let mut peer_public_key = String::new();
let mut peer_endpoint = String::new();
let mut allowed_ips = vec!["0.0.0.0/0".to_string()];
let mut preshared_key = None;
let mut current_section = "";
for line in content.lines() {
@@ -205,6 +328,7 @@ fn parse_wireguard_test_config(content: &str) -> Result<WireGuardTestConfig, Str
("interface", "DNS") => dns = Some(value.to_string()),
("peer", "PublicKey") => peer_public_key = value.to_string(),
("peer", "Endpoint") => peer_endpoint = value.to_string(),
("peer", "PresharedKey") => preshared_key = Some(value.to_string()),
("peer", "AllowedIPs") => {
allowed_ips = value.split(',').map(|s| s.trim().to_string()).collect();
}
@@ -230,12 +354,21 @@ fn parse_wireguard_test_config(content: &str) -> Result<WireGuardTestConfig, Str
peer_public_key,
peer_endpoint,
allowed_ips,
preshared_key,
})
}
/// Get WireGuard config from CI environment
fn get_ci_wireguard_config(host: &str, port: &str) -> Result<WireGuardTestConfig, String> {
// In CI, use environment variables or test fixtures
if std::env::var("VPN_TEST_WG_PRIVATE_KEY").is_err()
|| std::env::var("VPN_TEST_WG_PUBLIC_KEY").is_err()
{
return Err(
"External WireGuard test service is configured, but VPN_TEST_WG_PRIVATE_KEY and VPN_TEST_WG_PUBLIC_KEY are missing"
.to_string(),
);
}
let private_key =
std::env::var("VPN_TEST_WG_PRIVATE_KEY").unwrap_or_else(|_| "test-private-key".to_string());
let public_key =
@@ -248,11 +381,21 @@ fn get_ci_wireguard_config(host: &str, port: &str) -> Result<WireGuardTestConfig
peer_public_key: public_key,
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(),
})
}
/// Get OpenVPN config from CI environment
fn get_ci_openvpn_config(host: &str, port: &str) -> Result<OpenVpnTestConfig, String> {
if let Ok(raw_config) = std::env::var("VPN_TEST_OVPN_RAW_CONFIG") {
return Ok(OpenVpnTestConfig {
raw_config,
remote_host: host.to_string(),
remote_port: port.parse().unwrap_or(1194),
protocol: "udp".to_string(),
});
}
let raw_config = format!(
r#"
client
+536 -3
View File
@@ -3,13 +3,22 @@
//! These tests verify VPN config parsing, storage, and tunnel functionality.
//! Connection tests require Docker and are skipped if Docker is not available.
mod common;
mod test_harness;
use common::TestUtils;
use donutbrowser_lib::vpn::{
detect_vpn_type, parse_openvpn_config, parse_wireguard_config, OpenVpnConfig, VpnConfig,
VpnStorage, VpnType, WireGuardConfig,
};
use serde_json::Value;
use serial_test::serial;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::sleep;
// ============================================================================
// Config Parsing Tests
@@ -420,6 +429,530 @@ async fn test_tunnel_manager() {
assert_eq!(manager.active_count(), 0);
}
// NOTE: Actual connection tests require Docker containers running.
// These are meant to be run with the CI workflow that sets up service containers.
// To run locally: docker run -d --cap-add=NET_ADMIN -p 51820:51820/udp -e PEERS=1 linuxserver/wireguard
struct TestEnvGuard {
_root: PathBuf,
previous_data_dir: Option<String>,
previous_cache_dir: Option<String>,
}
impl TestEnvGuard {
fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
static TEST_RUNTIME_ROOT: OnceLock<PathBuf> = OnceLock::new();
let root = TEST_RUNTIME_ROOT
.get_or_init(|| {
std::env::temp_dir().join(format!("donutbrowser-vpn-e2e-{}", std::process::id()))
})
.clone();
let data_dir = root.join("data");
let cache_dir = root.join("cache");
let vpn_dir = data_dir.join("vpn");
let _ = std::fs::remove_dir_all(&data_dir);
let _ = std::fs::remove_dir_all(&cache_dir);
std::fs::create_dir_all(&vpn_dir)?;
std::fs::create_dir_all(&data_dir)?;
std::fs::create_dir_all(&cache_dir)?;
let previous_data_dir = std::env::var("DONUTBROWSER_DATA_DIR").ok();
let previous_cache_dir = std::env::var("DONUTBROWSER_CACHE_DIR").ok();
std::env::set_var("DONUTBROWSER_DATA_DIR", &data_dir);
std::env::set_var("DONUTBROWSER_CACHE_DIR", &cache_dir);
Ok(Self {
_root: root,
previous_data_dir,
previous_cache_dir,
})
}
}
impl Drop for TestEnvGuard {
fn drop(&mut self) {
if let Some(value) = &self.previous_data_dir {
std::env::set_var("DONUTBROWSER_DATA_DIR", value);
} else {
std::env::remove_var("DONUTBROWSER_DATA_DIR");
}
if let Some(value) = &self.previous_cache_dir {
std::env::set_var("DONUTBROWSER_CACHE_DIR", value);
} else {
std::env::remove_var("DONUTBROWSER_CACHE_DIR");
}
}
}
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>> {
let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
let project_root = PathBuf::from(cargo_manifest_dir)
.parent()
.unwrap()
.to_path_buf();
let proxy_binary_name = if cfg!(windows) {
"donut-proxy.exe"
} else {
"donut-proxy"
};
let proxy_binary = project_root
.join("src-tauri")
.join("target")
.join("debug")
.join(proxy_binary_name);
if !proxy_binary.exists() {
let build_status = tokio::process::Command::new("cargo")
.args(["build", "--bin", "donut-proxy"])
.current_dir(project_root.join("src-tauri"))
.status()
.await?;
if !build_status.success() {
return Err("Failed to build donut-proxy binary".into());
}
}
if !proxy_binary.exists() {
return Err("donut-proxy binary was not created successfully".into());
}
Ok(proxy_binary)
}
fn new_test_vpn_config(name: &str, vpn_type: VpnType, config_data: String) -> VpnConfig {
let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
VpnConfig {
id: uuid::Uuid::new_v4().to_string(),
name: name.to_string(),
vpn_type,
config_data,
created_at,
last_used: None,
sync_enabled: false,
last_sync: None,
}
}
fn build_wireguard_config(config: &test_harness::WireGuardTestConfig) -> String {
format!(
"[Interface]\nPrivateKey = {}\nAddress = {}\n{}\n[Peer]\nPublicKey = {}\n{}Endpoint = {}\nAllowedIPs = {}\nPersistentKeepalive = 25\n",
config.private_key,
config.address,
config
.dns
.as_ref()
.map(|dns| format!("DNS = {dns}\n"))
.unwrap_or_default(),
config.peer_public_key,
config
.preshared_key
.as_ref()
.map(|key| format!("PresharedKey = {key}\n"))
.unwrap_or_default(),
config.peer_endpoint,
config.allowed_ips.join(", ")
)
}
fn openvpn_client_available() -> bool {
if let Ok(path) = std::env::var("DONUTBROWSER_OPENVPN_BIN") {
return PathBuf::from(path).exists();
}
std::process::Command::new(if cfg!(windows) { "where" } else { "which" })
.arg("openvpn")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(windows)]
fn openvpn_adapter_available() -> bool {
let openvpn = std::process::Command::new("openvpn")
.arg("--show-adapters")
.output();
openvpn
.ok()
.map(|output| {
let text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
text
.lines()
.map(str::trim)
.any(|line| !line.is_empty() && !line.starts_with("Available adapters"))
})
.unwrap_or(false)
}
#[cfg(not(windows))]
fn openvpn_adapter_available() -> bool {
true
}
async fn start_proxy_with_upstream(
binary_path: &PathBuf,
upstream_proxy: &str,
bypass_rules: &[String],
blocklist_file: Option<&str>,
profile_id: Option<&str>,
) -> Result<ProxyProcess, Box<dyn std::error::Error + Send + Sync>> {
let upstream_url = url::Url::parse(upstream_proxy)?;
let host = upstream_url
.host_str()
.ok_or("Upstream proxy host is missing")?
.to_string();
let port = upstream_url
.port()
.ok_or("Upstream proxy port is missing")?;
let mut args = vec![
"proxy".to_string(),
"start".to_string(),
"--host".to_string(),
host,
"--proxy-port".to_string(),
port.to_string(),
"--type".to_string(),
upstream_url.scheme().to_string(),
];
if !bypass_rules.is_empty() {
args.push("--bypass-rules".to_string());
args.push(serde_json::to_string(bypass_rules)?);
}
if let Some(blocklist_file) = blocklist_file {
args.push("--blocklist-file".to_string());
args.push(blocklist_file.to_string());
}
if let Some(profile_id) = profile_id {
args.push("--profile-id".to_string());
args.push(profile_id.to_string());
}
let arg_refs = args.iter().map(String::as_str).collect::<Vec<_>>();
let output = TestUtils::execute_command(binary_path, &arg_refs).await?;
if !output.status.success() {
return Err(
format!(
"Failed to start local proxy - stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
}
let config: Value = serde_json::from_str(&String::from_utf8(output.stdout)?)?;
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(),
})
}
async fn stop_proxy(
binary_path: &PathBuf,
proxy_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let output =
TestUtils::execute_command(binary_path, &["proxy", "stop", "--id", proxy_id]).await?;
if !output.status.success() {
return Err(
format!(
"Failed to stop proxy '{}' - stdout: {}, stderr: {}",
proxy_id,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
}
Ok(())
}
async fn raw_http_request_via_proxy(
local_port: u16,
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 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?;
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;
test_harness::stop_vpn_servers().await;
}
async fn wait_for_file(
path: &std::path::Path,
timeout: Duration,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
if path.exists() {
return Ok(());
}
sleep(Duration::from_millis(250)).await;
}
Err(format!("Timed out waiting for file: {}", path.display()).into())
}
async fn run_proxy_feature_suite(
binary_path: &PathBuf,
vpn_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let vpn_worker = donutbrowser_lib::vpn_worker_runner::start_vpn_worker(vpn_id)
.await
.map_err(|error| error.to_string())?;
let vpn_upstream = vpn_worker
.local_url
.clone()
.ok_or("VPN worker did not expose a local URL")?;
let profile_id = format!("vpn-e2e-{}", uuid::Uuid::new_v4());
let proxy =
start_proxy_with_upstream(binary_path, &vpn_upstream, &[], None, Some(&profile_id)).await?;
sleep(Duration::from_millis(500)).await;
let http_response =
raw_http_request_via_proxy(proxy.local_port, "http://example.com/", "example.com").await?;
assert!(
http_response.contains("Example Domain"),
"HTTP traffic through donut-proxy+VPN 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));
wait_for_file(&stats_file, Duration::from_secs(8)).await?;
assert!(
stats_file.exists(),
"Traffic stats should exist for VPN-backed local proxy"
);
let stats: Value = serde_json::from_str(&std::fs::read_to_string(&stats_file)?)?;
let total_requests = stats["total_requests"].as_u64().unwrap_or_default();
assert!(
total_requests > 0,
"Traffic stats should record requests for VPN-backed local proxy"
);
let domains = stats["domains"]
.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"
);
stop_proxy(binary_path, &proxy.id).await?;
let blocklist_file = tempfile::NamedTempFile::new()?;
std::fs::write(blocklist_file.path(), "example.com\n")?;
let blocked_proxy = start_proxy_with_upstream(
binary_path,
&vpn_upstream,
&[],
blocklist_file.path().to_str(),
None,
)
.await?;
let blocked_response = raw_http_request_via_proxy(
blocked_proxy.local_port,
"http://example.com/",
"example.com",
)
.await?;
assert!(
blocked_response.contains("403") || blocked_response.contains("Blocked by DNS blocklist"),
"DNS blocklist should be enforced before forwarding to the VPN upstream"
);
stop_proxy(binary_path, &blocked_proxy.id).await?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let bypass_target_port = listener.local_addr()?.port();
let bypass_server = tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
let io = hyper_util::rt::TokioIo::new(stream);
tokio::spawn(async move {
let service = hyper::service::service_fn(|_req| async move {
Ok::<_, hyper::Error>(
hyper::Response::builder()
.status(hyper::StatusCode::OK)
.body(http_body_util::Full::new(hyper::body::Bytes::from(
"VPN-BYPASS-OK",
)))
.unwrap(),
)
});
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(io, service)
.await;
});
}
});
let bypass_proxy = start_proxy_with_upstream(
binary_path,
&vpn_upstream,
&["127.0.0.1".to_string(), "localhost".to_string()],
None,
None,
)
.await?;
let bypass_response = raw_http_request_via_proxy(
bypass_proxy.local_port,
&format!("http://127.0.0.1:{bypass_target_port}/"),
&format!("127.0.0.1:{bypass_target_port}"),
)
.await?;
assert!(
bypass_response.contains("VPN-BYPASS-OK"),
"Bypass rules should still work when donut-proxy is chained to a VPN worker"
);
stop_proxy(binary_path, &bypass_proxy.id).await?;
bypass_server.abort();
donutbrowser_lib::vpn_worker_runner::stop_vpn_worker(&vpn_worker.id)
.await
.map_err(|error| error.to_string())?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_wireguard_traffic_flows_through_donut_proxy(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let _env = TestEnvGuard::new()?;
cleanup_runtime().await;
if !test_harness::is_docker_available() {
eprintln!("skipping WireGuard e2e test because Docker is unavailable");
return Ok(());
}
let binary_path = ensure_donut_proxy_binary().await?;
let wg_config = match test_harness::start_wireguard_server().await {
Ok(config) => config,
Err(error) => {
eprintln!("skipping WireGuard e2e test: {error}");
return Ok(());
}
};
let vpn_config = new_test_vpn_config(
"WireGuard E2E",
VpnType::WireGuard,
build_wireguard_config(&wg_config),
);
{
let storage = donutbrowser_lib::vpn::VPN_STORAGE.lock().unwrap();
storage.save_config(&vpn_config)?;
}
let result = run_proxy_feature_suite(&binary_path, &vpn_config.id).await;
cleanup_runtime().await;
result
}
#[tokio::test]
#[serial]
async fn test_openvpn_traffic_flows_through_donut_proxy(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let _env = TestEnvGuard::new()?;
cleanup_runtime().await;
if std::env::var("DONUTBROWSER_RUN_OPENVPN_E2E")
.ok()
.as_deref()
!= Some("1")
{
eprintln!("skipping OpenVPN e2e test because DONUTBROWSER_RUN_OPENVPN_E2E is not set");
return Ok(());
}
if !test_harness::is_docker_available() {
eprintln!("skipping OpenVPN e2e test because Docker is unavailable");
return Ok(());
}
if !openvpn_client_available() {
eprintln!("skipping OpenVPN e2e test because the OpenVPN client binary is unavailable");
return Ok(());
}
if !openvpn_adapter_available() {
eprintln!("skipping OpenVPN e2e test because no Windows OpenVPN adapter is available");
return Ok(());
}
let binary_path = ensure_donut_proxy_binary().await?;
let ovpn_config = match test_harness::start_openvpn_server().await {
Ok(config) => config,
Err(error) => {
eprintln!("skipping OpenVPN e2e test: {error}");
return Ok(());
}
};
let vpn_config = new_test_vpn_config("OpenVPN E2E", VpnType::OpenVPN, ovpn_config.raw_config);
{
let storage = donutbrowser_lib::vpn::VPN_STORAGE.lock().unwrap();
storage.save_config(&vpn_config)?;
}
let result = run_proxy_feature_suite(&binary_path, &vpn_config.id).await;
cleanup_runtime().await;
result
}