Commit Graph

373 Commits

Author SHA1 Message Date
therealaleph a797830cca docs(readme): bust camo cache via path change on downloads badge
Adds .svg suffix to the downloads-badge URL path. Two effects:
1. The full URL hash (which camo keys on) changes, so GitHub's image proxy treats this as a brand-new URL and fetches fresh from shields.io instead of serving the previously-cached "invalid" SVG (camo's cache TTL was 30 min, locked in from the original transient shields.io flake earlier today).
2. .svg is a documented shields.io path suffix — verified via curl that `/total.svg?label=...` returns the same SVG content as `/total?label=...`.

The previous cacheSeconds=60 → cacheSeconds=300 dance also produces a new URL hash, but camo had separately cached the cacheSeconds=60 variant during a shields.io flake, so the user kept seeing invalid. New path = guaranteed fresh fetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:36:22 +03:00
Shin (Former Aleph) e9d4bb9399 Update README.md 2026-05-07 15:31:03 +03:00
therealaleph e714129945 docs(readme): cache-bust camo on the release+downloads badges
Both badges resolved correctly when fetched directly from shields.io but GitHub's image proxy (camo.githubusercontent.com) was still serving the previously-invalid cached SVG to README viewers. Adding `cacheSeconds=300` changes the URL hash camo keys on, forcing a fresh fetch, AND tells shields.io to set a 5-minute Cache-Control on the SVG so future invalidations propagate quickly.

Caught a separate shields.io quirk in passing: parameter ordering on `/github/downloads/...` is significant — `?label=X&logo=Y` resolves, `?logo=Y&label=X` returns invalid (reproduced via curl). Order in README is the working one. The `?sort=semver` parameter remains the original release-badge breaker (#previous fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:29:48 +03:00
therealaleph 8ae4600d33 docs(readme): drop sort=semver from release badge — shields.io was rendering it as "invalid"
The "release: invalid" + intermittent "downloads: invalid" rendering on the README badges (user-reported screenshot) traced to shields.io's GitHub release endpoint choking on `?sort=semver`. Bisected via curl — every variant with sort=semver returned `aria-label="release: invalid"`, dropping it returned the correct version.

Default ordering on shields.io's `/github/v/release/{owner}/{repo}` is "latest published release" which already gives us what we want (our tags are linear v1.x.y semver and the most-recent is always the latest release). Also bumped the badges with explicit colors so they match the rest of the badge row visually.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:27:51 +03:00
github-actions[bot] 09a25d6956 chore(releases): refresh prebuilt binaries for v1.9.15
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-06 22:08:19 +00:00
therealaleph 3e5970cc3f chore: cut v1.9.15 — h2 multiplexing + block QUIC + UI a11y + GitHub Actions full tunnel docs
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>
v1.9.15
2026-05-07 00:46:43 +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
dazzling-no-more 0e678630a8 feat: HTTP/2 multiplexing on relay leg with idempotency-safe h1 fallback (#799) 2026-05-07 00:43:10 +03:00
yyoyoian-pixel 5a07709be8 feat: block QUIC by default + UI toggle (Android & desktop) (#805)
QUIC over the TCP-based tunnel causes TCP-over-TCP meltdown — users
see <1 Mbps where HTTPS/TCP would do >50. The existing `block_quic`
config option was off by default and had no UI on either platform,
so most users suffered QUIC degradation without knowing why.

Changes:
- Default `block_quic` to `true` (was `false`). Browsers detect the
  silent UDP/443 drop and fall back to TCP/HTTPS within seconds.
- Add "Block QUIC" toggle in Android Advanced UI.
- Add "Block QUIC (UDP/443)" checkbox in desktop UI (was config-only,
  issue #213).
- Android: always emit `block_quic` in JSON so the Rust default
  doesn't silently override the user's choice.

Closes #793.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 00:40:35 +03:00
brightening-eyes ea624e886a added accessibility for the ui. it can work with my screen reader as well. (#819) 2026-05-07 00:40:29 +03:00
github-actions[bot] 1b38c5408e chore(releases): refresh prebuilt binaries for v1.9.14
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-05 16:07:56 +00:00
therealaleph 98181c2235 fix: v1.9.14 — block_doh default upgrade-path regression (#773)
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>
2026-05-05 18:45:54 +03:00
github-actions[bot] 545ff3d796 chore(releases): refresh prebuilt binaries for v1.9.13
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-05 11:58:35 +00:00
therealaleph 607d23c316 hotfix: v1.9.13 — fix mhrv-rs-ui compile breakage from missing block_doh field
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>
v1.9.13
2026-05-05 14:40:53 +03:00
therealaleph 9a21bc44cf fix: v1.9.12 — gate parallel_relay fan-out to idempotent methods only (#743)
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>
v1.9.12
2026-05-05 14:16:08 +03:00
therealaleph c2a33a80c7 chore: cut v1.9.11 — DoH block + TLS pool tuning + github.io fronting
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>
v1.9.11
2026-05-05 14:12:34 +03:00
Shayan Jafarpour f7ab99d72f Adding github.io to the fronting domains (#747)
* Update config.fronting-groups.example.json

Added more Fastly fronting groups

* Update config.fronting-groups.example.json

Added `github.io` which is one of the most important domains on the github website
2026-05-05 14:10:54 +03:00
yyoyoian-pixel ba99d2a5f5 perf: TLS connection pool + coalesce tuning for lower latency (#751)
TLS pool improvements:
- Increase POOL_TTL from 45s to 60s so connections live longer
- Add POOL_MIN (8): background refill loop keeps at least 8 ready
  TLS connections so acquire() never pays a cold handshake
- Refill checks every 5s, only counts connections with ≥20s
  remaining as "healthy" — nearly-expired entries don't count
- warm() now opens sequentially (500ms gaps) with 8s expiry
  offset per connection so they roll off gradually instead of
  all expiring together after a cliff
- acquire() picks the freshest connection (most remaining TTL)
  instead of popping whatever is on top

Coalesce step increase:
- DEFAULT_COALESCE_STEP_MS: 10 → 200. The dominant bottleneck is
  the Apps Script round-trip (~1.5s), so the extra 200ms wait is
  negligible to the user but lets significantly more ops land in
  each batch — measured 3–5 ops/batch vs 1 op/batch at 10ms
  during page loads, cutting round-trips roughly in half.

Tested on Android (Pixel 6 Pro) with full-mode tunnel. Pool
hit rate went from 96% (POOL_MIN=4) to 100% (POOL_MIN=8) —
zero cold TLS handshakes during requests.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 14:10:47 +03:00
yyoyoian-pixel e13bca822f fix: block DoH by default + fix Android tunnel_doh config mismatch (#763)
Problem:
PR #468 changed `tunnel_doh` default to `true` (tunnel DoH through
Apps Script) to avoid ISP-blocked DoH on censored networks. But this
added ~1.5s of Apps Script round-trip per DNS lookup — every page
load got noticeably slower because Chrome's DoH connections had to
traverse the full tunnel path before the page could even start
connecting.

The Android side had a separate bug: `tunnelDoh` defaulted to
`false` but only emitted `tunnel_doh` to JSON when `true`. Since
the Rust default is `true`, omitting the field meant Rust always
tunneled DoH regardless of the Android UI setting — bypass_doh was
silently broken on Android.

Fix:
- Add `block_doh` config option: immediately reject (RST) connections
  to known DoH endpoints. Browsers fall back to system DNS, which
  tun2proxy handles via virtual DNS (instant, zero tunnel cost).
  Eliminates the DoH round-trip without exposing DoH connections to
  the ISP (unlike bypass_doh which sends DoH direct).
- Default `block_doh: true` on Android — tested on Chrome/Brave,
  falls back to virtual DNS correctly.
- Fix Android `tunnelDoh` default to `true` (matches Rust).
- Always emit `tunnel_doh` and `block_doh` explicitly in Android
  JSON serialization — no more default-mismatch bugs.
- Add Block DoH and Bypass DoH toggles in Android Advanced UI.
  Block DoH takes priority; Bypass DoH is disabled when Block is on.

Tested on Pixel 6 Pro: zero chrome.cloudflare-dns.com tunnel
sessions with block_doh=true. All DNS resolves instantly via
tun2proxy virtual DNS.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-05 14:10:24 +03:00
github-actions[bot] b45b45f098 chore(releases): refresh prebuilt binaries for v1.9.10
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-04 16:29:33 +00: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>
v1.9.10
2026-05-04 19:11:56 +03:00
therealaleph 6c692441be ci(telegram): brief English bullets in announcement + cross-link, drop Persian-full
Telegram channel posts up through v1.9.9 inlined the full Persian half of `docs/changelog/v{version}.md` (often >2000 chars), with sub-bullets, contributor mentions, and architectural prose. In a chat-client viewport the result was an unreadable wall of mixed RTL Persian + LTR `<code>` / `<b>` spans + nested bullets that scrolled past most readers.

Switched to brief-extracted English instead:
- Added `brief_changelog(text)` — keeps only top-level `• ` bullets (drops sub-bullets), strips "by @user with full root cause + fix" / "from @user" prefatory phrases, replaces `[#nnn](url)` with `#nnn` for inline issue refs, cuts each bullet at the first natural sentence boundary (`:` after pos 30, `. `, ` — `), hard-caps at 200 chars per bullet, and trims any dangling unbalanced `(` or `[` left by the truncation.
- Both posts (files-channel announcement + main-channel cross-link) now use `english_brief = brief_changelog(english_notes)` instead of the full Persian.
- Title and footer chrome of both posts switched to English ("released" / "Files (Android, Windows, ...)" / "Channel:" / "or:").

The full Persian + full English text stays in `docs/changelog/v*.md` for archival; only the channel post becomes brief.

Verified locally on v1.9.7 / v1.9.8 / v1.9.9 — produces 246–458 char briefs with clean bullet structure, no dangling parens, no contributor noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:39:38 +03:00
github-actions[bot] 38c359f0f6 chore(releases): refresh prebuilt binaries for v1.9.9
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-04 01:02:42 +00:00
therealaleph 49b6fbfae7 fix: v1.9.9 — Android second disconnect crash + tunnel-node drain correctness
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>
v1.9.9
2026-05-04 03:44:20 +03:00
Shayan Jafarpour d8d03be11e Update config.fronting-groups.example.json (#696)
Added more Fastly fronting groups
2026-05-04 03:35:03 +03:00
dazzling-no-more 38d9d9fcd6 fix(tunnel-node): batch drain correctness and lock contention (#695)
* fix(tunnel-node): batch drain correctness and lock contention

* fix(tunnel-node): single-op lock contention and batch base64 consistency
2026-05-04 03:33:49 +03:00
github-actions[bot] 3cb56c36c7 chore(releases): refresh prebuilt binaries for v1.9.8
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-03 13:16:19 +00:00
therealaleph 677ec26bee fix: v1.9.8 — Android disconnect crash + UI test-button gate for non-apps_script modes
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>
v1.9.8
2026-05-03 15:57:53 +03:00
yyoyoian-pixel 994dd0b23c tune: lower coalesce/settle step from 40 → 10 ms, raise tunnel-node settle max to 1 s (#674)
The batch coalesce step controls how long the client (and the
tunnel-node's straggler settle) waits between checking for more ops
to pack into the same batch.  At 40 ms the wait was conservative —
good for packing uploads but needlessly slow on the download path
where the tunnel-node round-trip, not coalescing, is the bottleneck.

Lowering the step to 10 ms means we fire batches almost immediately
when there's nothing else queued, cutting ~30 ms of dead air on
every download-dominated round-trip.  When both sides DO have data
in flight (uploads, bursty page loads), the adaptive reset still
works: each arriving op resets the 10 ms step timer, so a rapid
burst naturally coalesces up to the 1 s hard cap without wasting
quota on many small batches.

In short: don't wait when there's nothing to wait for; batch
aggressively when there is.

Client side:
  - DEFAULT_COALESCE_STEP_MS  40 → 10 ms
  - DEFAULT_COALESCE_MAX_MS   unchanged at 1000 ms

Tunnel-node side:
  - STRAGGLER_SETTLE_STEP     40 → 10 ms  (matches client step)
  - STRAGGLER_SETTLE_MAX     500 → 1000 ms (more room to pack
    straggler responses when upstream targets reply at different
    speeds — saves Apps Script quota on the return leg)

Users who prefer the old behaviour can set "coalesce_step_ms": 40
in config.json.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 15:45:52 +03:00
therealaleph d53399dd7b docs(readme): collapse the Persian guides block into a 2-item numbered list
Replaces the YouTube thumbnail + caption + separate text-guide line with a single centered, RTL-directed paragraph containing two Persian-numerated items: ۱ for the video (YouTube) and ۲ for Kian Irani's text guide. Cleaner, less vertical space, and the (YouTube) suffix on item ۱ tells the reader where the link points without needing the big thumbnail to imply it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:47:35 +03:00
therealaleph 5ea77aedb9 docs(readme): rename video caption to "راهنمای تصویری راه اندازی به زبان فارسی"
Symmetric with the text-guide caption right below it ("راهنمای جامع متنی…") — تصویری/متنی parallel makes the video-vs-text distinction obvious at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:45:50 +03:00
therealaleph 81a0e44f12 docs(readme): single-line layout for Kian Irani's guide credit
Two links on one line: "راهنمای جامع متنی راه اندازی به زبان فارسی" → his guide, "Kian Irani" → his GitHub. Plain "با تشکر از" between them. Cleaner than the previous two-line + sub-text version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:45:00 +03:00
therealaleph 880d304bf3 docs(readme): link to @KIAN-IRANi's comprehensive Persian setup guide under the video
Adds a centered link below the video pointing to https://kian-irani.github.io/mhrv-setup-full-tunell/, with credit to @KIAN-IRANi (https://github.com/KIAN-IRANi). Both links open in a new tab. Persian-speaking users now have three escalating depths of guide right at the top of the README: the 5-min Quick Start, the YouTube walk-through, and Kian Irani's full long-form guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:43:37 +03:00
therealaleph e91189c4f5 docs(readme): open the Persian-guide video in a new tab
Adds target="_blank" + rel="noopener noreferrer" so the click doesn't navigate the README away from the repo. The `rel` value is the conventional safety pair: `noopener` blocks the new tab from accessing `window.opener`, `noreferrer` strips the Referer header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:41:46 +03:00
therealaleph e85e42bd98 docs(readme): embed Persian setup-guide YouTube video below the language-switcher links
YouTube iframe embeds get sanitized by GitHub's README renderer, so we use the standard "thumbnail-image-linking-to-youtube.com" pattern instead — visually identical (Play overlay + auto-loaded by YouTube's CDN), survives the sanitizer, and one click opens the video on YouTube.

Caption: "راهنمای راه اندازی به فارسی:".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:41:12 +03:00
therealaleph 50bb3d0eaf ci(telegram): include Persian changelog in announcement + main-channel cross-link
Up through v1.9.7 the Telegram posts said only "📦 mhrv-rs vX.Y.Z منتشر شد" + a hashtag and a link to the files channel — subscribers had to click through to GitHub to see what actually changed. Now the announcement post in the files channel and the cross-link post in the main channel both inline the Persian half of `docs/changelog/v{version}.md`.

How it works:
- New `load_changelog(repo_root, version)` reads `docs/changelog/v{version}.md`, strips the leading `<!-- ... -->` editor comment, splits on the lone `---` line that separates Persian from English. Returns (None, None) if the file doesn't exist (lets out-of-band re-publishes for old tags whose changelog file was never landed work without crashing).
- New `md_to_tg_html(md, max_len)` does a minimal markdown → Telegram-flavoured-HTML conversion: `**bold**`, `[text](url)`, `` `code` `` are translated; nested patterns (e.g. `[`code`](url)`, `**[`code`](url)**`) work via a placeholder/unwind pass that loops until stable. Truncates at the 4096-char sendMessage limit, snapping to a newline boundary so a span isn't cut in half, with a "see full notes on GitHub" tail.
- Falls back gracefully if the changelog file is missing — uses the old skeleton message.

Verified locally on docs/changelog/v1.9.7.md: 3039 chars after conversion, well under the 4096 limit, all bold / code / link spans render correctly including nested ones (`[`assets/apps_script/Code.gs`](url)` becomes `<a href="url"><code>assets/apps_script/Code.gs</code></a>`).

This change takes effect on the next release (v1.9.8+); v1.9.7 is already published with the old skeleton.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:23:01 +03:00
github-actions[bot] b75a816334 chore(releases): refresh prebuilt binaries for v1.9.7
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-01 16:33:35 +00:00
therealaleph 680502759a feat: v1.9.7 — friendly LAN-share toggle + supersede unshipped v1.9.6
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>
v1.9.7
2026-05-01 19:15:52 +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
github-actions[bot] d336bd39e5 chore(releases): refresh prebuilt binaries for v1.9.5
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-01 12:56:08 +00:00
therealaleph 541b37ad7d fix: v1.9.5 — exit-node tolerates TLS close without close_notify (#585)
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.
v1.9.5
2026-05-01 15:38:47 +03:00
github-actions[bot] 7268baf098 chore(releases): refresh prebuilt binaries for v1.9.4
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-05-01 09:10:33 +00: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.
v1.9.4
2026-05-01 11:52:32 +03:00
github-actions[bot] d65759d8b8 chore(releases): refresh prebuilt binaries for v1.9.3
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-04-30 17:17:30 +00:00
therealaleph 55c1256fe1 feat(android): v1.9.3 — youtube_via_relay toggle in Android UI + CI retry fix
- 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).
v1.9.3
2026-04-30 19:58:23 +03:00
Shin (Former Aleph) 1929f8e14b feat(android): add youtube_via_relay toggle to Advanced settings (#535)
Parity fix: the desktop UI already has a youtube_via_relay checkbox in src/bin/ui.rs, but the Android UI was missing it — users had to hand-edit config.json on Android. By @yyoyoian-pixel.

Adds youtubeViaRelay field to MhrvConfig with JSON serialization, deserialization, and config-sharing encode. New Switch toggle in Advanced settings section matching the desktop UI checkbox. EN + FA string resources for label and helper text.

Closes #520 (vampire137 Android youtube_via_relay request).

Local verification: cargo test --lib 169/169 passing, both release builds clean. Pure Kotlin/Android change; no Rust impact.
2026-04-30 19:57:13 +03:00
therealaleph c98ae73c92 ci(telegram-publish): use --clobber to survive partial-download retries
The v1.9.2 telegram publish failed because attempt 1 hit a transient
HTTP 500 from GitHub's release-asset CDN mid-download, leaving partial
files in assets/. Attempts 2 and 3 then errored on "already exists"
because gh release download refuses to overwrite without --clobber.

With --clobber, retries can complete cleanly even when an earlier
attempt left files behind. No change to the success path.
2026-04-30 19:55:46 +03:00
yyoyoian-pixel 576dfa69e4 feat(android): add youtube_via_relay toggle to Advanced settings
Expose the `youtube_via_relay` config flag in the Android UI, matching
the desktop checkbox. Adds the field to MhrvConfig with serialization
round-trip (toJson / loadFromJson / encode), a Switch in the Advanced
section, and EN + FA string resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 16:28:53 +02:00
github-actions[bot] d162a47e3c chore(releases): refresh prebuilt binaries for v1.9.2
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
2026-04-30 14:21:32 +00:00
therealaleph 8d0004c5f0 chore: v1.9.2 — Apps Script + Cloudflare Worker alternative backend
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.
v1.9.2
2026-04-30 17:02:28 +03:00