feat(rustproxy): introduce a Rust-powered proxy engine and workspace with core crates for proxy functionality, ACME/TLS support, passthrough and HTTP proxies, metrics, nftables integration, routing/security, management IPC, tests, and README updates
This commit is contained in:
439
rust/crates/rustproxy-config/src/proxy_options.rs
Normal file
439
rust/crates/rustproxy-config/src/proxy_options.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::route_types::RouteConfig;
|
||||
|
||||
/// Global ACME configuration options.
|
||||
/// Matches TypeScript: `IAcmeOptions`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcmeOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
/// Required when any route uses certificate: 'auto'
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<AcmeEnvironment>,
|
||||
/// Alias for email
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub account_email: Option<String>,
|
||||
/// Port for HTTP-01 challenges (default: 80)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub port: Option<u16>,
|
||||
/// Use Let's Encrypt production (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_production: Option<bool>,
|
||||
/// Days before expiry to renew (default: 30)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub renew_threshold_days: Option<u32>,
|
||||
/// Enable automatic renewal (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_renew: Option<bool>,
|
||||
/// Directory to store certificates (default: './certs')
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub certificate_store: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_configured_certs: Option<bool>,
|
||||
/// How often to check for renewals (default: 24)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub renew_check_interval_hours: Option<u32>,
|
||||
}
|
||||
|
||||
/// ACME environment.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AcmeEnvironment {
|
||||
Production,
|
||||
Staging,
|
||||
}
|
||||
|
||||
/// Default target configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DefaultTarget {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Default security configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DefaultSecurity {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ip_allow_list: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ip_block_list: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_connections: Option<u64>,
|
||||
}
|
||||
|
||||
/// Default configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DefaultConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target: Option<DefaultTarget>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub security: Option<DefaultSecurity>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preserve_source_ip: Option<bool>,
|
||||
}
|
||||
|
||||
/// Keep-alive treatment.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum KeepAliveTreatment {
|
||||
Standard,
|
||||
Extended,
|
||||
Immortal,
|
||||
}
|
||||
|
||||
/// Metrics configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetricsConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sample_interval_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub retention_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// RustProxy configuration options.
|
||||
/// Matches TypeScript: `ISmartProxyOptions`
|
||||
///
|
||||
/// This is the top-level configuration that can be loaded from a JSON file
|
||||
/// or constructed programmatically.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RustProxyOptions {
|
||||
/// The unified configuration array (required)
|
||||
pub routes: Vec<RouteConfig>,
|
||||
|
||||
/// Preserve client IP when forwarding
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preserve_source_ip: Option<bool>,
|
||||
|
||||
/// List of trusted proxy IPs that can send PROXY protocol
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub proxy_ips: Option<Vec<String>>,
|
||||
|
||||
/// Global option to accept PROXY protocol
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accept_proxy_protocol: Option<bool>,
|
||||
|
||||
/// Global option to send PROXY protocol to all targets
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub send_proxy_protocol: Option<bool>,
|
||||
|
||||
/// Global/default settings
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub defaults: Option<DefaultConfig>,
|
||||
|
||||
// ─── Timeout Settings ────────────────────────────────────────────
|
||||
|
||||
/// Timeout for establishing connection to backend (ms), default: 30000
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub connection_timeout: Option<u64>,
|
||||
|
||||
/// Timeout for initial data/SNI (ms), default: 60000
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initial_data_timeout: Option<u64>,
|
||||
|
||||
/// Socket inactivity timeout (ms), default: 3600000
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub socket_timeout: Option<u64>,
|
||||
|
||||
/// How often to check for inactive connections (ms), default: 60000
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub inactivity_check_interval: Option<u64>,
|
||||
|
||||
/// Default max connection lifetime (ms), default: 86400000
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_connection_lifetime: Option<u64>,
|
||||
|
||||
/// Inactivity timeout (ms), default: 14400000
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub inactivity_timeout: Option<u64>,
|
||||
|
||||
/// Maximum time to wait for connections to close during shutdown (ms)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub graceful_shutdown_timeout: Option<u64>,
|
||||
|
||||
// ─── Socket Optimization ─────────────────────────────────────────
|
||||
|
||||
/// Disable Nagle's algorithm (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_delay: Option<bool>,
|
||||
|
||||
/// Enable TCP keepalive (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keep_alive: Option<bool>,
|
||||
|
||||
/// Initial delay before sending keepalive probes (ms)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keep_alive_initial_delay: Option<u64>,
|
||||
|
||||
/// Maximum bytes to buffer during connection setup
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_pending_data_size: Option<u64>,
|
||||
|
||||
// ─── Enhanced Features ───────────────────────────────────────────
|
||||
|
||||
/// Disable inactivity checking entirely
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_inactivity_check: Option<bool>,
|
||||
|
||||
/// Enable TCP keep-alive probes
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_keep_alive_probes: Option<bool>,
|
||||
|
||||
/// Enable detailed connection logging
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_detailed_logging: Option<bool>,
|
||||
|
||||
/// Enable TLS handshake debug logging
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_tls_debug_logging: Option<bool>,
|
||||
|
||||
/// Randomize timeouts to prevent thundering herd
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_randomized_timeouts: Option<bool>,
|
||||
|
||||
// ─── Rate Limiting ───────────────────────────────────────────────
|
||||
|
||||
/// Maximum simultaneous connections from a single IP
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_connections_per_ip: Option<u64>,
|
||||
|
||||
/// Max new connections per minute from a single IP
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub connection_rate_limit_per_minute: Option<u64>,
|
||||
|
||||
// ─── Keep-Alive Settings ─────────────────────────────────────────
|
||||
|
||||
/// How to treat keep-alive connections
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keep_alive_treatment: Option<KeepAliveTreatment>,
|
||||
|
||||
/// Multiplier for inactivity timeout for keep-alive connections
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keep_alive_inactivity_multiplier: Option<f64>,
|
||||
|
||||
/// Extended lifetime for keep-alive connections (ms)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extended_keep_alive_lifetime: Option<u64>,
|
||||
|
||||
// ─── HttpProxy Integration ───────────────────────────────────────
|
||||
|
||||
/// Array of ports to forward to HttpProxy
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_http_proxy: Option<Vec<u16>>,
|
||||
|
||||
/// Port where HttpProxy is listening (default: 8443)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub http_proxy_port: Option<u16>,
|
||||
|
||||
// ─── Metrics ─────────────────────────────────────────────────────
|
||||
|
||||
/// Metrics configuration
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<MetricsConfig>,
|
||||
|
||||
// ─── ACME ────────────────────────────────────────────────────────
|
||||
|
||||
/// Global ACME configuration
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub acme: Option<AcmeOptions>,
|
||||
}
|
||||
|
||||
impl Default for RustProxyOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
routes: Vec::new(),
|
||||
preserve_source_ip: None,
|
||||
proxy_ips: None,
|
||||
accept_proxy_protocol: None,
|
||||
send_proxy_protocol: None,
|
||||
defaults: None,
|
||||
connection_timeout: None,
|
||||
initial_data_timeout: None,
|
||||
socket_timeout: None,
|
||||
inactivity_check_interval: None,
|
||||
max_connection_lifetime: None,
|
||||
inactivity_timeout: None,
|
||||
graceful_shutdown_timeout: None,
|
||||
no_delay: None,
|
||||
keep_alive: None,
|
||||
keep_alive_initial_delay: None,
|
||||
max_pending_data_size: None,
|
||||
disable_inactivity_check: None,
|
||||
enable_keep_alive_probes: None,
|
||||
enable_detailed_logging: None,
|
||||
enable_tls_debug_logging: None,
|
||||
enable_randomized_timeouts: None,
|
||||
max_connections_per_ip: None,
|
||||
connection_rate_limit_per_minute: None,
|
||||
keep_alive_treatment: None,
|
||||
keep_alive_inactivity_multiplier: None,
|
||||
extended_keep_alive_lifetime: None,
|
||||
use_http_proxy: None,
|
||||
http_proxy_port: None,
|
||||
metrics: None,
|
||||
acme: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RustProxyOptions {
|
||||
/// Load configuration from a JSON file.
|
||||
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let options: Self = serde_json::from_str(&content)?;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
/// Get the effective connection timeout in milliseconds.
|
||||
pub fn effective_connection_timeout(&self) -> u64 {
|
||||
self.connection_timeout.unwrap_or(30_000)
|
||||
}
|
||||
|
||||
/// Get the effective initial data timeout in milliseconds.
|
||||
pub fn effective_initial_data_timeout(&self) -> u64 {
|
||||
self.initial_data_timeout.unwrap_or(60_000)
|
||||
}
|
||||
|
||||
/// Get the effective socket timeout in milliseconds.
|
||||
pub fn effective_socket_timeout(&self) -> u64 {
|
||||
self.socket_timeout.unwrap_or(3_600_000)
|
||||
}
|
||||
|
||||
/// Get the effective max connection lifetime in milliseconds.
|
||||
pub fn effective_max_connection_lifetime(&self) -> u64 {
|
||||
self.max_connection_lifetime.unwrap_or(86_400_000)
|
||||
}
|
||||
|
||||
/// Get all unique ports that routes listen on.
|
||||
pub fn all_listening_ports(&self) -> Vec<u16> {
|
||||
let mut ports: Vec<u16> = self.routes
|
||||
.iter()
|
||||
.flat_map(|r| r.listening_ports())
|
||||
.collect();
|
||||
ports.sort();
|
||||
ports.dedup();
|
||||
ports
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::*;
|
||||
|
||||
#[test]
|
||||
fn test_serde_roundtrip_minimal() {
|
||||
let options = RustProxyOptions {
|
||||
routes: vec![create_http_route("example.com", "localhost", 8080)],
|
||||
..Default::default()
|
||||
};
|
||||
let json = serde_json::to_string(&options).unwrap();
|
||||
let parsed: RustProxyOptions = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.routes.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serde_roundtrip_full() {
|
||||
let options = RustProxyOptions {
|
||||
routes: vec![
|
||||
create_http_route("a.com", "backend1", 8080),
|
||||
create_https_passthrough_route("b.com", "backend2", 443),
|
||||
],
|
||||
connection_timeout: Some(5000),
|
||||
socket_timeout: Some(60000),
|
||||
max_connections_per_ip: Some(100),
|
||||
acme: Some(AcmeOptions {
|
||||
enabled: Some(true),
|
||||
email: Some("admin@example.com".to_string()),
|
||||
environment: Some(AcmeEnvironment::Staging),
|
||||
account_email: None,
|
||||
port: None,
|
||||
use_production: None,
|
||||
renew_threshold_days: None,
|
||||
auto_renew: None,
|
||||
certificate_store: None,
|
||||
skip_configured_certs: None,
|
||||
renew_check_interval_hours: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let json = serde_json::to_string_pretty(&options).unwrap();
|
||||
let parsed: RustProxyOptions = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.routes.len(), 2);
|
||||
assert_eq!(parsed.connection_timeout, Some(5000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_timeouts() {
|
||||
let options = RustProxyOptions::default();
|
||||
assert_eq!(options.effective_connection_timeout(), 30_000);
|
||||
assert_eq!(options.effective_initial_data_timeout(), 60_000);
|
||||
assert_eq!(options.effective_socket_timeout(), 3_600_000);
|
||||
assert_eq!(options.effective_max_connection_lifetime(), 86_400_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_timeouts() {
|
||||
let options = RustProxyOptions {
|
||||
connection_timeout: Some(5000),
|
||||
initial_data_timeout: Some(10000),
|
||||
socket_timeout: Some(30000),
|
||||
max_connection_lifetime: Some(60000),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(options.effective_connection_timeout(), 5000);
|
||||
assert_eq!(options.effective_initial_data_timeout(), 10000);
|
||||
assert_eq!(options.effective_socket_timeout(), 30000);
|
||||
assert_eq!(options.effective_max_connection_lifetime(), 60000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_listening_ports() {
|
||||
let options = RustProxyOptions {
|
||||
routes: vec![
|
||||
create_http_route("a.com", "backend", 8080), // port 80
|
||||
create_https_passthrough_route("b.com", "backend", 443), // port 443
|
||||
create_http_route("c.com", "backend", 9090), // port 80 (duplicate)
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let ports = options.all_listening_ports();
|
||||
assert_eq!(ports, vec![80, 443]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camel_case_field_names() {
|
||||
let options = RustProxyOptions {
|
||||
connection_timeout: Some(5000),
|
||||
max_connections_per_ip: Some(100),
|
||||
keep_alive_treatment: Some(KeepAliveTreatment::Extended),
|
||||
..Default::default()
|
||||
};
|
||||
let json = serde_json::to_string(&options).unwrap();
|
||||
assert!(json.contains("connectionTimeout"));
|
||||
assert!(json.contains("maxConnectionsPerIp"));
|
||||
assert!(json.contains("keepAliveTreatment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_example_json() {
|
||||
let content = std::fs::read_to_string(
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/../../config/example.json")
|
||||
).unwrap();
|
||||
let options: RustProxyOptions = serde_json::from_str(&content).unwrap();
|
||||
assert_eq!(options.routes.len(), 4);
|
||||
let ports = options.all_listening_ports();
|
||||
assert!(ports.contains(&80));
|
||||
assert!(ports.contains(&443));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user