Skip to main content

relay_core_api/
policy.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct RedactionPolicy {
6    #[serde(default = "default_false")]
7    pub enabled: bool,
8    #[serde(default = "default_sensitive_header_names")]
9    pub sensitive_header_names: Vec<String>,
10    #[serde(default = "default_sensitive_query_keys")]
11    pub sensitive_query_keys: Vec<String>,
12    #[serde(default = "default_false")]
13    pub redact_bodies: bool,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
17pub struct RedactionPolicyPatch {
18    #[serde(default)]
19    pub enabled: Option<bool>,
20    #[serde(default)]
21    pub sensitive_header_names: Option<Vec<String>>,
22    #[serde(default)]
23    pub sensitive_query_keys: Option<Vec<String>>,
24    #[serde(default)]
25    pub redact_bodies: Option<bool>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
29pub struct ProxyPolicyPatch {
30    #[serde(default)]
31    pub redaction: Option<RedactionPolicyPatch>,
32}
33
34impl Default for RedactionPolicy {
35    fn default() -> Self {
36        Self {
37            enabled: false,
38            sensitive_header_names: default_sensitive_header_names(),
39            sensitive_query_keys: default_sensitive_query_keys(),
40            redact_bodies: false,
41        }
42    }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ProxyPolicy {
47    /// In strict mode, invalid method/status does not silently rewrite to GET/200.
48    #[serde(default = "default_true")]
49    pub strict_http_semantics: bool,
50
51    /// Allow fallback to GET for invalid methods (only if strict_http_semantics is false)
52    #[serde(default = "default_false")]
53    pub allow_fallback_method: bool,
54
55    /// Allow fallback to 200 OK for invalid status (only if strict_http_semantics is false)
56    #[serde(default = "default_false")]
57    pub allow_fallback_status: bool,
58
59    /// Enable automatic retries for idempotent requests
60    #[serde(default = "default_false")]
61    pub enable_retry: bool,
62
63    /// Only retry idempotent methods (GET, HEAD, OPTIONS)
64    #[serde(default = "default_true")]
65    pub retry_idempotent_only: bool,
66
67    /// Maximum number of retries
68    #[serde(default = "default_max_retries")]
69    pub max_retries: u8,
70
71    /// Root directory for local file access (sandbox)
72    pub sandbox_root: Option<PathBuf>,
73
74    /// Maximum allowed size for local file read
75    #[serde(default = "default_max_file_bytes")]
76    pub max_local_file_bytes: usize,
77
78    /// Maximum allowed request/response body size for proxy inspection
79    #[serde(default = "default_max_body_bytes")]
80    pub max_body_size: usize,
81
82    /// Request timeout in milliseconds (connect + send request + receive response headers)
83    #[serde(default = "default_request_timeout_ms")]
84    pub request_timeout_ms: u64,
85
86    /// Enable transparent proxy mode
87    #[serde(default = "default_false")]
88    pub transparent_enabled: bool,
89
90    /// Require original destination to be present (strict mode)
91    #[serde(default = "default_true")]
92    pub transparent_require_original_dst: bool,
93
94    /// Allow fallback to Host header when original destination is missing
95    #[serde(default = "default_false")]
96    pub transparent_allow_host_fallback: bool,
97
98    /// Reject connections that would create a loop
99    #[serde(default = "default_true")]
100    pub transparent_reject_loopback_target: bool,
101
102    /// Log level for transparent proxy events
103    #[serde(default = "default_transparent_log_level")]
104    pub transparent_log_level: TransparentLogLevel,
105
106    /// QUIC handling mode
107    #[serde(default = "default_quic_mode")]
108    pub quic_mode: QuicMode,
109
110    /// Optionally emit Clear-Site-Data: "cache" to invalidate client Alt-Svc cache
111    #[serde(default = "default_false")]
112    pub quic_downgrade_clear_cache: bool,
113
114    #[serde(default)]
115    pub redaction: RedactionPolicy,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub enum TransparentLogLevel {
120    Silent,   // No logging
121    Info,     // Log connections only
122    Debug,    // Log with destination details
123    Trace,    // Full packet-level logging (expensive)
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub enum QuicMode {
128    /// Force clients to use HTTP/1.1 or HTTP/2
129    Downgrade,
130    
131    /// Pass through QUIC traffic without inspection
132    Passthrough,
133    
134    /// [EXPERIMENTAL] Full HTTP/3 MITM
135    #[cfg(feature = "quic_mitm_experimental")]
136    ExperimentalMitm,
137}
138
139fn default_quic_mode() -> QuicMode {
140    QuicMode::Downgrade
141}
142
143impl Default for ProxyPolicy {
144    fn default() -> Self {
145        Self {
146            strict_http_semantics: true,
147            allow_fallback_method: false,
148            allow_fallback_status: false,
149            enable_retry: false,
150            retry_idempotent_only: true,
151            max_retries: 3,
152            sandbox_root: None,
153            max_local_file_bytes: 10 * 1024 * 1024, // 10MB
154            max_body_size: 10 * 1024 * 1024, // 10MB
155            request_timeout_ms: 30_000, // 30 seconds
156            transparent_enabled: false,
157            transparent_require_original_dst: true,
158            transparent_allow_host_fallback: false,
159            transparent_reject_loopback_target: true,
160            transparent_log_level: TransparentLogLevel::Info,
161            quic_mode: QuicMode::Downgrade,
162            quic_downgrade_clear_cache: false,
163            redaction: RedactionPolicy::default(),
164        }
165    }
166}
167
168impl RedactionPolicy {
169    pub fn apply_patch(&mut self, patch: RedactionPolicyPatch) {
170        if let Some(enabled) = patch.enabled {
171            self.enabled = enabled;
172        }
173        if let Some(names) = patch.sensitive_header_names {
174            self.sensitive_header_names = names;
175        }
176        if let Some(keys) = patch.sensitive_query_keys {
177            self.sensitive_query_keys = keys;
178        }
179        if let Some(redact_bodies) = patch.redact_bodies {
180            self.redact_bodies = redact_bodies;
181        }
182    }
183}
184
185impl ProxyPolicy {
186    pub fn apply_patch(&mut self, patch: ProxyPolicyPatch) {
187        if let Some(redaction_patch) = patch.redaction {
188            self.redaction.apply_patch(redaction_patch);
189        }
190    }
191}
192
193fn default_true() -> bool { true }
194fn default_false() -> bool { false }
195fn default_max_retries() -> u8 { 3 }
196fn default_max_file_bytes() -> usize { 10 * 1024 * 1024 }
197fn default_max_body_bytes() -> usize { 10 * 1024 * 1024 }
198fn default_request_timeout_ms() -> u64 { 30_000 }
199fn default_transparent_log_level() -> TransparentLogLevel { TransparentLogLevel::Info }
200fn default_sensitive_header_names() -> Vec<String> {
201    vec![
202        "authorization".to_string(),
203        "proxy-authorization".to_string(),
204        "cookie".to_string(),
205        "set-cookie".to_string(),
206        "x-api-key".to_string(),
207        "x-auth-token".to_string(),
208    ]
209}
210fn default_sensitive_query_keys() -> Vec<String> {
211    vec![
212        "token".to_string(),
213        "access_token".to_string(),
214        "refresh_token".to_string(),
215        "api_key".to_string(),
216        "apikey".to_string(),
217        "password".to_string(),
218        "secret".to_string(),
219    ]
220}