use std::net::Ipv4Addr;
#[cfg(target_os = "linux")]
const OBJECT: &[u8] = include_bytes!("../../bpf/sandbox_nat.bpf.o");
#[cfg(target_os = "linux")]
const NAT_MAP: &str = "SANDBOX_NAT";
#[cfg(target_os = "linux")]
const POOL_MAP: &str = "SANDBOX_NAT_POOL";
#[cfg(target_os = "linux")]
const INGRESS_PROG: &str = "sandbox_nat_ingress";
#[cfg(target_os = "linux")]
const EGRESS_PROG: &str = "sandbox_nat_egress";
pub(super) fn pool_ip_value(ip: Ipv4Addr) -> u32 {
u32::from(ip).to_be()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Attach {
Done,
EngineUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PoolValues([u32; 3]);
impl PoolValues {
pub(super) fn new(base: Ipv4Addr, prefix_len: u8, gateway: Ipv4Addr) -> Self {
let mask = !0u32 << (32 - u32::from(prefix_len.min(32)));
Self([
(u32::from(base) & mask).to_be(),
mask.to_be(),
pool_ip_value(gateway),
])
}
#[cfg(target_os = "linux")]
fn entries(self) -> impl Iterator<Item = (u32, u32)> {
self.0.into_iter().enumerate().map(|(i, v)| (i as u32, v))
}
}
#[cfg(target_os = "linux")]
pub(super) use linux::Engine;
#[cfg(target_os = "linux")]
mod linux {
use std::collections::HashMap;
use std::net::Ipv4Addr;
use aya::maps::MapError;
use aya::programs::tc::{SchedClassifierLink, TcAttachOptions};
use aya::programs::{LinkOrder, SchedClassifier, TcAttachType};
use tracing::warn;
use super::{EGRESS_PROG, INGRESS_PROG, NAT_MAP, OBJECT, POOL_MAP, PoolValues, pool_ip_value};
use crate::error::{Result, VmmError};
pub(in super::super) enum Engine {
Unloaded,
Ready(Box<EbpfNat>),
Unavailable,
}
impl Engine {
pub(in super::super) fn ensure_loaded(&mut self, pool: PoolValues) -> Option<&mut EbpfNat> {
if matches!(self, Self::Unloaded) {
*self = match EbpfNat::load(pool) {
Ok(nat) => Self::Ready(Box::new(nat)),
Err(error) => {
warn!(
%error,
"eBPF sandbox datapath unavailable; using iptables NAT"
);
Self::Unavailable
}
};
}
self.loaded_mut()
}
pub(in super::super) fn loaded_mut(&mut self) -> Option<&mut EbpfNat> {
match self {
Self::Ready(nat) => Some(nat),
Self::Unloaded | Self::Unavailable => None,
}
}
}
pub(in super::super) struct EbpfNat {
ebpf: aya::Ebpf,
attachments: HashMap<String, TapAttachment>,
}
struct TapAttachment {
ifindex: u32,
_ingress: SchedClassifierLink,
_egress: SchedClassifierLink,
}
impl EbpfNat {
fn load(pool: PoolValues) -> Result<Self> {
let mut ebpf = aya::Ebpf::load(OBJECT)
.map_err(|e| VmmError::Network(format!("load sandbox NAT BPF object: {e}")))?;
{
let map = ebpf.map_mut(POOL_MAP).ok_or_else(|| missing(POOL_MAP))?;
let mut array = aya::maps::Array::<_, u32>::try_from(map)
.map_err(|e| VmmError::Network(format!("map {POOL_MAP}: {e}")))?;
for (index, value) in pool.entries() {
array.set(index, value, 0).map_err(|e| {
VmmError::Network(format!("write {POOL_MAP}[{index}]: {e}"))
})?;
}
}
for name in [INGRESS_PROG, EGRESS_PROG] {
classifier(&mut ebpf, name)?
.load()
.map_err(|e| VmmError::Network(format!("load program {name}: {e}")))?;
}
Ok(Self {
ebpf,
attachments: HashMap::new(),
})
}
pub(in super::super) fn attach(
&mut self,
tap: &str,
ifindex: u32,
pool_ip: Ipv4Addr,
) -> Result<()> {
let ingress = attach_one(&mut self.ebpf, INGRESS_PROG, tap, TcAttachType::Ingress)?;
let egress = attach_one(&mut self.ebpf, EGRESS_PROG, tap, TcAttachType::Egress)?;
let mut map = nat_map(&mut self.ebpf)?;
map.insert(ifindex, pool_ip_value(pool_ip), 0)
.map_err(|e| VmmError::Network(format!("{NAT_MAP} insert for {tap}: {e}")))?;
self.attachments.insert(
tap.to_owned(),
TapAttachment {
ifindex,
_ingress: ingress,
_egress: egress,
},
);
Ok(())
}
pub(in super::super) fn detach(&mut self, tap: &str) -> Result<()> {
let Some(attachment) = self.attachments.remove(tap) else {
return Ok(());
};
let mut map = nat_map(&mut self.ebpf)?;
match map.remove(&attachment.ifindex) {
Ok(()) => Ok(()),
Err(MapError::SyscallError(ref syscall))
if syscall.io_error.raw_os_error() == Some(libc::ENOENT) =>
{
Ok(())
}
Err(e) => Err(VmmError::Network(format!(
"{NAT_MAP} remove for {tap}: {e}"
))),
}
}
}
fn missing(name: &str) -> VmmError {
VmmError::Network(format!("sandbox NAT object has no {name:?}"))
}
fn classifier<'e>(ebpf: &'e mut aya::Ebpf, name: &str) -> Result<&'e mut SchedClassifier> {
ebpf.program_mut(name)
.ok_or_else(|| missing(name))?
.try_into()
.map_err(|e| VmmError::Network(format!("program {name}: {e}")))
}
fn nat_map(
ebpf: &mut aya::Ebpf,
) -> Result<aya::maps::HashMap<&mut aya::maps::MapData, u32, u32>> {
aya::maps::HashMap::try_from(ebpf.map_mut(NAT_MAP).ok_or_else(|| missing(NAT_MAP))?)
.map_err(|e| VmmError::Network(format!("map {NAT_MAP}: {e}")))
}
fn attach_one(
ebpf: &mut aya::Ebpf,
name: &str,
tap: &str,
attach_type: TcAttachType,
) -> Result<SchedClassifierLink> {
let program = classifier(ebpf, name)?;
let link_id = program
.attach_with_options(
tap,
attach_type,
TcAttachOptions::TcxOrder(LinkOrder::default()),
)
.map_err(|e| VmmError::Network(format!("TCX attach {name} on {tap}: {e}")))?;
program
.take_link(link_id)
.map_err(|e| VmmError::Network(format!("own TCX link {name} on {tap}: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_ip_value_bytes_are_network_order() {
let ip: Ipv4Addr = "172.20.3.17".parse().unwrap();
assert_eq!(pool_ip_value(ip).to_ne_bytes(), ip.octets());
}
#[test]
fn pool_values_mask_the_base_and_keep_network_order() {
let pool = PoolValues::new(
"172.20.7.9".parse().unwrap(), 16,
"172.20.0.1".parse().unwrap(),
);
assert_eq!(
pool,
PoolValues([
u32::from_ne_bytes([172, 20, 0, 0]),
u32::from_ne_bytes([255, 255, 0, 0]),
u32::from_ne_bytes([172, 20, 0, 1]),
])
);
}
}