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