Version bump reflects the scope — a unified Rust core that now ships
for desktop (Linux/macOS/Windows) AND Android from the same crate.
Android changes:
- build.gradle.kts: ABI filters expanded to arm64-v8a + armeabi-v7a
+ x86_64 + x86. cargoBuild{Debug,Release} pass all four ABIs to
cargo-ndk in a single invocation. normalizeTun2proxySo() walks every
ABI dir now (was arm64-only).
- Release buildType signs with the debug keystore — no Play Store
target, so signature identity doesn't matter, installability does.
Gradle auto-provisions ~/.android/debug.keystore if absent, so CI
runners inherit this without extra setup.
- versionName 1.0.0, versionCode 100 (room to bump monotonically).
CI:
- release.yml gets a dedicated `android:` job that sets up JDK 17,
Android SDK/NDK 26, all four rust-android targets, installs
cargo-ndk, runs assembleRelease, and uploads a single universal APK
named `mhrv-rs-android-universal-v<version>.apk` into the same
`dist/` collected by the release job downstream.
- `release:` job now gates on `needs: [build, android]` so tagging
v1.0.0 triggers both build matrices before cutting the GitHub
release.
Docs:
- docs/android.md — full 10-step install walk-through: APK sideload,
Apps Script deployment (with "Advanced → Go to (unsafe) → Allow"
reality check), config paste, SNI reachability test, MITM CA
install with OEM-specific nav paths (Pixel / Samsung / Xiaomi),
Start, troubleshooting common failure modes. Also documents the
known limitations — Cloudflare Turnstile loops (inherent to the
Apps Script egress IP pool), UDP/QUIC not tunnelled, IPv6 leaks,
Apps Script daily quota — so users know what to expect before
trying it on a site that won't work.
- releases/README.md — APK row added to the English and Persian
tables, version bumped everywhere to v1.0.0.
- Top-level README — Android listed under Platforms with a link
to docs/android.md.
Release artifact:
- releases/mhrv-rs-android-universal-v1.0.0.apk — 38 MB universal
APK built locally from this tree. Installs + launches on API 24+.
The CI job will regenerate it on tag push; this is the copy
committed for users who can't reach GitHub Releases.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
Android app — installation & first-run guide
This is the one-stop guide for running mhrv-rs on your Android phone. It covers the APK install, the MITM certificate dance (which changed a lot on Android 11+), the VPN permission, the first-time Apps Script deployment, and the common "why doesn't this site load" failure modes.
Estimated time: ~10 minutes if the Apps Script deployment already exists, ~15 if you're deploying it fresh.
Scope note. mhrv-rs relays through Google Apps Script. That's what makes it cheap and firewall-resilient, but it's also what imposes the limits documented under Known limitations. If you're evaluating this against "a real VPN" — WireGuard, Tailscale, OpenVPN — read that section first.
What you'll need
- An Android phone running Android 7.0 (API 24) or later. Arm64, armv7, x86_64, and x86 all ship in one APK.
- A Google account (you'll deploy the Apps Script under it). Any account works; a throwaway Gmail is fine.
- About 5 MB of mobile data for the APK download, 2 MB/day of relay traffic per GB of browsing (overhead from base64 + JSON wrapping).
- A way to set a screen lock (PIN, pattern, password, or biometric + fallback) — this is an Android OS requirement for installing any user CA certificate. You can remove the lock later if you want.
Step 1 — Install the APK
- On your phone, open the browser and go to:
https://github.com/therealaleph/MasterHttpRelayVPN-RUST/tree/main/releases - Tap
mhrv-rs-android-universal-v1.0.0.apk→ View raw (or Download). - When the browser finishes the download, tap the notification to open the APK.
- Android will ask: "Allow this source to install apps?" — tap Settings, toggle Allow from this source, then tap ← Back. Tap Install.
- Tap Open once install finishes. You should see the mhrv-rs main screen.
Why "unknown source" is necessary. The app isn't on Play Store (it's not the kind of app Google will accept — explicit VPN with MITM). Every Android phone ships with sideload capability for exactly this situation; you're just toggling the permission for the one app that's delivering the APK (Chrome, Files, etc.).
If Android refuses to install with "App not installed" or similar, 99% of the time it's because a different mhrv-rs build is already installed signed with a different key. Uninstall the old one first: Settings → Apps → mhrv-rs → Uninstall.
Step 2 — Deploy the Apps Script
Skip this step if you already have a working /exec URL from a previous install.
- On your laptop (easier than on phone), go to https://script.google.com and sign in.
- Click New project. You'll land in the script editor.
- Open
assets/apps_script/Code.gsin this repo, copy the entire contents. - Back in the script editor, select ALL of the default code (
function myFunction() {}) and paste over it. - Find this line near the top:
Replace the placeholder with a strong random secret (20+ characters, letters + digits). Save a copy somewhere — you'll paste the exact same string into the app.
const AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET"; - Save the file (⌘S or Ctrl+S). Name the project something like
mhrv-relay. - Top-right, click Deploy → New deployment.
- Click the gear icon → Web app.
- Fill in:
- Description:
mhrv-relay v1(or whatever) - Execute as: Me
- Who has access: Anyone
- Click Deploy.
- Description:
- First time only: Google asks for permissions. Click Authorize access → pick your account → on the "Google hasn't verified this app" screen click Advanced → Go to (unsafe) → Allow. This is the standard Apps Script deployment flow — the script runs under your account only, Google just hasn't manually audited it.
- Copy the Web app URL. It looks like
https://script.google.com/macros/s/AKfyc.../exec.
What the script actually does. It receives POST requests from our proxy containing
{ method, url, headers, body_base64 }, callsUrlFetchApp.fetch(url, ...)inside Google's datacenter, and returns{ status, headers, body_base64 }back. The "DPI bypass" comes from our proxy connecting toscript.google.comusing a different SNI than the Host header — we hit Google's edge under one name and then funnel traffic to Apps Script under another. Your ISP sees a plain TLS handshake towww.google.com.
Step 3 — Fill in the app's config
Back on the phone in the mhrv-rs app:
- Paste the
/execURL (or just theAKfyc...ID) into Deployment URL(s) or script ID(s). You can paste multiple — one per line — and the proxy will round-robin between them. Useful later when you hit the 20k/day per-script quota. - Paste your auth_key — the exact same string you put in
AUTH_KEYinsideCode.gs. - Leave google_ip at the default for now. You'll verify it in step 4.
- Leave front_domain at
www.google.com— that's the SNI we present on the outbound leg.
Step 4 — Test SNI reachability (strongly recommended)
Before you tap Start, expand the SNI pool + tester card and tap Test all.
- ✅ Green check + latency — your
google_ipis reachable and accepts the SNI. Proceed. - ❌ connect timeout on every row — your configured
google_ipis unreachable from your network. Fix by runningnslookup www.google.comon any working device and pasting the resulting IP into google_ip. Then Test all again. - ❌ connect timeout only on some rows — the blocked SNIs are DPI-filtered on your network. Leave them unchecked; the rotation pool only uses the ticked boxes.
- ❌ dns: … — your device can't resolve
www.google.comat all. Switch WiFi or check airplane mode.
Why this matters: if google_ip is wrong, the proxy will boot fine but every request will silently time out and you'll chase red herrings.
Step 5 — Install the MITM certificate
This step is annoying but unavoidable: the proxy terminates TLS on your behalf so it can re-encrypt to the Apps Script relay, which means your phone needs to trust a cert we minted locally.
- In the app, tap Install MITM certificate.
- Read the confirmation dialog — it shows the certificate fingerprint (handy to verify later). Tap Install.
- The app saves
Downloads/mhrv-ca.crtand deep-links Android into Settings → Security & privacy (or similar; wording varies by OEM). - If you don't have a screen lock: Android will prompt you to set one. You have to. User CAs require a screen lock, period. Set PIN/pattern/password. You can remove it after install if you really want; the cert stays installed.
- In Settings, navigate: Encryption & credentials → Install a certificate → "CA certificate".
- On Pixel / stock Android:
Security → More security settings → Encryption & credentials → Install a certificate → CA certificate. - On Samsung:
Biometrics and security → Other security settings → Install from device storage → CA certificate. - On Xiaomi/MIUI:
Passwords & Security → Privacy → Encryption & credentials → Install a certificate → CA certificate. - Do NOT pick "VPN & app user certificate" or "Wi-Fi certificate" — wrong category, won't work.
- On Pixel / stock Android:
- Android warns you: "Your network may be monitored by an unknown third party". That's us. Tap Install anyway.
- Pick Downloads → tap mhrv-ca.crt. Give it a friendly name (or accept the default). Tap OK.
- Return to the mhrv-rs app. A snackbar at the bottom will say Certificate installed ✓ (the app verifies by fingerprint against AndroidCAStore). If it says "not yet installed", go back to step 5 and try again.
Why the dance and not an inline KeyChain flow? Android 11 removed the inline
KeyChain.createInstallIntentpath — tapping Install MITM used to open a category picker directly, but it now opens a dead-end dialog with just a Close button. Google wants CA installs to be intentional, so they funnel you through Settings. We do the grunt work (save the file, deep-link Settings, verify afterwards) but the manual nav is unavoidable on current Android.
Step 6 — Start the proxy
- Tap Start.
- Android shows the VPN connection request dialog: "mhrv-rs wants to set up a VPN connection...". Tap OK.
- A key icon appears in the status bar. That's your VPN indicator.
- Open Chrome and visit any site. It should load normally — JavaScript, images, everything. Try
https://www.cloudflare.com,https://yahoo.com,https://discord.comas stress tests. - If you expand Live logs in the mhrv-rs app you'll see:
SOCKS5 CONNECT -> <hostname>:443— browser asking the TUN layer to open a flowdispatch <hostname>:443 -> MITM + Apps Script relay (TLS detected)— routing decisionMITM TLS -> <hostname>:443 (sni=<hostname>)— we minted the leaf cert and the browser accepted itrelay GET https://<hostname>/...— forwarded to Apps Scriptpreflight 204 https://...— a CORS preflight we answered ourselves (don't worry about these)
Known limitations
Read this before reporting a bug — most "it doesn't work" reports land in one of these buckets.
Cloudflare Turnstile (the "Verify you are human" checkbox) loops
On Cloudflare-protected sites that challenge every request (not just the first), you'll solve the challenge, reach the page, then get challenged again on the next click. This is fundamental to the Apps Script relay model and cannot be fixed in the app:
| Factor | Normal browser | Apps Script relay |
|---|---|---|
| Egress IP | Stable (your ISP) | Rotates across Google's datacenter pool per request |
| User-Agent | Chrome's | Fixed Google-Apps-Script (Google overrides it) |
| TLS JA3/JA4 | Chrome's | Google-datacenter's |
CF's cf_clearance cookie is bound to the (IP, UA, JA3) tuple the challenge was solved against. Different IP next request → re-challenge. No amount of cookie forwarding fixes this.
Sites that only gate the first page load (most of CF's Bot Fight Mode customers) work fine after one solve. Sites that challenge every request (cryptocurrency, adult, some forums) won't work through this architecture. Use a different tunnel for those.
UDP / QUIC (HTTP/3) doesn't go through
Our SOCKS5 listener only handles CONNECT, not UDP ASSOCIATE. Chrome tries HTTP/3 first and falls back to HTTP/2 over TCP, which works through the proxy. Effect: slightly slower connects on first visit, everything else fine.
IPv6 leaks
The TUN only routes IPv4 (0.0.0.0/0). IPv6 traffic goes out your normal interface, including WebRTC. If you're using mhrv-rs for privacy rather than DPI bypass, disable IPv6 on your WiFi network entirely.
Apps Script daily quota
Each deployment URL has a daily execution limit (20k/day for consumer Google accounts, higher for Workspace). Heavy streaming or infinite-scroll sites will burn through it. Mitigation: deploy 2–3 scripts and paste all their /exec URLs (one per line) into the app — the proxy round-robins across them.
The MITM cert has no Play Store signature
We ship the APK signed with the standard Gradle debug keystore. Android installs it fine, but you'll see "app is from an unknown developer" warnings, and Play Protect may flag it on some devices. That's accurate — the tradeoff is that the build is reproducible from source without us holding a secret key.
Troubleshooting
"504 Relay timeout" in Chrome
- Your Apps Script deployment isn't responding. Re-check the
/execURL (must end in/exec, not/dev). Watch Live logs forRelay timeoutvsconnect:— the former is the Apps Script leg, the latter is the outbound leg from Google to the origin.
"Your connection is not private — NET::ERR_CERT_AUTHORITY_INVALID"
- The MITM CA isn't installed, or Chrome doesn't see it. Go back to Step 5. If the snackbar confirmed install but Chrome still complains: clear Chrome's data for the site, or tap Advanced → Proceed anyway for testing, then install the cert properly.
"NET::ERR_CERT_COMMON_NAME_INVALID" on Cloudflare sites
- You're on a version before v1.0. Upgrade — this was fixed by peeking the TLS ClientHello.
JavaScript parts of a site don't load
- Pre-v1.0 Apps Script rejected
OPTIONSCORS preflights, which silently brokefetch(). Fixed in v1.0 by short-circuiting preflights at the MITM boundary. If you're on v1.0 and still seeing this: open Live logs, look forRelay failederrors, report them.
The app closes after tapping Stop then Start quickly
- Emulator-specific EGL renderer crash on rapid UI transitions. The VPN service itself survives; only the Compose UI process died. Debounced in v1.0 — buttons disable for 2 seconds after tap. On real hardware this rarely happens.
Chrome shows a white page with no error
- Very common on emulator with software rendering. Check
adb logcat | grep mhrv_rsto see if the relay is actually making requests. If yes → Chrome's renderer has issues on the emulator, try a real device. If no → the proxy isn't running; check that the VPN key icon is in the status bar.
Apps that ignore user CAs
Most non-browser Android apps opt out of trusting user CAs by default (Google's Network Security Config default as of API 24). Banking apps, Netflix, Spotify, most messengers — they'll fail through mhrv-rs with cert errors. The full TUN bridge routes their traffic to us, but their TLS stack refuses our cert. Only Chrome, Firefox, and apps that explicitly opt in will work. This is a general MITM limitation, not an mhrv-rs bug.
Uninstall
Settings → Apps → mhrv-rs → Uninstall.- Optionally remove the MITM CA:
Settings → Security → Encryption & credentials → User credentials → mhrv-rs MITM CA → Remove. - The VPN profile is automatically revoked on uninstall.
راهنمای فارسی
این فایل به انگلیسی نوشته شده تا با باقی مستندات پروژه هماهنگ باشد. اگر راهنمای فارسی میخواهید، لطفاً issue بسازید تا ترجمه کنیم.