27 Commits

Author SHA1 Message Date
dazzling-no-more 4da0da7a27 perf(apps_script): batch CacheService getAll + edge-DNS hot-path wins (#958) 2026-05-15 16:47:04 +03:00
dazzling-no-more f4f23c3173 fix(code.gs): wrap _doSingle normal-relay fetch in try/catch (#1047, #1049)
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>
2026-05-12 01:55:10 +03:00
therealaleph 9909b9b30b docs(tunnel): document ngrok *.ngrok-free.dev block + alternative hosts (#924)
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>
2026-05-10 12:15:29 +03:00
therealaleph c437598169 fix(exit_node): strip Content-Encoding + Content-Length on response (#964)
@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>
2026-05-09 21:27:58 +03:00
dazzling-no-more 072a917331 feat(code.gs): gzip cache bodies + status-aware TTL (#953)
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>
2026-05-09 21:23:15 +03:00
therealaleph f28b6b1074 docs(exit_node): ship wrapper.ts for VPS users (#912)
@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>
2026-05-08 18:02:46 +03:00
therealaleph 624914241a docs(ngrok): correct static-domain URL behavior + Full-mode verify advice (#877)
@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>
2026-05-08 01:35:53 +03:00
therealaleph 82a8cbfb84 docs(github-actions-tunnel): warn that cloudflared methods may not work from Iran ISP (#849)
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>
2026-05-07 15:42:44 +03:00
Euvel 3e8623571f docs: add GitHub Actions Full tunnel documentation and workflows (#783)
* docs: add GitHub Actions tunnel general documentation README.md

* docs: add cloudflared quick tunnel guide

* Add: add cloudflared quick tunnel workflow

* docs: add ngrok tunnel guide

* Add: add ngrok tunnel workflow

* fix: Update cloudflared-quick.md to add link to related yml file

* fix: Update ngrok.md to add related file link

* docs: add cloudflared named tunnel guide

* Add: add cloudflared named workflow
2026-05-07 00:43:16 +03:00
therealaleph c12ffd4dd4 chore: redact val.town from code and docs, rename exit-node script
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>
2026-05-04 19:11:56 +03:00
therealaleph cbb08468bc fix: v1.9.6 — Code.gs/CodeFull.gs hardening, goog.script.init unwrap, README rewrite
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>
2026-05-01 19:00:32 +03:00
therealaleph 4aac9a793f feat: v1.9.4 — exit node for ChatGPT/Claude/Grok + drop duplicate Telegram post
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.
2026-05-01 11:52:32 +03:00
dazzling-no-more 9013eb9ef7 feat(cfw): add Apps Script + Cloudflare Worker alternative backend 2026-04-30 16:41:19 +04:00
therealaleph cd6ff8d381 feat: v1.9.1 — operator quality-of-life: tunable auto-blacklist, configurable batch timeout, MHRV_AUTH_KEY hint, run.bat CLI fallback
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.
2026-04-30 08:33:53 +03:00
dazzling-no-more 72ed6d893a feat(codefull.gs): edge-cache DNS to skip tunnel-node round-trip 2026-04-29 19:10:19 +04:00
Euvel 90433998cf revert(Code.gs): revert back HTML output i accidentally removed 2026-04-28 21:17:40 +03:30
Euvel 08b22bb961 feat(code.gs): optional spreadsheet-backed response cache with TTL 2026-04-28 20:17:58 +03:30
therealaleph cb3732f920 feat: v1.8.0 — DPI evasion, active-probing defense, full-mode usage counters
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>
2026-04-28 01:39:47 +03:00
dazzling-no-more 40c2b6c509 feat(udp): SOCKS5 UDP ASSOCIATE relay through full tunnel
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.
2026-04-25 16:19:23 +04:00
dazzling-no-more 0a58943433 feat(tunnel): save one RTT per new HTTPS flow via connect_data op 2026-04-25 01:38:30 +04:00
vahidlazio 31ae569aa2 feat: tunnel-node service + CodeFull.gs (#93)
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.
2026-04-23 23:34:51 +03:00
Shin (Former Aleph) 28be8f67d5 v1.1.0: unified Connect button, proxy mode, app splitting, Persian UI, MIPS build (#41)
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>
2026-04-23 09:38:10 +03:00
therealaleph 62a75093ec docs: mirror upstream Code.gs locally (assets/apps_script/)
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.
2026-04-22 18:37:01 +03:00
therealaleph 5101a06a5d v0.8.0: dynamic IP discovery (from PR #9), OpenWRT fd fix (#8), Windows UI diagnostics (#7)
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.
2026-04-22 14:01:56 +03:00
therealaleph 6c5b62e5e6 v0.5.1: static musl builds for OpenWRT (amd64 + arm64)
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.
2026-04-22 02:29:26 +03:00
therealaleph 899ef06f4a v0.4.1: launcher scripts (run.sh / run.command / run.bat)
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.
2026-04-21 22:17:25 +03:00
therealaleph e4fe2b5939 v0.4.0: add cross-platform desktop UI (egui)
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.
2026-04-21 21:36:52 +03:00