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    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
98) -> Result<CloudflareBypassResult, Box<dyn std::error::Error + Send + Sync>> {
99    let start = std::time::Instant::now();
100
101    let clean_domain = domain
102        .trim_start_matches("https://")
103        .trim_start_matches("http://");
104
105    let client = Client::builder()
106        .timeout(Duration::from_secs(8))
107        .danger_accept_invalid_certs(true)
108        .redirect(reqwest::redirect::Policy::limited(3))
109        .build()?;
110
111    let ip_regex = Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap();
112    let mut found_ips: Vec<FoundIp> = Vec::new();
113
114    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 5.0, message: "Started real IP discovery...".into(), status: "Info".into() }).await; }
115
116    // ── 1. Direct DNS resolution ────────────────────────────────────────
117    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 10.0, message: "Performing direct DNS resolution...".into(), status: "Info".into() }).await; }
118    let dns_ip = tokio::net::lookup_host(format!("{}:80", clean_domain))
119        .await
120        .ok()
121        .and_then(|mut addrs| addrs.next())
122        .map(|a| a.ip().to_string());
123
124    let cloudflare_protected = if let Some(ref ip) = dns_ip {
125        is_cloudflare_ip(ip)
126    } else {
127        false
128    };
129
130    if let Some(ref ip) = dns_ip {
131        if !is_cloudflare_ip(ip) && !is_private_ip(ip) {
132            found_ips.push(FoundIp {
133                ip: ip.clone(),
134                source: "direct_dns".into(),
135                confidence: "Very High".into(),
136                description: None,
137                status: None,
138            });
139        }
140    }
141
142    // Only run bypass techniques if CF-protected
143    if cloudflare_protected {
144        // ── 2. Check common + domain-specific subdomains ────────────────
145        if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 30.0, message: "Checking infrastructure subdomains...".into(), status: "Info".into() }).await; }
146        let mut subdomains: Vec<String> =
147            vec!["direct", "origin", "api", "mail", "cpanel", "server", "ftp"]
148                .into_iter()
149                .map(|s| s.to_string())
150                .collect();
151
152        // Domain-specific subdomains
153        let name_part = clean_domain.split('.').next().unwrap_or("");
154        if !name_part.is_empty() {
155            subdomains.push(format!("origin-{}", name_part));
156            subdomains.push(format!("{}-origin", name_part));
157            subdomains.push(format!("direct-{}", name_part));
158            subdomains.push(format!("{}-direct", name_part));
159        }
160
161        for sub in &subdomains {
162            let full: String = format!("{}.{}:80", sub, clean_domain);
163            if let Ok(addrs) = tokio::net::lookup_host(full.as_str().to_owned()).await {
164                let resolved: Vec<_> = addrs.collect();
165                if let Some(addr) = resolved.first() {
166                    let ip = addr.ip().to_string();
167                    if !is_cloudflare_ip(&ip) && !is_private_ip(&ip) && is_valid_ip(&ip) {
168                        found_ips.push(FoundIp {
169                            ip,
170                            source: format!("subdomain_{}", sub),
171                            confidence: "Medium".into(),
172                            description: None,
173                            status: None,
174                        });
175                    }
176                }
177            }
178        }
179
180        // ── 3. Check response headers for IP leaks ──────────────────────
181        if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 60.0, message: "Analyzing response origin headers...".into(), status: "Info".into() }).await; }
182        if let Ok(resp) = client.get(format!("https://{}", clean_domain)).send().await {
183            for header in HEADERS_TO_CHECK {
184                if let Some(val) = resp.headers().get(*header) {
185                    if let Ok(val_str) = val.to_str() {
186                        for cap in ip_regex.find_iter(val_str) {
187                            let ip = cap.as_str().to_string();
188                            if is_valid_ip(&ip) && !is_cloudflare_ip(&ip) && !is_private_ip(&ip) {
189                                found_ips.push(FoundIp {
190                                    ip,
191                                    source: format!("header_{}", header),
192                                    confidence: "High".into(),
193                                    description: None,
194                                    status: None,
195                                });
196                            }
197                        }
198                    }
199                }
200            }
201        }
202
203        // ── 4. IP History lookup ────────────────────────────────────────
204        if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 75.0, message: "Querying historical DNS databases...".into(), status: "Info".into() }).await; }
205        let history_ips = check_ip_history(&client, clean_domain, &ip_regex, &progress_tx).await;
206        found_ips.extend(history_ips);
207    }
208
209    // ── Deduplicate, keeping highest confidence ─────────────────────────
210    let mut best: HashMap<String, FoundIp> = HashMap::new();
211    for ip_info in found_ips {
212        let key = ip_info.ip.clone();
213        let new_score = confidence_score(&ip_info.confidence);
214        if let Some(existing) = best.get(&key) {
215            if new_score > confidence_score(&existing.confidence) {
216                best.insert(key, ip_info);
217            }
218        } else {
219            best.insert(key, ip_info);
220        }
221    }
222
223    // Sort by confidence (highest first)
224    let mut results: Vec<FoundIp> = best.into_values().collect();
225    results.sort_by(|a, b| confidence_score(&b.confidence).cmp(&confidence_score(&a.confidence)));
226
227    // ── Verify top 5 IPs ────────────────────────────────────────────────
228    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 95.0, message: "Verifying active status of top leaked IPs...".into(), status: "Info".into() }).await; }
229    for i in 0..results.len().min(5) {
230        let status = verify_ip(&results[i].ip).await;
231        results[i].status = Some(status);
232    }
233    for item in results.iter_mut().skip(5) {
234        item.status = Some("unverified".into());
235    }
236
237    Ok(CloudflareBypassResult {
238        domain: clean_domain.to_string(),
239        cloudflare_protected,
240        found_ips: results,
241        scan_time_ms: start.elapsed().as_millis(),
242    })
243}
244
245// ── IP History ──────────────────────────────────────────────────────────────
246
247async fn check_ip_history(
248    client: &Client, 
249    domain: &str, 
250    ip_regex: &Regex,
251    progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
252) -> Vec<FoundIp> {
253    let mut results = Vec::new();
254
255    for (name, url_template) in IP_HISTORY_SOURCES {
256        if let Some(t) = progress_tx { let _ = t.send(crate::ScanProgress { module: "Cloudflare Bypass".into(), percentage: 80.0, message: format!("Querying IP history: {}", name), status: "Info".into() }).await; }
257        let url = url_template.replace("{}", domain);
258        let resp = match client
259            .get(&url)
260            .header(
261                "User-Agent",
262                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
263            )
264            .header(
265                "Accept",
266                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
267            )
268            .header("Referer", "https://www.google.com/")
269            .send()
270            .await
271        {
272            Ok(r) if r.status().is_success() => r,
273            _ => continue,
274        };
275
276        let body = match resp.text().await {
277            Ok(t) => t,
278            Err(_) => continue,
279        };
280
281        let mut seen = HashSet::new();
282        for cap in ip_regex.find_iter(&body) {
283            let ip = cap.as_str().to_string();
284            if is_valid_ip(&ip)
285                && !is_cloudflare_ip(&ip)
286                && !is_private_ip(&ip)
287                && seen.insert(ip.clone())
288            {
289                results.push(FoundIp {
290                    ip,
291                    source: format!("history_{}", name),
292                    confidence: "Medium".into(),
293                    description: None,
294                    status: None,
295                });
296            }
297        }
298    }
299
300    results
301}
302
303// ── IP Verification via TCP connect ─────────────────────────────────────────
304
305async fn verify_ip(ip: &str) -> String {
306    let addr = format!("{}:80", ip);
307    match tokio::time::timeout(
308        Duration::from_secs(3),
309        tokio::net::TcpStream::connect(&addr),
310    )
311    .await
312    {
313        Ok(Ok(_)) => "active".into(),
314        _ => "inactive".into(),
315    }
316}