Skip to content

Commit 3f0e266

Browse files
committed
add 'test' and 'scan-ips' subcommands
test: one-shot end-to-end probe. Issues a GET to api.ipify.org through the configured relay and prints status + body + timing. Clear pass/fail with specific diagnostics for 502/504 (auth_key mismatch, quota, etc). Verified live: 3.8s round-trip returning the caller's real IP. scan-ips: parallel TLS probe of 28 known Google frontend IPs with SNI=front_domain. Reports which are reachable and sorts by latency. Users pick the fastest and paste into google_ip. Verified live: 7/28 reachable (the others were Windscribe'd out), top 3 ranked. Both subcommands share the existing config.json and require no extra flags. Default 'mhrv-rs' with no subcommand runs the proxy as before.
1 parent c17afdd commit 3f0e266

3 files changed

Lines changed: 373 additions & 2 deletions

File tree

src/main.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ mod config;
66
mod domain_fronter;
77
mod mitm;
88
mod proxy_server;
9+
mod scan_ips;
10+
mod test_cmd;
911

1012
use std::path::{Path, PathBuf};
1113
use std::process::ExitCode;
@@ -25,14 +27,23 @@ struct Args {
2527
config_path: PathBuf,
2628
install_cert: bool,
2729
no_cert_check: bool,
30+
command: Command,
31+
}
32+
33+
enum Command {
34+
Serve,
35+
Test,
36+
ScanIps,
2837
}
2938

3039
fn print_help() {
3140
println!(
3241
"mhrv-rs {} — Rust port of MasterHttpRelayVPN (apps_script mode only)
3342
3443
USAGE:
35-
mhrv-rs [--config PATH] [--install-cert] [--no-cert-check]
44+
mhrv-rs [OPTIONS] Start the proxy server (default)
45+
mhrv-rs test [OPTIONS] Probe the Apps Script relay end-to-end
46+
mhrv-rs scan-ips [OPTIONS] Scan Google frontend IPs for reachability + latency
3647
3748
OPTIONS:
3849
-c, --config PATH Path to config.json (default: ./config.json)
@@ -52,8 +63,24 @@ fn parse_args() -> Result<Args, String> {
5263
let mut config_path = PathBuf::from("config.json");
5364
let mut install_cert = false;
5465
let mut no_cert_check = false;
66+
let mut command = Command::Serve;
67+
68+
let mut raw: Vec<String> = std::env::args().skip(1).collect();
69+
if let Some(first) = raw.first() {
70+
match first.as_str() {
71+
"test" => {
72+
command = Command::Test;
73+
raw.remove(0);
74+
}
75+
"scan-ips" => {
76+
command = Command::ScanIps;
77+
raw.remove(0);
78+
}
79+
_ => {}
80+
}
81+
}
5582

56-
let mut it = std::env::args().skip(1);
83+
let mut it = raw.into_iter();
5784
while let Some(arg) = it.next() {
5885
match arg.as_str() {
5986
"-h" | "--help" => {
@@ -77,6 +104,7 @@ fn parse_args() -> Result<Args, String> {
77104
config_path,
78105
install_cert,
79106
no_cert_check,
107+
command,
80108
})
81109
}
82110

@@ -136,6 +164,18 @@ async fn main() -> ExitCode {
136164

137165
init_logging(&config.log_level);
138166

167+
match args.command {
168+
Command::Test => {
169+
let ok = test_cmd::run(&config).await;
170+
return if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE };
171+
}
172+
Command::ScanIps => {
173+
let ok = scan_ips::run(&config).await;
174+
return if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE };
175+
}
176+
Command::Serve => {}
177+
}
178+
139179
tracing::warn!("mhrv-rs {} starting (mode: apps_script)", VERSION);
140180
tracing::info!(
141181
"Apps Script relay: SNI={} -> script.google.com (via {})",

src/scan_ips.rs

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
use std::net::SocketAddr;
2+
use std::sync::Arc;
3+
use std::time::{Duration, Instant};
4+
5+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
6+
use tokio::net::TcpStream;
7+
use tokio_rustls::rustls::client::danger::{
8+
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
9+
};
10+
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
11+
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
12+
use tokio_rustls::TlsConnector;
13+
14+
use crate::config::Config;
15+
16+
const CANDIDATE_IPS: &[&str] = &[
17+
"216.239.32.120",
18+
"216.239.34.120",
19+
"216.239.36.120",
20+
"216.239.38.120",
21+
"216.58.212.142",
22+
"142.250.80.142",
23+
"142.250.80.138",
24+
"142.250.179.110",
25+
"142.250.185.110",
26+
"142.250.184.206",
27+
"142.250.190.238",
28+
"142.250.191.78",
29+
"172.217.1.206",
30+
"172.217.14.206",
31+
"172.217.16.142",
32+
"172.217.22.174",
33+
"172.217.164.110",
34+
"172.217.168.206",
35+
"172.217.169.206",
36+
"34.107.221.82",
37+
"142.251.32.110",
38+
"142.251.33.110",
39+
"142.251.46.206",
40+
"142.251.46.238",
41+
"142.250.80.170",
42+
"142.250.72.206",
43+
"142.250.64.206",
44+
"142.250.72.110",
45+
];
46+
47+
const PROBE_TIMEOUT: Duration = Duration::from_secs(4);
48+
const CONCURRENCY: usize = 8;
49+
50+
struct Result_ {
51+
ip: String,
52+
latency_ms: Option<u128>,
53+
error: Option<String>,
54+
}
55+
56+
pub async fn run(config: &Config) -> bool {
57+
let sni = config.front_domain.clone();
58+
println!("Scanning {} Google frontend IPs (SNI={}, timeout={}s)...", CANDIDATE_IPS.len(), sni, PROBE_TIMEOUT.as_secs());
59+
println!();
60+
61+
let tls_cfg = ClientConfig::builder()
62+
.dangerous()
63+
.with_custom_certificate_verifier(Arc::new(NoVerify))
64+
.with_no_client_auth();
65+
let connector = TlsConnector::from(Arc::new(tls_cfg));
66+
67+
let sem = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY));
68+
let mut tasks = Vec::with_capacity(CANDIDATE_IPS.len());
69+
for ip in CANDIDATE_IPS {
70+
let sni = sni.clone();
71+
let connector = connector.clone();
72+
let sem = sem.clone();
73+
let ip = ip.to_string();
74+
tasks.push(tokio::spawn(async move {
75+
let _permit = sem.acquire().await.ok();
76+
probe(&ip, &sni, connector).await
77+
}));
78+
}
79+
80+
let mut results: Vec<Result_> = Vec::with_capacity(tasks.len());
81+
for t in tasks {
82+
if let Ok(r) = t.await {
83+
results.push(r);
84+
}
85+
}
86+
results.sort_by_key(|r| r.latency_ms.unwrap_or(u128::MAX));
87+
88+
println!("{:<20} {:>12} {}", "IP", "LATENCY", "STATUS");
89+
println!("{:-<20} {:->12} {}", "", "", "-------");
90+
let mut ok_count = 0usize;
91+
for r in &results {
92+
match r.latency_ms {
93+
Some(ms) => {
94+
println!("{:<20} {:>10}ms OK", r.ip, ms);
95+
ok_count += 1;
96+
}
97+
None => {
98+
let err = r.error.as_deref().unwrap_or("failed");
99+
println!("{:<20} {:>12} {}", r.ip, "-", err);
100+
}
101+
}
102+
}
103+
println!();
104+
println!("{} / {} reachable. Fastest:", ok_count, results.len());
105+
for r in results.iter().filter(|r| r.latency_ms.is_some()).take(3) {
106+
println!(" {} ({} ms)", r.ip, r.latency_ms.unwrap());
107+
}
108+
println!();
109+
if ok_count == 0 {
110+
println!("No Google IPs reachable from this network.");
111+
false
112+
} else {
113+
println!("To use the fastest, set \"google_ip\" in config.json to the top result above.");
114+
true
115+
}
116+
}
117+
118+
async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ {
119+
let start = Instant::now();
120+
let addr: SocketAddr = match format!("{}:443", ip).parse() {
121+
Ok(a) => a,
122+
Err(e) => {
123+
return Result_ {
124+
ip: ip.into(),
125+
latency_ms: None,
126+
error: Some(e.to_string()),
127+
}
128+
}
129+
};
130+
131+
let tcp = match tokio::time::timeout(PROBE_TIMEOUT, TcpStream::connect(addr)).await {
132+
Ok(Ok(t)) => t,
133+
Ok(Err(e)) => {
134+
return Result_ {
135+
ip: ip.into(),
136+
latency_ms: None,
137+
error: Some(format!("connect: {}", e)),
138+
}
139+
}
140+
Err(_) => {
141+
return Result_ {
142+
ip: ip.into(),
143+
latency_ms: None,
144+
error: Some("timeout".into()),
145+
}
146+
}
147+
};
148+
let _ = tcp.set_nodelay(true);
149+
150+
let server_name = match ServerName::try_from(sni.to_string()) {
151+
Ok(n) => n,
152+
Err(e) => {
153+
return Result_ {
154+
ip: ip.into(),
155+
latency_ms: None,
156+
error: Some(format!("bad sni: {}", e)),
157+
}
158+
}
159+
};
160+
161+
let mut tls = match tokio::time::timeout(PROBE_TIMEOUT, connector.connect(server_name, tcp)).await {
162+
Ok(Ok(t)) => t,
163+
Ok(Err(e)) => {
164+
return Result_ {
165+
ip: ip.into(),
166+
latency_ms: None,
167+
error: Some(format!("tls: {}", e)),
168+
}
169+
}
170+
Err(_) => {
171+
return Result_ {
172+
ip: ip.into(),
173+
latency_ms: None,
174+
error: Some("tls timeout".into()),
175+
}
176+
}
177+
};
178+
179+
let req = format!(
180+
"HEAD / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
181+
sni
182+
);
183+
if tls.write_all(req.as_bytes()).await.is_err() {
184+
return Result_ {
185+
ip: ip.into(),
186+
latency_ms: None,
187+
error: Some("write failed".into()),
188+
};
189+
}
190+
let _ = tls.flush().await;
191+
192+
let mut buf = [0u8; 256];
193+
match tokio::time::timeout(PROBE_TIMEOUT, tls.read(&mut buf)).await {
194+
Ok(Ok(n)) if n > 0 => {
195+
let elapsed = start.elapsed().as_millis();
196+
let head = String::from_utf8_lossy(&buf[..n.min(32)]);
197+
if head.starts_with("HTTP/") {
198+
Result_ {
199+
ip: ip.into(),
200+
latency_ms: Some(elapsed),
201+
error: None,
202+
}
203+
} else {
204+
Result_ {
205+
ip: ip.into(),
206+
latency_ms: None,
207+
error: Some(format!("bad reply: {:?}", head)),
208+
}
209+
}
210+
}
211+
Ok(Ok(_)) => Result_ {
212+
ip: ip.into(),
213+
latency_ms: None,
214+
error: Some("empty reply".into()),
215+
},
216+
Ok(Err(e)) => Result_ {
217+
ip: ip.into(),
218+
latency_ms: None,
219+
error: Some(format!("read: {}", e)),
220+
},
221+
Err(_) => Result_ {
222+
ip: ip.into(),
223+
latency_ms: None,
224+
error: Some("read timeout".into()),
225+
},
226+
}
227+
}
228+
229+
#[derive(Debug)]
230+
struct NoVerify;
231+
232+
impl ServerCertVerifier for NoVerify {
233+
fn verify_server_cert(
234+
&self,
235+
_end_entity: &CertificateDer<'_>,
236+
_intermediates: &[CertificateDer<'_>],
237+
_server_name: &ServerName<'_>,
238+
_ocsp_response: &[u8],
239+
_now: UnixTime,
240+
) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
241+
Ok(ServerCertVerified::assertion())
242+
}
243+
244+
fn verify_tls12_signature(
245+
&self,
246+
_: &[u8],
247+
_: &CertificateDer<'_>,
248+
_: &DigitallySignedStruct,
249+
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
250+
Ok(HandshakeSignatureValid::assertion())
251+
}
252+
253+
fn verify_tls13_signature(
254+
&self,
255+
_: &[u8],
256+
_: &CertificateDer<'_>,
257+
_: &DigitallySignedStruct,
258+
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
259+
Ok(HandshakeSignatureValid::assertion())
260+
}
261+
262+
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
263+
vec![
264+
SignatureScheme::RSA_PKCS1_SHA256,
265+
SignatureScheme::RSA_PKCS1_SHA384,
266+
SignatureScheme::RSA_PKCS1_SHA512,
267+
SignatureScheme::ECDSA_NISTP256_SHA256,
268+
SignatureScheme::ECDSA_NISTP384_SHA384,
269+
SignatureScheme::RSA_PSS_SHA256,
270+
SignatureScheme::RSA_PSS_SHA384,
271+
SignatureScheme::RSA_PSS_SHA512,
272+
SignatureScheme::ED25519,
273+
]
274+
}
275+
}

0 commit comments

Comments
 (0)