mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 06:34:41 +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"]`.
|
`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
|
### Step 5 — Point your client at the proxy
|
||||||
|
|
||||||
The tool listens on **two** ports. Use whichever your client supports:
|
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` را تایپ کنید
|
۱. منوی `Settings` را باز کنید، در خانهٔ جستوجو عبارت `proxy` را تایپ کنید
|
||||||
۲. روی **`Network Settings`** کلیک کنید
|
۲. روی **`Network Settings`** کلیک کنید
|
||||||
۳. گزینهٔ **`Manual proxy configuration`** را انتخاب کنید
|
۳. گزینهٔ **`Manual proxy configuration`** را انتخاب کنید
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ struct FormState {
|
|||||||
sni_custom_input: String,
|
sni_custom_input: String,
|
||||||
/// 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,
|
||||||
|
max_ips_to_scan: usize,
|
||||||
|
scan_batch_size:usize,
|
||||||
|
google_ip_validation: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -182,6 +186,10 @@ fn load_form() -> FormState {
|
|||||||
sni_pool,
|
sni_pool,
|
||||||
sni_custom_input: String::new(),
|
sni_custom_input: String::new(),
|
||||||
sni_editor_open: false,
|
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 {
|
} else {
|
||||||
FormState {
|
FormState {
|
||||||
@@ -200,6 +208,10 @@ fn load_form() -> FormState {
|
|||||||
sni_pool: sni_pool_for_form(None, "www.google.com"),
|
sni_pool: sni_pool_for_form(None, "www.google.com"),
|
||||||
sni_custom_input: String::new(),
|
sni_custom_input: String::new(),
|
||||||
sni_editor_open: false,
|
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)
|
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`.
|
/// various times). Can be tested per-name via the UI or `mhrv-rs test-sni`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sni_hosts: Option<Vec<String>>,
|
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 {
|
fn default_google_ip() -> String {
|
||||||
"216.239.38.120".into()
|
"216.239.38.120".into()
|
||||||
}
|
}
|
||||||
|
|||||||
+461
-26
@@ -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::{
|
||||||
@@ -44,6 +45,17 @@ const CANDIDATE_IPS: &[&str] = &[
|
|||||||
"142.250.72.110",
|
"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 PROBE_TIMEOUT: Duration = Duration::from_secs(4);
|
||||||
const CONCURRENCY: usize = 8;
|
const CONCURRENCY: usize = 8;
|
||||||
|
|
||||||
@@ -54,8 +66,15 @@ struct Result_ {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(config: &Config) -> bool {
|
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();
|
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!();
|
println!();
|
||||||
|
|
||||||
let tls_cfg = ClientConfig::builder()
|
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 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::with_capacity(CANDIDATE_IPS.len());
|
let mut tasks = Vec::with_capacity(ips.len());
|
||||||
for ip in CANDIDATE_IPS {
|
for ip in &ips {
|
||||||
let sni = sni.clone();
|
let sni = sni.clone();
|
||||||
let connector = connector.clone();
|
let connector = connector.clone();
|
||||||
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
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 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,
|
||||||
@@ -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 {
|
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",
|
||||||
@@ -189,12 +607,29 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ {
|
|||||||
}
|
}
|
||||||
let _ = tls.flush().await;
|
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 {
|
match tokio::time::timeout(PROBE_TIMEOUT, tls.read(&mut buf)).await {
|
||||||
Ok(Ok(n)) if n > 0 => {
|
Ok(Ok(n)) if n > 0 => {
|
||||||
let elapsed = start.elapsed().as_millis();
|
let response = String::from_utf8_lossy(&buf[..n]);
|
||||||
let head = String::from_utf8_lossy(&buf[..n.min(32)]);
|
|
||||||
if head.starts_with("HTTP/") {
|
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_ {
|
Result_ {
|
||||||
ip: ip.into(),
|
ip: ip.into(),
|
||||||
latency_ms: Some(elapsed),
|
latency_ms: Some(elapsed),
|
||||||
@@ -204,7 +639,7 @@ async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ {
|
|||||||
Result_ {
|
Result_ {
|
||||||
ip: ip.into(),
|
ip: ip.into(),
|
||||||
latency_ms: None,
|
latency_ms: None,
|
||||||
error: Some(format!("bad reply: {:?}", head)),
|
error: Some("not google frontend".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user