use crate::error::Error;
use crate::packet::Packet;
use crate::pcap::CaptureWriter;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TapErrorPolicy {
#[default]
Continue,
DropTap,
FailStream,
}
pub(crate) trait TapWriter: Send {
fn write(&mut self, pkt: &Packet<'_>) -> Result<(), pcap_file::PcapError>;
}
impl<W: std::io::Write + Send + 'static> TapWriter for CaptureWriter<W> {
fn write(&mut self, pkt: &Packet<'_>) -> Result<(), pcap_file::PcapError> {
self.write_packet(pkt)
}
}
pub struct PcapTap {
inner: Box<dyn TapWriter>,
policy: TapErrorPolicy,
dropped: bool,
}
impl PcapTap {
pub(crate) fn new<W>(writer: CaptureWriter<W>, policy: TapErrorPolicy) -> Self
where
W: std::io::Write + Send + 'static,
{
Self::from_writer(Box::new(writer), policy)
}
pub(crate) fn from_writer(writer: Box<dyn TapWriter>, policy: TapErrorPolicy) -> Self {
Self {
inner: writer,
policy,
dropped: false,
}
}
pub(crate) fn write_or_handle(&mut self, pkt: &Packet<'_>) -> Option<Error> {
if self.dropped {
return None;
}
let result = self.inner.write(pkt);
self.handle_result(result)
}
fn handle_result(&mut self, result: Result<(), pcap_file::PcapError>) -> Option<Error> {
match result {
Ok(()) => None,
Err(err) => match self.policy {
TapErrorPolicy::Continue => {
tracing::warn!(
target: "netring::pcap_tap",
?err,
"pcap tap write failed; continuing (policy = Continue)"
);
None
}
TapErrorPolicy::DropTap => {
tracing::warn!(
target: "netring::pcap_tap",
?err,
"pcap tap write failed; dropping tap (policy = DropTap)"
);
self.dropped = true;
None
}
TapErrorPolicy::FailStream => Some(Error::Io(std::io::Error::other(format!(
"pcap tap write failed: {err}"
)))),
},
}
}
pub fn is_dropped(&self) -> bool {
self.dropped
}
}
#[cfg(test)]
mod tests {
use super::*;
struct NoopWriter;
impl TapWriter for NoopWriter {
fn write(&mut self, _pkt: &Packet<'_>) -> Result<(), pcap_file::PcapError> {
Ok(())
}
}
fn tap_with(policy: TapErrorPolicy) -> PcapTap {
PcapTap::from_writer(Box::new(NoopWriter), policy)
}
fn simulated_failure() -> Result<(), pcap_file::PcapError> {
Err(pcap_file::PcapError::IoError(std::io::Error::other(
"simulated",
)))
}
#[test]
fn continue_policy_swallows_errors() {
let mut tap = tap_with(TapErrorPolicy::Continue);
for _ in 0..3 {
assert!(tap.handle_result(simulated_failure()).is_none());
}
assert!(!tap.is_dropped(), "Continue keeps the tap alive");
}
#[test]
fn drop_policy_retires_tap_after_first_failure() {
let mut tap = tap_with(TapErrorPolicy::DropTap);
assert!(tap.handle_result(simulated_failure()).is_none());
assert!(tap.is_dropped());
}
#[test]
fn fail_stream_policy_surfaces_error() {
let mut tap = tap_with(TapErrorPolicy::FailStream);
let err = tap
.handle_result(simulated_failure())
.expect("FailStream propagates");
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn ok_path_no_error_no_drop() {
let mut tap = tap_with(TapErrorPolicy::FailStream);
for _ in 0..5 {
assert!(tap.handle_result(Ok(())).is_none());
}
assert!(!tap.is_dropped());
}
#[test]
fn default_policy_is_continue() {
assert_eq!(TapErrorPolicy::default(), TapErrorPolicy::Continue);
}
}