Skip to main content

web_analyzer/
cloudflare_bypass.rs

1use regex::Regex;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5use std::time::Duration;
6
7/// Known Cloudflare IPv4 CIDR ranges (simplified to prefix checks)
8const CF_PREFIXES: &[&str] = &[
9    "173.245.", "103.21.", "103.22.", "103.31.", "141.101.", "108.162.", "190.93.", "188.114.",
10    "197.234.", "198.41.", "162.158.", "162.159.", "104.16.", "104.17.", "104.18.", "104.19.",
11    "104.20.", "104.21.", "104.22.", "104.23.", "104.24.", "104.25.", "104.26.", "104.27.",
12    "172.64.", "172.65.", "172.66.", "172.67.", "131.0.",
13];
14
15/// Headers that may leak origin IPs
16const HEADERS_TO_CHECK: &[&str] = &[
17    "x-forwarded-for",
18    "x-real-ip",
19    "x-origin-ip",
20    "cf-connecting-ip",
21    "x-server-ip",
22    "server-ip",
23    "x-backend-server",
24    "x-origin-server",
25];
26
27/// IP history lookup sources
28const IP_HISTORY_SOURCES: &[(&str, &str)] = &[
29    ("ViewDNS", "https://viewdns.info/iphistory/?domain={}"),
30    (
31        "SecurityTrails",
32        "https://securitytrails.com/domain/{}/history/a",
33    ),
34    ("WhoIs", "https://who.is/whois/{}"),
35];
36
37/// Private IP prefixes (RFC 1918 + loopback + link-local)
38const PRIVATE_PREFIXES: &[&str] = &[
39    "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.", "172.23.",
40    "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.",
41    "192.168.", "127.", "0.", "169.254.",
42];
43
44// ── Structs ─────────────────────────────────────────────────────────────────
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct FoundIp {
48    pub ip: String,
49    pub source: String,
50    pub confidence: String,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub description: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub status: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CloudflareBypassResult {
59    pub domain: String,
60    pub cloudflare_protected: bool,
61    pub found_ips: Vec<FoundIp>,
62    pub scan_time_ms: u128,
63}
64
65// ── IP classification helpers ───────────────────────────────────────────────
66
67fn is_cloudflare_ip(ip: &str) -> bool {
68    CF_PREFIXES.iter().any(|prefix| ip.starts_with(prefix))
69}
70
71fn is_private_ip(ip: &str) -> bool {
72    PRIVATE_PREFIXES.iter().any(|prefix| ip.starts_with(prefix))
73}
74
75fn is_valid_ip(ip: &str) -> bool {
76    let parts: Vec<&str> = ip.split('.').collect();
77    if parts.len() != 4 {
78        return false;
79    }
80    parts.iter().all(|p| p.parse::<u8>().is_ok())
81}
82
83fn confidence_score(c: &str) -> u8 {
84    match c {
85        "Very High" => 4,
86        "High" => 3,
87        "Medium" => 2,
88        "Low" => 1,
89        _ => 0,
90    }
91}
92
93// ── Main scanner ────────────────────────────────────────────────────────────
94
95pub async fn find_real_ip(
96    domain: &str,
97) -> Result<CloudflareBypassResult, Box<dyn std::error::Error + Send + Sync>> {
98    let start = std::time::Instant::now();
99
100    let clean_domain = domain
101        .trim_start_matches("https://")
102        .trim_start_matches("http://");
103
104    let client = Client::builder()
105        .timeout(Duration::from_secs(8))
106        .danger_accept_invalid_certs(true)
107        .redirect(reqwest::redirect::Policy::limited(3))
108        .build()?;
109
110    let ip_regex = Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap();
111    let mut found_ips: Vec<FoundIp> = Vec::new();
112
113    // ── 1. Direct DNS resolution ────────────────────────────────────────
114    let dns_ip = tokio::net::lookup_host(format!("{}:80", clean_domain))
115        .await
116        .ok()
117        .and_then(|mut addrs| addrs.next())
118        .map(|a| a.ip().to_string());
119
120    let cloudflare_protected = if let Some(ref ip) = dns_ip {
121        is_cloudflare_ip(ip)
122    } else {
123        false
124    };
125
126    if let Some(ref ip) = dns_ip {
127        if !is_cloudflare_ip(ip) && !is_private_ip(ip) {
128            found_ips.push(FoundIp {
129                ip: ip.clone(),
130                source: "direct_dns".into(),
131                confidence: "Very High".into(),
132                description: None,
133                status: None,
134            });
135        }
136    }
137
138    // Only run bypass techniques if CF-protected
139    if cloudflare_protected {
140        // ── 2. Check common + domain-specific subdomains ────────────────
141        let mut subdomains: Vec<String> =
142            vec!["direct", "origin", "api", "mail", "cpanel", "server", "ftp"]
143                .into_iter()
144                .map(|s| s.to_string())
145                .collect();
146
147        // Domain-specific subdomains
148        let name_part = clean_domain.split('.').next().unwrap_or("");
149        if !name_part.is_empty() {
150            subdomains.push(format!("origin-{}", name_part));
151            subdomains.push(format!("{}-origin", name_part));
152            subdomains.push(format!("direct-{}", name_part));
153            subdomains.push(format!("{}-direct", name_part));
154        }
155
156        for sub in &subdomains {
157            let full: String = format!("{}.{}:80", sub, clean_domain);
158            if let Ok(addrs) = tokio::net::lookup_host(full.as_str().to_owned()).await {
159                let resolved: Vec<_> = addrs.collect();
160                if let Some(addr) = resolved.first() {
161                    let ip = addr.ip().to_string();
162                    if !is_cloudflare_ip(&ip) && !is_private_ip(&ip) && is_valid_ip(&ip) {
163                        found_ips.push(FoundIp {
164                            ip,
165                            source: format!("subdomain_{}", sub),
166                            confidence: "Medium".into(),
167                            description: None,
168                            status: None,
169                        });
170                    }
171                }
172            }
173        }
174
175        // ── 3. Check response headers for IP leaks ──────────────────────
176        if let Ok(resp) = client.get(format!("https://{}", clean_domain)).send().await {
177            for header in HEADERS_TO_CHECK {
178                if let Some(val) = resp.headers().get(*header) {
179                    if let Ok(val_str) = val.to_str() {
180                        for cap in ip_regex.find_iter(val_str) {
181                            let ip = cap.as_str().to_string();
182                            if is_valid_ip(&ip) && !is_cloudflare_ip(&ip) && !is_private_ip(&ip) {
183                                found_ips.push(FoundIp {
184                                    ip,
185                                    source: format!("header_{}", header),
186                                    confidence: "High".into(),
187                                    description: None,
188                                    status: None,
189                                });
190                            }
191                        }
192                    }
193                }
194            }
195        }
196
197        // ── 4. IP History lookup ────────────────────────────────────────
198        let history_ips = check_ip_history(&client, clean_domain, &ip_regex).await;
199        found_ips.extend(history_ips);
200    }
201
202    // ── Deduplicate, keeping highest confidence ─────────────────────────
203    let mut best: HashMap<String, FoundIp> = HashMap::new();
204    for ip_info in found_ips {
205        let key = ip_info.ip.clone();
206        let new_score = confidence_score(&ip_info.confidence);
207        if let Some(existing) = best.get(&key) {
208            if new_score > confidence_score(&existing.confidence) {
209                best.insert(key, ip_info);
210            }
211        } else {
212            best.insert(key, ip_info);
213        }
214    }
215
216    // Sort by confidence (highest first)
217    let mut results: Vec<FoundIp> = best.into_values().collect();
218    results.sort_by(|a, b| confidence_score(&b.confidence).cmp(&confidence_score(&a.confidence)));
219
220    // ── Verify top 5 IPs ────────────────────────────────────────────────
221    for i in 0..results.len().min(5) {
222        let status = verify_ip(&results[i].ip).await;
223        results[i].status = Some(status);
224    }
225    for item in results.iter_mut().skip(5) {
226        item.status = Some("unverified".into());
227    }
228
229    Ok(CloudflareBypassResult {
230        domain: clean_domain.to_string(),
231        cloudflare_protected,
232        found_ips: results,
233        scan_time_ms: start.elapsed().as_millis(),
234    })
235}
236
237// ── IP History ──────────────────────────────────────────────────────────────
238
239async fn check_ip_history(client: &Client, domain: &str, ip_regex: &Regex) -> Vec<FoundIp> {
240    let mut results = Vec::new();
241
242    for (name, url_template) in IP_HISTORY_SOURCES {
243        let url = url_template.replace("{}", domain);
244        let resp = match client
245            .get(&url)
246            .header(
247                "User-Agent",
248                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
249            )
250            .header(
251                "Accept",
252                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
253            )
254            .header("Referer", "https://www.google.com/")
255            .send()
256            .await
257        {
258            Ok(r) if r.status().is_success() => r,
259            _ => continue,
260        };
261
262        let body = match resp.text().await {
263            Ok(t) => t,
264            Err(_) => continue,
265        };
266
267        let mut seen = HashSet::new();
268        for cap in ip_regex.find_iter(&body) {
269            let ip = cap.as_str().to_string();
270            if is_valid_ip(&ip)
271                && !is_cloudflare_ip(&ip)
272                && !is_private_ip(&ip)
273                && seen.insert(ip.clone())
274            {
275                results.push(FoundIp {
276                    ip,
277                    source: format!("history_{}", name),
278                    confidence: "Medium".into(),
279                    description: None,
280                    status: None,
281                });
282            }
283        }
284    }
285
286    results
287}
288
289// ── IP Verification via TCP connect ─────────────────────────────────────────
290
291async fn verify_ip(ip: &str) -> String {
292    let addr = format!("{}:80", ip);
293    match tokio::time::timeout(
294        Duration::from_secs(3),
295        tokio::net::TcpStream::connect(&addr),
296    )
297    .await
298    {
299        Ok(Ok(_)) => "active".into(),
300        _ => "inactive".into(),
301    }
302}