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>
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>
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.
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.