From bdb5cf5add603e1343d3d5e6ec52c2650ab71ba6 Mon Sep 17 00:00:00 2001 From: tianrking Date: Fri, 20 Mar 2026 11:33:15 +0800 Subject: [PATCH] feat: add proxy group selection via --group and [ProxyList:] --- README.md | 13 ++++ proxychains-bin/src/main.rs | 23 ++++++- proxychains.conf | 7 ++ proxychains/src/config/parser.rs | 110 ++++++++++++++++++++++++++++++- 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 301636f..14a9c9f 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Usage: proxychains4 [OPTIONS] [ARGS...] Options: -f, --file Configuration file path + --group Proxy group name (uses [ProxyList:]) -q, --quiet Quiet mode (suppress output) -v, --verbose Verbose mode (debug output) -h, --help Show help @@ -226,6 +227,18 @@ socks4 192.168.1.2 1080 # HTTP proxies http 192.168.1.3 8080 http 192.168.1.4 8080 user pass + +# Optional grouped lists +[ProxyList:jp] +socks5 10.0.0.2 1080 + +[ProxyList:us] +socks5 10.0.0.3 1080 +``` + +Group usage example: +```bash +proxychains4 --group jp curl https://ifconfig.me ``` ### Configuration File Locations diff --git a/proxychains-bin/src/main.rs b/proxychains-bin/src/main.rs index 72df130..98e920c 100644 --- a/proxychains-bin/src/main.rs +++ b/proxychains-bin/src/main.rs @@ -35,6 +35,10 @@ struct Args { #[arg(short = 'v', long)] verbose: bool, + /// Proxy group name, matches config sections like [ProxyList:] + #[arg(long, value_name = "GROUP")] + group: Option, + /// The command to run #[arg(required = true, trailing_var_arg = true)] command: Vec, @@ -86,6 +90,9 @@ fn main() { 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); + } } // Execute the command with platform-specific injection @@ -110,6 +117,10 @@ fn load_config(args: &Args) -> Result { parser = parser.with_path(path.clone()); } + if let Some(ref group) = args.group { + parser = parser.with_group(group.clone()); + } + parser.parse().map_err(|e| e.to_string()) } @@ -123,8 +134,13 @@ fn set_proxychains_env(config: &Config, args: &Args) { env::set_var("PROXYCHAINS_DNS", "1"); } - // Note: The actual config file path is handled by the library - // when it reads PROXYCHAINS_CONF_FILE + 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); + } } // ============================================================================ @@ -322,6 +338,8 @@ mod tests { "-q", "-f", "/etc/proxychains.conf", + "--group", + "jp", "wget", "http://example.com", ]); @@ -329,6 +347,7 @@ mod tests { 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"]); } } diff --git a/proxychains.conf b/proxychains.conf index f129d4d..865033d 100644 --- a/proxychains.conf +++ b/proxychains.conf @@ -44,3 +44,10 @@ localnet 10.0.0.0/255.0.0.0 # Example: Default Tor proxy # socks5 127.0.0.1 9050 + +# Optional grouped lists (select with: proxychains4 --group ...) +# [ProxyList:jp] +# socks5 1.2.3.4 1080 +# +# [ProxyList:us] +# socks5 5.6.7.8 1080 diff --git a/proxychains/src/config/parser.rs b/proxychains/src/config/parser.rs index 112d4d5..5cec4a7 100644 --- a/proxychains/src/config/parser.rs +++ b/proxychains/src/config/parser.rs @@ -17,16 +17,21 @@ pub const ENV_QUIET_MODE: &str = "PROXYCHAINS_QUIET_MODE"; pub const ENV_SOCKS5_HOST: &str = "PROXYCHAINS_SOCKS5_HOST"; pub const ENV_SOCKS5_PORT: &str = "PROXYCHAINS_SOCKS5_PORT"; pub const ENV_DNS: &str = "PROXYCHAINS_DNS"; +pub const ENV_PROXY_GROUP: &str = "PROXYCHAINS_PROXY_GROUP"; /// Configuration parser pub struct ConfigParser { config_path: Option, + proxy_group: Option, } impl ConfigParser { /// Create a new configuration parser pub fn new() -> Self { - Self { config_path: None } + Self { + config_path: None, + proxy_group: None, + } } /// Set the configuration file path @@ -35,6 +40,20 @@ impl ConfigParser { self } + /// Set proxy group name. Matches sections like [ProxyList:]. + pub fn with_group(mut self, group: impl Into) -> Self { + self.proxy_group = Some(group.into()); + self + } + + fn selected_group(&self) -> Option { + self.proxy_group + .clone() + .or_else(|| env::var(ENV_PROXY_GROUP).ok()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + } + /// Find the configuration file pub fn find_config_file(&self) -> Option { // If path was explicitly set, use it @@ -115,6 +134,8 @@ impl ConfigParser { let mut config = Config::default(); let mut in_proxy_list = false; + let selected_group = self.selected_group().map(|s| s.to_lowercase()); + let mut matched_selected_group = selected_group.is_none(); for line in reader.lines() { let line = line?; @@ -127,7 +148,31 @@ impl ConfigParser { // Check for section markers if trimmed.starts_with('[') { - in_proxy_list = trimmed.to_lowercase().contains("proxylist"); + let section = trimmed + .trim_start_matches('[') + .trim_end_matches(']') + .trim(); + let section_lower = section.to_lowercase(); + + if section_lower.starts_with("proxylist") { + let group_name = section + .split_once(':') + .map(|(_, g)| g.trim().to_lowercase()) + .filter(|g| !g.is_empty()) + .unwrap_or_else(|| "default".to_string()); + + in_proxy_list = if let Some(ref selected) = selected_group { + let is_match = &group_name == selected; + if is_match { + matched_selected_group = true; + } + is_match + } else { + true + }; + } else { + in_proxy_list = false; + } continue; } @@ -141,6 +186,15 @@ impl ConfigParser { // Apply environment variable overrides self.apply_env_overrides(&mut config)?; + if let Some(group) = selected_group { + if !matched_selected_group { + return Err(Error::Config(format!( + "Proxy group not found: {}", + group + ))); + } + } + Ok(config) } @@ -314,7 +368,9 @@ mod dirs { #[cfg(test)] mod tests { use super::*; + use std::fs; use std::str::FromStr; + use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn test_parse_proxy_type() { @@ -338,4 +394,54 @@ mod tests { assert!(localnet.contains(&Ipv4Addr::new(192, 168, 255, 255))); assert!(!localnet.contains(&Ipv4Addr::new(10, 0, 0, 1))); } + + #[test] + fn test_parse_proxy_group() { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("proxychains_group_{}.conf", ts)); + let content = r#" +dynamic_chain +[ProxyList] +socks5 127.0.0.1 1080 +[ProxyList:jp] +socks5 127.0.0.2 1080 +"#; + fs::write(&path, content).unwrap(); + + let config = ConfigParser::new() + .with_path(path.clone()) + .with_group("jp") + .parse() + .unwrap(); + assert_eq!(config.proxies.len(), 1); + assert_eq!(config.proxies[0].ip, Ipv4Addr::new(127, 0, 0, 2)); + + let _ = fs::remove_file(path); + } + + #[test] + fn test_missing_proxy_group_returns_error() { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("proxychains_group_missing_{}.conf", ts)); + let content = r#" +[ProxyList] +socks5 127.0.0.1 1080 +"#; + fs::write(&path, content).unwrap(); + + let err = ConfigParser::new() + .with_path(path.clone()) + .with_group("us") + .parse() + .unwrap_err(); + assert!(format!("{}", err).contains("Proxy group not found")); + + let _ = fs::remove_file(path); + } }