use std::net::SocketAddr;
use std::path::PathBuf;
use crate::error::ProxyError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CredentialAction {
Block,
#[default]
RedactOnly,
AlertOnly,
}
#[derive(Debug)]
pub struct ProxyConfig {
pub bind_addr: SocketAddr,
pub ca_dir: PathBuf,
pub cert_cache_capacity: usize,
pub llm_only: bool,
pub mitm_hosts: Vec<String>,
pub denied_hosts: Vec<String>,
pub network_allowlist: Vec<String>,
pub skip_upstream_tls_verify: bool,
pub credential_action: CredentialAction,
pub upstream_override: Option<SocketAddr>,
pub gateway_endpoint: Option<String>,
pub mcp_fail_open: bool,
pub allow_private_connect_targets: bool,
}
impl ProxyConfig {
pub fn from_env() -> Result<Self, ProxyError> {
Ok(Self {
bind_addr: parse_bind_addr()?,
ca_dir: parse_ca_dir()?,
cert_cache_capacity: parse_cert_cache_capacity()?,
llm_only: parse_llm_only(),
mitm_hosts: env_csv("AA_PROXY_MITM_HOSTS"),
denied_hosts: env_csv("AA_PROXY_DENIED_HOSTS"),
network_allowlist: env_csv("AA_PROXY_NETWORK_ALLOWLIST"),
skip_upstream_tls_verify: resolve_skip_upstream_tls_verify(),
credential_action: parse_credential_action_env()?,
upstream_override: None,
gateway_endpoint: env_optional("AA_PROXY_GATEWAY_ENDPOINT"),
mcp_fail_open: env_truthy("AA_PROXY_MCP_FAIL_OPEN"),
allow_private_connect_targets: false,
})
}
}
fn parse_bind_addr() -> Result<SocketAddr, ProxyError> {
match std::env::var("AA_PROXY_ADDR") {
Ok(val) => val
.parse::<SocketAddr>()
.map_err(|e| ProxyError::Config(format!("invalid AA_PROXY_ADDR: {e}"))),
Err(_) => Ok(SocketAddr::from(([127, 0, 0, 1], 8899))),
}
}
fn parse_ca_dir() -> Result<PathBuf, ProxyError> {
match std::env::var("AA_CA_DIR") {
Ok(val) => Ok(PathBuf::from(val)),
Err(_) => dirs::home_dir()
.ok_or_else(|| ProxyError::Config("cannot determine home directory".into()))
.map(|h| h.join(".aa").join("ca")),
}
}
fn parse_cert_cache_capacity() -> Result<usize, ProxyError> {
match std::env::var("AA_PROXY_CERT_CACHE_CAPACITY") {
Ok(val) => val
.parse::<usize>()
.map_err(|e| ProxyError::Config(format!("invalid AA_PROXY_CERT_CACHE_CAPACITY: {e}"))),
Err(_) => Ok(1000),
}
}
fn parse_llm_only() -> bool {
match std::env::var("AA_PROXY_LLM_ONLY") {
Ok(val) => val != "0" && val.to_lowercase() != "false",
Err(_) => true,
}
}
fn resolve_skip_upstream_tls_verify() -> bool {
let requested = env_truthy("AA_PROXY_SKIP_UPSTREAM_TLS_VERIFY");
if cfg!(debug_assertions) {
requested
} else {
if requested {
tracing::error!(
"AA_PROXY_SKIP_UPSTREAM_TLS_VERIFY is set but IGNORED in a release build — \
upstream TLS verification stays ENABLED. This flag is debug-only."
);
}
false
}
}
fn parse_credential_action_env() -> Result<CredentialAction, ProxyError> {
match std::env::var("AA_PROXY_CREDENTIAL_ACTION") {
Ok(val) => parse_credential_action(&val),
Err(_) => Ok(CredentialAction::default()),
}
}
fn env_optional(name: &str) -> Option<String> {
match std::env::var(name) {
Ok(val) if !val.is_empty() => Some(val),
_ => None,
}
}
fn env_truthy(name: &str) -> bool {
match std::env::var(name) {
Ok(val) => val == "1" || val.to_lowercase() == "true",
Err(_) => false,
}
}
fn env_csv(name: &str) -> Vec<String> {
match std::env::var(name) {
Ok(val) if !val.is_empty() => val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
_ => Vec::new(),
}
}
fn parse_credential_action(s: &str) -> Result<CredentialAction, ProxyError> {
match s.trim().to_ascii_lowercase().as_str() {
"block" => Ok(CredentialAction::Block),
"redact_only" => Ok(CredentialAction::RedactOnly),
"alert_only" => Ok(CredentialAction::AlertOnly),
other => Err(ProxyError::Config(format!(
"invalid AA_PROXY_CREDENTIAL_ACTION: {other:?} (expected block | redact_only | alert_only)"
))),
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn clear_env_vars() {
std::env::remove_var("AA_PROXY_ADDR");
std::env::remove_var("AA_CA_DIR");
std::env::remove_var("AA_PROXY_CERT_CACHE_CAPACITY");
std::env::remove_var("AA_PROXY_LLM_ONLY");
std::env::remove_var("AA_PROXY_MITM_HOSTS");
std::env::remove_var("AA_PROXY_DENIED_HOSTS");
std::env::remove_var("AA_PROXY_SKIP_UPSTREAM_TLS_VERIFY");
std::env::remove_var("AA_PROXY_CREDENTIAL_ACTION");
std::env::remove_var("AA_PROXY_GATEWAY_ENDPOINT");
std::env::remove_var("AA_PROXY_MCP_FAIL_OPEN");
}
#[test]
fn from_env_returns_defaults_when_no_vars_set() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.bind_addr, SocketAddr::from(([127, 0, 0, 1], 8899)));
assert!(cfg.ca_dir.ends_with(".aa/ca"));
assert_eq!(cfg.cert_cache_capacity, 1000);
assert!(cfg.llm_only);
assert!(cfg.denied_hosts.is_empty());
assert!(!cfg.skip_upstream_tls_verify);
assert!(!cfg.mcp_fail_open);
}
#[test]
fn from_env_reads_aa_proxy_addr() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_ADDR", "0.0.0.0:9000");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.bind_addr, SocketAddr::from(([0, 0, 0, 0], 9000)));
}
#[test]
fn from_env_invalid_addr_returns_config_error() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_ADDR", "not-an-addr");
let err = ProxyConfig::from_env().unwrap_err();
assert!(err.to_string().contains("AA_PROXY_ADDR"));
}
#[test]
fn from_env_reads_aa_ca_dir() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_CA_DIR", "/tmp/custom-ca");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.ca_dir, PathBuf::from("/tmp/custom-ca"));
}
#[test]
fn from_env_reads_llm_only_false() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_LLM_ONLY", "false");
let cfg = ProxyConfig::from_env().unwrap();
assert!(!cfg.llm_only);
}
#[test]
fn from_env_reads_llm_only_zero() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_LLM_ONLY", "0");
let cfg = ProxyConfig::from_env().unwrap();
assert!(!cfg.llm_only);
}
#[test]
fn from_env_reads_denied_hosts_csv() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_DENIED_HOSTS", "evil.com, bad.example.com");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.denied_hosts, vec!["evil.com", "bad.example.com"]);
}
#[test]
fn from_env_reads_mitm_hosts_csv() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_MITM_HOSTS", "generativelanguage.googleapis.com, *.groq.com");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.mitm_hosts, vec!["generativelanguage.googleapis.com", "*.groq.com"]);
}
#[test]
fn from_env_mitm_hosts_defaults_empty() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
let cfg = ProxyConfig::from_env().unwrap();
assert!(cfg.mitm_hosts.is_empty());
}
#[test]
fn from_env_denied_hosts_empty_string_gives_empty_vec() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_DENIED_HOSTS", "");
let cfg = ProxyConfig::from_env().unwrap();
assert!(cfg.denied_hosts.is_empty());
}
#[test]
fn from_env_skip_upstream_tls_verify_honoured_in_debug_only() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_SKIP_UPSTREAM_TLS_VERIFY", "1");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.skip_upstream_tls_verify, cfg!(debug_assertions));
}
#[test]
fn from_env_skip_upstream_tls_verify_false_by_default() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
let cfg = ProxyConfig::from_env().unwrap();
assert!(!cfg.skip_upstream_tls_verify);
}
#[test]
fn from_env_credential_action_defaults_to_redact_only() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.credential_action, CredentialAction::RedactOnly);
}
#[test]
fn from_env_credential_action_reads_block() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_CREDENTIAL_ACTION", "block");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.credential_action, CredentialAction::Block);
}
#[test]
fn from_env_credential_action_reads_alert_only_case_insensitive() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_CREDENTIAL_ACTION", "ALERT_ONLY");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.credential_action, CredentialAction::AlertOnly);
}
#[test]
fn from_env_credential_action_invalid_returns_config_error() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_CREDENTIAL_ACTION", "nope");
let err = ProxyConfig::from_env().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("AA_PROXY_CREDENTIAL_ACTION"),
"error must name the env var, got: {msg}"
);
}
#[test]
fn from_env_gateway_endpoint_defaults_to_none() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.gateway_endpoint, None);
}
#[test]
fn from_env_reads_gateway_endpoint() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_GATEWAY_ENDPOINT", "http://127.0.0.1:50051");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.gateway_endpoint.as_deref(), Some("http://127.0.0.1:50051"));
}
#[test]
fn from_env_gateway_endpoint_empty_string_is_none() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_GATEWAY_ENDPOINT", "");
let cfg = ProxyConfig::from_env().unwrap();
assert_eq!(cfg.gateway_endpoint, None);
}
#[test]
fn from_env_mcp_fail_open_defaults_to_false() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
let cfg = ProxyConfig::from_env().unwrap();
assert!(!cfg.mcp_fail_open);
}
#[test]
fn from_env_mcp_fail_open_reads_one() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_MCP_FAIL_OPEN", "1");
let cfg = ProxyConfig::from_env().unwrap();
assert!(cfg.mcp_fail_open);
}
#[test]
fn from_env_mcp_fail_open_reads_true_case_insensitive() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_MCP_FAIL_OPEN", "TRUE");
let cfg = ProxyConfig::from_env().unwrap();
assert!(cfg.mcp_fail_open);
}
#[test]
fn from_env_mcp_fail_open_other_value_is_false() {
let _lock = ENV_LOCK.lock().unwrap();
clear_env_vars();
std::env::set_var("AA_PROXY_MCP_FAIL_OPEN", "no");
let cfg = ProxyConfig::from_env().unwrap();
assert!(!cfg.mcp_fail_open);
}
}