#[cfg(target_os = "linux")]
use aya::{maps::Array, programs::UProbe, Ebpf};
use crate::error::EbpfError;
pub struct UprobeManager {
target_pid: Option<i32>,
#[cfg(target_os = "linux")]
_links: Vec<Box<dyn std::any::Any>>,
}
impl UprobeManager {
#[cfg(target_os = "linux")]
pub fn attach(bpf: &mut Ebpf, target_pid: Option<i32>) -> Result<Self, EbpfError> {
{
let map = bpf.map_mut("TARGET_PID").ok_or_else(|| EbpfError::MapNotFound {
name: "TARGET_PID".into(),
})?;
let mut pid_map: Array<_, u32> = Array::try_from(map)?;
let pid_val: u32 = target_pid.map(|p| p as u32).unwrap_or(0);
pid_map.set(0, pid_val, 0)?;
}
let ssl_path = find_openssl_path(target_pid)?;
let mut links: Vec<Box<dyn std::any::Any>> = Vec::with_capacity(3);
{
let prog: &mut UProbe = bpf
.program_mut("ssl_write")
.ok_or_else(|| EbpfError::ProgramNotFound {
name: "ssl_write".into(),
})?
.try_into()?;
load_program(prog, "ssl_write")?;
links.push(Box::new(prog.attach(Some("SSL_write"), 0, &ssl_path, target_pid)?));
}
{
let prog: &mut UProbe = bpf
.program_mut("ssl_read_entry")
.ok_or_else(|| EbpfError::ProgramNotFound {
name: "ssl_read_entry".into(),
})?
.try_into()?;
load_program(prog, "ssl_read_entry")?;
links.push(Box::new(prog.attach(Some("SSL_read"), 0, &ssl_path, target_pid)?));
}
{
let prog: &mut UProbe = bpf
.program_mut("ssl_read_exit")
.ok_or_else(|| EbpfError::ProgramNotFound {
name: "ssl_read_exit".into(),
})?
.try_into()?;
load_program(prog, "ssl_read_exit")?;
links.push(Box::new(prog.attach(Some("SSL_read"), 0, &ssl_path, target_pid)?));
}
Ok(Self {
target_pid,
_links: links,
})
}
#[cfg(not(target_os = "linux"))]
pub fn attach(_bpf: &mut (), _target_pid: Option<i32>) -> Result<Self, EbpfError> {
Err(EbpfError::ProgramNotFound {
name: "uprobe attachment requires Linux".into(),
})
}
}
impl Drop for UprobeManager {
fn drop(&mut self) {
#[cfg(target_os = "linux")]
let count = self._links.len();
#[cfg(not(target_os = "linux"))]
let count = 0_usize;
tracing::debug!(
target_pid = ?self.target_pid,
probe_count = count,
"detaching uprobe links",
);
}
}
#[cfg(target_os = "linux")]
fn load_program(prog: &mut UProbe, name: &str) -> Result<(), EbpfError> {
prog.load().map_err(|e| {
let msg = e.to_string();
if msg.contains("EPERM") || msg.contains("Operation not permitted") {
EbpfError::PermissionDenied {
detail: format!("loading program `{name}` requires CAP_BPF + CAP_PERFMON (or root)"),
}
} else {
EbpfError::Program(e)
}
})
}
#[cfg(target_os = "linux")]
static LIBSSL_FALLBACK_PATHS: &[&str] = &[
"/usr/lib/x86_64-linux-gnu/libssl.so.3",
"/usr/lib/x86_64-linux-gnu/libssl.so.1.1",
"/usr/lib64/libssl.so.3",
"/usr/lib64/libssl.so.1.1",
"/usr/lib/libssl.so.3",
"/usr/lib/libssl.so.1.1",
"/usr/local/lib/libssl.so",
"/usr/local/lib64/libssl.so",
];
#[cfg(target_os = "linux")]
fn find_openssl_path(target_pid: Option<i32>) -> Result<String, EbpfError> {
let pid_str = target_pid.map(|p| p.to_string()).unwrap_or_else(|| "self".to_string());
let maps_path = format!("/proc/{}/maps", pid_str);
let maps_result = std::fs::read_to_string(&maps_path);
match (maps_result, target_pid) {
(Err(e), Some(_)) => return Err(EbpfError::Io(e)),
(Ok(content), _) => {
for line in content.lines() {
if let Some(pathname) = line.split_whitespace().last() {
if pathname.contains("libssl.so") {
return Ok(pathname.to_string());
}
}
}
}
(Err(_), None) => { }
}
for &path in LIBSSL_FALLBACK_PATHS {
if std::path::Path::new(path).exists() {
return Ok(path.to_string());
}
}
Err(EbpfError::OpenSslNotFound { pid: target_pid })
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn find_openssl_path_nonexistent_pid_returns_io_error() {
let result = find_openssl_path(Some(4_194_303));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, EbpfError::Io(_)), "expected Io error, got: {err}",);
}
#[cfg(target_os = "linux")]
#[test]
fn find_openssl_path_system_wide_falls_through_to_filesystem() {
let result = find_openssl_path(None);
match &result {
Ok(path) => assert!(path.contains("libssl.so"), "unexpected path: {path}"),
Err(EbpfError::OpenSslNotFound { .. }) => { }
Err(e) => panic!("unexpected error variant: {e}"),
}
}
}