feat(smart-proxy): add hot-reloadable global ingress security policy across Rust and TypeScript proxy layers
This commit is contained in:
@@ -3,15 +3,15 @@
|
||||
//! Configuration types for RustProxy, fully compatible with SmartProxy's JSON schema.
|
||||
//! All types use `#[serde(rename_all = "camelCase")]` to match TypeScript field naming.
|
||||
|
||||
pub mod route_types;
|
||||
pub mod proxy_options;
|
||||
pub mod tls_types;
|
||||
pub mod route_types;
|
||||
pub mod security_types;
|
||||
pub mod tls_types;
|
||||
pub mod validation;
|
||||
|
||||
// Re-export all primary types
|
||||
pub use route_types::*;
|
||||
pub use proxy_options::*;
|
||||
pub use tls_types::*;
|
||||
pub use route_types::*;
|
||||
pub use security_types::*;
|
||||
pub use tls_types::*;
|
||||
pub use validation::*;
|
||||
|
||||
@@ -97,6 +97,16 @@ pub struct MetricsConfig {
|
||||
pub retention_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// Global ingress security policy.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SecurityPolicy {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blocked_ips: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blocked_cidrs: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// RustProxy configuration options.
|
||||
/// Matches TypeScript: `ISmartProxyOptions`
|
||||
///
|
||||
@@ -235,6 +245,10 @@ pub struct RustProxyOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metrics: Option<MetricsConfig>,
|
||||
|
||||
/// Global ingress security policy, enforced before route selection.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub security_policy: Option<SecurityPolicy>,
|
||||
|
||||
// ─── ACME ────────────────────────────────────────────────────────
|
||||
/// Global ACME configuration
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -275,6 +289,7 @@ impl Default for RustProxyOptions {
|
||||
use_http_proxy: None,
|
||||
http_proxy_port: None,
|
||||
metrics: None,
|
||||
security_policy: None,
|
||||
acme: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +111,7 @@ pub enum IpAllowEntry {
|
||||
/// Plain IP/CIDR — allowed for all domains on this route
|
||||
Plain(String),
|
||||
/// Domain-scoped — allowed only when the requested domain matches
|
||||
DomainScoped {
|
||||
ip: String,
|
||||
domains: Vec<String>,
|
||||
},
|
||||
DomainScoped { ip: String, domains: Vec<String> },
|
||||
}
|
||||
|
||||
/// Security options for routes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::route_types::{RouteConfig, RouteActionType};
|
||||
use crate::route_types::{RouteActionType, RouteConfig};
|
||||
|
||||
/// Validation errors for route configurations.
|
||||
#[derive(Debug, Error)]
|
||||
@@ -30,9 +30,10 @@ pub enum ValidationError {
|
||||
/// Validate a single route configuration.
|
||||
pub fn validate_route(route: &RouteConfig) -> Result<(), Vec<ValidationError>> {
|
||||
let mut errors = Vec::new();
|
||||
let name = route.name.clone().unwrap_or_else(|| {
|
||||
route.id.clone().unwrap_or_else(|| "unnamed".to_string())
|
||||
});
|
||||
let name = route
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| route.id.clone().unwrap_or_else(|| "unnamed".to_string()));
|
||||
|
||||
// Check ports
|
||||
let ports = route.listening_ports();
|
||||
@@ -160,7 +161,9 @@ mod tests {
|
||||
let mut route = make_valid_route();
|
||||
route.action.targets = None;
|
||||
let errors = validate_route(&route).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::MissingTargets { .. })));
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::MissingTargets { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -168,7 +171,9 @@ mod tests {
|
||||
let mut route = make_valid_route();
|
||||
route.action.targets = Some(vec![]);
|
||||
let errors = validate_route(&route).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::EmptyTargets { .. })));
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::EmptyTargets { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -176,7 +181,9 @@ mod tests {
|
||||
let mut route = make_valid_route();
|
||||
route.route_match.ports = PortRange::Single(0);
|
||||
let errors = validate_route(&route).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::InvalidPort { port: 0, .. })));
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::InvalidPort { port: 0, .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -186,7 +193,9 @@ mod tests {
|
||||
let mut r2 = make_valid_route();
|
||||
r2.id = Some("route-1".to_string());
|
||||
let errors = validate_routes(&[r1, r2]).unwrap_err();
|
||||
assert!(errors.iter().any(|e| matches!(e, ValidationError::DuplicateId { .. })));
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::DuplicateId { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user