509 lines
14 KiB
Rust
509 lines
14 KiB
Rust
//! Proxychains4 CLI binary
|
|
//!
|
|
//! This is the main entry point for running commands through proxy chains.
|
|
//!
|
|
//! Platform support:
|
|
//! - Unix (Linux/macOS): Uses LD_PRELOAD/DYLD_INSERT_LIBRARIES
|
|
//! - Windows: Uses DLL injection
|
|
|
|
use std::env;
|
|
use std::net::{SocketAddr, SocketAddrV4, TcpStream};
|
|
use std::path::PathBuf;
|
|
use std::process;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use clap::Parser;
|
|
use tracing::{debug, error, info, Level};
|
|
use tracing_subscriber::FmtSubscriber;
|
|
|
|
use proxychains::{Config, ConfigParser};
|
|
|
|
/// Proxychains4 - Run commands through proxy chains
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "proxychains4")]
|
|
#[command(author = "Proxychains Rust Team")]
|
|
#[command(version)]
|
|
#[command(about = "Run commands through a chain of proxies", long_about = None)]
|
|
struct Args {
|
|
/// Quiet mode - suppress output
|
|
#[arg(short = 'q', long)]
|
|
quiet: bool,
|
|
|
|
/// Configuration file path
|
|
#[arg(short = 'f', long, value_name = "FILE")]
|
|
config: Option<PathBuf>,
|
|
|
|
/// Print debug information
|
|
#[arg(short = 'v', long)]
|
|
verbose: bool,
|
|
|
|
/// Proxy group name, matches config sections like [ProxyList:<group>]
|
|
#[arg(long, value_name = "GROUP")]
|
|
group: Option<String>,
|
|
|
|
/// List available proxy groups from config and exit
|
|
#[arg(long)]
|
|
list_groups: bool,
|
|
|
|
/// Validate config and print selected proxies without running command
|
|
#[arg(long)]
|
|
check: bool,
|
|
|
|
/// Probe configured proxy nodes and print per-node reachability
|
|
#[arg(long)]
|
|
probe: bool,
|
|
|
|
/// Probe timeout in milliseconds (default: config tcp_connect_time_out)
|
|
#[arg(long, value_name = "MS")]
|
|
probe_timeout_ms: Option<u64>,
|
|
|
|
/// The command to run
|
|
#[arg(
|
|
required_unless_present_any = ["list_groups", "check", "probe"],
|
|
trailing_var_arg = true
|
|
)]
|
|
command: Vec<String>,
|
|
}
|
|
|
|
fn main() {
|
|
let args = Args::parse();
|
|
|
|
// Initialize logging
|
|
let log_level = if args.verbose {
|
|
Level::DEBUG
|
|
} else if args.quiet {
|
|
Level::ERROR
|
|
} else {
|
|
Level::INFO
|
|
};
|
|
|
|
if !args.quiet {
|
|
let subscriber = FmtSubscriber::builder()
|
|
.with_max_level(log_level)
|
|
.with_target(false)
|
|
.compact()
|
|
.finish();
|
|
tracing::subscriber::set_global_default(subscriber)
|
|
.expect("Failed to set tracing subscriber");
|
|
}
|
|
|
|
if args.list_groups {
|
|
if let Err(e) = list_groups(&args) {
|
|
if !args.quiet {
|
|
eprintln!("proxychains: {}", e);
|
|
}
|
|
process::exit(1);
|
|
}
|
|
process::exit(0);
|
|
}
|
|
|
|
// Parse configuration
|
|
let config = load_config(&args);
|
|
|
|
if config.is_err() {
|
|
if !args.quiet {
|
|
eprintln!("proxychains: {}", config.as_ref().unwrap_err());
|
|
}
|
|
process::exit(1);
|
|
}
|
|
|
|
let config = config.unwrap();
|
|
|
|
// Check if we have proxies configured
|
|
if !config.has_proxies() {
|
|
if !args.quiet {
|
|
eprintln!("proxychains: No proxies configured");
|
|
}
|
|
process::exit(1);
|
|
}
|
|
|
|
if !args.quiet {
|
|
info!("Proxychains4 starting...");
|
|
info!("Chain type: {}", config.chain_type);
|
|
info!("Proxies: {}", config.proxy_count());
|
|
if let Some(group) = args.group.as_deref() {
|
|
info!("Proxy group: {}", group);
|
|
}
|
|
}
|
|
|
|
if args.check {
|
|
print_check_summary(&config, &args);
|
|
process::exit(0);
|
|
}
|
|
|
|
if args.probe {
|
|
let failed = run_probe(&config, &args);
|
|
process::exit(if failed == 0 { 0 } else { 2 });
|
|
}
|
|
|
|
// Execute the command with platform-specific injection
|
|
match execute_command(&args, &config) {
|
|
Ok(status) => {
|
|
process::exit(status);
|
|
}
|
|
Err(e) => {
|
|
if !args.quiet {
|
|
error!("Failed to execute command: {}", e);
|
|
}
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load configuration from file or environment
|
|
fn load_config(args: &Args) -> Result<Config, String> {
|
|
build_parser(args).parse().map_err(|e| e.to_string())
|
|
}
|
|
|
|
fn build_parser(args: &Args) -> ConfigParser {
|
|
let mut parser = ConfigParser::new();
|
|
|
|
if let Some(ref path) = args.config {
|
|
parser = parser.with_path(path.clone());
|
|
}
|
|
|
|
if let Some(ref group) = args.group {
|
|
parser = parser.with_group(group.clone());
|
|
}
|
|
|
|
parser
|
|
}
|
|
|
|
fn list_groups(args: &Args) -> Result<(), String> {
|
|
let parser = build_parser(args);
|
|
let mut groups = parser.list_proxy_groups().map_err(|e| e.to_string())?;
|
|
groups.sort();
|
|
if groups.is_empty() {
|
|
println!("No [ProxyList] groups found in config");
|
|
return Ok(());
|
|
}
|
|
for group in groups {
|
|
println!("{}", group);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn print_check_summary(config: &Config, args: &Args) {
|
|
println!("Config check: OK");
|
|
println!("Chain type: {}", config.chain_type);
|
|
println!("Proxy count: {}", config.proxy_count());
|
|
println!("Proxy DNS: {}", config.proxy_dns);
|
|
println!(
|
|
"Selected group: {}",
|
|
args.group.as_deref().unwrap_or("default/all")
|
|
);
|
|
for (idx, proxy) in config.proxies.iter().enumerate() {
|
|
let auth = if proxy.user.is_some() { "auth" } else { "no-auth" };
|
|
println!(
|
|
" {}. {} {}:{} ({})",
|
|
idx + 1,
|
|
proxy.proxy_type,
|
|
proxy.ip,
|
|
proxy.port,
|
|
auth
|
|
);
|
|
}
|
|
}
|
|
|
|
fn run_probe(config: &Config, args: &Args) -> usize {
|
|
let timeout = args
|
|
.probe_timeout_ms
|
|
.map(Duration::from_millis)
|
|
.unwrap_or(config.tcp_connect_timeout);
|
|
let mut failed = 0usize;
|
|
|
|
println!("Proxy probe:");
|
|
println!(" timeout_ms={}", timeout.as_millis());
|
|
println!(
|
|
" group={}",
|
|
args.group.as_deref().unwrap_or("default/all")
|
|
);
|
|
|
|
for (idx, proxy) in config.proxies.iter().enumerate() {
|
|
let target_v4 = SocketAddrV4::new(proxy.ip, proxy.port);
|
|
let target = SocketAddr::V4(target_v4);
|
|
let start = Instant::now();
|
|
match TcpStream::connect_timeout(&target, timeout) {
|
|
Ok(stream) => {
|
|
let elapsed = start.elapsed().as_millis();
|
|
let _ = stream.shutdown(std::net::Shutdown::Both);
|
|
println!(
|
|
" [{}] OK {:<6} {}:{} {} ms",
|
|
idx + 1,
|
|
proxy.proxy_type,
|
|
proxy.ip,
|
|
proxy.port,
|
|
elapsed
|
|
);
|
|
}
|
|
Err(e) => {
|
|
failed += 1;
|
|
let elapsed = start.elapsed().as_millis();
|
|
println!(
|
|
" [{}] FAIL {:<6} {}:{} {} ms ({})",
|
|
idx + 1,
|
|
proxy.proxy_type,
|
|
proxy.ip,
|
|
proxy.port,
|
|
elapsed,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
println!(
|
|
"Probe summary: total={}, ok={}, fail={}",
|
|
config.proxies.len(),
|
|
config.proxies.len().saturating_sub(failed),
|
|
failed
|
|
);
|
|
failed
|
|
}
|
|
|
|
/// Set proxychains-specific environment variables
|
|
fn set_proxychains_env(config: &Config, args: &Args) {
|
|
if args.quiet {
|
|
env::set_var("PROXYCHAINS_QUIET_MODE", "1");
|
|
}
|
|
|
|
if config.proxy_dns {
|
|
env::set_var("PROXYCHAINS_DNS", "1");
|
|
}
|
|
|
|
if let Some(ref path) = args.config {
|
|
env::set_var("PROXYCHAINS_CONF_FILE", path);
|
|
}
|
|
|
|
if let Some(ref group) = args.group {
|
|
env::set_var("PROXYCHAINS_PROXY_GROUP", group);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unix Implementation (LD_PRELOAD/DYLD_INSERT_LIBRARIES)
|
|
// ============================================================================
|
|
|
|
#[cfg(unix)]
|
|
fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
|
|
use std::ffi::CString;
|
|
|
|
if args.command.is_empty() {
|
|
return Err("No command specified".to_string());
|
|
}
|
|
|
|
// Find the library path
|
|
let library_path = find_library_path()?;
|
|
|
|
debug!("Library path: {}", library_path);
|
|
|
|
// Set environment variables
|
|
set_preload_env(&library_path)?;
|
|
|
|
// Set proxychains environment variables
|
|
set_proxychains_env(config, args);
|
|
|
|
// Build command arguments
|
|
let program = &args.command[0];
|
|
let c_args: Vec<CString> = args
|
|
.command
|
|
.iter()
|
|
.map(|s| CString::new(s.as_str()).unwrap())
|
|
.collect();
|
|
|
|
let c_argv: Vec<*const libc::c_char> = c_args
|
|
.iter()
|
|
.map(|s| s.as_ptr())
|
|
.chain(std::iter::once(std::ptr::null()))
|
|
.collect();
|
|
|
|
debug!("Executing: {}", program);
|
|
|
|
// Execute the command
|
|
unsafe {
|
|
let ret = libc::execvp(
|
|
CString::new(program.as_str()).unwrap().as_ptr(),
|
|
c_argv.as_ptr(),
|
|
);
|
|
|
|
if ret < 0 {
|
|
return Err(format!("execvp failed: {}", std::io::Error::last_os_error()));
|
|
}
|
|
}
|
|
|
|
Ok(0)
|
|
}
|
|
|
|
/// Find the proxychains library path (Unix)
|
|
#[cfg(unix)]
|
|
fn find_library_path() -> Result<String, String> {
|
|
// Try common locations
|
|
let search_paths = vec![
|
|
// Same directory as binary
|
|
get_binary_dir()
|
|
.map(|d| d.join("libproxychains.dylib"))
|
|
.unwrap_or_default(),
|
|
get_binary_dir()
|
|
.map(|d| d.join("libproxychains.so"))
|
|
.unwrap_or_default(),
|
|
// Build directory
|
|
PathBuf::from("./target/release/libproxychains.dylib"),
|
|
PathBuf::from("./target/release/libproxychains.so"),
|
|
PathBuf::from("./target/debug/libproxychains.dylib"),
|
|
PathBuf::from("./target/debug/libproxychains.so"),
|
|
// System paths
|
|
PathBuf::from("/usr/local/lib/libproxychains.dylib"),
|
|
PathBuf::from("/usr/local/lib/libproxychains.so"),
|
|
PathBuf::from("/usr/lib/libproxychains.so"),
|
|
PathBuf::from("/usr/lib/x86_64-linux-gnu/libproxychains.so"),
|
|
];
|
|
|
|
for path in search_paths {
|
|
if path.exists() {
|
|
return Ok(path.to_string_lossy().to_string());
|
|
}
|
|
}
|
|
|
|
// Try to find via environment variable
|
|
if let Ok(path) = env::var("PROXYCHAINS_LIB") {
|
|
return Ok(path);
|
|
}
|
|
|
|
Err("Could not find libproxychains library".to_string())
|
|
}
|
|
|
|
/// Set LD_PRELOAD environment variable (Unix)
|
|
#[cfg(unix)]
|
|
fn set_preload_env(library_path: &str) -> Result<(), String> {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
env::set_var("LD_PRELOAD", library_path);
|
|
debug!("Set LD_PRELOAD={}", library_path);
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
env::set_var("DYLD_INSERT_LIBRARIES", library_path);
|
|
env::set_var("DYLD_FORCE_FLAT_NAMESPACE", "1");
|
|
debug!("Set DYLD_INSERT_LIBRARIES={}", library_path);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Windows Implementation (DLL Injection)
|
|
// ============================================================================
|
|
|
|
#[cfg(windows)]
|
|
fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
|
|
use proxychains_injector::{ProxychainsInjector, ProcessInfo, find_library_path};
|
|
|
|
if args.command.is_empty() {
|
|
return Err("No command specified".to_string());
|
|
}
|
|
|
|
// Find the DLL path
|
|
let dll_path = find_library_path().map_err(|e| e.to_string())?;
|
|
|
|
debug!("DLL path: {:?}", dll_path);
|
|
|
|
// Set proxychains environment variables
|
|
set_proxychains_env(config, args);
|
|
|
|
// Create the injector
|
|
let injector = ProxychainsInjector::new(&dll_path)
|
|
.map_err(|e| format!("Failed to create injector: {}", e))?;
|
|
|
|
// Create process info
|
|
let process_info = ProcessInfo {
|
|
pid: None,
|
|
name: None,
|
|
command: args.command[0].clone(),
|
|
args: args.command[1..].to_vec(),
|
|
};
|
|
|
|
debug!("Spawning and injecting: {:?}", process_info);
|
|
|
|
// Spawn the process and inject DLL
|
|
let mut child = injector
|
|
.spawn_and_inject(&process_info)
|
|
.map_err(|e| format!("Failed to spawn and inject: {}", e))?;
|
|
|
|
// Wait for the process to complete
|
|
let status = child
|
|
.wait()
|
|
.map_err(|e| format!("Failed to wait for process: {}", e))?;
|
|
|
|
let exit_code = status
|
|
.code()
|
|
.unwrap_or(1);
|
|
|
|
info!("Process exited with code: {}", exit_code);
|
|
|
|
Ok(exit_code)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Common Functions
|
|
// ============================================================================
|
|
|
|
/// Get the directory containing the current binary
|
|
fn get_binary_dir() -> Option<PathBuf> {
|
|
env::current_exe()
|
|
.ok()
|
|
.and_then(|path| path.parent().map(|p| p.to_path_buf()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_args_parsing() {
|
|
let args = Args::try_parse_from(["proxychains4", "curl", "https://example.com"]);
|
|
assert!(args.is_ok());
|
|
let args = args.unwrap();
|
|
assert_eq!(args.command, vec!["curl", "https://example.com"]);
|
|
assert!(!args.quiet);
|
|
}
|
|
|
|
#[test]
|
|
fn test_args_with_options() {
|
|
let args = Args::try_parse_from([
|
|
"proxychains4",
|
|
"-q",
|
|
"-f",
|
|
"/etc/proxychains.conf",
|
|
"--group",
|
|
"jp",
|
|
"wget",
|
|
"http://example.com",
|
|
]);
|
|
assert!(args.is_ok());
|
|
let args = args.unwrap();
|
|
assert!(args.quiet);
|
|
assert_eq!(args.config, Some(PathBuf::from("/etc/proxychains.conf")));
|
|
assert_eq!(args.group, Some("jp".to_string()));
|
|
assert_eq!(args.command, vec!["wget", "http://example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_args_list_groups_without_command() {
|
|
let args = Args::try_parse_from(["proxychains4", "--list-groups"]);
|
|
assert!(args.is_ok());
|
|
let args = args.unwrap();
|
|
assert!(args.list_groups);
|
|
assert!(args.command.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_args_probe_without_command() {
|
|
let args = Args::try_parse_from(["proxychains4", "--probe"]);
|
|
assert!(args.is_ok());
|
|
let args = args.unwrap();
|
|
assert!(args.probe);
|
|
assert!(args.command.is_empty());
|
|
}
|
|
}
|