diff --git a/Cargo.lock b/Cargo.lock index 7d1468fc..ba5ab453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3341,6 +3341,7 @@ dependencies = [ "tun2proxy", "url", "webpki-roots 0.26.11", + "winreg 0.56.0", "x509-parser 0.16.0", "zstd", ] @@ -6955,6 +6956,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "wintun-bindings" version = "0.7.34" @@ -6968,7 +6979,7 @@ dependencies = [ "log", "thiserror 2.0.18", "windows-sys 0.61.2", - "winreg", + "winreg 0.55.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d79e879e..9d97a02f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,9 @@ eframe = { version = "0.28", default-features = false, features = [ ], optional = true } url = "2.5.8" +[target.'cfg(windows)'.dependencies] +winreg = "0.56.0" + # Unix-only deps. Must come after `[dependencies]` because starting a new # table here otherwise ends the main one — anything below it (incl. eframe) # would end up scoped to cfg(unix) and disappear on Windows builds. diff --git a/scripts/proxy_set_linux_sh b/scripts/proxy_set_linux_sh new file mode 100644 index 00000000..01d356cc --- /dev/null +++ b/scripts/proxy_set_linux_sh @@ -0,0 +1,179 @@ +#!/bin/bash + +trim() { + local -n ref=$1 + ref="${ref#"${ref%%[![:space:]]*}"}" + ref="${ref%"${ref##*[![:space:]]}"}" +} + +build_gsettings_array() { + [[ -z "$1" ]] && echo "[]" && return + local host joined hosts=() + IFS=',' read -ra parts <<< "$1" + for host in "${parts[@]}"; do + trim host + [[ -n "$host" ]] && hosts+=("$host") + done + [[ ${#hosts[@]} -eq 0 ]] && echo "[]" && return + printf -v joined "'%s'," "${hosts[@]}" + echo "[${joined%,}]" +} + +# Function to set proxy for GNOME +set_gnome_proxy() { + local MODE=$1 + local PROXY_IP=$2 + local PROXY_PORT=$3 + local SOCKS_PORT=$4 + local IGNORE_HOSTS=$5 + + # Set the proxy mode + gsettings set org.gnome.system.proxy mode "$MODE" + + if [ "$MODE" == "manual" ]; then + # List of protocols + local PROTOCOLS=("http" "https" "ftp") + + # Loop through protocols to set the proxy + for PROTOCOL in "${PROTOCOLS[@]}"; do + gsettings set org.gnome.system.proxy.$PROTOCOL host "$PROXY_IP" + gsettings set org.gnome.system.proxy.$PROTOCOL port "$PROXY_PORT" + gsettings set org.gnome.system.proxy.socks port "$SOCKS_PORT" + done + + # Set ignored hosts + gsettings set org.gnome.system.proxy ignore-hosts "$(build_gsettings_array "$IGNORE_HOSTS")" + + echo "GNOME: Manual proxy settings applied." + echo "Proxy IP: $PROXY_IP" + echo "Proxy Port: $PROXY_PORT" + echo "Ignored Hosts: $IGNORE_HOSTS" + elif [ "$MODE" == "none" ]; then + echo "GNOME: Proxy disabled." + fi +} + +# Function to set proxy for KDE +set_kde_proxy() { + local MODE=$1 + local PROXY_IP=$2 + local PROXY_PORT=$3 + local SOCKS_PORT=$4 + local IGNORE_HOSTS=$5 + + # Determine the correct kwriteconfig command based on KDE_SESSION_VERSION + if [ "$KDE_SESSION_VERSION" == "6" ]; then + KWRITECONFIG="kwriteconfig6" + else + KWRITECONFIG="kwriteconfig5" + fi + + # KDE uses kwriteconfig to modify proxy settings + if [ "$MODE" == "manual" ]; then + # Set proxy for all protocols + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ProxyType 1 + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key httpProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key httpsProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ftpProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key socksProxy "http://$PROXY_IP:$SOCKS_PORT" + + # Set ignored hosts + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key NoProxyFor "$IGNORE_HOSTS" + + echo "KDE: Manual proxy settings applied." + echo "Proxy IP: $PROXY_IP" + echo "Proxy Port: $PROXY_PORT" + echo "Ignored Hosts: $IGNORE_HOSTS" + elif [ "$MODE" == "none" ]; then + # Disable proxy + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ProxyType 0 + echo "KDE: Proxy disabled." + fi + + # Apply changes by restarting KDE's network settings + dbus-send --type=signal /KIO/Scheduler org.kde.KIO.Scheduler.reparseSlaveConfiguration string:"" +} + +# Detect the current desktop environment +detect_desktop_environment() { + if [[ "$XDG_CURRENT_DESKTOP" == *"GNOME"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"GNOME"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"XFCE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"XFCE"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"X-Cinnamon"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"cinnamon"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"UKUI"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"ukui"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"DDE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"dde"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"MATE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"mate"* ]]; then + echo "gnome" + return + fi + + local KDE_ENVIRONMENTS=("KDE" "plasma") + for ENV in "${KDE_ENVIRONMENTS[@]}"; do + if [ "$XDG_CURRENT_DESKTOP" == "$ENV" ] || [ "$XDG_SESSION_DESKTOP" == "$ENV" ]; then + echo "kde" + return + fi + done + + # Fallback to GNOME method if CLI utility is available. This solves the + # proxy configuration issues on minimal installation systems, like setups + # with only window managers, that borrow some parts from big DEs. + if command -v gsettings >/dev/null 2>&1; then + echo "gnome" + return + fi + + echo "unsupported" +} + +# Main script logic +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [proxy_ip proxy_port ignore_hosts]" + echo " mode: 'none' or 'manual'" + echo " If mode is 'manual', provide proxy IP, port, and ignore hosts." + exit 1 +fi + +# Get the mode +MODE=$1 +PROXY_IP=$2 +PROXY_PORT=$3 +IGNORE_HOSTS=$4 + +if ! [[ "$MODE" =~ ^(manual|none)$ ]]; then + echo "Invalid mode. Use 'none' or 'manual'." >&2 + exit 1 +fi + +# Detect desktop environment +DE=$(detect_desktop_environment) + +# Apply settings based on the desktop environment +if [ "$DE" == "gnome" ]; then + set_gnome_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" +elif [ "$DE" == "kde" ]; then + set_gnome_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" + set_kde_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" +else + echo "Unsupported desktop environment: $DE" >&2 + exit 1 +fi diff --git a/scripts/proxy_set_osx_sh b/scripts/proxy_set_osx_sh new file mode 100644 index 00000000..925e3838 --- /dev/null +++ b/scripts/proxy_set_osx_sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Function to set proxy +set_proxy() { + PROXY_IP=$1 + PROXY_PORT=$2 + SOCKS_PORT=$3 + + shift 2 + BYPASS_DOMAINS=("$@") + # If no bypass domains are provided, set it to empty by default + if [ ${#BYPASS_DOMAINS[@]} -eq 0 ]; then + BYPASS_DOMAINS=("") + fi + + # Get all network service names + SERVICES=$(networksetup -listallnetworkservices | grep -v '*') + + # Loop through each network service + echo "$SERVICES" | while read -r SERVICE; do + echo "Setting proxy for network service '$SERVICE'..." + # Set HTTP proxy + networksetup -setwebproxy "$SERVICE" "$PROXY_IP" "$PROXY_PORT" + + # Set HTTPS proxy + networksetup -setsecurewebproxy "$SERVICE" "$PROXY_IP" "$PROXY_PORT" + + # Set SOCKS proxy + networksetup -setsocksfirewallproxy "$SERVICE" "$PROXY_IP" "$SOCKS_PORT" + + # Set bypass domains + networksetup -setproxybypassdomains "$SERVICE" "${BYPASS_DOMAINS[@]}" + echo "Proxy for network service '$SERVICE' has been set to $PROXY_IP:$PROXY_PORT" + done + echo "Proxy settings for all network services are complete!" +} + +# Function to disable proxy +clear_proxy() { + # Get all network service names + SERVICES=$(networksetup -listallnetworkservices | grep -v '*') + + # Loop through each network service + echo "$SERVICES" | while read -r SERVICE; do + echo "Disabling proxy and clearing bypass domains for network service '$SERVICE'..." + # Disable HTTP proxy + networksetup -setwebproxystate "$SERVICE" off + + # Disable HTTPS proxy + networksetup -setsecurewebproxystate "$SERVICE" off + + # Disable SOCKS proxy + networksetup -setsocksfirewallproxystate "$SERVICE" off + + echo "Proxy for network service '$SERVICE' has been disabled" + done + echo "Proxy for all network services has been disabled!" +} + +# Main script logic +if [ "$1" == "set" ]; then + # Check if enough parameters are passed for setting proxy + if [ "$#" -lt 3 ]; then + echo "Usage: $0 set [Bypass Domain 1 Bypass Domain 2 ...]" + exit 1 + fi + set_proxy "$2" "$3" "${@:4}" +elif [ "$1" == "clear" ]; then + clear_proxy +else + echo "Usage:" + echo " To set proxy: $0 set [Bypass Domain 1 Bypass Domain 2 ...]" + echo " To clear proxy: $0 clear" + exit 1 +fi diff --git a/src/bin/ui.rs b/src/bin/ui.rs index 7bf7c800..36eabea1 100644 --- a/src/bin/ui.rs +++ b/src/bin/ui.rs @@ -16,6 +16,7 @@ use mhrv_rs::domain_fronter::{DomainFronter, DEFAULT_GOOGLE_SNI_POOL}; use mhrv_rs::lan_utils::{detect_lan_ip, is_share_on_lan}; use mhrv_rs::mitm::{MitmCertManager, CA_CERT_FILE}; use mhrv_rs::proxy_server::ProxyServer; +use mhrv_rs::system_proxy::{disable_system_proxy, enable_system_proxy}; use mhrv_rs::{scan_ips, scan_sni, test_cmd}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -159,6 +160,7 @@ struct UiState { /// TODO(quota-dashboard): feed this into a dedicated QuotaWidget once /// the UI is remodeled. quota: Option, + system_proxy: bool, } #[derive(Clone, Debug)] @@ -210,6 +212,8 @@ enum Cmd { url: String, name: String, }, + EnableSystemProxy(Config), + DisableSystemProxy, } struct App { @@ -738,11 +742,21 @@ struct ConfigWire<'a> { 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_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() @@ -1336,7 +1350,7 @@ impl eframe::App for App { ui.add_space(8.0); // ── Status + stats card ──────────────────────────────────────── - let (running, started_at, stats, ca_trusted, last_test_msg, per_site, quota_state) = { + let (running, started_at, stats, ca_trusted, last_test_msg, per_site, quota_state, system_proxy) = { let s = self.shared.state.lock().unwrap(); ( s.running, @@ -1346,6 +1360,7 @@ impl eframe::App for App { s.last_test_msg.clone(), s.last_per_site.clone(), s.quota.clone(), + s.system_proxy, ) }; @@ -1692,6 +1707,35 @@ impl eframe::App for App { } } } + + if !system_proxy { + let btn = egui::Button::new( + egui::RichText::new("Enable System Proxy").color(egui::Color32::WHITE).strong(), + ) + .fill(OK_GREEN) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(4.0); + if ui.add(btn).clicked() { + match self.form.to_config() { + Ok(cfg) => { + let _ = self.cmd_tx.send(Cmd::EnableSystemProxy(cfg)); + } + Err(e) => { + self.toast = Some((format!("Cannot start: {}", e), Instant::now())); + } + } + } + } else { + let btn = egui::Button::new( + egui::RichText::new("Disable System Proxy").color(egui::Color32::WHITE).strong(), + ) + .fill(ERR_RED) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(4.0); + if ui.add(btn).clicked() { + let _ = self.cmd_tx.send(Cmd::DisableSystemProxy); + } + } }); // Secondary actions — smaller, grouped together on their own line. @@ -1999,6 +2043,10 @@ impl eframe::App for App { }); // end ScrollArea }); } + + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + disable_system_proxy().ok(); + } } impl App { @@ -2191,8 +2239,7 @@ impl App { let custom_label = ui.add_sized( [0.0, 0.0], egui::Label::new( - egui::RichText::new("Custom SNI") - .color(egui::Color32::TRANSPARENT), + egui::RichText::new("Custom SNI").color(egui::Color32::TRANSPARENT), ), ); ui.add( @@ -2413,14 +2460,14 @@ fn background_thread(shared: Arc, rx: Receiver) { https://whatismyipaddress.com in your browser \ via 127.0.0.1:8085. The IP shown should be your \ tunnel-node's VPS IP. Tracking a real Full-mode \ - test in #160." + test in #160.", ), Some(mhrv_rs::config::Mode::Direct) => Some( "Test Relay is wired only for apps_script mode. \ In direct mode there is no Apps Script relay — \ every request goes through the SNI-rewrite tunnel \ straight to Google's edge. Verify by loading \ - https://www.google.com via the proxy." + https://www.google.com via the proxy.", ), _ => None, }; @@ -2675,6 +2722,49 @@ fn background_thread(shared: Arc, rx: Receiver) { } }); } + + Ok(Cmd::EnableSystemProxy(cfg)) => { + push_log(&shared, "[ui] setting system proxy..."); + + shared.state.lock().unwrap().system_proxy = true; + + let res = enable_system_proxy(&cfg); + match res { + Ok(()) => { + push_log( + &shared, + &format!( + "[ui] system proxy set to {}:{}", + cfg.listen_host, cfg.listen_port + ), + ); + } + Err(e) => { + push_log( + &shared, + &format!("[ui] failed to enable system proxy: {}", e), + ); + } + } + } + + Ok(Cmd::DisableSystemProxy) => { + shared.state.lock().unwrap().system_proxy = false; + + let res = disable_system_proxy(); + match res { + Ok(()) => { + push_log(&shared, "[ui] system proxy disabled"); + } + Err(e) => { + push_log( + &shared, + &format!("[ui] failed to disable system proxy: {}", e), + ); + } + } + } + Err(_) => {} } @@ -2793,10 +2883,7 @@ fn install_ui_tracing(shared: Arc, config_level: &str) { /// by `install_ui_tracing`. `apply_log_level` uses it to swap in a new /// filter when the user clicks Save with a different log level (#401). static LOG_RELOAD: std::sync::OnceLock< - tracing_subscriber::reload::Handle< - tracing_subscriber::EnvFilter, - tracing_subscriber::Registry, - >, + tracing_subscriber::reload::Handle, > = std::sync::OnceLock::new(); /// Reinstall the tracing filter at runtime. Called from the Save handler diff --git a/src/lib.rs b/src/lib.rs index 900e2966..fec02b5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,15 +6,16 @@ pub mod config; pub mod data_dir; pub mod domain_fronter; pub mod lan_utils; +pub mod logging; pub mod mitm; pub mod proxy_server; pub mod quota_tracker; pub mod rlimit; -pub mod tunnel_client; pub mod scan_ips; pub mod scan_sni; +pub mod system_proxy; pub mod test_cmd; -pub mod logging; +pub mod tunnel_client; pub mod update_check; pub use quota_tracker::QuotaSummary; diff --git a/src/system_proxy.rs b/src/system_proxy.rs new file mode 100644 index 00000000..9311f326 --- /dev/null +++ b/src/system_proxy.rs @@ -0,0 +1,129 @@ +use std::fs; +use std::io::{Error, ErrorKind}; +use std::process::Command; + +use crate::config::Config; + +const LINUX_SCRIPT: &str = include_str!("../scripts/proxy_set_linux_sh"); +const MACOS_SCRIPT: &str = include_str!("../scripts/proxy_set_osx_sh"); + +#[cfg(all(not(target_os = "android"), not(target_os = "ios")))] +pub fn enable_system_proxy(cfg: &Config) -> Result<(), Error> { + #[cfg(target_os = "windows")] + { + let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); + let internet_settings = hkcu.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", + winreg::enums::KEY_SET_VALUE, + ); + match internet_settings { + Ok(key) => { + key.set_value("ProxyEnable", &1u32)?; + let proxy_server = format!("{}:{}", cfg.listen_host, cfg.listen_port); + key.set_value("ProxyServer", &proxy_server)?; + + Ok(()) + } + Err(e) => Err(e), + } + } + + #[cfg(target_os = "macos")] + { + let socks = cfg.socks5_port.unwrap_or(cfg.listen_port); + run_script( + MACOS_SCRIPT, + &[ + "set", + &cfg.listen_host, + &cfg.listen_port.to_string(), + &socks.to_string(), + ], + ) + .map_err(|e| { + Error::new( + ErrorKind::Other, + format!("Failed to enable system proxy: {}", e), + ) + }) + } + + #[cfg(all(unix, not(target_os = "macos")))] + { + let socks = cfg.socks5_port.unwrap_or(cfg.listen_port); + run_script( + LINUX_SCRIPT, + &[ + "manual", + &cfg.listen_host, + &cfg.listen_port.to_string(), + &socks.to_string(), + ], + ) + .map_err(|e| { + Error::new( + ErrorKind::Other, + format!("Failed to enable system proxy: {}", e), + ) + }) + } +} + +#[cfg(all(not(target_os = "android"), not(target_os = "ios")))] +pub fn disable_system_proxy() -> Result<(), Error> { + #[cfg(target_os = "windows")] + { + let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); + let internet_settings = hkcu.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", + winreg::enums::KEY_SET_VALUE, + ); + match internet_settings { + Ok(key) => { + key.set_value("ProxyEnable", &0u32)?; + Ok(()) + } + Err(e) => Err(e), + } + } + + #[cfg(target_os = "macos")] + { + run_script(MACOS_SCRIPT, &["clear"]).map_err(|e| { + Error::new( + ErrorKind::Other, + format!("Failed to disable system proxy: {}", e), + ) + }) + } + + #[cfg(all(unix, not(target_os = "macos")))] + { + run_script(LINUX_SCRIPT, &["none"]).map_err(|e| { + Error::new( + ErrorKind::Other, + format!("Failed to enable system proxy: {}", e), + ) + }) + } +} + +#[cfg(all(not(target_os = "android"), not(target_os = "ios")))] +fn run_script(script_content: &str, args: &[&str]) -> std::io::Result<()> { + let script_path = std::env::temp_dir().join("proxy_script.sh"); + fs::write(&script_path, script_content)?; + + let output = Command::new("/bin/bash") + .arg(&script_path) + .args(args) + .output()?; + + if !output.status.success() { + eprintln!("Script error: {}", String::from_utf8_lossy(&output.stderr)); + let _ = fs::remove_file(script_path); + return Err(Error::new(ErrorKind::Other, "Failed to run proxy script")); + } + + let _ = fs::remove_file(script_path); + Ok(()) +}