fluidattacks_core_git/
warp.rs1use 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}