feat: add proxy group selection via --group and [ProxyList:<name>]

This commit is contained in:
tianrking committed 2026-03-20 11:33:15 +08:00
1 parent 473a7faf47
commit bdb5cf5add
4 files changed
+149 -4

No files matched your search

+13
View File
@@ -157,6 +157,7 @@ Usage: proxychains4 [OPTIONS] <COMMAND> [ARGS...]
Options:
-f, --file <PATH> Configuration file path
--group <GROUP> Proxy group name (uses [ProxyList:<GROUP>])
-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
+21 -2
View File
@@ -35,6 +35,10 @@ struct Args {
#[arg(short = 'v', long)]
verbose: bool,
/// Proxy group name, matches config sections like [ProxyList:<group>]
#[arg(long, value_name = "GROUP")]
group: Option<String>,
/// The command to run
#[arg(required = true, trailing_var_arg = true)]
command: Vec<String>,
@@ -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<Config, String> {
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"]);
}
}
+7
View File
@@ -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 <name> ...)
# [ProxyList:jp]
# socks5 1.2.3.4 1080
#
# [ProxyList:us]
# socks5 5.6.7.8 1080
+108 -2
View File
@@ -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<PathBuf>,
proxy_group: Option<String>,
}
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:<group>].
pub fn with_group(mut self, group: impl Into<String>) -> Self {
self.proxy_group = Some(group.into());
self
}
fn selected_group(&self) -> Option<String> {
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<PathBuf> {
// 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);
}
}