Three user-reported fixes / features in one release.
=== PR #9 — dynamic Google IP discovery (@v4g4b0nd-0x76) ===
Already merged in the previous commit. Opt-in via 'fetch_ips_from_api'
in config. Pulls goog.json from www.gstatic.com, maps it against
resolved IPs of well-known Google domains, samples from matching
CIDRs, and validates each candidate with gws / x-google / alt-svc
response-header checks. Graceful fallback to the static list if the
fetch fails or nothing passes validation. Default is off so existing
users are unaffected. Closes#10.
=== Issue #8 — OpenWRT: 'accept: No file descriptors available' ===
OpenWRT routers ship a very low RLIMIT_NOFILE (often 1024, sometimes
256 on constrained devices). A browser's burst of ~30 parallel sub-
resource requests can fill the limit within seconds, after which
accept(2) returns EMFILE and the proxy is effectively dead.
Two-fold fix:
1. New assets/openwrt/mhrv-rs.init now sets procd limits nofile=
"16384 16384" on the service. procd raises the per-process fd
limit before the binary even starts.
2. New src/rlimit.rs best-effort-raises RLIMIT_NOFILE in the binary
itself (Unix only, no new runtime deps — libc is already
transitively present via tokio). Targets 16384 soft, capped to
whatever hard limit the kernel already allows the user (so it
doesn't need root).
Both layers mean the fix applies whether the user runs via
/etc/init.d/mhrv-rs start (procd limits kick in)
or
./mhrv-rs --config ... (in-binary bump kicks in)
or any other invocation path.
Closes#8.
=== Issue #7 — Windows UI crashes silently ===
User report: on Win 11, run.bat prints 'Starting mhrv-rs UI...' and
exits clean, but no UI window ever appears. Root cause: the old
run.bat used 'start "" "mhrv-rs-ui.exe"' which returns
immediately — if the UI binary dies at launch time (missing GPU
driver, RDP without GL accel, AV blocking, …), the crash is invisible
because start already disowned the child.
Fix: run the UI in-place (not via 'start'), so its stderr and exit
code land in the run.bat cmd window. On non-zero exit print a helpful
checklist of common Windows launch failures and pause so the user can
screenshot the output for an issue report.
This doesn't fix the underlying crash for affected users, but it
turns a ghost-crash bug into a self-diagnosing one so the next report
includes actionable info. Closes-via-diag #7.
=== Fixes folded into the PR #9 merge ===
- src/scan_ips.rs: rand::thread_rng() held across an .await tripped
the Send bound on the async fn. Scoped the rng in a block so it
drops before the subsequent awaits.
- src/scan_ips.rs: defend /0 and /32 CIDRs in cidr_to_ips and
ip_in_cidr against 1u32 << 32 shift panic.
All 36 unit tests pass.
Two user-reported issues.
=== GLIBC too new (reported via twitter) ===
Our linux-amd64 and linux-arm64 gnu builds were compiled on
ubuntu-latest (24.04, GLIBC 2.39), which means the resulting binaries
refuse to load on anything older:
./mhrv-rs: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39'
not found (required by ./mhrv-rs)
Users on Ubuntu 22.04 / Mint 21 (GLIBC 2.35) — the typical user in Iran
where this project's target audience lives, and where they can't
dist-upgrade because they're behind exactly the kind of network
restriction this tool exists to bypass — could not run the gnu builds
at all.
Fix: pin the linux-gnu matrix entries to ubuntu-22.04 runners. GLIBC
2.35 is now the minimum; binaries load on Ubuntu 22.04, Mint 21,
Debian 12, Fedora 36+, RHEL 9+ and everything newer.
Users on older distros (Ubuntu 20.04, CentOS 7) can still use the
static musl builds (mhrv-rs-linux-musl-amd64.tar.gz et al.) which
have no GLIBC dependency at all.
=== Short-screen laptops — main window content clipped (PR #6) ===
Co-authored fix from @v4g4b0nd-0x76 in PR #6 (manually applied to
avoid pulling in 400 lines of unrelated cargo-fmt churn):
- Wrap the CentralPanel body in ScrollArea::vertical()
.auto_shrink([false; 2]) so everything stays reachable on short
screens.
- Lower the min_inner_size from [420, 540] to [420, 400] so laptops
with ~13" screens at default scaling can shrink the window without
clipping UI elements.
Closes#6.
Co-authored-by: v4g4b0nd-0x76 <v4g4b0nd-0x76@users.noreply.github.com>
New feature — users can now edit exactly which SNI names are rotated
through the outbound Google-edge tunnel, and probe each one's
reachability. Useful when an ISP selectively blocks individual Google
subdomains (e.g. mail.google.com in Iran at various times).
=== Data model ===
Config gains an optional 'sni_hosts' field:
"sni_hosts": ["www.google.com", "drive.google.com"]
Precedence in domain_fronter::build_sni_pool_for():
1. If sni_hosts is set & non-empty, use that list verbatim.
2. Else, if front_domain is one of the default Google-edge names,
auto-expand to {www, mail, drive, docs, calendar}.google.com.
3. Else, use just [front_domain].
Empty / all-disabled list saves as None so the backend falls back to
the defaults instead of having zero names to rotate through.
=== New scan_sni module ===
probe_one(ip, sni) / probe_all(ip, snis) does, for each candidate:
1. DNS lookup on the SNI (catches typos / non-existent names — Google
GFE returns a valid wildcard cert for ANY *.google.com, so the
TLS handshake alone can't tell apart a real name from gibberish).
2. TCP connect to google_ip:443 (3s timeout).
3. TLS handshake with the candidate SNI (3s timeout). RST mid-
handshake signals DPI block.
4. Small HTTP HEAD over the tunnel to confirm it's still speaking
HTTP (catches weird misroutes).
Returns ProbeResult { latency_ms, error } per candidate.
=== New 'test-sni' CLI subcommand ===
$ mhrv-rs test-sni
Probing 5 SNI candidates against google_ip=216.239.38.120 ...
SNI LATENCY STATUS
www.google.com 142 ms ok
drive.google.com 138 ms ok
mail.google.com - handshake RST (SNI may be blocked)
...
Working: 3 / 5
Exit 0 if >=1 passed, non-zero otherwise. Uses the same probe logic
the UI uses.
=== UI editor ===
New 'SNI pool... (active/total)' button in the main form, styled with
a solid blue fill + white text so it's clearly actionable. Opens a
floating egui::Window (resizable, movable, closable) with:
- Action bar: 'Test all' | 'Keep working only' | 'Enable all' |
'Clear status' | 'Reset to defaults'
- Scrollable list of rows, each: checkbox, monospaced name editor
(230px), status cell (150px, 'ok 142 ms' green / 'fail <reason>'
red / 'testing...' gray / 'untested' gray), per-row 'Test' and
'remove' buttons
- Bottom: text input + '+ Add' that auto-probes the newly added name
immediately (instead of leaving it silently 'untested')
All rendered with ASCII status text instead of unicode check/cross
glyphs, since egui's default font doesn't ship them on some hosts
and they rendered as a missing-glyph box.
Changes only commit when the user hits Save config in the main window;
probe state is held in UiState::sni_probe so it survives opening and
closing the editor.
=== README ===
English + Persian 'SNI pool editor' sections with the two workflows
(UI button + 'sni_hosts' config field), plus a 'test-sni' line added
to the Diagnostics section. Feature list updated.
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)
The Apps Script relay is HTTP-only, and the SNI-rewrite tunnel only
works for Google-hosted domains — so MTProto / IMAP / SSH / anything
else used to drop to a direct-TCP passthrough, which provides zero
circumvention. Users behind a DPI that blocks Telegram saw constant
disconnect/reconnect loops because the raw TCP ran right into the
block.
Fix: add an optional 'upstream_socks5' config field. When set, the
raw-TCP fallback chains the flow into that SOCKS5 proxy (typically a
local xray / v2ray / sing-box with a VLESS / Trojan / Shadowsocks
outbound to your own VPS) instead of connecting directly. The whole
rest of the pipeline is unchanged:
- HTTP / HTTPS still MITMs and relays via Apps Script
- SNI-rewrite suffixes (google.com, youtube.com, …) still hit the
direct Google-edge tunnel (so YouTube stays fast)
- Only the raw-TCP bucket (Telegram MTProto, SSH, IMAP, …) gets the
new upstream chain
Changes:
- config.rs: add Option<String> upstream_socks5 field
- proxy_server.rs: thread it through RewriteCtx; rewrite
plain_tcp_passthrough to call a new socks5_connect_via() helper
when configured, with graceful fallback to direct
- ui.rs: new 'Upstream SOCKS5' input with tooltip + placeholder,
ConfigWire round-trip
- README.md: new 'Pair with xray for Telegram' section (EN + FA)
with the architecture diagram and example config
Verified end-to-end in Docker: xray with the user's working VLESS
Reality config, mhrv-rs with upstream_socks5 pointing at it.
- HTTPS via mhrv-rs SOCKS5: origin = Google IP (Apps Script path) ✓
- Raw TCP to 3 Telegram DCs + api.telegram.org: all SOCKS5 rep=0, log
shows 'tcp via upstream-socks5 127.0.0.1:50529 -> …' ✓
- youtube.com / google.com: 'SNI-rewrite tunnel' (unchanged) ✓
- Real Telegram Desktop stayed connected cleanly (user-confirmed).
Follow-up to v0.4.3 — users asked for one-ID-per-line instead of
comma-separated, which is easier to edit in the UI. Field is now a
3-row textarea with a rolling hint underneath:
- 0 or 1 IDs: 'Tip: add more IDs for round-robin with auto-failover'
- 2+ IDs: 'N IDs — round-robin with auto-failover on quota'
The parser accepts both newlines and commas as separators, so existing
configs keep loading cleanly. Closes#1.
The backend already supported a comma-separated list of Apps Script IDs
with round-robin dispatch and automatic 10-minute sidelining on quota
errors (429 / 403 / 'quota' in body). But the UI label just said 'Apps
Script ID' (singular), so users didn't realize multiple IDs are
allowed.
- Rename the label to 'Apps Script ID(s)'
- Hover tooltip explaining comma-separated input and quota auto-failover
- Placeholder hint 'id1, id2, id3 …'
- Live count line under the field when 2+ IDs are entered
Requested by a user in Farsi: 'add the ability to set multiple script
IDs; switch to the next when one hits its limit'. The feature existed,
this just surfaces it.
The UI was creating its own DomainFronter instance and polling stats
from it, while traffic actually went through the ProxyServer's own
internal fronter. Result: stats grid stuck at zero even with traffic
flowing.
Fix: expose ProxyServer::fronter() and have the UI pick up that handle
once the server is built, instead of constructing a parallel fronter.
New bin 'mhrv-rs-ui' behind the 'ui' feature flag. CLI users pay
zero egui compile cost; UI users get a single static binary.
UI features:
- Config form (Apps Script ID, auth key, Google IP, front domain,
ports, log level, verify_ssl)
- Start/Stop buttons that spawn the proxy on a dedicated tokio thread
- Live stats (relay calls, failures, cache hit rate, bytes relayed,
blacklisted scripts) polled every ~700ms
- Test button (end-to-end relay probe)
- Install CA / Check CA buttons
- Recent log panel (last 200 lines)
- Dense, dark, utility-look: no emojis, no cards, no gradients
Architecture:
- Refactored crate into lib + two bins (mhrv-rs, mhrv-rs-ui).
src/lib.rs exposes all modules, main.rs uses them via 'use mhrv_rs::...'
- New src/data_dir.rs: platform-appropriate user data dir
(~/Library/Application Support/mhrv-rs on macOS,
~/.config/mhrv-rs on Linux, %APPDATA%\mhrv-rs on Windows).
CLI falls back to ./config.json for backward compat.
- CA moves to {data_dir}/ca/ca.crt (was ./ca/ca.crt).
- UI background thread owns the tokio runtime and proxy handle;
communicates with UI via std::mpsc commands + Arc<Mutex<UiState>>.
- macOS .app bundle: assets/macos/Info.plist template + build-app.sh
that assembles .app from the binary. Bundled into release zips.
- CI: Linux system libs (libxkbcommon, libwayland, libxcb*, libx11,
libgl, libgtk-3) installed on Ubuntu runners for eframe. aarch64
Linux UI is best-effort cross-compile. Windows MinGW, macOS native.
25 lib tests still pass. 5MB release UI binary on macOS.