ostp/ostp-tun/src/windows_route.rs

204 lines
7.4 KiB
Rust

/// Windows routing table utilities for OSTP split tunneling.
///
/// The approach used here matches how sing-box/v2rayN implement split tunneling on Windows:
/// - A high-priority default route (metric=1) via ostp_tun captures ALL traffic.
/// - Per-host /32 routes via the REAL gateway with an even lower metric (=0, auto-managed by OS)
/// force excluded IPs to bypass the TUN.
/// - Process-based exclusions are NOT supported via pure routing — they would require WFP.
/// Instead, we surface a diagnostic warning in logs.
#[cfg(target_os = "windows")]
pub mod sys {
use std::mem;
use std::net::Ipv4Addr;
use std::ptr;
use winapi::shared::ipmib::{MIB_IPFORWARDROW, MIB_IPFORWARDTABLE};
use winapi::shared::minwindef::{DWORD, ULONG};
use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, NO_ERROR};
use winapi::um::iphlpapi::{
CreateIpForwardEntry, DeleteIpForwardEntry, GetAdaptersAddresses, GetIpForwardTable,
};
use winapi::um::iptypes::{
GAA_FLAG_SKIP_ANYCAST, GAA_FLAG_SKIP_DNS_SERVER, GAA_FLAG_SKIP_MULTICAST, IP_ADAPTER_ADDRESSES,
};
use winapi::shared::ws2def::AF_INET;
fn ipv4_to_dword(ip: Ipv4Addr) -> DWORD {
u32::from_ne_bytes(ip.octets())
}
fn dword_to_ipv4(dw: DWORD) -> Ipv4Addr {
Ipv4Addr::from(dw.to_ne_bytes())
}
/// Returns the (gateway_ip, interface_index) of the physical default IPv4 route,
/// excluding any route that goes through an interface named "ostp_tun".
pub fn get_default_ipv4_route() -> Option<(Ipv4Addr, u32)> {
// Enumerate adapters to find the ostp_tun interface index, so we can skip it.
let tun_index = get_interface_index("ostp_tun");
unsafe {
let mut size: ULONG = 0;
let mut ret = GetIpForwardTable(ptr::null_mut(), &mut size, 0);
if ret != ERROR_INSUFFICIENT_BUFFER {
return None;
}
let mut buf: Vec<u8> = vec![0; size as usize];
let table = buf.as_mut_ptr() as *mut MIB_IPFORWARDTABLE;
ret = GetIpForwardTable(table, &mut size, 0);
if ret != NO_ERROR {
return None;
}
let entries = std::slice::from_raw_parts((*table).table.as_ptr(), (*table).dwNumEntries as usize);
let mut best_gw = None;
let mut best_metric = u32::MAX;
let mut best_ifindex = 0u32;
for row in entries {
// Only consider default routes (0.0.0.0/0)
if row.dwForwardDest == 0 && row.dwForwardMask == 0 {
// Skip the TUN interface
if let Some(ti) = tun_index {
if row.dwForwardIfIndex == ti {
continue;
}
}
let metric = row.dwForwardMetric1;
if metric < best_metric {
best_metric = metric;
best_gw = Some(dword_to_ipv4(row.dwForwardNextHop));
best_ifindex = row.dwForwardIfIndex;
}
}
}
best_gw.map(|gw| (gw, best_ifindex))
}
}
pub fn add_ipv4_route(
dest: Ipv4Addr,
mask: Ipv4Addr,
nexthop: Ipv4Addr,
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;
let ret = unsafe { CreateIpForwardEntry(&mut row) };
if ret == NO_ERROR {
Ok(())
} else {
Err(format!("CreateIpForwardEntry failed: {}", ret))
}
}
pub fn delete_ipv4_route(
dest: Ipv4Addr,
mask: Ipv4Addr,
nexthop: Ipv4Addr,
if_index: 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;
let ret = unsafe { DeleteIpForwardEntry(&mut row) };
if ret == NO_ERROR || ret == 2 {
Ok(())
} else {
Err(format!("DeleteIpForwardEntry failed: {}", ret))
}
}
pub fn get_interface_index(name: &str) -> Option<u32> {
unsafe {
let mut size: ULONG = 0;
let flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
let mut ret = GetAdaptersAddresses(
AF_INET as u32,
flags,
ptr::null_mut(),
ptr::null_mut(),
&mut size,
);
if ret != ERROR_INSUFFICIENT_BUFFER {
return None;
}
let mut buf: Vec<u8> = vec![0; size as usize];
let addresses = buf.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES;
ret = GetAdaptersAddresses(AF_INET as u32, flags, ptr::null_mut(), addresses, &mut size);
if ret != NO_ERROR {
return None;
}
let mut curr = addresses;
while !curr.is_null() {
let friendly_name_ptr = (*curr).FriendlyName;
if !friendly_name_ptr.is_null() {
let mut len = 0;
while *friendly_name_ptr.offset(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(friendly_name_ptr, len as usize);
let friendly_name = String::from_utf16_lossy(slice);
if friendly_name == name {
return Some((*(*curr).u.s()).IfIndex);
}
}
curr = (*curr).Next;
}
None
}
}
/// Add bypass routes for a list of resolved IP addresses (typically from exclusion config).
/// Each IP gets a /32 host route via the physical gateway so it bypasses the TUN.
/// Returns list of (ip, gw, if_index) that were successfully added, for later cleanup.
pub fn add_bypass_routes(
ips: &[Ipv4Addr],
gw: Ipv4Addr,
if_index: u32,
metric: u32,
) -> Vec<(Ipv4Addr, Ipv4Addr, u32)> {
let mut added = Vec::new();
for &ip in ips {
let mask = Ipv4Addr::new(255, 255, 255, 255);
match add_ipv4_route(ip, mask, gw, if_index, metric) {
Ok(()) => {
added.push((ip, gw, if_index));
}
Err(e) => {
// 87 = ERROR_INVALID_PARAMETER (route may already exist)
tracing::debug!("bypass route add {ip}/32 via {gw}: {e}");
}
}
}
added
}
/// Remove all bypass routes previously added by add_bypass_routes.
pub fn remove_bypass_routes(routes: &[(Ipv4Addr, Ipv4Addr, u32)]) {
for &(ip, gw, if_index) in routes {
let mask = Ipv4Addr::new(255, 255, 255, 255);
let _ = delete_ipv4_route(ip, mask, gw, if_index);
}
}
}