mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 23:54:48 +03:00
v1.0.2: stable release signature, idempotent Stop, top-level Settings for CA install (#33)
Three fixes + one behaviour change from v1.0.1 reports. APK signature is now stable (release.jks committed) ---------------------------------------------------- v1.0.0 and v1.0.1 signed release APKs with Gradle's auto-generated debug keystore, which is randomly generated per machine and per CI runner. Result: every upgrade failed with INSTALL_FAILED_UPDATE_INCOMPATIBLE and users had to uninstall first. Unfixable without a stable key. android/app/release.jks now holds that key, committed to the repo with the password in plaintext in build.gradle.kts. This is fine for a FOSS sideload project without a Play Store identity — the trust model is "trust the source tree you pulled from," not "trust the key we hold." Anyone forking and shipping a rebranded build should generate their own key. One-time cost: v1.0.1 → v1.0.2 STILL requires uninstall, because we're switching signature keys. Every upgrade from v1.0.2 onward is clean. Stop no longer (sometimes) closes the app ----------------------------------------- teardown() is reachable from three paths on two threads: 1. ACTION_STOP onStartCommand branch (mhrv-teardown worker) 2. onDestroy after stopSelf (main thread) 3. VpnService revocation out-of-band (main thread) Running the full native cleanup sequence twice races the two threads through Tun2proxy.stop() → fd.close() → Native.stopProxy(handle) on state that's already been nullified — SIGSEGV source, user-visible as "tap Stop, app disappears." New AtomicBoolean `tornDown` gates entry: first caller wins, every subsequent caller logs "teardown: already done" and returns. onDestroy also wraps the call in try/catch — crashing out of onDestroy takes the whole process with it, which is exactly the bug we're trying to fix. Smoke-tested on emulator: teardown now logs teardown: begin caller=mhrv-teardown ... clean sequence ... teardown: done onDestroy entered teardown: already done, skipping (caller=main) onDestroy done with PID unchanged throughout. CA install now routes to the Settings search -------------------------------------------- Old flow: `Settings.ACTION_SECURITY_SETTINGS` deep-link, then walk "Encryption & credentials → Install a certificate → CA certificate". That path varies wildly between OEMs (Samsung buries it under "Biometrics and security → Other security settings"; Xiaomi under "Passwords & Security → Privacy"; Pixel splits it between "More security settings" and "Privacy controls" depending on Android version). Users got lost. New flow: open the top-level Settings app (`Settings.ACTION_SETTINGS`) and instruct the user to use the Settings search bar to find "CA certificate". Search is consistent across OEMs and Android versions; the menu paths are not. Dialog, snackbar, and `docs/android.md` copy all updated to match. Version bump: 1.0.1 → 1.0.2 (versionCode 101 → 102). releases/mhrv-rs-android-universal-v1.0.1.apk replaced with the v1.0.2 build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
b734f41faa
commit
64409f6b41
Generated
+1
-1
@@ -1960,7 +1960,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mhrv-rs"
|
name = "mhrv-rs"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.therealaleph.mhrv"
|
applicationId = "com.therealaleph.mhrv"
|
||||||
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
|
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 101
|
versionCode = 102
|
||||||
versionName = "1.0.1"
|
versionName = "1.0.2"
|
||||||
|
|
||||||
// Ship all four mainstream Android ABIs:
|
// Ship all four mainstream Android ABIs:
|
||||||
// - arm64-v8a — 95%+ of real-world Android phones since 2019
|
// - arm64-v8a — 95%+ of real-world Android phones since 2019
|
||||||
@@ -30,6 +30,31 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
// Committed keystore — fixed signature across machines and
|
||||||
|
// across CI runs. Using the auto-generated debug keystore
|
||||||
|
// (as v1.0.0 / v1.0.1 did) makes every release APK fail to
|
||||||
|
// install over the previous one with
|
||||||
|
// INSTALL_FAILED_UPDATE_INCOMPATIBLE, because Android treats
|
||||||
|
// a signature change as "different app": the user has to
|
||||||
|
// uninstall first. That's awful UX.
|
||||||
|
//
|
||||||
|
// The password is in plaintext because this is an
|
||||||
|
// open-source project without Play Store identity. A
|
||||||
|
// forked/rebuilt APK signed with a different key is
|
||||||
|
// fundamentally a different install path anyway — the
|
||||||
|
// protection model here is "trust the source tree you
|
||||||
|
// pulled from," not "trust that we hold a key you can't
|
||||||
|
// see." If you're forking, generate your own key, commit
|
||||||
|
// it, and ship.
|
||||||
|
storeFile = file("release.jks")
|
||||||
|
storePassword = "mhrv-rs-release"
|
||||||
|
keyAlias = "mhrv-rs"
|
||||||
|
keyPassword = "mhrv-rs-release"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
@@ -37,15 +62,7 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro",
|
"proguard-rules.pro",
|
||||||
)
|
)
|
||||||
// Sign release builds with the debug keystore so users can
|
signingConfig = signingConfigs.getByName("release")
|
||||||
// sideload the APK without us shipping a proper release key.
|
|
||||||
// The project has no Play Store presence, so signature
|
|
||||||
// identity per-build doesn't matter — installability does.
|
|
||||||
// Gradle auto-creates `~/.android/debug.keystore` on first use;
|
|
||||||
// CI runners inherit that behaviour. Anyone rebuilding from
|
|
||||||
// source gets their own signature, which is what we want for
|
|
||||||
// an open-source project: trust the source, not a key we hold.
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -147,19 +147,26 @@ object CaInstall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intent that opens the system "Security" settings screen. The exact
|
* Intent that opens the TOP-LEVEL system Settings app. The Settings
|
||||||
* landing page depends on OEM and Android version:
|
* search bar is the most portable way to get users to the CA-install
|
||||||
* - Pixel / stock AOSP: Settings → Security
|
* screen across OEMs — every Android vendor ships the CA install
|
||||||
* - from there the user navigates Encryption & credentials →
|
* flow under a subtly different menu path (Encryption & credentials,
|
||||||
* Install a certificate → CA certificate → pick our .crt file.
|
* Other security settings, Privacy → Credentials, etc.), but they
|
||||||
|
* all respond to a search for "CA certificate".
|
||||||
*
|
*
|
||||||
* We tried KeyChain.createInstallIntent first (nicer flow) but on
|
* Earlier versions used `Settings.ACTION_SECURITY_SETTINGS` which
|
||||||
* Android 11+ that intent just opens a dialog saying "Install CA
|
* landed on Security & privacy directly, but on some OEMs (Samsung,
|
||||||
* certificates in Settings" with a Close button and no path forward —
|
* Xiaomi, newer Pixel builds) that screen doesn't have the cert
|
||||||
* Google intentionally removed the inline install path. Settings is
|
* install entry one tap away and users got stuck. Top-level Settings
|
||||||
* the fallback Google themselves point users at.
|
* + "search for CA certificate" is the instruction that actually
|
||||||
|
* works everywhere.
|
||||||
|
*
|
||||||
|
* We DO NOT use KeyChain.createInstallIntent — on Android 11+ that
|
||||||
|
* intent opens a dialog that just says "Install CA certificates in
|
||||||
|
* Settings" with a Close button and no forward path. Google
|
||||||
|
* intentionally removed the inline install flow in that release.
|
||||||
*/
|
*/
|
||||||
fun buildSettingsIntent(): Intent = Intent(Settings.ACTION_SECURITY_SETTINGS)
|
fun buildSettingsIntent(): Intent = Intent(Settings.ACTION_SETTINGS)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ class MhrvVpnService : VpnService() {
|
|||||||
private var tun2proxyThread: Thread? = null
|
private var tun2proxyThread: Thread? = null
|
||||||
private val tun2proxyRunning = AtomicBoolean(false)
|
private val tun2proxyRunning = AtomicBoolean(false)
|
||||||
|
|
||||||
|
// Idempotency guard. teardown() is reachable from three paths:
|
||||||
|
// 1. ACTION_STOP onStartCommand branch (background thread)
|
||||||
|
// 2. onDestroy() (main thread, fires whenever stopSelf resolves
|
||||||
|
// OR Android decides to kill the service)
|
||||||
|
// 3. Android revoking the VPN profile out-of-band (also onDestroy)
|
||||||
|
// Running the full native cleanup sequence twice races two threads
|
||||||
|
// through Tun2proxy.stop(), fd.close(), Native.stopProxy() on state
|
||||||
|
// that's already been nullified — the second pass was the
|
||||||
|
// SIGSEGV-or-zombie source. This flag makes the second call a
|
||||||
|
// no-op.
|
||||||
|
private val tornDown = AtomicBoolean(false)
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.i(TAG, "onStartCommand action=${intent?.action ?: "<null>"} startId=$startId")
|
Log.i(TAG, "onStartCommand action=${intent?.action ?: "<null>"} startId=$startId")
|
||||||
return when (intent?.action) {
|
return when (intent?.action) {
|
||||||
@@ -187,7 +199,21 @@ class MhrvVpnService : VpnService() {
|
|||||||
* 4. Shut down the Rust proxy runtime (nothing left to forward to).
|
* 4. Shut down the Rust proxy runtime (nothing left to forward to).
|
||||||
*/
|
*/
|
||||||
private fun teardown() {
|
private fun teardown() {
|
||||||
Log.i(TAG, "teardown: begin (tun2proxy running=${tun2proxyRunning.get()}, proxyHandle=$proxyHandle)")
|
// Idempotency guard. Without this, onDestroy racing the
|
||||||
|
// ACTION_STOP background thread has been observed to crash the
|
||||||
|
// process — two threads into Tun2proxy.stop() and
|
||||||
|
// Native.stopProxy(handle) where handle has already been zeroed
|
||||||
|
// is a SIGSEGV waiting to happen. First caller wins, subsequent
|
||||||
|
// callers return immediately.
|
||||||
|
if (!tornDown.compareAndSet(false, true)) {
|
||||||
|
Log.i(TAG, "teardown: already done, skipping (caller=${Thread.currentThread().name})")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"teardown: begin caller=${Thread.currentThread().name} " +
|
||||||
|
"(tun2proxy running=${tun2proxyRunning.get()}, proxyHandle=$proxyHandle)",
|
||||||
|
)
|
||||||
|
|
||||||
// 1. Cooperative stop signal.
|
// 1. Cooperative stop signal.
|
||||||
if (tun2proxyRunning.get()) {
|
if (tun2proxyRunning.get()) {
|
||||||
@@ -200,7 +226,9 @@ class MhrvVpnService : VpnService() {
|
|||||||
// ParcelFileDescriptor no longer owns the fd and close() here
|
// ParcelFileDescriptor no longer owns the fd and close() here
|
||||||
// is a no-op; the real fd is owned by tun2proxy (closeFdOnDrop
|
// is a no-op; the real fd is owned by tun2proxy (closeFdOnDrop
|
||||||
// = true), which closes it on return from run().
|
// = true), which closes it on return from run().
|
||||||
try { tun?.close() } catch (_: Throwable) {}
|
try { tun?.close() } catch (t: Throwable) {
|
||||||
|
Log.w(TAG, "tun.close: ${t.message}")
|
||||||
|
}
|
||||||
tun = null
|
tun = null
|
||||||
|
|
||||||
// 3. Join the worker. 4s is enough in the happy case; if tun2proxy
|
// 3. Join the worker. 4s is enough in the happy case; if tun2proxy
|
||||||
@@ -219,19 +247,31 @@ class MhrvVpnService : VpnService() {
|
|||||||
// on the Rust side, so this is bounded even if the runtime
|
// on the Rust side, so this is bounded even if the runtime
|
||||||
// has in-flight tasks (common when the Apps Script relay has
|
// has in-flight tasks (common when the Apps Script relay has
|
||||||
// piled up pending 30s timeouts).
|
// piled up pending 30s timeouts).
|
||||||
if (proxyHandle != 0L) {
|
val handle = proxyHandle
|
||||||
Log.i(TAG, "teardown: stopping proxy handle=$proxyHandle")
|
proxyHandle = 0L
|
||||||
try { Native.stopProxy(proxyHandle) } catch (t: Throwable) {
|
if (handle != 0L) {
|
||||||
|
Log.i(TAG, "teardown: stopping proxy handle=$handle")
|
||||||
|
try { Native.stopProxy(handle) } catch (t: Throwable) {
|
||||||
Log.e(TAG, "Native.stopProxy threw: ${t.message}", t)
|
Log.e(TAG, "Native.stopProxy threw: ${t.message}", t)
|
||||||
}
|
}
|
||||||
proxyHandle = 0L
|
|
||||||
}
|
}
|
||||||
Log.i(TAG, "teardown: done")
|
Log.i(TAG, "teardown: done")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
teardown()
|
Log.i(TAG, "onDestroy entered")
|
||||||
|
try {
|
||||||
|
teardown()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Belt-and-suspenders. Crashing out of onDestroy takes the
|
||||||
|
// whole process with it — user-visible as the app closing
|
||||||
|
// right when they tap Stop, which is exactly the symptom we
|
||||||
|
// are trying to fix. Anything that gets here is logged and
|
||||||
|
// swallowed.
|
||||||
|
Log.e(TAG, "onDestroy teardown threw: ${t.message}", t)
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
Log.i(TAG, "onDestroy done")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNotif(proxyPort: Int): Notification {
|
private fun buildNotif(proxyPort: Int): Notification {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ fun HomeScreen(
|
|||||||
append("Certificate not yet installed.")
|
append("Certificate not yet installed.")
|
||||||
if (!o.downloadPath.isNullOrBlank()) {
|
if (!o.downloadPath.isNullOrBlank()) {
|
||||||
append(" Saved to ${o.downloadPath}. ")
|
append(" Saved to ${o.downloadPath}. ")
|
||||||
append("In Settings: Encryption & credentials → Install a certificate → \"CA certificate\" (not VPN, not Wi-Fi) → pick that file.")
|
append("In Settings, search for \"CA certificate\" and install from there — NOT \"VPN & app user certificate\" or \"Wi-Fi\".")
|
||||||
} else {
|
} else {
|
||||||
append(" Tap Install again to retry.")
|
append(" Tap Install again to retry.")
|
||||||
}
|
}
|
||||||
@@ -374,11 +374,13 @@ fun HomeScreen(
|
|||||||
Text(
|
Text(
|
||||||
"On Android 11+ the system removed the inline install path, so " +
|
"On Android 11+ the system removed the inline install path, so " +
|
||||||
"tapping Install will: (1) save a PEM copy to Downloads/mhrv-ca.crt, " +
|
"tapping Install will: (1) save a PEM copy to Downloads/mhrv-ca.crt, " +
|
||||||
"(2) open Security settings. From there navigate to Encryption & " +
|
"(2) open the Settings app.\n\n" +
|
||||||
"credentials → Install a certificate → pick \"CA certificate\" (NOT " +
|
"Inside Settings, tap the search bar and type \"CA certificate\". " +
|
||||||
"\"VPN & app user certificate\" or \"Wi-Fi certificate\") → select " +
|
"Open the result labelled \"CA certificate\" (NOT \"VPN & app user " +
|
||||||
"mhrv-ca.crt from Downloads. If you don't have a screen lock, Android " +
|
"certificate\" or \"Wi-Fi certificate\"). Pick mhrv-ca.crt from " +
|
||||||
"will ask you to add one first."
|
"Downloads when prompted. If you don't have a screen lock, Android " +
|
||||||
|
"will ask you to add one first — that's an OS requirement for " +
|
||||||
|
"installing any user CA."
|
||||||
)
|
)
|
||||||
if (fp != null) {
|
if (fp != null) {
|
||||||
Text("Subject: ${cn ?: "(unknown)"}", style = MaterialTheme.typography.labelMedium)
|
Text("Subject: ${cn ?: "(unknown)"}", style = MaterialTheme.typography.labelMedium)
|
||||||
@@ -911,10 +913,10 @@ private fun HowToUseCard(listenPort: Int) {
|
|||||||
Text(
|
Text(
|
||||||
"1. Paste one or more Apps Script deployment URLs (or bare IDs) and your auth_key.\n" +
|
"1. Paste one or more Apps Script deployment URLs (or bare IDs) and your auth_key.\n" +
|
||||||
"2. Tap Install MITM certificate. Confirm the dialog — the cert is saved to " +
|
"2. Tap Install MITM certificate. Confirm the dialog — the cert is saved to " +
|
||||||
"Downloads/mhrv-ca.crt and Security settings opens. Navigate: Encryption & " +
|
"Downloads/mhrv-ca.crt and the Settings app opens. Use Settings' search bar " +
|
||||||
"credentials → Install a certificate → \"CA certificate\" (NOT \"VPN & app user " +
|
"to find \"CA certificate\", tap that result (NOT \"VPN & app user certificate\" " +
|
||||||
"certificate\" or \"Wi-Fi\"). Pick mhrv-ca.crt from Downloads. You'll be asked to " +
|
"or \"Wi-Fi\"), and pick mhrv-ca.crt from Downloads. You'll be asked to set a " +
|
||||||
"set a screen lock if you don't have one (Android requirement).\n" +
|
"screen lock if you don't have one (Android requirement).\n" +
|
||||||
"3. Before tapping Start, expand \"SNI pool + tester\" and hit \"Test all\". If " +
|
"3. Before tapping Start, expand \"SNI pool + tester\" and hit \"Test all\". If " +
|
||||||
"every entry times out, your google_ip is unreachable — replace it with one that " +
|
"every entry times out, your google_ip is unreachable — replace it with one that " +
|
||||||
"resolves locally (e.g. `nslookup www.google.com` on any working device).\n" +
|
"resolves locally (e.g. `nslookup www.google.com` on any working device).\n" +
|
||||||
|
|||||||
+2
-5
@@ -89,12 +89,9 @@ This step is annoying but unavoidable: the proxy terminates TLS on your behalf s
|
|||||||
|
|
||||||
1. In the app, tap **Install MITM certificate**.
|
1. In the app, tap **Install MITM certificate**.
|
||||||
2. Read the confirmation dialog — it shows the certificate fingerprint (handy to verify later). Tap **Install**.
|
2. Read the confirmation dialog — it shows the certificate fingerprint (handy to verify later). Tap **Install**.
|
||||||
3. The app saves `Downloads/mhrv-ca.crt` and deep-links Android into **Settings → Security & privacy** (or similar; wording varies by OEM).
|
3. The app saves `Downloads/mhrv-ca.crt` and opens the top-level **Settings** app.
|
||||||
4. 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.
|
4. 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.
|
||||||
5. In Settings, navigate: **Encryption & credentials → Install a certificate → "CA certificate"**.
|
5. In Settings, tap the **search bar at the top** and type `CA certificate`. Pick the result labelled **"CA certificate"** (or on some OEMs "Install CA certificate"). The menu path varies wildly between Pixel / Samsung / Xiaomi / etc., which is why searching beats navigating.
|
||||||
- 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.
|
- **Do NOT** pick "VPN & app user certificate" or "Wi-Fi certificate" — wrong category, won't work.
|
||||||
6. Android warns you: **"Your network may be monitored by an unknown third party"**. That's us. Tap **Install anyway**.
|
6. Android warns you: **"Your network may be monitored by an unknown third party"**. That's us. Tap **Install anyway**.
|
||||||
7. Pick **Downloads** → tap **mhrv-ca.crt**. Give it a friendly name (or accept the default). Tap **OK**.
|
7. Pick **Downloads** → tap **mhrv-ca.crt**. Give it a friendly name (or accept the default). Tap **OK**.
|
||||||
|
|||||||
+5
-5
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
This folder contains the prebuilt binaries from the latest release, committed directly to the repository for users who cannot reach the GitHub Releases page.
|
This folder contains the prebuilt binaries from the latest release, committed directly to the repository for users who cannot reach the GitHub Releases page.
|
||||||
|
|
||||||
Current version: **v1.0.1**
|
Current version: **v1.0.2**
|
||||||
|
|
||||||
| File | Platform | Contents |
|
| File | Platform | Contents |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `mhrv-rs-android-universal-v1.0.1.apk` | Android 7.0+ (all ABIs) | Universal APK — arm64-v8a, armeabi-v7a, x86_64, x86 in one file |
|
| `mhrv-rs-android-universal-v1.0.2.apk` | Android 7.0+ (all ABIs) | Universal APK — arm64-v8a, armeabi-v7a, x86_64, x86 in one file |
|
||||||
| `mhrv-rs-linux-amd64.tar.gz` | Linux x86_64 | `mhrv-rs`, `mhrv-rs-ui`, `run.sh` |
|
| `mhrv-rs-linux-amd64.tar.gz` | Linux x86_64 | `mhrv-rs`, `mhrv-rs-ui`, `run.sh` |
|
||||||
| `mhrv-rs-linux-arm64.tar.gz` | Linux aarch64 | `mhrv-rs`, `run.sh` (CLI only) |
|
| `mhrv-rs-linux-arm64.tar.gz` | Linux aarch64 | `mhrv-rs`, `run.sh` (CLI only) |
|
||||||
| `mhrv-rs-raspbian-armhf.tar.gz` | Raspberry Pi / ARMv7 hardfloat | `mhrv-rs`, `run.sh` (CLI only) |
|
| `mhrv-rs-raspbian-armhf.tar.gz` | Raspberry Pi / ARMv7 hardfloat | `mhrv-rs`, `run.sh` (CLI only) |
|
||||||
@@ -45,7 +45,7 @@ Extract `mhrv-rs-windows-amd64.zip`, then double-click `run.bat` inside the extr
|
|||||||
|
|
||||||
### Android
|
### Android
|
||||||
|
|
||||||
Copy `mhrv-rs-android-universal-v1.0.1.apk` to your phone, tap it from the Files app, and allow "Install unknown apps" for whichever app is opening the APK (Files, Chrome, etc.). See [the Android guide](../docs/android.md) for the full walk-through of the first-run steps (Apps Script deployment, MITM CA install, VPN permission, SNI tester).
|
Copy `mhrv-rs-android-universal-v1.0.2.apk` to your phone, tap it from the Files app, and allow "Install unknown apps" for whichever app is opening the APK (Files, Chrome, etc.). See [the Android guide](../docs/android.md) for the full walk-through of the first-run steps (Apps Script deployment, MITM CA install, VPN permission, SNI tester).
|
||||||
|
|
||||||
See the [main README](../README.md) for desktop setup (Apps Script deployment, config, browser proxy settings).
|
See the [main README](../README.md) for desktop setup (Apps Script deployment, config, browser proxy settings).
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ See the [main README](../README.md) for desktop setup (Apps Script deployment, c
|
|||||||
|
|
||||||
این پوشه شامل فایلهای آخرین نسخه است و مستقیماً در ریپو قرار گرفته برای کاربرانی که به صفحهٔ GitHub Releases دسترسی ندارند.
|
این پوشه شامل فایلهای آخرین نسخه است و مستقیماً در ریپو قرار گرفته برای کاربرانی که به صفحهٔ GitHub Releases دسترسی ندارند.
|
||||||
|
|
||||||
نسخهٔ فعلی: **v1.0.1**
|
نسخهٔ فعلی: **v1.0.2**
|
||||||
|
|
||||||
### دانلود از طریق ZIP
|
### دانلود از طریق ZIP
|
||||||
|
|
||||||
@@ -73,6 +73,6 @@ cd mhrv-rs-macos-arm64
|
|||||||
|
|
||||||
**ویندوز:** فایل `mhrv-rs-windows-amd64.zip` را extract کنید و داخل پوشه روی `run.bat` دو بار کلیک کنید (UAC را قبول کنید تا گواهی MITM نصب شود).
|
**ویندوز:** فایل `mhrv-rs-windows-amd64.zip` را extract کنید و داخل پوشه روی `run.bat` دو بار کلیک کنید (UAC را قبول کنید تا گواهی MITM نصب شود).
|
||||||
|
|
||||||
**اندروید:** فایل `mhrv-rs-android-universal-v1.0.1.apk` را روی گوشی کپی کنید، از Files app روی آن tap کنید و اجازهٔ "نصب برنامههای ناشناس" را بدهید. راهنمای کامل شروع به کار (دیپلوی Apps Script، نصب CA، اجازهٔ VPN، تستر SNI) در [راهنمای اندروید](../docs/android.md) هست.
|
**اندروید:** فایل `mhrv-rs-android-universal-v1.0.2.apk` را روی گوشی کپی کنید، از Files app روی آن tap کنید و اجازهٔ "نصب برنامههای ناشناس" را بدهید. راهنمای کامل شروع به کار (دیپلوی Apps Script، نصب CA، اجازهٔ VPN، تستر SNI) در [راهنمای اندروید](../docs/android.md) هست.
|
||||||
|
|
||||||
برای راهاندازی کامل دسکتاپ (دیپلوی Apps Script، config، تنظیم proxy مرورگر) به [README اصلی](../README.md) مراجعه کنید.
|
برای راهاندازی کامل دسکتاپ (دیپلوی Apps Script، config، تنظیم proxy مرورگر) به [README اصلی](../README.md) مراجعه کنید.
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user