chor: update docs and read batch size and header checking from conf

This commit is contained in:
Mohammadreza Jafari
2026-04-22 13:52:05 +03:30
parent 2b3386db01
commit 8e1ed523ac
4 changed files with 163 additions and 90 deletions
+6 -2
View File
@@ -183,7 +183,9 @@ You can enable dynamic IP discovery by setting fetch_ips_from_api to true in con
```json ```json
{ {
"fetch_ips_from_api": true, "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 ```json
{ {
"fetch_ips_from_api": true, "fetch_ips_from_api": true,
"max_ips_to_scan": 100 "max_ips_to_scan": 100,
"scan_batch_size":100,
"google_ip_validation": true // برسی هدر های بازگشته از ایپی برای برسی هدر ها و تشخیص کاربردی بودن ایپی
} }
``` ```
+11 -3
View File
@@ -132,7 +132,9 @@ struct FormState {
/// Whether the floating SNI editor window is open. /// Whether the floating SNI editor window is open.
sni_editor_open: bool, sni_editor_open: bool,
fetch_ips_from_api: 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)] #[derive(Clone, Debug)]
@@ -180,6 +182,8 @@ fn load_form() -> FormState {
sni_editor_open: false, sni_editor_open: false,
fetch_ips_from_api:c.fetch_ips_from_api, fetch_ips_from_api:c.fetch_ips_from_api,
max_ips_to_scan:c.max_ips_to_scan, max_ips_to_scan:c.max_ips_to_scan,
google_ip_validation: c.google_ip_validation,
scan_batch_size:c.scan_batch_size
} }
} else { } else {
FormState { FormState {
@@ -200,6 +204,8 @@ fn load_form() -> FormState {
sni_editor_open: false, sni_editor_open: false,
fetch_ips_from_api:false, fetch_ips_from_api:false,
max_ips_to_scan:100, max_ips_to_scan:100,
google_ip_validation:true,
scan_batch_size:500
} }
} }
} }
@@ -304,8 +310,10 @@ impl FormState {
// on an empty pool. // on an empty pool.
if active.is_empty() { None } else { Some(active) } if active.is_empty() { None } else { Some(active) }
}, },
fetch_ips_from_api:false, fetch_ips_from_api:self.fetch_ips_from_api,
max_ips_to_scan: 100, max_ips_to_scan: self.max_ips_to_scan,
google_ip_validation:self.google_ip_validation,
scan_batch_size:self.scan_batch_size
}) })
} }
} }
+8 -1
View File
@@ -85,11 +85,18 @@ pub struct Config {
#[serde(default = "default_max_ips_to_scan")] #[serde(default = "default_max_ips_to_scan")]
pub max_ips_to_scan: usize, 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_fetch_ips_from_api() -> bool { false }
fn default_max_ips_to_scan() -> usize { 100 } 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 { fn default_google_ip() -> String {
"216.239.38.120".into() "216.239.38.120".into()
+138 -84
View File
@@ -2,6 +2,7 @@ use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use rand::seq::SliceRandom;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_rustls::rustls::client::danger::{ 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::pki_types::{CertificateDer, ServerName, UnixTime};
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme}; use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use tokio_rustls::TlsConnector; use tokio_rustls::TlsConnector;
use rand::seq::SliceRandom;
use crate::config::Config; use crate::config::Config;
@@ -67,9 +67,14 @@ struct Result_ {
pub async fn run(config: &Config) -> bool { pub async fn run(config: &Config) -> bool {
let ips = fetch_google_ips(config).await; let ips = fetch_google_ips(config).await;
let google_ip_validation = config.google_ip_validation;
let sni = config.front_domain.clone(); 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!(); println!();
let tls_cfg = ClientConfig::builder() let tls_cfg = ClientConfig::builder()
@@ -86,8 +91,8 @@ pub async fn run(config: &Config) -> bool {
let sem = sem.clone(); let sem = sem.clone();
let ip = ip.to_string(); let ip = ip.to_string();
tasks.push(tokio::spawn(async move { tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.ok(); let _permit: Option<tokio::sync::SemaphorePermit<'_>> = sem.acquire().await.ok();
probe(&ip, &sni, connector).await probe(&ip, &sni, connector,google_ip_validation).await
})); }));
} }
@@ -135,7 +140,14 @@ async fn fetch_google_ips(config: &Config) -> Vec<String> {
return CANDIDATE_IPS.iter().map(|s| s.to_string()).collect(); 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() => { Ok(ips) if !ips.is_empty() => {
tracing::info!("✓ Validated {} working IPs from goog.json", ips.len()); tracing::info!("✓ Validated {} working IPs from goog.json", ips.len());
ips ips
@@ -145,15 +157,26 @@ async fn fetch_google_ips(config: &Config) -> Vec<String> {
CANDIDATE_IPS.iter().map(|s| s.to_string()).collect() CANDIDATE_IPS.iter().map(|s| s.to_string()).collect()
} }
Err(e) => { 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() CANDIDATE_IPS.iter().map(|s| s.to_string()).collect()
} }
} }
} }
async fn fetch_and_validate_google_ips(sni: &str, max_ips: usize) -> Result<Vec<String>, Box<dyn std::error::Error>> { async fn fetch_and_validate_google_ips(
sni: &str,
max_ips: usize,
batch_size: usize,
google_ip_validation: bool
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let famous_ips = resolve_famous_domains().await; 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?; let cidrs = fetch_google_cidrs().await?;
tracing::info!("Fetched {} CIDR blocks from goog.json", cidrs.len()); 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<Vec<
} }
let other_ips_len = &other_ips.len(); let other_ips_len = &other_ips.len();
tracing::info!("Extracted {} priority IPs and {} other IPs", priority_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 mut rng = rand::thread_rng();
let priority_ips_len = &priority_ips.len(); let priority_ips_len = &priority_ips.len();
priority_ips.shuffle(&mut rng); priority_ips.shuffle(&mut rng);
other_ips.shuffle(&mut rng); other_ips.shuffle(&mut rng);
let mut candidate_ips = Vec::new(); let mut candidate_ips = Vec::new();
@@ -194,23 +221,29 @@ async fn fetch_and_validate_google_ips(sni: &str, max_ips: usize) -> Result<Vec<
return Err("No valid IPs extracted from CIDRs".into()); 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); tracing::info!(
"Selected {} IPs to test (from {} total), testing in batches...",
candidate_ips.len(),
priority_ips_len + other_ips_len
);
let batch_size = 50;
let mut working_ips = Vec::new(); let mut working_ips = Vec::new();
for (i, chunk) in candidate_ips.chunks(batch_size).enumerate() { for (i, chunk) in candidate_ips.chunks(batch_size).enumerate() {
tracing::debug!("Testing batch {} ({} IPs)...", i + 1, chunk.len()); tracing::debug!("Testing batch {} ({} IPs)...", i + 1, chunk.len());
let batch_working = validate_ips(chunk, sni).await; let batch_working = validate_ips(chunk, sni,google_ip_validation).await;
working_ips.extend(batch_working); working_ips.extend(batch_working);
} }
tracing::info!("Found {} working IPs from {} tested", working_ips.len(), candidate_ips.len()); tracing::info!(
"Found {} working IPs from {} tested",
working_ips.len(),
candidate_ips.len()
);
Ok(working_ips) Ok(working_ips)
} }
async fn resolve_famous_domains() -> Vec<String> { async fn resolve_famous_domains() -> Vec<String> {
let mut ips = Vec::new(); let mut ips = Vec::new();
for domain in FAMOUS_GOOGLE_DOMAINS { for domain in FAMOUS_GOOGLE_DOMAINS {
@@ -288,8 +321,9 @@ fn ip_to_u32(ip: &str) -> Option<u32> {
async fn fetch_google_cidrs() -> Result<Vec<String>, Box<dyn std::error::Error>> { async fn fetch_google_cidrs() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let stream = tokio::time::timeout( let stream = tokio::time::timeout(
Duration::from_secs(10), Duration::from_secs(10),
TcpStream::connect("www.gstatic.com:443") TcpStream::connect("www.gstatic.com:443"),
).await??; )
.await??;
let tls_cfg = ClientConfig::builder() let tls_cfg = ClientConfig::builder()
.dangerous() .dangerous()
@@ -297,11 +331,12 @@ async fn fetch_google_cidrs() -> Result<Vec<String>, Box<dyn std::error::Error>>
.with_no_client_auth(); .with_no_client_auth();
let connector = TlsConnector::from(Arc::new(tls_cfg)); let connector = TlsConnector::from(Arc::new(tls_cfg));
let server_name = ServerName::try_from("www.gstatic.com".to_string())?; let server_name = ServerName::try_from("www.gstatic.com".to_string())?;
let mut tls_stream = tokio::time::timeout( let mut tls_stream = tokio::time::timeout(
Duration::from_secs(10), Duration::from_secs(10),
connector.connect(server_name, stream) connector.connect(server_name, stream),
).await??; )
.await??;
let request = "GET /ipranges/goog.json HTTP/1.1\r\n\ let request = "GET /ipranges/goog.json HTTP/1.1\r\n\
Host: www.gstatic.com\r\n\ Host: www.gstatic.com\r\n\
@@ -311,19 +346,17 @@ async fn fetch_google_cidrs() -> Result<Vec<String>, Box<dyn std::error::Error>>
tls_stream.flush().await?; tls_stream.flush().await?;
let mut response = Vec::new(); let mut response = Vec::new();
tokio::time::timeout( tokio::time::timeout(Duration::from_secs(15), async {
Duration::from_secs(15), let mut buf = [0u8; 4096];
async { loop {
let mut buf = [0u8; 4096]; match tls_stream.read(&mut buf).await {
loop { Ok(0) => break,
match tls_stream.read(&mut buf).await { Ok(n) => response.extend_from_slice(&buf[..n]),
Ok(0) => break, Err(_) => break,
Ok(n) => response.extend_from_slice(&buf[..n]),
Err(_) => break,
}
} }
} }
).await?; })
.await?;
let response_str = String::from_utf8_lossy(&response); let response_str = String::from_utf8_lossy(&response);
let body = response_str let body = response_str
@@ -332,9 +365,7 @@ async fn fetch_google_cidrs() -> Result<Vec<String>, Box<dyn std::error::Error>>
.ok_or("No HTTP body found")?; .ok_or("No HTTP body found")?;
let json: serde_json::Value = serde_json::from_str(body)?; let json: serde_json::Value = serde_json::from_str(body)?;
let prefixes = json["prefixes"] let prefixes = json["prefixes"].as_array().ok_or("No prefixes array")?;
.as_array()
.ok_or("No prefixes array")?;
let mut cidrs = Vec::new(); let mut cidrs = Vec::new();
for prefix in prefixes { for prefix in prefixes {
@@ -351,33 +382,34 @@ fn cidr_to_ips(cidr: &str) -> Vec<String> {
if parts.len() != 2 { if parts.len() != 2 {
return Vec::new(); return Vec::new();
} }
let base_ip = parts[0]; let base_ip = parts[0];
let prefix_len: u8 = match parts[1].parse() { let prefix_len: u8 = match parts[1].parse() {
Ok(p) => p, Ok(p) => p,
Err(_) => return Vec::new(), Err(_) => return Vec::new(),
}; };
let octets: Vec<&str> = base_ip.split('.').collect(); let octets: Vec<&str> = base_ip.split('.').collect();
if octets.len() != 4 { if octets.len() != 4 {
return Vec::new(); return Vec::new();
} }
let o: Vec<u8> = match octets.iter().map(|s| s.parse()).collect() { let o: Vec<u8> = match octets.iter().map(|s| s.parse()).collect() {
Ok(v) => v, Ok(v) => v,
Err(_) => return Vec::new(), 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 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 host_bits = 32 - prefix_len;
let num_hosts = 1u32 << host_bits; let num_hosts = 1u32 << host_bits;
let limit = num_hosts.min(256); let limit = num_hosts.min(256);
(1..limit - 1) (1..limit - 1)
.map(|i| { .map(|i| {
let ip = base + i; let ip = base + i;
format!("{}.{}.{}.{}", format!(
"{}.{}.{}.{}",
(ip >> 24) & 0xFF, (ip >> 24) & 0xFF,
(ip >> 16) & 0xFF, (ip >> 16) & 0xFF,
(ip >> 8) & 0xFF, (ip >> 8) & 0xFF,
@@ -387,39 +419,44 @@ fn cidr_to_ips(cidr: &str) -> Vec<String> {
.collect() .collect()
} }
async fn validate_ips(ips: &[String], sni: &str) -> Vec<String> { async fn validate_ips(ips: &[String], sni: &str, google_ip_validation: bool) -> Vec<String> {
let tls_cfg = ClientConfig::builder() let tls_cfg = ClientConfig::builder()
.dangerous() .dangerous()
.with_custom_certificate_verifier(Arc::new(NoVerify)) .with_custom_certificate_verifier(Arc::new(NoVerify))
.with_no_client_auth(); .with_no_client_auth();
let connector = TlsConnector::from(Arc::new(tls_cfg)); let connector = TlsConnector::from(Arc::new(tls_cfg));
let sem = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY)); let sem = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY));
let mut tasks = Vec::new(); let mut tasks = Vec::new();
for ip in ips { for ip in ips {
let ip = ip.clone(); let ip = ip.clone();
let sni = sni.to_string(); let sni = sni.to_string();
let connector = connector.clone(); let connector = connector.clone();
let sem = sem.clone(); let sem = sem.clone();
tasks.push(tokio::spawn(async move { tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.ok(); 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) (ip, result)
})); }));
} }
let mut working = Vec::new(); let mut working = Vec::new();
for task in tasks { for task in tasks {
if let Ok((ip, true)) = task.await { if let Ok((ip, true)) = task.await {
working.push(ip); working.push(ip);
} }
} }
working 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() { let addr: SocketAddr = match format!("{}:443", ip).parse() {
Ok(a) => a, Ok(a) => a,
Err(_) => return false, Err(_) => return false,
@@ -435,12 +472,18 @@ async fn quick_probe(ip: &str, sni: &str, connector: TlsConnector) -> bool {
Err(_) => return false, Err(_) => return false,
}; };
let mut tls = match tokio::time::timeout(Duration::from_secs(2), connector.connect(server_name, tcp)).await { let mut tls =
Ok(Ok(t)) => t, match tokio::time::timeout(Duration::from_secs(2), connector.connect(server_name, tcp))
_ => return false, .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() { if tls.write_all(req.as_bytes()).await.is_err() {
return false; 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 { match tokio::time::timeout(Duration::from_secs(2), tls.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => { Ok(Ok(n)) if n > 0 => {
let response = String::from_utf8_lossy(&buf[..n]); let response = String::from_utf8_lossy(&buf[..n]);
if !response.starts_with("HTTP/") { if !response.starts_with("HTTP/") {
return false; return false;
} }
let lower = response.to_lowercase(); if google_ip_validation {
lower.contains("server: gws") || let lower = response.to_lowercase();
lower.contains("x-google-") || return lower.contains("server: gws")
lower.contains("alt-svc: h3=") || lower.contains("x-google-")
|| lower.contains("alt-svc: h3=");
}
true
} }
_ => false, _ => false,
} }
} }
async fn probe(
async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ { ip: &str,
sni: &str,
connector: TlsConnector,
google_ip_validation: bool,
) -> Result_ {
let start = Instant::now(); let start = Instant::now();
let addr: SocketAddr = match format!("{}:443", ip).parse() { let addr: SocketAddr = match format!("{}:443", ip).parse() {
Ok(a) => a, 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 { let mut tls =
Ok(Ok(t)) => t, match tokio::time::timeout(PROBE_TIMEOUT, connector.connect(server_name, tcp)).await {
Ok(Err(e)) => { Ok(Ok(t)) => t,
return Result_ { Ok(Err(e)) => {
ip: ip.into(), return Result_ {
latency_ms: None, ip: ip.into(),
error: Some(format!("tls: {}", e)), latency_ms: None,
error: Some(format!("tls: {}", e)),
}
} }
} Err(_) => {
Err(_) => { return Result_ {
return Result_ { ip: ip.into(),
ip: ip.into(), latency_ms: None,
latency_ms: None, error: Some("tls timeout".into()),
error: Some("tls timeout".into()), }
} }
} };
};
let req = format!( let req = format!(
"HEAD / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", "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 { match tokio::time::timeout(PROBE_TIMEOUT, tls.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => { Ok(Ok(n)) if n > 0 => {
let response = String::from_utf8_lossy(&buf[..n]); let response = String::from_utf8_lossy(&buf[..n]);
if !response.starts_with("HTTP/") { if !response.starts_with("HTTP/") {
return Result_ { return Result_ {
ip: ip.into(), ip: ip.into(),
@@ -551,12 +602,15 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ {
error: Some("bad reply".into()), error: Some("bad reply".into()),
}; };
} }
let lower = response.to_lowercase(); let lower = response.to_lowercase();
let is_google = lower.contains("server: gws") || let mut is_google = true;
lower.contains("x-google-") || if google_ip_validation {
lower.contains("alt-svc: h3="); is_google = lower.contains("server: gws")
|| lower.contains("x-google-")
|| lower.contains("alt-svc: h3=");
}
if is_google { if is_google {
let elapsed = start.elapsed().as_millis(); let elapsed = start.elapsed().as_millis();
Result_ { Result_ {