From 8e1ed523ace418f3ff96fa255121a00ef54aee4a Mon Sep 17 00:00:00 2001 From: Mohammadreza Jafari Date: Wed, 22 Apr 2026 13:52:05 +0330 Subject: [PATCH] chor: update docs and read batch size and header checking from conf --- README.md | 8 +- src/bin/ui.rs | 14 ++- src/config.rs | 9 +- src/scan_ips.rs | 222 ++++++++++++++++++++++++++++++------------------ 4 files changed, 163 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 6e1bd14..04b7900 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,9 @@ You can enable dynamic IP discovery by setting fetch_ips_from_api to true in con ```json { "fetch_ips_from_api": true, - "max_ips_to_scan": 100 + "max_ips_to_scan": 100, + "scan_batch_size":100, + "google_ip_validation": true // check whether ips belongs to frontend sites of google or not } ``` @@ -470,7 +472,9 @@ Firefox cert store خودش را جدا دارد؛ installer تلاش می‌ک ```json { "fetch_ips_from_api": true, - "max_ips_to_scan": 100 + "max_ips_to_scan": 100, + "scan_batch_size":100, + "google_ip_validation": true // برسی هدر های بازگشته از ایپی برای برسی هدر ها و تشخیص کاربردی بودن ایپی } ``` diff --git a/src/bin/ui.rs b/src/bin/ui.rs index 62b62c5..2d5b4ba 100644 --- a/src/bin/ui.rs +++ b/src/bin/ui.rs @@ -132,7 +132,9 @@ struct FormState { /// Whether the floating SNI editor window is open. sni_editor_open: bool, fetch_ips_from_api: bool, -max_ips_to_scan: usize, + max_ips_to_scan: usize, + scan_batch_size:usize, + google_ip_validation: bool } #[derive(Clone, Debug)] @@ -180,6 +182,8 @@ fn load_form() -> FormState { 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 +204,8 @@ fn load_form() -> FormState { sni_editor_open: false, fetch_ips_from_api:false, max_ips_to_scan:100, + google_ip_validation:true, + scan_batch_size:500 } } } @@ -304,8 +310,10 @@ impl FormState { // on an empty pool. if active.is_empty() { None } else { Some(active) } }, - fetch_ips_from_api:false, - max_ips_to_scan: 100, + 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 19aa7af..bf8ed4c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -85,11 +85,18 @@ pub struct Config { #[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 6bc4ce5..924817c 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::{ @@ -10,7 +11,6 @@ use tokio_rustls::rustls::client::danger::{ use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime}; use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme}; use tokio_rustls::TlsConnector; -use rand::seq::SliceRandom; use crate::config::Config; @@ -67,9 +67,14 @@ 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)...", 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() @@ -86,8 +91,8 @@ pub async fn run(config: &Config) -> bool { 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 })); } @@ -135,7 +140,14 @@ async fn fetch_google_ips(config: &Config) -> Vec { return CANDIDATE_IPS.iter().map(|s| s.to_string()).collect(); } - match fetch_and_validate_google_ips(&config.front_domain, config.max_ips_to_scan).await { + 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 @@ -145,15 +157,26 @@ async fn fetch_google_ips(config: &Config) -> Vec { CANDIDATE_IPS.iter().map(|s| s.to_string()).collect() } Err(e) => { - tracing::warn!("Failed to fetch/validate Google IPs: {}, using static fallback", 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) -> Result, Box> { +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()); + 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()); @@ -174,13 +197,17 @@ async fn fetch_and_validate_google_ips(sni: &str, max_ips: usize) -> Result Result Vec { let mut ips = Vec::new(); for domain in FAMOUS_GOOGLE_DOMAINS { @@ -288,8 +321,9 @@ fn ip_to_u32(ip: &str) -> Option { async fn fetch_google_cidrs() -> Result, Box> { let stream = tokio::time::timeout( Duration::from_secs(10), - TcpStream::connect("www.gstatic.com:443") - ).await??; + TcpStream::connect("www.gstatic.com:443"), + ) + .await??; let tls_cfg = ClientConfig::builder() .dangerous() @@ -297,11 +331,12 @@ async fn fetch_google_cidrs() -> Result, Box> .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??; + connector.connect(server_name, stream), + ) + .await??; let request = "GET /ipranges/goog.json HTTP/1.1\r\n\ Host: www.gstatic.com\r\n\ @@ -311,19 +346,17 @@ async fn fetch_google_cidrs() -> Result, Box> 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, - } + 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?; + }) + .await?; let response_str = String::from_utf8_lossy(&response); let body = response_str @@ -332,9 +365,7 @@ async fn fetch_google_cidrs() -> Result, Box> .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 prefixes = json["prefixes"].as_array().ok_or("No prefixes array")?; let mut cidrs = Vec::new(); for prefix in prefixes { @@ -351,33 +382,34 @@ fn cidr_to_ips(cidr: &str) -> Vec { 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!("{}.{}.{}.{}", + format!( + "{}.{}.{}.{}", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, @@ -387,39 +419,44 @@ fn cidr_to_ips(cidr: &str) -> Vec { .collect() } -async fn validate_ips(ips: &[String], sni: &str) -> Vec { +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).await; + 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) -> bool { +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, @@ -435,12 +472,18 @@ async fn quick_probe(ip: &str, sni: &str, connector: TlsConnector) -> bool { 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 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); + 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; } @@ -450,22 +493,29 @@ async fn quick_probe(ip: &str, sni: &str, connector: TlsConnector) -> bool { 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; } - - let lower = response.to_lowercase(); - lower.contains("server: gws") || - lower.contains("x-google-") || - lower.contains("alt-svc: h3=") + + 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) -> Result_ { +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, @@ -508,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", @@ -543,7 +594,7 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { match tokio::time::timeout(PROBE_TIMEOUT, 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 Result_ { ip: ip.into(), @@ -551,12 +602,15 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { error: Some("bad reply".into()), }; } - + let lower = response.to_lowercase(); - let is_google = lower.contains("server: gws") || - lower.contains("x-google-") || - lower.contains("alt-svc: h3="); - + 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_ {