mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-17 21:24:48 +03:00
Merge PR #9: dynamic Google IP discovery + frontend validation
Thanks @v4g4b0nd-0x76 for the feature. Two small fixes folded in on the merge so master still builds + doesn't hit sharp edges: - src/scan_ips.rs: rand::thread_rng() held across an .await tripped the Send bound on the async fn (ThreadRng isn't Send). Scoped the rng in a block so it drops before subsequent awaits. - src/scan_ips.rs: guard /0 and /32 CIDRs in cidr_to_ips and ip_in_cidr against the 1u32 << 32 shift panic (debug mode). goog.json is unlikely to contain either but defensive. Behavior unchanged otherwise: - fetch_ips_from_api=false (default): identical to previous static scan-ips behavior. - fetch_ips_from_api=true: fetches goog.json from www.gstatic.com, resolves famous Google domain IPs, prioritises matching CIDRs, samples up to max_ips_to_scan candidates, validates with gws/ x-google-/alt-svc headers. If the fetch fails, falls back to the static list cleanly — verified locally. Closes #10.
This commit is contained in:
@@ -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: <https://github.com/masterking32/MasterHttpRelayVPN> 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`** را انتخاب کنید
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<String>>,
|
||||
#[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()
|
||||
}
|
||||
|
||||
+461
-26
@@ -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<tokio::sync::SemaphorePermit<'_>> = sem.acquire().await.ok();
|
||||
probe(&ip, &sni, connector,google_ip_validation).await
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -115,7 +134,405 @@ 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<String> {
|
||||
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<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()
|
||||
);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Extracted {} priority IPs and {} other IPs",
|
||||
priority_ips.len(),
|
||||
other_ips.len()
|
||||
);
|
||||
|
||||
let priority_ips_len = priority_ips.len();
|
||||
let other_ips_len = other_ips.len();
|
||||
|
||||
// Scope the rng so it's dropped before any subsequent `.await` — rand's
|
||||
// ThreadRng isn't Send, so holding it across an await would error out
|
||||
// the whole async fn's Send bound.
|
||||
{
|
||||
let mut rng = rand::thread_rng();
|
||||
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<String> {
|
||||
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<String> {
|
||||
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,
|
||||
};
|
||||
|
||||
// Mask defends against /0 (shift-32 of u32 would panic in debug).
|
||||
let mask: u32 = if prefix_len == 0 {
|
||||
0
|
||||
} else if prefix_len >= 32 {
|
||||
u32::MAX
|
||||
} else {
|
||||
!((1u32 << (32 - prefix_len)) - 1)
|
||||
};
|
||||
(ip_num & mask) == (base_num & mask)
|
||||
}
|
||||
|
||||
fn ip_to_u32(ip: &str) -> Option<u32> {
|
||||
let octets: Vec<&str> = ip.split('.').collect();
|
||||
if octets.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let o: Vec<u8> = 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<Vec<String>, Box<dyn std::error::Error>> {
|
||||
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<String> {
|
||||
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<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);
|
||||
if prefix_len > 32 {
|
||||
return Vec::new();
|
||||
}
|
||||
let host_bits = 32 - prefix_len;
|
||||
let num_hosts: u32 = if host_bits >= 32 { u32::MAX } else { 1u32 << host_bits };
|
||||
|
||||
let limit = num_hosts.min(256);
|
||||
if limit < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
(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<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, 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 +575,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 +607,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 +639,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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user