From 04a548ca233d97e3101d4e8ec28abf0271338268 Mon Sep 17 00:00:00 2001 From: tianrking Date: Fri, 20 Mar 2026 17:08:43 +0800 Subject: [PATCH] feat(core): tighten windows proxy enforcement and dns/ipv6 handling --- proxychains/src/config/types.rs | 32 ++- proxychains/src/hook/hooks.rs | 6 +- proxychains/src/hook/hooks_windows.rs | 252 +++++++++++++++++++++- proxychains/src/hook/interpose_windows.rs | 72 ++++++- proxychains/src/net/socket.rs | 22 ++ proxychains/src/net/socket_windows.rs | 19 ++ 6 files changed, 389 insertions(+), 14 deletions(-) diff --git a/proxychains/src/config/types.rs b/proxychains/src/config/types.rs index e26f939..e220399 100644 --- a/proxychains/src/config/types.rs +++ b/proxychains/src/config/types.rs @@ -238,8 +238,14 @@ impl Config { pub fn should_bypass_ip(&self, ip: &IpAddr) -> bool { match ip { IpAddr::V4(v4) => self.should_bypass(v4), - // For now, IPv6 bypass only loopback. - IpAddr::V6(v6) => v6.is_loopback(), + // Mirror localhost/local semantics for IPv6 to avoid unintended proxying + // of link-local/unspecified traffic. + IpAddr::V6(v6) => { + v6.is_loopback() + || v6.is_unspecified() + || v6.is_unique_local() + || v6.is_unicast_link_local() + } } } @@ -274,3 +280,25 @@ impl Config { !self.proxies.is_empty() } } + +#[cfg(test)] +mod tests { + use super::*; + use std::net::Ipv6Addr; + + #[test] + fn test_ipv6_bypass_local_scopes() { + let cfg = Config::default(); + assert!(cfg.should_bypass_ip(&IpAddr::V6(Ipv6Addr::LOCALHOST))); + assert!(cfg.should_bypass_ip(&IpAddr::V6(Ipv6Addr::UNSPECIFIED))); + assert!(cfg.should_bypass_ip(&IpAddr::V6(Ipv6Addr::new( + 0xfe80, 0, 0, 0, 1, 2, 3, 4 + )))); + assert!(cfg.should_bypass_ip(&IpAddr::V6(Ipv6Addr::new( + 0xfc00, 0, 0, 0, 1, 2, 3, 4 + )))); + assert!(!cfg.should_bypass_ip(&IpAddr::V6(Ipv6Addr::new( + 0x2606, 0x4700, 0, 0, 0, 0, 0, 0x1111 + )))); + } +} diff --git a/proxychains/src/hook/hooks.rs b/proxychains/src/hook/hooks.rs index 43e12bb..6d48410 100644 --- a/proxychains/src/hook/hooks.rs +++ b/proxychains/src/hook/hooks.rs @@ -407,7 +407,7 @@ pub unsafe fn hook_getaddrinfo( // Get the hostname let hostname = match CStr::from_ptr(node).to_str() { Ok(s) => s.to_string(), - Err(_) => return original_getaddrinfo(node, service, hints, res), + Err(_) => return libc::EAI_NONAME, }; debug!("getaddrinfo hook for: {}", hostname); @@ -475,7 +475,7 @@ pub unsafe fn hook_gethostbyname(name: *const c_char) -> *mut libc::hostent { // Get the hostname let hostname = match CStr::from_ptr(name).to_str() { Ok(s) => s.to_string(), - Err(_) => return original_gethostbyname(name), + Err(_) => return std::ptr::null_mut(), }; debug!("gethostbyname hook for: {}", hostname); @@ -504,7 +504,7 @@ pub unsafe fn hook_gethostbyname(name: *const c_char) -> *mut libc::hostent { let name_cstr = match CString::new(hostname) { Ok(v) => v, - Err(_) => return original_gethostbyname(name), + Err(_) => return std::ptr::null_mut(), }; GETHOSTBYNAME_STORE.with(|slot| { diff --git a/proxychains/src/hook/hooks_windows.rs b/proxychains/src/hook/hooks_windows.rs index 774d6f8..97568f8 100644 --- a/proxychains/src/hook/hooks_windows.rs +++ b/proxychains/src/hook/hooks_windows.rs @@ -16,10 +16,12 @@ use parking_lot::Mutex; use rand::seq::SliceRandom; use tracing::{debug, error, info, warn}; use windows::Win32::Networking::WinSock::{ - ADDRINFOA, AF_INET, AF_INET6, IN_ADDR, IN_ADDR_0, IPPROTO_TCP, SOCKADDR, SOCKADDR_IN, - SOCK_STREAM, SOCKET_ERROR, WSAEALREADY, WSAECONNREFUSED, WSAEFAULT, WSAEINPROGRESS, WSAEINVAL, - WSAEWOULDBLOCK, WSAGetLastError, WSAHOST_NOT_FOUND, WSASetLastError, + ADDRINFOA, ADDRINFOW, AF_INET, AF_INET6, IN_ADDR, IN_ADDR_0, IPPROTO_TCP, SOCKADDR, + SOCKADDR_IN, SOCK_STREAM, SOCKET_ERROR, WSAEALREADY, WSAECONNREFUSED, WSAEFAULT, + WSAEINPROGRESS, WSAEINVAL, WSAEOPNOTSUPP, WSAEWOULDBLOCK, WSAGetLastError, WSAHOST_NOT_FOUND, + WSASetLastError, }; +use windows::core::GUID; use crate::config::{ChainType, Config, ProxyData, ProxyState}; use crate::dns::{is_fake_ip, DnsResolver}; @@ -30,7 +32,7 @@ use crate::ConfigParser; use super::interpose_windows::{ init_original_functions, original_connect, original_freeaddrinfo, original_getaddrinfo, - original_gethostbyname, original_getnameinfo, + original_getaddrinfow, original_gethostbyname, original_getnameinfo, original_wsa_ioctl, }; use super::reload::config_reload_interval; @@ -66,6 +68,7 @@ static CUSTOM_ADDRINFO_ALLOCATIONS: OnceLock &'static Mutex> { @@ -307,6 +310,35 @@ fn parse_service_port(service: *const i8) -> u16 { } } +fn parse_service_port_wide(service: *const u16) -> u16 { + if service.is_null() { + return 0; + } + unsafe { + let mut len = 0usize; + while *service.add(len) != 0 { + len += 1; + } + let slice = std::slice::from_raw_parts(service, len); + let s = String::from_utf16_lossy(slice); + s.parse::().unwrap_or(0) + } +} + +fn parse_wide_string(ptr: *const u16) -> std::result::Result { + if ptr.is_null() { + return Err(()); + } + unsafe { + let mut len = 0usize; + while *ptr.add(len) != 0 { + len += 1; + } + let slice = std::slice::from_raw_parts(ptr, len); + String::from_utf16(slice).map_err(|_| ()) + } +} + /// Windows connect hook implementation. #[cfg(windows)] pub unsafe extern "system" fn hook_connect_impl( @@ -423,6 +455,104 @@ pub unsafe extern "system" fn hook_wsa_connect_impl( hook_connect_impl(sock, name, namelen) } +const SIO_GET_EXTENSION_FUNCTION_POINTER: u32 = 0xC800_0006; +const WSAID_CONNECTEX: GUID = GUID::from_u128(0x25a207b9_ddf3_4660_8ee9_76e58c74063e); + +/// ConnectEx replacement used when applications query extension function pointers via WSAIoctl. +/// +/// This implementation is intentionally synchronous: +/// - `lpOverlapped != NULL` is rejected (`WSAEOPNOTSUPP`) to avoid silent bypass. +/// - `dwSendDataLength > 0` is rejected (`WSAEOPNOTSUPP`) because initial-send semantics are +/// not yet mirrored. +#[cfg(windows)] +pub unsafe extern "system" fn hook_connect_ex_impl( + sock: usize, + name: *const c_void, + namelen: i32, + send_buf: *const c_void, + send_len: u32, + bytes_sent: *mut u32, + overlapped: *mut c_void, +) -> i32 { + if !overlapped.is_null() || (!send_buf.is_null() && send_len > 0) { + WSASetLastError(WSAEOPNOTSUPP.0); + return 0; + } + + if !bytes_sent.is_null() { + *bytes_sent = 0; + } + + let ret = hook_connect_impl(sock, name, namelen); + if ret == SOCKET_ERROR { + 0 + } else { + 1 + } +} + +/// WSAIoctl hook implementation. +/// +/// Intercepts `SIO_GET_EXTENSION_FUNCTION_POINTER` for `WSAID_CONNECTEX` and returns our own +/// ConnectEx function pointer so ConnectEx-based clients still go through proxy enforcement. +#[cfg(windows)] +pub unsafe extern "system" fn hook_wsa_ioctl_impl( + sock: usize, + io_control_code: u32, + in_buffer: *mut c_void, + in_buffer_len: u32, + out_buffer: *mut c_void, + out_buffer_len: u32, + bytes_returned: *mut u32, + overlapped: *mut c_void, + completion_routine: *mut c_void, +) -> i32 { + if io_control_code != SIO_GET_EXTENSION_FUNCTION_POINTER + || in_buffer_len < std::mem::size_of::() as u32 + || out_buffer_len < std::mem::size_of::<*const c_void>() as u32 + || in_buffer.is_null() + || out_buffer.is_null() + { + return original_wsa_ioctl( + sock, + io_control_code, + in_buffer, + in_buffer_len, + out_buffer, + out_buffer_len, + bytes_returned, + overlapped, + completion_routine, + ); + } + + let requested = *(in_buffer as *const GUID); + if requested != WSAID_CONNECTEX { + return original_wsa_ioctl( + sock, + io_control_code, + in_buffer, + in_buffer_len, + out_buffer, + out_buffer_len, + bytes_returned, + overlapped, + completion_routine, + ); + } + + let replacement = hook_connect_ex_impl as *const c_void; + std::ptr::copy_nonoverlapping( + &replacement as *const *const c_void as *const u8, + out_buffer as *mut u8, + std::mem::size_of::<*const c_void>(), + ); + if !bytes_returned.is_null() { + *bytes_returned = std::mem::size_of::<*const c_void>() as u32; + } + 0 +} + /// Windows getaddrinfo hook implementation. #[cfg(windows)] pub unsafe extern "system" fn hook_getaddrinfo_impl( @@ -443,7 +573,7 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl( let dns_resolver = DnsResolver::new(config.proxy_dns, config.remote_dns_subnet); if pnode.is_null() || ppresult.is_null() { - return original_getaddrinfo(pnode, pservice, phints, ppresult); + return WSAEINVAL.0; } let requested_family = if phints.is_null() { @@ -461,7 +591,7 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl( let hostname = match CStr::from_ptr(pnode).to_str() { Ok(s) => s, - Err(_) => return original_getaddrinfo(pnode, pservice, phints, ppresult), + Err(_) => return WSAEINVAL.0, }; if hostname.parse::().is_ok() { @@ -518,6 +648,7 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl( CustomAddrinfoAllocation { sockaddr_ptr: sockaddr_ptr as usize, family: requested_family, + is_wide: false, }, ); @@ -525,6 +656,109 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl( 0 } +/// Windows GetAddrInfoW hook implementation. +#[cfg(windows)] +pub unsafe extern "system" fn hook_getaddrinfow_impl( + pnode: *const u16, + pservice: *const u16, + phints: *const c_void, + ppresult: *mut *mut c_void, +) -> i32 { + let state = match get_hook_state() { + Some(s) => s, + None => return original_getaddrinfow(pnode, pservice, phints, ppresult), + }; + maybe_reload_config(state); + let config = state.config.lock().clone(); + if !config.proxy_dns { + return original_getaddrinfow(pnode, pservice, phints, ppresult); + } + let dns_resolver = DnsResolver::new(config.proxy_dns, config.remote_dns_subnet); + + if pnode.is_null() || ppresult.is_null() { + return WSAEINVAL.0; + } + + let requested_family = if phints.is_null() { + AF_INET.0 as i32 + } else { + let hints = &*(phints as *const ADDRINFOW); + if hints.ai_family == 0 || hints.ai_family == AF_INET.0 as i32 { + AF_INET.0 as i32 + } else if hints.ai_family == AF_INET6.0 as i32 { + AF_INET6.0 as i32 + } else { + return WSAHOST_NOT_FOUND.0; + } + }; + + let hostname = match parse_wide_string(pnode) { + Ok(s) => s, + Err(_) => return WSAEINVAL.0, + }; + + if hostname.parse::().is_ok() { + return original_getaddrinfow(pnode, pservice, phints, ppresult); + } + + if crate::dns::lookup_in_hosts(&hostname).is_some() { + return original_getaddrinfow(pnode, pservice, phints, ppresult); + } + + let fake_ip = match dns_resolver.resolve(&hostname) { + Ok(ip) => ip, + Err(e) => { + error!("Failed to resolve {} in hook_getaddrinfow_impl: {}", hostname, e); + return WSAHOST_NOT_FOUND.0; + } + }; + + let service_port = parse_service_port_wide(pservice); + let (sockaddr_ptr, addrlen) = if requested_family == AF_INET6.0 as i32 { + let sockaddr_raw = Box::new(make_sockaddr_in6_mapped_bytes(fake_ip, service_port)); + ( + Box::into_raw(sockaddr_raw) as *mut SOCKADDR, + mem::size_of::<[u8; 28]>(), + ) + } else { + let sockaddr_box = Box::new(make_sockaddr_in(fake_ip, service_port)); + ( + Box::into_raw(sockaddr_box) as *mut SOCKADDR, + mem::size_of::(), + ) + }; + + let mut ai = ADDRINFOW::default(); + ai.ai_family = requested_family; + ai.ai_socktype = if phints.is_null() { + SOCK_STREAM.0 + } else { + (*(phints as *const ADDRINFOW)).ai_socktype + }; + ai.ai_protocol = if phints.is_null() { + IPPROTO_TCP.0 + } else { + (*(phints as *const ADDRINFOW)).ai_protocol + }; + ai.ai_addrlen = addrlen; + ai.ai_addr = sockaddr_ptr; + ai.ai_next = std::ptr::null_mut(); + + let ai_ptr = Box::into_raw(Box::new(ai)); + *ppresult = ai_ptr as *mut c_void; + custom_alloc_map().lock().insert( + ai_ptr as usize, + CustomAddrinfoAllocation { + sockaddr_ptr: sockaddr_ptr as usize, + family: requested_family, + is_wide: true, + }, + ); + + debug!("Assigned fake IP {} for {} (GetAddrInfoW)", fake_ip, hostname); + 0 +} + /// Windows freeaddrinfo hook implementation. #[cfg(windows)] pub unsafe extern "system" fn hook_freeaddrinfo_impl(pres: *mut c_void) { @@ -538,7 +772,11 @@ pub unsafe extern "system" fn hook_freeaddrinfo_impl(pres: *mut c_void) { } else { let _ = Box::from_raw(alloc.sockaddr_ptr as *mut SOCKADDR_IN); } - let _ = Box::from_raw(pres as *mut ADDRINFOA); + if alloc.is_wide { + let _ = Box::from_raw(pres as *mut ADDRINFOW); + } else { + let _ = Box::from_raw(pres as *mut ADDRINFOA); + } return; } diff --git a/proxychains/src/hook/interpose_windows.rs b/proxychains/src/hook/interpose_windows.rs index 123bc0c..6f0d8e2 100644 --- a/proxychains/src/hook/interpose_windows.rs +++ b/proxychains/src/hook/interpose_windows.rs @@ -14,8 +14,8 @@ use windows::Win32::Networking::WinSock::{WSAECONNREFUSED, WSAHOST_NOT_FOUND, WS use crate::error::{Error, Result}; use super::hooks_windows::{ - hook_connect_impl, hook_freeaddrinfo_impl, hook_getaddrinfo_impl, hook_gethostbyname_impl, - hook_getnameinfo_impl, hook_wsa_connect_impl, + hook_connect_impl, hook_freeaddrinfo_impl, hook_getaddrinfo_impl, hook_getaddrinfow_impl, + hook_gethostbyname_impl, hook_getnameinfo_impl, hook_wsa_connect_impl, hook_wsa_ioctl_impl, }; type ConnectFn = unsafe extern "system" fn(usize, *const c_void, i32) -> i32; @@ -30,17 +30,32 @@ type WsaConnectFn = unsafe extern "system" fn( ) -> i32; type GetAddrInfoFn = unsafe extern "system" fn(*const i8, *const i8, *const c_void, *mut *mut c_void) -> i32; +type GetAddrInfoWFn = + unsafe extern "system" fn(*const u16, *const u16, *const c_void, *mut *mut c_void) -> i32; type FreeAddrInfoFn = unsafe extern "system" fn(*mut c_void); type GetHostByNameFn = unsafe extern "system" fn(*const i8) -> *mut c_void; type GetNameInfoFn = unsafe extern "system" fn(*const c_void, i32, *mut i8, u32, *mut i8, u32, i32) -> i32; +type WsaIoctlFn = unsafe extern "system" fn( + usize, + u32, + *mut c_void, + u32, + *mut c_void, + u32, + *mut u32, + *mut c_void, + *mut c_void, +) -> i32; static ORIGINAL_CONNECT: OnceLock = OnceLock::new(); static ORIGINAL_WSA_CONNECT: OnceLock = OnceLock::new(); static ORIGINAL_GETADDRINFO: OnceLock = OnceLock::new(); +static ORIGINAL_GETADDRINFOW: OnceLock = OnceLock::new(); static ORIGINAL_FREEADDRINFO: OnceLock = OnceLock::new(); static ORIGINAL_GETHOSTBYNAME: OnceLock = OnceLock::new(); static ORIGINAL_GETNAMEINFO: OnceLock = OnceLock::new(); +static ORIGINAL_WSA_IOCTL: OnceLock = OnceLock::new(); static HOOKS_READY: AtomicBool = AtomicBool::new(false); @@ -68,10 +83,16 @@ impl OriginalFunctions { install_api_hook("connect", hook_connect_impl as *const () as *mut c_void)?; let wsa_connect_fn: WsaConnectFn = install_api_hook("WSAConnect", hook_wsa_connect_impl as *const () as *mut c_void)?; + let wsa_ioctl_fn: WsaIoctlFn = + install_api_hook("WSAIoctl", hook_wsa_ioctl_impl as *const () as *mut c_void)?; let getaddrinfo_fn: GetAddrInfoFn = install_api_hook( "getaddrinfo", hook_getaddrinfo_impl as *const () as *mut c_void, )?; + let getaddrinfow_fn: GetAddrInfoWFn = install_api_hook( + "GetAddrInfoW", + hook_getaddrinfow_impl as *const () as *mut c_void, + )?; let freeaddrinfo_fn: FreeAddrInfoFn = install_api_hook( "freeaddrinfo", hook_freeaddrinfo_impl as *const () as *mut c_void, @@ -87,7 +108,9 @@ impl OriginalFunctions { let _ = ORIGINAL_CONNECT.set(connect_fn); let _ = ORIGINAL_WSA_CONNECT.set(wsa_connect_fn); + let _ = ORIGINAL_WSA_IOCTL.set(wsa_ioctl_fn); let _ = ORIGINAL_GETADDRINFO.set(getaddrinfo_fn); + let _ = ORIGINAL_GETADDRINFOW.set(getaddrinfow_fn); let _ = ORIGINAL_FREEADDRINFO.set(freeaddrinfo_fn); let _ = ORIGINAL_GETHOSTBYNAME.set(gethostbyname_fn); let _ = ORIGINAL_GETNAMEINFO.set(getnameinfo_fn); @@ -154,6 +177,36 @@ pub unsafe fn original_wsa_connect( } } +/// Call the original WSAIoctl function. +pub unsafe fn original_wsa_ioctl( + sock: usize, + io_control_code: u32, + in_buffer: *mut c_void, + in_buffer_len: u32, + out_buffer: *mut c_void, + out_buffer_len: u32, + bytes_returned: *mut u32, + overlapped: *mut c_void, + completion_routine: *mut c_void, +) -> i32 { + if let Some(f) = ORIGINAL_WSA_IOCTL.get() { + f( + sock, + io_control_code, + in_buffer, + in_buffer_len, + out_buffer, + out_buffer_len, + bytes_returned, + overlapped, + completion_routine, + ) + } else { + WSASetLastError(WSAECONNREFUSED.0); + -1 + } +} + /// Call the original getaddrinfo function. pub unsafe fn original_getaddrinfo( node: *const i8, @@ -169,6 +222,21 @@ pub unsafe fn original_getaddrinfo( } } +/// Call the original GetAddrInfoW function. +pub unsafe fn original_getaddrinfow( + node: *const u16, + service: *const u16, + hints: *const c_void, + res: *mut *mut c_void, +) -> i32 { + if let Some(f) = ORIGINAL_GETADDRINFOW.get() { + f(node, service, hints, res) + } else { + WSASetLastError(WSAHOST_NOT_FOUND.0); + WSAHOST_NOT_FOUND.0 + } +} + /// Call the original freeaddrinfo function. pub unsafe fn original_freeaddrinfo(res: *mut c_void) { if let Some(f) = ORIGINAL_FREEADDRINFO.get() { diff --git a/proxychains/src/net/socket.rs b/proxychains/src/net/socket.rs index b8cd4e8..c1a431f 100644 --- a/proxychains/src/net/socket.rs +++ b/proxychains/src/net/socket.rs @@ -152,6 +152,10 @@ pub fn get_ip_from_sockaddr(addr: *const libc::sockaddr) -> Option { let addr_in = addr as *const libc::sockaddr_in; let ip_bytes = (*addr_in).sin_addr.s_addr.to_ne_bytes(); Some(Ipv4Addr::from(ip_bytes)) + } else if sa_family == libc::AF_INET6 as libc::sa_family_t { + let addr_in6 = addr as *const libc::sockaddr_in6; + let v6 = Ipv6Addr::from((*addr_in6).sin6_addr.s6_addr); + v6.to_ipv4_mapped() } else { None } @@ -201,4 +205,22 @@ mod tests { assert!(is_localhost(&Ipv4Addr::new(127, 0, 0, 1))); assert!(!is_localhost(&Ipv4Addr::new(192, 168, 1, 1))); } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_get_ip_from_sockaddr_ipv6_mapped() { + let sockaddr = libc::sockaddr_in6 { + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))] + sin6_len: std::mem::size_of::() as u8, + sin6_family: libc::AF_INET6 as libc::sa_family_t, + sin6_port: 0, + sin6_flowinfo: 0, + sin6_addr: libc::in6_addr { + s6_addr: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 5, 6, 7, 8], + }, + sin6_scope_id: 0, + }; + let ptr = &sockaddr as *const libc::sockaddr_in6 as *const libc::sockaddr; + assert_eq!(get_ip_from_sockaddr(ptr), Some(Ipv4Addr::new(5, 6, 7, 8))); + } } diff --git a/proxychains/src/net/socket_windows.rs b/proxychains/src/net/socket_windows.rs index 6c4016f..8ce9dd5 100644 --- a/proxychains/src/net/socket_windows.rs +++ b/proxychains/src/net/socket_windows.rs @@ -47,6 +47,13 @@ pub fn get_ip_from_sockaddr(addr: *const core::ffi::c_void) -> Option *ip_ptr.add(3), ]; Some(Ipv4Addr::from(ip_bytes)) + } else if sa_family == 23 { + // Handle IPv4-mapped IPv6 (::ffff:a.b.c.d) so reverse fake-DNS lookup works. + let ip_ptr = (addr as *const u8).add(8); + let mut octets = [0u8; 16]; + std::ptr::copy_nonoverlapping(ip_ptr, octets.as_mut_ptr(), 16); + let v6 = Ipv6Addr::from(octets); + v6.to_ipv4_mapped() } else { None } @@ -174,4 +181,16 @@ mod tests { ))) ); } + + #[test] + fn test_get_ip_from_sockaddr_ipv6_mapped() { + let mut raw = [0u8; 28]; + raw[0] = 23; // AF_INET6 + raw[1] = 0; + raw[8..24].copy_from_slice(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 1, 2, 3, 4, + ]); + let addr_ptr = raw.as_ptr() as *const core::ffi::c_void; + assert_eq!(get_ip_from_sockaddr(addr_ptr), Some(Ipv4Addr::new(1, 2, 3, 4))); + } }