feat(core): tighten windows proxy enforcement and dns/ipv6 handling

This commit is contained in:
tianrking committed 2026-03-20 17:08:43 +08:00
1 parent c452b0ea42
commit 04a548ca23
6 files changed
+389 -14

No files matched your search

+30 -2
View File
@@ -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
))));
}
}
+3 -3
View File
@@ -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| {
+245 -7
View File
@@ -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<Mutex<HashMap<usize, CustomAddrinfo
struct CustomAddrinfoAllocation {
sockaddr_ptr: usize,
family: i32,
is_wide: bool,
}
fn custom_alloc_map() -> &'static Mutex<HashMap<usize, CustomAddrinfoAllocation>> {
@@ -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::<u16>().unwrap_or(0)
}
}
fn parse_wide_string(ptr: *const u16) -> std::result::Result<String, ()> {
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::<GUID>() 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::<IpAddr>().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::<IpAddr>().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::<SOCKADDR_IN>(),
)
};
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;
}
+70 -2
View File
@@ -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<ConnectFn> = OnceLock::new();
static ORIGINAL_WSA_CONNECT: OnceLock<WsaConnectFn> = OnceLock::new();
static ORIGINAL_GETADDRINFO: OnceLock<GetAddrInfoFn> = OnceLock::new();
static ORIGINAL_GETADDRINFOW: OnceLock<GetAddrInfoWFn> = OnceLock::new();
static ORIGINAL_FREEADDRINFO: OnceLock<FreeAddrInfoFn> = OnceLock::new();
static ORIGINAL_GETHOSTBYNAME: OnceLock<GetHostByNameFn> = OnceLock::new();
static ORIGINAL_GETNAMEINFO: OnceLock<GetNameInfoFn> = OnceLock::new();
static ORIGINAL_WSA_IOCTL: OnceLock<WsaIoctlFn> = 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() {
+22
View File
@@ -152,6 +152,10 @@ pub fn get_ip_from_sockaddr(addr: *const libc::sockaddr) -> Option<Ipv4Addr> {
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::<libc::sockaddr_in6>() 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)));
}
}
+19
View File
@@ -47,6 +47,13 @@ pub fn get_ip_from_sockaddr(addr: *const core::ffi::c_void) -> Option<Ipv4Addr>
*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)));
}
}