#![allow(unsafe_code)]
#![deny(missing_debug_implementations)]
#![warn(missing_docs)]
use crate::error::AttachError;
use crate::xdp_attach::{XdpAttachMode, attach_xdp_raw, detach_xdp_raw};
use libbpf_rs::{Link, Object, ProgramType, TC_EGRESS, TC_INGRESS, TcAttachPoint, TcHook};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TcDirection {
Ingress,
Egress,
}
impl TcDirection {
#[inline]
fn to_attach_point(self) -> TcAttachPoint {
match self {
TcDirection::Ingress => TC_INGRESS,
TcDirection::Egress => TC_EGRESS,
}
}
pub fn as_str(self) -> &'static str {
match self {
TcDirection::Ingress => "ingress",
TcDirection::Egress => "egress",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CgroupAttachType {
SockOps,
InetIngress,
InetEgress,
SockCreate,
}
impl CgroupAttachType {
#[inline]
fn expected_prog_type(self) -> ProgramType {
match self {
CgroupAttachType::SockOps => ProgramType::SockOps,
CgroupAttachType::InetIngress
| CgroupAttachType::InetEgress
| CgroupAttachType::SockCreate => ProgramType::CgroupSkb,
}
}
pub fn as_str(self) -> &'static str {
match self {
CgroupAttachType::SockOps => "sock_ops",
CgroupAttachType::InetIngress => "inet_ingress",
CgroupAttachType::InetEgress => "inet_egress",
CgroupAttachType::SockCreate => "sock_create",
}
}
}
#[derive(Debug, Clone)]
pub enum AttachPoint {
Xdp {
ifindex: i32,
mode: XdpAttachMode,
},
Tc {
ifindex: i32,
direction: TcDirection,
replace: bool,
},
Cgroup {
cgroup_path: PathBuf,
attach_type: CgroupAttachType,
},
SocketFilter {
socket_fd: i32,
},
}
impl AttachPoint {
pub fn kind(&self) -> &'static str {
match self {
AttachPoint::Xdp { .. } => "xdp",
AttachPoint::Tc { .. } => "tc",
AttachPoint::Cgroup { .. } => "cgroup",
AttachPoint::SocketFilter { .. } => "socket_filter",
}
}
#[inline]
pub fn xdp_auto(ifindex: i32) -> Self {
AttachPoint::Xdp {
ifindex,
mode: XdpAttachMode::Auto,
}
}
#[inline]
pub fn xdp_with_mode(ifindex: i32, mode: XdpAttachMode) -> Self {
AttachPoint::Xdp { ifindex, mode }
}
#[inline]
pub fn tc_ingress(ifindex: i32) -> Self {
AttachPoint::Tc {
ifindex,
direction: TcDirection::Ingress,
replace: false,
}
}
#[inline]
pub fn tc_egress(ifindex: i32) -> Self {
AttachPoint::Tc {
ifindex,
direction: TcDirection::Egress,
replace: false,
}
}
#[inline]
pub fn cgroup_sock_ops<P: Into<PathBuf>>(cgroup_path: P) -> Self {
AttachPoint::Cgroup {
cgroup_path: cgroup_path.into(),
attach_type: CgroupAttachType::SockOps,
}
}
#[inline]
pub fn socket_filter(socket_fd: i32) -> Self {
AttachPoint::SocketFilter { socket_fd }
}
}
#[derive(Debug)]
pub struct AttachedHandle {
inner: Option<AttachedHandleInner>,
}
#[derive(Debug)]
enum AttachedHandleInner {
Xdp {
ifindex: i32,
mode: XdpAttachMode,
},
Tc {
direction: TcDirection,
hook: TcHook,
},
Cgroup {
link: Link,
#[allow(dead_code)]
cgroup_fd: OwnedFd,
},
SocketFilter {
socket_fd: i32,
},
}
impl AttachedHandle {
pub fn kind(&self) -> &'static str {
match &self.inner {
Some(AttachedHandleInner::Xdp { .. }) => "xdp",
Some(AttachedHandleInner::Tc { .. }) => "tc",
Some(AttachedHandleInner::Cgroup { .. }) => "cgroup",
Some(AttachedHandleInner::SocketFilter { .. }) => "socket_filter",
None => "detached",
}
}
pub fn is_detached(&self) -> bool {
self.inner.is_none()
}
pub fn detach(&mut self) -> Result<(), AttachError> {
if let Some(mut inner) = self.inner.take() {
detach_handle_inner(&mut inner)
} else {
Ok(())
}
}
}
impl Drop for AttachedHandle {
fn drop(&mut self) {
if let Some(mut inner) = self.inner.take() {
let _ = detach_handle_inner(&mut inner);
}
}
}
pub fn attach_program(
obj: &mut Object,
prog_name: &str,
point: &AttachPoint,
) -> Result<AttachedHandle, AttachError> {
match point {
AttachPoint::Xdp { ifindex, mode } => {
attach_xdp(obj, prog_name, *ifindex, *mode)
}
AttachPoint::Tc {
ifindex,
direction,
replace,
} => attach_tc(obj, prog_name, *ifindex, *direction, *replace),
AttachPoint::Cgroup {
cgroup_path,
attach_type,
} => attach_cgroup(obj, prog_name, cgroup_path, *attach_type),
AttachPoint::SocketFilter { socket_fd } => {
attach_socket_filter(obj, prog_name, *socket_fd)
}
}
}
pub fn detach_program(handle: &mut AttachedHandle) -> Result<(), AttachError> {
handle.detach()
}
fn attach_xdp(
obj: &mut Object,
prog_name: &str,
ifindex: i32,
mode: XdpAttachMode,
) -> Result<AttachedHandle, AttachError> {
let prog = find_program_by_name_mut(obj, prog_name)?;
validate_prog_type(&prog, ProgramType::Xdp, "xdp")?;
let prog_fd: i32 = prog.as_fd().as_raw_fd();
let actual_mode = attach_xdp_raw(ifindex, prog_fd, mode)?;
Ok(AttachedHandle {
inner: Some(AttachedHandleInner::Xdp {
ifindex,
mode: actual_mode,
}),
})
}
fn attach_tc(
obj: &mut Object,
prog_name: &str,
ifindex: i32,
direction: TcDirection,
replace: bool,
) -> Result<AttachedHandle, AttachError> {
let prog = find_program_by_name_mut(obj, prog_name)?;
validate_prog_type(&prog, ProgramType::SchedCls, "tc")?;
let prog_fd_borrowed: BorrowedFd<'_> = prog.as_fd();
let mut hook = TcHook::new(prog_fd_borrowed);
hook.ifindex(ifindex);
hook.attach_point(direction.to_attach_point());
if replace {
hook.replace(true);
}
hook.create()
.map_err(|e| AttachError::Libbpf(format!("TC create clsact qdisc 失败: {}", e)))?;
hook.attach()
.map_err(|e| AttachError::Libbpf(format!("TC attach 失败 ({}): {}", direction.as_str(), e)))?;
Ok(AttachedHandle {
inner: Some(AttachedHandleInner::Tc { direction, hook }),
})
}
fn attach_cgroup(
obj: &mut Object,
prog_name: &str,
cgroup_path: &std::path::Path,
attach_type: CgroupAttachType,
) -> Result<AttachedHandle, AttachError> {
if !cgroup_path.exists() {
return Err(AttachError::InterfaceNotFound(format!(
"cgroup 路径不存在: {}",
cgroup_path.display()
)));
}
let prog = find_program_by_name_mut(obj, prog_name)?;
validate_prog_type(&prog, attach_type.expected_prog_type(), "cgroup")?;
let cgroup_path_cstr = std::ffi::CString::new(cgroup_path.as_os_str().to_str().ok_or_else(|| {
AttachError::InvalidFlags(format!(
"cgroup 路径包含非 UTF-8 字符: {}",
cgroup_path.display()
))
})?)
.map_err(|e| AttachError::InvalidFlags(format!("CString 转换失败: {}", e)))?;
let cgroup_fd_raw = unsafe {
libc::open(
cgroup_path_cstr.as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY,
)
};
if cgroup_fd_raw < 0 {
let err = std::io::Error::last_os_error();
return Err(AttachError::Libbpf(format!(
"打开 cgroup 路径失败 ({}): {} (errno={})",
cgroup_path.display(),
err,
err.raw_os_error().unwrap_or(0)
)));
}
let cgroup_fd = unsafe { OwnedFd::from_raw_fd(cgroup_fd_raw) };
let link = prog
.attach_cgroup(cgroup_fd.as_raw_fd())
.map_err(|e| {
AttachError::Libbpf(format!(
"cgroup attach 失败 (type={}): {}",
attach_type.as_str(),
e
))
})?;
Ok(AttachedHandle {
inner: Some(AttachedHandleInner::Cgroup { link, cgroup_fd }),
})
}
fn attach_socket_filter(
obj: &mut Object,
prog_name: &str,
socket_fd: i32,
) -> Result<AttachedHandle, AttachError> {
if socket_fd < 0 {
return Err(AttachError::InvalidFlags(format!(
"无效的 socket fd: {}",
socket_fd
)));
}
let prog = find_program_by_name_mut(obj, prog_name)?;
validate_prog_type(&prog, ProgramType::SocketFilter, "socket_filter")?;
let prog_fd: i32 = prog.as_fd().as_raw_fd();
let ret = unsafe {
libc::setsockopt(
socket_fd,
libc::SOL_SOCKET,
libc::SO_ATTACH_BPF,
&prog_fd as *const i32 as *const libc::c_void,
std::mem::size_of::<i32>() as libc::socklen_t,
)
};
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(AttachError::Libbpf(format!(
"SO_ATTACH_BPF 失败: {} (errno={})",
err,
err.raw_os_error().unwrap_or(0)
)));
}
Ok(AttachedHandle {
inner: Some(AttachedHandleInner::SocketFilter { socket_fd }),
})
}
fn find_program_by_name_mut<'a>(
obj: &'a mut Object,
prog_name: &str,
) -> Result<libbpf_rs::ProgramMut<'a>, AttachError> {
obj.progs_mut()
.find(|p| p.name().to_str().unwrap_or("") == prog_name)
.ok_or_else(|| AttachError::Libbpf(format!("程序 {} 未找到", prog_name)))
}
#[inline]
fn validate_prog_type(
prog: &libbpf_rs::ProgramMut<'_>,
expected: ProgramType,
attach_kind: &str,
) -> Result<(), AttachError> {
let actual = prog.prog_type();
if actual as u32 != expected as u32 {
return Err(AttachError::InvalidFlags(format!(
"程序类型不匹配(attach={}):期望 {:?},实际 {:?}。\
请确保 BPF 程序的 SEC() 声明与 attach 点匹配。",
attach_kind, expected, actual
)));
}
Ok(())
}
fn detach_handle_inner(handle: &mut AttachedHandleInner) -> Result<(), AttachError> {
match handle {
AttachedHandleInner::Xdp { ifindex, mode } => {
detach_xdp_raw(*ifindex, *mode)?;
Ok(())
}
AttachedHandleInner::Tc {
hook, direction, ..
} => {
hook.detach().map_err(|e| {
AttachError::Libbpf(format!(
"TC detach 失败 ({}): {}",
direction.as_str(),
e
))
})
}
AttachedHandleInner::Cgroup { link, .. } => {
link.detach().map_err(|e| {
AttachError::Libbpf(format!("cgroup detach 失败: {}", e))
})
}
AttachedHandleInner::SocketFilter { socket_fd } => {
let ret = unsafe {
libc::setsockopt(
*socket_fd,
libc::SOL_SOCKET,
libc::SO_DETACH_BPF,
std::ptr::null(),
0,
)
};
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(AttachError::Libbpf(format!(
"SO_DETACH_BPF 失败: {} (errno={})",
err,
err.raw_os_error().unwrap_or(0)
)));
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tc_direction_as_str() {
assert_eq!(TcDirection::Ingress.as_str(), "ingress");
assert_eq!(TcDirection::Egress.as_str(), "egress");
}
#[test]
fn test_tc_direction_to_attach_point() {
assert_eq!(TcDirection::Ingress.to_attach_point(), TC_INGRESS);
assert_eq!(TcDirection::Egress.to_attach_point(), TC_EGRESS);
}
#[test]
fn test_tc_direction_eq() {
assert_eq!(TcDirection::Ingress, TcDirection::Ingress);
assert_ne!(TcDirection::Ingress, TcDirection::Egress);
}
#[test]
fn test_cgroup_attach_type_as_str() {
assert_eq!(CgroupAttachType::SockOps.as_str(), "sock_ops");
assert_eq!(CgroupAttachType::InetIngress.as_str(), "inet_ingress");
assert_eq!(CgroupAttachType::InetEgress.as_str(), "inet_egress");
assert_eq!(CgroupAttachType::SockCreate.as_str(), "sock_create");
}
#[test]
fn test_cgroup_attach_type_unique() {
let types = [
CgroupAttachType::SockOps,
CgroupAttachType::InetIngress,
CgroupAttachType::InetEgress,
CgroupAttachType::SockCreate,
];
for (i, a) in types.iter().enumerate() {
for (j, b) in types.iter().enumerate() {
if i != j {
assert_ne!(a, b);
assert_ne!(a.as_str(), b.as_str());
}
}
}
assert_ne!(
CgroupAttachType::SockOps.expected_prog_type() as u32,
CgroupAttachType::InetIngress.expected_prog_type() as u32
);
assert_eq!(
CgroupAttachType::InetIngress.expected_prog_type() as u32,
CgroupAttachType::InetEgress.expected_prog_type() as u32
);
assert_eq!(
CgroupAttachType::InetEgress.expected_prog_type() as u32,
CgroupAttachType::SockCreate.expected_prog_type() as u32
);
}
#[test]
fn test_attach_point_kind() {
let xdp = AttachPoint::Xdp {
ifindex: 1,
mode: XdpAttachMode::Auto,
};
assert_eq!(xdp.kind(), "xdp");
let tc = AttachPoint::Tc {
ifindex: 1,
direction: TcDirection::Ingress,
replace: false,
};
assert_eq!(tc.kind(), "tc");
let cg = AttachPoint::Cgroup {
cgroup_path: PathBuf::from("/sys/fs/cgroup/"),
attach_type: CgroupAttachType::SockOps,
};
assert_eq!(cg.kind(), "cgroup");
let sf = AttachPoint::SocketFilter { socket_fd: 3 };
assert_eq!(sf.kind(), "socket_filter");
}
#[test]
fn test_attach_point_clone() {
let p1 = AttachPoint::Xdp {
ifindex: 1,
mode: XdpAttachMode::Drv,
};
let p2 = p1.clone();
if let AttachPoint::Xdp { ifindex, mode } = &p2 {
assert_eq!(*ifindex, 1);
assert_eq!(*mode, XdpAttachMode::Drv);
} else {
panic!("clone 后类型应为 Xdp");
}
}
#[test]
fn test_attach_point_debug() {
let p = AttachPoint::Tc {
ifindex: 1,
direction: TcDirection::Egress,
replace: true,
};
let s = format!("{:?}", p);
assert!(s.contains("Tc"));
assert!(s.contains("Egress"));
}
#[test]
fn test_cgroup_attach_type_all_variants() {
let variants = [
CgroupAttachType::SockOps,
CgroupAttachType::InetIngress,
CgroupAttachType::InetEgress,
CgroupAttachType::SockCreate,
];
assert_eq!(variants.len(), 4);
for v in variants.iter() {
assert!(!v.as_str().is_empty());
}
}
#[test]
fn test_tc_direction_all_variants() {
let variants = [TcDirection::Ingress, TcDirection::Egress];
assert_eq!(variants.len(), 2);
for v in variants.iter() {
assert!(!v.as_str().is_empty());
}
}
#[test]
fn test_attach_point_xdp_auto() {
let p = AttachPoint::xdp_auto(2);
assert_eq!(p.kind(), "xdp");
if let AttachPoint::Xdp { ifindex, mode } = p {
assert_eq!(ifindex, 2);
assert_eq!(mode, XdpAttachMode::Auto);
} else {
panic!("应为 Xdp 变体");
}
}
#[test]
fn test_attach_point_xdp_with_mode() {
let p = AttachPoint::xdp_with_mode(3, XdpAttachMode::Drv);
if let AttachPoint::Xdp { ifindex, mode } = p {
assert_eq!(ifindex, 3);
assert_eq!(mode, XdpAttachMode::Drv);
} else {
panic!("应为 Xdp 变体");
}
}
#[test]
fn test_attach_point_tc_ingress() {
let p = AttachPoint::tc_ingress(5);
if let AttachPoint::Tc {
ifindex,
direction,
replace,
} = p
{
assert_eq!(ifindex, 5);
assert_eq!(direction, TcDirection::Ingress);
assert!(!replace);
} else {
panic!("应为 Tc 变体");
}
}
#[test]
fn test_attach_point_tc_egress() {
let p = AttachPoint::tc_egress(7);
if let AttachPoint::Tc { direction, .. } = p {
assert_eq!(direction, TcDirection::Egress);
} else {
panic!("应为 Tc 变体");
}
}
#[test]
fn test_attach_point_cgroup_sock_ops() {
let p = AttachPoint::cgroup_sock_ops("/sys/fs/cgroup/test");
if let AttachPoint::Cgroup {
cgroup_path,
attach_type,
} = p
{
assert_eq!(cgroup_path, PathBuf::from("/sys/fs/cgroup/test"));
assert_eq!(attach_type, CgroupAttachType::SockOps);
} else {
panic!("应为 Cgroup 变体");
}
}
#[test]
fn test_attach_point_socket_filter() {
let p = AttachPoint::socket_filter(42);
if let AttachPoint::SocketFilter { socket_fd } = p {
assert_eq!(socket_fd, 42);
} else {
panic!("应为 SocketFilter 变体");
}
}
#[test]
fn test_cgroup_attach_type_expected_prog_type() {
assert_eq!(
CgroupAttachType::SockOps.expected_prog_type() as u32,
ProgramType::SockOps as u32
);
assert_eq!(
CgroupAttachType::InetIngress.expected_prog_type() as u32,
ProgramType::CgroupSkb as u32
);
assert_eq!(
CgroupAttachType::InetEgress.expected_prog_type() as u32,
ProgramType::CgroupSkb as u32
);
assert_eq!(
CgroupAttachType::SockCreate.expected_prog_type() as u32,
ProgramType::CgroupSkb as u32
);
}
#[test]
fn test_cgroup_attach_type_prog_type_consistency() {
for _ in 0..10 {
assert_eq!(
CgroupAttachType::SockOps.expected_prog_type() as u32,
ProgramType::SockOps as u32
);
}
}
#[test]
fn test_attached_handle_is_detached_initial() {
let handle = AttachedHandle { inner: None };
assert!(handle.is_detached());
assert_eq!(handle.kind(), "detached");
}
#[test]
fn test_attached_handle_detach_when_already_detached() {
let mut handle = AttachedHandle { inner: None };
let result = handle.detach();
assert!(result.is_ok(), "detach 幂等应返回 Ok: {:?}", result.err());
}
}