Apps Script's response body cap is ~50 MiB. tunnel-node had a TCP_DRAIN_MAX_BYTES = 16 MiB per-session cap to stay under it, but multiple sessions in the same batch each contributed up to 16 MiB raw, summing past 50 MiB on busy VPS — N≥4 concurrent sessions × 16 MiB → ≥64 MiB raw → ≥85 MiB after base64. Steam updates and other CDN-served large downloads hit this exactly: `EOF while parsing a string at line 1 column 52428630` from the client and the session aborts mid-stream.
Fix: new BATCH_RESPONSE_BUDGET = 32 MiB total-batch cap. Drain loop tracks remaining budget across sessions and stops one short of the cliff. drain_now() now takes max_bytes; effective cap = min(budget, TCP_DRAIN_MAX_BYTES). Sessions deferred this batch keep their buffered data — no data loss, they drain on the next poll.
Single-op-path callers and existing tests pass usize::MAX (no extra constraint, original TCP_DRAIN_MAX_BYTES still enforced). New regression test `drain_now_respects_caller_budget_below_per_session_cap` covers the new behavior.
Tests: 197 lib + 36 tunnel-node (was 35) all green. UI release build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps four already-merged PRs into a release:
- PR #799 (@dazzling-no-more): HTTP/2 multiplexing on the relay leg with idempotency-safe h1 fallback. ALPN-negotiates h2; one TCP/TLS connection multiplexes ~100 streams instead of the pool. Slow Apps Script calls no longer head-of-line-block the queue on the same socket. force_http1 kill switch in config. 180→197 tests (+17).
- PR #805 (@yyoyoian-pixel): block_quic default true. QUIC over the TCP-based tunnel was TCP-over-TCP meltdown; browsers fall back to TCP/HTTPS within seconds when UDP/443 is dropped. Adds Android + desktop UI toggles.
- PR #819 (@brightening-eyes): enabled accesskit on eframe so screen readers (NVDA/JAWS/VoiceOver/Orca) can navigate the desktop UI. Closes#750.
- PR #783 (@euvel): GitHub Actions Full tunnel docs + workflow YAML files for users who can't buy a VPS. cloudflared Quick / ngrok / cloudflared Named.
Strategically: h2 multiplexing is the architectural fix for #781 / #773 perceived-slowness regression — it makes the pool tuning machinery much less load-bearing. force_http1 kill switch is there if anything goes sideways in the wild.
Tests: 197 lib + 35 tunnel-node green. UI release-mode build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #763 added `block_doh: bool` with `#[serde(default)]`, which resolves to Rust's `Default::default() = false` for bool, not the `true` PR #763's docs intended. Existing configs upgrading from v1.9.10 → v1.9.13 had no block_doh field, so they got `false` paired with `tunnel_doh: true` (new default from #468) — every browser DoH lookup got tunneled through Apps Script, adding ~1.5s overhead per page load. User-perceived as "v1.9.13 is slower than v1.9.10" in #773.
Switched to a named-default function `default_block_doh() -> bool { true }` so the upgrade path actually delivers the fast block-then-system-DNS behaviour PR #763 advertised. Power users who specifically want browser DoH (with the latency cost) can still opt in with explicit `block_doh: false`.
Tests: 180 lib + 35 tunnel-node + UI release-mode build all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both v1.9.11 and v1.9.12 release CI runs failed because PR #763 added a new `block_doh: bool` field to `Config` but didn't update `src/bin/ui.rs::FormState::to_config()` which builds Config via a struct literal — caught by `cargo build --features ui --bin mhrv-rs-ui` only, not by the lib `cargo test` I'd run during PR review. Added the field to FormState (round-trip from Config), to ConfigWire (skip_serializing_if = "is_true" so default-true configs stay clean), and a new is_true helper. Verified mhrv-rs-ui release build green locally before pushing.
Net effect: v1.9.13 ships everything v1.9.11 and v1.9.12 were supposed to ship (DoH block by default, TLS pool refill loop, github.io fronting group, parallel_relay safe-method gating) plus this UI fix. No additional behavior change.
Tests: 180 lib + 35 tunnel-node + UI release-mode build all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported in #743: with `parallel_relay > 1`, a single POST (e.g. submitting a comment) reached the destination as N concurrent requests, so the comment got posted twice. Root cause is unfixable from the Rust side: `select_ok` cancels only OUR futures, but Apps Script has no way to learn the cancellation, so every fan-out call still runs to completion and each `UrlFetchApp.fetch()` still hits the destination.
Fan-out now only triggers for idempotent methods (GET / HEAD / OPTIONS); POST / PUT / PATCH / DELETE always go sequential. Same pattern as `SAFE_REPLAY_METHODS` in Code.gs `_doBatch` fallback — safe methods are idempotent so re-firing is at worst wasteful, unsafe methods can have side effects so re-firing is incorrect.
New regression test locks down `is_method_safe_for_fanout` predicate. Tests: 180 lib + 35 tunnel-node green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps three already-merged PRs into a release:
- PR #763 (@yyoyoian-pixel): block_doh: true default; rejects browser DoH at SOCKS5 listener so it falls back to system DNS via tun2proxy virtual DNS instead of paying ~1.5s tunnel round-trip per name lookup. Also fixes the Android tunnel_doh config mismatch (was false on Android, true on Rust — silently broke bypass_doh_hosts).
- PR #751 (@yyoyoian-pixel): TLS pool refill loop keeping ≥8 ready connections, freshest-first acquire, pool TTL 45→60s, coalesce step 10→200ms (more conservative revert from v1.9.8 for full-mode batch packing).
- PR #747 (@Shjpr9): added github.io to Fastly fronting group example.
Tests: 179 lib + 35 tunnel-node green.
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>
Android (#700 from @ilok67):
- Reordered MhrvVpnService.teardown() to call Native.stopProxy() FIRST. The previous order (tun2proxy.stop → tun.close → join → stopProxy) crashed SIGSEGV ~2s after Disconnect: tun2proxy's worker thread was blocked in native code on a SOCKS5 socket read; after the 2s+4s timeouts expired with the worker still alive, Native.stopProxy freed the runtime including that socket, and the worker hit use-after-free in the next read. The old comment claimed "runtime shutdown will knock the rest of the world over" — wrong, Native.stopProxy can't forcibly terminate a separate native thread, it just frees memory the other thread is still using. New order closes the socket first, the worker's blocking read returns with EOF, the worker exits cleanly through its error path, and the join is then near-instant.
tunnel-node (PR #695 from @dazzling-no-more, merged):
- Cleanup now tracks eof'd sids from drain_now's return value, not the raw atomic — was silently dropping the tail on >16 MiB buffers when EOF arrived between polls.
- Phase-1 `data` op no longer holds the sessions map across upstream write/flush — was head-of-line-blocking every other batch op.
- Mixed TCP+UDP batch wait switched from tokio::join! to tokio::select! — was paying the UDP LONGPOLL_DEADLINE (15 s) on TCP-ready bursts.
- Watcher tasks now wrapped in AbortOnDrop newtype — was leaking Arc<Inner> permits when select!'s loser arm dropped its future.
- 2 new regression tests, 35/35 pass.
Example configs:
- config.exit-node.example.json: added aistudio.google.com + ai.google.dev to default hosts (#701 — AI Studio sanctions Iran IPs).
- config.fronting-groups.example.json: PR #696 from @Shjpr9 added Reddit/Fastly/Pinterest/CNN/BuzzFeed family domains on the Fastly 151.101.x.x edge.
Tests: 179 lib + 35 tunnel-node green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Android (#666 from @ilok67 with full root cause):
- MainActivity.onStop was sending ACTION_STOP via startService() AND immediately calling stopService() on the same service. ACTION_STOP runs teardown() on a background thread that stopSelf()s at the end; the redundant stopService() triggered onDestroy() in parallel, racing the lifecycle and crashing on every Disconnect tap. Removed the stopService() — ACTION_STOP alone is sufficient for both the live-service and the zombie-after-process-death cases. The tornDown AtomicBoolean already guards against double-teardown of native state but couldn't protect against OS-level stopSelf vs stopService race.
UI (#665 from @cmptrnb):
- Test Relay button was showing red "test result: fail" status when used in full or direct mode. The underlying test_cmd::run deliberately refuses in those modes because probing Apps Script directly while the data plane goes via tunnel-node would give a misleading result, but the refuse path was getting translated to generic "test failed". UI now checks mode before running and shows a mode-specific explainer for full/direct (point users at https://whatismyipaddress.com in the browser via the proxy as the right way to verify).
Includes already-merged PR #674 from @yyoyoian-pixel: drop client coalesce_step + tunnel-node straggler settle_step from 40 ms → 10 ms, raise tunnel-node settle max from 500 ms → 1000 ms. Asymmetric tuning: fast-fire when nothing else is queued, but adaptive coalesce on bursts. Backwards compatible — existing configs with explicit `coalesce_step_ms: 40` keep old behavior.
Tests: 179 lib + 33 tunnel-node green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The desktop UI now has a single "Share with other devices on my Wi-Fi / network" checkbox in place of the cryptic `listen_host: 0.0.0.0` text field. When enabled:
- Bind auto-flips to 0.0.0.0
- LAN IP is detected via the standard UDP-connect trick (no actual traffic) and shown alongside the proxy ports for handing to the guest device
- Tooltip explains macOS Firewall prompt behavior
- A pre-existing custom bind IP in config.json is preserved with a "Custom bind: ..." badge so the next Save can't clobber it
New `src/lan_utils.rs` module with detect_lan_ip / is_share_on_lan / is_loopback_only helpers (3 unit tests).
Also rolls in the v1.9.6 changes (release was cancelled before binaries shipped):
- Code.gs / CodeFull.gs: removed duplicate doGet, switched HtmlService -> ContentService, stripped X-Forwarded-* family in SKIP_HEADERS, added SAFE_REPLAY_METHODS fallback when fetchAll throws as a whole.
- Rust client: parse_relay_json now unwraps goog.script.init iframe wrappers (defense-in-depth for legacy deployments or redirect-induced GET-on-doGet).
- README rewritten as a short bilingual landing page; advanced reference moved to docs/guide.md + docs/guide.fa.md. Persian guide's `[x]` task list replaced with a table because GitHub's RTL renderer mangles checkbox positions inside `<div dir="rtl">`.
Tests: 6 new regression tests (3 goog.script.init unwrap + 3 lan_utils). 179 lib + 33 tunnel-node tests all passing. Bind on 0.0.0.0 smoke-tested via lsof.
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>
Issue #585 from @gregtheph: v1.9.4's exit-node feature failed for every
ChatGPT/Claude/Grok request with `io: peer closed connection without
sending TLS close_notify` and fell back to direct Apps Script (which
can't reach those sites either, producing the no-json error chain).
Root cause: rustls is strict about TLS shutdown — when the peer (val.town's
host) closes the underlying TCP without first sending a TLS close_notify
alert, rustls surfaces this as `io::ErrorKind::UnexpectedEof`. Our
read_http_response propagated this as a hard error, even when the body
was already complete per Content-Length.
Fix: treat UnexpectedEof the same as `n == 0` (graceful EOF). If
Content-Length is satisfied, return the response; if mid-body truncation,
still error as BadResponse. Same handling added to the chunked reader
and the no-framing reader.
4 new regression tests:
- read_http_response_tolerates_unexpected_eof_with_content_length
- read_http_response_tolerates_unexpected_eof_no_framing
- parse_exit_node_response_unwraps_valtown_envelope
- parse_exit_node_response_surfaces_explicit_error
173 lib tests + 33 tunnel-node tests + both release builds passing.
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.
- PR #535 by @yyoyoian-pixel: Switch toggle for youtube_via_relay in
Android Advanced settings, matching the desktop UI checkbox. Closes
the parity gap that forced Android users to hand-edit config.json.
Closes#520.
- ci(telegram-publish): --clobber on gh release download so retries
survive partial downloads (caused the v1.9.2 telegram publish to
fail; manually re-triggered).
Pure docs + GAS/Worker addition shipped via PR #533 (#380 / #393 audit
task). No Rust client changes. Bumping to v1.9.2 so users get the new
deploy assets in the next release tarball + Telegram channel binaries
include the updated docs.
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.
Three substantive PRs landed for this release plus an Iran-safe DoH default:
- #488 by @dazzling-no-more (with credit to @patterniha): fronting_groups
config field generalizes the Google-edge SNI-rewrite trick to any
multi-tenant CDN edge (Vercel, Fastly, etc.). Renames `mode = "google_only"`
→ `mode = "direct"` with a deprecated alias keeping existing configs working.
This is the v1.9.0 headline — new top-level config field + public mode-string
rename are minor-bump territory. xmux moves to v1.10.0.
- #494 by @dazzling-no-more: edge-cache DNS at Apps Script (CodeFull.gs)
using CacheService. udp_open / port=53 ops served from cache or DoH
fallback chain (Cloudflare → Google → Quad9). Cache hits drop typical
first-hop DNS latency from 600-1200ms to 200-400ms. Default-on, opt-out
via ENABLE_EDGE_DNS_CACHE; every failure mode falls through to existing
tunnel-node forward path (zero regression).
- #483 by @yyoyoian-pixel: default listen_host from 127.0.0.1 to 0.0.0.0
so an Android phone running the tunnel + an iPhone/laptop on the same
hotspot can use it as proxy. Old configs with explicit 127.0.0.1 are
honored (not overwritten).
Plus: default `tunnel_doh: true` (flipped from false in v1.8.x) per #468
— Iran ISPs filter direct connections to dns.google, chrome.cloudflare-dns.com,
and other pinned DoH hosts. The bypass-on default silently broke DNS for
the dominant Iranian userbase. The safe default keeps DoH inside the
tunnel; non-Iran users can opt back into the bypass for the latency win.
Backwards-compatible — any config with explicit tunnel_doh keeps its setting.
169 mhrv-rs lib tests + 33 tunnel-node tests + 11 edge-DNS JS tests all
passing. Clean release + UI builds.
@bankbunk reported (#460) that on a 1 Gbps VPS, raw MP4 streams in Full
mode died with `batch JSON parse error: EOF while parsing a string at
line 1 column 52428685` minutes into playback. Root cause: drain_now
took the entire per-session read buffer in one shot. On high-bandwidth
VPS the reader task fills the buffer with tens of MiB between polls;
the resulting batch response (raw + base64 1.33× + JSON envelope)
exceeded Apps Script's ~50 MiB hard cap; Apps Script truncated mid-base64;
the client's serde_json parse hit EOF and the stream tore.
Fix: drain_now now returns at most TCP_DRAIN_MAX_BYTES (16 MiB) per call
and leaves the tail in the buffer for the next poll. EOF is held back
until the buffer is fully drained so partial drains don't tear the
session prematurely. Three regression tests cover the cap, the under-cap
pass-through, and the EOF-holdback case (33 tunnel-node tests passing).
@bankbunk's wondershaper rate-limit workaround (40 Mbps cap on the VPS
interface) is no longer necessary — high-bandwidth VPS users can run at
line rate again.
Three substantive PRs from contributors landed for this release:
- #443 by @euvel: optional spreadsheet-backed response cache in Code.gs.
Implements all 5 review suggestions from the design discussion (#400):
TTL-aware caching, 35 KB body-size gate, header rewriting on hit,
circular buffer for O(1) writes, Vary-aware compound keys.
- #439 by @dazzling-no-more: bypass Apps Script tunnel for known DoH
endpoints on TCP/443. Cloudflare/Google/Quad9/AdGuard/NextDNS/OpenDNS/
CleanBrowsing/dns.sb/dns0.eu/AliDNS/doh.pub/Mullvad. Saves the ~2s
UrlFetchApp roundtrip per name without losing privacy (DoH is
already encrypted). Default on; users can opt out via tunnel_doh: true
or extend the list via bypass_doh_hosts.
- #438 by @dazzling-no-more: H1 container keepalive + 431 oversized-
headers + clearer port-collision message. Cherry-picks from upstream
Python (Apr 23-26 window). Keepalive prevents Apps Script V8 cold
starts (visible as YouTube stalls after pause); 431 replaces silent
socket drops on >64 KB headers (which caused browser retry loops).
- src/bin/ui.rs: install_ui_tracing now takes config_level. Filter
precedence is RUST_LOG > config.log_level > info,hyper=warn. The
filter is wrapped in a reload::Layer; Save reinstalls it via
apply_log_level so users don't need to restart for a level change.
Fixes#401 (w0l4i) — config.log_level was previously dead on the
UI binary even though the CLI honored it via init_logging.
- src/tunnel_client.rs: v1.8.1 asserted "AUTH_KEY mismatch" on the
Apps Script placeholder body, but #404 (w0l4i) showed mixed
success/failure on the same script_id, which rules that out. The
body is also returned for Apps Script execution timeout, quota
tear, internal hiccup, and ISP-side truncation. Error message now
enumerates all four candidates and points to DIAGNOSTIC_MODE for
disambiguation.
Three small, ship-able-now changes from the past day's issue triage:
1. Client-side detection of the v1.8.0 bad-auth decoy HTML
(#404 w0l4i, #310 sina-b4hrm)
When mhrv-rs gets back the decoy HTML body that v1.8.0's Code.gs/
CodeFull.gs/tunnel-node return on bad AUTH_KEY, the client now
string-matches the body's distinctive "The script completed but
did not return anything" sentinel and emits an explicit ERROR
line naming AUTH_KEY mismatch as the likely cause + walking the
user through "redeploy as new version" + the DIAGNOSTIC_MODE
escape hatch — instead of the previous cryptic "WARN batch
failed: bad response: no json in batch response: <!DOCTYPE...".
Saves users hours of debugging. Reported pattern hits everyone
who edits Code.gs's AUTH_KEY without redeploying as a new version
(Apps Script doesn't auto-pick-up that change).
2. script_id in every batch-failure log (#404 w0l4i)
Previously WARN batch-failed lines didn't say which deployment
failed. In multi-deployment setups (5–10 deployments where
some have stale AUTH_KEY), users couldn't identify the culprit
without the per-deployment curl probe loop.
All four failure paths in tunnel_client::fire_batch — timeout,
bad response, decoy detection, missing-response-in-batch — now
include the script_id short prefix: `batch failed (script
AKfycbz4): ...`. Combined with #1 above, this is the first
reliable diagnostic for the "1 of 8 deployments has bad
AUTH_KEY" pattern.
3. New disable_padding config flag (#391 EBRAHIM-AM)
Default false (padding active = stronger DPI defense). For
users on heavily-throttled ISPs where v1.8.0's ~25% bandwidth
overhead from random padding compounds with the throttle and
pushes borderline-working batches into timeouts, setting
`"disable_padding": true` in config.json recovers headroom at
the cost of losing length-distribution DPI defense.
Don't flip on speculatively — only enable if you've measured
actual throughput improvement on your specific ISP path. For
users where Apps Script outbound flows freely, padding is free
defense.
Tested:
- cargo build --release --bin mhrv-rs: clean
- cargo build --release --bin mhrv-rs-ui --features ui: clean
- cargo test --release --lib: 154 passed
- UI FormState round-trips disable_padding through save/load
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
v1.7.10 release run shipped no assets due to two CI failures stacked:
1. The i686-pc-windows-msvc job (added in v1.7.7 for Win7 support per
#318) failed because Rust 1.77.2 — the last stable that produces
Win7-loadable binaries — can't parse modern transitive crate
manifests (`time` 0.3.47 in this case). Pinning transitives across
the dep tree at every MSRV bump in our deps isn't sustainable, so
the target is removed from the release matrix. Win7 32-bit users
self-build per #318's instructions.
2. The `release` job hit `actions/download-artifact@v4`'s 5-retries-
exhausted error on multiple artifacts. Same flake we worked around
in #288 for `commit-releases`. The `release` and `telegram` jobs
now use `gh run download` wrapped in a 3-attempt retry loop, mirror-
ing the working pattern.
v1.7.11 is the first full release after v1.7.9; ships #337 (Apps
Script gzip-decoded range probe) and #344 (Android Paste button) that
were tagged in v1.7.10 but never published as assets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release for the changes shipped via #337 (Apps Script range
probe gzip-decoded body handling) and #344 (Android Paste button on
13+) plus a CI fix that restores the Win7 i686 binary missing from
v1.7.9 (Cargo.lock format mismatch with Rust 1.77).
The Cargo.lock version=4 (Rust 1.78+) wasn't readable by the pinned
1.77.2 toolchain on the i686 job. Workflow now regenerates the
lockfile with the pinned toolchain on that job only, leaving every
other target unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release for the Win7 i686 binary fix shipped via #323.
No code changes; CI workflow change only — Cargo, Gradle, and
changelog bumps in lockstep so the release produces a fresh
i686 Windows binary built against Rust 1.77.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release for the auto-blacklist of timeout-saturated deployments
shipped via #319. No new features; bugfix only — cargo, gradle, and
changelog bumps in lockstep so the release workflow can ship matching
artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- #288 (@amiralishoja): adds i686-pc-windows-msvc to the release
matrix. 32-bit Windows users get mhrv-rs-windows-i686.zip on
every release.
- #290 (@dazzling-no-more): per-deployment longpoll fallback state
with TTL-based auto-recovery. Replaces a global AtomicBool that
one degraded deployment could permanently flip. Now the aggregate
legacy gate only fires when every configured deployment is marked,
and self-corrects on TTL expiry — upgraded tunnel-nodes rejoin
the fast path automatically. 4 new tokio::test virtual-time tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.7.4 added googlevideo.com to SNI_REWRITE_SUFFIXES on the theory
that video chunks should bypass the Apps Script relay. Multiple
users (#275 amirabbas117, #281 mrerf) reported total YouTube
breakage on v1.7.4: SNI-rewriting googlevideo.com:443 to a GFE IP
returned TLS handshake failure / wrong-cert error.
Root cause: googlevideo.com is served by Google's separate "EVA"
edge IPs, not the regular GFE IPs that the user's `google_ip`
typically points at. The SNI-rewrite tunnel TLS handshake against
a GFE IP for googlevideo.com SNI fails because the GFE IP doesn't
hold a googlevideo.com cert.
Pre-v1.7.4 behaviour restored: video chunks fall through to the
Apps Script relay path. Slower but reliable on every GFE IP.
The other v1.7.4 youtube_via_relay carve-out fixes (ytimg.com
correctly stays on SNI rewrite, youtubei.googleapis.com correctly
goes through relay) remain intact — those were a separate
improvement and still correct.
Future: if we want direct googlevideo.com routing, it needs a
separate `eva_edge_ip` config knob — users can populate from their
own EVA scan, defaulting to "use relay" if not configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Adds `block_quic = true` config flag for client-side QUIC drop.
SOCKS5 UDP relay refuses UDP/443 datagrams; browsers fall back to
TCP/HTTPS through the relay. Opt-in. Thanks @w0l4i
- Workflow now auto-refreshes the in-repo releases/ folder on each
release tag, so Iranian users behind GitHub-Releases-page filtering
can download via Code → Download ZIP. Practice was started before
v1.1.0 then dropped; resumed at user request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#275: youtube_via_relay no longer routes video/image CDNs through
Apps Script. The flag now correctly carves out only the API/HTML
hosts where Restricted Mode is enforced; video chunks come direct
from googlevideo.com (which was missing from the SNI rewrite list
entirely — fixed). Long videos no longer hit Apps Script's 6-min
execution cap, and single-chunk timeouts no longer abort playback.
#280: TunnelMux now caches "destination unreachable" responses from
the tunnel-node (Network is unreachable / No route to host) for 30
seconds, short-circuiting subsequent CONNECTs to that destination
with 502 (HTTP) or 0x04 (SOCKS5). Saves ~5 batches/second on
IPv6-only host probes. Startup pre-warm pool grew 12→24.
143/143 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move from yyoyoian-pixel/tun2proxy fork (with patched JNI signature)
to canonical tun2proxy 0.7.21 from crates.io with feature flag
"udpgw". Cargo.toml [patch.crates-io] section removed entirely.
The Android side now resolves tun2proxy_run_with_cli_args at runtime
via dlsym from libtun2proxy.so, which is the upstream maintainer's
recommended path for callers that need full CLI flexibility.
mhrv-rs builds the CLI string in MhrvVpnService and passes it through
Native.runTun2proxy → src/android_jni.rs → dlsym → tun2proxy.
Future tun2proxy upgrades are now a single Cargo version bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- mhrv-rs:// deep links, QR scanner, clipboard banner, share sheet
- DEFLATE-compressed base64 encoding (~200 chars vs ~800 raw)
- Every import path requires explicit user confirmation; the dialog
shows the new deployment IDs and a trust warning so an attacker
posting a malicious mhrv-rs:// link in a public channel can't
silently overwrite a user's auth_key + script_ids
- ZXing for QR generation/scanning (no Google Play Services)
Closes#266. Thanks @yyoyoian-pixel — the rebase from auto-import
to confirmation-gated import is exactly the right shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mhrv-rs --remove-cert (CLI) and Remove CA button (UI) for verified
clean-slate revocation. Clears OS trust store, NSS browser stores
(Linux Firefox/Chrome), and the on-disk ca/ directory. config.json
and the Apps Script deployment are untouched.
By-name trust verification runs before browser-state mutation; OS
removal failures return RemovalIncomplete with browser state intact
so retries are idempotent. Sudo-aware on Unix (re-roots HOME to the
real user). 29 new unit tests on the pure logic (Firefox user.js
marker handling, getent passwd parsing, NSS stderr classification,
NssReport state rules).
Tested end-to-end on Windows by the contributor; macOS verified at
merge time on real hardware (login keychain delete + NSS-missing
fallback). Linux paths await user testing.
Closes#121.
Thanks @dazzling-no-more.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Highlights:
- Native udpgw protocol in Full mode (#222) — Telegram voice/video
calls and Google Meet now work in Full mode on Android. UDP flows
through one persistent TCP tunnel (instead of session-per-destination)
so STUN/RTP flow counts no longer stall. Requires redeploying the
tunnel-node Docker image (ghcr.io/therealaleph/mhrv-tunnel-node:1.7.0).
- Android home screen restructure (#258, closes#246) — Connect button
now pinned under Mode field, App picker shows pre-selected apps at
top. With long deployment-ID lists, Connect no longer scrolls
off-screen.
- release-drafter + prepare-release tooling (#260) — incrementally
drafts release notes from merged PR titles; manual workflow_dispatch
prepares version bumps + changelog stubs.
No protocol breaking changes; existing apps_script-mode and Full-mode
deployments work unchanged. Full-mode users get udpgw automatically
once the tunnel-node Docker image is updated.
Thanks to @yyoyoian-pixel and @dazzling-no-more.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- #245 (@Parsa307): match twitter.com in X.com URL normalization
- #255 (@dazzling-no-more): copy-logs button + selectable log lines on Android
- #257 (@dazzling-no-more): bulk paste of multiple deployment IDs on Android
- #256 (@dazzling-no-more): plain HTTP proxy passthrough in google_only mode
(used to return 502; now falls through to direct TCP / upstream_socks5,
matching the existing CONNECT behavior)
No protocol or wire-format changes; existing config and Apps Script
deployments work unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The batch-build loop blocked on a 30 ms timeout for the first message,
then drained whatever else was in the channel via try_recv() and fired
the batch. Under any non-bursty workload, the channel queue was always
empty by the time the first op woke us up — so every "batch" had
exactly one op, defeating the entire batching premise. Reporter (w0l4i)
saw `batch: 1 ops → ..., rtt=6.3 s` repeating in logs even under high
concurrency.
Fix: after the first op lands, hold the buffer open for an 8 ms
coalescing window. Concurrent ops (parallel fetches, HTTP/2 stream
openings, etc.) now accumulate into the same batch. 8 ms is rounding
error against the 2–7 s Apps Script RTT we're amortizing, and restores
the multi-op-per-batch behavior the rest of the code already supports
(MAX_BATCH_OPS=50, MAX_BATCH_PAYLOAD_BYTES=4 MiB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buildNotif() hardcoded `proxyPort + 1` for the SOCKS5 line, ignoring
cfg.socks5Port entirely. With the default Android config
(listenPort=8080, socks5Port=1081) the foreground notification read
"Routing via SOCKS5 127.0.0.1:8081" but the real listener was on 1081 —
so users configuring per-app SOCKS5 (Telegram, etc.) against the
notification value silently failed.
Use the same `cfg.socks5Port ?: (cfg.listenPort + 1)` elvis fallback the
real listener uses, and surface both ports in the notification:
HTTP 127.0.0.1:8080 · SOCKS5 127.0.0.1:1081
Reported by vpnineh and l3est (with netstat screenshots showing the
exact mismatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In `relay_parallel_range`, when a chunk failed validation
(`extract_exact_range_body` returned Err) OR the stitched body length
didn't match the advertised total, the fallback path called
`rewrite_206_to_200(&first)` — which converted the 256 KiB probe
response into HTTP 200 + Content-Length=262144 and returned that as
if it were the full file. Browsers saw a complete-looking 200 and
treated the download as finished at 256 KB.
Common triggers for the chunk-validation failure (per the user
reports):
- Apps Script's UrlFetchApp stripping `Content-Range` from chunk
responses while preserving it on the probe
- Origin returning 200-OK on follow-up Range requests (some servers
flatten ranges after the first one)
- Mismatched `total` field across chunks for paths behind a varying
cache layer
The correct fallback is a single GET without any Range header —
Apps Script fetches the whole URL (up to its 50 MiB cap) and
returns a normal 200 with the complete body. Slower than parallel
for large files but produces a correct response, which is the
minimum bar.
Two independent reports (Ehsan in #162, Recruit1992 confirming).
98 lib tests still pass; existing `validate_probe_range_rejects_*`
and `extract_exact_range_body_*` tests already cover the validation
side, the fallback path is observed integration-testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five small but real Android-only fixes:
1. Connect/Disconnect button gated on VpnState.isRunning state-flow
with 12s backstop, replacing the fixed 2s transitionCooldown
timer. Closes the race where a tap-after-Stop hit "Address already
in use" because the previous teardown's listener-socket release
wasn't done.
2. Tun2proxy.stop() wrapped in 2s join() — if the native call hangs,
bounded teardown still releases the listener port instead of
holding the teardown thread.
3. fd-leak fixed between parcelFd.detachFd() and Thread.start(): an
OOM-thrown Thread.start used to orphan the detached fd. Now
adopted into a fresh ParcelFileDescriptor purely so we can close()
it.
4. Misleading teardown doc-comment rewritten — the "step 2 closes
the TUN fd to force EBADF on read" claim has been factually
wrong since detachFd landed.
5. Recursive crash trap: Log.e in MhrvApp's uncaught handler now
wrapped in try/catch so a logd failure during exception logging
falls through to the previous handler with the real exception.
No Rust changes; 98 lib + 22 tunnel-node tests still pass.
Local Android build verified, APK installed on mhrv_test emulator,
launches cleanly with v1.6.1 in title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships PR #183. SOCKS5 UDP ASSOCIATE → tunnel-mux udp_open/udp_data ops
→ tunnel-node UDP sessions → real UDP egress. QUIC/HTTP3, DNS, and
STUN now traverse the tunnel instead of falling back to TCP or
leaking outside it.
- 256-session-per-associate cap with FIFO eviction
- 9 KB datagram size guard (DNS/STUN tiny, QUIC max ~1452, leaves
IPv6 PMTUD headroom without burning Apps Script quota on rogue
traffic)
- Source-IP pinned to the control TCP peer; port locked to first
parseable datagram (malformed datagrams from the right IP no
longer DoS the legitimate flow)
- Event-driven UDP drain reusing v1.5.0's long-poll knobs
Backward compat: TunnelResponse.pkts is `Option<Vec<String>>` with
serde default; v1.5.0 clients hitting v1.6.0 tunnel-nodes ignore
the new field; v1.6.0 clients hitting v1.5.0 tunnel-nodes get
UNSUPPORTED_OP on udp_open and the existing fallback path takes
over (TCP-only). Apps Script CodeFull.gs is opaque to the new ops
— no redeploy needed; just doc-comment update.
98 lib tests + 22 tunnel-node tests pass (was 92 + 17 before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships PR #173 (event-driven drain) plus three operational improvements:
PR #173 — long-poll tunnel mode. The tunnel-node's batch drain
switched from a fixed 150 ms sleep to an event-driven Notify wait;
idle sessions long-poll up to 5 s and wake on the first byte from
upstream. Push notifications and chat messages now arrive in roughly
RTT instead of waiting for the next client poll tick. Backward compat
with pre-#173 tunnel-nodes is automatic via a sticky AtomicBool that
detects fast empty replies and reverts to the legacy cadence.
92 client tests + 17 tunnel-node tests pass, including end-to-end
TCP-pair verification of the notify wiring.
Docker image for tunnel-node. Adds a hardened Dockerfile (BuildKit
cache mounts, non-root runtime user, ca-certificates for HTTPS
upstreams) and a .dockerignore to keep build context small. New
`tunnel-docker` job in the release workflow builds + pushes
multi-arch (linux/amd64 + linux/arm64) to
ghcr.io/therealaleph/mhrv-tunnel-node with `:latest`, `:1.5`, and
`:1.5.0` tags on every release. Setting up Full Tunnel mode goes
from "rustup + cargo build on a 1 GB VPS" (which fails on memory
half the time) to a one-liner. tunnel-node/README.md updated with
prebuilt-image + docker-compose recipes.
Brief Persian release note in Telegram caption. The release-post
caption now leads with a `<blockquote>`-wrapped FA bullet headlines
extracted from `docs/changelog/v<ver>.md`, above the existing two
links (repo + release). Markdown links → Telegram HTML <a> for
clickability. Cap-budget-aware truncation at bullet boundaries
keeps total caption under Telegram's 1024-char limit. Headlines-only
rather than full bullets so multiple "what's new" items fit
comfortably (the full bullets remain on the GH release page and as
the optional --with-changelog reply-threaded message).
GitHub Releases page bodies now lead with the changelog content
(Persian section + `---` + English) instead of just a Full Changelog
comparison link. The auto comparison link is appended at the bottom
via `append_body: true` rather than removed.
Workflow changes:
- New `permissions: packages: write` at the workflow level (required
for ghcr push via docker/login-action).
- New `tunnel-docker` job needs `build` (not the full matrix) to
serialize the QEMU buildx layer with the matrix cache.
- Release job composes the body from `docs/changelog/v${VER}.md`
in a pre-step that handles both tag-push and workflow_dispatch
paths (uses inputs.version || github.ref_name like the rest of
the workflow).
Tested locally:
- `cargo test` — 92 lib tests pass
- `cargo test -p mhrv-tunnel-node` — 17 tests pass
- `docker build` of tunnel-node Dockerfile — 32 MB image, runs as
non-root, /health returns "ok", auth rejection works correctly,
legitimate requests open sessions to remote hosts
- Telegram script `--dry-run` mode added; rendered captions for
v1.4.0, v1.4.1, v1.5.0 all fit under 900 chars
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release covering:
- #160 (deniz_us): Test Relay returned Google datacenter IPs even in
Full Tunnel mode, because test_cmd::run unconditionally used
fronter.relay() (apps_script path) regardless of configured mode.
The user's tunnel-node was actually working — whatismyipaddress.com
in their browser showed the correct VPS IP — but Test Relay
contradicted it. Now Test Relay refuses cleanly in Full mode with
a clear message and points users at the right verification path.
A real Full-mode test through the tunnel mux is enhancement-tracked.
- mhrv-rs-openwrt-mipsel-softfloat artifact lands natively (commit
febeeca). v1.4.0 had a build break on the 32-bit MIPS target due to
PR #153's std::sync::atomic::AtomicU64 import — switched to
portable_atomic::AtomicU64 which is already the project's
convention for that reason. The artifact was hot-published to the
v1.4.0 release page via workflow_dispatch yesterday; v1.4.1 ships
it the normal way.
91 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rolls up #150 + #151 + #153 (all merged on main) into a tagged release.
Highlights:
- Full Tunnel mode opens new HTTPS connections ~500-2000 ms faster
(#153). The new connect_data tunnel op bundles the client's first
bytes (typically TLS ClientHello) with the CONNECT call, eliminating
one full Apps Script round trip per new flow. Backward compat is
handled via UNSUPPORTED_OP detection + sticky AtomicBool fallback +
pending_client_data replay so older deployments keep working without
byte loss. New `connect_data preread: X win / Y loss / Z skip`
metric in logs lets us measure the win ratio empirically.
- Android ONLY-mode split fix (#150): when the allow-list contained
only mhrv-rs or stale uninstalled packages, every
addAllowedApplication call silently failed and Android applied the
TUN to every app — looping our own proxy traffic. Now we count
successful adds; if zero, we fall back to ALL-mode self-exclusion.
Complements PR #143 which fixed the empty-list case.
- Memory-safety cap on relay range stitching (#151): a hostile or
buggy origin could advertise an absurd Content-Range total
(e.g. 10 GiB) and force range-parallel to plan millions of chunks
and preallocate a huge stitched buffer. Now capped at 64 MiB; larger
totals fall back to a normal single GET.
Tests: 91 lib tests pass (was 82; +1 from #151, +8 from #153).
Tunnel-node: 6 tests pass (all new from #153).
Local Android build verified — universal + four per-ABI APKs all
produced at expected sizes (universal 53 MB, arm64-v8a 21 MB,
armeabi-v7a 18 MB, x86_64 23 MB, x86 22 MB). Installed on mhrv_test
emulator, app launches and renders correctly with v1.4.0 in title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rolls up the four post-v1.2.14 commits on main into a single tagged
release. Highlights:
- Per-deployment concurrency (#142): each deployment ID gets its own
30-permit semaphore, so setups with deployments across multiple
Google accounts get a genuine 30×N throughput ceiling. Single-account
setups still cap at Google's per-account 30-simultaneous limit —
docs (EN + FA) updated to call that out.
- Android app-splitting ONLY-mode bug fix (#143): the previous code
called both addAllowedApplication and addDisallowedApplication,
which Android documents as mutually exclusive. ONLY mode was
silently failing establish(). Now fixed.
- Per-ABI Android APKs (#136): ships four split APKs (arm64-v8a ~21 MB,
armeabi-v7a ~18 MB, x86_64 ~23 MB, x86 ~22 MB) alongside the ~53 MB
universal. Huge distribution win for users on unreliable
censorship-tunnel paths — the 21 MB arm64-v8a download succeeds
where the universal doesn't.
- Honest IP-exposure note in Security Posture (#148): clarified that
v1.2.9's forwarded-header stripping only covers the client-side leg;
what Google's own infrastructure may add on the UrlFetchApp.fetch()
second leg is outside this client's control. Full Tunnel mode is
the recommendation for threat models where that matters.
- Telegram release-post format: added Persian preambles above both
links (GitHub repo + full Persian guide; release page + desktop/
router builds) so channel readers see the intent at a glance.
82 tests pass. Desktop + Android builds both verified clean locally
across the v1.2.15+ commit series.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a daily-budget visualization for users worried about hitting the
Apps Script free-tier quota (20,000 UrlFetchApp calls/day).
Usage today card (desktop + Android):
- today_calls / today_bytes / today_key / today_reset_secs atomics on
DomainFronter, hooked into the bytes_relayed fetch_add path so we only
count successful relays (matching what Google actually billed)
- Daily rollover at 00:00 UTC, std-only date math (Hinnant's
civil_from_days) — no chrono/time dep pull
- StatsSnapshot extended with the four new fields + to_json() for the
Android JNI bridge
- Desktop UI renders the card right under the existing Traffic stats
with a hyperlink to https://script.google.com/home/usage for the
authoritative Google-side number
- Android UI renders the same card via Compose, polling
Native.statsJson(handle) once a second only while the proxy is up,
with an Intent(ACTION_VIEW, …) opening the dashboard URL
JNI / state plumbing:
- New Java_…_statsJson reads the Arc<DomainFronter> kept in slot_map
- VpnState.proxyHandle StateFlow so HomeScreen knows which handle to
poll without poking into the service's internal state
- MhrvVpnService publishes the handle on start, zeroes on teardown
Persian localization:
- HowToUseBody (5-step guide + Cloudflare Turnstile note) was
hardcoded English even when locale=FA. Ported to a string resource
with a full FA translation in values-fa/strings.xml. Persian users
no longer drop to English at the bottom of the screen.
Also lands the deferred Android ConfigStore.kt wiring for
passthrough_hosts (commit fe9328e shipped the Rust + desktop side).
82 tests pass (added: unix_to_ymd_utc_handles_known_epochs,
seconds_until_utc_midnight_is_bounded). Built and visually verified on
both desktop and Android emulator (mhrv_test AVD).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug fix release. My v1.2.12 merge of Mode::Full bypassed the
deployment-ID + auth-key check on Android, but Full mode talks to
CodeFull.gs on Apps Script and needs those same credentials.
Users selecting "Full tunnel (no cert)" with empty fields would see
the VPN service bail silently instead of surfacing a clear "config
incomplete" error. Vahidlazio's fix changes the gate from
`mode == APPS_SCRIPT` to `mode != GOOGLE_ONLY` and removes the
Mode.FULL bypass in the Start button's enabled-state.
Also includes a UX refactor of the Deployment IDs editor (per-row
rows with add/remove buttons instead of raw newline-separated text),
making multi-deployment setups easier to manage on Android — useful
now that Full Tunnel Mode users routinely scale to 5+ deployments
per their Google accounts.
Android-only diff; Rust side is byte-identical to v1.2.12.
Rollup of PR #94 — Mode::Full dispatch + batch tunnel client. Ships
the long-awaited no-MITM path that was the motivating fix for half
the open issues this week.
User-facing: add `"mode": "full"` to config.json, deploy CodeFull.gs
as a second Apps Script alongside your existing one, deploy
tunnel-node (tunnel-node/README.md) on a VPS, and traffic is tunneled
end-to-end: client → mhrv-rs → script.google.com → your tunnel node →
destination. Browser speaks TLS directly with the destination; we
never see plaintext. No CA needed on the client device.
Android side gets a "Full tunnel (no cert)" dropdown option; toggling
it writes `"mode": "full"` to config.json.
Safety: Mode::AppsScript and Mode::GoogleOnly dispatch paths are
unchanged — Full mode is an additive branch at the top of
dispatch_tunnel. Existing users on the default apps_script mode see
zero behaviour change.
Testing status: compiles clean on all 10 CI targets; 75 tests pass
(+2 new config-validation tests for Full mode); end-to-end real-VPS
testing will come post-release from @Feiabyte and others who opt in.
Any Full-mode regression gets a fast-follow fix.
Single-bug release. Unblocks x.com browsing for users whose browsers
resolve to www.x.com rather than bare x.com — i.e. essentially
everyone using Firefox / Chrome / Safari.
Previous releases still advertised the URL-truncation fix as working
but it only matched exact Host: x.com, which never happens in real
traffic. v1.2.11 widens the matcher to x.com + *.x.com so www.x.com,
api.x.com, and any future x.com subdomain all get the shortened URL
through Apps Script's URL length cap.
Single-focus release. The Stop button in the UI previously only
stopped new connections from being accepted — in-flight clients kept
running on the old DomainFronter, which meant:
- Pages kept loading after Stop (users thought they'd stopped)
- Auth-key changes didn't take effect for domains with a live
keep-alive to the proxy
- Apps Script quota could still be consumed post-Stop
Fix (7338e76): wrap per-client spawns in a tokio::task::JoinSet
inside each accept loop. On shutdown, aborting the accept task drops
the JoinSet, which aborts every in-flight client. Sockets close,
the old fronter's TLS pool drops, and a subsequent Start builds a
clean new state.
Finding 1 of #99 (quota-exceeded → "timeout" instead of the real
502 body) is a separate pool-staleness issue and is NOT addressed
in this release.