From 5e054f1d824645ae0cc3b327f3530f90db6b6628 Mon Sep 17 00:00:00 2001 From: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Date: Wed, 6 May 2026 11:32:15 +0200 Subject: [PATCH] feat: block QUIC by default + UI toggle (Android & desktop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QUIC over the TCP-based tunnel causes TCP-over-TCP meltdown — users see <1 Mbps where HTTPS/TCP would do >50. The existing `block_quic` config option was off by default and had no UI on either platform, so most users suffered QUIC degradation without knowing why. Changes: - Default `block_quic` to `true` (was `false`). Browsers detect the silent UDP/443 drop and fall back to TCP/HTTPS within seconds. - Add "Block QUIC" toggle in Android Advanced UI. - Add "Block QUIC (UDP/443)" checkbox in desktop UI (was config-only, issue #213). - Android: always emit `block_quic` in JSON so the Rust default doesn't silently override the user's choice. Closes #793. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/com/therealaleph/mhrv/ConfigStore.kt | 5 +++++ .../com/therealaleph/mhrv/ui/HomeScreen.kt | 22 +++++++++++++++++++ src/bin/ui.rs | 12 +++++++++- src/config.rs | 7 +++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt b/android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt index 3c0566e1..8741e05a 100644 --- a/android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt +++ b/android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt @@ -98,6 +98,8 @@ data class MhrvConfig( val parallelRelay: Int = 1, val coalesceStepMs: Int = 10, val coalesceMaxMs: Int = 1000, + /** Block QUIC (UDP/443). QUIC over TCP tunnel causes meltdown. */ + val blockQuic: Boolean = true, val upstreamSocks5: String = "", /** @@ -219,6 +221,7 @@ data class MhrvConfig( put("parallel_relay", parallelRelay) if (coalesceStepMs != 10) put("coalesce_step_ms", coalesceStepMs) if (coalesceMaxMs != 1000) put("coalesce_max_ms", coalesceMaxMs) + put("block_quic", blockQuic) if (upstreamSocks5.isNotBlank()) { put("upstream_socks5", upstreamSocks5.trim()) } @@ -330,6 +333,7 @@ object ConfigStore { if (cfg.parallelRelay != defaults.parallelRelay) obj.put("parallel_relay", cfg.parallelRelay) if (cfg.coalesceStepMs != defaults.coalesceStepMs) obj.put("coalesce_step_ms", cfg.coalesceStepMs) if (cfg.coalesceMaxMs != defaults.coalesceMaxMs) obj.put("coalesce_max_ms", cfg.coalesceMaxMs) + if (cfg.blockQuic != defaults.blockQuic) obj.put("block_quic", cfg.blockQuic) if (cfg.upstreamSocks5.isNotBlank()) obj.put("upstream_socks5", cfg.upstreamSocks5) if (cfg.passthroughHosts.isNotEmpty()) obj.put("passthrough_hosts", JSONArray().apply { cfg.passthroughHosts.forEach { put(it) } }) if (cfg.tunnelDoh != defaults.tunnelDoh) obj.put("tunnel_doh", cfg.tunnelDoh) @@ -433,6 +437,7 @@ object ConfigStore { parallelRelay = obj.optInt("parallel_relay", 1), coalesceStepMs = obj.optInt("coalesce_step_ms", 10), coalesceMaxMs = obj.optInt("coalesce_max_ms", 1000), + blockQuic = obj.optBoolean("block_quic", true), upstreamSocks5 = obj.optString("upstream_socks5", ""), passthroughHosts = obj.optJSONArray("passthrough_hosts")?.let { arr -> buildList { for (i in 0 until arr.length()) add(arr.optString(i)) } diff --git a/android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt b/android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt index 19953f62..2550f959 100644 --- a/android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt +++ b/android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt @@ -1265,6 +1265,28 @@ private fun AdvancedSettings( ) } + // Block QUIC toggle + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + "Block QUIC", + style = MaterialTheme.typography.bodyMedium, + ) + Text( + "Drop UDP/443 so browsers use TCP/HTTPS. QUIC over TCP tunnel causes meltdown.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch( + checked = cfg.blockQuic, + onCheckedChange = { onChange(cfg.copy(blockQuic = it)) }, + ) + } + // Block DoH toggle Row( verticalAlignment = Alignment.CenterVertically, diff --git a/src/bin/ui.rs b/src/bin/ui.rs index a733237d..68974a14 100644 --- a/src/bin/ui.rs +++ b/src/bin/ui.rs @@ -420,7 +420,7 @@ fn load_form() -> (FormState, Option) { normalize_x_graphql: false, youtube_via_relay: false, passthrough_hosts: Vec::new(), - block_quic: false, + block_quic: true, disable_padding: false, tunnel_doh: true, bypass_doh_hosts: Vec::new(), @@ -1239,6 +1239,16 @@ impl eframe::App for App { Script relay instead — slower for video, but the visible SNI matches the site.", ); }); + ui.horizontal(|ui| { + ui.add_space(120.0 + 8.0); + ui.checkbox(&mut self.form.block_quic, "Block QUIC (UDP/443)") + .on_hover_text( + "Drop QUIC (UDP port 443) so browsers fall back to TCP/HTTPS. \ + QUIC over the TCP-based tunnel causes TCP-over-TCP meltdown \ + (<1 Mbps). Browsers detect the drop and switch to TCP within seconds. \ + Issue #213, #793.", + ); + }); }); }); diff --git a/src/config.rs b/src/config.rs index a7c49f5e..15bfec95 100644 --- a/src/config.rs +++ b/src/config.rs @@ -202,7 +202,7 @@ pub struct Config { /// flag lets users who care about consistency over peak speed /// opt out of QUIC at the source rather than discovering its /// failure modes later. Issue #213. - #[serde(default)] + #[serde(default = "default_block_quic")] pub block_quic: bool, /// When true, suppress the random `_pad` field that v1.8.0+ adds /// to outbound Apps Script requests for DPI evasion. Default off @@ -481,6 +481,11 @@ fn default_google_ip_validation() -> bool {true} /// opt back in with `tunnel_doh: false`. fn default_tunnel_doh() -> bool { true } +/// Default for `block_quic`: `true`. QUIC over the TCP-based tunnel +/// causes TCP-over-TCP meltdown (<1 Mbps). Browsers fall back to +/// HTTPS/TCP within seconds of the silent UDP drop. Issue #793. +fn default_block_quic() -> bool { true } + /// Default for `block_doh`: `true` (browser DoH is rejected so the /// browser falls back to system DNS, which `tun2proxy` resolves /// instantly via virtual DNS — saves the ~1.5s tunnel round-trip per