feat: harden proxy_dns hooks and improve ipv6 compatibility

This commit is contained in:
tianrking committed 2026-03-20 12:19:18 +08:00
1 parent 904d6ad3e1
commit 7611cd8a58
4 files changed
+274 -72

No files matched your search

+150 -56
View File
@@ -50,7 +50,8 @@ impl HookState {
/// Global hook state
static HOOK_STATE: OnceLock<HookState> = OnceLock::new();
/// Track addrinfo/sockaddr allocations created by our getaddrinfo hook.
static CUSTOM_ADDRINFO_ALLOCATIONS: OnceLock<Mutex<HashMap<usize, usize>>> = OnceLock::new();
static CUSTOM_ADDRINFO_ALLOCATIONS: OnceLock<Mutex<HashMap<usize, CustomAddrinfoAllocation>>> =
OnceLock::new();
thread_local! {
static GETHOSTBYNAME_STORE: RefCell<Option<Box<HostentStore>>> = const { RefCell::new(None) };
@@ -63,7 +64,13 @@ struct HostentStore {
hostent: libc::hostent,
}
fn custom_alloc_map() -> &'static Mutex<HashMap<usize, usize>> {
#[derive(Clone, Copy)]
struct CustomAddrinfoAllocation {
sockaddr_ptr: usize,
family: libc::c_int,
}
fn custom_alloc_map() -> &'static Mutex<HashMap<usize, CustomAddrinfoAllocation>> {
CUSTOM_ADDRINFO_ALLOCATIONS.get_or_init(|| Mutex::new(HashMap::new()))
}
@@ -93,6 +100,106 @@ unsafe fn write_hostname_to_buf(host: *mut c_char, hostlen: libc::socklen_t, hos
0
}
unsafe fn store_fake_addrinfo_result(
fake_ip: std::net::Ipv4Addr,
service_port: u16,
hints: *const libc::addrinfo,
requested_family: libc::c_int,
res: *mut *mut libc::addrinfo,
) -> c_int {
match requested_family {
libc::AF_INET => {
let mut sockaddr: libc::sockaddr_in = std::mem::zeroed();
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
{
sockaddr.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
}
sockaddr.sin_family = libc::AF_INET as libc::sa_family_t;
sockaddr.sin_port = service_port.to_be();
sockaddr.sin_addr = libc::in_addr {
s_addr: u32::from_ne_bytes(fake_ip.octets()),
};
let sockaddr_ptr = Box::into_raw(Box::new(sockaddr));
let mut ai: libc::addrinfo = std::mem::zeroed();
ai.ai_flags = if hints.is_null() { 0 } else { (*hints).ai_flags };
ai.ai_family = libc::AF_INET;
ai.ai_socktype = if hints.is_null() {
libc::SOCK_STREAM
} else {
(*hints).ai_socktype
};
ai.ai_protocol = if hints.is_null() {
libc::IPPROTO_TCP
} else {
(*hints).ai_protocol
};
ai.ai_addrlen = std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
ai.ai_addr = sockaddr_ptr as *mut libc::sockaddr;
ai.ai_canonname = std::ptr::null_mut();
ai.ai_next = std::ptr::null_mut();
let ai_ptr = Box::into_raw(Box::new(ai));
*res = ai_ptr;
custom_alloc_map().lock().insert(
ai_ptr as usize,
CustomAddrinfoAllocation {
sockaddr_ptr: sockaddr_ptr as usize,
family: libc::AF_INET,
},
);
0
}
libc::AF_INET6 => {
let mut mapped = [0u8; 16];
mapped[10] = 0xff;
mapped[11] = 0xff;
mapped[12..16].copy_from_slice(&fake_ip.octets());
let mut sockaddr: libc::sockaddr_in6 = std::mem::zeroed();
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
{
sockaddr.sin6_len = std::mem::size_of::<libc::sockaddr_in6>() as u8;
}
sockaddr.sin6_family = libc::AF_INET6 as libc::sa_family_t;
sockaddr.sin6_port = service_port.to_be();
sockaddr.sin6_addr = libc::in6_addr { s6_addr: mapped };
sockaddr.sin6_scope_id = 0;
let sockaddr_ptr = Box::into_raw(Box::new(sockaddr));
let mut ai: libc::addrinfo = std::mem::zeroed();
ai.ai_flags = if hints.is_null() { 0 } else { (*hints).ai_flags };
ai.ai_family = libc::AF_INET6;
ai.ai_socktype = if hints.is_null() {
libc::SOCK_STREAM
} else {
(*hints).ai_socktype
};
ai.ai_protocol = if hints.is_null() {
libc::IPPROTO_TCP
} else {
(*hints).ai_protocol
};
ai.ai_addrlen = std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t;
ai.ai_addr = sockaddr_ptr as *mut libc::sockaddr;
ai.ai_canonname = std::ptr::null_mut();
ai.ai_next = std::ptr::null_mut();
let ai_ptr = Box::into_raw(Box::new(ai));
*res = ai_ptr;
custom_alloc_map().lock().insert(
ai_ptr as usize,
CustomAddrinfoAllocation {
sockaddr_ptr: sockaddr_ptr as usize,
family: libc::AF_INET6,
},
);
0
}
_ => libc::EAI_FAMILY,
}
}
/// Initialize the hook library
pub fn init_hooks(config: Config) -> Result<()> {
// Initialize original functions first
@@ -161,12 +268,24 @@ pub unsafe fn hook_connect(
}
// Apply DNAT if configured
let (final_ip, final_port) = state.config.apply_dnat_ip(&target_ip, target_port);
let (dnat_ip, final_port) = state.config.apply_dnat_ip(&target_ip, target_port);
// Check if this is a fake IP (needs remote DNS resolution)
let target_domain = match final_ip {
IpAddr::V4(v4) if is_fake_ip(&v4) => state.dns_resolver.get_hostname(&v4),
_ => None,
// Check if this is a fake IP (needs remote DNS resolution). For IPv6-mapped
// fake addresses, collapse to IPv4 so SOCKS4a path can still use the domain.
let (final_ip, target_domain) = match dnat_ip {
IpAddr::V4(v4) if is_fake_ip(&v4) => (IpAddr::V4(v4), state.dns_resolver.get_hostname(&v4)),
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
if is_fake_ip(&v4) {
(IpAddr::V4(v4), state.dns_resolver.get_hostname(&v4))
} else {
(IpAddr::V6(v6), None)
}
} else {
(IpAddr::V6(v6), None)
}
}
_ => (dnat_ip, None),
};
// Check if proxy DNS is enabled and we have a domain
@@ -265,65 +384,36 @@ pub unsafe fn hook_getaddrinfo(
return original_getaddrinfo(node, service, hints, res);
}
// Preserve behavior for literal IPv4 strings.
if hostname.parse::<std::net::Ipv4Addr>().is_ok() {
// Preserve behavior for literal IP strings.
if hostname.parse::<std::net::IpAddr>().is_ok() {
return original_getaddrinfo(node, service, hints, res);
}
// Respect non-IPv4 family hints.
if !hints.is_null() {
let ai_family = (*hints).ai_family;
if ai_family != libc::AF_INET && ai_family != libc::AF_UNSPEC {
return original_getaddrinfo(node, service, hints, res);
let requested_family = if hints.is_null() {
libc::AF_INET
} else {
match (*hints).ai_family {
libc::AF_UNSPEC => libc::AF_INET,
libc::AF_INET => libc::AF_INET,
libc::AF_INET6 => libc::AF_INET6,
_ => return libc::EAI_FAMILY,
}
}
};
// Generate fake IP
let fake_ip = match state.dns_resolver.resolve(&hostname) {
Ok(ip) => ip,
Err(e) => {
error!("Failed to resolve {} in hook_getaddrinfo: {}", hostname, e);
return original_getaddrinfo(node, service, hints, res);
return libc::EAI_NONAME;
}
};
let service_port = parse_service_port(service);
let mut sockaddr: libc::sockaddr_in = std::mem::zeroed();
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
{
sockaddr.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
let ret = store_fake_addrinfo_result(fake_ip, service_port, hints, requested_family, res);
if ret != 0 {
return ret;
}
sockaddr.sin_family = libc::AF_INET as libc::sa_family_t;
sockaddr.sin_port = service_port.to_be();
sockaddr.sin_addr = libc::in_addr {
s_addr: u32::from_ne_bytes(fake_ip.octets()),
};
let sockaddr_ptr = Box::into_raw(Box::new(sockaddr));
let mut ai: libc::addrinfo = std::mem::zeroed();
ai.ai_flags = if hints.is_null() { 0 } else { (*hints).ai_flags };
ai.ai_family = libc::AF_INET;
ai.ai_socktype = if hints.is_null() {
libc::SOCK_STREAM
} else {
(*hints).ai_socktype
};
ai.ai_protocol = if hints.is_null() {
libc::IPPROTO_TCP
} else {
(*hints).ai_protocol
};
ai.ai_addrlen = std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
ai.ai_addr = sockaddr_ptr as *mut libc::sockaddr;
ai.ai_canonname = std::ptr::null_mut();
ai.ai_next = std::ptr::null_mut();
let ai_ptr = Box::into_raw(Box::new(ai));
*res = ai_ptr;
custom_alloc_map()
.lock()
.insert(ai_ptr as usize, sockaddr_ptr as usize);
debug!("Assigned fake IP {} for {}", fake_ip, hostname);
@@ -361,8 +451,8 @@ pub unsafe fn hook_gethostbyname(name: *const c_char) -> *mut libc::hostent {
return original_gethostbyname(name);
}
// Preserve behavior for literal IPv4 strings.
if hostname.parse::<std::net::Ipv4Addr>().is_ok() {
// Preserve behavior for literal IP strings.
if hostname.parse::<std::net::IpAddr>().is_ok() {
return original_gethostbyname(name);
}
@@ -371,7 +461,7 @@ pub unsafe fn hook_gethostbyname(name: *const c_char) -> *mut libc::hostent {
Ok(ip) => ip,
Err(e) => {
error!("Failed to resolve {} in hook_gethostbyname: {}", hostname, e);
return original_gethostbyname(name);
return std::ptr::null_mut();
}
};
@@ -415,8 +505,12 @@ pub unsafe fn hook_freeaddrinfo(res: *mut libc::addrinfo) {
return;
}
if let Some(sockaddr_ptr) = custom_alloc_map().lock().remove(&(res as usize)) {
let _ = Box::from_raw(sockaddr_ptr as *mut libc::sockaddr_in);
if let Some(alloc) = custom_alloc_map().lock().remove(&(res as usize)) {
if alloc.family == libc::AF_INET6 {
let _ = Box::from_raw(alloc.sockaddr_ptr as *mut libc::sockaddr_in6);
} else {
let _ = Box::from_raw(alloc.sockaddr_ptr as *mut libc::sockaddr_in);
}
let _ = Box::from_raw(res);
return;
}
+73 -15
View File
@@ -4,7 +4,7 @@
//! chain on the original socket handle.
use std::collections::HashMap;
use std::ffi::{c_void, CStr};
use std::ffi::{c_void, CStr, CString};
use std::mem::{self, ManuallyDrop};
use std::net::{IpAddr, Ipv4Addr};
use std::os::windows::io::FromRawSocket;
@@ -17,13 +17,14 @@ use tracing::{debug, error, info, warn};
use windows::Win32::Networking::WinSock::{
ADDRINFOA, AF_INET, IN_ADDR, IN_ADDR_0, IPPROTO_TCP, SOCKADDR, SOCKADDR_IN, SOCK_STREAM,
SOCKET_ERROR, WSAEALREADY, WSAECONNREFUSED, WSAEFAULT, WSAEINPROGRESS, WSAEINVAL,
WSAHOST_NOT_FOUND,
WSAEWOULDBLOCK, WSAGetLastError, WSASetLastError,
};
use crate::config::{ChainType, Config, ProxyData, ProxyState};
use crate::dns::{is_fake_ip, DnsResolver};
use crate::error::{Error, Result};
use crate::net::{get_ip_from_sockaddr, get_port_from_sockaddr};
use crate::net::{get_ip_from_sockaddr, get_ipaddr_from_sockaddr, get_port_from_sockaddr};
use crate::proxy::{tunnel_through_proxy, TargetAddress};
use super::interpose_windows::{
@@ -253,27 +254,37 @@ pub unsafe extern "system" fn hook_connect_impl(
return original_connect(sock, addr, len);
}
let target_ip = match get_ip_from_sockaddr(addr) {
let target_ip = match get_ipaddr_from_sockaddr(addr) {
Some(ip) => ip,
None => return original_connect(sock, addr, len),
};
let target_port = get_port_from_sockaddr(addr);
if state.config.should_bypass(&target_ip) {
if state.config.should_bypass_ip(&target_ip) {
return original_connect(sock, addr, len);
}
let (final_ip, final_port) = state.config.apply_dnat(&target_ip, target_port);
let target_domain = if is_fake_ip(&final_ip) {
state.dns_resolver.get_hostname(&final_ip)
} else {
None
let (dnat_ip, final_port) = state.config.apply_dnat_ip(&target_ip, target_port);
let (final_ip, target_domain) = match dnat_ip {
IpAddr::V4(v4) if is_fake_ip(&v4) => (IpAddr::V4(v4), state.dns_resolver.get_hostname(&v4)),
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
if is_fake_ip(&v4) {
(IpAddr::V4(v4), state.dns_resolver.get_hostname(&v4))
} else {
(IpAddr::V6(v6), None)
}
} else {
(IpAddr::V6(v6), None)
}
}
_ => (dnat_ip, None),
};
let target = if let Some(domain) = target_domain {
TargetAddress::from_both(IpAddr::V4(final_ip), domain)
TargetAddress::from_both(final_ip, domain)
} else {
TargetAddress::from_ip(IpAddr::V4(final_ip))
TargetAddress::from_ip(final_ip)
};
let max_attempts = state.config.max_chain_retries.max(1);
@@ -340,7 +351,7 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl(
if !phints.is_null() {
let hints = &*(phints as *const ADDRINFOA);
if hints.ai_family != 0 && hints.ai_family != AF_INET.0 as i32 {
return original_getaddrinfo(pnode, pservice, phints, ppresult);
return WSAHOST_NOT_FOUND.0;
}
}
@@ -349,7 +360,7 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl(
Err(_) => return original_getaddrinfo(pnode, pservice, phints, ppresult),
};
if hostname.parse::<Ipv4Addr>().is_ok() {
if hostname.parse::<IpAddr>().is_ok() {
return original_getaddrinfo(pnode, pservice, phints, ppresult);
}
@@ -361,7 +372,7 @@ pub unsafe extern "system" fn hook_getaddrinfo_impl(
Ok(ip) => ip,
Err(e) => {
error!("Failed to resolve {} in hook_getaddrinfo_impl: {}", hostname, e);
return original_getaddrinfo(pnode, pservice, phints, ppresult);
return WSAHOST_NOT_FOUND.0;
}
};
@@ -414,7 +425,54 @@ pub unsafe extern "system" fn hook_freeaddrinfo_impl(pres: *mut c_void) {
/// Windows gethostbyname hook implementation.
#[cfg(windows)]
pub unsafe extern "system" fn hook_gethostbyname_impl(name: *const i8) -> *mut c_void {
original_gethostbyname(name)
let state = match get_hook_state() {
Some(s) if s.config.proxy_dns => s,
_ => return original_gethostbyname(name),
};
if name.is_null() {
WSASetLastError(WSAEINVAL.0);
return std::ptr::null_mut();
}
let hostname = match CStr::from_ptr(name).to_str() {
Ok(s) => s,
Err(_) => {
WSASetLastError(WSAEINVAL.0);
return std::ptr::null_mut();
}
};
if hostname.parse::<IpAddr>().is_ok() {
return original_gethostbyname(name);
}
if crate::dns::lookup_in_hosts(hostname).is_some() {
return original_gethostbyname(name);
}
let fake_ip = match state.dns_resolver.resolve(hostname) {
Ok(ip) => ip,
Err(e) => {
error!("Failed to resolve {} in hook_gethostbyname_impl: {}", hostname, e);
WSASetLastError(WSAHOST_NOT_FOUND.0);
return std::ptr::null_mut();
}
};
let fake_ip_cstr = match CString::new(fake_ip.to_string()) {
Ok(v) => v,
Err(_) => {
WSASetLastError(WSAHOST_NOT_FOUND.0);
return std::ptr::null_mut();
}
};
let result = original_gethostbyname(fake_ip_cstr.as_ptr());
if result.is_null() {
WSASetLastError(WSAHOST_NOT_FOUND.0);
}
result
}
/// Windows getnameinfo hook implementation.
+49 -1
View File
@@ -3,7 +3,7 @@
//! This module provides Windows-specific implementations for socket operations
//! using the Winsock2 API.
use std::net::Ipv4Addr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
/// Get the port from a SOCKADDR structure (Windows-specific)
#[cfg(windows)]
@@ -18,6 +18,10 @@ pub fn get_port_from_sockaddr(addr: *const core::ffi::c_void) -> u16 {
// sin_port: u16 (2 bytes) - network byte order
let port_ptr = (addr as *const u8).add(2) as *const u16;
u16::from_be(*port_ptr)
} else if sa_family == 23 {
// SOCKADDR_IN6 has the same port offset.
let port_ptr = (addr as *const u8).add(2) as *const u16;
u16::from_be(*port_ptr)
} else {
0
}
@@ -49,6 +53,28 @@ pub fn get_ip_from_sockaddr(addr: *const core::ffi::c_void) -> Option<Ipv4Addr>
}
}
/// Get the IP address (IPv4/IPv6) from a SOCKADDR structure.
#[cfg(windows)]
pub fn get_ipaddr_from_sockaddr(addr: *const core::ffi::c_void) -> Option<IpAddr> {
unsafe {
let sa_family = *(addr as *const u16);
if sa_family == 2 {
let ip_ptr = (addr as *const u8).add(4);
let ip_bytes = [*ip_ptr, *ip_ptr.add(1), *ip_ptr.add(2), *ip_ptr.add(3)];
Some(IpAddr::V4(Ipv4Addr::from(ip_bytes)))
} else if sa_family == 23 {
// SOCKADDR_IN6 layout:
// sin6_family (2), sin6_port (2), sin6_flowinfo (4), sin6_addr (16), sin6_scope_id (4)
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);
Some(IpAddr::V6(Ipv6Addr::from(octets)))
} else {
None
}
}
}
/// Create a TCP socket (Windows-specific)
pub fn create_tcp_socket() -> std::io::Result<std::net::TcpStream> {
use socket2::{Domain, Protocol, Socket, Type};
@@ -126,4 +152,26 @@ mod tests {
assert!(is_localhost(&Ipv4Addr::new(127, 0, 0, 1)));
assert!(!is_localhost(&Ipv4Addr::new(192, 168, 1, 1)));
}
#[test]
fn test_parse_ipv6_sockaddr_layout() {
// Build a minimal SOCKADDR_IN6 byte layout.
let mut raw = [0u8; 28];
raw[0] = 23; // AF_INET6 low byte
raw[1] = 0;
raw[2] = 0x13; // port 4919 in network byte order
raw[3] = 0x37;
raw[8..24].copy_from_slice(&[
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]);
let addr_ptr = raw.as_ptr() as *const core::ffi::c_void;
assert_eq!(get_port_from_sockaddr(addr_ptr), 4919);
assert_eq!(
get_ipaddr_from_sockaddr(addr_ptr),
Some(IpAddr::V6(Ipv6Addr::new(
0x2001, 0x0db8, 0, 0, 0, 0, 0, 1
)))
);
}
}