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
{
"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 // برسی هدر های بازگشته از ایپی برای برسی هدر ها و تشخیص کاربردی بودن ایپی
}
```
+11 -3
View File
@@ -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
})
}
}
+8 -1
View File
@@ -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()
+138 -84
View File
@@ -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<tokio::sync::SemaphorePermit<'_>> = sem.acquire().await.ok();
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();
}
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<String> {
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<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;
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<Vec<
}
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 priority_ips_len = &priority_ips.len();
priority_ips.shuffle(&mut rng);
other_ips.shuffle(&mut rng);
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());
}
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();
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).await;
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());
tracing::info!(
"Found {} working IPs from {} tested",
working_ips.len(),
candidate_ips.len()
);
Ok(working_ips)
}
async fn resolve_famous_domains() -> Vec<String> {
let mut ips = Vec::new();
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>> {
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<Vec<String>, Box<dyn std::error::Error>>
.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<Vec<String>, Box<dyn std::error::Error>>
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<Vec<String>, Box<dyn std::error::Error>>
.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<String> {
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<u8> = 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<String> {
.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()
.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_ {