nfm-common/src/ebpf_mocks.rs (229 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
/*
* This module contains scaffolding used to mock out interactions with eBPF. The purpose is to
* enable our sock-ops logic to be unit-testable in a user-space binary, given the lack of unit
* test support in Aya [a]. Everything here has counterparts within the `ebpf_actuals` module.
*
* [a] https://github.com/aya-rs/aya/issues/36 Support Unit Testing of BPF Programs
*/
use crate::constants::{MAX_ENTRIES_SK_PROPS_HI, MAX_ENTRIES_SK_STATS_HI};
use crate::network::{
ControlData, CpuSockKey, EventCounters, SingletonKey, SockContext, SockOpsStats, SockStats,
SINGLETON_KEY,
};
use libc::{EINVAL, ENOMEM};
use std::collections::HashMap;
use std::hash::Hash;
pub const MOCK_CPU_ID: u64 = 199;
// Constants we depend on from `aya-ebpf-bindings`. Note that the values here do not need to match
// those within Aya.
pub const BPF_ANY: u64 = 0;
pub const BPF_NOEXIST: u64 = 1 << 1;
pub const BPF_EXIST: u64 = 1 << 2;
pub const BPF_F_NO_PREALLOC: u32 = 1;
pub const BPF_SOCK_OPS_RETRANS_CB_FLAG: u32 = 1 << 1;
pub const BPF_SOCK_OPS_RTO_CB_FLAG: u32 = 1 << 2;
pub const BPF_SOCK_OPS_RTT_CB_FLAG: u32 = 1 << 3;
pub const BPF_SOCK_OPS_STATE_CB_FLAG: u32 = 1 << 4;
pub const BPF_SOCK_OPS_PARSE_ALL_HDR_OPT_CB_FLAG: u32 = 1 << 5;
pub const BPF_SOCK_OPS_WRITE_HDR_OPT_CB_FLAG: u32 = 1 << 6;
pub const BPF_SOCK_OPS_ALL_CB_FLAGS: u32 = 1 << 7;
pub const BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: u32 = 1;
pub const BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: u32 = 2;
pub const BPF_SOCK_OPS_RETRANS_CB: u32 = 3;
pub const BPF_SOCK_OPS_RTO_CB: u32 = 4;
pub const BPF_SOCK_OPS_RTT_CB: u32 = 5;
pub const BPF_SOCK_OPS_STATE_CB: u32 = 6;
pub const BPF_SOCK_OPS_TCP_CONNECT_CB: u32 = 7;
pub const BPF_SOCK_OPS_PARSE_HDR_OPT_CB: u32 = 8;
pub const BPF_SOCK_OPS_HDR_OPT_LEN_CB: u32 = 9;
pub const BPF_TCP_CLOSE: u32 = 1;
pub const BPF_TCP_CLOSE_WAIT: u32 = 2;
pub const BPF_TCP_CLOSING: u32 = 3;
pub const BPF_TCP_ESTABLISHED: u32 = 4;
pub const BPF_TCP_FIN_WAIT1: u32 = 5;
pub const BPF_TCP_FIN_WAIT2: u32 = 6;
pub const BPF_TCP_LAST_ACK: u32 = 7;
pub const BPF_TCP_LISTEN: u32 = 8;
pub const BPF_TCP_SYN_RECV: u32 = 9;
pub const BPF_TCP_SYN_SENT: u32 = 10;
pub const BPF_TCP_TIME_WAIT: u32 = 11;
// The BPF_MAP interface we depend on from `aya-ebpf`.
pub struct SharedHashMap<K, V>
where
K: Copy + Eq + Hash,
V: Copy,
{
pub(crate) data: HashMap<K, V>,
capacity: u32,
}
impl<K, V> SharedHashMap<K, V>
where
K: Copy + Eq + Hash,
V: Copy,
{
pub fn with_max_entries(capacity: u32) -> SharedHashMap<K, V> {
SharedHashMap {
data: HashMap::new(),
capacity,
}
}
pub fn insert(&mut self, key: &K, val: &V, flags: u64) -> Result<(), i64> {
let exists = self.data.contains_key(key);
let should_exist = (flags & BPF_EXIST) > 0;
let should_not_exist = (flags & BPF_NOEXIST) > 0;
if !exists && self.data.len() >= self.capacity.try_into().unwrap() {
Err((-ENOMEM).into())
} else if (should_exist && !exists) || (should_not_exist && exists) {
Err((-EINVAL).into())
} else {
self.data.insert(*key, *val);
Ok(())
}
}
pub fn get(&self, key: &K) -> Option<&V> {
self.data.get(key)
}
pub fn get_ptr_mut(&mut self, key: &K) -> Option<*mut V> {
self.data.get_mut(key).map(|v| v as *mut V)
}
}
pub type PerCpuHashMap<K, V> = SharedHashMap<K, V>;
// Instances of eBPF maps used by our program. These maps are housed within a struct, instead of
// defined globally, so that the invocation of each unit test function has its own local state.
// This allows for test invocations that are free of both concurrency management and any risk of
// polluted state.
#[allow(non_snake_case)]
pub struct MockEbpfMaps {
pub NFM_CONTROL: SharedHashMap<SingletonKey, ControlData>,
pub NFM_COUNTERS: PerCpuHashMap<SingletonKey, EventCounters>,
pub NFM_SK_PROPS: SharedHashMap<CpuSockKey, SockContext>,
pub NFM_SK_STATS: SharedHashMap<CpuSockKey, SockStats>,
pub mock_rand: u32,
}
impl MockEbpfMaps {
pub fn new() -> Self {
Self {
NFM_CONTROL: SharedHashMap::<SingletonKey, ControlData>::with_max_entries(1),
NFM_COUNTERS: PerCpuHashMap::<SingletonKey, EventCounters>::with_max_entries(1),
NFM_SK_PROPS: SharedHashMap::<CpuSockKey, SockContext>::with_max_entries(
MAX_ENTRIES_SK_PROPS_HI.try_into().unwrap(),
),
NFM_SK_STATS: SharedHashMap::<CpuSockKey, SockStats>::with_max_entries(
MAX_ENTRIES_SK_STATS_HI.try_into().unwrap(),
),
mock_rand: 1,
}
}
pub fn control_data(&self) -> &ControlData {
self.NFM_CONTROL.data.get(&SINGLETON_KEY).unwrap()
}
pub fn counters(&self) -> &EventCounters {
self.NFM_COUNTERS.data.get(&SINGLETON_KEY).unwrap()
}
pub fn sock_props(&self, key: &CpuSockKey) -> &SockContext {
self.NFM_SK_PROPS.data.get(key).unwrap()
}
pub fn sock_stats(&self, key: &CpuSockKey) -> &SockStats {
self.NFM_SK_STATS.data.get(key).unwrap()
}
}
impl Default for MockEbpfMaps {
fn default() -> Self {
Self::new()
}
}
// The context passed into our eBPF SOCK_OPS program by the kernel.
#[derive(Default)]
pub struct SockOpsContext {
pub op: u32,
pub family: u32,
pub cb_flags: i32,
pub remote_ip4: u32,
pub local_ip4: u32,
pub remote_ip6: [u32; 4],
pub local_ip6: [u32; 4],
pub local_port: u32,
pub remote_port: u32,
pub args: [u32; 2],
pub stats: SockOpsStats,
pub cookie: u64,
pub sock_state: u32,
}
// Match the function interfaces of an Aya SockOpsContext.
impl SockOpsContext {
pub fn op(&self) -> u32 {
self.op
}
pub fn family(&self) -> u32 {
self.family
}
pub fn cb_flags(&self) -> i32 {
self.cb_flags
}
pub fn set_cb_flags(&self, flags: i32) -> Result<(), i64> {
assert!(flags as u32 & BPF_SOCK_OPS_RTT_CB_FLAG > 0);
assert!(flags as u32 & BPF_SOCK_OPS_RTO_CB_FLAG > 0);
assert!(flags as u32 & BPF_SOCK_OPS_STATE_CB_FLAG > 0);
assert!(flags as u32 & BPF_SOCK_OPS_RETRANS_CB_FLAG > 0);
// Note that without this callback, we receive no BPF events for a socket that's only
// receiving data. The socket would then be treated as inactive and evicted from our
// cache.
assert!(flags as u32 & BPF_SOCK_OPS_PARSE_ALL_HDR_OPT_CB_FLAG > 0);
Ok(())
}
pub fn remote_ip4(&self) -> u32 {
self.remote_ip4
}
pub fn local_ip4(&self) -> u32 {
self.local_ip4
}
pub fn remote_ip6(&self) -> [u32; 4] {
self.remote_ip6
}
pub fn local_ip6(&self) -> [u32; 4] {
self.local_ip6
}
pub fn local_port(&self) -> u32 {
self.local_port
}
pub fn remote_port(&self) -> u32 {
self.remote_port
}
pub fn arg(&self, n: usize) -> u32 {
self.args[n]
}
}
// Operations on BPF maps.
#[macro_export]
macro_rules! bpf_map_get {
($self:ident, $map_name:ident, $key:expr) => {
$self.mock_ebpf_maps.$map_name.get($key)
};
}
#[macro_export]
macro_rules! bpf_map_get_ptr_mut {
($self:ident, $map_name:ident, $key:expr) => {
$self
.mock_ebpf_maps
.as_mut()
.unwrap()
.$map_name
.get_ptr_mut($key)
};
}
#[macro_export]
macro_rules! bpf_map_insert {
($self:ident, $map_name:ident, $key:expr, $val:expr, $flags:expr) => {
$self
.mock_ebpf_maps
.as_mut()
.unwrap()
.$map_name
.insert($key, $val, $flags)
};
}
#[macro_export]
macro_rules! bpf_get_rand_u32 {
($self:ident) => {
$self.mock_ebpf_maps.mock_rand
};
}
// BPF helper functions.
pub fn nfm_get_cpu_id() -> u64 {
MOCK_CPU_ID
}
pub fn nfm_get_sock_cookie(ctx: &SockOpsContext) -> u64 {
ctx.cookie
}
pub fn nfm_get_sock_state(ctx: &SockOpsContext) -> u32 {
ctx.sock_state
}
pub fn nfm_get_sock_ops_stats(ctx: &SockOpsContext) -> SockOpsStats {
ctx.stats
}