use std::io;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("packet capture error: {0}")]
Capture(String),
#[error("TLS parse error: {0}")]
Parse(String),
#[error("connection tracking error: {0}")]
Tracker(String),
#[error("UI error: {0}")]
Ui(String),
#[error("configuration error: {0}")]
Config(String),
#[error(transparent)]
Io(#[from] io::Error),
}
impl Error {
#[must_use]
pub fn is_permission_denied(&self) -> bool {
matches!(
self,
Error::Capture(msg)
if msg.contains("permission")
|| msg.contains("Operation not permitted")
|| msg.contains("Permission denied")
)
}
#[must_use]
pub fn exit_code(&self) -> u8 {
match self {
_ if self.is_permission_denied() => 77,
Error::Parse(_) => 65,
Error::Config(_) => 78,
_ => 1,
}
}
}
#[must_use]
pub fn permission_denied_hint() -> &'static str {
#[cfg(target_os = "linux")]
{
"hint: live capture needs raw-socket privileges. Grant them once with:\n \
sudo setcap cap_net_raw,cap_net_admin=eip $(command -v seehandshake)\n\
then re-run. Re-apply setcap after every upgrade — capabilities live on \
the inode and are cleared when the binary is replaced."
}
#[cfg(target_os = "macos")]
{
"hint: live capture needs access to the BPF devices. Grant it with:\n \
sudo chown $USER /dev/bpf*\n\
or run the binary with sudo."
}
#[cfg(target_os = "windows")]
{
"hint: live capture needs Npcap and an elevated shell. Install Npcap from \
https://npcap.com/#download, then run seehandshake from a terminal \
opened as Administrator."
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"hint: live capture needs elevated privileges to open a raw socket; \
re-run with the appropriate permissions for your platform."
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permission_capture_errors_are_detected() {
for msg in [
"socket: Operation not permitted",
"You don't have permission to capture on that device",
"Permission denied",
] {
let err = Error::Capture(msg.to_string());
assert!(err.is_permission_denied(), "should flag: {msg}");
assert_eq!(err.exit_code(), 77);
}
}
#[test]
fn non_permission_capture_errors_are_not_flagged() {
let err = Error::Capture("no such device exists".to_string());
assert!(!err.is_permission_denied());
assert_eq!(err.exit_code(), 1);
}
#[test]
fn non_capture_errors_are_never_permission_denied() {
assert!(!Error::Config("bad flag".to_string()).is_permission_denied());
assert!(!Error::Parse("bad bytes".to_string()).is_permission_denied());
}
#[test]
fn permission_hint_is_non_empty() {
assert!(!permission_denied_hint().is_empty());
}
}