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,