diff --git a/Cargo.toml b/Cargo.toml index 5ec62672..12b0aaad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mhrv-rs" -version = "1.9.24" +version = "1.9.25" edition = "2021" description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting" license = "MIT" diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2cb00e5f..e7fc15ae 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.therealaleph.mhrv" minSdk = 24 // Android 7.0 — covers 99%+ of live devices. targetSdk = 34 - versionCode = 158 - versionName = "1.8.1" + versionCode = 159 + versionName = "1.9.25" // Ship all four mainstream Android ABIs: // - arm64-v8a — 95%+ of real-world Android phones since 2019 diff --git a/android/app/src/main/java/com/therealaleph/mhrv/MhrvVpnService.kt b/android/app/src/main/java/com/therealaleph/mhrv/MhrvVpnService.kt index 355f85a1..053edc29 100644 --- a/android/app/src/main/java/com/therealaleph/mhrv/MhrvVpnService.kt +++ b/android/app/src/main/java/com/therealaleph/mhrv/MhrvVpnService.kt @@ -268,7 +268,7 @@ class MhrvVpnService : VpnService() { append(" --dns virtual") append(" --verbosity info") append(" --close-fd-on-drop true") - if (cfg.mode == Mode.FULL) append(" --udpgw-server 198.18.0.1:7300") + if (cfg.mode == Mode.FULL) append(" --udpgw-server $UDPGW_MAGIC_DEST") } val worker = Thread({ try { @@ -499,5 +499,14 @@ class MhrvVpnService : VpnService() { private const val NOTIF_ID = 0x1001 private const val MTU = 1500 const val ACTION_STOP = "com.therealaleph.mhrv.STOP" + + // Magic udpgw destination passed to tun2proxy in Full mode. MUST stay + // outside tun2proxy's --dns virtual range (198.18.0.0/15) — otherwise + // virtual DNS can synthesise the magic IP for a real hostname and + // silently mis-route its traffic into the udpgw path. See issue #251 + // and `UDPGW_MAGIC_IP` / `UDPGW_MAGIC_PORT` in tunnel-node/src/udpgw.rs. + // Wire-protocol convention: both sides must agree. v1.9.25+ tunnel-nodes + // also accept the legacy 198.18.0.1:7300 for one deprecation cycle. + private const val UDPGW_MAGIC_DEST = "192.0.2.1:7300" } } diff --git a/android/app/src/main/java/com/therealaleph/mhrv/Native.kt b/android/app/src/main/java/com/therealaleph/mhrv/Native.kt index fbf44dc5..798d5236 100644 --- a/android/app/src/main/java/com/therealaleph/mhrv/Native.kt +++ b/android/app/src/main/java/com/therealaleph/mhrv/Native.kt @@ -114,7 +114,7 @@ object Native { * Start tun2proxy via its CLI args C API (`tun2proxy_run_with_cli_args`). * Resolved at runtime via dlsym from libtun2proxy.so — no fork needed. * - * @param cliArgs full CLI string, e.g. "tun2proxy --proxy socks5://... --tun-fd 42 --udpgw-server 198.18.0.1:7300" + * @param cliArgs full CLI string, e.g. "tun2proxy --proxy socks5://... --tun-fd 42 --udpgw-server 192.0.2.1:7300" * @param tunMtu TUN MTU (typically 1500) * @return 0 on normal shutdown, negative on error. BLOCKS. */ diff --git a/docs/changelog/v1.9.25.md b/docs/changelog/v1.9.25.md new file mode 100644 index 00000000..b08b62a4 --- /dev/null +++ b/docs/changelog/v1.9.25.md @@ -0,0 +1,48 @@ + +
+ +• **رفع باگ Full mode «Google و اکثر سایت‌ها خراب، تلگرام سالم» — `udpgw magic IP از داخل virtual-DNS range tun2proxy منتقل شد`** ([#251](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/251) by @dazzling-no-more). + +در Full mode روی Android، تلگرام کار می‌کرد ولی Google search و اکثر سایت‌ها silently fail می‌شدن — `apps_script` mode روی همون device سالم بود و VPS هم idle. + +**علت**: آدرس magic مربوط به udpgw (یعنی `198.18.0.1:7300`) داخل `198.18.0.0/15` بود، یعنی دقیقاً همون range‌ای که `tun2proxy --dns virtual` ازش IPهای ساختگی رو برای hostname lookupها اختصاص می‌ده. هر دفعه که virtual DNS اتفاقاً `198.18.0.1` رو به یک hostname مثل `www.google.com` allocate می‌کرد، traffic اون host به‌عنوان udpgw connection مصادره می‌شد و drop می‌شد. تلگرام immune بود چون native clientش از IPهای عددی hardcoded استفاده می‌کنه؛ همچنین `apps_script` mode هم immune بود چون اصلاً `--udpgw-server` ست نمی‌کنه. + +**راه‌حل**: ثابت `UDPGW_MAGIC_IP` به `192.0.2.1` (RFC 5737 TEST-NET-1) منتقل شد. دو فایل تغییر کرده: یکی `tunnel-node/src/udpgw.rs` (constant + tests) و دیگری `android/.../MhrvVpnService.kt` (که حالا از یک companion const به اسم `UDPGW_MAGIC_DEST` استفاده می‌کنه). + +**سازگاری با نسخه‌های قدیمی**: نسخهٔ جدید tunnel-node همچنان `198.18.0.1:7300` قدیمی رو هم accept می‌کنه برای یک deprecation cycle (حذف در v1.10.0) — یعنی اگه VPS رو زودتر آپدیت کنی، Android قدیمی هنوز کار می‌کنه. **ولی اگه Android رو زودتر آپدیت کنی، tunnel-node قدیمی UDP relay رو در Full mode break می‌کنه**. توصیه: اول tunnel-node رو آپدیت کن، بعد APK رو. + +
+--- +• **Fix Full mode "Google + most websites broken while Telegram works" — `udpgw magic IP moved out of tun2proxy virtual-DNS range`** ([#251](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/251) by @dazzling-no-more). Users on Android Full mode reported that Telegram worked fine but Google search and most other websites failed to load — while apps_script mode on the same device + same `google_ip` worked perfectly and the VPS was sitting idle. + +**Root cause**: the udpgw magic destination address (`198.18.0.1:7300`) lived inside `198.18.0.0/15` — the exact same range that tun2proxy's `--dns virtual` allocator uses to synthesise fake IPs for hostname lookups. Whenever virtual DNS happened to assign `198.18.0.1` to a real hostname (e.g. `www.google.com`), that hostname's connections were intercepted by tun2proxy *itself* as a udpgw request before they ever reached the SOCKS5 proxy. Result: a random subset of DNS-resolved hosts silently broke per session, depending on which hostname won the `198.18.0.1` allocation. Telegram was unaffected because its native client uses hardcoded numeric IPs (no DNS allocation needed). apps_script mode was unaffected because it doesn't pass `--udpgw-server` to tun2proxy at all. + +**Fix**: relocate `UDPGW_MAGIC_IP` from `198.18.0.1` to `192.0.2.1` (RFC 5737 TEST-NET-1). TEST-NET-1 is reserved for documentation, never routed on the public internet, and — critically — outside any virtual-DNS allocation pool. Structurally equivalent to the old address as a "guaranteed-not-real-destination", just no longer colliding with tun2proxy's reserved range. + +Coordinated two-side change: + +1. **`tunnel-node/src/udpgw.rs`**: `UDPGW_MAGIC_IP = [192, 0, 2, 1]`, doc comment now cites RFC 5737 + explicitly explains why it must stay out of `198.18.0.0/15`. Test additions: `is_udpgw_dest_works` covers both the new IP and the legacy IP (back-compat assertion); new `magic_ip_outside_virtual_dns_range` enforces the invariant at the `198.18.0.0/15` *range* level, so any future move to `198.19.x.y` would also fail the test rather than re-introducing the same class of bug. +2. **`android/.../MhrvVpnService.kt`**: `--udpgw-server $UDPGW_MAGIC_DEST` where `UDPGW_MAGIC_DEST = "192.0.2.1:7300"` is a new companion-object constant, with a docstring pointing back at the Rust constant — gives the next editor a single, labelled place to update if the convention ever changes again. + +**Back-compatibility — partial, one-way**: + +The udpgw magic IP is a wire-protocol convention between the Android client and the `mhrv-tunnel` Docker container. v1.9.25 tunnel-nodes accept both the new `192.0.2.1:7300` and the legacy `198.18.0.1:7300` for one deprecation cycle (slated for removal in v1.10.0). That softens — but does *not* fully resolve — the asymmetric-upgrade matrix: + +| Android | Tunnel-node | Full-mode UDP relay | +|---|---|---| +| v1.9.25 | v1.9.25 | ✅ fully fixed | +| ≤v1.9.24 | v1.9.25 | ⚠️ udpgw handshake works (legacy IP still recognised by the node), but the **old client still asks tun2proxy for `--udpgw-server 198.18.0.1:7300`** — meaning the underlying #251 virtual-DNS-pool collision is still live on the device. Telegram works; the random Google-search-style breakage persists until the APK is updated. | +| v1.9.25 | ≤v1.9.24 | ❌ **breaks silently** — new client sends `192.0.2.1`, old node treats it as a real TCP destination and the connect fails | +| ≤v1.9.24 | ≤v1.9.24 | unchanged from before (still has the original #251 bug) | + +**Recommended upgrade order**: update **both halves** to v1.9.25. The fix is on the *client* side (which magic IP it asks tun2proxy to reserve) — the tunnel-node back-compat shim only prevents a hard handshake break during the window where the node is upgraded first; it does not fix the original bug. If you can only update one half right now: do the **APK first** (or both together), since updating just the tunnel-node leaves clients still hitting the virtual-DNS collision. `apps_script`-only users are unaffected (the udpgw path isn't used in apps_script mode). + +**Diagnostic note for stuck users**: if Telegram works on Full mode but Google search / random websites silently fail on v1.9.24 or earlier, this is your bug. As a workaround pending upgrade, add Google domains to `passthrough_hosts` to route them through tunnel-node like Telegram does: + +```json +{ + "passthrough_hosts": [".google.com", ".gstatic.com", ".googleusercontent.com", ".googleapis.com", ".youtube.com", ".ytimg.com"] +} +``` + +Slower per-request (Apps Script overhead) but bypasses the virtual-DNS clash entirely. Remove once both halves are on v1.9.25. diff --git a/tunnel-node/src/udpgw.rs b/tunnel-node/src/udpgw.rs index 3c6e1800..6e9a0b31 100644 --- a/tunnel-node/src/udpgw.rs +++ b/tunnel-node/src/udpgw.rs @@ -19,11 +19,28 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream}; use tokio::net::UdpSocket; /// Magic address that the client connects to via the tunnel protocol. -/// `198.18.0.0/15` is reserved for benchmarking (RFC 2544) and will -/// never be a real destination. -pub const UDPGW_MAGIC_IP: [u8; 4] = [198, 18, 0, 1]; +/// `192.0.2.0/24` is reserved for documentation (RFC 5737 TEST-NET-1) +/// and will never be a real destination. +/// +/// Must NOT live in `198.18.0.0/15`: tun2proxy's `--dns virtual` allocator +/// (used by the Android client in Full mode) synthesises fake IPs in that +/// range for hostname lookups. If the magic IP collided with one of those +/// synthetic IPs, every request to whichever hostname got that allocation +/// would be silently mis-routed into the udpgw path. See issue #251. +pub const UDPGW_MAGIC_IP: [u8; 4] = [192, 0, 2, 1]; +/// Pre-formatted dotted-quad form of `UDPGW_MAGIC_IP`. Compared against +/// incoming hostnames in [`is_udpgw_dest`]; kept in sync with the octets +/// above by the `magic_host_matches_octets` test. +pub const UDPGW_MAGIC_HOST: &str = "192.0.2.1"; pub const UDPGW_MAGIC_PORT: u16 = 7300; +/// Pre-#251 magic IP — still recognised by `is_udpgw_dest` for one +/// deprecation cycle so users who upgrade the `mhrv-tunnel` Docker +/// container ahead of the Android APK don't lose Full-mode UDP relay +/// during the version-skew window. Slated for removal in v1.10.0. +const LEGACY_UDPGW_MAGIC_IP: [u8; 4] = [198, 18, 0, 1]; +const LEGACY_UDPGW_MAGIC_HOST: &str = "198.18.0.1"; + const FLAG_KEEPALIVE: u8 = 0x01; const FLAG_DATA: u8 = 0x02; const FLAG_ERR: u8 = 0x20; @@ -195,8 +212,12 @@ fn serialise_frame(frame: &Frame) -> Vec { // ------------------------------------------------------------------------- /// Returns `true` if the connect destination is the magic udpgw address. +/// +/// Accepts both the current `UDPGW_MAGIC_HOST` (`192.0.2.1`) and the legacy +/// `LEGACY_UDPGW_MAGIC_HOST` (`198.18.0.1`) so a v1.9.25+ tunnel-node still +/// works with pre-#251 Android clients during the upgrade window. pub fn is_udpgw_dest(host: &str, port: u16) -> bool { - port == UDPGW_MAGIC_PORT && host == format!("{}.{}.{}.{}", UDPGW_MAGIC_IP[0], UDPGW_MAGIC_IP[1], UDPGW_MAGIC_IP[2], UDPGW_MAGIC_IP[3]) + port == UDPGW_MAGIC_PORT && (host == UDPGW_MAGIC_HOST || host == LEGACY_UDPGW_MAGIC_HOST) } /// Per-conn_id persistent UDP socket with a background reader that @@ -505,8 +526,41 @@ mod tests { #[test] fn is_udpgw_dest_works() { + // Current magic IP — must be recognised. + assert!(is_udpgw_dest("192.0.2.1", 7300)); + // Legacy pre-#251 magic IP — still recognised for one deprecation + // cycle so old Android clients keep working against a new tunnel-node. + // Remove this assertion (and `LEGACY_UDPGW_MAGIC_IP`) in v1.10.0. assert!(is_udpgw_dest("198.18.0.1", 7300)); + // Wrong port on either IP, or unrelated host on the magic port, must not match. + assert!(!is_udpgw_dest("192.0.2.1", 80)); assert!(!is_udpgw_dest("198.18.0.1", 80)); assert!(!is_udpgw_dest("8.8.8.8", 7300)); } + + #[test] + fn magic_host_matches_octets() { + // The dotted-quad `_HOST` constants are what `is_udpgw_dest` actually + // compares against — but the `_IP` octet arrays are what tests and + // future humans reason about. If they drift, `is_udpgw_dest` silently + // stops matching what the Android client is sending. Pin them here. + let dotted = |ip: [u8; 4]| format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]); + assert_eq!(dotted(UDPGW_MAGIC_IP), UDPGW_MAGIC_HOST); + assert_eq!(dotted(LEGACY_UDPGW_MAGIC_IP), LEGACY_UDPGW_MAGIC_HOST); + } + + #[test] + fn magic_ip_outside_virtual_dns_range() { + // tun2proxy's `--dns virtual` allocator synthesises fake IPs inside + // 198.18.0.0/15 (covers 198.18.0.0 – 198.19.255.255). The *current* + // magic IP MUST stay outside that range — see #251. The legacy IP + // is intentionally still in the bad range (that was the bug); it + // is exempt and will be removed in v1.10.0. + let [a, b, _, _] = UDPGW_MAGIC_IP; + assert!( + !(a == 198 && (b == 18 || b == 19)), + "UDPGW_MAGIC_IP {:?} is inside 198.18.0.0/15 — will collide with tun2proxy --dns virtual (see #251)", + UDPGW_MAGIC_IP + ); + } }