feat(windows): add connectex-overlapped and dns api hardening with ci e2e

This commit is contained in:
tianrking committed 2026-03-20 18:03:05 +08:00
1 parent 04a548ca23
commit 38806cbbb1
7 files changed
+406 -4

No files matched your search

+8
View File
@@ -108,6 +108,14 @@ jobs:
cargo run -p proxychains-bin -- -f ./ci.e2e.conf --probe --probe-json > probe-report.json
grep -q "\"summary\"" probe-report.json
DNS_V4="$(cargo run -q -p proxychains-bin -- -q -f ./ci.e2e.conf python3 -c "import socket;print(socket.getaddrinfo(\"dns-proxychains-e2e.invalid\",443,0,socket.SOCK_STREAM)[0][4][0])" | tr -d "\r\n")"
echo "dns v4 fake ip: ${DNS_V4}"
[[ "${DNS_V4}" == 224.* ]]
DNS_V6="$(cargo run -q -p proxychains-bin -- -q -f ./ci.e2e.conf python3 -c "import socket;print(socket.getaddrinfo(\"dns-proxychains-e2e.invalid\",443,socket.AF_INET6,socket.SOCK_STREAM)[0][4][0])" | tr -d "\r\n")"
echo "dns v6 fake ip: ${DNS_V6}"
[[ "${DNS_V6}" == ::ffff:224.* ]]
PROXYCHAINS_TEST_SOCKS5_ADDR=127.0.0.1:19080 \
PROXYCHAINS_TEST_HTTP_ADDR=127.0.0.1:18080 \
cargo test -p proxychains --test real_backends -- --nocapture
+3
View File
@@ -88,6 +88,9 @@ proxychains4 curl https://ifconfig.me
proxychains4.exe curl https://ifconfig.me
```
Forced validation manual:
- [docs/forced-proxy-validation.md](docs/forced-proxy-validation.md)
Validate config and groups:
```bash
+3
View File
@@ -88,6 +88,9 @@ proxychains4 curl https://ifconfig.me
proxychains4.exe curl https://ifconfig.me
```
强制代理验证手册:
- [docs/forced-proxy-validation.md](docs/forced-proxy-validation.md)
查看分组/校验配置:
```bash
+87
View File
@@ -0,0 +1,87 @@
# Forced Proxy Validation Manual
This manual provides a fail-closed validation flow:
- direct path must show your direct egress IP;
- valid proxy must show a different proxy egress IP;
- invalid proxy must fail (must not silently fall back to direct).
## 1) Prepare a working local proxy
Example (replace with your own proxy endpoint):
- SOCKS5 at `127.0.0.1:5091`
- HTTP at `127.0.0.1:5098`
## 2) Create test configs
Good config (`proxy-good.conf`):
```ini
strict_chain
proxy_dns
remote_dns_subnet 224
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
socks5 127.0.0.1 5091
```
Bad config (`proxy-bad.conf`):
```ini
strict_chain
proxy_dns
tcp_read_time_out 5000
tcp_connect_time_out 2000
[ProxyList]
socks5 127.0.0.1 5999
```
## 3) One-shot verification commands
### Windows (PowerShell)
```powershell
curl.exe --max-time 15 https://ifconfig.me
.\proxychains4.exe -f .\proxy-good.conf -v curl --max-time 15 https://ifconfig.me
.\proxychains4.exe -f .\proxy-bad.conf -v curl --max-time 10 https://ifconfig.me; $LASTEXITCODE
```
Expected:
- line 1 (direct) and line 2 (proxy) should be different IPs;
- line 3 should fail and return non-zero exit code (for curl usually `7`).
### Linux/macOS
```bash
curl --max-time 15 https://ifconfig.me
./proxychains4 -f ./proxy-good.conf -v curl --max-time 15 https://ifconfig.me
./proxychains4 -f ./proxy-bad.conf -v curl --max-time 10 https://ifconfig.me; echo $?
```
Expected:
- direct IP != proxied IP
- bad proxy run exits non-zero
## 4) DNS leak protection quick check
When `proxy_dns` is enabled, hostname resolution inside proxied process should be fake-IP mapped.
Linux/macOS:
```bash
./proxychains4 -q -f ./proxy-good.conf python3 -c "import socket;print(socket.getaddrinfo('dns-proxychains-check.invalid',443,0,socket.SOCK_STREAM)[0][4][0])"
./proxychains4 -q -f ./proxy-good.conf python3 -c "import socket;print(socket.getaddrinfo('dns-proxychains-check.invalid',443,socket.AF_INET6,socket.SOCK_STREAM)[0][4][0])"
```
Expected:
- IPv4 output starts with `224.`
- IPv6 output starts with `::ffff:224.`
## 5) Troubleshooting
- `No proxies configured`: verify `[ProxyList]` and `-f` path.
- Direct/proxy IP same: proxy endpoint may share same egress as direct path; run bad-proxy test to verify fail-closed behavior.
- Windows: keep `proxychains4.exe` and `proxychains.dll` in same directory.
+1
View File
@@ -31,6 +31,7 @@ tracing-subscriber.workspace = true
windows = { version = "0.54", features = [
"Win32_Foundation",
"Win32_Networking_WinSock",
"Win32_System_IO",
"Win32_System_Threading",
"Win32_System_Memory",
"Win32_System_LibraryLoader",
+171 -3
View File
@@ -22,6 +22,8 @@ use windows::Win32::Networking::WinSock::{
WSASetLastError,
};
use windows::core::GUID;
use windows::Win32::System::IO::OVERLAPPED;
use windows::Win32::System::Threading::SetEvent;
use crate::config::{ChainType, Config, ProxyData, ProxyState};
use crate::dns::{is_fake_ip, DnsResolver};
@@ -32,7 +34,8 @@ use crate::ConfigParser;
use super::interpose_windows::{
init_original_functions, original_connect, original_freeaddrinfo, original_getaddrinfo,
original_getaddrinfow, original_gethostbyname, original_getnameinfo, original_wsa_ioctl,
original_getaddrinfoexw, original_getaddrinfow, original_gethostbyname, original_getnameinfo,
original_dns_query_a, original_dns_query_w, original_wsa_ioctl,
};
use super::reload::config_reload_interval;
@@ -461,7 +464,6 @@ const WSAID_CONNECTEX: GUID = GUID::from_u128(0x25a207b9_ddf3_4660_8ee9_76e58c74
/// 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)]
@@ -474,7 +476,7 @@ pub unsafe extern "system" fn hook_connect_ex_impl(
bytes_sent: *mut u32,
overlapped: *mut c_void,
) -> i32 {
if !overlapped.is_null() || (!send_buf.is_null() && send_len > 0) {
if !send_buf.is_null() && send_len > 0 {
WSASetLastError(WSAEOPNOTSUPP.0);
return 0;
}
@@ -487,6 +489,12 @@ pub unsafe extern "system" fn hook_connect_ex_impl(
if ret == SOCKET_ERROR {
0
} else {
if !overlapped.is_null() {
let ov = &mut *(overlapped as *mut OVERLAPPED);
if !ov.hEvent.is_invalid() {
let _ = SetEvent(ov.hEvent);
}
}
1
}
}
@@ -759,6 +767,166 @@ pub unsafe extern "system" fn hook_getaddrinfow_impl(
0
}
/// Windows GetAddrInfoExW hook implementation.
#[cfg(windows)]
pub unsafe extern "system" fn hook_getaddrinfoexw_impl(
pname: *const u16,
pservice: *const u16,
namespace: u32,
pnspid: *mut c_void,
hints: *const c_void,
ppresult: *mut *mut c_void,
timeout: *mut c_void,
overlapped: *mut c_void,
completion_routine: *mut c_void,
pname_handle: *mut c_void,
) -> i32 {
let state = match get_hook_state() {
Some(s) => s,
None => {
return original_getaddrinfoexw(
pname,
pservice,
namespace,
pnspid,
hints,
ppresult,
timeout,
overlapped,
completion_routine,
pname_handle,
)
}
};
maybe_reload_config(state);
let config = state.config.lock().clone();
if !config.proxy_dns {
return original_getaddrinfoexw(
pname,
pservice,
namespace,
pnspid,
hints,
ppresult,
timeout,
overlapped,
completion_routine,
pname_handle,
);
}
let dns_resolver = DnsResolver::new(config.proxy_dns, config.remote_dns_subnet);
if pname.is_null() {
return WSAEINVAL.0;
}
let hostname = match parse_wide_string(pname) {
Ok(s) => s,
Err(_) => return WSAEINVAL.0,
};
if hostname.parse::<IpAddr>().is_ok() || crate::dns::lookup_in_hosts(&hostname).is_some() {
return original_getaddrinfoexw(
pname,
pservice,
namespace,
pnspid,
hints,
ppresult,
timeout,
overlapped,
completion_routine,
pname_handle,
);
}
let fake_ip = match dns_resolver.resolve(&hostname) {
Ok(ip) => ip,
Err(_) => return WSAHOST_NOT_FOUND.0,
};
let fake_wide: Vec<u16> = fake_ip
.to_string()
.encode_utf16()
.chain(std::iter::once(0))
.collect();
original_getaddrinfoexw(
fake_wide.as_ptr(),
pservice,
namespace,
pnspid,
hints,
ppresult,
timeout,
overlapped,
completion_routine,
pname_handle,
)
}
const DNS_ERROR_RCODE_NAME_ERROR: i32 = 9003;
/// Windows DnsQuery_A hook implementation.
#[cfg(windows)]
pub unsafe extern "system" fn hook_dns_query_a_impl(
name: *const i8,
query_type: u16,
options: u32,
extra: *mut c_void,
result: *mut *mut c_void,
reserved: *mut c_void,
) -> i32 {
let state = match get_hook_state() {
Some(s) => s,
None => return original_dns_query_a(name, query_type, options, extra, result, reserved),
};
maybe_reload_config(state);
let config = state.config.lock().clone();
if !config.proxy_dns || name.is_null() {
return original_dns_query_a(name, query_type, options, extra, result, reserved);
}
let hostname = match CStr::from_ptr(name).to_str() {
Ok(s) => s,
Err(_) => return DNS_ERROR_RCODE_NAME_ERROR,
};
if hostname.parse::<IpAddr>().is_ok() || crate::dns::lookup_in_hosts(hostname).is_some() {
return original_dns_query_a(name, query_type, options, extra, result, reserved);
}
DNS_ERROR_RCODE_NAME_ERROR
}
/// Windows DnsQuery_W hook implementation.
#[cfg(windows)]
pub unsafe extern "system" fn hook_dns_query_w_impl(
name: *const u16,
query_type: u16,
options: u32,
extra: *mut c_void,
result: *mut *mut c_void,
reserved: *mut c_void,
) -> i32 {
let state = match get_hook_state() {
Some(s) => s,
None => return original_dns_query_w(name, query_type, options, extra, result, reserved),
};
maybe_reload_config(state);
let config = state.config.lock().clone();
if !config.proxy_dns || name.is_null() {
return original_dns_query_w(name, query_type, options, extra, result, reserved);
}
let hostname = match parse_wide_string(name) {
Ok(s) => s,
Err(_) => return DNS_ERROR_RCODE_NAME_ERROR,
};
if hostname.parse::<IpAddr>().is_ok() || crate::dns::lookup_in_hosts(&hostname).is_some() {
return original_dns_query_w(name, query_type, options, extra, result, reserved);
}
DNS_ERROR_RCODE_NAME_ERROR
}
/// Windows freeaddrinfo hook implementation.
#[cfg(windows)]
pub unsafe extern "system" fn hook_freeaddrinfo_impl(pres: *mut c_void) {
+133 -1
View File
@@ -15,7 +15,8 @@ use crate::error::{Error, Result};
use super::hooks_windows::{
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,
hook_getaddrinfoexw_impl, hook_gethostbyname_impl, hook_getnameinfo_impl,
hook_dns_query_a_impl, hook_dns_query_w_impl, hook_wsa_connect_impl, hook_wsa_ioctl_impl,
};
type ConnectFn = unsafe extern "system" fn(usize, *const c_void, i32) -> i32;
@@ -32,6 +33,18 @@ 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 GetAddrInfoExWFn = unsafe extern "system" fn(
*const u16,
*const u16,
u32,
*mut c_void,
*const c_void,
*mut *mut c_void,
*mut c_void,
*mut c_void,
*mut c_void,
*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 =
@@ -47,15 +60,34 @@ type WsaIoctlFn = unsafe extern "system" fn(
*mut c_void,
*mut c_void,
) -> i32;
type DnsQueryAFn = unsafe extern "system" fn(
*const i8,
u16,
u32,
*mut c_void,
*mut *mut c_void,
*mut c_void,
) -> i32;
type DnsQueryWFn = unsafe extern "system" fn(
*const u16,
u16,
u32,
*mut c_void,
*mut *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_GETADDRINFOEXW: OnceLock<GetAddrInfoExWFn> = 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 ORIGINAL_DNS_QUERY_A: OnceLock<DnsQueryAFn> = OnceLock::new();
static ORIGINAL_DNS_QUERY_W: OnceLock<DnsQueryWFn> = OnceLock::new();
static HOOKS_READY: AtomicBool = AtomicBool::new(false);
@@ -69,6 +101,16 @@ unsafe fn install_api_hook<F>(proc_name: &str, detour: *mut c_void) -> Result<F>
Ok(std::mem::transmute_copy::<*mut c_void, F>(&trampoline))
}
unsafe fn install_api_hook_from_module<F>(
module: &str,
proc_name: &str,
detour: *mut c_void,
) -> Result<F> {
let trampoline = MinHook::create_hook_api(module, proc_name, detour)
.map_err(|s| mh_to_error("create_hook_api", s))?;
Ok(std::mem::transmute_copy::<*mut c_void, F>(&trampoline))
}
/// Store for original function pointers.
pub struct OriginalFunctions;
@@ -114,6 +156,32 @@ impl OriginalFunctions {
let _ = ORIGINAL_FREEADDRINFO.set(freeaddrinfo_fn);
let _ = ORIGINAL_GETHOSTBYNAME.set(gethostbyname_fn);
let _ = ORIGINAL_GETNAMEINFO.set(getnameinfo_fn);
if let Ok(getaddrinfoexw_fn) = install_api_hook(
"GetAddrInfoExW",
hook_getaddrinfoexw_impl as *const () as *mut c_void,
) {
let _ = ORIGINAL_GETADDRINFOEXW.set(getaddrinfoexw_fn);
} else {
debug!("GetAddrInfoExW hook not installed (symbol unavailable)");
}
if let Ok(dns_query_a_fn) = install_api_hook_from_module(
"dnsapi.dll",
"DnsQuery_A",
hook_dns_query_a_impl as *const () as *mut c_void,
) {
let _ = ORIGINAL_DNS_QUERY_A.set(dns_query_a_fn);
} else {
debug!("DnsQuery_A hook not installed (dnsapi.dll unavailable)");
}
if let Ok(dns_query_w_fn) = install_api_hook_from_module(
"dnsapi.dll",
"DnsQuery_W",
hook_dns_query_w_impl as *const () as *mut c_void,
) {
let _ = ORIGINAL_DNS_QUERY_W.set(dns_query_w_fn);
} else {
debug!("DnsQuery_W hook not installed (dnsapi.dll unavailable)");
}
MinHook::enable_all_hooks().map_err(|s| mh_to_error("enable_all_hooks", s))?;
}
@@ -237,6 +305,38 @@ pub unsafe fn original_getaddrinfow(
}
}
/// Call the original GetAddrInfoExW function.
pub unsafe fn original_getaddrinfoexw(
node: *const u16,
service: *const u16,
namespace: u32,
nspid: *mut c_void,
hints: *const c_void,
res: *mut *mut c_void,
timeout: *mut c_void,
overlapped: *mut c_void,
completion_routine: *mut c_void,
name_handle: *mut c_void,
) -> i32 {
if let Some(f) = ORIGINAL_GETADDRINFOEXW.get() {
f(
node,
service,
namespace,
nspid,
hints,
res,
timeout,
overlapped,
completion_routine,
name_handle,
)
} 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() {
@@ -270,6 +370,38 @@ pub unsafe fn original_getnameinfo(
}
}
/// Call the original DnsQuery_A function.
pub unsafe fn original_dns_query_a(
name: *const i8,
query_type: u16,
options: u32,
extra: *mut c_void,
result: *mut *mut c_void,
reserved: *mut c_void,
) -> i32 {
if let Some(f) = ORIGINAL_DNS_QUERY_A.get() {
f(name, query_type, options, extra, result, reserved)
} else {
9003
}
}
/// Call the original DnsQuery_W function.
pub unsafe fn original_dns_query_w(
name: *const u16,
query_type: u16,
options: u32,
extra: *mut c_void,
result: *mut *mut c_void,
reserved: *mut c_void,
) -> i32 {
if let Some(f) = ORIGINAL_DNS_QUERY_W.get() {
f(name, query_type, options, extra, result, reserved)
} else {
9003
}
}
#[cfg(test)]
mod tests {
use super::*;