Skip to main content

fluidattacks_core_git/
warp.rs

1use anyhow::{bail, Context, Result};
2use std::time::Duration;
3use tokio::process::Command;
4use tokio::time::sleep;
5
6pub async fn connect() -> Result<()> {
7    run_warp_cli(&["connect"]).await?;
8    sleep(Duration::from_secs(5)).await;
9    Ok(())
10}
11
12pub async fn disconnect() -> Result<()> {
13    run_warp_cli(&["disconnect"]).await?;
14    sleep(Duration::from_secs(5)).await;
15    Ok(())
16}
17
18pub async fn status() -> Result<String> {
19    let output = Command::new("warp-cli")
20        .args(["--accept-tos", "status"])
21        .output()
22        .await
23        .context("running warp-cli status")?;
24
25    if !output.status.success() {
26        let stderr = String::from_utf8_lossy(&output.stderr);
27        bail!("warp-cli status failed: {stderr}");
28    }
29
30    Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
31}
32
33pub async fn set_virtual_network(vnet_name: &str) -> Result<()> {
34    let vnet_id = get_virtual_network_id(vnet_name).await?;
35    run_warp_cli(&["vnet", &vnet_id]).await?;
36    sleep(Duration::from_secs(5)).await;
37    Ok(())
38}
39
40pub async fn test_public_ip(expected_ip: &str) -> Result<bool> {
41    let client = reqwest::Client::builder()
42        .timeout(Duration::from_secs(10))
43        .build()?;
44
45    let resp = client
46        .get("https://api.ipify.org")
47        .send()
48        .await
49        .context("fetching public IP")?;
50
51    let ip = resp.text().await.context("reading IP response")?;
52    Ok(ip.trim() == expected_ip)
53}
54
55pub async fn wait_public_ip(expected_ip: &str, attempts: u32, interval_secs: u64) -> Result<()> {
56    for i in 0..attempts {
57        if test_public_ip(expected_ip).await.unwrap_or(false) {
58            return Ok(());
59        }
60        if i.saturating_add(1) < attempts {
61            sleep(Duration::from_secs(interval_secs)).await;
62        }
63    }
64    bail!("public IP did not match {expected_ip} after {attempts} attempts");
65}
66
67pub async fn wait_dns_ready(host: &str, attempts: u32, interval_secs: u64) -> Result<()> {
68    for i in 0..attempts {
69        let output = Command::new("nslookup")
70            .arg(host)
71            .output()
72            .await
73            .context("running nslookup")?;
74
75        if output.status.success() {
76            return Ok(());
77        }
78        if i.saturating_add(1) < attempts {
79            sleep(Duration::from_secs(interval_secs)).await;
80        }
81    }
82    bail!("DNS resolution for {host} failed after {attempts} attempts");
83}
84
85pub async fn is_using_split_tunnel(host: &str) -> Result<bool> {
86    let output = Command::new("ip")
87        .args(["route", "get", host])
88        .output()
89        .await
90        .context("running ip route get")?;
91
92    let stdout = String::from_utf8_lossy(&output.stdout);
93    Ok(stdout.contains("CloudflareWARP"))
94}
95
96pub async fn get_virtual_network_id(vnet_name: &str) -> Result<String> {
97    let output = tokio::time::timeout(
98        Duration::from_secs(30),
99        Command::new("warp-cli")
100            .args(["--accept-tos", "vnet"])
101            .kill_on_drop(true)
102            .output(),
103    )
104    .await
105    .context("warp-cli vnet timed out")?
106    .context("running warp-cli vnet")?;
107
108    let stdout = String::from_utf8_lossy(&output.stdout);
109    let pattern = format!("ID: (.*)\n  Name: {}\n", regex::escape(vnet_name));
110    let re = regex::Regex::new(&pattern).context("building vnet regex")?;
111
112    re.captures(&stdout)
113        .and_then(|c| c.get(1))
114        .map(|m| m.as_str().trim().to_owned())
115        .ok_or_else(|| anyhow::anyhow!("virtual network '{vnet_name}' not found"))
116}
117
118async fn run_warp_cli(args: &[&str]) -> Result<()> {
119    let mut full_args = vec!["--accept-tos"];
120    full_args.extend(args);
121
122    let output = tokio::time::timeout(
123        Duration::from_secs(30),
124        Command::new("warp-cli")
125            .args(&full_args)
126            .kill_on_drop(true)
127            .output(),
128    )
129    .await
130    .context("warp-cli timed out")?
131    .context("running warp-cli")?;
132
133    if !output.status.success() {
134        let stderr = String::from_utf8_lossy(&output.stderr);
135        bail!("warp-cli {} failed: {}", args.join(" "), stderr);
136    }
137
138    Ok(())
139}