Skip to content

Commit ca10f77

Browse files
scan-sni: add automated SNI discovery + DPI validation (#83)
New `mhrv-rs scan-sni` subcommand: pulls Google's published IP ranges, issues PTR lookups via dns.google, filters results to Google-related hostnames, then TLS-probes each discovered SNI against the user's configured `google_ip`. Prints the SNIs that pass DPI for the user to paste into `sni_hosts`. Also expands the hardcoded FAMOUS_GOOGLE_DOMAINS list the existing scan-ips command already used. Adds `url` crate for URL parsing in the DNS-over-HTTPS client. No other behavioural changes.
1 parent af44abb commit ca10f77

5 files changed

Lines changed: 411 additions & 42 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ eframe = { version = "0.28", default-features = false, features = [
7272
"wgpu",
7373
"persistence",
7474
], optional = true }
75+
url = "2.5.8"
7576

7677
# Unix-only deps. Must come after `[dependencies]` because starting a new
7778
# table here otherwise ends the main one — anything below it (incl. eframe)

src/main.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use mhrv_rs::cert_installer::{install_ca, is_ca_trusted};
1111
use mhrv_rs::config::Config;
1212
use mhrv_rs::mitm::{MitmCertManager, CA_CERT_FILE};
1313
use mhrv_rs::proxy_server::ProxyServer;
14-
use mhrv_rs::{scan_ips, test_cmd};
14+
use mhrv_rs::{scan_ips, scan_sni, test_cmd};
1515

1616
const VERSION: &str = env!("CARGO_PKG_VERSION");
1717

@@ -27,6 +27,7 @@ enum Command {
2727
Test,
2828
ScanIps,
2929
TestSni,
30+
ScanSni,
3031
}
3132

3233
fn print_help() {
@@ -37,6 +38,7 @@ USAGE:
3738
mhrv-rs [OPTIONS] Start the proxy server (default)
3839
mhrv-rs test [OPTIONS] Probe the Apps Script relay end-to-end
3940
mhrv-rs scan-ips [OPTIONS] Scan Google frontend IPs for reachability + latency
41+
mhrv-rs scan-sni Scan Google SNI name using Google frontend IPs found in 'scan-ips' command
4042
mhrv-rs test-sni [OPTIONS] Probe each SNI name in the rotation pool against google_ip
4143
4244
OPTIONS:
@@ -70,6 +72,10 @@ fn parse_args() -> Result<Args, String> {
7072
command = Command::ScanIps;
7173
raw.remove(0);
7274
}
75+
"scan-sni" => {
76+
command = Command::ScanSni;
77+
raw.remove(0);
78+
}
7379
"test-sni" => {
7480
command = Command::TestSni;
7581
raw.remove(0);
@@ -190,8 +196,17 @@ async fn main() -> ExitCode {
190196
ExitCode::FAILURE
191197
};
192198
}
199+
Command::ScanSni => {
200+
let ok = scan_sni::discover_snis_from_google_ips(&config).await;
201+
return if ok {
202+
ExitCode::SUCCESS
203+
} else {
204+
ExitCode::FAILURE
205+
};
206+
}
207+
193208
Command::TestSni => {
194-
let ok = mhrv_rs::scan_sni::run(&config).await;
209+
let ok = scan_sni::run(&config).await;
195210
return if ok {
196211
ExitCode::SUCCESS
197212
} else {

src/scan_ips.rs

Lines changed: 132 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,118 @@ const CANDIDATE_IPS: &[&str] = &[
4444
"142.250.64.206",
4545
"142.250.72.110",
4646
];
47-
48-
const FAMOUS_GOOGLE_DOMAINS: &[&str] = &[
47+
pub const FAMOUS_GOOGLE_DOMAINS: &[&str] = &[
48+
// Core services
4949
"google.com",
5050
"www.google.com",
5151
"youtube.com",
5252
"www.youtube.com",
5353
"gmail.com",
54+
"www.gmail.com",
5455
"drive.google.com",
5556
"docs.google.com",
57+
"sheets.google.com",
58+
"slides.google.com",
5659
"maps.google.com",
60+
"www.maps.google.com",
61+
// Search & Discovery
62+
"search.google.com",
63+
"images.google.com",
64+
"www.images.google.com",
65+
"news.google.com",
66+
"www.news.google.com",
67+
"scholar.google.com",
68+
"www.scholar.google.com",
69+
"books.google.com",
70+
"translate.google.com",
71+
"www.translate.google.com",
72+
// Communication
73+
"mail.google.com",
74+
"chat.google.com",
75+
"meet.google.com",
76+
"hangouts.google.com",
77+
"voice.google.com",
78+
"allo.google.com",
79+
// Media & Entertainment
80+
"play.google.com",
81+
"music.google.com",
82+
"movies.google.com",
83+
"video.google.com",
84+
"videos.google.com",
85+
"photos.google.com",
86+
"picasa.google.com",
87+
"picasaweb.google.com",
88+
// Productivity
89+
"calendar.google.com",
90+
"keep.google.com",
91+
"contacts.google.com",
92+
"tasks.google.com",
93+
"forms.google.com",
94+
"sites.google.com",
95+
"www.sites.google.com",
96+
// Account & Settings
97+
"accounts.google.com",
98+
"myaccount.google.com",
99+
"myactivity.google.com",
100+
"passwords.google.com",
101+
"adssettings.google.com",
102+
// Business & Ads
103+
"ads.google.com",
104+
"adwords.google.com",
105+
"www.adwords.google.com",
106+
"adsense.google.com",
107+
"analytics.google.com",
108+
"business.google.com",
109+
"mybusiness.google.com",
110+
"merchants.google.com",
111+
// Developer & Cloud
112+
"console.cloud.google.com",
113+
"cloud.google.com",
114+
"firebase.google.com",
115+
"console.firebase.google.com",
116+
"developers.google.com",
117+
"console.developers.google.com",
118+
"apis.google.com",
119+
"fonts.google.com",
120+
// Mobile & Apps
121+
"android.google.com",
122+
"chrome.google.com",
123+
"chromebook.google.com",
124+
// Education & Learning
125+
"classroom.google.com",
126+
"edu.google.com",
127+
// Shopping & Payments
128+
"shopping.google.com",
129+
"pay.google.com",
130+
"payments.google.com",
131+
"wallet.google.com",
132+
"store.google.com",
133+
// Travel & Local
134+
"flights.google.com",
135+
"hotels.google.com",
136+
"travel.google.com",
137+
// Other Services
138+
"blogger.google.com",
139+
"domains.google.com",
140+
"trends.google.com",
141+
"alerts.google.com",
142+
"podcasts.google.com",
143+
"fit.google.com",
144+
"home.google.com",
145+
"assistant.google.com",
146+
"gemini.google.com",
147+
// Support & Info
148+
"support.google.com",
149+
"policies.google.com",
150+
"privacy.google.com",
151+
"about.google.com",
152+
"blog.google.com",
153+
// Legacy/Regional
154+
"plus.google.com",
155+
"www.plus.google.com",
156+
"orkut.google.com",
157+
"reader.google.com",
158+
"wave.google.com",
57159
];
58160

59161
const PROBE_TIMEOUT: Duration = Duration::from_secs(4);
@@ -92,7 +194,7 @@ pub async fn run(config: &Config) -> bool {
92194
let ip = ip.to_string();
93195
tasks.push(tokio::spawn(async move {
94196
let _permit: Option<tokio::sync::SemaphorePermit<'_>> = sem.acquire().await.ok();
95-
probe(&ip, &sni, connector,google_ip_validation).await
197+
probe(&ip, &sni, connector, google_ip_validation).await
96198
}));
97199
}
98200

@@ -134,7 +236,7 @@ pub async fn run(config: &Config) -> bool {
134236
}
135237
}
136238

137-
async fn fetch_google_ips(config: &Config) -> Vec<String> {
239+
pub async fn fetch_google_ips(config: &Config) -> Vec<String> {
138240
if !config.fetch_ips_from_api {
139241
tracing::info!("fetch_ips_from_api disabled, using static fallback");
140242
return CANDIDATE_IPS.iter().map(|s| s.to_string()).collect();
@@ -144,7 +246,7 @@ async fn fetch_google_ips(config: &Config) -> Vec<String> {
144246
&config.front_domain,
145247
config.max_ips_to_scan,
146248
config.scan_batch_size,
147-
config.google_ip_validation
249+
config.google_ip_validation,
148250
)
149251
.await
150252
{
@@ -170,7 +272,7 @@ async fn fetch_and_validate_google_ips(
170272
sni: &str,
171273
max_ips: usize,
172274
batch_size: usize,
173-
google_ip_validation: bool
275+
google_ip_validation: bool,
174276
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
175277
let famous_ips = resolve_famous_domains().await;
176278
tracing::info!(
@@ -224,21 +326,32 @@ async fn fetch_and_validate_google_ips(
224326
if candidate_ips.is_empty() {
225327
return Err("No valid IPs extracted from CIDRs".into());
226328
}
227-
329+
let total_batches = (candidate_ips.len() + batch_size - 1) / batch_size;
228330
tracing::info!(
229-
"Selected {} IPs to test (from {} total), testing in batches...",
331+
"Selected {} IPs to test (from {} total), testing in {} batches...",
230332
candidate_ips.len(),
231-
priority_ips_len + other_ips_len
333+
priority_ips_len + other_ips_len,
334+
total_batches
232335
);
233336

234337
let mut working_ips = Vec::new();
338+
let mut total_tested = 0;
235339

236340
for (i, chunk) in candidate_ips.chunks(batch_size).enumerate() {
237-
tracing::debug!("Testing batch {} ({} IPs)...", i + 1, chunk.len());
238-
let batch_working = validate_ips(chunk, sni,google_ip_validation).await;
239-
working_ips.extend(batch_working);
341+
let batch_working = validate_ips(chunk, sni, google_ip_validation).await;
342+
working_ips.extend(batch_working.clone());
343+
total_tested += chunk.len();
344+
345+
tracing::info!(
346+
"Batch {}/{}: tested {} IPs, found {} working (total: {}/{})",
347+
i + 1,
348+
total_batches,
349+
chunk.len(),
350+
batch_working.len(),
351+
total_tested,
352+
candidate_ips.len()
353+
);
240354
}
241-
242355
tracing::info!(
243356
"Found {} working IPs from {} tested",
244357
working_ips.len(),
@@ -415,7 +528,11 @@ fn cidr_to_ips(cidr: &str) -> Vec<String> {
415528
return Vec::new();
416529
}
417530
let host_bits = 32 - prefix_len;
418-
let num_hosts: u32 = if host_bits >= 32 { u32::MAX } else { 1u32 << host_bits };
531+
let num_hosts: u32 = if host_bits >= 32 {
532+
u32::MAX
533+
} else {
534+
1u32 << host_bits
535+
};
419536

420537
let limit = num_hosts.min(256);
421538
if limit < 2 {
@@ -621,7 +738,7 @@ async fn probe(
621738
}
622739

623740
let lower = response.to_lowercase();
624-
let mut is_google = true;
741+
let mut is_google = true;
625742
if google_ip_validation {
626743
is_google = lower.contains("server: gws")
627744
|| lower.contains("x-google-")

0 commit comments

Comments
 (0)