1use std::net::SocketAddr;
2use std::path::PathBuf;
3
4use crate::destination::parse_destination_rule;
5use crate::MitmError;
6use crate::TlsVersion;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum InterceptMode {
14 Monitor,
17 Enforce,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
28pub struct MitmConfig {
29 pub bind: SocketAddr,
30 pub unix_socket_path: Option<PathBuf>,
31 pub interception: InterceptionScope,
32 pub process_attribution: ProcessAttributionConfig,
33 pub tls: TlsConfig,
34 pub http2_enabled: bool,
35 pub http2_max_header_list_size: u32,
36 pub http3_passthrough: bool,
37 pub max_http_head_bytes: usize,
38 pub accept_retry_backoff_ms: u64,
39 pub max_flow_event_backlog: usize,
40 pub max_in_flight_bytes: usize,
41 pub max_concurrent_flows: usize,
42 pub upstream: UpstreamConfig,
43 pub connection_pool: ConnectionPoolConfig,
44 pub body: BodyConfig,
45 pub intercept_mode: InterceptMode,
46 pub handler: HandlerConfig,
47 pub flow_runtime: FlowRuntimeConfig,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[non_exhaustive]
52pub struct InterceptionScope {
53 pub destinations: Vec<String>,
54 pub passthrough_unlisted: bool,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct TlsConfig {
60 pub ca_cert_path: PathBuf,
61 pub ca_key_path: PathBuf,
62 pub min_version: TlsVersion,
63 pub capture_fingerprint: bool,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct ProcessAttributionConfig {
69 pub enabled: bool,
70 pub lookup_timeout_ms: u64,
71 pub cache_capacity: usize,
72 pub cache_ttl_ms: Option<u64>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct UpstreamConfig {
78 pub timeout_ms: u64,
79 pub h2_header_stage_timeout_ms: u64,
80 pub h2_body_idle_timeout_ms: u64,
81 pub h2_response_overflow_mode: H2ResponseOverflowMode,
82 pub connect_timeout_ms: u64,
83 pub retry_on_failure: bool,
84 pub retry_delay_ms: u64,
85 pub verify_upstream_tls: bool,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum H2ResponseOverflowMode {
90 TruncateContinue,
91 StrictFail,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct ConnectionPoolConfig {
97 pub max_connections_per_host: u32,
98 pub idle_timeout_ms: u64,
99 pub max_idle_per_host: u32,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103#[non_exhaustive]
104pub struct BodyConfig {
105 pub max_size_bytes: usize,
106 pub buffer_request_bodies: bool,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110#[non_exhaustive]
111pub struct HandlerConfig {
112 pub request_timeout_ms: u64,
113 pub response_timeout_ms: u64,
114 pub recover_from_panics: bool,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[non_exhaustive]
119pub struct FlowRuntimeConfig {
120 pub dispatch_queue_capacity: Option<usize>,
121 pub closed_flow_lru_capacity: Option<usize>,
122 pub stale_flow_ttl_ms: Option<u64>,
123 pub stale_reap_max_batch: Option<usize>,
124 pub dispatch_queue_send_timeout_ms: Option<u64>,
125 pub dispatch_close_join_timeout_ms: Option<u64>,
126}
127
128impl Default for MitmConfig {
129 fn default() -> Self {
130 Self {
131 bind: "127.0.0.1:8080"
132 .parse()
133 .expect("default bind address must parse"),
134 unix_socket_path: None,
135 interception: InterceptionScope::default(),
136 process_attribution: ProcessAttributionConfig::default(),
137 tls: TlsConfig::default(),
138 http2_enabled: true,
139 http2_max_header_list_size: 64 * 1024,
140 http3_passthrough: true,
141 max_http_head_bytes: 64 * 1024,
142 accept_retry_backoff_ms: 100,
143 max_flow_event_backlog: 8 * 1024,
144 max_in_flight_bytes: 64 * 1024 * 1024,
145 max_concurrent_flows: 2_048,
146 upstream: UpstreamConfig::default(),
147 connection_pool: ConnectionPoolConfig::default(),
148 body: BodyConfig::default(),
149 intercept_mode: InterceptMode::Monitor,
150 handler: HandlerConfig::default(),
151 flow_runtime: FlowRuntimeConfig::default(),
152 }
153 }
154}
155
156impl Default for InterceptionScope {
157 fn default() -> Self {
158 Self {
159 destinations: Vec::new(),
160 passthrough_unlisted: true,
161 }
162 }
163}
164
165impl Default for TlsConfig {
166 fn default() -> Self {
167 Self {
168 ca_cert_path: PathBuf::from("./certs/soth-mitm-ca.pem"),
169 ca_key_path: PathBuf::from("./certs/soth-mitm-ca-key.pem"),
170 min_version: TlsVersion::Tls12,
171 capture_fingerprint: true,
172 }
173 }
174}
175
176impl Default for ProcessAttributionConfig {
177 fn default() -> Self {
178 Self {
179 enabled: true,
180 lookup_timeout_ms: 5_000,
181 cache_capacity: 4_096,
182 cache_ttl_ms: Some(300_000), }
184 }
185}
186
187impl Default for UpstreamConfig {
188 fn default() -> Self {
189 Self {
190 timeout_ms: 30_000,
191 h2_header_stage_timeout_ms: 30_000,
192 h2_body_idle_timeout_ms: 120_000,
193 h2_response_overflow_mode: H2ResponseOverflowMode::TruncateContinue,
194 connect_timeout_ms: 10_000,
195 retry_on_failure: false,
196 retry_delay_ms: 200,
197 verify_upstream_tls: true,
198 }
199 }
200}
201
202impl Default for ConnectionPoolConfig {
203 fn default() -> Self {
204 Self {
205 max_connections_per_host: 32,
206 idle_timeout_ms: 600_000,
207 max_idle_per_host: 8,
208 }
209 }
210}
211
212impl Default for BodyConfig {
213 fn default() -> Self {
214 Self {
215 max_size_bytes: 32 * 1024 * 1024,
216 buffer_request_bodies: false,
217 }
218 }
219}
220
221impl Default for HandlerConfig {
222 fn default() -> Self {
223 Self {
224 request_timeout_ms: 15_000,
225 response_timeout_ms: 15_000,
226 recover_from_panics: true,
227 }
228 }
229}
230
231impl Default for FlowRuntimeConfig {
232 fn default() -> Self {
233 Self {
234 dispatch_queue_capacity: None, closed_flow_lru_capacity: Some(4_096), stale_flow_ttl_ms: Some(60_000), stale_reap_max_batch: Some(50), dispatch_queue_send_timeout_ms: None, dispatch_close_join_timeout_ms: None, }
241 }
242}
243
244impl MitmConfig {
245 pub fn validate(&self) -> Result<(), MitmError> {
246 if self.interception.destinations.is_empty() {
247 return Err(MitmError::InvalidConfig(
248 "interception.destinations must not be empty".to_string(),
249 ));
250 }
251 for destination in &self.interception.destinations {
252 parse_destination_rule(destination)?;
253 }
254 if self.process_attribution.enabled && self.process_attribution.lookup_timeout_ms == 0 {
255 return Err(MitmError::InvalidConfig(
256 "process_attribution.lookup_timeout_ms must be greater than zero".to_string(),
257 ));
258 }
259 if self.process_attribution.cache_capacity == 0 {
260 return Err(MitmError::InvalidConfig(
261 "process_attribution.cache_capacity must be greater than zero".to_string(),
262 ));
263 }
264 if self.process_attribution.cache_ttl_ms == Some(0) {
265 return Err(MitmError::InvalidConfig(
266 "process_attribution.cache_ttl_ms must be greater than zero when set".to_string(),
267 ));
268 }
269 if self.max_http_head_bytes == 0 {
270 return Err(MitmError::InvalidConfig(
271 "max_http_head_bytes must be greater than zero".to_string(),
272 ));
273 }
274 if self.accept_retry_backoff_ms == 0 {
275 return Err(MitmError::InvalidConfig(
276 "accept_retry_backoff_ms must be greater than zero".to_string(),
277 ));
278 }
279 if self.http2_max_header_list_size == 0 {
280 return Err(MitmError::InvalidConfig(
281 "http2_max_header_list_size must be greater than zero".to_string(),
282 ));
283 }
284 if self.max_flow_event_backlog == 0 {
285 return Err(MitmError::InvalidConfig(
286 "max_flow_event_backlog must be greater than zero".to_string(),
287 ));
288 }
289 if self.max_in_flight_bytes == 0 {
290 return Err(MitmError::InvalidConfig(
291 "max_in_flight_bytes must be greater than zero".to_string(),
292 ));
293 }
294 if self.max_concurrent_flows == 0 {
295 return Err(MitmError::InvalidConfig(
296 "max_concurrent_flows must be greater than zero".to_string(),
297 ));
298 }
299 if self.upstream.timeout_ms == 0 {
300 return Err(MitmError::InvalidConfig(
301 "upstream.timeout_ms must be greater than zero".to_string(),
302 ));
303 }
304 if self.upstream.h2_header_stage_timeout_ms == 0 {
305 return Err(MitmError::InvalidConfig(
306 "upstream.h2_header_stage_timeout_ms must be greater than zero".to_string(),
307 ));
308 }
309 if self.upstream.h2_body_idle_timeout_ms == 0 {
310 return Err(MitmError::InvalidConfig(
311 "upstream.h2_body_idle_timeout_ms must be greater than zero".to_string(),
312 ));
313 }
314 if self.upstream.connect_timeout_ms == 0 {
315 return Err(MitmError::InvalidConfig(
316 "upstream.connect_timeout_ms must be greater than zero".to_string(),
317 ));
318 }
319 if self.body.max_size_bytes == 0 {
320 return Err(MitmError::InvalidConfig(
321 "body.max_size_bytes must be greater than zero".to_string(),
322 ));
323 }
324 if self.handler.request_timeout_ms == 0 {
325 return Err(MitmError::InvalidConfig(
326 "handler.request_timeout_ms must be greater than zero".to_string(),
327 ));
328 }
329 if self.handler.response_timeout_ms == 0 {
330 return Err(MitmError::InvalidConfig(
331 "handler.response_timeout_ms must be greater than zero".to_string(),
332 ));
333 }
334 if self.flow_runtime.dispatch_queue_capacity == Some(0) {
335 return Err(MitmError::InvalidConfig(
336 "flow_runtime.dispatch_queue_capacity must be greater than zero when set"
337 .to_string(),
338 ));
339 }
340 if self.flow_runtime.closed_flow_lru_capacity == Some(0) {
341 return Err(MitmError::InvalidConfig(
342 "flow_runtime.closed_flow_lru_capacity must be greater than zero when set"
343 .to_string(),
344 ));
345 }
346 if self.flow_runtime.stale_flow_ttl_ms == Some(0) {
347 return Err(MitmError::InvalidConfig(
348 "flow_runtime.stale_flow_ttl_ms must be greater than zero when set".to_string(),
349 ));
350 }
351 if self.flow_runtime.stale_reap_max_batch == Some(0) {
352 return Err(MitmError::InvalidConfig(
353 "flow_runtime.stale_reap_max_batch must be greater than zero when set".to_string(),
354 ));
355 }
356 if self.flow_runtime.dispatch_queue_send_timeout_ms == Some(0) {
357 return Err(MitmError::InvalidConfig(
358 "flow_runtime.dispatch_queue_send_timeout_ms must be greater than zero when set"
359 .to_string(),
360 ));
361 }
362 if self.flow_runtime.dispatch_close_join_timeout_ms == Some(0) {
363 return Err(MitmError::InvalidConfig(
364 "flow_runtime.dispatch_close_join_timeout_ms must be greater than zero when set"
365 .to_string(),
366 ));
367 }
368 if self.connection_pool.max_connections_per_host == 0 {
369 return Err(MitmError::InvalidConfig(
370 "connection_pool.max_connections_per_host must be greater than zero".to_string(),
371 ));
372 }
373 if self.connection_pool.idle_timeout_ms == 0 {
374 return Err(MitmError::InvalidConfig(
375 "connection_pool.idle_timeout_ms must be greater than zero".to_string(),
376 ));
377 }
378 if self.connection_pool.max_idle_per_host == 0 {
379 return Err(MitmError::InvalidConfig(
380 "connection_pool.max_idle_per_host must be greater than zero".to_string(),
381 ));
382 }
383 Ok(())
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::MitmConfig;
390
391 fn valid_config() -> MitmConfig {
392 let mut config = MitmConfig::default();
393 config
394 .interception
395 .destinations
396 .push("api.example.com:443".to_string());
397 config
398 }
399
400 #[test]
401 fn default_runtime_knobs_match_expected_values() {
402 let config = MitmConfig::default();
403 assert!(config.http2_enabled);
404 assert_eq!(config.http2_max_header_list_size, 64 * 1024);
405 assert!(config.http3_passthrough);
406 assert_eq!(config.max_http_head_bytes, 64 * 1024);
407 assert_eq!(config.accept_retry_backoff_ms, 100);
408 assert_eq!(config.max_flow_event_backlog, 8 * 1024);
409 assert_eq!(config.max_in_flight_bytes, 64 * 1024 * 1024);
410 assert_eq!(config.max_concurrent_flows, 2_048);
411 assert_eq!(config.process_attribution.cache_capacity, 4_096);
412 assert_eq!(config.process_attribution.cache_ttl_ms, Some(300_000));
413 assert_eq!(config.upstream.h2_header_stage_timeout_ms, 30_000);
414 assert_eq!(config.upstream.h2_body_idle_timeout_ms, 120_000);
415 assert_eq!(config.body.max_size_bytes, 32 * 1024 * 1024);
416 assert_eq!(config.handler.request_timeout_ms, 15_000);
417 assert_eq!(config.handler.response_timeout_ms, 15_000);
418 }
419
420 #[test]
421 fn validate_rejects_zero_core_runtime_knobs() {
422 let mut config = valid_config();
423 config.max_concurrent_flows = 0;
424 let error = config
425 .validate()
426 .expect_err("zero runtime budget must fail");
427 let message = error.to_string();
428 assert!(message.contains("max_concurrent_flows"));
429 }
430
431 #[test]
432 fn validate_rejects_zero_h2_timeout_knobs() {
433 let mut config = valid_config();
434 config.upstream.h2_header_stage_timeout_ms = 0;
435 let error = config
436 .validate()
437 .expect_err("zero h2 header timeout must fail");
438 assert!(error.to_string().contains("h2_header_stage_timeout_ms"));
439
440 config.upstream.h2_header_stage_timeout_ms = 30_000;
441 config.upstream.h2_body_idle_timeout_ms = 0;
442 let error = config
443 .validate()
444 .expect_err("zero h2 body idle timeout must fail");
445 assert!(error.to_string().contains("h2_body_idle_timeout_ms"));
446 }
447
448 #[test]
449 fn validate_rejects_zero_flow_runtime_overrides() {
450 let mut config = valid_config();
451 config.flow_runtime.dispatch_queue_capacity = Some(0);
452 let error = config.validate().expect_err("zero flow override must fail");
453 let message = error.to_string();
454 assert!(message.contains("flow_runtime.dispatch_queue_capacity"));
455 }
456}