feat(client): adaptive batch coalescing with configurable UI#2
Open
yyoyoian-pixel wants to merge 69 commits intomainfrom
Open
feat(client): adaptive batch coalescing with configurable UI#2yyoyoian-pixel wants to merge 69 commits intomainfrom
yyoyoian-pixel wants to merge 69 commits intomainfrom
Conversation
…ecycle-reliability fix(android): tighten VPN session lifecycle reliability
Five small but real Android-only fixes: 1. Connect/Disconnect button gated on VpnState.isRunning state-flow with 12s backstop, replacing the fixed 2s transitionCooldown timer. Closes the race where a tap-after-Stop hit "Address already in use" because the previous teardown's listener-socket release wasn't done. 2. Tun2proxy.stop() wrapped in 2s join() — if the native call hangs, bounded teardown still releases the listener port instead of holding the teardown thread. 3. fd-leak fixed between parcelFd.detachFd() and Thread.start(): an OOM-thrown Thread.start used to orphan the detached fd. Now adopted into a fresh ParcelFileDescriptor purely so we can close() it. 4. Misleading teardown doc-comment rewritten — the "step 2 closes the TUN fd to force EBADF on read" claim has been factually wrong since detachFd landed. 5. Recursive crash trap: Log.e in MhrvApp's uncaught handler now wrapped in try/catch so a logd failure during exception logging falls through to the previous handler with the real exception. No Rust changes; 98 lib + 22 tunnel-node tests still pass. Local Android build verified, APK installed on mhrv_test emulator, launches cleanly with v1.6.1 in title. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In `relay_parallel_range`, when a chunk failed validation (`extract_exact_range_body` returned Err) OR the stitched body length didn't match the advertised total, the fallback path called `rewrite_206_to_200(&first)` — which converted the 256 KiB probe response into HTTP 200 + Content-Length=262144 and returned that as if it were the full file. Browsers saw a complete-looking 200 and treated the download as finished at 256 KB. Common triggers for the chunk-validation failure (per the user reports): - Apps Script's UrlFetchApp stripping `Content-Range` from chunk responses while preserving it on the probe - Origin returning 200-OK on follow-up Range requests (some servers flatten ranges after the first one) - Mismatched `total` field across chunks for paths behind a varying cache layer The correct fallback is a single GET without any Range header — Apps Script fetches the whole URL (up to its 50 MiB cap) and returns a normal 200 with the complete body. Slower than parallel for large files but produces a correct response, which is the minimum bar. Two independent reports (Ehsan in therealaleph#162, Recruit1992 confirming). 98 lib tests still pass; existing `validate_probe_range_rejects_*` and `extract_exact_range_body_*` tests already cover the validation side, the fallback path is observed integration-testing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buildNotif() hardcoded `proxyPort + 1` for the SOCKS5 line, ignoring cfg.socks5Port entirely. With the default Android config (listenPort=8080, socks5Port=1081) the foreground notification read "Routing via SOCKS5 127.0.0.1:8081" but the real listener was on 1081 — so users configuring per-app SOCKS5 (Telegram, etc.) against the notification value silently failed. Use the same `cfg.socks5Port ?: (cfg.listenPort + 1)` elvis fallback the real listener uses, and surface both ports in the notification: HTTP 127.0.0.1:8080 · SOCKS5 127.0.0.1:1081 Reported by vpnineh and l3est (with netstat screenshots showing the exact mismatch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The batch-build loop blocked on a 30 ms timeout for the first message, then drained whatever else was in the channel via try_recv() and fired the batch. Under any non-bursty workload, the channel queue was always empty by the time the first op woke us up — so every "batch" had exactly one op, defeating the entire batching premise. Reporter (w0l4i) saw `batch: 1 ops → ..., rtt=6.3 s` repeating in logs even under high concurrency. Fix: after the first op lands, hold the buffer open for an 8 ms coalescing window. Concurrent ops (parallel fetches, HTTP/2 stream openings, etc.) now accumulate into the same batch. 8 ms is rounding error against the 2–7 s Apps Script RTT we're amortizing, and restores the multi-op-per-batch behavior the rest of the code already supports (MAX_BATCH_OPS=50, MAX_BATCH_PAYLOAD_BYTES=4 MiB). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: include twitter.com in X.com URL normalization (therealaleph#245)
- therealaleph#245 (@Parsa307): match twitter.com in X.com URL normalization - therealaleph#255 (@dazzling-no-more): copy-logs button + selectable log lines on Android - therealaleph#257 (@dazzling-no-more): bulk paste of multiple deployment IDs on Android - therealaleph#256 (@dazzling-no-more): plain HTTP proxy passthrough in google_only mode (used to return 502; now falls through to direct TCP / upstream_socks5, matching the existing CONNECT behavior) No protocol or wire-format changes; existing config and Apps Script deployments work unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…table VoIP, faster browsing - needs new tunnel deployment for udpgw (therealaleph#222) * feat: native udpgw protocol alongside existing UDP associate Why udpgw is needed even with UDP associate: UDP associate (udp_open/udp_data) creates one tunnel session per UDP destination and polls each independently. On high-latency or shaky networks this compounds — N simultaneous UDP flows need N separate polling loops, each paying its own batch round-trip overhead. Google Meet calls, which fire dozens of concurrent STUN + RTP flows, stall or fail entirely because the per-destination polling can't keep up. udpgw multiplexes ALL UDP over one persistent TCP-like session using conn_id framing. One batch op carries frames for many destinations. Persistent sockets per (conn_id, dest) with continuous reader tasks keep source ports stable — critical for protocols like Telegram VoIP and STUN that expect replies on the same port. Both paths coexist — they serve different traffic: - UDP associate (SOCKS5): apps that negotiate SOCKS5 UDP relay - udpgw (198.18.0.1:7300): TUN-captured UDP (DNS, QUIC, Meet, etc.) tun2proxy vendored as git submodule at v0.7.20 with one transparent commit adding udpgw_server to the Android JNI run() function. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: block QUIC (UDP 443) and DNS (UDP 53) from udpgw QUIC through udpgw is slower than TCP/HTTP2 through the batch pipeline — blocking it forces browsers to fall back to TCP, improving YouTube and general browsing speed. DNS is better handled by tun2proxy's virtual DNS / SOCKS5 UDP associate path which is more reliable for single request-response exchanges. VoIP (Telegram, Meet) still flows through udpgw normally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace submodule with [patch.crates-io] for tun2proxy udpgw Use the idiomatic Rust [patch.crates-io] mechanism instead of a git submodule. Points to yyoyoian-pixel/tun2proxy fork with the udpgw JNI parameter patch (upstream PR: tun2proxy/tun2proxy#247). Will be removed once upstream ships the change in tun2proxy >= 0.8. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: pin tun2proxy patch SHA in Cargo.lock Locks tun2proxy at dfc24ed1 so the patch resolution is recorded and any branch rewrite is visible in the lockfile diff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use AbortHandle for ConnSocket readers to prevent FD leaks JoinHandle::drop detaches the task without aborting it. When udpgw_server_task is cancelled (session close), the post-loop cleanup never runs and per-(conn_id, dest) reader tasks become zombies holding Arc<UdpSocket> file descriptors. AbortHandle::drop aborts the task automatically, so cleanup is correct by construction regardless of how the parent task exits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Highlights: - Native udpgw protocol in Full mode (therealaleph#222) — Telegram voice/video calls and Google Meet now work in Full mode on Android. UDP flows through one persistent TCP tunnel (instead of session-per-destination) so STUN/RTP flow counts no longer stall. Requires redeploying the tunnel-node Docker image (ghcr.io/therealaleph/mhrv-tunnel-node:1.7.0). - Android home screen restructure (therealaleph#258, closes therealaleph#246) — Connect button now pinned under Mode field, App picker shows pre-selected apps at top. With long deployment-ID lists, Connect no longer scrolls off-screen. - release-drafter + prepare-release tooling (therealaleph#260) — incrementally drafts release notes from merged PR titles; manual workflow_dispatch prepares version bumps + changelog stubs. No protocol breaking changes; existing apps_script-mode and Full-mode deployments work unchanged. Full-mode users get udpgw automatically once the tunnel-node Docker image is updated. Thanks to @yyoyoian-pixel and @dazzling-no-more. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te revocation (therealaleph#121) * feat(cert): add --remove-cert flag and Remove CA button for clean-slate revocation * fix(cert): testable euid-root branch + orphan enterprise_roots warning
mhrv-rs --remove-cert (CLI) and Remove CA button (UI) for verified clean-slate revocation. Clears OS trust store, NSS browser stores (Linux Firefox/Chrome), and the on-disk ca/ directory. config.json and the Apps Script deployment are untouched. By-name trust verification runs before browser-state mutation; OS removal failures return RemovalIncomplete with browser state intact so retries are idempotent. Sudo-aware on Unix (re-roots HOME to the real user). 29 new unit tests on the pure logic (Firefox user.js marker handling, getent passwd parsing, NSS stderr classification, NssReport state rules). Tested end-to-end on Windows by the contributor; macOS verified at merge time on real hardware (login keychain delete + NSS-missing fallback). Linux paths await user testing. Closes therealaleph#121. Thanks @dazzling-no-more. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Telegram release notifier used to post just the universal APK with a single-document caption. This change ships the per-platform binaries for macOS (amd64+arm64 CLI), Linux (amd64+arm64 CLI), Windows (amd64 UI), and Android (universal APK) as a single Telegram media group with one caption listing every filename + SHA-256. Workflow side (.github/workflows/release.yml): - The telegram job now downloads ALL artifacts (was: APK only). - New `Prepare files for Telegram media group` step extracts the raw binaries out of each per-platform .tar.gz / .zip (no archive wrappers in the channel) and renames them with version suffixes (mhrv-rs-linux-amd64-v1.7.2, mhrv-rs-windows-amd64-ui-v1.7.2.exe, etc.). Per-platform extraction is best-effort: a missing artifact emits a `::warning::` and skips that platform rather than failing the whole post. - The post step builds a `--files <path>` arg list from tg-files/, sorted for deterministic order across runs, and invokes the notifier without --with-changelog (the script auto-replies with changelog whenever --files is used). Script side (.github/scripts/telegram_release_notify.py): - New --files arg (repeatable). 2..=10 files → sendMediaGroup; 1 file → sendDocument with the same caption shape; 0 → error. Telegram's sendMediaGroup rejects single-item groups, so the 1-file fallback isn't optional. - New build_media_group_caption() composes title + per-file filename+SHA list + repo/release URLs. Fits ~860 chars for a 6-file release; fallback to filename-only-list if a future swell pushes past Telegram's 1024-char caption cap. - send_media_group() handles the multipart/form-data shape with each file referenced as `attach://fileN` from the media JSON. Caption is attached to file 0 only (Telegram clients render per-item captions inconsistently for media groups; first-item-only is the safe pattern). - Legacy --apk path kept for any caller that hasn't migrated; either --apk or --files must be present (validated at startup). - _content_type_for() picks application/vnd.android.package-archive for .apk and application/octet-stream for everything else, so Telegram clients label the APK with the Android icon and label desktop binaries by filename without a misleading icon. Behavioural change for users: - The Telegram channel now sees one grouped post per release with all primary platform binaries inline, instead of just the APK. macOS users wanting the gatekeeper-friendly .app.zip still grab it from the GitHub Releases page; the Telegram drop is for the "give me the binary, I'll run it" path. - The Persian/English changelog reply that used to be opt-in (via TELEGRAM_INCLUDE_CHANGELOG=true) is now automatic in the --files path because the per-file SHA list eats the caption budget that previously held the FA brief-note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…therealaleph#266) * feat(android): config import/export via clipboard, QR code, deep link, and share sheet - Clipboard paste: banner auto-detects mhrv:// or raw JSON in clipboard, one tap to import. Clipboard cleared after successful import. - Export dialog: QR code + compressed hash + copy button + Android share sheet (sends QR image + text together). - QR scanner: ZXing embedded scanner in portrait orientation. - Deep link: mhrv:// URIs auto-open the app and import the config. - Compact encoding: only non-default fields included, DEFLATE compressed before base64. Accepts both compressed and raw JSON on import. - ConfigStore.loadFromJson() deduplicated — shared by file load + import. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: deep link requires confirmation, trust warning on import, mhrv-rs:// scheme Security fix: deep link (mhrv-rs://) no longer auto-imports config. Stashes decoded config for UI confirmation dialog — same flow as clipboard paste and QR scan. Import confirmation dialog now shows: - Trust warning: "Importing routes your traffic through the deployment IDs in this config. Only import from trusted sources." - Mode and deployment ID count with first 3 IDs previewed - Explicit Import / Cancel buttons Also: - Renamed scheme from mhrv:// to mhrv-rs:// (less collision risk) - Deduplicated import dialog into shared ImportConfirmDialog composable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mhrv-rs:// deep links, QR scanner, clipboard banner, share sheet - DEFLATE-compressed base64 encoding (~200 chars vs ~800 raw) - Every import path requires explicit user confirmation; the dialog shows the new deployment IDs and a trust warning so an attacker posting a malicious mhrv-rs:// link in a public channel can't silently overwrite a user's auth_key + script_ids - ZXing for QR generation/scanning (no Google Play Services) Closes therealaleph#266. Thanks @yyoyoian-pixel — the rebase from auto-import to confirmation-gated import is exactly the right shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia group" This reverts commit e9ce03e.
…ph#271) Uses tun2proxy_run_with_cli_args (the C API) via dlsym instead of modifying the JNI run() signature. The upstream tun2proxy maintainer recommended this path — the CLI API accepts --udpgw-server natively. - Cargo.toml: enable udpgw feature, remove [patch.crates-io] - MhrvVpnService.kt: build CLI args with --udpgw-server in full mode - Native.kt + android_jni.rs: dlsym wrapper for the C API - Tun2proxy.kt: reverted to upstream signature No fork, no patch, no submodule. Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move from yyoyoian-pixel/tun2proxy fork (with patched JNI signature) to canonical tun2proxy 0.7.21 from crates.io with feature flag "udpgw". Cargo.toml [patch.crates-io] section removed entirely. The Android side now resolves tun2proxy_run_with_cli_args at runtime via dlsym from libtun2proxy.so, which is the upstream maintainer's recommended path for callers that need full CLI flexibility. mhrv-rs builds the CLI string in MhrvVpnService and passes it through Native.runTun2proxy → src/android_jni.rs → dlsym → tun2proxy. Future tun2proxy upgrades are now a single Cargo version bump. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-therealaleph#275, youtube_via_relay=true routed every YouTube-related host through Apps Script — including ytimg.com (thumbnails) and any googlevideo.com chunk request the player issued. Two problems: 1. ytimg.com via Apps Script is wasted quota — image CDN, no Restricted Mode logic to bypass. 2. googlevideo.com wasn't even in SNI_REWRITE_SUFFIXES, so video chunks hit the relay regardless of the flag. A single chunk timeout aborted the whole video on Firefox; long videos risked the Apps Script 6-min execution cap mid-playback. Fix: split YouTube into "API/HTML hosts" (where Restricted Mode lives, gated by the flag) and "asset CDNs" (always direct). The new YOUTUBE_RELAY_HOSTS list is youtube.com, youtu.be, youtube-nocookie.com, youtubei.googleapis.com — those go through relay when the flag is on. ytimg.com, googlevideo.com (added), ggpht.com all stay on SNI rewrite. The matches_sni_rewrite logic was also restructured: the carve-out now runs FIRST before the SNI suffix match, so the broad googleapis.com entry can't override the narrower youtubei.googleapis.com decision. Reported with detailed analysis by @amirabbas117. Will ship in v1.7.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tunnel-docker job in v1.7.3 release failed with:
error: failed to unpack package `serde_json v1.0.149`
Caused by: failed to open `/usr/local/cargo/registry/src/.../serde_json-1.0.149/.cargo-ok`
Caused by: File exists (os error 17)
Root cause: BuildKit's default cache-mount sharing is "shared" — both
linux/amd64 and linux/arm64 build stages mount the SAME on-disk cache
dir. Cargo's registry source extraction is non-atomic; both arches
race on `tar -xzf serde_json-1.0.149.crate` into the same destination,
and the loser hits EEXIST mid-unpack.
Fix: scope each cache mount with `id=cargo-registry-${TARGETPLATFORM}`
(and matching for cargo-git + target). BuildKit then keeps separate
on-disk caches per architecture — no race. Per-arch warm-build speedup
is preserved (each cache fills with that arch's pre-built deps); the
only loss is one cache miss per arch on the first build after this
change, which we already paid in v1.7.3.
The target/ mount is also platform-scoped since target/ holds compiled
object files for a single ABI; sharing across arches would either miss
or, worse, link wrong-ABI objects together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ative-cache + pre-warm) therealaleph#275: youtube_via_relay no longer routes video/image CDNs through Apps Script. The flag now correctly carves out only the API/HTML hosts where Restricted Mode is enforced; video chunks come direct from googlevideo.com (which was missing from the SNI rewrite list entirely — fixed). Long videos no longer hit Apps Script's 6-min execution cap, and single-chunk timeouts no longer abort playback. therealaleph#280: TunnelMux now caches "destination unreachable" responses from the tunnel-node (Network is unreachable / No route to host) for 30 seconds, short-circuiting subsequent CONNECTs to that destination with 502 (HTTP) or 0x04 (SOCKS5). Saves ~5 batches/second on IPv6-only host probes. Startup pre-warm pool grew 12→24. 143/143 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
w0l4i has been asking for client-side QUIC block since therealaleph#213. Now implemented as a small config flag. When `block_quic = true`, the SOCKS5 UDP relay drops any datagram destined for port 443 — that's HTTP/3-over-UDP. The client's QUIC stack retries a couple of times and then falls back to TCP/HTTPS through the regular CONNECT path (which goes through the relay normally). Why client-side rather than server-side udpgw block: the udpgw block in therealaleph#222 is bound to Full mode + Android tun2proxy. This covers everyone — apps_script users, desktop, Full mode, all the same path. Skipping at the SOCKS5 layer rather than the tunnel-node layer also avoids paying 200–500 ms tunnel-node round-trip per QUIC datagram drop, which compounds during browser retries. Silent drop is the contractually correct shape: SOCKS5 UDP wire has no `host unreachable` reply (RFC 1928 §6 only defines that for TCP CONNECT). Browsers' QUIC stacks have a "no response → fall back" timeout, so silent drop matches what the protocol expects. Default false (opt-in) — udpgw mitigates QUIC partly via persistent sockets, and a tiny minority of sites only support HTTP/3. Will ship in v1.7.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resume the practice (dropped after v1.1.0) of committing prebuilt binaries to the repo's releases/ folder. Iranian users behind state network filtering frequently can't reach the GitHub Releases page (/releases/tag/...) but CAN reach the static source tree via Code → Download ZIP — that pulls the in-repo releases/ folder along with the source. Telegram channel feedback explicitly requested this be resumed. The new commit-releases job: 1. Runs after release+build+android succeed. 2. Wipes existing binary artifacts from releases/ (.apk, .tar.gz, .zip) but preserves README.md and .gitattributes. 3. Copies all desktop archives (which already have stable platform-suffixed names like mhrv-rs-linux-amd64.tar.gz). 4. Copies all per-ABI Android APKs (so users on slow connections can grab the ~37 MB arm64-v8a APK instead of the ~110 MB universal). 5. sed-updates the "Current version" line and APK filename refs in releases/README.md (both English and Persian copies). 6. Commits as github-actions[bot] and pushes to main. The GitHub Release page itself keeps the canonical versioned artifacts as before — this in-repo folder is the fallback for users who can't reach that URL. Tag protection rules don't apply to refs/heads/main so the push isn't gated. release-drafter.yml triggers on push-to-main but only updates the next-release draft, no cycle risk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… folder - Adds `block_quic = true` config flag for client-side QUIC drop. SOCKS5 UDP relay refuses UDP/443 datagrams; browsers fall back to TCP/HTTPS through the relay. Opt-in. Thanks @w0l4i - Workflow now auto-refreshes the in-repo releases/ folder on each release tag, so Iranian users behind GitHub-Releases-page filtering can download via Code → Download ZIP. Practice was started before v1.1.0 then dropped; resumed at user request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release for the Win7 i686 binary fix shipped via therealaleph#323. No code changes; CI workflow change only — Cargo, Gradle, and changelog bumps in lockstep so the release produces a fresh i686 Windows binary built against Rust 1.77.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
Accept a synthetic first range probe when Content-Range proves the whole entity was returned, even if Apps Script decoded the body and left compressed Content-Range metadata intact. The response is then rewritten to HTTP 200 with Content-Range removed and Content-Length based on the decoded body, avoiding an unnecessary fallback full GET. Keep strict validation for real client Range requests and later chunks. Also recognize localized Apps Script bandwidth quota errors. Co-authored-by: freeinternet865 <free@internet865.com>
…herealaleph#344) Android 13+ restricts clipboard access for background-to-foreground transitions — the auto-detect banner never appeared on resume. Replace with a permanent Paste button that reads clipboard on tap (user interaction grants clipboard permission). Also: Export button is now icon-only (share icon) to save space. Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…file Patch release for the changes shipped via therealaleph#337 (Apps Script range probe gzip-decoded body handling) and therealaleph#344 (Android Paste button on 13+) plus a CI fix that restores the Win7 i686 binary missing from v1.7.9 (Cargo.lock format mismatch with Rust 1.77). The Cargo.lock version=4 (Rust 1.78+) wasn't readable by the pinned 1.77.2 toolchain on the i686 job. Workflow now regenerates the lockfile with the pinned toolchain on that job only, leaving every other target unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…download v1.7.10 release run shipped no assets due to two CI failures stacked: 1. The i686-pc-windows-msvc job (added in v1.7.7 for Win7 support per therealaleph#318) failed because Rust 1.77.2 — the last stable that produces Win7-loadable binaries — can't parse modern transitive crate manifests (`time` 0.3.47 in this case). Pinning transitives across the dep tree at every MSRV bump in our deps isn't sustainable, so the target is removed from the release matrix. Win7 32-bit users self-build per therealaleph#318's instructions. 2. The `release` job hit `actions/download-artifact@v4`'s 5-retries- exhausted error on multiple artifacts. Same flake we worked around in therealaleph#288 for `commit-releases`. The `release` and `telegram` jobs now use `gh run download` wrapped in a 3-attempt retry loop, mirror- ing the working pattern. v1.7.11 is the first full release after v1.7.9; ships therealaleph#337 (Apps Script gzip-decoded range probe) and therealaleph#344 (Android Paste button) that were tagged in v1.7.10 but never published as assets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
…ounters Five user-visible changes shipping together. Each is independently useful + bounded; bundled because they're all "small architectural hardening" that benefits from one release announcement. 1. Random payload padding (therealaleph#313, therealaleph#365 §1) Every outbound Apps Script JSON request now carries a `_pad` field of uniform-random length 0..1024 bytes (base64). Defeats DPI that fingerprints on the tight length distribution of mhrv-rs's previous per-mode-bound packet sizes. ~25% bandwidth on a typical 2 KB batch, negligible against Apps Script's per-call latency floor. Backward- compatible — old `Code.gs` deployments ignore the unknown field. Applied at all three payload-build sites: single relay, single tunnel op, batch tunnel. 2. Active-probing decoy: GAS bad-auth → 200 HTML (therealaleph#365 §3) `Code.gs` and `CodeFull.gs` now return a benign Apps-Script-style placeholder HTML page on bad/missing AUTH_KEY instead of the JSON `{"e":"unauthorized"}`. To an active scanner the deployment looks like one of the millions of forgotten public Apps Script projects rather than an obvious API endpoint. New `DIAGNOSTIC_MODE` const restores JSON errors during setup; default false (production-strong). 3. Active-probing decoy: tunnel-node bad-auth → 404 nginx (therealaleph#365 §3) `tunnel-node` returns an HTTP 404 with an nginx-style HTML body on bad auth instead of `{"e":"unauthorized"}`. Active scanners cataloging the host see "static web server, nothing tunnel-shaped here." New `MHRV_DIAGNOSTIC=1` env var restores verbose JSON during setup. 4. Fix: Full-mode usage counter stuck at zero (therealaleph#230, therealaleph#362) `today_calls` / `today_bytes` were only being incremented on the apps_script-mode relay path. Full-mode batches go through `tunnel_client::fire_batch` which never wired into the counter. Now `fire_batch` calls `record_today(response_bytes)` after each successful batch — bytes estimated from the `d` (TCP payload) and `pkts` (UDP datagrams) sizes in the BatchTunnelResponse. Full-mode users now see real usage numbers. 5. Fix: quota reset countdown was UTC, should be PT (therealaleph#230, therealaleph#362) Apps Script's UrlFetchApp daily quota resets at midnight Pacific Time, not UTC. We were displaying the countdown to UTC midnight, off by 7-8h depending on DST. New `current_pt_day_key()` and `seconds_until_pacific_midnight()` helpers with hand-rolled US DST detection (2nd Sunday March → 1st Sunday November = PDT, else PST) so we don't pull `chrono-tz` and a ~3 MB IANA tzdb just for one helper. UI label "UTC day" → "PT day". Tests pin DST window boundaries against March/November of 2024, 2026, 2027 to catch regressions in the day-of-week math. Tested: - cargo test --lib: 154 passed (was 152, +2 for DST window + day-of-week) - cargo build --release: clean - cargo build --release --bin mhrv-rs-ui --features ui: clean (macOS arm64) - tunnel-node cargo test: 30 passed - Android: ./gradlew assembleDebug succeeds; APK installs + launches on mhrv_test emulator (arm64-v8a), no UnsatisfiedLink, no crash Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
Mirror of the English README with the same setup paths (Cloud Run / Docker prebuilt / Docker source / direct binary), env-var table, and protocol section, plus a Persian-language FAQ section answering the specific questions users keep filing: - bandwidth overhead (~25-30% from base64 + JSON envelope + v1.8.0 random padding) - whether all Android apps get tunneled (yes in Tunnel mode + VpnService; no in Proxy mode) - realistic per-flow throughput (~50-200 KB/s, bound by Apps Script's per-roundtrip floor; horizontal-scale via more deployments) - whether a VPS is required for Full mode (yes; not required for apps_script or google_only) - which VPS to pick (Hetzner CX11 €4/mo for general use; Cloud Run free tier specifically for Iran users hit by therealaleph#313 since destination IP stays Google-internal) Adds an `MHRV_DIAGNOSTIC` env-var row to both the English and Persian env-var tables — was added in v1.8.0 but never documented. Closes therealaleph#372. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New workflow + script that posts every artifact (Android APKs, Windows ZIP, macOS .app + CLI tarballs, Linux glibc + musl, OpenWRT, Raspbian) to the Telegram channel as separate sendDocument calls, each with a Persian caption naming the platform variant and a `#v<NNN>` hashtag (e.g. `#v180`, `#v1810`, `#v200`) so users can find a specific release later via the channel's hashtag search. Files larger than 45 MB (the Bot API's effective ceiling after multipart + caption overhead) are split into byte chunks named `<name>.part_aa`, `.part_ab`, ... and posted with reassembly instructions in the caption. For the v1.8.0 file set everything is ≤41 MB so the split path is defensive. Decoupled from `release.yml` so it can be re-triggered for any past tag via `workflow_dispatch` without rebuilding artifacts — downloads from the GitHub Release page directly via `gh release download`. Also auto-runs on each successful `release.yml` completion via `workflow_run`. Hard-codes the channel ID `-1003966234444` (one well-known channel, auditable in source). Reuses `secrets.TELEGRAM_BOT_TOKEN` which already has post permissions there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…to files channel
Two changes on top of last commit:
1. SHA-256 ("تایید اصالت") now in every file caption. Each artifact's
caption gets a `<code>...</code>` line with the file's SHA-256 hex
so recipients can `sha256sum <file>` after download and verify it
matches what the channel posted. Defends against modified copies
if the channel ever gets relayed through a third party.
For chunked uploads (file > 45 MB), each part shows BOTH:
- SHA-256 of that specific part (verifies the chunk downloaded
intact before bothering to reassemble)
- SHA-256 of the full reassembled file (verifies the final result
after `cat <name>.part_* > <name>`)
2. Main channel post is now a cross-link, not files.
Previously the legacy `telegram` job in release.yml posted the
universal APK + full changelog as one sendDocument + sendMessage
pair to the main announcement channel.
New behaviour: telegram-publish-files.yml's last step posts a short
message to the main channel saying "v1.8.0 released, click here
for files" with a t.me link pointing at the files channel's
announcement anchor post. Recipients land on the anchor, scroll
to find the platform-specific artifact they need.
Link format: `t.me/c/<chat_id>/<msg>` for private channels (works
for members), or `t.me/<username>/<msg>` if `FILES_CHANNEL_USERNAME`
repo variable is set (works for everyone — useful if the files
channel is later made public).
Legacy telegram job in release.yml stays in source, dormant,
gated on `vars.TELEGRAM_NOTIFY_ENABLED == 'true'` (default false).
Comment updated to note the new workflow is the canonical path.
If both are turned on at once, the main channel gets two posts
per release.
Tested manually for syntax + caption rendering — actual SHA-256 values
will appear on the next workflow_dispatch run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes to the main-channel cross-post (one message per
release that points at the files channel):
1. Post-link now uses the public-username form `t.me/mhrv_rs/<msg_id>`
instead of the private `t.me/c/<chat_id>/<msg_id>`. The latter only
resolves for channel members; the former works for everyone, so
recipients seeing the main-channel announcement can click through
to a specific release post even if they're not yet subscribed.
Wired via the existing `FILES_CHANNEL_USERNAME` workflow env var,
now defaulting to `mhrv_rs` (the channel's public username) if the
`vars.FILES_CHANNEL_USERNAME` repo variable is unset. Override per
repo if the channel is renamed.
2. Channel-join links rendered in the body of the main-channel post,
below the post-link:
لینک کانال:
https://t.me/mhrv_rs
و یا: https://t.me/+R1OyoHX2boA1ZDgx
Two forms cover the cases where one or the other is filtered:
- `t.me/mhrv_rs` — public username form, prettier, surfaces in
Telegram search
- `t.me/+<hash>` — invite link, the only join path that works for
private/restricted channels and for users whose client doesn't
resolve public usernames cleanly
Wired via new `FILES_CHANNEL_INVITE` env var, defaulting to the
current invite hash; override via `vars.FILES_CHANNEL_INVITE` if
rotated.
Per user request — not running this against v1.8.0 retroactively,
just wiring it up for the next release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dding flag Three small, ship-able-now changes from the past day's issue triage: 1. Client-side detection of the v1.8.0 bad-auth decoy HTML (therealaleph#404 w0l4i, therealaleph#310 sina-b4hrm) When mhrv-rs gets back the decoy HTML body that v1.8.0's Code.gs/ CodeFull.gs/tunnel-node return on bad AUTH_KEY, the client now string-matches the body's distinctive "The script completed but did not return anything" sentinel and emits an explicit ERROR line naming AUTH_KEY mismatch as the likely cause + walking the user through "redeploy as new version" + the DIAGNOSTIC_MODE escape hatch — instead of the previous cryptic "WARN batch failed: bad response: no json in batch response: <!DOCTYPE...". Saves users hours of debugging. Reported pattern hits everyone who edits Code.gs's AUTH_KEY without redeploying as a new version (Apps Script doesn't auto-pick-up that change). 2. script_id in every batch-failure log (therealaleph#404 w0l4i) Previously WARN batch-failed lines didn't say which deployment failed. In multi-deployment setups (5–10 deployments where some have stale AUTH_KEY), users couldn't identify the culprit without the per-deployment curl probe loop. All four failure paths in tunnel_client::fire_batch — timeout, bad response, decoy detection, missing-response-in-batch — now include the script_id short prefix: `batch failed (script AKfycbz4): ...`. Combined with #1 above, this is the first reliable diagnostic for the "1 of 8 deployments has bad AUTH_KEY" pattern. 3. New disable_padding config flag (therealaleph#391 EBRAHIM-AM) Default false (padding active = stronger DPI defense). For users on heavily-throttled ISPs where v1.8.0's ~25% bandwidth overhead from random padding compounds with the throttle and pushes borderline-working batches into timeouts, setting `"disable_padding": true` in config.json recovers headroom at the cost of losing length-distribution DPI defense. Don't flip on speculatively — only enable if you've measured actual throughput improvement on your specific ISP path. For users where Apps Script outbound flows freely, padding is free defense. Tested: - cargo build --release --bin mhrv-rs: clean - cargo build --release --bin mhrv-rs-ui --features ui: clean - cargo test --release --lib: 154 passed - UI FormState round-trips disable_padding through save/load Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
…tion - src/bin/ui.rs: install_ui_tracing now takes config_level. Filter precedence is RUST_LOG > config.log_level > info,hyper=warn. The filter is wrapped in a reload::Layer; Save reinstalls it via apply_log_level so users don't need to restart for a level change. Fixes therealaleph#401 (w0l4i) — config.log_level was previously dead on the UI binary even though the CLI honored it via init_logging. - src/tunnel_client.rs: v1.8.1 asserted "AUTH_KEY mismatch" on the Apps Script placeholder body, but therealaleph#404 (w0l4i) showed mixed success/failure on the same script_id, which rules that out. The body is also returned for Apps Script execution timeout, quota tear, internal hiccup, and ISP-side truncation. Error message now enumerates all four candidates and points to DIAGNOSTIC_MODE for disambiguation.
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).
Cherry-picks of stability/hardening fixes from upstream Python (masterking32/MasterHttpRelayVPN), Apr 23-26 window. By @dazzling-no-more. - 240s H1 container keepalive prevents Apps Script V8 cold-start stalls - 64 KB header-block cap returns explicit HTTP/1.1 431 instead of socket drop - Clearer port-collision error message Local verification: cargo build clean, cargo test --lib 154/154 passing.
…aleph#439) Routes browser DoH lookups (Cloudflare, Google, Quad9, AdGuard, NextDNS, OpenDNS, dns.sb, dns0.eu, AliDNS, doh.pub, Mullvad) around the Apps Script tunnel via plain TCP. By @dazzling-no-more. DNS-over-HTTPS is already encrypted; tunneling it adds 2s UrlFetchApp roundtrip per name without privacy benefit. New `bypass_doh_hosts` config (default true) lets users opt out. Gated to TCP/443 — private DoH on :8443 should use `passthrough_hosts`. Local verification: cargo build clean, cargo test --lib 160/160 passing (+6 new matches_doh_host tests).
…herealaleph#443) Adds optional Sheets-backed response cache to Code.gs. By @euvel. All five review suggestions from therealaleph#400 addressed: - TTL-aware caching (Cache-Control max-age, no-cache, no-store, private) - 35 KB body-size gate (under Sheets cell limit) - Header rewriting on cache hit (Date, Age, Cache-Control, X-Cache, X-Cached-At) - Circular buffer for O(1) writes (no appendRow/deleteRows reindexing) - Vary-aware compound cache keys (Accept-Encoding, Accept-Language) Opt-in via single constant. Zero overhead when disabled. No breaking changes.
…er errors Three substantive PRs from contributors landed for this release: - therealaleph#443 by @euvel: optional spreadsheet-backed response cache in Code.gs. Implements all 5 review suggestions from the design discussion (therealaleph#400): TTL-aware caching, 35 KB body-size gate, header rewriting on hit, circular buffer for O(1) writes, Vary-aware compound keys. - therealaleph#439 by @dazzling-no-more: bypass Apps Script tunnel for known DoH endpoints on TCP/443. Cloudflare/Google/Quad9/AdGuard/NextDNS/OpenDNS/ CleanBrowsing/dns.sb/dns0.eu/AliDNS/doh.pub/Mullvad. Saves the ~2s UrlFetchApp roundtrip per name without losing privacy (DoH is already encrypted). Default on; users can opt out via tunnel_doh: true or extend the list via bypass_doh_hosts. - therealaleph#438 by @dazzling-no-more: H1 container keepalive + 431 oversized- headers + clearer port-collision message. Cherry-picks from upstream Python (Apr 23-26 window). Keepalive prevents Apps Script V8 cold starts (visible as YouTube stalls after pause); 431 replaces silent socket drops on >64 KB headers (which caused browser retry loops).
5555cdb to
fe98cee
Compare
Replace the fixed 8ms batch coalesce window with an adaptive scheme: after each new op arrives, wait another 40ms (step) for more ops. The timer resets on every arrival, up to 1000ms (max) from the first op. Both values are configurable via config JSON (coalesce_step_ms, coalesce_max_ms) and two new sliders in the Android Advanced section. Why this helps: Apps Script adds ~1.5s overhead per HTTP call. The previous 8ms window barely caught any concurrent ops — most batches carried just 1 op. With 40ms/1000ms, batches average 2-3 ops, reducing total Apps Script calls for the same workload. Tested on device in Iran: Before: P75=6.2s, 61% fast (<3s), ~1 op/batch After: P75=3.0s, 74-85% fast, ~2-3 ops/batch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fe98cee to
0ca6f77
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fire_batchnon-blocking under heavy loadWhy
Apps Script adds ~1.5s overhead per HTTP call — the relay is the bottleneck, not the tunnel-node. Packing more ops into each batch means fewer total calls for the same workload. The previous 8ms coalesce window barely caught concurrent ops (~1 op/batch). With adaptive coalescing, batches average 2-3 ops — ~50% fewer Apps Script calls.
How the adaptive coalesce works
A lone op waits at most 40ms. Burst traffic (page load with 10 parallel fetches) coalesces into one batch. The 1000ms max prevents unbounded delay.
Why disable legacy detection
The legacy detection flagged tunnel-nodes as "old" when empty poll responses returned faster than 1.5s, then backed off polling to 30s. With longer drain/settle times on the tunnel-node, this triggered false positives. Since users deploying from this repo control their own tunnel-node, auto-detection is unnecessary.
Why non-blocking fire_batch
Previously,
fire_batch().awaitblocked the mux_loop on the per-deployment semaphore (30 slots). If all slots were busy, new ops piled up in the channel un-collected. Now the semaphore acquire happens inside the spawned task — the mux_loop keeps collecting ops for the next batch while the previous batch waits for a slot.Performance (tested on device in Iran)
Test plan
🤖 Generated with Claude Code