Files
MasterHttpRelayVPN-RUST/android/app/build.gradle.kts
T
dazzling-no-more e70947ff0d fix(udpgw): move magic IP out of tun2proxy virtual-DNS range (#251, #1143)
Closes #251. In Android Full mode, Telegram worked but Google search and most other websites failed silently. `apps_script` mode on the same setup was unaffected.

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

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

## Back-compat

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

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

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

## Verified locally

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

## Version bump

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

Reviewed via Anthropic Claude.

Co-Authored-By: dazzling-no-more <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:45:23 +03:00

225 lines
8.9 KiB
Kotlin

import org.gradle.api.tasks.Exec
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.therealaleph.mhrv"
compileSdk = 34
defaultConfig {
applicationId = "com.therealaleph.mhrv"
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
targetSdk = 34
versionCode = 159
versionName = "1.9.25"
// Ship all four mainstream Android ABIs:
// - arm64-v8a — 95%+ of real-world Android phones since 2019
// - armeabi-v7a — older/cheaper devices still on 32-bit ARM
// - x86_64 — Android emulator on Intel Macs + Chromebooks
// - x86 — legacy 32-bit Intel emulator; cheap to include
// Per-ABI .so files push the APK up to ~50 MB, but users expect one
// APK that Just Works rather than "pick the right ABI" which nobody
// does correctly. Google Play would auto-split; we ship universal.
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
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 {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName("release")
}
}
// Per-ABI APK splits in addition to the universal APK.
//
// Issue #136: GitHub Releases is filtered from inside IR, and the
// universal APK (~50 MB, all four ABIs bundled) is the bottleneck —
// users on slow or unstable censorship-tunnel paths often can't
// pull down 50 MB reliably. Per-ABI APKs are ~15 MB each (only one
// copy of libmhrv_rs.so + libtun2proxy.so instead of four), which
// is small enough to succeed where the universal fails.
//
// Keeping the universal APK too (`isUniversalApk = true`) because
// existing download paths / docs / Telegram mirrors all reference
// the universal name — removing it would break every link in the
// wild. The per-ABI outputs are additive.
splits {
abi {
isEnable = true
reset()
include("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
isUniversalApk = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
// libmhrv_rs.so is produced by `cargo ndk` in the repo root and dropped
// under app/src/main/jniLibs/<abi>/. The cargoBuild task below runs
// that before each assembleDebug / assembleRelease.
sourceSets["main"].jniLibs.srcDirs("src/main/jniLibs")
packaging {
resources.excludes += setOf(
"META-INF/AL2.0",
"META-INF/LGPL2.1",
)
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.06.00")
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity-compose:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
// AppCompatDelegate.setApplicationLocales is the only thing we need
// out of AppCompat — lets us flip the whole app locale at runtime
// from MhrvApp.onCreate without touching every composable.
implementation("androidx.appcompat:appcompat:1.7.0")
// Compose UI.
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
// QR code generation + scanning (self-contained, no ML Kit needed).
implementation("com.google.zxing:core:3.5.3")
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
// --------------------------------------------------------------------------
// Cross-compile the Rust crate to arm64 Android and drop the .so into the
// place Android's packager looks. We hand the work off to `cargo ndk` which
// wraps the right CC / AR / linker env vars for us.
//
// This ties to the `assemble*` task so every debug/release build triggers
// a `cargo ndk` — no manual step. In CI we'd cache the target/ dir to
// avoid full rebuilds.
// --------------------------------------------------------------------------
val rustCrateDir = rootProject.projectDir.parentFile
val jniLibsDir = file("src/main/jniLibs")
// After cargo-ndk dumps artifacts into each jniLibs/<abi>/ dir, the
// tun2proxy cdylib lands as `libtun2proxy-<hash>.so` (rustc's deps/ naming
// convention, because tun2proxy is a transitive dep not a root crate).
// Android's System.loadLibrary expects a stable name, and the hash changes
// between builds, so we normalize it to `libtun2proxy.so` in every ABI dir.
// Also deletes any stale hash-suffixed copies from previous builds.
fun normalizeTun2proxySo() {
val jniLibsRoot = file("src/main/jniLibs")
if (!jniLibsRoot.isDirectory) return
jniLibsRoot.listFiles()?.filter { it.isDirectory }?.forEach { abiDir ->
val hashed = abiDir.listFiles { f -> f.name.matches(Regex("libtun2proxy-[0-9a-f]+\\.so")) }
?: emptyArray()
val newest = hashed.maxByOrNull { it.lastModified() }
if (newest != null) {
val target = abiDir.resolve("libtun2proxy.so")
if (target.exists()) target.delete()
newest.copyTo(target, overwrite = true)
}
hashed.forEach { it.delete() }
}
}
// All ABIs we ship. Keep in sync with `android.defaultConfig.ndk.abiFilters`
// above; if these drift, the APK either includes .so files with no matching
// ABI entry (dead weight) or advertises ABIs with no .so (runtime
// UnsatisfiedLinkError on devices that pick that split).
val androidAbis = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
tasks.register<Exec>("cargoBuildDebug") {
group = "build"
// Intentionally ALWAYS uses --release. The Rust debug build is 80+MB
// of unoptimized object code vs 3MB with release; the 20x APK bloat is
// never worth it just for a Rust stack trace you wouldn't see in
// logcat anyway. If you need Rust debug symbols, temporarily drop
// `--release` below and accept the APK size.
description = "Cross-compile mhrv_rs for all ABIs (release — same as cargoBuildRelease)"
workingDir = rustCrateDir
commandLine(buildList<String> {
add("cargo"); add("ndk")
androidAbis.forEach { add("-t"); add(it) }
add("-o"); add(jniLibsDir.absolutePath)
add("build"); add("--release")
})
doLast { normalizeTun2proxySo() }
}
tasks.register<Exec>("cargoBuildRelease") {
group = "build"
description = "Cross-compile mhrv_rs for all ABIs (release)"
workingDir = rustCrateDir
commandLine(buildList<String> {
add("cargo"); add("ndk")
androidAbis.forEach { add("-t"); add(it) }
add("-o"); add(jniLibsDir.absolutePath)
add("build"); add("--release")
})
doLast { normalizeTun2proxySo() }
}
// Hook the right cargo task in front of each Android build variant.
tasks.configureEach {
when (name) {
"mergeDebugJniLibFolders" -> dependsOn("cargoBuildDebug")
"mergeReleaseJniLibFolders" -> dependsOn("cargoBuildRelease")
}
}