Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ data class MhrvConfig(
* per name lookup with no real privacy gain. Set this to true to
* 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
Expand All @@ -127,6 +127,13 @@ data class MhrvConfig(
*/
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). */
val connectionMode: ConnectionMode = ConnectionMode.VPN_TUN,

Expand Down Expand Up @@ -218,7 +225,8 @@ data class MhrvConfig(
if (passthroughHosts.isNotEmpty()) {
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)
// Trim/drop-empty/dedupe before serializing — symmetric with the
// read-side normalization in loadFromJson(), so a user typing
Expand Down Expand Up @@ -325,6 +333,7 @@ object ConfigStore {
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)
if (cfg.blockDoh != defaults.blockDoh) obj.put("block_doh", cfg.blockDoh)
if (cfg.youtubeViaRelay != defaults.youtubeViaRelay) obj.put("youtube_via_relay", cfg.youtubeViaRelay)
val cleanBypassDohHosts = cfg.bypassDohHosts
.map { it.trim() }
Expand Down Expand Up @@ -428,7 +437,8 @@ object ConfigStore {
passthroughHosts = obj.optJSONArray("passthrough_hosts")?.let { arr ->
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
}?.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),
bypassDohHosts = obj.optJSONArray("bypass_doh_hosts")?.let { arr ->
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
Expand Down
45 changes: 45 additions & 0 deletions android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Column {
Text(
Expand Down
16 changes: 16 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,22 @@ pub struct Config {
#[serde(default)]
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
/// (edge IP, front SNI, member domains): when a CONNECT to one of
/// the member domains arrives, the proxy MITMs at the local CA
Expand Down
16 changes: 16 additions & 0 deletions src/proxy_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ pub struct RewriteCtx {
/// `matches_doh_host` for matching, and config.rs `tunnel_doh` for
/// the trade-off.
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.
/// Same matching semantics as `passthrough_hosts`.
pub bypass_doh_hosts: Vec<String>,
Expand Down Expand Up @@ -504,6 +507,7 @@ impl ProxyServer {
passthrough_hosts: config.passthrough_hosts.clone(),
block_quic: config.block_quic,
bypass_doh: !config.tunnel_doh,
block_doh: config.block_doh,
bypass_doh_hosts: config.bypass_doh_hosts.clone(),
fronting_groups,
});
Expand Down Expand Up @@ -1581,6 +1585,18 @@ async fn dispatch_tunnel(
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
// in Full mode (every browser name lookup costs a ~2 s Apps
// Script round-trip), and the tunnel adds no privacy beyond
Expand Down
Loading