Files
proxychains-rs/proxychains-bin/src/main.rs
T

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());
}
}