feat(tree): add cross-platform --tree mode with windows descendant injection

This commit is contained in:
tianrking committed 2026-03-20 18:58:38 +08:00
1 parent f72cf87b3a
commit 1c94b83014
5 files changed
+203 -3

No files matched your search

+4
View File
@@ -37,6 +37,9 @@ Recommendation:
- Proxy protocols: `socks5`, `socks4`/`socks4a`, `http`, `raw`
- Chain modes: `dynamic_chain`, `strict_chain`, `random_chain`, `load_balance`, `failover`
- Proxy groups: `[ProxyList:<group>]` + `--group`
- Process tree mode: `--tree`
- Windows: root process + child/grandchild process injection
- Linux/macOS: inherited preload environment for child processes
- Discovery/validation:
- `--list-groups`
- `--check`
@@ -99,6 +102,7 @@ proxychains4 -f ./proxychains.conf --group jp --check
proxychains4 -f ./proxychains.conf --group jp --probe
proxychains4 -f ./proxychains.conf --probe --probe-timeout-ms 1200
proxychains4 -f ./proxychains.conf --probe --probe-json
proxychains4 -f ./proxychains.conf --tree curl https://ifconfig.me
```
## Example Config
+4
View File
@@ -37,6 +37,9 @@
- 代理协议:`socks5`、`socks4`/`socks4a`、`http`、`raw`
- 链路模式:`dynamic_chain`、`strict_chain`、`random_chain`、`load_balance`、`failover`
- 代理分组:`[ProxyList:<group>]` + `--group`
- 进程树模式:`--tree`
- Windows:根进程 + 子/孙进程注入
- Linux/macOS:通过 preload 环境变量继承覆盖子进程
- 配置发现/校验:
- `--list-groups`
- `--check`
@@ -99,6 +102,7 @@ proxychains4 -f ./proxychains.conf --group jp --check
proxychains4 -f ./proxychains.conf --group jp --probe
proxychains4 -f ./proxychains.conf --probe --probe-timeout-ms 1200
proxychains4 -f ./proxychains.conf --probe --probe-json
proxychains4 -f ./proxychains.conf --tree curl https://ifconfig.me
```
## 示例配置
+31 -3
View File
@@ -70,6 +70,10 @@ struct Args {
#[arg(long)]
probe_fail_only: bool,
/// Enable process-tree mode (inject/proxy child and grandchild processes)
#[arg(long)]
tree: bool,
/// The command to run
#[arg(
required_unless_present_any = ["list_groups", "check", "probe"],
@@ -496,6 +500,10 @@ fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
// Set proxychains environment variables
set_proxychains_env(config, args);
if args.tree {
debug!("tree mode enabled (unix): relying on environment inheritance");
}
// Build command arguments
let program = &args.command[0];
let c_args: Vec<CString> = args
@@ -618,9 +626,15 @@ fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
debug!("Spawning and injecting: {:?}", process_info);
let exit_code = injector
.spawn_inject_wait(&process_info)
.map_err(|e| format!("Failed to spawn/inject/wait: {}", e))?;
let exit_code = if args.tree {
injector
.spawn_inject_tree_wait(&process_info)
.map_err(|e| format!("Failed to spawn/inject tree/wait: {}", e))?
} else {
injector
.spawn_inject_wait(&process_info)
.map_err(|e| format!("Failed to spawn/inject/wait: {}", e))?
};
info!("Process exited with code: {}", exit_code);
@@ -704,4 +718,18 @@ mod tests {
let args = args.unwrap();
assert!(args.probe_fail_only);
}
#[test]
fn test_args_tree_without_command_fails() {
let args = Args::try_parse_from(["proxychains4", "--tree"]);
assert!(args.is_err());
}
#[test]
fn test_args_tree_with_command() {
let args = Args::try_parse_from(["proxychains4", "--tree", "curl", "https://example.com"]);
assert!(args.is_ok());
let args = args.unwrap();
assert!(args.tree);
}
}
+1
View File
@@ -18,6 +18,7 @@ tracing.workspace = true
[target.'cfg(windows)'.dependencies]
windows = { version = "0.54", features = [
"Win32_Foundation",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
"Win32_System_Memory",
"Win32_System_LibraryLoader",
+163
View File
@@ -221,6 +221,121 @@ impl ProxychainsInjector {
}
}
/// Create a suspended process, inject DLL, resume, and keep injecting descendants
/// (child/grandchild processes) until the root process exits.
#[cfg(windows)]
pub fn spawn_inject_tree_wait(&self, process_info: &ProcessInfo) -> Result<i32> {
use std::collections::HashSet;
use std::os::windows::ffi::OsStrExt;
use windows::core::{PCWSTR, PWSTR};
use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{
CreateProcessW, GetExitCodeProcess, ResumeThread, WaitForSingleObject,
CREATE_SUSPENDED, PROCESS_INFORMATION, STARTUPINFOW,
};
fn quote_arg(arg: &str) -> String {
if arg.is_empty()
|| arg.contains(' ')
|| arg.contains('\t')
|| arg.contains('"')
|| arg.contains('\\')
{
let escaped = arg.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
} else {
arg.to_string()
}
}
let mut command_line = quote_arg(&process_info.command);
for arg in &process_info.args {
command_line.push(' ');
command_line.push_str(&quote_arg(arg));
}
debug!("Spawning suspended process (tree mode): {}", command_line);
let mut cmd_wide: Vec<u16> = std::ffi::OsStr::new(&command_line)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut startup_info = STARTUPINFOW::default();
startup_info.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut process_info_win = PROCESS_INFORMATION::default();
unsafe {
CreateProcessW(
PCWSTR::null(),
PWSTR(cmd_wide.as_mut_ptr()),
None,
None,
false,
CREATE_SUSPENDED,
None,
PCWSTR::null(),
&startup_info,
&mut process_info_win,
)
.map_err(|e| InjectorError::ProcessCreationFailed(format!("{:?}", e)))?;
let process_handle: HANDLE = process_info_win.hProcess;
let thread_handle: HANDLE = process_info_win.hThread;
let root_pid = process_info_win.dwProcessId;
debug!("Suspended root process created with PID: {}", root_pid);
if let Err(e) = self.inject_dll(process_handle) {
let _ = CloseHandle(thread_handle);
let _ = CloseHandle(process_handle);
return Err(e);
}
info!("Successfully injected DLL into suspended root process {}", root_pid);
let resume_ret = ResumeThread(thread_handle);
if resume_ret == u32::MAX {
let _ = CloseHandle(thread_handle);
let _ = CloseHandle(process_handle);
return Err(InjectorError::WindowsApi("ResumeThread failed".into()));
}
let mut injected_pids: HashSet<u32> = HashSet::new();
injected_pids.insert(root_pid);
loop {
let descendants = enumerate_descendant_pids(root_pid)?;
for pid in descendants {
if injected_pids.contains(&pid) {
continue;
}
match self.inject_by_pid(pid) {
Ok(_) => {
injected_pids.insert(pid);
}
Err(e) => {
debug!("Tree-mode injection skipped for PID {}: {}", pid, e);
}
}
}
let wait_ret = WaitForSingleObject(process_handle, 150);
if wait_ret == WAIT_OBJECT_0 {
break;
}
}
let mut exit_code = 1u32;
GetExitCodeProcess(process_handle, &mut exit_code).map_err(|e| {
InjectorError::WindowsApi(format!("GetExitCodeProcess failed: {:?}", e))
})?;
let _ = CloseHandle(thread_handle);
let _ = CloseHandle(process_handle);
Ok(exit_code as i32)
}
}
/// Internal DLL injection method (Windows)
#[cfg(windows)]
unsafe fn inject_dll(&self, process: windows::Win32::Foundation::HANDLE) -> Result<()> {
@@ -322,6 +437,54 @@ impl ProxychainsInjector {
}
}
#[cfg(windows)]
fn enumerate_descendant_pids(root_pid: u32) -> Result<Vec<u32>> {
use std::collections::{HashMap, HashSet, VecDeque};
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
};
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
.map_err(|e| InjectorError::WindowsApi(format!("CreateToolhelp32Snapshot failed: {:?}", e)))?;
let mut entry = PROCESSENTRY32W::default();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
let mut parent_by_pid: HashMap<u32, u32> = HashMap::new();
if Process32FirstW(snapshot, &mut entry).is_ok() {
loop {
parent_by_pid.insert(entry.th32ProcessID, entry.th32ParentProcessID);
if Process32NextW(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
let mut seen: HashSet<u32> = HashSet::new();
let mut queue: VecDeque<u32> = VecDeque::new();
let mut descendants = Vec::new();
queue.push_back(root_pid);
seen.insert(root_pid);
while let Some(parent) = queue.pop_front() {
for (&pid, &ppid) in parent_by_pid.iter() {
if ppid == parent && seen.insert(pid) {
descendants.push(pid);
queue.push_back(pid);
}
}
}
Ok(descendants)
}
}
/// Find the proxychains DLL/library path
pub fn find_library_path() -> Result<std::path::PathBuf> {
// Try to find the library relative to the executable