Skip to content

Commit 3f0bbfd

Browse files
committed
v0.6.0: performance pack — pool prewarm, SNI rotation, per-site stats, parallel dispatch
Tier-1 perf changes from the brainstorm, all on by default except where they change semantics (parallel_relay is opt-in). Connection pool pre-warm (domain_fronter.rs): On startup, open 3 TLS connections to Google edge in parallel and park them in the pool. First user request skips the ~300-500 ms handshake cost. Best-effort: warm failures are logged at debug and ignored. Triggered from ProxyServer::run() in a fire-and-forget tokio spawn. SNI rotation (domain_fronter.rs): Replace the single sni_host String with a Vec<String> plus an atomic round-robin index. When front_domain is one of the known Google-edge subdomains, build_sni_pool() expands it to include the other four (www/mail/drive/docs/calendar.google.com), so outbound TLS connection counts get spread across names instead of concentrating on one. Custom front_domain values are preserved as the single entry (we can't verify siblings of a non-Google edge). Expanded SNI-rewrite suffix list (proxy_server.rs): Added gstatic.com, googleusercontent.com, googleapis.com, ggpht.com, ytimg.com, blogspot.com, blogger.com to the list of domains routed directly via the Google-edge tunnel instead of through the Apps Script relay. Bigger bypass = less UA-locking, less quota burn on static CDN content. Per-site stats (domain_fronter.rs + ui.rs): New HostStat struct {requests, cache_hits, bytes, total_latency_ns} tracked per URL host. Records on both cache hits and relay calls, not on SNI-rewrite bypasses (those never touch the fronter). UI renders a collapsible table under the existing stats grid with the top 60 hosts sorted by request count, showing req count, cache hit %, bytes, avg latency ms. Parallel script-ID dispatch (config.rs, domain_fronter.rs, ui.rs): New config field parallel_relay: u8 (default 0 = off). When >= 2 and there are enough non-blacklisted IDs, do_relay_with_retry fans out the request to N script instances concurrently via futures_util's select_ok, returns first success, cancels the rest. Kills long-tail latency when one Apps Script instance happens to be slow, at the cost of N× quota per request. UI exposes it as a DragValue 0-8. TCP_NODELAY audit (proxy_server.rs): Added the missing set_nodelay(true) call on the SNI-rewrite outbound TCP stream. All six TcpStream::connect sites in the user traffic path now disable Nagle. Expanded feature list in README, added futures-util dep, added unit tests for extract_host and build_sni_pool. Verified end-to-end locally: - Pool pre-warm log line appears on startup: 'pool pre-warmed with 3 connection(s)'. - Static asset hit 3x: first = 2.2s (Apps Script), 2-3 = 6ms (cache). - youtube.com / google.com: SNI-rewrite tunnel (unchanged). - All 28 unit tests pass. Deferred (not in this release, each needs its own cycle): - uTLS / Chrome fingerprint mimicry (TLS stack swap) - QUIC/HTTP3 transport (new transport) - ETag / If-None-Match revalidation (needs cache schema change) - JSON envelope gzip on request (needs Code.gs change) - Firebase Cloud Functions as alt backend (new architecture) - MSS clamp / TCP Fast Open (platform-specific, marginal)
1 parent 561cbad commit 3f0bbfd

7 files changed

Lines changed: 392 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"
@@ -44,6 +44,7 @@ h2 = "0.4"
4444
http = "1"
4545
flate2 = "1"
4646
directories = "5"
47+
futures-util = { version = "0.3", default-features = false, features = ["std"] }
4748

4849
# Optional UI dep: only pulled in when --features ui is set.
4950
eframe = { version = "0.28", default-features = false, features = [

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ This port focuses on the **`apps_script` mode** — the only one that reliably w
261261
- [x] Script IDs masked in logs (`prefix…suffix`) so `info` logs don't leak deployment IDs
262262
- [x] Desktop UI (egui) — cross-platform, no bundler needed
263263
- [x] Optional upstream SOCKS5 chaining for non-HTTP traffic (Telegram MTProto, IMAP, SSH…) so raw-TCP flows can be tunneled through xray / v2ray / sing-box instead of connecting directly. HTTP/HTTPS keeps going through the Apps Script relay.
264+
- [x] Connection pool pre-warm on startup (first request skips the TLS handshake to Google edge).
265+
- [x] Per-connection SNI rotation across a pool of Google subdomains (`www/mail/drive/docs/calendar.google.com`), so outbound connection counts aren't concentrated on one SNI.
266+
- [x] Optional parallel script-ID dispatch (`parallel_relay`): fan out a relay request to N script instances concurrently, return first success, kill p95 latency at the cost of N× quota.
267+
- [x] Per-site stats drill-down in the UI (requests, cache hit %, bytes, avg latency per host) for live debugging.
268+
- [x] OpenWRT / Alpine / musl builds — static binaries, procd init script included.
264269

265270
Intentionally **not** implemented (rationale included so future contributors don't spend cycles on them):
266271

src/bin/ui.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct UiState {
7070
running: bool,
7171
started_at: Option<Instant>,
7272
last_stats: Option<mhrv_rs::domain_fronter::StatsSnapshot>,
73+
last_per_site: Vec<(String, mhrv_rs::domain_fronter::HostStat)>,
7374
log: VecDeque<String>,
7475
ca_trusted: Option<bool>,
7576
last_test_ok: Option<bool>,
@@ -105,6 +106,7 @@ struct FormState {
105106
log_level: String,
106107
verify_ssl: bool,
107108
upstream_socks5: String,
109+
parallel_relay: u8,
108110
show_auth_key: bool,
109111
}
110112

@@ -139,6 +141,7 @@ fn load_form() -> FormState {
139141
log_level: c.log_level,
140142
verify_ssl: c.verify_ssl,
141143
upstream_socks5: c.upstream_socks5.unwrap_or_default(),
144+
parallel_relay: c.parallel_relay,
142145
show_auth_key: false,
143146
}
144147
} else {
@@ -153,6 +156,7 @@ fn load_form() -> FormState {
153156
log_level: "info".into(),
154157
verify_ssl: true,
155158
upstream_socks5: String::new(),
159+
parallel_relay: 0,
156160
show_auth_key: false,
157161
}
158162
}
@@ -208,6 +212,7 @@ impl FormState {
208212
let v = self.upstream_socks5.trim();
209213
if v.is_empty() { None } else { Some(v.to_string()) }
210214
},
215+
parallel_relay: self.parallel_relay,
211216
})
212217
}
213218
}
@@ -241,6 +246,12 @@ struct ConfigWire<'a> {
241246
hosts: &'a std::collections::HashMap<String, String>,
242247
#[serde(skip_serializing_if = "Option::is_none")]
243248
upstream_socks5: Option<&'a str>,
249+
#[serde(skip_serializing_if = "is_zero_u8")]
250+
parallel_relay: u8,
251+
}
252+
253+
fn is_zero_u8(v: &u8) -> bool {
254+
*v == 0
244255
}
245256

246257
#[derive(serde::Serialize)]
@@ -269,6 +280,7 @@ impl<'a> From<&'a Config> for ConfigWire<'a> {
269280
verify_ssl: c.verify_ssl,
270281
hosts: &c.hosts,
271282
upstream_socks5: c.upstream_socks5.as_deref(),
283+
parallel_relay: c.parallel_relay,
272284
}
273285
}
274286
}
@@ -382,6 +394,20 @@ impl eframe::App for App {
382394
.desired_width(f32::INFINITY));
383395
ui.end_row();
384396

397+
ui.label("Parallel dispatch")
398+
.on_hover_text(
399+
"Fire this many Apps Script IDs in parallel per relay request and\n\
400+
return the first successful response. 0/1 = off (round-robin).\n\
401+
Higher values eliminate long-tail latency (slow script instance\n\
402+
doesn't hold up the fast one) but spend that many times more\n\
403+
daily quota. Only effective with multiple IDs configured.\n\
404+
Recommend 2-3 if you have plenty of quota headroom."
405+
);
406+
ui.add(egui::DragValue::new(&mut self.form.parallel_relay)
407+
.speed(1)
408+
.range(0..=8));
409+
ui.end_row();
410+
385411
ui.label("Log level");
386412
egui::ComboBox::from_id_source("loglevel")
387413
.selected_text(&self.form.log_level)
@@ -415,9 +441,16 @@ impl eframe::App for App {
415441
ui.separator();
416442

417443
// Status + stats
418-
let (running, started_at, stats, ca_trusted, last_test_msg) = {
444+
let (running, started_at, stats, ca_trusted, last_test_msg, per_site) = {
419445
let s = self.shared.state.lock().unwrap();
420-
(s.running, s.started_at, s.last_stats, s.ca_trusted, s.last_test_msg.clone())
446+
(
447+
s.running,
448+
s.started_at,
449+
s.last_stats,
450+
s.ca_trusted,
451+
s.last_test_msg.clone(),
452+
s.last_per_site.clone(),
453+
)
421454
};
422455

423456
ui.horizontal(|ui| {
@@ -464,6 +497,41 @@ impl eframe::App for App {
464497
});
465498
}
466499

500+
if !per_site.is_empty() {
501+
ui.add_space(2.0);
502+
egui::CollapsingHeader::new(format!("Per-site ({} hosts)", per_site.len()))
503+
.default_open(false)
504+
.show(ui, |ui| {
505+
egui::ScrollArea::vertical()
506+
.max_height(140.0)
507+
.show(ui, |ui| {
508+
egui::Grid::new("per_site")
509+
.num_columns(5)
510+
.spacing([8.0, 2.0])
511+
.striped(true)
512+
.show(ui, |ui| {
513+
ui.label(egui::RichText::new("host").strong());
514+
ui.label(egui::RichText::new("req").strong());
515+
ui.label(egui::RichText::new("hit%").strong());
516+
ui.label(egui::RichText::new("bytes").strong());
517+
ui.label(egui::RichText::new("avg ms").strong());
518+
ui.end_row();
519+
for (host, st) in per_site.iter().take(60) {
520+
let hit_pct = if st.requests > 0 {
521+
(st.cache_hits as f64 / st.requests as f64) * 100.0
522+
} else { 0.0 };
523+
ui.label(egui::RichText::new(host).monospace());
524+
ui.label(egui::RichText::new(st.requests.to_string()).monospace());
525+
ui.label(egui::RichText::new(format!("{:.0}%", hit_pct)).monospace());
526+
ui.label(egui::RichText::new(fmt_bytes(st.bytes)).monospace());
527+
ui.label(egui::RichText::new(format!("{:.0}", st.avg_latency_ms())).monospace());
528+
ui.end_row();
529+
}
530+
});
531+
});
532+
});
533+
}
534+
467535
ui.add_space(4.0);
468536

469537
ui.horizontal(|ui| {
@@ -573,7 +641,10 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
573641
let f = slot.lock().await;
574642
if let Some(fronter) = f.as_ref() {
575643
let s = fronter.snapshot_stats();
576-
shared.state.lock().unwrap().last_stats = Some(s);
644+
let per_site = fronter.snapshot_per_site();
645+
let mut st = shared.state.lock().unwrap();
646+
st.last_stats = Some(s);
647+
st.last_per_site = per_site;
577648
}
578649
});
579650
}

src/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ pub struct Config {
6262
/// unaffected.
6363
#[serde(default)]
6464
pub upstream_socks5: Option<String>,
65+
/// Fan-out factor for non-cached relay requests when multiple
66+
/// `script_id`s are configured. `0` or `1` = off (round-robin, the
67+
/// default). `2` or more = fire that many Apps Script instances in
68+
/// parallel per request and return the first successful response —
69+
/// kills long-tail latency caused by a single slow Apps Script
70+
/// instance, at the cost of using that much more daily quota.
71+
/// Value is clamped to the number of available (non-blacklisted)
72+
/// script IDs.
73+
#[serde(default)]
74+
pub parallel_relay: u8,
6575
}
6676

6777
fn default_google_ip() -> String {

0 commit comments

Comments
 (0)