Three substantive PRs from contributors landed for this release:
- #443 by @euvel: optional spreadsheet-backed response cache in Code.gs.
Implements all 5 review suggestions from the design discussion (#400):
TTL-aware caching, 35 KB body-size gate, header rewriting on hit,
circular buffer for O(1) writes, Vary-aware compound keys.
- #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.
- #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).
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).
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).
- 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#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 #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).
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
(#404 w0l4i, #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 (#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 (#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>
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>
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>
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>
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 #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#372.
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).
Five user-visible changes shipping together. Each is independently
useful + bounded; bundled because they're all "small architectural
hardening" that benefits from one release announcement.
1. Random payload padding (#313, #365 §1)
Every outbound Apps Script JSON request now carries a `_pad` field
of uniform-random length 0..1024 bytes (base64). Defeats DPI that
fingerprints on the tight length distribution of mhrv-rs's previous
per-mode-bound packet sizes. ~25% bandwidth on a typical 2 KB batch,
negligible against Apps Script's per-call latency floor. Backward-
compatible — old `Code.gs` deployments ignore the unknown field.
Applied at all three payload-build sites: single relay, single
tunnel op, batch tunnel.
2. Active-probing decoy: GAS bad-auth → 200 HTML (#365 §3)
`Code.gs` and `CodeFull.gs` now return a benign Apps-Script-style
placeholder HTML page on bad/missing AUTH_KEY instead of the JSON
`{"e":"unauthorized"}`. To an active scanner the deployment looks
like one of the millions of forgotten public Apps Script projects
rather than an obvious API endpoint. New `DIAGNOSTIC_MODE` const
restores JSON errors during setup; default false (production-strong).
3. Active-probing decoy: tunnel-node bad-auth → 404 nginx (#365 §3)
`tunnel-node` returns an HTTP 404 with an nginx-style HTML body on
bad auth instead of `{"e":"unauthorized"}`. Active scanners cataloging
the host see "static web server, nothing tunnel-shaped here." New
`MHRV_DIAGNOSTIC=1` env var restores verbose JSON during setup.
4. Fix: Full-mode usage counter stuck at zero (#230, #362)
`today_calls` / `today_bytes` were only being incremented on the
apps_script-mode relay path. Full-mode batches go through
`tunnel_client::fire_batch` which never wired into the counter.
Now `fire_batch` calls `record_today(response_bytes)` after each
successful batch — bytes estimated from the `d` (TCP payload) and
`pkts` (UDP datagrams) sizes in the BatchTunnelResponse. Full-mode
users now see real usage numbers.
5. Fix: quota reset countdown was UTC, should be PT (#230, #362)
Apps Script's UrlFetchApp daily quota resets at midnight Pacific
Time, not UTC. We were displaying the countdown to UTC midnight,
off by 7-8h depending on DST. New `current_pt_day_key()` and
`seconds_until_pacific_midnight()` helpers with hand-rolled US DST
detection (2nd Sunday March → 1st Sunday November = PDT, else PST)
so we don't pull `chrono-tz` and a ~3 MB IANA tzdb just for one
helper. UI label "UTC day" → "PT day". Tests pin DST window
boundaries against March/November of 2024, 2026, 2027 to catch
regressions in the day-of-week math.
Tested:
- cargo test --lib: 154 passed (was 152, +2 for DST window + day-of-week)
- cargo build --release: clean
- cargo build --release --bin mhrv-rs-ui --features ui: clean (macOS arm64)
- tunnel-node cargo test: 30 passed
- Android: ./gradlew assembleDebug succeeds; APK installs + launches
on mhrv_test emulator (arm64-v8a), no UnsatisfiedLink, no crash
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
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
#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 #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 #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 #337 (Apps
Script gzip-decoded range probe) and #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>
Patch release for the changes shipped via #337 (Apps Script range
probe gzip-decoded body handling) and #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>
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>
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>
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).
Patch release for the Win7 i686 binary fix shipped via #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>
* ci(release): pin i686-pc-windows-msvc to Rust 1.77.2 for Win7 compat
Fixes#318. Rust 1.78 (May 2024) raised the std MSRV for Windows from
Win7 to Win10 by switching std::time to GetSystemTimePreciseAsFileTime,
a kernel32 export that doesn't exist on Win7 SP1. Building the i686
binary with stable Rust (currently 1.86+) produces an exe that fails
to load on Win7 with "the procedure entry point
GetSystemTimePreciseAsFile could not be located in the dynamic link
library kernel32.dll" — making the whole reason we ship i686 (legacy
Win7 32-bit boxes per #272) moot.
Add a per-matrix `rust_toolchain` knob; only i686-pc-windows-msvc uses
it, pinning to 1.77.2 (last stable that supports Win7). Other targets
remain on @stable and pick up regular Rust updates.
dtolnay/rust-toolchain switches from `@stable` to `@master` because
the per-tag aliases (`@stable`, `@1.77.2`) can't be selected via a
matrix variable — `@master` accepts the toolchain string as input.
Cache key gains a toolchain suffix so the 1.77.2 cache doesn't collide
with the stable cache for the same target, and a future toolchain bump
invalidates only the affected slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(release): make i686-pc-windows-msvc continue-on-error
Companion to the Rust 1.77.2 pin: if the deps' MSRV ever moves above
1.77, the i686 target will fail to build, but we don't want it to
block the rest of the release. Mirror the mipsel-softfloat approach.
If/when this triggers, options are dropping i686 entirely or moving
to the tier-3 i686-win7-windows-msvc target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
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).
Patch release for the auto-blacklist of timeout-saturated deployments
shipped via #319. No new features; bugfix only — cargo, gradle, and
changelog bumps in lockstep so the release workflow can ship matching
artifacts.
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).
The v1.7.7 tag commit (6885800) only updated the changelog; the
version field edits failed earlier due to file-state-changed-mid-edit
race. Fixing forward — Cargo.toml + build.gradle.kts now show 1.7.7
properly.
Workflow will build from main HEAD on workflow_dispatch, so the
v1.7.7 release-page artifacts will have the correct internal version
even though the tag commit itself doesn't include the version bump.
- #288 (@amiralishoja): adds i686-pc-windows-msvc to the release
matrix. 32-bit Windows users get mhrv-rs-windows-i686.zip on
every release.
- #290 (@dazzling-no-more): per-deployment longpoll fallback state
with TTL-based auto-recovery. Replaces a global AtomicBool that
one degraded deployment could permanently flip. Now the aggregate
legacy gate only fires when every configured deployment is marked,
and self-corrects on TTL expiry — upgraded tunnel-nodes rejoin
the fast path automatically. 4 new tokio::test virtual-time tests.
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).
The commit-releases job's `actions/download-artifact@v4` step has
failed twice in a row (v1.7.5 retrigger, v1.7.6) with the same
shape: ~10 artifacts download successfully, then "Unable to
download artifact(s): Artifact download failed after 5 retries" on
the 11th-13th. The 10 that complete print their SHA256 digests
cleanly; the failure is unambiguously inside actions/download-
artifact, not on our side.
Workaround: pull from `gh release download` instead. The `release`
job populated the GitHub Release page a few seconds earlier with
the same artifacts; pulling from there reads from a different
CDN (Release-page blob store) with different retry / rate-limit
characteristics. Empirically more reliable for our 13-artifact
release size.
Filtered to *.tar.gz / *.zip / *.apk so we only fetch the user-
facing artifacts (skipping anything like checksum sidecars that
softprops/action-gh-release@v2 might add later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.7.4 added googlevideo.com to SNI_REWRITE_SUFFIXES on the theory
that video chunks should bypass the Apps Script relay. Multiple
users (#275 amirabbas117, #281 mrerf) reported total YouTube
breakage on v1.7.4: SNI-rewriting googlevideo.com:443 to a GFE IP
returned TLS handshake failure / wrong-cert error.
Root cause: googlevideo.com is served by Google's separate "EVA"
edge IPs, not the regular GFE IPs that the user's `google_ip`
typically points at. The SNI-rewrite tunnel TLS handshake against
a GFE IP for googlevideo.com SNI fails because the GFE IP doesn't
hold a googlevideo.com cert.
Pre-v1.7.4 behaviour restored: video chunks fall through to the
Apps Script relay path. Slower but reliable on every GFE IP.
The other v1.7.4 youtube_via_relay carve-out fixes (ytimg.com
correctly stays on SNI rewrite, youtubei.googleapis.com correctly
goes through relay) remain intact — those were a separate
improvement and still correct.
Future: if we want direct googlevideo.com routing, it needs a
separate `eva_edge_ip` config knob — users can populate from their
own EVA scan, defaulting to "use relay" if not configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.7.5's block_quic config field broke the UI binary build because
src/bin/ui.rs constructs Config{} explicitly and I forgot to add the
new field there. CLI binary loads from JSON via serde so it didn't
trip — only the 4 UI-building targets failed (linux-amd64-gnu,
windows-amd64, macos-amd64, macos-arm64).
block_quic is round-tripped through the form (config-only for now,
no UI control) so save doesn't drop a user-set true.
- 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>
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>
w0l4i has been asking for client-side QUIC block since #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 #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>
#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.
#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>
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>
Pre-#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>
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>
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>
- 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#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>
* 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>