mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 23:54:48 +03:00
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.
This commit is contained in:
+42
-2
@@ -6,6 +6,8 @@ mod config;
|
||||
mod domain_fronter;
|
||||
mod mitm;
|
||||
mod proxy_server;
|
||||
mod scan_ips;
|
||||
mod test_cmd;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
@@ -25,6 +27,13 @@ struct Args {
|
||||
config_path: PathBuf,
|
||||
install_cert: bool,
|
||||
no_cert_check: bool,
|
||||
command: Command,
|
||||
}
|
||||
|
||||
enum Command {
|
||||
Serve,
|
||||
Test,
|
||||
ScanIps,
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
@@ -32,7 +41,9 @@ fn print_help() {
|
||||
"mhrv-rs {} — Rust port of MasterHttpRelayVPN (apps_script mode only)
|
||||
|
||||
USAGE:
|
||||
mhrv-rs [--config PATH] [--install-cert] [--no-cert-check]
|
||||
mhrv-rs [OPTIONS] Start the proxy server (default)
|
||||
mhrv-rs test [OPTIONS] Probe the Apps Script relay end-to-end
|
||||
mhrv-rs scan-ips [OPTIONS] Scan Google frontend IPs for reachability + latency
|
||||
|
||||
OPTIONS:
|
||||
-c, --config PATH Path to config.json (default: ./config.json)
|
||||
@@ -52,8 +63,24 @@ fn parse_args() -> Result<Args, String> {
|
||||
let mut config_path = PathBuf::from("config.json");
|
||||
let mut install_cert = false;
|
||||
let mut no_cert_check = false;
|
||||
let mut command = Command::Serve;
|
||||
|
||||
let mut it = std::env::args().skip(1);
|
||||
let mut raw: Vec<String> = std::env::args().skip(1).collect();
|
||||
if let Some(first) = raw.first() {
|
||||
match first.as_str() {
|
||||
"test" => {
|
||||
command = Command::Test;
|
||||
raw.remove(0);
|
||||
}
|
||||
"scan-ips" => {
|
||||
command = Command::ScanIps;
|
||||
raw.remove(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut it = raw.into_iter();
|
||||
while let Some(arg) = it.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => {
|
||||
@@ -77,6 +104,7 @@ fn parse_args() -> Result<Args, String> {
|
||||
config_path,
|
||||
install_cert,
|
||||
no_cert_check,
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,6 +164,18 @@ async fn main() -> ExitCode {
|
||||
|
||||
init_logging(&config.log_level);
|
||||
|
||||
match args.command {
|
||||
Command::Test => {
|
||||
let ok = test_cmd::run(&config).await;
|
||||
return if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE };
|
||||
}
|
||||
Command::ScanIps => {
|
||||
let ok = scan_ips::run(&config).await;
|
||||
return if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE };
|
||||
}
|
||||
Command::Serve => {}
|
||||
}
|
||||
|
||||
tracing::warn!("mhrv-rs {} starting (mode: apps_script)", VERSION);
|
||||
tracing::info!(
|
||||
"Apps Script relay: SNI={} -> script.google.com (via {})",
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::rustls::client::danger::{
|
||||
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
|
||||
};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
const CANDIDATE_IPS: &[&str] = &[
|
||||
"216.239.32.120",
|
||||
"216.239.34.120",
|
||||
"216.239.36.120",
|
||||
"216.239.38.120",
|
||||
"216.58.212.142",
|
||||
"142.250.80.142",
|
||||
"142.250.80.138",
|
||||
"142.250.179.110",
|
||||
"142.250.185.110",
|
||||
"142.250.184.206",
|
||||
"142.250.190.238",
|
||||
"142.250.191.78",
|
||||
"172.217.1.206",
|
||||
"172.217.14.206",
|
||||
"172.217.16.142",
|
||||
"172.217.22.174",
|
||||
"172.217.164.110",
|
||||
"172.217.168.206",
|
||||
"172.217.169.206",
|
||||
"34.107.221.82",
|
||||
"142.251.32.110",
|
||||
"142.251.33.110",
|
||||
"142.251.46.206",
|
||||
"142.251.46.238",
|
||||
"142.250.80.170",
|
||||
"142.250.72.206",
|
||||
"142.250.64.206",
|
||||
"142.250.72.110",
|
||||
];
|
||||
|
||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(4);
|
||||
const CONCURRENCY: usize = 8;
|
||||
|
||||
struct Result_ {
|
||||
ip: String,
|
||||
latency_ms: Option<u128>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn run(config: &Config) -> bool {
|
||||
let sni = config.front_domain.clone();
|
||||
println!("Scanning {} Google frontend IPs (SNI={}, timeout={}s)...", CANDIDATE_IPS.len(), sni, PROBE_TIMEOUT.as_secs());
|
||||
println!();
|
||||
|
||||
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::with_capacity(CANDIDATE_IPS.len());
|
||||
for ip in CANDIDATE_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 mut results: Vec<Result_> = Vec::with_capacity(tasks.len());
|
||||
for t in tasks {
|
||||
if let Ok(r) = t.await {
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
results.sort_by_key(|r| r.latency_ms.unwrap_or(u128::MAX));
|
||||
|
||||
println!("{:<20} {:>12} {}", "IP", "LATENCY", "STATUS");
|
||||
println!("{:-<20} {:->12} {}", "", "", "-------");
|
||||
let mut ok_count = 0usize;
|
||||
for r in &results {
|
||||
match r.latency_ms {
|
||||
Some(ms) => {
|
||||
println!("{:<20} {:>10}ms OK", r.ip, ms);
|
||||
ok_count += 1;
|
||||
}
|
||||
None => {
|
||||
let err = r.error.as_deref().unwrap_or("failed");
|
||||
println!("{:<20} {:>12} {}", r.ip, "-", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("{} / {} reachable. Fastest:", ok_count, results.len());
|
||||
for r in results.iter().filter(|r| r.latency_ms.is_some()).take(3) {
|
||||
println!(" {} ({} ms)", r.ip, r.latency_ms.unwrap());
|
||||
}
|
||||
println!();
|
||||
if ok_count == 0 {
|
||||
println!("No Google IPs reachable from this network.");
|
||||
false
|
||||
} else {
|
||||
println!("To use the fastest, set \"google_ip\" in config.json to the top result above.");
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe(ip: &str, sni: &str, connector: TlsConnector) -> Result_ {
|
||||
let start = Instant::now();
|
||||
let addr: SocketAddr = match format!("{}:443", ip).parse() {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
return Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let tcp = match tokio::time::timeout(PROBE_TIMEOUT, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(t)) => t,
|
||||
Ok(Err(e)) => {
|
||||
return Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some(format!("connect: {}", e)),
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some("timeout".into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
let _ = tcp.set_nodelay(true);
|
||||
|
||||
let server_name = match ServerName::try_from(sni.to_string()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
return Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some(format!("bad sni: {}", 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()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some("write failed".into()),
|
||||
};
|
||||
}
|
||||
let _ = tls.flush().await;
|
||||
|
||||
let mut buf = [0u8; 256];
|
||||
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/") {
|
||||
Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: Some(elapsed),
|
||||
error: None,
|
||||
}
|
||||
} else {
|
||||
Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some(format!("bad reply: {:?}", head)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Ok(_)) => Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some("empty reply".into()),
|
||||
},
|
||||
Ok(Err(e)) => Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some(format!("read: {}", e)),
|
||||
},
|
||||
Err(_) => Result_ {
|
||||
ip: ip.into(),
|
||||
latency_ms: None,
|
||||
error: Some("read timeout".into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoVerify;
|
||||
|
||||
impl ServerCertVerifier for NoVerify {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: &CertificateDer<'_>,
|
||||
_: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: &CertificateDer<'_>,
|
||||
_: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::domain_fronter::DomainFronter;
|
||||
|
||||
const TEST_URL: &str = "https://api.ipify.org/?format=json";
|
||||
|
||||
pub async fn run(config: &Config) -> bool {
|
||||
let fronter = match DomainFronter::new(config) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("FAIL: could not create fronter: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
println!("Probing relay end-to-end...");
|
||||
println!(" front_domain : {}", config.front_domain);
|
||||
println!(" google_ip : {}", config.google_ip);
|
||||
println!(" test URL : {}", TEST_URL);
|
||||
println!();
|
||||
|
||||
let t0 = Instant::now();
|
||||
let resp = fronter.relay("GET", TEST_URL, &[], &[]).await;
|
||||
let elapsed = t0.elapsed();
|
||||
|
||||
let resp_str = String::from_utf8_lossy(&resp);
|
||||
let status_line = resp_str.lines().next().unwrap_or("").to_string();
|
||||
let body_start = resp_str.find("\r\n\r\n").map(|p| p + 4).unwrap_or(0);
|
||||
let body = &resp_str[body_start..];
|
||||
|
||||
println!("Response in {}ms:", elapsed.as_millis());
|
||||
println!(" status : {}", status_line);
|
||||
let body_trunc: String = body.chars().take(500).collect();
|
||||
println!(" body : {}", body_trunc);
|
||||
println!();
|
||||
|
||||
let ok = status_line.contains("200 OK");
|
||||
if ok {
|
||||
println!("PASS: relay round-trip successful.");
|
||||
if body.contains("\"ip\"") {
|
||||
println!(" returned an IP address — end-to-end verified.");
|
||||
}
|
||||
true
|
||||
} else if status_line.contains("502") || status_line.contains("504") {
|
||||
println!("FAIL: gateway error. Likely causes:");
|
||||
println!(" - Apps Script deployment ID is wrong");
|
||||
println!(" - auth_key doesn't match Code.gs AUTH_KEY");
|
||||
println!(" - Google IP / front_domain unreachable from this network");
|
||||
println!(" - Apps Script has hit its daily quota (try a different script_id)");
|
||||
false
|
||||
} else {
|
||||
println!("FAIL: unexpected status");
|
||||
false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user