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) -> 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 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 if cloudflare_protected {
140 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 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 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 let history_ips = check_ip_history(&client, clean_domain, &ip_regex).await;
199 found_ips.extend(history_ips);
200 }
201
202 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 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 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
237async 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
289async 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}