Skip to content
Open
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
43 changes: 43 additions & 0 deletions android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ data class MhrvConfig(
*/
val youtubeViaRelay: Boolean = false,

/**
* SABR quality-track strip kill-switch (Rust `sabr_strip`).
* Default true. See `src/config.rs` `sabr_strip` for the trade-off
* and when to flip — Android-side is just round-trip plumbing.
*/
val sabrStrip: Boolean = true,

/**
* Path-pinned relay routing (Rust `relay_url_patterns`).
* See `src/config.rs` `relay_url_patterns` for the full semantics —
* suppression gates, default pattern, host-overlap rules. This
* Android-side field is for *additional* user entries only,
* round-tripped so a hand-edited list survives Save.
*/
val relayUrlPatterns: List<String> = emptyList(),

/** UI language toggle. Non-Rust; honoured only by the Android wrapper. */
val uiLang: UiLang = UiLang.AUTO,
) {
Expand Down Expand Up @@ -240,6 +256,21 @@ data class MhrvConfig(
put("tunnel_doh", tunnelDoh)
put("block_doh", blockDoh)
if (youtubeViaRelay) put("youtube_via_relay", true)
// sabr_strip default is true on the Rust side; emit only
// when the user has explicitly disabled it so unchanged
// configs stay clean. #977 kill-switch.
if (!sabrStrip) put("sabr_strip", false)
// Trim/drop-empty/dedupe before serializing — same pattern
// as bypass_doh_hosts. Skip the key entirely when the user
// hasn't added any extras so we don't leak an empty array
// into otherwise-clean configs.
val cleanRelayUrlPatterns = relayUrlPatterns
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
if (cleanRelayUrlPatterns.isNotEmpty()) {
put("relay_url_patterns", JSONArray().apply { cleanRelayUrlPatterns.forEach { put(it) } })
}
// Trim/drop-empty/dedupe before serializing — symmetric with the
// read-side normalization in loadFromJson(), so a user typing
// " doh.foo " or accidentally adding a duplicate doesn't end up
Expand Down Expand Up @@ -349,13 +380,21 @@ object ConfigStore {
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.sabrStrip != defaults.sabrStrip) obj.put("sabr_strip", cfg.sabrStrip)
val cleanBypassDohHosts = cfg.bypassDohHosts
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
if (cleanBypassDohHosts.isNotEmpty()) {
obj.put("bypass_doh_hosts", JSONArray().apply { cleanBypassDohHosts.forEach { put(it) } })
}
val cleanRelayUrlPatterns = cfg.relayUrlPatterns
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
if (cleanRelayUrlPatterns.isNotEmpty()) {
obj.put("relay_url_patterns", JSONArray().apply { cleanRelayUrlPatterns.forEach { put(it) } })
}

// Compress with DEFLATE then base64.
val jsonBytes = obj.toString().toByteArray(Charsets.UTF_8)
Expand Down Expand Up @@ -456,9 +495,13 @@ object ConfigStore {
tunnelDoh = obj.optBoolean("tunnel_doh", true),
blockDoh = obj.optBoolean("block_doh", true),
youtubeViaRelay = obj.optBoolean("youtube_via_relay", false),
sabrStrip = obj.optBoolean("sabr_strip", true),
bypassDohHosts = obj.optJSONArray("bypass_doh_hosts")?.let { arr ->
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
}?.filter { it.isNotBlank() }.orEmpty(),
relayUrlPatterns = obj.optJSONArray("relay_url_patterns")?.let { arr ->
buildList { for (i in 0 until arr.length()) add(arr.optString(i)) }
}?.filter { it.isNotBlank() }.orEmpty(),
connectionMode = when (obj.optString("connection_mode", "vpn_tun")) {
"proxy_only" -> ConnectionMode.PROXY_ONLY
else -> ConnectionMode.VPN_TUN
Expand Down
18 changes: 17 additions & 1 deletion android/app/src/main/java/com/therealaleph/mhrv/Native.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,23 @@ object Native {
* points scope as h2_calls. Compute h2 health as
* h2_calls / (h2_calls + h2_fallbacks)),
* h2_disabled (boolean: true when h2 fast path is permanently
* off — config force_http1 set, or peer refused h2 via ALPN)
* off — config force_http1 set, or peer refused h2 via ALPN),
* forwarder_calls (successful upstream fetches via the
* SNI-rewrite forwarder — fast path for non-/youtubei/
* paths on `force_mitm_hosts`. Counted at upstream-success,
* before the downstream write to the browser, so a client
* disconnect mid-write still counts. Zero in non-AppsScript
* modes / when no `relay_url_patterns` host is in play),
* forwarder_bytes (response bytes successfully fetched by the
* forwarder; same upstream-fetch-success semantic as
* forwarder_calls),
* forwarder_errors (forwarder dispatch errors — connect failure,
* TLS error, read timeout, response cap exceeded. Distinct
* from relay_failures: this counts fast-path-only misses
* regardless of whether the relay-fallback then recovered the
* request. Combine with relay_failures to distinguish "fast
* path missed but request served" from "request failed
* end-to-end")
*
* Cheap — just reads atomics. Safe to poll on a second-scale timer.
*/
Expand Down
42 changes: 41 additions & 1 deletion src/bin/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ struct FormState {
google_ip_validation: bool,
normalize_x_graphql: bool,
youtube_via_relay: bool,
/// See `config::Config::relay_url_patterns` for semantics + defaults.
/// No UI control; round-tripped so a hand-edited list survives Save.
relay_url_patterns: Vec<String>,
/// See `config::Config::sabr_strip` for trade-off + when to flip.
/// No UI control; round-tripped so a hand-edited `false` survives Save.
sabr_strip: bool,
passthrough_hosts: Vec<String>,
/// Round-tripped from config.json so the UI's save path doesn't
/// drop the user's setting. Not currently exposed as a UI control;
Expand Down Expand Up @@ -385,6 +391,8 @@ fn load_form() -> (FormState, Option<String>) {
scan_batch_size: c.scan_batch_size,
normalize_x_graphql: c.normalize_x_graphql,
youtube_via_relay: c.youtube_via_relay,
relay_url_patterns: c.relay_url_patterns.clone(),
sabr_strip: c.sabr_strip,
passthrough_hosts: c.passthrough_hosts.clone(),
block_quic: c.block_quic,
disable_padding: c.disable_padding,
Expand Down Expand Up @@ -424,6 +432,8 @@ fn load_form() -> (FormState, Option<String>) {
scan_batch_size: 500,
normalize_x_graphql: false,
youtube_via_relay: false,
relay_url_patterns: Vec::new(),
sabr_strip: true,
passthrough_hosts: Vec::new(),
block_quic: true,
disable_padding: false,
Expand Down Expand Up @@ -580,6 +590,10 @@ impl FormState {
// config-only flag for now. Passed through from the loaded
// config if set, otherwise defaults to false.
youtube_via_relay: self.youtube_via_relay,
// Config-only round-trips. Source of truth for both fields
// is `config::Config` (defaults, gating, trade-offs).
relay_url_patterns: self.relay_url_patterns.clone(),
sabr_strip: self.sabr_strip,
// Similarly config-only for now; round-trips through the
// file so the UI doesn't drop the user's entries on save.
passthrough_hosts: self.passthrough_hosts.clone(),
Expand Down Expand Up @@ -666,6 +680,14 @@ struct ConfigWire<'a> {
normalize_x_graphql: bool,
#[serde(skip_serializing_if = "is_false")]
youtube_via_relay: bool,
/// See `config::Config::relay_url_patterns`. Skipped when empty so
/// the proxy-applied default isn't echoed into config.json.
#[serde(skip_serializing_if = "Vec::is_empty")]
relay_url_patterns: &'a Vec<String>,
/// See `config::Config::sabr_strip`. Default `true`; emitted only
/// when explicitly disabled so unchanged configs stay clean.
#[serde(skip_serializing_if = "is_true")]
sabr_strip: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
passthrough_hosts: &'a Vec<String>,
// IP-scan knobs. These used to be missing from the wire struct, so
Expand Down Expand Up @@ -773,6 +795,8 @@ impl<'a> From<&'a Config> for ConfigWire<'a> {
.map(|v| v.iter().map(String::as_str).collect()),
normalize_x_graphql: c.normalize_x_graphql,
youtube_via_relay: c.youtube_via_relay,
relay_url_patterns: &c.relay_url_patterns,
sabr_strip: c.sabr_strip,
passthrough_hosts: &c.passthrough_hosts,
fetch_ips_from_api: c.fetch_ips_from_api,
max_ips_to_scan: c.max_ips_to_scan,
Expand Down Expand Up @@ -1314,7 +1338,7 @@ impl eframe::App for App {
if let Some(s) = &stats {
// Compact two-column layout so 7 metrics fit in ~4 rows
// instead of a tall vertical strip.
let rows: Vec<(&str, String)> = vec![
let mut rows: Vec<(&str, String)> = vec![
("relay calls", s.relay_calls.to_string()),
("failures", s.relay_failures.to_string()),
("coalesced", s.coalesced.to_string()),
Expand All @@ -1338,6 +1362,22 @@ impl eframe::App for App {
),
),
];
// Forwarder rows only appear once the path filter
// has fired at least once — otherwise the typical
// (no-pattern-hit / non-AppsScript) user sees an
// empty pair of "0" rows that adds noise without
// signal. `err` is fast-path-miss count; combine
// with `relay_failures` to gauge end-to-end health.
if s.forwarder_calls + s.forwarder_errors > 0 {
rows.push((
"fwd calls",
format!(
"{} (err {})",
s.forwarder_calls, s.forwarder_errors
),
));
rows.push(("fwd bytes", fmt_bytes(s.forwarder_bytes)));
}
egui::Grid::new("stats")
.num_columns(4)
.spacing([16.0, 4.0])
Expand Down
Loading
Loading