use std::process::ExitCode;
use std::time::Duration;
use eggress_cli::{
EXIT_CLI_PARSE_ERROR, EXIT_CONFIG_VALIDATION, EXIT_RUNTIME_FAILURE, EXIT_SUCCESS,
EXIT_UNSUPPORTED_FEATURE,
};
const VERSION: &str = concat!("eggress-pproxy-compat ", env!("CARGO_PKG_VERSION"));
const HELP_TEXT: &str = "\
pproxy compatibility binary (eggress-pproxy-compat)
This binary provides compatibility with pproxy 2.7.9 command-line
interface. It translates pproxy-style arguments to eggress TOML configuration
and starts the eggress proxy service.
Unsupported options cause startup to fail with a non-zero exit code.
Run 'eggress pproxy check -- <args>' to inspect all classifications.
USAGE:
pproxy [OPTIONS]
OPTIONS:
-l <URI> Local listener URI (repeatable)
-r <URI> Remote/upstream URI (repeatable)
-ul <URI> UDP listener URI (repeatable)
-ur <URI> UDP upstream URI (repeatable)
-b <PATTERN> Block rule pattern (regex)
-a <SECONDS> Alive/health check interval
-s <SCHEDULER> Scheduler (rr, fa, rc, lc)
-d Debug traceback/error visibility (repeatable)
-v Verbose connection output (repeatable; -vv adds traffic stats)
--ssl <CERT,KEY> Enable TLS on listeners
--pac <PATH> Serve PAC content at PATH
--test <URL> Test the supplied target and exit
--sys Apply the selected local HTTP/SOCKS5 listener as system proxy
--reuse Listener SO_REUSEPORT (Linux only)
--auth <SECONDS> Per-client source-IP auth reuse interval
--get <PATH,FILE> Serve FILE at PATH through the admin server
--daemon Linux daemon-compatible re-exec (optional feature)
--version Print version and exit
-h, --help Print this help and exit
EXAMPLES:
pproxy -l http://:8080 -r socks5://127.0.0.1:1080
pproxy -l socks5://:1080 -r http://proxy:8080 -r socks5://backup:1080
pproxy -l http://:8080 -r socks5://127.0.0.1:1080 --ssl cert.pem,key.pem
pproxy -l http://:8080 -r socks5://127.0.0.1:1080 --test http://example.com
NOTE:
This is an eggress compatibility wrapper, not the original pproxy.
Some features are unsupported or behave differently. Run
'eggress pproxy check -- <args>' to see compatibility details.
";
fn print_version() {
println!("{VERSION}");
}
fn print_help() {
print!("{HELP_TEXT}");
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args_os()
.skip(1)
.map(|a| a.to_string_lossy().into_owned())
.collect();
let pproxy_args = if eggress_pproxy_compat::PproxyArgs::has_args(&args) {
match eggress_pproxy_compat::PproxyArgs::parse(&args) {
Ok(a) => a,
Err(e) => {
eprintln!("pproxy: error: {e}");
std::process::exit(EXIT_CLI_PARSE_ERROR);
}
}
} else {
eggress_pproxy_compat::PproxyArgs::default_args()
};
if pproxy_args.version {
print_version();
return ExitCode::SUCCESS;
}
if pproxy_args.help {
print_help();
return ExitCode::SUCCESS;
}
if let Some(flag) = pproxy_args.strict_parser_violations().first() {
eprintln!("pproxy: error: unknown option or positional argument '{flag}'");
std::process::exit(EXIT_CLI_PARSE_ERROR);
}
if let Err(e) = pproxy_args.validate_strict_values() {
eprintln!("pproxy: error: {e}");
std::process::exit(EXIT_CLI_PARSE_ERROR);
}
let output = match eggress_pproxy_compat::translate_pproxy_args(&pproxy_args) {
Ok(o) => o,
Err(e) => {
eprintln!("pproxy: error: {e}");
std::process::exit(EXIT_CONFIG_VALIDATION);
}
};
let gate = eggress_pproxy_compat::evaluate_execution_gate(&pproxy_args, &output);
if !gate.allows_start() {
for blocker in &gate.blockers {
match blocker {
eggress_pproxy_compat::BlockReason::UnknownFlag(flag) => {
eprintln!("pproxy: error: unknown option '{flag}'");
}
eggress_pproxy_compat::BlockReason::Unsupported(u) => {
eprintln!("pproxy: error: {u}");
}
}
}
eprintln!();
if gate
.blockers
.iter()
.any(|b| matches!(b, eggress_pproxy_compat::BlockReason::UnknownFlag(_)))
{
eprintln!("Run 'eggress pproxy check -- <args>' for supported options.");
std::process::exit(EXIT_CLI_PARSE_ERROR);
}
eprintln!("These features are not supported by eggress and prevent startup.");
eprintln!("Run 'eggress pproxy check -- <args>' for detailed compatibility report.");
std::process::exit(EXIT_UNSUPPORTED_FEATURE);
}
for w in &gate.warnings {
eprintln!("pproxy: note: {w}");
}
let test_target = pproxy_args.test_target();
if test_target.is_none() {
print_startup_banner(&pproxy_args, &output);
}
let (rt_config, _warnings) =
match eggress_config::validate_and_compile_toml_with_warnings(&output.toml) {
Ok(r) => r,
Err(e) => {
eprintln!("pproxy: config error: {e}");
std::process::exit(EXIT_CONFIG_VALIDATION);
}
};
if let Some(target) = test_target {
let timeout = Duration::from_secs(10);
let target = match eggress_cli::parse_pproxy_test_target(target) {
Ok(target) => target.to_string(),
Err(error) => {
eprintln!("pproxy: error: {error}");
std::process::exit(EXIT_CLI_PARSE_ERROR);
}
};
if rt_config.upstreams.is_empty() {
std::process::exit(EXIT_SUCCESS);
}
let exit_code = eggress_cli::run_upstream_test(&rt_config, Some(&target), timeout, false);
std::process::exit(exit_code);
}
#[cfg(feature = "pproxy-daemon")]
if let Err(error) = eggress_cli::maybe_daemonize(pproxy_args.daemon) {
eprintln!("pproxy: error: {error}");
std::process::exit(EXIT_UNSUPPORTED_FEATURE);
}
init_logging(&pproxy_args);
tracing::info!("starting eggress with pproxy-compatible config");
let compatibility_options = eggress_runtime::CompatibilityOptions {
compatibility_mode: true,
auth_timeout: Some(pproxy_args.effective_auth_timeout()),
system_proxy: pproxy_args.system_proxy,
debug: pproxy_args.debug,
verbose_level: pproxy_args.verbose_level,
};
match eggress_runtime::ServiceSupervisor::start_from_config_with_options(
rt_config,
None,
compatibility_options,
) {
Ok(mut supervisor) => {
if let Err(e) = supervisor.run() {
eprintln!("pproxy: runtime error: {e}");
std::process::exit(EXIT_RUNTIME_FAILURE);
}
}
Err(e) => {
eprintln!("pproxy: runtime error: {e}");
std::process::exit(EXIT_RUNTIME_FAILURE);
}
}
ExitCode::SUCCESS
}
fn init_logging(pproxy_args: &eggress_pproxy_compat::PproxyArgs) {
let level = pproxy_args.default_log_level();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level)),
)
.pretty()
.init();
}
fn print_startup_banner(
pproxy_args: &eggress_pproxy_compat::PproxyArgs,
_output: &eggress_pproxy_compat::TranslationOutput,
) {
eprintln!("{VERSION}");
for local in &pproxy_args.local {
eprintln!(" listen: {}", redact_uri(local));
}
for remote in &pproxy_args.remotes {
eprintln!(" remote: {}", redact_uri(remote));
}
let has_udp = pproxy_args
.known_unsupported
.iter()
.any(|f| f.starts_with("udp-listen="));
if has_udp {
for flag in &pproxy_args.known_unsupported {
if let Some(addr) = flag.strip_prefix("udp-listen=") {
eprintln!(" udp: {addr}");
}
}
}
let has_ssl = pproxy_args
.known_unsupported
.iter()
.any(|f| f.starts_with("ssl="));
if has_ssl {
eprintln!(" tls: enabled");
}
let has_pac = pproxy_args
.known_unsupported
.iter()
.any(|f| f.starts_with("pac="));
if has_pac {
eprintln!(" pac: enabled");
}
if pproxy_args.reuse_port {
eprintln!(" reuse: SO_REUSEPORT");
}
eprintln!();
eprintln!("pproxy started, waiting for connections...");
}
fn redact_uri(uri: &str) -> String {
eggress_uri::parse_proxy_chain(uri)
.map(|chain| eggress_uri::RedactedUri::new(&chain).to_string())
.unwrap_or_else(|_| eggress_uri::redact_proxy_uri(uri))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_help_flag() {
assert!(HELP_TEXT.contains("--help"));
assert!(HELP_TEXT.contains("--version"));
assert!(HELP_TEXT.contains("-l"));
assert!(HELP_TEXT.contains("-r"));
assert!(HELP_TEXT.contains("--test"));
assert!(HELP_TEXT.contains("--sys"));
assert!(HELP_TEXT.contains("--ssl"));
assert!(HELP_TEXT.contains("--pac"));
assert!(HELP_TEXT.contains("-d"));
assert!(HELP_TEXT.contains("--reuse"));
assert!(HELP_TEXT.contains("--auth"));
assert!(HELP_TEXT.contains("--daemon"));
}
#[test]
fn test_version_string() {
assert!(VERSION.contains("eggress-pproxy-compat"));
}
#[test]
fn in_memory_config_from_translated_toml() {
let args = vec![
"-l".to_string(),
"http://127.0.0.1:0".to_string(),
"-r".to_string(),
"socks5://127.0.0.1:1080".to_string(),
];
let pproxy_args = eggress_pproxy_compat::PproxyArgs::parse(&args).unwrap();
let output = eggress_pproxy_compat::translate_pproxy_args(&pproxy_args).unwrap();
assert!(!output.toml.is_empty());
assert!(output.toml.contains("[[listeners]]"));
let (rt_config, _warnings) =
eggress_config::validate_and_compile_toml_with_warnings(&output.toml).unwrap();
assert_eq!(rt_config.listeners.len(), 1);
assert!(rt_config.listeners[0].name.starts_with("pproxy-"));
}
#[test]
fn in_memory_config_rejects_invalid_toml() {
let bad_toml = "version = 1\n[[listeners]]\nname = \"bad\"\nbind = \"not-a-addr\"\nprotocols = [\"http\"]\nconnection_limit = 0\n";
let result = eggress_config::validate_and_compile_toml_with_warnings(bad_toml);
assert!(result.is_err());
}
#[test]
fn translation_output_toml_is_parseable() {
let args = vec![
"-l".to_string(),
"socks5://127.0.0.1:0".to_string(),
"-r".to_string(),
"http://proxy:8080".to_string(),
];
let pproxy_args = eggress_pproxy_compat::PproxyArgs::parse(&args).unwrap();
let output = eggress_pproxy_compat::translate_pproxy_args(&pproxy_args).unwrap();
let result = eggress_config::validate_and_compile_toml(&output.toml);
assert!(
result.is_ok(),
"translated TOML should be valid: {:?}",
result.err()
);
}
}