diff --git a/ostp-tun/src/windows.rs b/ostp-tun/src/windows.rs index 00a6fbd..9dc42b2 100644 --- a/ostp-tun/src/windows.rs +++ b/ostp-tun/src/windows.rs @@ -99,18 +99,34 @@ pub async fn create(opts: OstpTunOptions) -> Result { let current_exe = std::env::current_exe()?.to_string_lossy().into_owned(); - // A freshly created WinTun adapter can take several seconds to appear in - // GetAdaptersAddresses (it only shows up once it has an operational IPv4 - // binding). The default route via the TUN is what actually captures - // traffic, so this lookup is critical — give it a generous window (~15s). - let mut tun_index = None; - for _ in 0..75 { - if let Some(idx) = windows_route::sys::get_interface_index("ostp_tun") { - tun_index = Some(idx); - break; + // Take the interface index straight from the adapter WinTun just created, + // via the tun crate. The old code looked it up by FriendlyName == "ostp_tun" + // through GetAdaptersAddresses — but WinTun does NOT set the FriendlyName to + // the adapter name, so that match never succeeded: on every single connect + // it spun the full 15s and then gave up with "traffic will NOT be captured", + // leaving the default route (and, above, the server-IP bypass) uninstalled. + // get_adapter_index() is instant and correct. + use tun::AbstractDevice; + let tun_index = match dev.tun_index() { + Ok(idx) if idx > 0 => Some(idx as u32), + Ok(idx) => { + tracing::error!("WinTun reported a non-positive interface index ({idx})"); + None } - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - } + Err(e) => { + // Fall back to the old name lookup rather than fail outright. + tracing::warn!("Could not read TUN index from the adapter ({e}); falling back to name lookup"); + let mut idx = None; + for _ in 0..75 { + if let Some(i) = windows_route::sys::get_interface_index("ostp_tun") { + idx = Some(i); + break; + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + idx + } + }; if let Some(idx) = tun_index { match windows_route::sys::add_ipv4_route( diff --git a/ostp-tun/src/windows_route.rs b/ostp-tun/src/windows_route.rs index d090b89..db95704 100644 --- a/ostp-tun/src/windows_route.rs +++ b/ostp-tun/src/windows_route.rs @@ -17,7 +17,7 @@ pub mod sys { use winapi::shared::minwindef::{DWORD, ULONG}; use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, NO_ERROR}; use winapi::um::iphlpapi::{ - CreateIpForwardEntry, DeleteIpForwardEntry, GetAdaptersAddresses, GetIpForwardTable, + DeleteIpForwardEntry, GetAdaptersAddresses, GetIpForwardTable, }; use winapi::um::iptypes::{ GAA_FLAG_SKIP_ANYCAST, GAA_FLAG_SKIP_DNS_SERVER, GAA_FLAG_SKIP_MULTICAST, IP_ADAPTER_ADDRESSES, @@ -88,20 +88,47 @@ pub mod sys { if_index: u32, metric: u32, ) -> Result<(), String> { - let mut row: MIB_IPFORWARDROW = unsafe { mem::zeroed() }; - row.dwForwardDest = ipv4_to_dword(dest); - row.dwForwardMask = ipv4_to_dword(mask); - row.dwForwardNextHop = ipv4_to_dword(nexthop); - row.dwForwardIfIndex = if_index; - row.ForwardType = if nexthop == Ipv4Addr::UNSPECIFIED || dest == nexthop { 3 } else { 4 }; - row.ForwardProto = 3; // MIB_IPPROTO_NETMGMT - row.dwForwardMetric1 = metric; + // Installed through route.exe rather than CreateIpForwardEntry. + // + // The legacy CreateIpForwardEntry API was failing here with error 160 + // (ERROR_BAD_ARGUMENTS) on every single route — server-IP bypass and TUN + // default route alike — which left the server IP routed INTO the tunnel + // (a loop that froze the link for seconds under load) and the default + // route uninstalled. route.exe resolves the interface and validates the + // gateway itself, and is what the teardown path already uses, so it + // succeeds where the hand-built MIB_IPFORWARDROW did not. + use std::os::windows::process::CommandExt; + use std::process::Command; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; - let ret = unsafe { CreateIpForwardEntry(&mut row) }; - if ret == NO_ERROR { + // route add mask metric if + let out = Command::new("route") + .args([ + "add", + &dest.to_string(), + "mask", + &mask.to_string(), + &nexthop.to_string(), + "metric", + &metric.to_string(), + "if", + &if_index.to_string(), + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(|e| format!("could not run route.exe: {e}"))?; + + if out.status.success() { Ok(()) } else { - Err(format!("CreateIpForwardEntry failed: {}", ret)) + // route.exe prints its diagnostics to stdout, not stderr. + let msg = String::from_utf8_lossy(&out.stdout); + let msg = msg.trim(); + Err(format!( + "route add failed (exit {:?}): {}", + out.status.code(), + if msg.is_empty() { "no output" } else { msg } + )) } }