diff --git a/README.md b/README.md index 8036f892..2cfc462b 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,32 @@ Then: `script_id` can also be a JSON array: `["id1", "id2", "id3"]`. +#### scan-ips configuration (optional) + +By default, the scan-ips subcommand uses a static array of IPs. + +You can enable dynamic IP discovery by setting fetch_ips_from_api to true in config.json: + +```json +{ + "fetch_ips_from_api": true, + "max_ips_to_scan": 100, + "scan_batch_size":100, + "google_ip_validation": true // check whether ips belongs to frontend sites of google or not +} +``` + +When enabled: + +- Fetches goog.json from Google’s public IP ranges API +- Extracts all CIDRs and expands them to individual IPs +- Prioritizes IPs from famous Google domains (google.com, youtube.com, etc.) +- Randomly selects up to max_ips_to_scan candidates (prioritized IPs first) +- Tests only the selected candidates for connectivity and frontend validation. + +By using this options you may find ips witch are faster than static array that is provided as default but there is no guarantee that this ips would work. + + ### Step 5 — Point your client at the proxy The tool listens on **two** ports. Use whichever your client supports: @@ -423,6 +449,32 @@ Original project: by [@mast **فایرفاکس (ساده‌ترین):** + +#### پیکربندی scan-ips (اختیاری) +به‌طور پیش‌فرض، دستور scan-ips از آرایه‌ای ثابت از IPها استفاده می‌کند. + +می‌توانید کشف پویای IP را با تنظیم fetch_ips_from_api روی true در config.json فعال کنید: + +```json +{ + "fetch_ips_from_api": true, + "max_ips_to_scan": 100, + "scan_batch_size":100, + "google_ip_validation": true // برسی هدر های بازگشته از ایپی برای برسی هدر ها و تشخیص کاربردی بودن ایپی +} +``` + +زمانی که فعال باشد: + +- فایل goog.json را از API محدوده‌های عمومی IP گوگل دریافت می‌کند +تمام CIDRها را استخراج کرده و به IPهای جداگانه تبدیل می‌کند +- به IPهای دامنه‌های معروف گوگل (google.com، youtube.com و غیره) اولویت می‌دهد +به‌صورت تصادفی تا max_ips_to_scan کاندید انتخاب می‌کند (ابتدا IPهای اولویت‌دار) +فقط کاندیدهای انتخاب‌شده را برای اتصال و اعتبارسنجی frontend تست می‌کند. + +با استفاده از این گزینه‌ها ممکن است IPهایی پیدا کنید که سریع‌تر از آرایه ثابت پیش‌فرض هستند اما تضمینی وجود ندارد که این IPها کار کنند. + +#### ۵. تنظیم proxy در کلاینت ۱. منوی `Settings` را باز کنید، در خانهٔ جست‌وجو عبارت `proxy` را تایپ کنید ۲. روی **`Network Settings`** کلیک کنید ۳. گزینهٔ **`Manual proxy configuration`** را انتخاب کنید diff --git a/src/bin/ui.rs b/src/bin/ui.rs index 1326decf..8e131849 100644 --- a/src/bin/ui.rs +++ b/src/bin/ui.rs @@ -137,6 +137,10 @@ struct FormState { sni_custom_input: String, /// Whether the floating SNI editor window is open. sni_editor_open: bool, + fetch_ips_from_api: bool, + max_ips_to_scan: usize, + scan_batch_size:usize, + google_ip_validation: bool } #[derive(Clone, Debug)] @@ -182,6 +186,10 @@ fn load_form() -> FormState { sni_pool, sni_custom_input: String::new(), sni_editor_open: false, + fetch_ips_from_api:c.fetch_ips_from_api, + max_ips_to_scan:c.max_ips_to_scan, + google_ip_validation: c.google_ip_validation, + scan_batch_size:c.scan_batch_size } } else { FormState { @@ -200,6 +208,10 @@ fn load_form() -> FormState { sni_pool: sni_pool_for_form(None, "www.google.com"), sni_custom_input: String::new(), sni_editor_open: false, + fetch_ips_from_api:false, + max_ips_to_scan:100, + google_ip_validation:true, + scan_batch_size:500 } } } @@ -321,6 +333,10 @@ impl FormState { Some(active) } }, + fetch_ips_from_api:self.fetch_ips_from_api, + max_ips_to_scan: self.max_ips_to_scan, + google_ip_validation:self.google_ip_validation, + scan_batch_size:self.scan_batch_size }) } } diff --git a/src/config.rs b/src/config.rs index bf9e62df..bf8ed4ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -80,8 +80,24 @@ pub struct Config { /// various times). Can be tested per-name via the UI or `mhrv-rs test-sni`. #[serde(default)] pub sni_hosts: Option>, + #[serde(default = "default_fetch_ips_from_api")] + pub fetch_ips_from_api: bool, + + #[serde(default = "default_max_ips_to_scan")] + pub max_ips_to_scan: usize, + + #[serde(default = "default_scan_batch_size")] + pub scan_batch_size:usize, + + #[serde(default = "default_google_ip_validation")] + pub google_ip_validation: bool } +fn default_fetch_ips_from_api() -> bool { false } +fn default_max_ips_to_scan() -> usize { 100 } +fn default_scan_batch_size() -> usize {500} +fn default_google_ip_validation() -> bool {true} + fn default_google_ip() -> String { "216.239.38.120".into() } diff --git a/src/scan_ips.rs b/src/scan_ips.rs index 94daf83b..924817c0 100644 --- a/src/scan_ips.rs +++ b/src/scan_ips.rs @@ -2,6 +2,7 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, Instant}; +use rand::seq::SliceRandom; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio_rustls::rustls::client::danger::{ @@ -44,6 +45,17 @@ const CANDIDATE_IPS: &[&str] = &[ "142.250.72.110", ]; +const FAMOUS_GOOGLE_DOMAINS: &[&str] = &[ + "google.com", + "www.google.com", + "youtube.com", + "www.youtube.com", + "gmail.com", + "drive.google.com", + "docs.google.com", + "maps.google.com", +]; + const PROBE_TIMEOUT: Duration = Duration::from_secs(4); const CONCURRENCY: usize = 8; @@ -54,8 +66,15 @@ struct Result_ { } pub async fn run(config: &Config) -> bool { + let ips = fetch_google_ips(config).await; + let google_ip_validation = config.google_ip_validation; let sni = config.front_domain.clone(); - println!("Scanning {} Google frontend IPs (SNI={}, timeout={}s)...", CANDIDATE_IPS.len(), sni, PROBE_TIMEOUT.as_secs()); + println!( + "Scanning {} Google frontend IPs (SNI={}, timeout={}s)...", + ips.len(), + sni, + PROBE_TIMEOUT.as_secs() + ); println!(); let tls_cfg = ClientConfig::builder() @@ -65,15 +84,15 @@ pub async fn run(config: &Config) -> bool { let connector = TlsConnector::from(Arc::new(tls_cfg)); let sem = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY)); - let mut tasks = Vec::with_capacity(CANDIDATE_IPS.len()); - for ip in CANDIDATE_IPS { + let mut tasks = Vec::with_capacity(ips.len()); + for ip in &ips { let sni = sni.clone(); let connector = connector.clone(); let sem = sem.clone(); let ip = ip.to_string(); tasks.push(tokio::spawn(async move { - let _permit = sem.acquire().await.ok(); - probe(&ip, &sni, connector).await + let _permit: Option> = sem.acquire().await.ok(); + probe(&ip, &sni, connector,google_ip_validation).await })); } @@ -115,7 +134,388 @@ pub async fn run(config: &Config) -> bool { } } -async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { +async fn fetch_google_ips(config: &Config) -> Vec { + if !config.fetch_ips_from_api { + tracing::info!("fetch_ips_from_api disabled, using static fallback"); + return CANDIDATE_IPS.iter().map(|s| s.to_string()).collect(); + } + + match fetch_and_validate_google_ips( + &config.front_domain, + config.max_ips_to_scan, + config.scan_batch_size, + config.google_ip_validation + ) + .await + { + Ok(ips) if !ips.is_empty() => { + tracing::info!("✓ Validated {} working IPs from goog.json", ips.len()); + ips + } + Ok(_) => { + tracing::warn!("No working IPs found in goog.json, using static fallback"); + CANDIDATE_IPS.iter().map(|s| s.to_string()).collect() + } + Err(e) => { + tracing::warn!( + "Failed to fetch/validate Google IPs: {}, using static fallback", + e + ); + CANDIDATE_IPS.iter().map(|s| s.to_string()).collect() + } + } +} + +async fn fetch_and_validate_google_ips( + sni: &str, + max_ips: usize, + batch_size: usize, + google_ip_validation: bool +) -> Result, Box> { + let famous_ips = resolve_famous_domains().await; + tracing::info!( + "Resolved {} IPs from famous Google domains", + famous_ips.len() + ); + + let cidrs = fetch_google_cidrs().await?; + tracing::info!("Fetched {} CIDR blocks from goog.json", cidrs.len()); + + let priority_cidrs = find_matching_cidrs(&famous_ips, &cidrs); + tracing::info!("Found {} CIDRs containing famous IPs", priority_cidrs.len()); + + let mut priority_ips = Vec::new(); + for cidr in &priority_cidrs { + priority_ips.extend(cidr_to_ips(cidr)); + } + + let mut other_ips = Vec::new(); + for cidr in &cidrs { + if !priority_cidrs.contains(cidr) { + other_ips.extend(cidr_to_ips(cidr)); + } + } + let other_ips_len = &other_ips.len(); + + tracing::info!( + "Extracted {} priority IPs and {} other IPs", + priority_ips.len(), + other_ips.len() + ); + + let mut rng = rand::thread_rng(); + let priority_ips_len = &priority_ips.len(); + + priority_ips.shuffle(&mut rng); + + other_ips.shuffle(&mut rng); + + let mut candidate_ips = Vec::new(); + candidate_ips.extend(priority_ips.into_iter().take(max_ips)); + if candidate_ips.len() < max_ips { + let remaining = max_ips - candidate_ips.len(); + candidate_ips.extend(other_ips.into_iter().take(remaining)); + } + + if candidate_ips.is_empty() { + return Err("No valid IPs extracted from CIDRs".into()); + } + + tracing::info!( + "Selected {} IPs to test (from {} total), testing in batches...", + candidate_ips.len(), + priority_ips_len + other_ips_len + ); + + let mut working_ips = Vec::new(); + + for (i, chunk) in candidate_ips.chunks(batch_size).enumerate() { + tracing::debug!("Testing batch {} ({} IPs)...", i + 1, chunk.len()); + let batch_working = validate_ips(chunk, sni,google_ip_validation).await; + working_ips.extend(batch_working); + } + + tracing::info!( + "Found {} working IPs from {} tested", + working_ips.len(), + candidate_ips.len() + ); + + Ok(working_ips) +} + +async fn resolve_famous_domains() -> Vec { + let mut ips = Vec::new(); + for domain in FAMOUS_GOOGLE_DOMAINS { + match tokio::net::lookup_host(format!("{}:443", domain)).await { + Ok(addrs) => { + for addr in addrs { + if let SocketAddr::V4(v4) = addr { + ips.push(v4.ip().to_string()); + } + } + } + Err(e) => { + tracing::debug!("Failed to resolve {}: {}", domain, e); + } + } + } + ips.sort(); + ips.dedup(); + ips +} + +fn find_matching_cidrs(ips: &[String], cidrs: &[String]) -> Vec { + let mut matches = Vec::new(); + for cidr in cidrs { + for ip in ips { + if ip_in_cidr(ip, cidr) { + matches.push(cidr.clone()); + break; + } + } + } + matches +} + +fn ip_in_cidr(ip: &str, cidr: &str) -> bool { + let parts: Vec<&str> = cidr.split('/').collect(); + if parts.len() != 2 { + return false; + } + + let base_ip = parts[0]; + let prefix_len: u8 = match parts[1].parse() { + Ok(p) => p, + Err(_) => return false, + }; + + let ip_num = match ip_to_u32(ip) { + Some(n) => n, + None => return false, + }; + + let base_num = match ip_to_u32(base_ip) { + Some(n) => n, + None => return false, + }; + + let mask = !((1u32 << (32 - prefix_len)) - 1); + (ip_num & mask) == (base_num & mask) +} + +fn ip_to_u32(ip: &str) -> Option { + let octets: Vec<&str> = ip.split('.').collect(); + if octets.len() != 4 { + return None; + } + + let o: Vec = octets.iter().filter_map(|s| s.parse().ok()).collect(); + if o.len() != 4 { + return None; + } + + Some(((o[0] as u32) << 24) | ((o[1] as u32) << 16) | ((o[2] as u32) << 8) | (o[3] as u32)) +} + +async fn fetch_google_cidrs() -> Result, Box> { + let stream = tokio::time::timeout( + Duration::from_secs(10), + TcpStream::connect("www.gstatic.com:443"), + ) + .await??; + + let tls_cfg = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerify)) + .with_no_client_auth(); + let connector = TlsConnector::from(Arc::new(tls_cfg)); + let server_name = ServerName::try_from("www.gstatic.com".to_string())?; + + let mut tls_stream = tokio::time::timeout( + Duration::from_secs(10), + connector.connect(server_name, stream), + ) + .await??; + + let request = "GET /ipranges/goog.json HTTP/1.1\r\n\ + Host: www.gstatic.com\r\n\ + Connection: close\r\n\ + \r\n"; + tls_stream.write_all(request.as_bytes()).await?; + tls_stream.flush().await?; + + let mut response = Vec::new(); + tokio::time::timeout(Duration::from_secs(15), async { + let mut buf = [0u8; 4096]; + loop { + match tls_stream.read(&mut buf).await { + Ok(0) => break, + Ok(n) => response.extend_from_slice(&buf[..n]), + Err(_) => break, + } + } + }) + .await?; + + let response_str = String::from_utf8_lossy(&response); + let body = response_str + .split("\r\n\r\n") + .nth(1) + .ok_or("No HTTP body found")?; + + let json: serde_json::Value = serde_json::from_str(body)?; + let prefixes = json["prefixes"].as_array().ok_or("No prefixes array")?; + + let mut cidrs = Vec::new(); + for prefix in prefixes { + if let Some(ipv4) = prefix["ipv4Prefix"].as_str() { + cidrs.push(ipv4.to_string()); + } + } + + Ok(cidrs) +} + +fn cidr_to_ips(cidr: &str) -> Vec { + let parts: Vec<&str> = cidr.split('/').collect(); + if parts.len() != 2 { + return Vec::new(); + } + + let base_ip = parts[0]; + let prefix_len: u8 = match parts[1].parse() { + Ok(p) => p, + Err(_) => return Vec::new(), + }; + + let octets: Vec<&str> = base_ip.split('.').collect(); + if octets.len() != 4 { + return Vec::new(); + } + + let o: Vec = match octets.iter().map(|s| s.parse()).collect() { + Ok(v) => v, + Err(_) => return Vec::new(), + }; + + let base = ((o[0] as u32) << 24) | ((o[1] as u32) << 16) | ((o[2] as u32) << 8) | (o[3] as u32); + let host_bits = 32 - prefix_len; + let num_hosts = 1u32 << host_bits; + + let limit = num_hosts.min(256); + + (1..limit - 1) + .map(|i| { + let ip = base + i; + format!( + "{}.{}.{}.{}", + (ip >> 24) & 0xFF, + (ip >> 16) & 0xFF, + (ip >> 8) & 0xFF, + ip & 0xFF + ) + }) + .collect() +} + +async fn validate_ips(ips: &[String], sni: &str, google_ip_validation: bool) -> Vec { + let tls_cfg = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerify)) + .with_no_client_auth(); + let connector = TlsConnector::from(Arc::new(tls_cfg)); + + let sem = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY)); + let mut tasks = Vec::new(); + + for ip in ips { + let ip = ip.clone(); + let sni = sni.to_string(); + let connector = connector.clone(); + let sem = sem.clone(); + + tasks.push(tokio::spawn(async move { + let _permit = sem.acquire().await.ok(); + let result = quick_probe(&ip, &sni, connector, google_ip_validation).await; + (ip, result) + })); + } + + let mut working = Vec::new(); + for task in tasks { + if let Ok((ip, true)) = task.await { + working.push(ip); + } + } + + working +} +async fn quick_probe( + ip: &str, + sni: &str, + connector: TlsConnector, + google_ip_validation: bool, +) -> bool { + let addr: SocketAddr = match format!("{}:443", ip).parse() { + Ok(a) => a, + Err(_) => return false, + }; + + let tcp = match tokio::time::timeout(Duration::from_secs(2), TcpStream::connect(addr)).await { + Ok(Ok(t)) => t, + _ => return false, + }; + + let server_name = match ServerName::try_from(sni.to_string()) { + Ok(n) => n, + Err(_) => return false, + }; + + let mut tls = + match tokio::time::timeout(Duration::from_secs(2), connector.connect(server_name, tcp)) + .await + { + Ok(Ok(t)) => t, + _ => return false, + }; + + let req = format!( + "HEAD / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", + sni + ); + if tls.write_all(req.as_bytes()).await.is_err() { + return false; + } + let _ = tls.flush().await; + + let mut buf = [0u8; 1024]; + match tokio::time::timeout(Duration::from_secs(2), tls.read(&mut buf)).await { + Ok(Ok(n)) if n > 0 => { + let response = String::from_utf8_lossy(&buf[..n]); + + if !response.starts_with("HTTP/") { + return false; + } + + if google_ip_validation { + let lower = response.to_lowercase(); + return lower.contains("server: gws") + || lower.contains("x-google-") + || lower.contains("alt-svc: h3="); + } + true + } + _ => false, + } +} + +async fn probe( + ip: &str, + sni: &str, + connector: TlsConnector, + google_ip_validation: bool, +) -> Result_ { let start = Instant::now(); let addr: SocketAddr = match format!("{}:443", ip).parse() { Ok(a) => a, @@ -158,23 +558,24 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { } }; - let mut tls = match tokio::time::timeout(PROBE_TIMEOUT, connector.connect(server_name, tcp)).await { - Ok(Ok(t)) => t, - Ok(Err(e)) => { - return Result_ { - ip: ip.into(), - latency_ms: None, - error: Some(format!("tls: {}", e)), + let mut tls = + match tokio::time::timeout(PROBE_TIMEOUT, connector.connect(server_name, tcp)).await { + Ok(Ok(t)) => t, + Ok(Err(e)) => { + return Result_ { + ip: ip.into(), + latency_ms: None, + error: Some(format!("tls: {}", e)), + } } - } - Err(_) => { - return Result_ { - ip: ip.into(), - latency_ms: None, - error: Some("tls timeout".into()), + Err(_) => { + return Result_ { + ip: ip.into(), + latency_ms: None, + error: Some("tls timeout".into()), + } } - } - }; + }; let req = format!( "HEAD / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", @@ -189,12 +590,29 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { } let _ = tls.flush().await; - let mut buf = [0u8; 256]; + let mut buf = [0u8; 1024]; match tokio::time::timeout(PROBE_TIMEOUT, tls.read(&mut buf)).await { Ok(Ok(n)) if n > 0 => { - let elapsed = start.elapsed().as_millis(); - let head = String::from_utf8_lossy(&buf[..n.min(32)]); - if head.starts_with("HTTP/") { + let response = String::from_utf8_lossy(&buf[..n]); + + if !response.starts_with("HTTP/") { + return Result_ { + ip: ip.into(), + latency_ms: None, + error: Some("bad reply".into()), + }; + } + + let lower = response.to_lowercase(); + let mut is_google = true; + if google_ip_validation { + is_google = lower.contains("server: gws") + || lower.contains("x-google-") + || lower.contains("alt-svc: h3="); + } + + if is_google { + let elapsed = start.elapsed().as_millis(); Result_ { ip: ip.into(), latency_ms: Some(elapsed), @@ -204,7 +622,7 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { Result_ { ip: ip.into(), latency_ms: None, - error: Some(format!("bad reply: {:?}", head)), + error: Some("not google frontend".into()), } } }