1use regex::Regex;
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5use std::time::Duration;
6
7const 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
15const 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
27const 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
37const 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#[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
65fn 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
93pub 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 = crate::http_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 {
115 let _ = t
116 .send(crate::ScanProgress {
117 module: "Cloudflare Bypass".into(),
118 percentage: 5.0,
119 message: "Started real IP discovery...".into(),
120 status: "Info".into(),
121 })
122 .await;
123 }
124
125 if let Some(t) = &progress_tx {
127 let _ = t
128 .send(crate::ScanProgress {
129 module: "Cloudflare Bypass".into(),
130 percentage: 10.0,
131 message: "Performing direct DNS resolution...".into(),
132 status: "Info".into(),
133 })
134 .await;
135 }
136 let dns_ip = tokio::net::lookup_host(format!("{}:80", clean_domain))
137 .await
138 .ok()
139 .and_then(|mut addrs| addrs.next())
140 .map(|a| a.ip().to_string());
141
142 let cloudflare_protected = if let Some(ref ip) = dns_ip {
143 is_cloudflare_ip(ip)
144 } else {
145 false
146 };
147
148 if let Some(ref ip) = dns_ip {
149 if !is_cloudflare_ip(ip) && !is_private_ip(ip) {
150 found_ips.push(FoundIp {
151 ip: ip.clone(),
152 source: "direct_dns".into(),
153 confidence: "Very High".into(),
154 description: None,
155 status: None,
156 });
157 }
158 }
159
160 if cloudflare_protected {
162 if let Some(t) = &progress_tx {
164 let _ = t
165 .send(crate::ScanProgress {
166 module: "Cloudflare Bypass".into(),
167 percentage: 30.0,
168 message: "Checking infrastructure subdomains...".into(),
169 status: "Info".into(),
170 })
171 .await;
172 }
173 let mut subdomains: Vec<String> =
174 vec!["direct", "origin", "api", "mail", "cpanel", "server", "ftp"]
175 .into_iter()
176 .map(|s| s.to_string())
177 .collect();
178
179 let name_part = clean_domain.split('.').next().unwrap_or("");
181 if !name_part.is_empty() {
182 subdomains.push(format!("origin-{}", name_part));
183 subdomains.push(format!("{}-origin", name_part));
184 subdomains.push(format!("direct-{}", name_part));
185 subdomains.push(format!("{}-direct", name_part));
186 }
187
188 for sub in &subdomains {
189 let full: String = format!("{}.{}:80", sub, clean_domain);
190 if let Ok(addrs) = tokio::net::lookup_host(full.as_str().to_owned()).await {
191 let resolved: Vec<_> = addrs.collect();
192 if let Some(addr) = resolved.first() {
193 let ip = addr.ip().to_string();
194 if !is_cloudflare_ip(&ip) && !is_private_ip(&ip) && is_valid_ip(&ip) {
195 found_ips.push(FoundIp {
196 ip,
197 source: format!("subdomain_{}", sub),
198 confidence: "Medium".into(),
199 description: None,
200 status: None,
201 });
202 }
203 }
204 }
205 }
206
207 if let Some(t) = &progress_tx {
209 let _ = t
210 .send(crate::ScanProgress {
211 module: "Cloudflare Bypass".into(),
212 percentage: 60.0,
213 message: "Analyzing response origin headers...".into(),
214 status: "Info".into(),
215 })
216 .await;
217 }
218 if let Ok(resp) = client.get(format!("https://{}", clean_domain)).send().await {
219 for header in HEADERS_TO_CHECK {
220 if let Some(val) = resp.headers().get(*header) {
221 if let Ok(val_str) = val.to_str() {
222 for cap in ip_regex.find_iter(val_str) {
223 let ip = cap.as_str().to_string();
224 if is_valid_ip(&ip) && !is_cloudflare_ip(&ip) && !is_private_ip(&ip) {
225 found_ips.push(FoundIp {
226 ip,
227 source: format!("header_{}", header),
228 confidence: "High".into(),
229 description: None,
230 status: None,
231 });
232 }
233 }
234 }
235 }
236 }
237 }
238
239 if let Some(t) = &progress_tx {
241 let _ = t
242 .send(crate::ScanProgress {
243 module: "Cloudflare Bypass".into(),
244 percentage: 75.0,
245 message: "Querying historical DNS databases...".into(),
246 status: "Info".into(),
247 })
248 .await;
249 }
250 let history_ips = check_ip_history(&client, clean_domain, &ip_regex, &progress_tx).await;
251 found_ips.extend(history_ips);
252 }
253
254 let mut best: HashMap<String, FoundIp> = HashMap::new();
256 for ip_info in found_ips {
257 let key = ip_info.ip.clone();
258 let new_score = confidence_score(&ip_info.confidence);
259 if let Some(existing) = best.get(&key) {
260 if new_score > confidence_score(&existing.confidence) {
261 best.insert(key, ip_info);
262 }
263 } else {
264 best.insert(key, ip_info);
265 }
266 }
267
268 let mut results: Vec<FoundIp> = best.into_values().collect();
270 results.sort_by_key(|result| std::cmp::Reverse(confidence_score(&result.confidence)));
271
272 if let Some(t) = &progress_tx {
274 let _ = t
275 .send(crate::ScanProgress {
276 module: "Cloudflare Bypass".into(),
277 percentage: 95.0,
278 message: "Verifying active status of top leaked IPs...".into(),
279 status: "Info".into(),
280 })
281 .await;
282 }
283 for i in 0..results.len().min(5) {
284 let status = verify_ip(&results[i].ip).await;
285 results[i].status = Some(status);
286 }
287 for item in results.iter_mut().skip(5) {
288 item.status = Some("unverified".into());
289 }
290
291 Ok(CloudflareBypassResult {
292 domain: clean_domain.to_string(),
293 cloudflare_protected,
294 found_ips: results,
295 scan_time_ms: start.elapsed().as_millis(),
296 })
297}
298
299async fn check_ip_history(
302 client: &Client,
303 domain: &str,
304 ip_regex: &Regex,
305 progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
306) -> Vec<FoundIp> {
307 let mut results = Vec::new();
308
309 for (name, url_template) in IP_HISTORY_SOURCES {
310 if let Some(t) = progress_tx {
311 let _ = t
312 .send(crate::ScanProgress {
313 module: "Cloudflare Bypass".into(),
314 percentage: 80.0,
315 message: format!("Querying IP history: {}", name),
316 status: "Info".into(),
317 })
318 .await;
319 }
320 let url = url_template.replace("{}", domain);
321 let resp = match client
322 .get(&url)
323 .header(
324 "User-Agent",
325 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
326 )
327 .header(
328 "Accept",
329 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
330 )
331 .header("Referer", "https://www.google.com/")
332 .send()
333 .await
334 {
335 Ok(r) if r.status().is_success() => r,
336 _ => continue,
337 };
338
339 let body = match resp.text().await {
340 Ok(t) => t,
341 Err(_) => continue,
342 };
343
344 let mut seen = HashSet::new();
345 for cap in ip_regex.find_iter(&body) {
346 let ip = cap.as_str().to_string();
347 if is_valid_ip(&ip)
348 && !is_cloudflare_ip(&ip)
349 && !is_private_ip(&ip)
350 && seen.insert(ip.clone())
351 {
352 results.push(FoundIp {
353 ip,
354 source: format!("history_{}", name),
355 confidence: "Medium".into(),
356 description: None,
357 status: None,
358 });
359 }
360 }
361 }
362
363 results
364}
365
366async fn verify_ip(ip: &str) -> String {
369 let addr = format!("{}:80", ip);
370 match tokio::time::timeout(
371 Duration::from_secs(3),
372 tokio::net::TcpStream::connect(&addr),
373 )
374 .await
375 {
376 Ok(Ok(_)) => "active".into(),
377 _ => "inactive".into(),
378 }
379}