diff --git a/ostp-client/src/bin/test_route.rs b/ostp-client/src/bin/test_route.rs new file mode 100644 index 0000000..8dcc5c1 --- /dev/null +++ b/ostp-client/src/bin/test_route.rs @@ -0,0 +1,4 @@ +fn main() { + let route = ostp_tun::windows::windows_route::sys::get_default_ipv4_route(); + println!("Default IPv4 route: {:?}", route); +} diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index ad605dd..44a2055 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -239,7 +239,13 @@ pub async fn run_client_core( } let _sysproxy_guard = if config.mode == "proxy" { - Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr)) + // Enable system proxy and set initial ProxyOverride with user exclusions + let guard = Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr)); + crate::sysproxy::update_proxy_bypass_list( + &config.exclusions.domains, + &config.exclusions.ips, + ); + guard } else { None }; @@ -355,6 +361,12 @@ pub async fn run_client_core( } => { if let Some(ref rx) = config_rx { let new_cfg = rx.borrow().clone(); + // Update Windows ProxyOverride so excluded domains/IPs + // bypass the system proxy immediately (proxy mode only). + crate::sysproxy::update_proxy_bypass_list( + &new_cfg.exclusions.domains, + &new_cfg.exclusions.ips, + ); let _ = reload_tx.send(new_cfg.exclusions); } } diff --git a/ostp-client/src/sysproxy.rs b/ostp-client/src/sysproxy.rs index 6de0634..c08f38a 100644 --- a/ostp-client/src/sysproxy.rs +++ b/ostp-client/src/sysproxy.rs @@ -64,7 +64,79 @@ pub fn enable_windows_proxy(proxy_addr: &str) { _ => {} } - // Set bypass list to prevent proxy loop for localhost traffic + // Set initial bypass list (will be expanded by update_proxy_bypass_list) + update_proxy_bypass_list_windows(&[], &[]); + + refresh_wininet(); + tracing::info!("System proxy enabled successfully"); +} + +/// Update the Windows ProxyOverride registry value to include user-configured +/// excluded domains and IPs. This makes excluded hosts bypass the OSTP proxy +/// entirely at the OS level — the most reliable split-tunneling mechanism. +/// +/// For each domain `d`, adds both `d` and `*.d` so both the root and all +/// subdomains bypass the proxy. +/// For IPs, adds them verbatim (Windows supports exact IPs and wildcards like +/// `192.168.*`). +#[cfg(target_os = "windows")] +pub fn update_proxy_bypass_list(domains: &[String], ips: &[String]) { + update_proxy_bypass_list_windows(domains, ips); + refresh_wininet(); +} + +#[cfg(not(target_os = "windows"))] +pub fn update_proxy_bypass_list(_domains: &[String], _ips: &[String]) { + // Linux/macOS: no-op (gnome/kde proxy bypass list update not implemented) +} + +#[cfg(target_os = "windows")] +fn update_proxy_bypass_list_windows(domains: &[String], ips: &[String]) { + // Base list: always bypass local addresses + let mut parts: Vec = vec![ + "localhost".into(), + "127.*".into(), + "10.*".into(), + "172.16.*".into(), + "172.17.*".into(), + "172.18.*".into(), + "172.19.*".into(), + "172.20.*".into(), + "172.21.*".into(), + "172.22.*".into(), + "172.23.*".into(), + "172.24.*".into(), + "172.25.*".into(), + "172.26.*".into(), + "172.27.*".into(), + "172.28.*".into(), + "172.29.*".into(), + "172.30.*".into(), + "172.31.*".into(), + "192.168.*".into(), + "".into(), + ]; + + // Add excluded domains: both exact and wildcard subdomain form + for d in domains { + let d = d.trim().trim_start_matches('.').to_lowercase(); + if d.is_empty() { continue; } + parts.push(d.clone()); + parts.push(format!("*.{}", d)); + } + + // Add excluded IPs verbatim + for ip in ips { + let ip = ip.trim(); + if ip.is_empty() { continue; } + // Strip CIDR suffix if present — Windows ProxyOverride doesn't support CIDR + let host = ip.split('/').next().unwrap_or(ip); + parts.push(host.to_string()); + } + + let override_value = parts.join(";"); + tracing::info!("Updating ProxyOverride: {}", override_value); + let _ = Command::new("reg") .creation_flags(CREATE_NO_WINDOW) .args([ @@ -72,13 +144,10 @@ pub fn enable_windows_proxy(proxy_addr: &str) { "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "/v", "ProxyOverride", "/t", "REG_SZ", - "/d", "localhost;127.*;10.*;192.168.*;", + "/d", &override_value, "/f", ]) .output(); - - refresh_wininet(); - tracing::info!("System proxy enabled successfully"); } #[cfg(target_os = "windows")] diff --git a/ostp-client/src/tunnel/native_handler.rs b/ostp-client/src/tunnel/native_handler.rs index 71ba768..572e4e8 100644 --- a/ostp-client/src/tunnel/native_handler.rs +++ b/ostp-client/src/tunnel/native_handler.rs @@ -40,6 +40,12 @@ pub async fn run_native_tunnel( let debug = config.debug; tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)..."); + // Capture physical interface index for bypass BEFORE we create the TUN device and alter routes. + #[cfg(target_os = "windows")] + let phys_if_for_bypass: Option = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx); + #[cfg(not(target_os = "windows"))] + let phys_if_for_bypass: Option = None; + // ── 1. Resolve server IP ────────────────────────────────────────────────── let server_ip = config .ostp @@ -204,11 +210,7 @@ pub async fn run_native_tunnel( } }); - // Physical interface index — Some on Windows, None everywhere else - #[cfg(target_os = "windows")] - let phys_if_for_bypass: Option = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx); - #[cfg(not(target_os = "windows"))] - let phys_if_for_bypass: Option = None; + // Physical interface index was captured at the start of the function. // Linux: physical interface name for SO_BINDTODEVICE #[cfg(target_os = "linux")] diff --git a/ostp-client/src/tunnel/proxy.rs b/ostp-client/src/tunnel/proxy.rs index e1634c9..9191199 100644 --- a/ostp-client/src/tunnel/proxy.rs +++ b/ostp-client/src/tunnel/proxy.rs @@ -32,6 +32,7 @@ extern "system" { pub fn bind_socket_to_interface(socket: &impl AsRawSocket, is_ipv6: bool, if_index: u32) -> std::io::Result<()> { let s = socket.as_raw_socket() as usize; if is_ipv6 { + // IPV6_UNICAST_IF expects interface index in host byte order let optval = if_index; let ret = unsafe { setsockopt( @@ -46,7 +47,8 @@ pub fn bind_socket_to_interface(socket: &impl AsRawSocket, is_ipv6: bool, if_ind return Err(std::io::Error::last_os_error()); } } else { - let optval = if_index.to_be(); + // IP_UNICAST_IF expects interface index in host byte order (NOT big-endian) + let optval = if_index; let ret = unsafe { setsockopt( s, diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index 5a74e38..7cce7d1 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -341,22 +341,13 @@
-
- -
- TUN only - -
-
+
- Only works in TUN mode. Excluded apps bypass the VPN. + Only works in TUN mode. Type process name and press Enter.
@@ -368,24 +359,6 @@
- - -