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    /// P1: Maximum bytes to buffer for rule body inspection.
83    /// When exceeded, body-stage rules are skipped with budget_exceeded tag.
84    /// Default: 1 MB. Set to 0 to disable rule body inspection entirely.
85    #[serde(default = "default_rule_body_inspect_budget")]
86    pub rule_body_inspect_budget: usize,
87
88    /// Request timeout in milliseconds (connect + send request + receive response headers)
89    #[serde(default = "default_request_timeout_ms")]
90    pub request_timeout_ms: u64,
91
92    /// Enable transparent proxy mode
93    #[serde(default = "default_false")]
94    pub transparent_enabled: bool,
95
96    /// Require original destination to be present (strict mode)
97    #[serde(default = "default_true")]
98    pub transparent_require_original_dst: bool,
99
100    /// Allow fallback to Host header when original destination is missing
101    #[serde(default = "default_false")]
102    pub transparent_allow_host_fallback: bool,
103
104    /// Reject connections that would create a loop
105    #[serde(default = "default_true")]
106    pub transparent_reject_loopback_target: bool,
107
108    /// Log level for transparent proxy events
109    #[serde(default = "default_transparent_log_level")]
110    pub transparent_log_level: TransparentLogLevel,
111
112    /// QUIC handling mode
113    #[serde(default = "default_quic_mode")]
114    pub quic_mode: QuicMode,
115
116    /// Optionally emit Clear-Site-Data: "cache" to invalidate client Alt-Svc cache
117    #[serde(default = "default_false")]
118    pub quic_downgrade_clear_cache: bool,
119
120    #[serde(default)]
121    pub redaction: RedactionPolicy,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125pub enum TransparentLogLevel {
126    Silent, // No logging
127    Info,   // Log connections only
128    Debug,  // Log with destination details
129    Trace,  // Full packet-level logging (expensive)
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133pub enum QuicMode {
134    /// Force clients to use HTTP/1.1 or HTTP/2
135    Downgrade,
136
137    /// Pass through QUIC traffic without inspection
138    Passthrough,
139
140    /// [EXPERIMENTAL] Full HTTP/3 MITM
141    #[cfg(feature = "quic_mitm_experimental")]
142    ExperimentalMitm,
143}
144
145fn default_quic_mode() -> QuicMode {
146    QuicMode::Downgrade
147}
148
149impl Default for ProxyPolicy {
150    fn default() -> Self {
151        Self {
152            strict_http_semantics: true,
153            allow_fallback_method: false,
154            allow_fallback_status: false,
155            enable_retry: false,
156            retry_idempotent_only: true,
157            max_retries: 3,
158            sandbox_root: None,
159            max_local_file_bytes: 10 * 1024 * 1024, // 10MB
160            max_body_size: 10 * 1024 * 1024,        // 10MB
161            rule_body_inspect_budget: 1024 * 1024,  // 1MB
162            request_timeout_ms: 30_000,             // 30 seconds
163            transparent_enabled: false,
164            transparent_require_original_dst: true,
165            transparent_allow_host_fallback: false,
166            transparent_reject_loopback_target: true,
167            transparent_log_level: TransparentLogLevel::Info,
168            quic_mode: QuicMode::Downgrade,
169            quic_downgrade_clear_cache: false,
170            redaction: RedactionPolicy::default(),
171        }
172    }
173}
174
175impl RedactionPolicy {
176    pub fn apply_patch(&mut self, patch: RedactionPolicyPatch) {
177        if let Some(enabled) = patch.enabled {
178            self.enabled = enabled;
179        }
180        if let Some(names) = patch.sensitive_header_names {
181            self.sensitive_header_names = names;
182        }
183        if let Some(keys) = patch.sensitive_query_keys {
184            self.sensitive_query_keys = keys;
185        }
186        if let Some(redact_bodies) = patch.redact_bodies {
187            self.redact_bodies = redact_bodies;
188        }
189    }
190}
191
192impl ProxyPolicy {
193    pub fn apply_patch(&mut self, patch: ProxyPolicyPatch) {
194        if let Some(redaction_patch) = patch.redaction {
195            self.redaction.apply_patch(redaction_patch);
196        }
197    }
198}
199
200fn default_true() -> bool {
201    true
202}
203fn default_false() -> bool {
204    false
205}
206fn default_max_retries() -> u8 {
207    3
208}
209fn default_max_file_bytes() -> usize {
210    10 * 1024 * 1024
211}
212fn default_max_body_bytes() -> usize {
213    10 * 1024 * 1024
214}
215fn default_rule_body_inspect_budget() -> usize {
216    1024 * 1024 // 1MB
217}
218fn default_request_timeout_ms() -> u64 {
219    30_000
220}
221fn default_transparent_log_level() -> TransparentLogLevel {
222    TransparentLogLevel::Info
223}
224fn default_sensitive_header_names() -> Vec<String> {
225    vec![
226        "authorization".to_string(),
227        "proxy-authorization".to_string(),
228        "cookie".to_string(),
229        "set-cookie".to_string(),
230        "x-api-key".to_string(),
231        "x-auth-token".to_string(),
232    ]
233}
234fn default_sensitive_query_keys() -> Vec<String> {
235    vec![
236        "token".to_string(),
237        "access_token".to_string(),
238        "refresh_token".to_string(),
239        "api_key".to_string(),
240        "apikey".to_string(),
241        "password".to_string(),
242        "secret".to_string(),
243    ]
244}