Commit Graph

359 Commits

Author SHA1 Message Date
therealaleph 9e9a7d13f3 chore(release): v1.9.25 — udpgw virtual-DNS fix + LibreWolf cert install (#251, #1145)
v1.9.25 ships two bug fixes from @dazzling-no-more:

- #1143 (#251): Android Full-mode `udpgw magic IP` moved from
  198.18.0.1 → 192.0.2.1 to avoid clash with tun2proxy's virtual-DNS
  allocator range. Resolves "Google + most websites silently broken
  while Telegram works" on Android Full mode. Back-compat: legacy IP
  still recognised by tunnel-node for one deprecation cycle.
- #1159 (#1145): MITM CA now installs into LibreWolf NSS stores
  alongside Firefox. Closes `MOZILLA_PKIX_ERROR_MITM_DETECTED` HSTS
  lockout on LibreWolf. Same class as already-closed #955/#959.

Cargo.toml bump (1.9.24 → 1.9.25) came in via #1143. This commit
amends the pre-baked v1.9.25 changelog to include #1159 and refreshes
Cargo.lock.

239 lib tests + 38 tunnel-node tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.25
2026-05-13 22:48:46 +03:00
dazzling-no-more 108b0718c4 fix(cert): install MITM CA into LibreWolf NSS stores (#1145, #1159)
Closes #1145. LibreWolf users were getting `MOZILLA_PKIX_ERROR_MITM_DETECTED` when visiting HSTS-protected sites (bing.com, youtube.com, …) through MasterHttpRelayVPN's MITM mode. HSTS gives no "Add Exception" affordance, so users were fully locked out of those sites despite the OS-level CA install having succeeded.

**Root cause**: `cert_installer.rs` only scanned Firefox profile roots (`~/.mozilla/firefox`, the snap variant, `%APPDATA%\Mozilla\Firefox\Profiles`, `~/Library/Application Support/Firefox/Profiles`). LibreWolf is a Firefox fork with strict privacy defaults; it shares Firefox's NSS DB layout and respects the same `security.enterprise_roots.enabled` pref, but stores its profile tree under its own app dir. Neither the per-profile `certutil -A` install nor the `user.js` enterprise-roots auto-trust fallback ever touched LibreWolf, so the browser never trusted our CA.

Same failure mode behind already-closed #955 and #959 (Firefox-fork users reporting the identical "secure connection could not be established" symptom).

**Fix**: extend Mozilla-family profile discovery to cover LibreWolf on every supported platform. No behavioural change for Firefox installs.

## Changes (`src/cert_installer.rs`-only)

- Renamed `firefox_profile_dirs()` → `mozilla_family_profile_dirs()`. Same flat-vec return type so all five call sites read identically; the rename is signposting only.
- Extracted `mozilla_family_profile_roots(os, home, appdata, xdg_config_home)`: returns the union of Firefox + LibreWolf profile root directories, per-OS:
  - **Linux**: `~/.mozilla/firefox`, snap variant, `~/.librewolf`, `$XDG_CONFIG_HOME/librewolf` (LibreWolf respects XDG by default).
  - **macOS**: `~/Library/Application Support/Firefox/Profiles`, `~/Library/Application Support/LibreWolf/Profiles`.
  - **Windows**: `%APPDATA%\Mozilla\Firefox\Profiles`, `%APPDATA%\LibreWolf\Profiles`.
- All five existing call sites (per-profile install, enterprise-roots fallback, uninstall, dry-run reporter, test-mode reporter) read from the renamed function without further changes.

## Verified locally (on top of v1.9.24)

- `cargo test --lib --release`: **239/239**  (was 231; this PR adds 8 new tests covering LibreWolf-path discovery on each OS).
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 

## Will combine with #1143

PR #1143 already pre-baked the v1.9.25 release files (Cargo.toml + changelog). This PR doesn't touch either, so the squash-merge will land cleanly alongside #1143's changes. Will edit v1.9.25's changelog to include #1159 as a second bullet before tagging.

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-13 22:47:48 +03:00
dazzling-no-more e70947ff0d fix(udpgw): move magic IP out of tun2proxy virtual-DNS range (#251, #1143)
Closes #251. In Android Full mode, Telegram worked but Google search and most other websites failed silently. `apps_script` mode on the same setup was unaffected.

**Root cause**: the udpgw magic destination (`198.18.0.1:7300`) was inside `198.18.0.0/15` — the exact range tun2proxy's `--dns virtual` allocator uses to synthesise fake IPs for hostname lookups. Whenever virtual DNS assigned `198.18.0.1` to a real hostname, that hostname's traffic was intercepted by tun2proxy *itself* as a udpgw connection and dropped. Telegram was immune because it uses hardcoded numeric IPs; `apps_script` mode was immune because it never sets `--udpgw-server`.

**Fix**: move `UDPGW_MAGIC_IP` to `192.0.2.1` (RFC 5737 TEST-NET-1) — outside any virtual-DNS allocation pool. Coordinated change across the tunnel-node constant and the Android `--udpgw-server` flag.

## Back-compat

v1.9.25 tunnel-nodes still recognise the legacy `198.18.0.1:7300` for one deprecation cycle (removal in v1.10.0).

| Android | Tunnel-node | Full-mode UDP |
|---|---|---|
| v1.9.25 | v1.9.25 |  fully fixed |
| ≤v1.9.24 | v1.9.25 | ⚠️ handshake works (legacy IP still recognised), but the old client still asks tun2proxy for `198.18.0.1`, so the #251 virtual-DNS collision is still live on-device |
| v1.9.25 | ≤v1.9.24 |  breaks silently (old node rejects `192.0.2.1`) |

The fix lives on the client side (which magic IP it asks tun2proxy to reserve). The back-compat is on the tunnel-node side (accepting both during the deprecation window).

## Verified locally

- `cargo test --lib --release`: 231/231 
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 
- `(cd tunnel-node && cargo test --release)`: 38/38  (+2 new tests for the IP change)

## Version bump

Cargo.toml already bumped to 1.9.25 in this PR; `docs/changelog/v1.9.25.md` pre-baked. Will combine with any other PRs landing into v1.9.25 before tagging.

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-13 22:45:23 +03:00
github-actions[bot] 283073fe77 chore(releases): refresh prebuilt binaries for v1.9.24
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-13 11:19:04 +00:00
therealaleph 1c9e73e4e7 chore(release): v1.9.24 — fix timeout cascade + Cloud Run docker build (#1088, #620)
Bumps v1.9.23 → v1.9.24. Two PRs from @dazzling-no-more:
- #1108 (#1088): batch header read honors request_timeout_secs.
  Closes the 10s inner timeout cliff that was cascade-killing tunnel
  sessions under slow Apps Script edges. +22 regression tests (231 total).
- #1117 (#620): cargo-chef Dockerfile so tunnel-node builds without
  BuildKit. Cloud Run's gcloud-deploy path now works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.24
2026-05-13 13:58:16 +03:00
dazzling-no-more 4d135a4e2f fix(tunnel): batch header read honors request_timeout_secs (#1088, #1108)
Fixes #1088 — under Full mode, a single slow Apps Script edge cascade-killed every in-flight tunnel session sharing its batch. Users on 1.9.21+ saw frequent 10s "batch timeout" errors and lost download progress on Telegram / browser sessions.

## Root cause

`read_http_response` in `domain_fronter.rs` had a **hardcoded 10s header-read timeout** that ran *inside* `tunnel_batch_request_to` — independent of and shorter than the outer `tokio::time::timeout(batch_timeout, …)` in `fire_batch`. Apps Script cold starts routinely land in the 8-12s range (PR #1040's A/B recorded 4/30 H1 batches timing out at exactly 10s after the H2→H1 switch), so the inner cliff fired as a false-positive batch timeout well before `request_timeout_secs` (default 30s) could.

Secondary: even with a parameterized timeout, the per-read `timeout(d, stream.read(...))` form would silently extend its budget if a peer drip-fed bytes just under `d` each — a slow edge could keep the loop alive past the outer `batch_timeout` and defeat the whole wiring.

## Fix (two changes in `domain_fronter.rs`)

1. **`tunnel_batch_request_to` passes `batch_timeout` to the header read** via new `read_http_response_with_header_timeout` helper. `Config::request_timeout_secs` is now the only knob controlling how long we wait for an Apps Script edge to start responding. Other callers (relay path, exit-node) keep the historical 10s value.

2. **Header read uses a single absolute deadline** (`tokio::time::timeout_at(deadline, …)`) instead of per-read `timeout()`. Total elapsed across all header reads is bounded by `header_read_timeout`, regardless of read cadence.

## Bonus (in `tunnel_client.rs`)

3. **`TunnelMux::reply_timeout` co-varies with `batch_timeout`**: computed at construction as `fronter.batch_timeout() + 5s slack` instead of the fixed 35s const. Operators raising `request_timeout_secs` no longer have sessions abandon `reply_rx` just before `fire_batch`'s HTTP round-trip would complete.

## Verified locally (on top of v1.9.23 / main after #1117 merge)

- `cargo test --lib --release`: **231/231**  (was 209 in v1.9.23 baseline; this PR adds 22 new tests covering the deadline/co-variance behavior)
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 

## Interaction with v1.9.20 (PR #1029)

PR #1029 added `H1_OPEN_TIMEOUT_SECS = 8` to bound the TCP+TLS handshake in `open()`. That bound is **separate** from the header-read timeout this PR addresses — both bounds exist in the same call chain. Issue #1131 (BuffOvrFlw, just opened) reports `h1 open timed out after 8s` errors which are the `open()` bound firing, not the header-read bound. Worth a follow-up to make `H1_OPEN_TIMEOUT_SECS` parameterized too, but that's a separate 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-13 13:57:05 +03:00
dazzling-no-more 4d2ce91c04 fix(docker): cargo-chef so tunnel-node builds without BuildKit (#620, #1117)
Fixes #620 — `tunnel-node/Dockerfile` used BuildKit-only `RUN --mount=type=cache` directives, breaking on Cloud Run's `gcloud run deploy --source .` path (the underlying `gcr.io/cloud-builders/docker` builder doesn't enable BuildKit, and `--set-build-env-vars DOCKER_BUILDKIT=1` doesn't flip it on either).

Reworked to use **cargo-chef**: a dedicated planner stage emits `recipe.json` for dependency metadata, a `cargo chef cook` stage builds just the deps in their own Docker layer, the final build stage adds `src/` on top. Docker's regular layer cache handles dependency reuse — warm rebuilds where only `src/` changes still skip the slow crate compile.

## Changes (`tunnel-node/Dockerfile`-only)

- Dropped `# syntax=docker/dockerfile:1` parser directive and all `RUN --mount=type=cache,...` blocks
- Added cargo-chef multi-stage build (`chef` → `planner` → `builder`)
- Pinned `cargo-chef` to exact `0.1.77` with `--locked` for reproducible installs
- Bumped base from `rust:1.85-slim` → `rust:1.90-slim` (cargo-chef's transitive deps require rustc 1.86+; tunnel-node's `Cargo.toml` has no `rust-version` pin so the bump is internal-only)
- Removed `ARG TARGETPLATFORM` per-platform cache-id workaround — Docker's regular layer cache is already arch-scoped

## Non-changes (deliberate)

- `tunnel-node/Cargo.toml` left alone — the old Dockerfile comment claimed "matches MSRV in Cargo.toml" but no `rust-version` field actually exists. The Docker base bump is internal build-env, not a declared MSRV.
- Base image digest pinning left on tag refs — without Renovate/Dependabot to keep digests fresh, pinning trades automatic glibc/openssl/ca-certificates CVE patching for a reproducibility property this repo doesn't currently need.

## Verified locally

- `cd tunnel-node && cargo build --release`: clean (binary side unchanged)
- `cd tunnel-node && cargo test --release`: 36/36
- Local `docker build` couldn't run (daemon not started on the dev machine); the PR author's test plan documents successful build under classic Docker daemon.

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-13 13:55:03 +03:00
github-actions[bot] ab05c5316f chore(releases): refresh prebuilt binaries for v1.9.23
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-12 13:15:52 +00:00
therealaleph ca24ebdc1e chore(release): v1.9.23 — stream large range-parallel downloads (#1042, #1085)
Bumps Cargo.toml v1.9.22 → v1.9.23. Ships @dazzling-no-more's PR #1085
which converts relay_parallel_range into a writer-based API that streams
files >50 MiB chunk-by-chunk instead of trying to buffer the whole
response and hitting Apps Script's body ceiling. Four-way dispatch
(Buffered / Stream / FallbackSingleGet / RejectTooLarge) with O(1)
memory range planning + a 16 GiB hostile-origin guard. 209 → 227 lib
tests (+18 new). Unblocks GitHub releases / large CDN binaries through
apps_script mode without needing Full mode or external mirrors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.23
2026-05-12 15:55:10 +03:00
dazzling-no-more aa16abcafc fix(relay): stream range-parallel downloads larger than Apps Script's 50 MiB cap (#1042, #1085)
Fixes #1042 — range-capable downloads larger than ~50 MiB through the
Apps Script relay returned `504 Relay timeout — Apps Script unresponsive`
instead of the file. The 104 MiB v2rayN DMG in the reported logs was the
canonical repro (also matches @Paymanonline's report in #1077, closed
prior as "architectural limit, use mirrors" — this PR makes it actually
work via streaming).

## Root cause

`relay_parallel_range` capped the stitched response at 64 MiB and fell
back to a single `relay()` for anything larger. Single-GET routes through
Apps Script's ~50 MiB response ceiling, so Apps Script killed the script
mid-execution and we hung for the full 25s relay timeout before returning
504.

## Fix

Convert `relay_parallel_range` into a writer-based API that streams large
files chunk-by-chunk to the client socket. Each chunk is still one
≤256 KiB Apps Script call (well under the 50 MiB cap); only the host-side
buffering changes. Backward-compatible `Vec<u8>` wrapper preserves the
pre-1.9.23 API surface for external library consumers.

Three-way dispatch via `RangeDispatch { Buffered, Stream, FallbackSingleGet,
RejectTooLarge }` and the pure `dispatch_range_response(total,
streaming_allowed)` predicate:

- **`Buffered`** — `total ≤ APPS_SCRIPT_BODY_MAX_BYTES` (40 MiB) on either
  surface. Existing stitch + single-GET fallback path; fully recovers on
  chunk failure.
- **`Stream`** — writer API above 40 MiB. Streams; chunk failure flushes
  the committed prefix and returns `Err` so the `Content-Length`
  mismatch tells download clients to resume via `Range`.
- **`FallbackSingleGet`** — wrapper above 64 MiB. Matches pre-1.9.23 cliff
  for external library consumers stuck on the old API.
- **`RejectTooLarge`** — writer API above 16 GiB. Refuses with 502;
  bounds worst-case Apps Script quota drain from a hostile origin
  advertising an absurd `Content-Range` total.

## Memory bounds

Lazy `plan_remaining_ranges` (via `std::iter::from_fn` + `saturating_*`):
range planning is `O(1)` memory regardless of advertised total. Even a
`u64::MAX` total no longer drives a ~6 GB `Vec<(u64, u64)>` allocation.

## CORS interaction

MITM HTTPS and plain-HTTP call sites updated to use `relay_parallel_range_to`
with a CORS-aware `transform_head` closure. Extracted `inject_cors_into_head`
(head-only variant of `inject_cors_response_headers`) so the streaming
path can rewrite ACL headers before the body has been assembled.

## Verified locally on top of v1.9.22

- `cargo test --lib --release`: 227/227  (was 209; +18 new — 15 stated
  in PR body + 3 incidental from the helper extractions)
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 

Manual repro of the 104 MiB v2rayN DMG download is unchecked in the PR
test plan — the unit tests cover the dispatch + streaming + flush
contracts thoroughly. The architectural reasoning is sound and the new
test count (+18) is concrete.

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 15:54:17 +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
github-actions[bot] 5cfb972deb chore(releases): refresh prebuilt binaries for v1.9.22
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-11 13:25:37 +00:00
therealaleph ee54e03704 chore(release): v1.9.22 — complete H2 skip for tunnel_request single ops (#1041)
Bumps Cargo.toml v1.9.21 → v1.9.22. Ships @yyoyoian-pixel's PR #1041
which completes #1040 — v1.9.21 skipped H2 for tunnel_batch_request_to
but missed tunnel_request (single-op connect path). 5/5 h2_relay_request
call sites now audited; all full-tunnel paths use H1, relay paths keep
H2. 209 lib tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.22
2026-05-11 16:05:51 +03:00
yyoyoian-pixel 2d5bd340c6 fix: skip H2 for tunnel_request (single ops) — completes #1040 (#1041)
Completes [#1040](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/pull/1040) (v1.9.21). #1040 skipped H2 for `tunnel_batch_request_to` but missed `tunnel_request` — the single-op path used for plain `connect` ops. The 16-17s long-poll stalls persisted on full-tunnel sessions that go through the single-op path; this PR closes that gap.

Same fix shape: remove the H2 try/fallback/NonRetryable block from `tunnel_request`, go straight to H1 pool `acquire()`. H2 remains active for relay-mode paths (`do_relay_once_with`, exit-node `relay()`).

## All h2_relay_request call sites audited

| Call site | Function | Mode | H2 skipped? |
|---|---|---|---|
| `do_relay_once_with` | relay | Relay | No (correct — relay benefits from H2) |
| `relay()` exit-node | relay | Relay | No (correct) |
| `tunnel_request` | tunnel single op | Full tunnel | **YES — this PR** |
| `tunnel_batch_request_to` | tunnel batch | Full tunnel | Yes (PR #1040) |
| `tunnel_batch_request_with_timeout` | tunnel batch | Full tunnel | Yes (PR #1040) |

No other full-tunnel paths use H2 after this fix.

## Verified locally on top of v1.9.21

- `cargo test --lib --release`: 209/209 
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 

Reviewed via Anthropic Claude.

Co-Authored-By: yyoyoian-pixel <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:04:15 +03:00
github-actions[bot] b259dd0df1 chore(releases): refresh prebuilt binaries for v1.9.21
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-11 00:02:26 +00:00
therealaleph 4c8cf9ac16 chore(release): v1.9.21 — skip H2 for full-tunnel batches (#1040)
Bumps Cargo.toml v1.9.20 → v1.9.21 and ships the changelog. Headline:
PR #1040 (@yyoyoian-pixel) skips H2 multiplexing for tunnel_batch_request_to
(Full-mode batch path). Tunnel batches already coalesce N ops into one
HTTP request, so H2 stream multiplexing has nothing to multiplex there;
the H2 path was actively hurting via long-poll stalls + silent batch
drops + pool starvation. H2 remains active for relay mode (apps_script),
where r0ar's #962 A/B data confirmed it's strictly better. A/B on Pixel
6 Pro: 0/30 vs 8-10/30 long-poll stalls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.21
2026-05-11 02:42:47 +03:00
yyoyoian-pixel dd7b3553ec perf: skip H2 for full-tunnel batch requests (#1040)
Skip H2 for `tunnel_batch_request_to` (the Full-mode batch path).
Tunnel batches already coalesce N ops into one HTTP request — H2 stream
multiplexing has nothing to multiplex. The H2 try/fallback path on this
specific code path introduced three regressions vs v1.9.14:

1. **Long-poll stalls**: idle polls completed at 16-17s (`LONGPOLL_DEADLINE`
   + network latency) instead of timing out at 10s on H1. Each poll held
   an Apps Script execution slot 60% longer.
2. **Silent batch drops**: `RequestSent::Maybe` failures dropped the
   entire batch with no retry — a failure mode H1 doesn't have.
3. **Pool starvation**: `POOL_MIN_H2_FALLBACK = 2` trimmed the H1 pool
   from 8 → 2 once H2 connected, but tunnel batches still used H1 and
   needed the full pool.

H2 multiplexing is **kept active for relay mode** (non-full) where each
browser request is a separate HTTP call that genuinely benefits from
stream multiplexing. r0ar's controlled A/B test in #962 confirmed h2
is strictly better than `force_http1: true` for apps_script-mode users,
and that path is unchanged here.

## Changes

- `tunnel_batch_request_to`: remove H2 try/fallback/NonRetryable block,
  go straight to H1 pool `acquire()` (-54 lines).
- `run_pool_refill`: always maintain `POOL_MIN = 8`. Remove the
  `POOL_MIN_H2_FALLBACK = 2` trim that was starving tunnel batches
  (-12 lines).

## A/B results (Pixel 6 Pro, 30 batch samples each)

| Metric | H2 (stock v1.9.20) | H1 (this PR) | v1.9.14 (baseline) |
|---|---|---|---|
| 16-17s batches | **8-10/30** | **0/30** | **0/30** |
| 10s timeouts | 0 | 4/30 | 5/30 |
| Active RTTs | 1.4-2.4s | 1.3-2.2s | 1.4-2.3s |

Restores v1.9.14 tunnel performance while keeping all v1.9.15+
improvements (H2 for relay, zero-copy mux, block DoH/QUIC, TLS pool
tuning, PR #1029's warm-race fix).

## Verified locally on top of v1.9.20

- `cargo test --lib --release`: 209/209  (matches v1.9.20 baseline)
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 

## Interaction with PR #1029 (just shipped in v1.9.20)

PR #1029 added `H2Cell.dead: Arc<AtomicBool>` for synchronous dead-cell
detection. With this PR removing the H2 path for tunnel batches, the
dead-cell flag is no longer consulted on the tunnel batch path — that's
intentional (the flag now scopes to relay mode, which is the path it
was protecting in practice).

Reviewed via Anthropic Claude.

Co-Authored-By: yyoyoian-pixel <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:41:59 +03:00
github-actions[bot] 9611279fbd chore(releases): refresh prebuilt binaries for v1.9.20
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-10 21:57:22 +00:00
therealaleph 786a9703c9 chore(release): v1.9.20 — fix Full-mode warm-up race (#924, #1029)
Bumps Cargo.toml v1.9.19 → v1.9.20 and ships the changelog. Headline
fix: the v1.9.15 Full-mode regression that's been tracking in #924 for
~3 weeks is resolved by @rezaisrad's PR #1029. Bisect-quality root
cause (h1 prewarm gated behind h2 handshake, both stall on cold start
under the same network conditions). Affected users can drop the
`force_http1: true` workaround now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.20
2026-05-11 00:37:38 +03:00
Reza Rad 4d3e62195c fix: v1.9.15 full-mode warm-up race during h2 init (#924, #1029)
Fixes #924 — the canonical tracking thread for the v1.9.15 Full-mode
regression cluster that spanned ~3 weeks and 18+ duplicate reports.

**Root cause** (rigorously bisected to PR #799's `warm()`):
PR #799 added HTTP/2 multiplexing on the relay leg, and gated the h1
socket-pool prewarm behind `ensure_h2().await`. `ensure_h2()` is bounded
by `H2_OPEN_TIMEOUT_SECS = 8s` but can take the full window on a cold
connection. During that window the h1 fallback pool is empty, so any
request that arrives gets:

1. `Err((Relay("h2 unavailable"), No))` immediately → falls back to h1
2. h1 path calls `acquire()` → empty pool → cold `open()` → fresh
   TCP+TLS handshake to `connect_host:443`
3. Same network conditions that stalled h2 also stall h1; cold open
   exceeds the 30s `batch_timeout` enforced in `dispatch_full_tunnel`
4. User sees `batch timed out after 30s` while apps_script mode keeps
   working

**Fix** (two commits, both `domain_fronter.rs`-only):

Commit 1 — `warm h1 pool in parallel with h2`:
Spawn h2 prewarm in a separate task so the h1 prewarm loop runs
concurrently. Full `n` h1 sockets are warm before user traffic, even
when the h2 handshake stalls or hits its 8s timeout. `run_pool_refill`
trims the pool back to `POOL_MIN_H2_FALLBACK = 2` within 5s once h2
lands as the fast path.

Commit 2 — `bound h1 open() + detect dead h2 cells synchronously`:
- `H1_OPEN_TIMEOUT_SECS = 8` wraps the TCP+TLS handshake in `open()`
  so a stuck handshake to a blackholed `connect_host:443` doesn't
  block `acquire()` until the outer 30s batch budget elapses (same
  symptom #924 hits during the warm-race window).
- `H2Cell.dead: Arc<AtomicBool>` flipped by the connection driver task
  when `Connection::await` ends (GOAWAY, network error, normal close).
  `ensure_h2`'s fast path and `run_pool_refill`'s pool-target check
  both consult the flag — known-dead cells are rejected within ≤5s
  instead of waiting for `H2_CONN_TTL_SECS = 540s` to expire or for a
  request to discover the breakage via `ready()` failure.

**API impact**:
`h2_handshake_post_tls`'s return type changes to `(SendRequest, Arc<AtomicBool>)`.
One existing test (`h2_handshake_post_tls_returns_alpn_refused_when_peer_picks_h1`)
tweaks its `Ok` arm to match — panic message unchanged.

**Verified locally on top of v1.9.19**:
- `cargo test --lib --release`: 209/209 (was 208; +1 new test
  `ensure_h2_rejects_dead_cell_within_ttl`)
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean
- `(cd tunnel-node && cargo test --release)`: 36/36

**Live end-to-end** (from PR description):
- 5 cold restarts (warm-up race window): 5/5 pass, 9.6-22.5s
- Concurrent burst (5 simultaneous SOCKS5 streams): 5/5
- Default full.json baseline: 200 OK in 13.3s
- `force_http1: true` sanity: 200 OK in 17.7s

**A/B vs PR #903** (per-session pipelining): commits land in disjoint
functions, cherry-picked clean on top. If #903 lands first, this needs
a mechanical rebase only.

Exemplary debugging work — bisect with concrete probe data, root cause
identification down to specific commit + line, working fix with bounded
timeouts on the two adjacent paths the same stall pattern could recur
through, +1 regression test. The kind of PR that lands fast.

Closes #924.

Reviewed via Anthropic Claude.

Co-Authored-By: rezaisrad <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:36:49 +03:00
github-actions[bot] 24534f7827 chore(releases): refresh prebuilt binaries for v1.9.19
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-10 14:21:11 +00:00
therealaleph 907a492cd8 chore(release): v1.9.19 — UI a11y labels for NVDA / Narrator (#1015)
Bumps Cargo.toml v1.9.18 → v1.9.19 and ships the changelog. User-visible
binary changes since v1.9.18:

- UI a11y: widgets now associate with visible labels via
  `.labelled_by(label_id)`, so NVDA / Narrator read the field name
  instead of just the control type. Fixes #916. Verified by
  @brightening-eyes (the blind user who reported the issue).

Also rolling up the exit_node Content-Encoding fix (c437598 / #964) in
the changelog — that's an asset-only commit (exit_node.ts), no Rust
binary change for it, but worth flagging in the release notes so
existing exit-node users know to redeploy their .ts script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.19
2026-05-10 17:01:36 +03:00
brightening-eyes 5d69079f1e feat(ui): label widgets with .labelled_by for NVDA / Narrator (#1015)
Adds `egui::Id` plumbing through `form_row` so each widget can call
`.labelled_by(label_id)` to associate with its visible label. NVDA /
Narrator now reads the field name when focus moves to a control,
instead of just announcing the control type. AccessKit was already
enabled in `Cargo.toml` (`eframe` features), but without the explicit
`labelled_by` association, the screen reader had no way to map the
text input or combobox to its preceding label.

Verified locally on top of v1.9.18 / main:
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean
- `cargo test --lib --release`: 208/208

Tested by @brightening-eyes (the blind user who originally reported
#916) with NVDA — confirmed working.

`form_row`'s signature now takes `widget: impl FnOnce(&mut egui::Ui,
egui::Id)`. Two existing callers that don't need the label_id (the
`Mode` combobox, `Share on LAN` checkbox) ignore it via `_label_id`
binding — no functional change there.

Closes #916.

Reviewed via Anthropic Claude.

Co-Authored-By: brightening-eyes <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:00:41 +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
github-actions[bot] c774851b2d chore(releases): refresh prebuilt binaries for v1.9.18
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-08 03:16:05 +00:00
therealaleph 3e599f29e8 chore(release): v1.9.18 — perf: zero-copy mux + base64 off mux thread (#881)
Bumps Cargo.toml v1.9.17 → v1.9.18 and ships the changelog for the
zero-copy mux refactor merged in 54552bb. No user-visible behavior
change; perf-focused release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.9.18
2026-05-08 05:56:41 +03:00
dazzling-no-more 54552bbdac perf(tunnel): zero-copy mux + base64 off mux thread (#881)
Performance refactor of full-tunnel mode hot path. No wire-protocol or
behavior changes — internal data flow only.

**1. Zero-copy reads via `Bytes`/`BytesMut`**
`tunnel_loop` and the SOCKS5 UDP receive loop drop their per-iteration
`Vec::to_vec()` copies. `MuxMsg::{ConnectData,Data,UdpOpen,UdpData}` now
carry `Bytes` instead of `Vec<u8>`/`Arc<Vec<u8>>`; the `Arc::try_unwrap`
dance for `pending_client_data` is gone (Bytes is already Arc-backed).

TCP path is threshold-based to avoid the obvious memory regression:
- n ≥ 32 KB: `BytesMut::split().freeze()` — saves the 64 KB memcpy on
  hot downloads.
- n < 32 KB: `Bytes::copy_from_slice` + `buf.clear()` — payload-sized
  retention. Without this split, a queued tiny TLS record would refcount-
  pin the full 64 KB recv buffer (worst case ~96 MB on a backpressured
  tunnel).

UDP path: fixed `Vec<u8>` recv buffer + `Bytes::copy_from_slice` after
the 9 KB MAX_UDP_PAYLOAD_BYTES guard. `parse_socks5_udp_packet` split
into `_offsets` + `&[u8]` wrapper so callers stay on the reusable buffer.

**2. Base64 encoding moved off the single mux thread**
New internal `PendingOp { data: Option<Bytes>, encode_empty: bool }`
flows through `mux_loop` with raw bytes. Actual `B64.encode(...)` runs
in `fire_batch`'s spawned task, after the per-deployment semaphore. Up
to ~3 MB of encoding per batch (50 ops × 64 KB) no longer serializes
the single mux task.

**3. Code quality**
- `BatchAccum::push_or_fire` collapses 4× ~25-line match arms → ~10 each.
- `should_fire(pending_len, payload_bytes, op_bytes)` extracted with
  `saturating_add` for a self-contained contract.
- `encode_pending(p) -> BatchOp` extracted as a free function so the
  encoding contract is directly testable.

**Tests:** 208/208 (was 200, +8 new):
- `encode_pending_*` × 4 — base64-encode contract per MuxMsg variant
- `should_fire_*` × 3 — first-op, MAX_BATCH_OPS boundary, payload cap
- `batch_accum_reindexes_after_flush` — regression test for post-flush
  reply index lookup in `fire_batch`

**Public API:** `TunnelMux::udp_open` and `udp_data` now take
`data: impl Into<Bytes>` instead of `Vec<u8>`. Existing call sites
keep compiling.

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-08 05:25:48 +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
github-actions[bot] 834785deb5 chore(releases): refresh prebuilt binaries for v1.9.17
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-07 18:16:39 +00:00
therealaleph 141cd6c7a8 feat: v1.9.17 — CORS response header injection (#561 / YouTube comments)
mhrv-rs already short-circuits CORS preflight (OPTIONS → 204 with permissive ACL headers, no relay round-trip). What was missing: the actual cross-origin fetch that follows the preflight also needs CORS-compliant headers on the response, or the browser drops the response and the JS layer sees a CORS failure even though the relay succeeded.

Apps Script's UrlFetchApp.fetch() preserves the destination's response headers inconsistently — sometimes the origin returns `Access-Control-Allow-Origin: *` (which is incompatible with `Allow-Credentials: true`), sometimes drops ACL headers entirely. The visible symptom is YouTube comments not loading + the "restricted mode" error surfacing on responses the browser silently rejected before the JS handler could read them.

Fix: after the relay returns, if the original request had an `Origin` header, we strip any `Access-Control-*` headers the destination emitted and inject a fresh permissive set echoing the request's origin (required for credentialed fetches; `*` is invalid alongside Allow-Credentials).

The body is preserved byte-for-byte; only the header block before the first \r\n\r\n is rewritten. Malformed responses (no header/body separator) round-trip unchanged so we never corrupt non-HTTP/1.x bytes.

Idea credit: ThisIsDara/mhr-cfw-go — Go rewrite of upstream Python's CFW variant added the same fix; reviewing their code surfaced the gap in mhrv-rs. Their other claimed improvements (HTTP/2, connection pooling, request coalescing, response caching, range-parallel) are already in mhrv-rs.

Tests: 200 lib (was 197, +3 covering wildcard-origin replacement, non-ACL header preservation, malformed-response passthrough) + 36 tunnel-node green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:56:48 +03:00
github-actions[bot] 5a4f535f90 chore(releases): refresh prebuilt binaries for v1.9.16
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-07 16:48:06 +00:00
therealaleph 2c9c693d13 fix: v1.9.16 — Full mode 50 MiB batch-response truncation (#863)
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>
v1.9.16
2026-05-07 19:25:50 +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
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