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 domain_fronter;
|
||||||
mod mitm;
|
mod mitm;
|
||||||
mod proxy_server;
|
mod proxy_server;
|
||||||
|
mod scan_ips;
|
||||||
|
mod test_cmd;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
@@ -25,6 +27,13 @@ struct Args {
|
|||||||
config_path: PathBuf,
|
config_path: PathBuf,
|
||||||
install_cert: bool,
|
install_cert: bool,
|
||||||
no_cert_check: bool,
|
no_cert_check: bool,
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Command {
|
||||||
|
Serve,
|
||||||
|
Test,
|
||||||
|
ScanIps,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
@@ -32,7 +41,9 @@ fn print_help() {
|
|||||||
"mhrv-rs {} — Rust port of MasterHttpRelayVPN (apps_script mode only)
|
"mhrv-rs {} — Rust port of MasterHttpRelayVPN (apps_script mode only)
|
||||||
|
|
||||||
USAGE:
|
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:
|
OPTIONS:
|
||||||
-c, --config PATH Path to config.json (default: ./config.json)
|
-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 config_path = PathBuf::from("config.json");
|
||||||
let mut install_cert = false;
|
let mut install_cert = false;
|
||||||
let mut no_cert_check = 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() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
@@ -77,6 +104,7 @@ fn parse_args() -> Result<Args, String> {
|
|||||||
config_path,
|
config_path,
|
||||||
install_cert,
|
install_cert,
|
||||||
no_cert_check,
|
no_cert_check,
|
||||||
|
command,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +164,18 @@ async fn main() -> ExitCode {
|
|||||||
|
|
||||||
init_logging(&config.log_level);
|
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::warn!("mhrv-rs {} starting (mode: apps_script)", VERSION);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Apps Script relay: SNI={} -> script.google.com (via {})",
|
"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