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 = 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 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 if cloudflare_protected {
144 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 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 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 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 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 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 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
245async 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
303async 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}