mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-19 08:04:39 +03:00
fix: block DoH by default + fix Android tunnel_doh config mismatch (#763)
Problem: PR #468 changed `tunnel_doh` default to `true` (tunnel DoH through Apps Script) to avoid ISP-blocked DoH on censored networks. But this added ~1.5s of Apps Script round-trip per DNS lookup — every page load got noticeably slower because Chrome's DoH connections had to traverse the full tunnel path before the page could even start connecting. The Android side had a separate bug: `tunnelDoh` defaulted to `false` but only emitted `tunnel_doh` to JSON when `true`. Since the Rust default is `true`, omitting the field meant Rust always tunneled DoH regardless of the Android UI setting — bypass_doh was silently broken on Android. Fix: - Add `block_doh` config option: immediately reject (RST) connections to known DoH endpoints. Browsers fall back to system DNS, which tun2proxy handles via virtual DNS (instant, zero tunnel cost). Eliminates the DoH round-trip without exposing DoH connections to the ISP (unlike bypass_doh which sends DoH direct). - Default `block_doh: true` on Android — tested on Chrome/Brave, falls back to virtual DNS correctly. - Fix Android `tunnelDoh` default to `true` (matches Rust). - Always emit `tunnel_doh` and `block_doh` explicitly in Android JSON serialization — no more default-mismatch bugs. - Add Block DoH and Bypass DoH toggles in Android Advanced UI. Block DoH takes priority; Bypass DoH is disabled when Block is on. Tested on Pixel 6 Pro: zero chrome.cloudflare-dns.com tunnel sessions with block_doh=true. All DNS resolves instantly via tun2proxy virtual DNS. 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:
@@ -118,7 +118,7 @@ data class MhrvConfig(
|
|||||||
* per name lookup with no real privacy gain. Set this to true to
|
* per name lookup with no real privacy gain. Set this to true to
|
||||||
* keep DoH inside the tunnel. See `src/config.rs` `tunnel_doh`.
|
* keep DoH inside the tunnel. See `src/config.rs` `tunnel_doh`.
|
||||||
*/
|
*/
|
||||||
val tunnelDoh: Boolean = false,
|
val tunnelDoh: Boolean = true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra hostnames added to the built-in DoH default list. Same
|
* Extra hostnames added to the built-in DoH default list. Same
|
||||||
@@ -127,6 +127,13 @@ data class MhrvConfig(
|
|||||||
*/
|
*/
|
||||||
val bypassDohHosts: List<String> = emptyList(),
|
val bypassDohHosts: List<String> = emptyList(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true, reject all connections to known DoH endpoints.
|
||||||
|
* Browsers fall back to system DNS (tun2proxy virtual DNS — instant).
|
||||||
|
* Takes priority over tunnel_doh / bypass_doh.
|
||||||
|
*/
|
||||||
|
val blockDoh: Boolean = true,
|
||||||
|
|
||||||
/** VPN_TUN (everything routed) vs PROXY_ONLY (user configures per-app). */
|
/** VPN_TUN (everything routed) vs PROXY_ONLY (user configures per-app). */
|
||||||
val connectionMode: ConnectionMode = ConnectionMode.VPN_TUN,
|
val connectionMode: ConnectionMode = ConnectionMode.VPN_TUN,
|
||||||
|
|
||||||
@@ -218,7 +225,8 @@ data class MhrvConfig(
|
|||||||
if (passthroughHosts.isNotEmpty()) {
|
if (passthroughHosts.isNotEmpty()) {
|
||||||
put("passthrough_hosts", JSONArray().apply { passthroughHosts.forEach { put(it) } })
|
put("passthrough_hosts", JSONArray().apply { passthroughHosts.forEach { put(it) } })
|
||||||
}
|
}
|
||||||
if (tunnelDoh) put("tunnel_doh", true)
|
put("tunnel_doh", tunnelDoh)
|
||||||
|
put("block_doh", blockDoh)
|
||||||
if (youtubeViaRelay) put("youtube_via_relay", true)
|
if (youtubeViaRelay) put("youtube_via_relay", true)
|
||||||
// Trim/drop-empty/dedupe before serializing — symmetric with the
|
// Trim/drop-empty/dedupe before serializing — symmetric with the
|
||||||
// read-side normalization in loadFromJson(), so a user typing
|
// read-side normalization in loadFromJson(), so a user typing
|
||||||
@@ -325,6 +333,7 @@ object ConfigStore {
|
|||||||
if (cfg.upstreamSocks5.isNotBlank()) obj.put("upstream_socks5", cfg.upstreamSocks5)
|
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.passthroughHosts.isNotEmpty()) obj.put("passthrough_hosts", JSONArray().apply { cfg.passthroughHosts.forEach { put(it) } })
|
||||||
if (cfg.tunnelDoh != defaults.tunnelDoh) obj.put("tunnel_doh", cfg.tunnelDoh)
|
if (cfg.tunnelDoh != defaults.tunnelDoh) obj.put("tunnel_doh", cfg.tunnelDoh)
|
||||||
|
if (cfg.blockDoh != defaults.blockDoh) obj.put("block_doh", cfg.blockDoh)
|
||||||
if (cfg.youtubeViaRelay != defaults.youtubeViaRelay) obj.put("youtube_via_relay", cfg.youtubeViaRelay)
|
if (cfg.youtubeViaRelay != defaults.youtubeViaRelay) obj.put("youtube_via_relay", cfg.youtubeViaRelay)
|
||||||
val cleanBypassDohHosts = cfg.bypassDohHosts
|
val cleanBypassDohHosts = cfg.bypassDohHosts
|
||||||
.map { it.trim() }
|
.map { it.trim() }
|
||||||
@@ -428,7 +437,8 @@ object ConfigStore {
|
|||||||
passthroughHosts = obj.optJSONArray("passthrough_hosts")?.let { arr ->
|
passthroughHosts = obj.optJSONArray("passthrough_hosts")?.let { arr ->
|
||||||
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
|
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
|
||||||
}?.filter { it.isNotBlank() }.orEmpty(),
|
}?.filter { it.isNotBlank() }.orEmpty(),
|
||||||
tunnelDoh = obj.optBoolean("tunnel_doh", false),
|
tunnelDoh = obj.optBoolean("tunnel_doh", true),
|
||||||
|
blockDoh = obj.optBoolean("block_doh", true),
|
||||||
youtubeViaRelay = obj.optBoolean("youtube_via_relay", false),
|
youtubeViaRelay = obj.optBoolean("youtube_via_relay", false),
|
||||||
bypassDohHosts = obj.optJSONArray("bypass_doh_hosts")?.let { arr ->
|
bypassDohHosts = obj.optJSONArray("bypass_doh_hosts")?.let { arr ->
|
||||||
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
|
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
|
||||||
|
|||||||
@@ -1265,6 +1265,51 @@ private fun AdvancedSettings(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block DoH toggle
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
"Block DoH",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Reject browser DoH — forces instant system DNS via tun2proxy. Saves ~1.5s per domain lookup.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = cfg.blockDoh,
|
||||||
|
onCheckedChange = { onChange(cfg.copy(blockDoh = it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass DoH toggle
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
"Bypass DoH",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Send browser DoH direct, not through tunnel. Faster DNS — queries are still encrypted.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = !cfg.tunnelDoh,
|
||||||
|
onCheckedChange = { onChange(cfg.copy(tunnelDoh = !it)) },
|
||||||
|
enabled = !cfg.blockDoh,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Batch coalesce step slider
|
// Batch coalesce step slider
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -269,6 +269,22 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bypass_doh_hosts: Vec<String>,
|
pub bypass_doh_hosts: Vec<String>,
|
||||||
|
|
||||||
|
/// When true, immediately reject (close) any CONNECT to a known DoH
|
||||||
|
/// endpoint. Takes priority over `tunnel_doh` — the connection is
|
||||||
|
/// never established in either direction. Browsers fall back to system
|
||||||
|
/// DNS, which tun2proxy handles via virtual DNS (instant, no tunnel
|
||||||
|
/// round-trip). This eliminates the ~1.5s per-domain DoH overhead
|
||||||
|
/// that #468's `tunnel_doh: true` default introduced.
|
||||||
|
///
|
||||||
|
/// Background: #468 changed `tunnel_doh` from false (bypass) to true
|
||||||
|
/// (tunnel) because Iranian ISPs block direct DoH endpoints. But
|
||||||
|
/// tunneling DoH costs an extra ~1.5s Apps Script round-trip per DNS
|
||||||
|
/// lookup, which made every page load noticeably slower. Blocking
|
||||||
|
/// DoH entirely avoids both problems: no ISP-visible DoH connection,
|
||||||
|
/// no tunnel round-trip — browsers use the system DNS path instead.
|
||||||
|
#[serde(default)]
|
||||||
|
pub block_doh: bool,
|
||||||
|
|
||||||
/// Multi-edge domain-fronting groups. Each group is a triple of
|
/// Multi-edge domain-fronting groups. Each group is a triple of
|
||||||
/// (edge IP, front SNI, member domains): when a CONNECT to one of
|
/// (edge IP, front SNI, member domains): when a CONNECT to one of
|
||||||
/// the member domains arrives, the proxy MITMs at the local CA
|
/// the member domains arrives, the proxy MITMs at the local CA
|
||||||
|
|||||||
@@ -246,6 +246,9 @@ pub struct RewriteCtx {
|
|||||||
/// `matches_doh_host` for matching, and config.rs `tunnel_doh` for
|
/// `matches_doh_host` for matching, and config.rs `tunnel_doh` for
|
||||||
/// the trade-off.
|
/// the trade-off.
|
||||||
pub bypass_doh: bool,
|
pub bypass_doh: bool,
|
||||||
|
/// When true, immediately reject connections to known DoH hosts.
|
||||||
|
/// Takes priority over bypass_doh.
|
||||||
|
pub block_doh: bool,
|
||||||
/// User-supplied DoH hostnames added to the built-in default list.
|
/// User-supplied DoH hostnames added to the built-in default list.
|
||||||
/// Same matching semantics as `passthrough_hosts`.
|
/// Same matching semantics as `passthrough_hosts`.
|
||||||
pub bypass_doh_hosts: Vec<String>,
|
pub bypass_doh_hosts: Vec<String>,
|
||||||
@@ -504,6 +507,7 @@ impl ProxyServer {
|
|||||||
passthrough_hosts: config.passthrough_hosts.clone(),
|
passthrough_hosts: config.passthrough_hosts.clone(),
|
||||||
block_quic: config.block_quic,
|
block_quic: config.block_quic,
|
||||||
bypass_doh: !config.tunnel_doh,
|
bypass_doh: !config.tunnel_doh,
|
||||||
|
block_doh: config.block_doh,
|
||||||
bypass_doh_hosts: config.bypass_doh_hosts.clone(),
|
bypass_doh_hosts: config.bypass_doh_hosts.clone(),
|
||||||
fronting_groups,
|
fronting_groups,
|
||||||
});
|
});
|
||||||
@@ -1581,6 +1585,18 @@ async fn dispatch_tunnel(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0.4. DoH block. Reject connections to known DoH endpoints so browsers
|
||||||
|
// fall back to system DNS (tun2proxy virtual DNS — instant).
|
||||||
|
// Takes priority over bypass_doh.
|
||||||
|
if rewrite_ctx.block_doh
|
||||||
|
&& port == 443
|
||||||
|
&& matches_doh_host(&host, &rewrite_ctx.bypass_doh_hosts)
|
||||||
|
{
|
||||||
|
tracing::info!("dispatch {}:{} -> blocked (block_doh)", host, port);
|
||||||
|
drop(sock);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// 0.5. DoH bypass. DNS-over-HTTPS is the dominant per-flow DNS cost
|
// 0.5. DoH bypass. DNS-over-HTTPS is the dominant per-flow DNS cost
|
||||||
// in Full mode (every browser name lookup costs a ~2 s Apps
|
// in Full mode (every browser name lookup costs a ~2 s Apps
|
||||||
// Script round-trip), and the tunnel adds no privacy beyond
|
// Script round-trip), and the tunnel adds no privacy beyond
|
||||||
|
|||||||
Reference in New Issue
Block a user