use anyhow::{bail, Context, Result};
use std::time::Duration;
use tokio::process::Command;
use tokio::time::sleep;
pub async fn connect() -> Result<()> {
run_warp_cli(&["connect"]).await?;
sleep(Duration::from_secs(5)).await;
Ok(())
}
pub async fn disconnect() -> Result<()> {
run_warp_cli(&["disconnect"]).await?;
sleep(Duration::from_secs(5)).await;
Ok(())
}
pub async fn status() -> Result<String> {
let output = Command::new("warp-cli")
.args(["--accept-tos", "status"])
.output()
.await
.context("running warp-cli status")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("warp-cli status failed: {stderr}");
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub async fn set_virtual_network(vnet_name: &str) -> Result<()> {
let vnet_id = get_virtual_network_id(vnet_name).await?;
run_warp_cli(&["vnet", &vnet_id]).await?;
sleep(Duration::from_secs(5)).await;
Ok(())
}
pub async fn test_public_ip(expected_ip: &str) -> Result<bool> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let resp = client
.get("https://api.ipify.org")
.send()
.await
.context("fetching public IP")?;
let ip = resp.text().await.context("reading IP response")?;
Ok(ip.trim() == expected_ip)
}
pub async fn wait_public_ip(expected_ip: &str, attempts: u32, interval_secs: u64) -> Result<()> {
for i in 0..attempts {
if test_public_ip(expected_ip).await.unwrap_or(false) {
return Ok(());
}
if i + 1 < attempts {
sleep(Duration::from_secs(interval_secs)).await;
}
}
bail!("public IP did not match {expected_ip} after {attempts} attempts");
}
pub async fn wait_dns_ready(host: &str, attempts: u32, interval_secs: u64) -> Result<()> {
for i in 0..attempts {
let output = Command::new("nslookup")
.arg(host)
.output()
.await
.context("running nslookup")?;
if output.status.success() {
return Ok(());
}
if i + 1 < attempts {
sleep(Duration::from_secs(interval_secs)).await;
}
}
bail!("DNS resolution for {host} failed after {attempts} attempts");
}
pub async fn is_using_split_tunnel(host: &str) -> Result<bool> {
let output = Command::new("ip")
.args(["route", "get", host])
.output()
.await
.context("running ip route get")?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.contains("CloudflareWARP"))
}
pub async fn get_virtual_network_id(vnet_name: &str) -> Result<String> {
let output = tokio::time::timeout(
Duration::from_secs(30),
Command::new("warp-cli")
.args(["--accept-tos", "vnet"])
.output(),
)
.await
.context("warp-cli vnet timed out")?
.context("running warp-cli vnet")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let pattern = format!("ID: (.*)\n Name: {}\n", regex::escape(vnet_name));
let re = regex::Regex::new(&pattern).context("building vnet regex")?;
re.captures(&stdout)
.and_then(|c| c.get(1))
.map(|m| m.as_str().trim().to_string())
.ok_or_else(|| anyhow::anyhow!("virtual network '{vnet_name}' not found"))
}
async fn run_warp_cli(args: &[&str]) -> Result<()> {
let mut full_args = vec!["--accept-tos"];
full_args.extend(args);
let output = tokio::time::timeout(
Duration::from_secs(30),
Command::new("warp-cli").args(&full_args).output(),
)
.await
.context("warp-cli timed out")?
.context("running warp-cli")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("warp-cli {} failed: {}", args.join(" "), stderr);
}
Ok(())
}