feat: block QUIC by default + UI toggle (Android & desktop) (#805)

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: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yyoyoian-pixel
2026-05-06 23:40:35 +02:00
committed by GitHub
parent ea624e886a
commit 5a07709be8
4 changed files with 44 additions and 2 deletions
@@ -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)) }
@@ -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,
+11 -1
View File
@@ -420,7 +420,7 @@ fn load_form() -> (FormState, Option<String>) {
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.",
);
});
});
});
+6 -1
View File
@@ -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