1
/// Opt-in panic guard that wraps a filter and catches panics at the trait boundary.
2
///
3
/// When a wrapped filter method panics, `CatchUnwind`:
4
/// 1. Drops the inner filter (preventing access to potentially inconsistent state).
5
/// 2. Logs the panic payload.
6
/// 3. Fails closed — sends a 500 response and/or returns `StopIteration` for HTTP filters, closes
7
///    the connection for network filters, etc.
8
///
9
/// Subsequent callbacks on a poisoned wrapper behave differently depending on type:
10
/// - Status-returning callbacks panic immediately. This indicates the fail-closed mechanism did not
11
///   terminate the stream as expected.
12
/// - Late async, event, and cleanup callbacks are silently skipped.
13
///
14
/// # Usage
15
///
16
/// ```ignore
17
/// fn new_http_filter(&self, envoy: &mut EHF) -> Box<dyn HttpFilter<EHF>> {
18
///     Box::new(CatchUnwind::new(MyFilter::new()))
19
/// }
20
/// ```
21
pub struct CatchUnwind<F> {
22
  filter: Option<F>,
23
}
24

            
25
enum CatchError {
26
  Panicked,
27
  Poisoned,
28
}
29

            
30
impl<F> CatchUnwind<F> {
31
  pub fn new(filter: F) -> Self {
32
    Self {
33
      filter: Some(filter),
34
    }
35
  }
36

            
37
  /// Run `f` on the inner filter, catching any panic.
38
  ///
39
  /// If the filter was already poisoned by a prior panic, panics immediately — a
40
  /// status-returning callback on a poisoned filter means the fail-closed mechanism
41
  /// didn't terminate the stream as expected.
42
  fn catch<R>(&mut self, name: &str, f: impl FnOnce(&mut F) -> R) -> Result<R, ()> {
43
    let mut filter = self
44
      .filter
45
      .take()
46
      .expect("status-returning callback invoked on a poisoned filter");
47
    // AssertUnwindSafe is sound here: if a panic occurs, `filter` is not put back into
48
    // `self.filter` — it is dropped at the end of this scope, so no code ever observes
49
    // the potentially inconsistent state.
50
    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&mut filter))) {
51
      Ok(val) => {
52
        self.filter = Some(filter);
53
        Ok(val)
54
      },
55
      Err(panic) => {
56
        crate::envoy_log_error!(
57
          "{}: caught panic: {}",
58
          name,
59
          crate::panic_payload_to_string(panic)
60
        );
61
        Err(())
62
      },
63
    }
64
  }
65

            
66
  /// Like [`catch`](Self::catch), but skips if the filter is already poisoned.
67
  ///
68
  /// This is intended for async, event, and cleanup callbacks that Envoy may still
69
  /// invoke after a prior fail-closed.
70
  ///
71
  /// Returns:
72
  /// - `Ok(R)` if the callback completed successfully.
73
  /// - `Err(CatchError::Panicked)` if this invocation panicked and the caller should apply its
74
  ///   fail-closed action.
75
  /// - `Err(CatchError::Poisoned)` if the wrapper was already poisoned and the callback was
76
  ///   skipped.
77
  fn catch_or_skip<R>(&mut self, name: &str, f: impl FnOnce(&mut F) -> R) -> Result<R, CatchError> {
78
    let Some(mut filter) = self.filter.take() else {
79
      return Err(CatchError::Poisoned);
80
    };
81
    // See `catch` for AssertUnwindSafe justification.
82
    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&mut filter))) {
83
      Ok(val) => {
84
        self.filter = Some(filter);
85
        Ok(val)
86
      },
87
      Err(panic) => {
88
        crate::envoy_log_error!(
89
          "{}: caught panic: {}",
90
          name,
91
          crate::panic_payload_to_string(panic)
92
        );
93
        Err(CatchError::Panicked)
94
      },
95
    }
96
  }
97
}
98

            
99
use crate::abi;
100
use crate::buffer::EnvoyBuffer;
101
// ---------------------------------------------------------------------------
102
// HttpFilter
103
// ---------------------------------------------------------------------------
104
use crate::http::{EnvoyHttpFilter, HttpFilter};
105

            
106
/// Fail-closed 500 response sent when an HTTP filter panics on the request path.
107
fn send_500<EHF: EnvoyHttpFilter>(envoy: &mut EHF) {
108
  envoy.send_response(
109
    500,
110
    &[("content-type", b"text/plain")],
111
    Some(b"Internal Server Error: filter panic"),
112
    None,
113
  );
114
}
115

            
116
/// Reset the stream when an HTTP filter panics on the response path.
117
/// We can't send a 500 at this point because response headers may already be sent downstream.
118
fn reset_stream<EHF: EnvoyHttpFilter>(envoy: &mut EHF) {
119
  envoy.reset_stream(
120
    abi::envoy_dynamic_module_type_http_filter_stream_reset_reason::LocalReset,
121
    "filter panic",
122
  );
123
}
124

            
125
impl<EHF: EnvoyHttpFilter, F: HttpFilter<EHF>> HttpFilter<EHF> for CatchUnwind<F> {
126
  // Request-path panics: send a 500 local reply which terminates the downstream request.
127
  // sendLocalReply sets `sent_local_reply_` on the C++ side, preventing further filter
128
  // iteration on the request path. The `on_local_reply` callback is handled below.
129
  fn on_request_headers(
130
    &mut self,
131
    envoy_filter: &mut EHF,
132
    end_of_stream: bool,
133
  ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status {
134
    self
135
      .catch("on_request_headers", |f| {
136
        f.on_request_headers(envoy_filter, end_of_stream)
137
      })
138
      .unwrap_or_else(|_| {
139
        send_500(envoy_filter);
140
        abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::StopIteration
141
      })
142
  }
143

            
144
  fn on_request_body(
145
    &mut self,
146
    envoy_filter: &mut EHF,
147
    end_of_stream: bool,
148
  ) -> abi::envoy_dynamic_module_type_on_http_filter_request_body_status {
149
    self
150
      .catch("on_request_body", |f| {
151
        f.on_request_body(envoy_filter, end_of_stream)
152
      })
153
      .unwrap_or_else(|_| {
154
        send_500(envoy_filter);
155
        abi::envoy_dynamic_module_type_on_http_filter_request_body_status::StopIterationNoBuffer
156
      })
157
  }
158

            
159
  fn on_request_trailers(
160
    &mut self,
161
    envoy_filter: &mut EHF,
162
  ) -> abi::envoy_dynamic_module_type_on_http_filter_request_trailers_status {
163
    self
164
      .catch("on_request_trailers", |f| {
165
        f.on_request_trailers(envoy_filter)
166
      })
167
      .unwrap_or_else(|_| {
168
        send_500(envoy_filter);
169
        abi::envoy_dynamic_module_type_on_http_filter_request_trailers_status::StopIteration
170
      })
171
  }
172

            
173
  // Response-path panics: can't send a 500 because response headers may already be sent
174
  // downstream. Instead, reset the stream (LocalReset) which tears down the connection.
175
  fn on_response_headers(
176
    &mut self,
177
    envoy_filter: &mut EHF,
178
    end_of_stream: bool,
179
  ) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status {
180
    self
181
      .catch("on_response_headers", |f| {
182
        f.on_response_headers(envoy_filter, end_of_stream)
183
      })
184
      .unwrap_or_else(|_| {
185
        reset_stream(envoy_filter);
186
        abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::StopIteration
187
      })
188
  }
189

            
190
  fn on_response_body(
191
    &mut self,
192
    envoy_filter: &mut EHF,
193
    end_of_stream: bool,
194
  ) -> abi::envoy_dynamic_module_type_on_http_filter_response_body_status {
195
    self
196
      .catch("on_response_body", |f| {
197
        f.on_response_body(envoy_filter, end_of_stream)
198
      })
199
      .unwrap_or_else(|_| {
200
        reset_stream(envoy_filter);
201
        abi::envoy_dynamic_module_type_on_http_filter_response_body_status::StopIterationNoBuffer
202
      })
203
  }
204

            
205
  fn on_response_trailers(
206
    &mut self,
207
    envoy_filter: &mut EHF,
208
  ) -> abi::envoy_dynamic_module_type_on_http_filter_response_trailers_status {
209
    self
210
      .catch("on_response_trailers", |f| {
211
        f.on_response_trailers(envoy_filter)
212
      })
213
      .unwrap_or_else(|_| {
214
        reset_stream(envoy_filter);
215
        abi::envoy_dynamic_module_type_on_http_filter_response_trailers_status::StopIteration
216
      })
217
  }
218

            
219
  // Void callbacks: cleanup/event notifications that Envoy may invoke after the stream is
220
  // already terminated. Safe to skip on a poisoned filter.
221
  fn on_stream_complete(&mut self, envoy_filter: &mut EHF) {
222
    let _ = self.catch_or_skip("on_stream_complete", |f| f.on_stream_complete(envoy_filter));
223
  }
224

            
225
  fn on_http_callout_done(
226
    &mut self,
227
    envoy_filter: &mut EHF,
228
    callout_id: u64,
229
    result: abi::envoy_dynamic_module_type_http_callout_result,
230
    response_headers: Option<&[(EnvoyBuffer, EnvoyBuffer)]>,
231
    response_body: Option<&[EnvoyBuffer]>,
232
  ) {
233
    if matches!(
234
      self.catch_or_skip("on_http_callout_done", |f| {
235
        f.on_http_callout_done(
236
          envoy_filter,
237
          callout_id,
238
          result,
239
          response_headers,
240
          response_body,
241
        )
242
      }),
243
      Err(CatchError::Panicked)
244
    ) {
245
      reset_stream(envoy_filter);
246
    }
247
  }
248

            
249
  fn on_http_stream_headers(
250
    &mut self,
251
    envoy_filter: &mut EHF,
252
    stream_handle: u64,
253
    response_headers: &[(EnvoyBuffer, EnvoyBuffer)],
254
    end_stream: bool,
255
  ) {
256
    if matches!(
257
      self.catch_or_skip("on_http_stream_headers", |f| {
258
        f.on_http_stream_headers(envoy_filter, stream_handle, response_headers, end_stream)
259
      }),
260
      Err(CatchError::Panicked)
261
    ) {
262
      reset_stream(envoy_filter);
263
    }
264
  }
265

            
266
  fn on_http_stream_data(
267
    &mut self,
268
    envoy_filter: &mut EHF,
269
    stream_handle: u64,
270
    response_data: &[EnvoyBuffer],
271
    end_stream: bool,
272
  ) {
273
    if matches!(
274
      self.catch_or_skip("on_http_stream_data", |f| {
275
        f.on_http_stream_data(envoy_filter, stream_handle, response_data, end_stream)
276
      }),
277
      Err(CatchError::Panicked)
278
    ) {
279
      reset_stream(envoy_filter);
280
    }
281
  }
282

            
283
  fn on_http_stream_trailers(
284
    &mut self,
285
    envoy_filter: &mut EHF,
286
    stream_handle: u64,
287
    response_trailers: &[(EnvoyBuffer, EnvoyBuffer)],
288
  ) {
289
    if matches!(
290
      self.catch_or_skip("on_http_stream_trailers", |f| {
291
        f.on_http_stream_trailers(envoy_filter, stream_handle, response_trailers)
292
      }),
293
      Err(CatchError::Panicked)
294
    ) {
295
      reset_stream(envoy_filter);
296
    }
297
  }
298

            
299
  fn on_http_stream_complete(&mut self, envoy_filter: &mut EHF, stream_handle: u64) {
300
    let _ = self.catch_or_skip("on_http_stream_complete", |f| {
301
      f.on_http_stream_complete(envoy_filter, stream_handle)
302
    });
303
  }
304

            
305
  fn on_http_stream_reset(
306
    &mut self,
307
    envoy_filter: &mut EHF,
308
    stream_handle: u64,
309
    reset_reason: abi::envoy_dynamic_module_type_http_stream_reset_reason,
310
  ) {
311
    let _ = self.catch_or_skip("on_http_stream_reset", |f| {
312
      f.on_http_stream_reset(envoy_filter, stream_handle, reset_reason)
313
    });
314
  }
315

            
316
  fn on_scheduled(&mut self, envoy_filter: &mut EHF, event_id: u64) {
317
    if matches!(
318
      self.catch_or_skip("on_scheduled", |f| f.on_scheduled(envoy_filter, event_id)),
319
      Err(CatchError::Panicked)
320
    ) {
321
      reset_stream(envoy_filter);
322
    }
323
  }
324

            
325
  fn on_downstream_above_write_buffer_high_watermark(&mut self, envoy_filter: &mut EHF) {
326
    let _ = self.catch_or_skip("on_downstream_above_write_buffer_high_watermark", |f| {
327
      f.on_downstream_above_write_buffer_high_watermark(envoy_filter)
328
    });
329
  }
330

            
331
  fn on_downstream_below_write_buffer_low_watermark(&mut self, envoy_filter: &mut EHF) {
332
    let _ = self.catch_or_skip("on_downstream_below_write_buffer_low_watermark", |f| {
333
      f.on_downstream_below_write_buffer_low_watermark(envoy_filter)
334
    });
335
  }
336

            
337
  // on_local_reply is invoked synchronously by sendLocalReply (triggered by send_500 above),
338
  // so it may be called while the filter is already poisoned. Must not abort in that case.
339
  fn on_local_reply(
340
    &mut self,
341
    envoy_filter: &mut EHF,
342
    response_code: u32,
343
    details: EnvoyBuffer,
344
    reset_imminent: bool,
345
  ) -> abi::envoy_dynamic_module_type_on_http_filter_local_reply_status {
346
    // send_500 triggers sendLocalReply synchronously, so this may be invoked while the
347
    // filter is already poisoned.
348
    self
349
      .catch_or_skip("on_local_reply", |f| {
350
        f.on_local_reply(envoy_filter, response_code, details, reset_imminent)
351
      })
352
      .unwrap_or(abi::envoy_dynamic_module_type_on_http_filter_local_reply_status::Continue)
353
  }
354
}
355

            
356
// ---------------------------------------------------------------------------
357
// NetworkFilter
358
// ---------------------------------------------------------------------------
359

            
360
use crate::network::{EnvoyNetworkFilter, NetworkFilter};
361

            
362
impl<ENF: EnvoyNetworkFilter, F: NetworkFilter<ENF>> NetworkFilter<ENF> for CatchUnwind<F> {
363
  // Data-path panics: close the connection (FlushWrite) to terminate gracefully.
364
  // After close, Envoy fires on_event(LocalClose) and on_destroy as cleanup.
365
  fn on_new_connection(
366
    &mut self,
367
    envoy_filter: &mut ENF,
368
  ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status {
369
    self
370
      .catch("on_new_connection", |f| f.on_new_connection(envoy_filter))
371
      .unwrap_or_else(|_| {
372
        envoy_filter
373
          .close(abi::envoy_dynamic_module_type_network_connection_close_type::FlushWrite);
374
        abi::envoy_dynamic_module_type_on_network_filter_data_status::StopIteration
375
      })
376
  }
377

            
378
  fn on_read(
379
    &mut self,
380
    envoy_filter: &mut ENF,
381
    data_length: usize,
382
    end_stream: bool,
383
  ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status {
384
    self
385
      .catch("on_read", |f| {
386
        f.on_read(envoy_filter, data_length, end_stream)
387
      })
388
      .unwrap_or_else(|_| {
389
        envoy_filter
390
          .close(abi::envoy_dynamic_module_type_network_connection_close_type::FlushWrite);
391
        abi::envoy_dynamic_module_type_on_network_filter_data_status::StopIteration
392
      })
393
  }
394

            
395
  fn on_write(
396
    &mut self,
397
    envoy_filter: &mut ENF,
398
    data_length: usize,
399
    end_stream: bool,
400
  ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status {
401
    self
402
      .catch("on_write", |f| {
403
        f.on_write(envoy_filter, data_length, end_stream)
404
      })
405
      .unwrap_or_else(|_| {
406
        envoy_filter
407
          .close(abi::envoy_dynamic_module_type_network_connection_close_type::FlushWrite);
408
        abi::envoy_dynamic_module_type_on_network_filter_data_status::StopIteration
409
      })
410
  }
411

            
412
  // Void callbacks: event/cleanup notifications that Envoy may invoke after close.
413
  fn on_event(
414
    &mut self,
415
    envoy_filter: &mut ENF,
416
    event: abi::envoy_dynamic_module_type_network_connection_event,
417
  ) {
418
    let _ = self.catch_or_skip("on_event", |f| f.on_event(envoy_filter, event));
419
  }
420

            
421
  fn on_http_callout_done(
422
    &mut self,
423
    envoy_filter: &mut ENF,
424
    callout_id: u64,
425
    result: abi::envoy_dynamic_module_type_http_callout_result,
426
    headers: Vec<(EnvoyBuffer, EnvoyBuffer)>,
427
    body_chunks: Vec<EnvoyBuffer>,
428
  ) {
429
    if matches!(
430
      self.catch_or_skip("on_http_callout_done", |f| {
431
        f.on_http_callout_done(envoy_filter, callout_id, result, headers, body_chunks)
432
      }),
433
      Err(CatchError::Panicked)
434
    ) {
435
      envoy_filter.close(abi::envoy_dynamic_module_type_network_connection_close_type::FlushWrite);
436
    }
437
  }
438

            
439
  fn on_destroy(&mut self, envoy_filter: &mut ENF) {
440
    let _ = self.catch_or_skip("on_destroy", |f| f.on_destroy(envoy_filter));
441
  }
442

            
443
  fn on_scheduled(&mut self, envoy_filter: &mut ENF, event_id: u64) {
444
    if matches!(
445
      self.catch_or_skip("on_scheduled", |f| f.on_scheduled(envoy_filter, event_id)),
446
      Err(CatchError::Panicked)
447
    ) {
448
      envoy_filter.close(abi::envoy_dynamic_module_type_network_connection_close_type::FlushWrite);
449
    }
450
  }
451

            
452
  fn on_above_write_buffer_high_watermark(&mut self, envoy_filter: &mut ENF) {
453
    let _ = self.catch_or_skip("on_above_write_buffer_high_watermark", |f| {
454
      f.on_above_write_buffer_high_watermark(envoy_filter)
455
    });
456
  }
457

            
458
  fn on_below_write_buffer_low_watermark(&mut self, envoy_filter: &mut ENF) {
459
    let _ = self.catch_or_skip("on_below_write_buffer_low_watermark", |f| {
460
      f.on_below_write_buffer_low_watermark(envoy_filter)
461
    });
462
  }
463
}
464

            
465
// ---------------------------------------------------------------------------
466
// ListenerFilter
467
// ---------------------------------------------------------------------------
468

            
469
use crate::listener::{EnvoyListenerFilter, ListenerFilter};
470

            
471
impl<ELF: EnvoyListenerFilter, F: ListenerFilter<ELF>> ListenerFilter<ELF> for CatchUnwind<F> {
472
  // Status-returning panics: close the socket to reject the connection immediately.
473
  fn on_accept(
474
    &mut self,
475
    envoy_filter: &mut ELF,
476
  ) -> abi::envoy_dynamic_module_type_on_listener_filter_status {
477
    self
478
      .catch("on_accept", |f| f.on_accept(envoy_filter))
479
      .unwrap_or_else(|_| {
480
        envoy_filter.close_socket(Some("filter panic"));
481
        abi::envoy_dynamic_module_type_on_listener_filter_status::StopIteration
482
      })
483
  }
484

            
485
  fn on_data(
486
    &mut self,
487
    envoy_filter: &mut ELF,
488
  ) -> abi::envoy_dynamic_module_type_on_listener_filter_status {
489
    self
490
      .catch("on_data", |f| f.on_data(envoy_filter))
491
      .unwrap_or_else(|_| {
492
        envoy_filter.close_socket(Some("filter panic"));
493
        abi::envoy_dynamic_module_type_on_listener_filter_status::StopIteration
494
      })
495
  }
496

            
497
  // Void callbacks: cleanup after the socket is already closed.
498
  fn on_close(&mut self, envoy_filter: &mut ELF) {
499
    let _ = self.catch_or_skip("on_close", |f| f.on_close(envoy_filter));
500
  }
501

            
502
  fn on_http_callout_done(
503
    &mut self,
504
    envoy_filter: &mut ELF,
505
    callout_id: u64,
506
    result: abi::envoy_dynamic_module_type_http_callout_result,
507
    response_headers: Vec<(EnvoyBuffer, EnvoyBuffer)>,
508
    response_body: Vec<EnvoyBuffer>,
509
  ) {
510
    if matches!(
511
      self.catch_or_skip("on_http_callout_done", |f| {
512
        f.on_http_callout_done(
513
          envoy_filter,
514
          callout_id,
515
          result,
516
          response_headers,
517
          response_body,
518
        )
519
      }),
520
      Err(CatchError::Panicked)
521
    ) {
522
      envoy_filter.close_socket(Some("filter panic"));
523
    }
524
  }
525

            
526
  fn on_scheduled(&mut self, envoy_filter: &mut ELF, event_id: u64) {
527
    if matches!(
528
      self.catch_or_skip("on_scheduled", |f| f.on_scheduled(envoy_filter, event_id)),
529
      Err(CatchError::Panicked)
530
    ) {
531
      envoy_filter.close_socket(Some("filter panic"));
532
    }
533
  }
534
}