Fixes#1047. `_doSingle`'s normal-relay path (cache disabled or cache miss on
non-cachable request) ran `UrlFetchApp.fetch` → `getContent` → `base64Encode`
with no error wrapper. Any throw — most commonly when the response body
approaches Apps Script's ~50 MB ceiling and `base64Encode` blows the V8 heap,
also URL-too-long / payload-too-large / quota exhaustion / 6-minute execution
timeout — propagated unhandled, and Apps Script served its default
`<title>Web App</title>` HTML error page in place of the JSON envelope.
The Rust client (`parse_relay_json` in `domain_fronter.rs`) then failed to
find JSON and surfaced the cryptic `bad response: no json in: <!DOCTYPE html>...`
with no signal as to the actual cause.
The reporter's symptom — a single failing host (`shc-dist.lostsig.co`,
sonichacking.org) serving large ROM-hack binaries — matches this exactly.
Every other download worked because they were all under the body-size
ceiling.
## Fix
Wrap the normal-relay block in `_doSingle` with
`try { ... } catch (err) { return _json({ e: "fetch failed: " + String(err) }); }`.
Mirrors the per-item try/catch already present in `_doBatch`. Turns the
silent HTML crash into a structured `FronterError::Relay("fetch failed: …")`
on the client side that pinpoints the real underlying error.
Cache path intentionally untouched:
- `_fetchAndCache` already wraps its own fetch in try/catch and returns
`null` on any failure (so `_doSingle` falls through cleanly to the
normal relay).
- The cached-read path is bounded to ≤ `CACHE_MAX_BODY_BYTES` (35 KB)
so it cannot trip the size limits that caused this bug.
## Verified locally on top of v1.9.22
- `node --check assets/apps_script/Code.gs`: clean ✅
- `cargo test --lib --release`: 209/209 ✅ (sanity — no Rust change)
Reviewed via Anthropic Claude.
Co-Authored-By: dazzling-no-more <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multiple Iran-ISP users (#924 Recruit1992, #913 ehsan272727) report that
ngrok's free tier now exclusively hands out *.ngrok-free.dev domains for
new accounts, with no path to claim the older *.ngrok-free.app TLD. Some
Iran ISPs (TCI, Irancell, IRMCI confirmed) block *.ngrok-free.dev at DNS
or TCP. Symptom: curl from Iran network to ngrok URL times out, but works
from non-Iran.
Updates README.md and ngrok.md to:
1. Note the ngrok TLD shift (.app grandfathered, .dev for new accounts).
2. List ISPs confirmed to block *.ngrok-free.dev.
3. Add an "Alternative hosts" section recommending HuggingFace Spaces
(Docker SDK) as the most Iran-friendly option in 2026 — permanent
*.hf.space URL with no tunnel layer.
4. Update the URL behavior column for Method 2 since ngrok now gives a
permanent dev domain by default (not "new URL each session").
No code changes — docs only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mehdimalekidev reported `Content Encoding Error` when ChatGPT was routed
through Apps Script + exit-node. Root cause:
1. Browser → Apps Script with `Accept-Encoding: gzip, br` (default).
2. Apps Script forwards header to exit-node.
3. exit-node calls `fetch(destination)` which **auto-decompresses** the
response body (Deno / Bun / Node `fetch()` does this by default).
4. `resp.arrayBuffer()` returns **plain decompressed bytes** but
`resp.headers` still has `Content-Encoding: gzip` from the destination.
5. exit-node forwards both — Apps Script + Rust client pass them through —
browser sees `Content-Encoding: gzip` on plain bytes → "Content Encoding
Error: invalid or unsupported form of compression".
Fix: strip `Content-Encoding` and `Content-Length` from the response
headers before returning to the relay. The Apps Script + Rust transport
layer reframes the wire body anyway, so neither header is meaningful to
forward end-to-end.
Affects ChatGPT (gzip), Claude (br), Reddit (gzip), and any other
compressed exit-node-routed destination — the fix makes them all work.
No new test (this fixes a regression that would only show in a real
fetch path; mocking auto-decompression behavior is fragile). Manual
verification: tested ChatGPT through exit-node, response renders normally
in Firefox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three orthogonal cache-correctness wins on `Code.gs`'s spreadsheet response
cache. All compound: more URLs become cacheable, the cache stops being
poisoned by transient outages, and 4xx storms cost zero quota.
**1. Gzip body before base64 storage** (new `Z` column, base64(gzip(rawBytes))
when worthwhile, skipped when already encoded or under 256 bytes). 3-5×
compression on text bodies — many ~100-150 KB responses now fit under the
35 KB cell ceiling that previously rejected them. `_getFromCache`
decompresses on hit and re-encodes for the wire — relay protocol's `b`
field stays `base64(rawBytes)`, no client change.
**2. 5xx never enters the cache.** A flapping upstream that returned 503
once was previously pinned for 24h, breaking the URL for every subsequent
client. `status >= 500` now early-returns the live response without
writing.
**3. Negative caching for persistent 4xx** (404/410/451 → 5-minute TTL when
upstream is silent on Cache-Control). Long enough to absorb favicon /
telemetry / dev-tools-probe storms; short enough that transient 404s
self-heal. Origin-stated max-age still wins when present.
**Schema migration**: cache sheet grows 7 → 8 columns (added `Z` flag).
Existing 7-column rows read back with `Z = undefined` (falsy → falls
through the not-gzipped branch) — fully backward-compatible, no user
action required.
**Verified locally** on top of v1.9.18 / main:
- `node --check Code.gs` clean
- `cargo build --bins --lib` clean
- `cargo test --lib --release`: 208/208 (no client-side change so this is
a sanity check only)
Behavior changes flagged in the PR body for users relying on the old
"everything cached for 24h" default — the 5xx-never-cached fix is
intentional, the 4xx 5-min default is overrideable via origin
Cache-Control.
Squash-merging.
Reviewed via Anthropic Claude.
Co-Authored-By: dazzling-no-more <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@BehroozAmoozad reported the docs reference a wrapper.ts file that
doesn't exist in the repo — the README.md / README.fa.md "Your own VPS"
row pointed at a wrapper.ts the user was supposed to write themselves.
For non-JavaScript users, this was a hard blocker.
Adds assets/exit_node/wrapper.ts that:
- Imports the handler from exit_node.ts directly (so editing the PSK in
exit_node.ts is the only setup step).
- Auto-detects Deno, Bun, or Node 22+ at runtime and uses the matching
HTTP server primitive (Deno.serve / Bun.serve / node:http).
- Reads PORT, HOST, CERT_FILE, KEY_FILE from env.
- Defaults to plain HTTP on port 8443 and recommends a reverse proxy
(Caddy / nginx / Cloudflare Tunnel) for TLS termination — the path
most VPS setups already have.
- Optional standalone TLS via CERT_FILE + KEY_FILE for users without
a reverse proxy.
README.md + README.fa.md updated to link the file directly and adjust
the deno run command to include the new --allow-env / --allow-read
permissions the wrapper needs.
No code changes — assets-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Montazeran8 noticed two stale doc claims in the ngrok tunnel guide:
1. ngrok.md Step 8 told users to run `mhrv-rs test` to verify a Full-mode
tunnel — but `mhrv-rs test` is wired for the apps_script relay path only
and refuses to run in Full mode. Fixed to direct users to ipleak.net /
whatismyipaddress.com instead.
2. ngrok.md "Renewing the Tunnel" + "Limitations" sections claimed the
*.ngrok-free.app URL changes every run. ngrok's free tier now ships with
a default static domain per account, so the URL stays the same across
runs once assigned. Updated both sections to distinguish static-domain
accounts (no CodeFull.gs redeploy needed) from older accounts that opted
out.
3. README.md "Limitations" + "After Starting the Tunnel" sections updated
to reflect that only Method 1 (cloudflared Quick) has truly volatile URLs.
Method 2 (ngrok) keeps the same URL on accounts with a static domain.
No code changes — doc-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apps Script's outbound runs from Google datacenter IPs, which Cloudflare's anti-bot heuristics flag as bots and serves a 403 / Persian Google Docs error on the Apps Script → trycloudflare.com / your-CF-domain step. This blocks both Method 1 (cloudflared Quick) and Method 3 (cloudflared Named) from Iran ISP per #849's reproducible report. ngrok (Method 2) doesn't go through CF edge so it works.
Updated the methods table with a "Iran ISP friendly?" column + a callout block above explaining the failure mode + recommends Method 2 (ngrok) as the starting point for Iran-based users. Methods 1 and 3 stay documented for completeness — they DO work on networks where CF's anti-bot doesn't fire against Google datacenter IPs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The val.town founder asked us not to promote using their service. This
commit removes every val.town reference from the codebase and rewrites
the exit-node guides to be platform-agnostic.
Changes:
- Renamed assets/exit_node/valtown.ts → assets/exit_node/exit_node.ts.
TypeScript itself is unchanged — same web-standard Request/Response/
fetch API that runs on any serverless runtime.
- Rewrote assets/exit_node/README.md and README.fa.md to recommend
Deno Deploy as the primary host for users who want a free serverless
TS endpoint, with fly.io and your-own-VPS as alternatives. CF Workers
is explicitly called out as not-helpful (CF outbound is still on
CF's flagged IP space).
- Updated all val.town mentions in source comments (src/config.rs,
src/domain_fronter.rs, src/bin/ui.rs) to neutral wording.
- Updated config.exit-node.example.json `_comment` strings and the
example URL.
- Updated main README.md FAQ entries (Persian + English) and
docs/guide.md / docs/guide.fa.md.
- Old changelog files (v1.9.4 / v1.9.5 / v1.9.9) had val.town mentions
retroactively replaced too — same redaction principle.
- Bumped to v1.9.10 with a changelog noting the rename + Telegram
channel brief format from earlier today.
Users who already have an exit node deployed (on whichever host they
picked) don't need to change anything — the wire protocol is identical
and the renamed script is byte-identical to the old one.
Tests: 179 lib + 35 tunnel-node green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-side (Apps Script) fixes — users replace their Code.gs with assets/apps_script/Code.gs (or CodeFull.gs for full mode) and Manage deployments → ✏️ → New version → Deploy:
- Removed duplicate doGet in Code.gs (HtmlService one was overriding ContentService one due to JS hoisting → every GET to /exec returned a goog.script.init iframe instead of the placeholder HTML)
- CodeFull.gs doGet switched from HtmlService to ContentService (same reason)
- SKIP_HEADERS now strips X-Forwarded-* / Forwarded / Via family — second line of defense to v1.2.9's client-side stripping (#104), in case a misconfigured upstream proxy adds these
- _doBatch fallback when UrlFetchApp.fetchAll() throws as a whole — per-item fetch on safe methods so one bad URL no longer poisons the entire batch (port from masterking32@3094288)
Client-side (Rust) defense-in-depth:
- parse_relay_json now unwraps goog.script.init("...userHtml...") if any deployment returns the iframe-wrapped form (legacy Code.gs, or a redirect that GETs doGet). New extract_apps_script_user_html + decode_js_string_escapes helpers. Tested against a real deployment's doGet response.
Docs:
- README rewritten as short bilingual landing page (English + Persian RTL) targeting normal users; advanced reference moved to docs/guide.md + docs/guide.fa.md.
Tests: 3 new regression tests. 176 lib + 33 tunnel-node tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes addressing user-reported issues today:
1. Exit-node feature ported from upstream masterking32@464a6e1d, with
hardening. Cloudflare-protected sites (chatgpt.com, claude.ai,
grok.com, x.com, openai.com) flag Google datacenter IPs as bots and
return Turnstile / CAPTCHA / 502 challenges. Apps Script's UrlFetchApp
exits from those IPs, so v1.9.3 surfaces these as "Relay error: json:
key must be a string..." with no apps_script-mode workaround.
Now a small TypeScript HTTP endpoint (assets/exit_node/valtown.ts)
deployed on val.town / Deno Deploy sits between Apps Script and the
destination. Chain: client → Apps Script (Google IP) → val.town
(non-Google IP) → destination. Destination sees val.town's IP, no
CF challenge.
Config:
"exit_node": {
"enabled": true,
"relay_url": "https://...web.val.run",
"psk": "<openssl rand -hex 32>",
"mode": "selective",
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
}
Hardening over upstream: PSK fail-closed if still placeholder (fresh
deploy can't be open relay), loop guard (refuses fetch of own host),
explicit 503 on misconfigured. Fallback to direct Apps Script on exit
node failure (CF-affected sites fail, others keep working). Setup
docs in English + Persian at assets/exit_node/README*.md. Example
config at config.exit-node.example.json.
2. Removed the legacy `telegram` job from release.yml. With
TELEGRAM_NOTIFY_ENABLED repo var set to true, every release was
producing two duplicate APK posts on the main Telegram channel: the
old bundled-APK-on-main job AND the newer per-file files-channel
posts (telegram-publish-files.yml). Only the per-file flow is wanted.
Legacy job and its helper telegram_release_notify.py are gone.
Recoverable from git log if anyone needs the bundled pattern back.
169 mhrv-rs lib tests + 33 tunnel-node tests + UI build clean.
Four small fixes that address recurring user-issue patterns:
- src/config.rs / src/domain_fronter.rs: auto_blacklist_strikes,
auto_blacklist_window_secs, auto_blacklist_cooldown_secs config fields
(#391, #444). Previously 3 strikes / 30s window / 120s cooldown were
hard-coded. Single-deployment users on flaky networks hit this too
aggressively; multi-deployment users want tighter fail-fast. Defaults
preserve historical behavior. Power-user file edit only — no UI
control yet. Clamps to [1, 86400] for durations.
- src/config.rs / src/domain_fronter.rs / src/tunnel_client.rs:
request_timeout_secs config field (#430, masterking32 PR #25).
Replaces hard-coded BATCH_TIMEOUT 30s. DomainFronter::batch_timeout()
exposes the value, fire_batch reads it. Clamped to [5s, 300s].
- tunnel-node/src/main.rs: detect MHRV_AUTH_KEY env var being set
while TUNNEL_AUTH_KEY is unset, and emit a specific warning pointing
at the right env var name. Catches the recurring #391/#444 docker
run typo that made users chase phantom AUTH_KEY-mismatch decoys.
- assets/launchers/run.bat: when both UI renderers (glow + wgpu)
fail on older Windows / RDP / VM-without-GPU, fall back to launching
mhrv-rs.exe (CLI) instead of just printing "open an issue".
Addresses #417 / #426 / #487. CLI has the same proxy functionality
on 127.0.0.1:8085 (HTTP) / :8086 (SOCKS5).
169 mhrv-rs lib tests + 33 tunnel-node tests still passing. UI build
clean. ConfigWire round-trips the new fields with skip-default-on-write
so unchanged configs stay clean.
Five user-visible changes shipping together. Each is independently
useful + bounded; bundled because they're all "small architectural
hardening" that benefits from one release announcement.
1. Random payload padding (#313, #365 §1)
Every outbound Apps Script JSON request now carries a `_pad` field
of uniform-random length 0..1024 bytes (base64). Defeats DPI that
fingerprints on the tight length distribution of mhrv-rs's previous
per-mode-bound packet sizes. ~25% bandwidth on a typical 2 KB batch,
negligible against Apps Script's per-call latency floor. Backward-
compatible — old `Code.gs` deployments ignore the unknown field.
Applied at all three payload-build sites: single relay, single
tunnel op, batch tunnel.
2. Active-probing decoy: GAS bad-auth → 200 HTML (#365 §3)
`Code.gs` and `CodeFull.gs` now return a benign Apps-Script-style
placeholder HTML page on bad/missing AUTH_KEY instead of the JSON
`{"e":"unauthorized"}`. To an active scanner the deployment looks
like one of the millions of forgotten public Apps Script projects
rather than an obvious API endpoint. New `DIAGNOSTIC_MODE` const
restores JSON errors during setup; default false (production-strong).
3. Active-probing decoy: tunnel-node bad-auth → 404 nginx (#365 §3)
`tunnel-node` returns an HTTP 404 with an nginx-style HTML body on
bad auth instead of `{"e":"unauthorized"}`. Active scanners cataloging
the host see "static web server, nothing tunnel-shaped here." New
`MHRV_DIAGNOSTIC=1` env var restores verbose JSON during setup.
4. Fix: Full-mode usage counter stuck at zero (#230, #362)
`today_calls` / `today_bytes` were only being incremented on the
apps_script-mode relay path. Full-mode batches go through
`tunnel_client::fire_batch` which never wired into the counter.
Now `fire_batch` calls `record_today(response_bytes)` after each
successful batch — bytes estimated from the `d` (TCP payload) and
`pkts` (UDP datagrams) sizes in the BatchTunnelResponse. Full-mode
users now see real usage numbers.
5. Fix: quota reset countdown was UTC, should be PT (#230, #362)
Apps Script's UrlFetchApp daily quota resets at midnight Pacific
Time, not UTC. We were displaying the countdown to UTC midnight,
off by 7-8h depending on DST. New `current_pt_day_key()` and
`seconds_until_pacific_midnight()` helpers with hand-rolled US DST
detection (2nd Sunday March → 1st Sunday November = PDT, else PST)
so we don't pull `chrono-tz` and a ~3 MB IANA tzdb just for one
helper. UI label "UTC day" → "PT day". Tests pin DST window
boundaries against March/November of 2024, 2026, 2027 to catch
regressions in the day-of-week math.
Tested:
- cargo test --lib: 154 passed (was 152, +2 for DST window + day-of-week)
- cargo build --release: clean
- cargo build --release --bin mhrv-rs-ui --features ui: clean (macOS arm64)
- tunnel-node cargo test: 30 passed
- Android: ./gradlew assembleDebug succeeds; APK installs + launches
on mhrv_test emulator (arm64-v8a), no UnsatisfiedLink, no crash
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds end-to-end UDP support: SOCKS5 client UDP ASSOCIATE → tunnel-mux
udp_open/udp_data ops → tunnel-node UDP sessions → real UDP to upstream.
QUIC/HTTP3, DNS, and STUN now traverse full mode without falling back to
TCP or leaking outside the tunnel.
Apps Script proxies the new ops opaquely through the existing batch
endpoint; CodeFull.gs only gets a doc-comment update.
Highlights:
- proxy_server.rs: SOCKS5 UDP ASSOCIATE handler with per-session task,
bounded uplink mpsc channel, adaptive empty-poll backoff (500 ms → 30 s),
source-IP validation against the control TCP peer, port-locking on
first valid datagram, and self-removal from the dispatch map on eof.
- tunnel_client.rs: UdpOpen / UdpData / close_session mux variants
alongside the existing TCP plumbing; pkts decoder helper.
- tunnel-node: UdpSessionInner with bounded VecDeque queue, drop-oldest
on overflow with queue_drops counter and warn-then-throttled logs,
last_active refreshed only on real activity (uplink send or upstream
recv — empty polls do not refresh), independent TCP/UDP drain in
handle_batch Phase 2, separate active-drain (150 ms) and retry
(250 ms) windows for UDP, idle long-poll (5 s).
- Tests: SOCKS5 UDP packet parser (IPv4/IPv6/DOMAIN round-trips,
truncation rejects, fragmented rejects), UDP queue overflow drop +
counter, regression test that batch with both UDP and TCP-data ops
still runs the TCP retry pass.
Docs: README + android.{md,fa.md} updated to reflect UDP availability
in full mode; tunnel-node/README documents the new ops.
Standalone Rust/axum HTTP server + Apps Script-side CodeFull.gs for users who want to deploy a remote tunnel node. All new files; no changes to the main Rust crate. This is part 1 of 3 of the full-tunnel feature — it adds scaffolding that users can opt into once the Rust-side Mode::Full lands in #94.
Major feature release across Android + desktop. Six items the user
asked for, verified end-to-end on the emulator.
Android
-------
* Unified Connect/Disconnect button. Single large button swaps
between green "Connect" (when the service is down) and red
"Disconnect" (when it's up). Tracks the real service state via a
new process-wide `VpnState` singleton flipped from the service's
startEverything() / teardown() — not optimistic, the button only
reports what the service actually did.
* Connection mode dropdown (issue #37). Two options: VPN (TUN) —
routes every app — and Proxy only — user configures per-app via
Wi-Fi proxy to 127.0.0.1:8080 (HTTP) / :1081 (SOCKS5). PROXY_ONLY
skips VpnService.prepare() entirely (no OS VPN grant prompt) and
the service just keeps the foreground listeners up. Default is
VPN_TUN so existing behaviour is preserved for users who upgrade
without looking at the dropdown.
* App splitting. In VPN_TUN mode you can pick All / Only selected /
All except selected, with a picker dialog that lists installed
user-visible apps (LazyColumn with search, "show system apps"
toggle, multi-select checkboxes). ONLY calls
`Builder.addAllowedApplication()` for each chosen package;
EXCEPT calls `addDisallowedApplication()` additive to the
mandatory self-exclude. Requires QUERY_ALL_PACKAGES — added to
the manifest along with a `<queries>` launcher-intent filter so
the picker rows can render app labels, not just package strings.
* Persian/English UI toggle with RTL. Top-bar TextButton cycles
AUTO → FA → EN → AUTO. Persian strings live in
`res/values-fa/strings.xml`; English in `res/values/strings.xml`.
`AppCompatDelegate.setApplicationLocales()` is used as the
persistence layer (plus `AppLocalesMetadataHolderService` meta
and `locales_config.xml` for the per-app-language OS entry on
API 33+). MainActivity overrides `attachBaseContext` to wrap the
context with the right locale at the earliest possible moment —
otherwise a saved preference wouldn't apply until the SECOND
process after toggling. RTL swaps automatically because Persian
is script="Arab" in Android's locale database.
* Collapsible How-to-use card. The big instruction block that used
to dominate the bottom of the screen now lives inside a
CollapsibleSection that starts expanded for a fresh install
(empty deployment URLs / auth_key) and collapsed otherwise.
* Update check auto-fires on first composition, silent-on-up-to-date,
snackbar-only-if-available. Still surfaces via the version badge
tap for manual checks.
* MhrvVpnService teardown guard was kept from v1.0.2 —
`AtomicBoolean` makes the second caller a no-op, which is the
SIGSEGV fix for "tap Stop, app closes" from before. Stress-tested
under rapid Connect/Disconnect cycles.
Desktop
-------
* Fix: Advanced section silently resetting on every Save. `ConfigWire`
was missing `fetch_ips_from_api` / `max_ips_to_scan` /
`scan_batch_size` / `google_ip_validation` — every persist dropped
them, every reload fell back to the serde defaults, user saw their
Advanced toggles reset. Added the fields to the wire struct (issue
surfaced by the user as "Advanced resets after reopening the app").
* Windows renderer fallback (issue #28). `eframe` is now built with
BOTH `glow` (OpenGL 2+) and `wgpu` (DX12/Vulkan/Metal); runtime
defaults to glow for compat but honours `MHRV_RENDERER=wgpu` for
boxes that crash with "egui_glow requires opengl 2.0+" — old
Windows hardware, RDP sessions, VMs without GPU acceleration.
`run.bat` auto-retries the UI with `MHRV_RENDERER=wgpu` if the
first launch exits non-zero, so users don't need to know about
the flag.
CI
--
* Added OpenWRT mipsel-softfloat build target (issue #26). MT7621
routers specifically need soft-float because the CPU has no FPU;
a hard-float binary segfaults on first fp op. Built via
`messense/rust-musl-cross:mipsel-musl-softfloat` docker image +
nightly Rust with `-Z build-std` (mipsel is Rust tier 3 since
1.72, no pre-built std). Marked `continue-on-error: true` — the
tier-3 target occasionally regresses and we'd rather ship the
rest of the release than block on MT7621 support.
Signature / versioning
----------------------
* versionCode 110, versionName 1.1.0; Cargo bumped to 1.1.0.
* Release APK signed with the committed `release.jks` (same as
v1.0.2), so v1.0.2 → v1.1.0 upgrades install in-place without
the uninstall-first dance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two reasons to pin a copy in the repo:
1. Users on networks where raw.githubusercontent.com is intermittent
can still get the deploy-ready file via a repo ZIP / clone.
2. The Apps Script relay protocol between mhrv-rs and Code.gs is
informal — upstream changes can silently break us. Keeping a
snapshot lets future-us diff against what we tested against
when diagnosing protocol-drift bugs.
Fetched verbatim from:
https://raw.githubusercontent.com/masterking32/MasterHttpRelayVPN/refs/heads/python_testing/apps_script/Code.gs
Credit stays with @masterking32. The assets/apps_script/README.md
next to it calls out that we don't modify this file — users deploy
it as-is into their own Google Apps Script project.
Updated the Setup Guide link in both the English and Persian
sections so offline / restricted-network users have a fallback path.
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.
A user on OpenWRT x86_64 reported the linux release doesn't run there —
root cause was glibc vs musl mismatch (our gnu binary was looking for a
dynamic linker that doesn't exist on router userlands). Add two musl
targets that produce fully static PIE binaries:
- x86_64-unknown-linux-musl -> mhrv-rs-linux-musl-amd64.tar.gz
- aarch64-unknown-linux-musl -> mhrv-rs-linux-musl-arm64.tar.gz
CI uses the messense/rust-musl-cross docker images (better-maintained
than cargo-zigbuild with a pinned zig, which has version regressions
on the ar wrapper between 0.13 and 0.16).
Locally verified:
- both archs cross-compile green in docker
- resulting x86_64 binary (3.3 MB) runs in an alpine:latest container,
--version / --help work, no dynamic lib requirements
The musl archive skips the UI (routers are headless) and swaps run.sh
for a procd init script (assets/openwrt/mhrv-rs.init) expecting the
binary at /usr/bin/mhrv-rs and config at /etc/mhrv-rs/config.json.
Side effect: switched tokio-rustls to default-features=false + ring
(was pulling aws-lc-rs transitively, which can't easily cross-compile
for musl). The main crate already uses ring explicitly, so no runtime
behavior change.
README gets a 'Running on OpenWRT (or any musl distro)' section in
both English and Persian with scp + procd enable/start recipe.
Closes#2.
First run needs the CLI to install the MITM CA into the system trust
store (sudo/admin prompt), which the UI alone can't do reliably from a
double-click. Add a small launcher for each platform that runs the CLI
with --install-cert once, then starts the UI. Each release archive now
contains a run.* script alongside the binaries.
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.