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
1 change: 1 addition & 0 deletions src/android_jni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ fn install_logging_once() {
.with_target(false)
.with_ansi(false)
.with_writer(LogcatWriter)
.with_timer(crate::logging::CompactUtcTime)
.try_init();

let _ = rustls::crypto::ring::default_provider().install_default();
Expand Down
167 changes: 166 additions & 1 deletion src/bin/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ struct FormState {
auto_blacklist_window_secs: u64,
auto_blacklist_cooldown_secs: u64,
request_timeout_secs: u64,
stream_timeout_secs: u64,
/// Optional second-hop exit node for CF-anti-bot bypass (chatgpt.com /
/// claude.ai / grok.com / x.com). Config-only — no UI editor yet.
/// See `assets/exit_node/` for the generic exit-node handler.
Expand Down Expand Up @@ -391,6 +392,7 @@ fn load_form() -> (FormState, Option<String>) {
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
request_timeout_secs: c.request_timeout_secs,
stream_timeout_secs: c.stream_timeout_secs,
exit_node: c.exit_node.clone(),
}
} else {
Expand Down Expand Up @@ -433,6 +435,7 @@ fn load_form() -> (FormState, Option<String>) {
auto_blacklist_window_secs: 30,
auto_blacklist_cooldown_secs: 120,
request_timeout_secs: 30,
stream_timeout_secs: 300,
exit_node: mhrv_rs::config::ExitNodeConfig::default(),
}
};
Expand Down Expand Up @@ -618,6 +621,7 @@ impl FormState {
auto_blacklist_window_secs: self.auto_blacklist_window_secs,
auto_blacklist_cooldown_secs: self.auto_blacklist_cooldown_secs,
request_timeout_secs: self.request_timeout_secs,
stream_timeout_secs: self.stream_timeout_secs,
// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai
// / grok.com / x.com). Round-trip through FormState — config-only
// editing for now, UI editor planned for v1.9.x desktop UI batch.
Expand All @@ -637,6 +641,166 @@ fn save_config(cfg: &Config) -> Result<PathBuf, String> {
Ok(path)
}

#[derive(serde::Serialize)]
struct ConfigWire<'a> {
mode: &'a str,
google_ip: &'a str,
front_domain: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
script_id: Option<ScriptIdWire<'a>>,
auth_key: &'a str,
listen_host: &'a str,
listen_port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
socks5_port: Option<u16>,
log_level: &'a str,
verify_ssl: bool,
#[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
hosts: &'a std::collections::HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
upstream_socks5: Option<&'a str>,
#[serde(skip_serializing_if = "is_zero_u8")]
parallel_relay: u8,
#[serde(skip_serializing_if = "Option::is_none")]
sni_hosts: Option<Vec<&'a str>>,
#[serde(skip_serializing_if = "is_false")]
normalize_x_graphql: bool,
#[serde(skip_serializing_if = "is_false")]
youtube_via_relay: 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
// every Save-config silently dropped them — the user would toggle
// "fetch from API" on, save, reopen, and find it off again. Add
// them here and keep them in sync if Config ever grows more.
#[serde(skip_serializing_if = "is_false")]
fetch_ips_from_api: bool,
max_ips_to_scan: usize,
scan_batch_size: usize,
google_ip_validation: bool,
/// Default false (= bypass DoH). Only emitted when explicitly true
/// so unchanged configs stay clean.
#[serde(skip_serializing_if = "is_false")]
tunnel_doh: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
bypass_doh_hosts: &'a Vec<String>,
/// PR #763: default true (= browser DoH rejected, system DNS used).
/// Skip when matching default to keep unchanged configs clean —
/// emit only when the user has explicitly disabled the block.
#[serde(skip_serializing_if = "is_true")]
block_doh: bool,
/// Default false. Emit only when the user enables STUN/TURN blocking.
#[serde(skip_serializing_if = "is_false")]
block_stun: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
fronting_groups: &'a Vec<FrontingGroup>,
/// Auto-blacklist tuning + batch timeout (#391, #444, #430). Skip
/// serialization when matching the historical defaults so unchanged
/// configs stay clean — only emitted when the user has explicitly
/// tuned them.
#[serde(skip_serializing_if = "is_default_strikes")]
auto_blacklist_strikes: u32,
#[serde(skip_serializing_if = "is_default_window_secs")]
auto_blacklist_window_secs: u64,
#[serde(skip_serializing_if = "is_default_cooldown_secs")]
auto_blacklist_cooldown_secs: u64,
#[serde(skip_serializing_if = "is_default_timeout_secs")]
request_timeout_secs: u64,
#[serde(skip_serializing_if = "is_default_stream_timeout_secs")]
stream_timeout_secs: u64,
/// HTTP/2 multiplexing kill switch. Default false (h2 active); only
/// emitted on save when the user has explicitly disabled h2, so
/// unchanged configs stay clean.
#[serde(skip_serializing_if = "is_false")]
force_http1: bool,
/// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai /
/// grok.com / x.com via exit-node second-hop relay). Skip when fully
/// default (disabled with no URL/PSK/hosts) so configs without
/// exit-node setup stay clean. Round-tripped through FormState so
/// Save preserves user-edited values.
#[serde(skip_serializing_if = "is_default_exit_node")]
exit_node: &'a mhrv_rs::config::ExitNodeConfig,
}

fn is_default_strikes(v: &u32) -> bool { *v == 3 }
fn is_default_window_secs(v: &u64) -> bool { *v == 30 }
fn is_default_cooldown_secs(v: &u64) -> bool { *v == 120 }
fn is_default_timeout_secs(v: &u64) -> bool { *v == 30 }
fn is_default_stream_timeout_secs(v: &u64) -> bool { *v == 300 }
fn is_default_exit_node(en: &&mhrv_rs::config::ExitNodeConfig) -> bool {
!en.enabled
&& en.relay_url.is_empty()
&& en.psk.is_empty()
&& en.hosts.is_empty()
&& (en.mode.is_empty() || en.mode == "selective")
}

fn is_false(b: &bool) -> bool {
!*b
}

fn is_true(b: &bool) -> bool {
*b
}

fn is_zero_u8(v: &u8) -> bool {
*v == 0
}

#[derive(serde::Serialize)]
#[serde(untagged)]
enum ScriptIdWire<'a> {
One(&'a str),
Many(Vec<&'a str>),
}

impl<'a> From<&'a Config> for ConfigWire<'a> {
fn from(c: &'a Config) -> Self {
let script_id = c.script_id.as_ref().map(|s| match s {
ScriptId::One(v) => ScriptIdWire::One(v.as_str()),
ScriptId::Many(v) => ScriptIdWire::Many(v.iter().map(String::as_str).collect()),
});
ConfigWire {
mode: c.mode.as_str(),
google_ip: c.google_ip.as_str(),
front_domain: c.front_domain.as_str(),
script_id,
auth_key: c.auth_key.as_str(),
listen_host: c.listen_host.as_str(),
listen_port: c.listen_port,
socks5_port: c.socks5_port,
log_level: c.log_level.as_str(),
verify_ssl: c.verify_ssl,
hosts: &c.hosts,
upstream_socks5: c.upstream_socks5.as_deref(),
parallel_relay: c.parallel_relay,
sni_hosts: c
.sni_hosts
.as_ref()
.map(|v| v.iter().map(String::as_str).collect()),
normalize_x_graphql: c.normalize_x_graphql,
youtube_via_relay: c.youtube_via_relay,
passthrough_hosts: &c.passthrough_hosts,
fetch_ips_from_api: c.fetch_ips_from_api,
max_ips_to_scan: c.max_ips_to_scan,
scan_batch_size: c.scan_batch_size,
google_ip_validation: c.google_ip_validation,
tunnel_doh: c.tunnel_doh,
bypass_doh_hosts: &c.bypass_doh_hosts,
block_doh: c.block_doh,
block_stun: c.block_stun,
fronting_groups: &c.fronting_groups,
auto_blacklist_strikes: c.auto_blacklist_strikes,
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
request_timeout_secs: c.request_timeout_secs,
stream_timeout_secs: c.stream_timeout_secs,
force_http1: c.force_http1,
exit_node: &c.exit_node,
}
}
}

/// Accent color — same blue used throughout the UI for primary actions.
const ACCENT: egui::Color32 = egui::Color32::from_rgb(70, 120, 180);
const ACCENT_HOVER: egui::Color32 = egui::Color32::from_rgb(90, 145, 205);
Expand Down Expand Up @@ -2480,7 +2644,8 @@ fn install_ui_tracing(shared: Arc<Shared>, config_level: &str) {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_ansi(false)
.with_writer(writer);
.with_writer(writer)
.with_timer(mhrv_rs::logging::CompactUtcTime);

let _ = tracing_subscriber::registry()
.with(filter_layer)
Expand Down
21 changes: 21 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,22 @@ pub struct Config {
/// retry sooner when a deployment hangs. Floor `5`, ceiling `300`
/// (anything beyond exceeds Apps Script's hard 6-min cap with
/// no benefit).
///
/// This applies to connection establishment and response header
/// arrival only. Body streaming is governed by `stream_timeout_secs`.
#[serde(default = "default_request_timeout_secs")]
pub request_timeout_secs: u64,

/// Per-chunk body streaming idle timeout (seconds). Default `300`.
/// Applies to each individual body chunk read after headers arrive —
/// a chunk that goes silent for longer than this is considered a
/// stalled connection and the request is aborted. Distinct from
/// `request_timeout_secs` so large responses through Apps Script
/// (where each 256 KB range chunk can take 30-90s) are not killed
/// mid-transfer. Floor `10`, ceiling `3600`.
#[serde(default = "default_stream_timeout_secs")]
pub stream_timeout_secs: u64,

/// Optional second-hop exit node, for sites that block traffic
/// from Google datacenter IPs (Apps Script's outbound IP space).
/// Most visibly: Cloudflare-fronted services that flag the GCP IP
Expand Down Expand Up @@ -531,6 +544,10 @@ fn default_auto_blacklist_cooldown_secs() -> u64 { 120 }
/// hard-coded `BATCH_TIMEOUT` and Apps Script's typical response cliff.
fn default_request_timeout_secs() -> u64 { 30 }

/// Default for `stream_timeout_secs`: 300s per-chunk idle timeout for
/// body streaming, separate from the header/connect timeout.
fn default_stream_timeout_secs() -> u64 { 300 }

fn default_google_ip() -> String {
"216.239.38.120".into()
}
Expand Down Expand Up @@ -766,6 +783,8 @@ pub struct TomlRelay {
pub auto_blacklist_cooldown_secs: u64,
#[serde(default = "default_request_timeout_secs")]
pub request_timeout_secs: u64,
#[serde(default = "default_stream_timeout_secs")]
pub stream_timeout_secs: u64,
}

/// [network] section of config.toml.
Expand Down Expand Up @@ -919,6 +938,7 @@ impl From<TomlConfig> for Config {
auto_blacklist_window_secs: t.relay.auto_blacklist_window_secs,
auto_blacklist_cooldown_secs: t.relay.auto_blacklist_cooldown_secs,
request_timeout_secs: t.relay.request_timeout_secs,
stream_timeout_secs: t.relay.stream_timeout_secs,
exit_node: t.exit_node,
}
}
Expand Down Expand Up @@ -946,6 +966,7 @@ impl From<&Config> for TomlConfig {
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
request_timeout_secs: c.request_timeout_secs,
stream_timeout_secs: c.stream_timeout_secs,
},
network: TomlNetwork {
google_ip: c.google_ip.clone(),
Expand Down
Loading
Loading