use std::io;
use std::net::{IpAddr, Ipv6Addr};
use std::os::fd::AsRawFd;
use std::sync::Mutex;
use super::insn::{
Asm, BPF_FUNC_MAP_LOOKUP_ELEM, BPF_FUNC_REDIRECT_MAP, Insn, Jmp, R0, R1, R2, R3, R6, R7, R8,
R10, Size, host_be16, ld_map_fd,
};
use super::map::{Map, UpdateFlags, lpm_key};
use super::prog::{Action, Link, Mode, Program};
use crate::{EtherType, IpPrefix, Result};
const ETH_HLEN: i32 = 14;
const ETH_TYPE: i16 = 12;
const IPV4_SRC: i16 = ETH_HLEN as i16 + 12;
const IPV4_DST: i16 = ETH_HLEN as i16 + 16;
const IPV4_MIN: i32 = ETH_HLEN + 20;
const IPV6_SRC: i16 = ETH_HLEN as i16 + 8;
const IPV6_DST: i16 = ETH_HLEN as i16 + 24;
const IPV6_MIN: i32 = ETH_HLEN + 40;
const ARP_PTYPE: i16 = ETH_HLEN as i16 + 2;
const ARP_SPA: i16 = ETH_HLEN as i16 + 14;
const ARP_TPA: i16 = ETH_HLEN as i16 + 24;
const ARP_MIN: i32 = ETH_HLEN + 28;
const XDP_MD_RX_QUEUE_INDEX: i16 = 16;
const V4_DST_KEY: i16 = -8;
const V4_SRC_KEY: i16 = -16;
const V6_DST_KEY: i16 = -40;
const V6_SRC_KEY: i16 = -64;
const _: () = {
assert!(V4_DST_KEY % 4 == 0 && V4_SRC_KEY % 4 == 0);
assert!(V6_DST_KEY % 4 == 0 && V6_SRC_KEY % 4 == 0);
assert!(V4_SRC_KEY + 8 <= V4_DST_KEY, "v4 keys overlap");
assert!(
V6_DST_KEY + 20 <= V4_SRC_KEY,
"v6 dst key overlaps a v4 key"
);
assert!(V6_SRC_KEY + 20 <= V6_DST_KEY, "v6 keys overlap");
assert!(V6_SRC_KEY > -512, "keys exceed the BPF stack");
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MatchField {
#[default]
Dst,
Src,
Either,
}
impl MatchField {
#[inline]
fn wants_dst(self) -> bool {
matches!(self, MatchField::Dst | MatchField::Either)
}
#[inline]
fn wants_src(self) -> bool {
matches!(self, MatchField::Src | MatchField::Either)
}
}
#[derive(Debug, Clone)]
pub struct CaptureConfig {
pub match_field: MatchField,
pub arp: bool,
pub neighbor_discovery: bool,
pub default_action: Action,
pub min_prefix_v4: u8,
pub min_prefix_v6: u8,
pub max_prefixes: u32,
pub max_queues: u32,
}
impl Default for CaptureConfig {
fn default() -> CaptureConfig {
CaptureConfig {
match_field: MatchField::Dst,
arp: true,
neighbor_discovery: true,
default_action: Action::PASS,
min_prefix_v4: 1,
min_prefix_v6: 1,
max_prefixes: 1024,
max_queues: 64,
}
}
}
impl CaptureConfig {
pub fn validate(&self) -> Result<()> {
if self.min_prefix_v4 == 0 || self.min_prefix_v6 == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"xdp: min_prefix_v4/min_prefix_v6 must be at least 1; a /0 matches every packet on the interface",
));
}
if self.min_prefix_v4 > 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("xdp: min_prefix_v4 is /{}, max is /32", self.min_prefix_v4),
));
}
if self.min_prefix_v6 > 128 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("xdp: min_prefix_v6 is /{}, max is /128", self.min_prefix_v6),
));
}
if self.default_action != Action::PASS && self.default_action != Action::DROP {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"xdp: default_action must be PASS or DROP, got {:?}",
self.default_action
),
));
}
if self.max_prefixes == 0 || self.max_queues == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"xdp: max_prefixes and max_queues must be non-zero",
));
}
Ok(())
}
pub fn check_prefix(&self, prefix: IpPrefix) -> Result<()> {
let (min, family) = if prefix.is_v4() {
(self.min_prefix_v4.max(1), "IPv4")
} else {
(self.min_prefix_v6.max(1), "IPv6")
};
if prefix.bits() >= min {
return Ok(());
}
let why = if prefix.bits() == 0 {
" — a /0 matches every packet on the interface".to_string()
} else {
format!(" — the {family} floor is /{min}")
};
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("xdp: refusing to capture {prefix}{why}"),
))
}
}
fn coverage(prefixes: &[IpPrefix], v4: bool) -> u128 {
let width: u32 = if v4 { 32 } else { 128 };
prefixes
.iter()
.filter(|p| p.is_v4() == v4)
.fold(0u128, |acc, p| {
let host_bits = width - u32::from(p.bits()).min(width);
let n = 1u128.checked_shl(host_bits).unwrap_or(u128::MAX);
acc.saturating_add(n)
})
}
fn family_total(v4: bool) -> u128 {
if v4 { 1u128 << 32 } else { u128::MAX }
}
fn check_coverage(held: &[IpPrefix], new: IpPrefix) -> Result<()> {
let v4 = new.is_v4();
let mut combined = held.to_vec();
combined.push(new);
if coverage(&combined, v4) >= family_total(v4) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"xdp: refusing to capture {new}: it would leave the capture set covering every {} address on the interface",
if v4 { "IPv4" } else { "IPv6" }
),
));
}
Ok(())
}
#[derive(Debug)]
pub struct CaptureMaps {
pub xskmap: Map,
pub v4: Map,
pub v6: Map,
}
impl CaptureMaps {
pub fn create(cfg: &CaptureConfig) -> Result<CaptureMaps> {
Ok(CaptureMaps {
xskmap: Map::xskmap(cfg.max_queues)?,
v4: Map::lpm_trie(4, 4, cfg.max_prefixes)?,
v6: Map::lpm_trie(16, 4, cfg.max_prefixes)?,
})
}
}
fn stage_v4(asm: &mut Asm, slot: i16, pkt_off: i16) {
asm.emit(Insn::mov64_imm(R1, 32));
asm.emit(Insn::stx(Size::W, R10, slot, R1));
asm.emit(Insn::ldx(Size::W, R1, R7, pkt_off));
asm.emit(Insn::stx(Size::W, R10, slot + 4, R1));
}
fn stage_v6(asm: &mut Asm, slot: i16, pkt_off: i16) {
asm.emit(Insn::mov64_imm(R1, 128));
asm.emit(Insn::stx(Size::W, R10, slot, R1));
for w in 0..4i16 {
asm.emit(Insn::ldx(Size::W, R1, R7, pkt_off + w * 4));
asm.emit(Insn::stx(Size::W, R10, slot + 4 + w * 4, R1));
}
}
fn lookup(asm: &mut Asm, map_fd: i32, slot: i16, hit: super::insn::Label) {
asm.emit_all(&ld_map_fd(R1, map_fd));
asm.emit(Insn::mov64_reg(R2, R10));
asm.emit(Insn::add64_imm(R2, slot as i32));
asm.emit(Insn::call(BPF_FUNC_MAP_LOOKUP_ELEM));
asm.jump(Insn::jmp_imm(Jmp::JNE, R0, 0, 0), hit);
}
fn need_bytes(asm: &mut Asm, n: i32, miss: super::insn::Label) {
asm.emit(Insn::mov64_reg(R1, R7));
asm.emit(Insn::add64_imm(R1, n));
asm.jump(Insn::jmp_reg(Jmp::JGT, R1, R8, 0), miss);
}
pub fn build_program(cfg: &CaptureConfig, maps: &CaptureMaps) -> Result<Vec<Insn>> {
build_program_with_fds(
cfg,
maps.xskmap.as_raw_fd(),
maps.v4.as_raw_fd(),
maps.v6.as_raw_fd(),
)
}
fn build_program_with_fds(
cfg: &CaptureConfig,
xskmap_fd: i32,
v4_fd: i32,
v6_fd: i32,
) -> Result<Vec<Insn>> {
let mut asm = Asm::new();
let l_v4 = asm.label();
let l_v6 = asm.label();
let l_arp = asm.label();
let l_redirect = asm.label();
let l_default = asm.label();
asm.emit(Insn::mov64_reg(R6, R1));
asm.emit(Insn::ldx(Size::W, R7, R6, 0));
asm.emit(Insn::ldx(Size::W, R8, R6, 4));
need_bytes(&mut asm, ETH_HLEN, l_default);
asm.emit(Insn::ldx(Size::H, R2, R7, ETH_TYPE));
asm.jump(
Insn::jmp_imm(Jmp::JEQ, R2, host_be16(EtherType::IPV4.0), 0),
l_v4,
);
asm.jump(
Insn::jmp_imm(Jmp::JEQ, R2, host_be16(EtherType::IPV6.0), 0),
l_v6,
);
if cfg.arp {
asm.jump(
Insn::jmp_imm(Jmp::JEQ, R2, host_be16(EtherType::ARP.0), 0),
l_arp,
);
}
asm.jump(Insn::ja(0), l_default);
asm.place(l_v4);
need_bytes(&mut asm, IPV4_MIN, l_default);
if cfg.match_field.wants_dst() {
stage_v4(&mut asm, V4_DST_KEY, IPV4_DST);
}
if cfg.match_field.wants_src() {
stage_v4(&mut asm, V4_SRC_KEY, IPV4_SRC);
}
if cfg.match_field.wants_dst() {
lookup(&mut asm, v4_fd, V4_DST_KEY, l_redirect);
}
if cfg.match_field.wants_src() {
lookup(&mut asm, v4_fd, V4_SRC_KEY, l_redirect);
}
asm.jump(Insn::ja(0), l_default);
asm.place(l_v6);
need_bytes(&mut asm, IPV6_MIN, l_default);
if cfg.match_field.wants_dst() {
stage_v6(&mut asm, V6_DST_KEY, IPV6_DST);
}
if cfg.match_field.wants_src() {
stage_v6(&mut asm, V6_SRC_KEY, IPV6_SRC);
}
if cfg.match_field.wants_dst() {
lookup(&mut asm, v6_fd, V6_DST_KEY, l_redirect);
}
if cfg.match_field.wants_src() {
lookup(&mut asm, v6_fd, V6_SRC_KEY, l_redirect);
}
asm.jump(Insn::ja(0), l_default);
if cfg.arp {
asm.place(l_arp);
need_bytes(&mut asm, ARP_MIN, l_default);
asm.emit(Insn::ldx(Size::H, R2, R7, ARP_PTYPE));
asm.jump(
Insn::jmp_imm(Jmp::JNE, R2, host_be16(EtherType::IPV4.0), 0),
l_default,
);
if cfg.match_field.wants_dst() {
stage_v4(&mut asm, V4_DST_KEY, ARP_TPA);
}
if cfg.match_field.wants_src() {
stage_v4(&mut asm, V4_SRC_KEY, ARP_SPA);
}
if cfg.match_field.wants_dst() {
lookup(&mut asm, v4_fd, V4_DST_KEY, l_redirect);
}
if cfg.match_field.wants_src() {
lookup(&mut asm, v4_fd, V4_SRC_KEY, l_redirect);
}
asm.jump(Insn::ja(0), l_default);
}
asm.place(l_redirect);
asm.emit_all(&ld_map_fd(R1, xskmap_fd));
asm.emit(Insn::ldx(Size::W, R2, R6, XDP_MD_RX_QUEUE_INDEX));
asm.emit(Insn::mov64_imm(R3, Action::PASS.0 as i32));
asm.emit(Insn::call(BPF_FUNC_REDIRECT_MAP));
asm.emit(Insn::exit());
asm.place(l_default);
asm.emit(Insn::mov64_imm(R0, cfg.default_action.0 as i32));
asm.emit(Insn::exit());
asm.build()
}
#[derive(Debug)]
pub struct Capture {
maps: CaptureMaps,
_prog: Program,
link: Link,
cfg: CaptureConfig,
prefixes: Mutex<Vec<IpPrefix>>,
}
impl Capture {
pub fn attach(ifindex: u32, cfg: CaptureConfig, mode: Mode) -> Result<Capture> {
cfg.validate()?;
let maps = CaptureMaps::create(&cfg)?;
let insns = build_program(&cfg, &maps)?;
let prog = Program::load(&insns, "pktkit_cap")?;
let link = prog.attach(ifindex, mode)?;
Ok(Capture {
maps,
_prog: prog,
link,
cfg,
prefixes: Mutex::new(Vec::new()),
})
}
#[inline]
pub fn mode(&self) -> Mode {
self.link.mode()
}
#[inline]
pub fn xskmap(&self) -> &Map {
&self.maps.xskmap
}
pub fn add(&self, prefix: IpPrefix) -> Result<()> {
let prefix = prefix.masked();
self.cfg.check_prefix(prefix)?;
let mut held = self.prefixes.lock().unwrap();
if held.contains(&prefix) {
return Ok(());
}
check_coverage(&held, prefix)?;
self.insert(prefix)?;
if let Some(sn) = self.solicited_node(prefix) {
self.insert(sn)?;
}
held.push(prefix);
Ok(())
}
pub fn remove(&self, prefix: IpPrefix) -> Result<bool> {
let prefix = prefix.masked();
let mut held = self.prefixes.lock().unwrap();
let had = match held.iter().position(|p| *p == prefix) {
Some(i) => {
held.remove(i);
true
}
None => false,
};
if let Some(sn) = self.solicited_node(prefix) {
let still_needed = held
.iter()
.any(|p| *p == sn || self.solicited_node(*p) == Some(sn));
if !still_needed {
self.map_for(sn).delete(lpm_key(sn).as_bytes())?;
}
}
drop(held);
let removed = self.map_for(prefix).delete(lpm_key(prefix).as_bytes())?;
Ok(had || removed)
}
pub fn contains(&self, addr: IpAddr) -> Result<bool> {
let full = IpPrefix::new(addr, if addr.is_ipv4() { 32 } else { 128 });
let mut out = [0u8; 4];
self.map_for(full)
.lookup(lpm_key(full).as_bytes(), &mut out)
}
pub fn prefixes(&self) -> Vec<IpPrefix> {
self.prefixes.lock().unwrap().clone()
}
fn insert(&self, prefix: IpPrefix) -> Result<()> {
self.map_for(prefix).update(
lpm_key(prefix).as_bytes(),
&1u32.to_ne_bytes(),
UpdateFlags::ANY,
)
}
#[inline]
fn map_for(&self, prefix: IpPrefix) -> &Map {
if prefix.is_v4() {
&self.maps.v4
} else {
&self.maps.v6
}
}
fn solicited_node(&self, prefix: IpPrefix) -> Option<IpPrefix> {
if !self.cfg.neighbor_discovery || prefix.bits() != 128 {
return None;
}
match prefix.addr() {
IpAddr::V6(a) => Some(IpPrefix::new(solicited_node_multicast(a).into(), 128)),
IpAddr::V4(_) => None,
}
}
}
pub fn solicited_node_multicast(addr: Ipv6Addr) -> Ipv6Addr {
let o = addr.octets();
let mut sn = [0u8; 16];
sn[0] = 0xff;
sn[1] = 0x02;
sn[11] = 0x01;
sn[12] = 0xff;
sn[13..16].copy_from_slice(&o[13..16]);
Ipv6Addr::from(sn)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xdp::insn::{BPF_ADD, BPF_ALU64, BPF_JMP, BPF_K, BPF_STX};
use std::net::Ipv4Addr;
fn v4(a: [u8; 4], bits: u8) -> IpPrefix {
IpPrefix::new(Ipv4Addr::from(a).into(), bits)
}
fn program(cfg: &CaptureConfig) -> Vec<Insn> {
build_program_with_fds(cfg, 10, 11, 12).unwrap()
}
fn jumps(p: &[Insn]) -> Vec<usize> {
p.iter()
.enumerate()
.filter(|(_, i)| i.code & 0x07 == BPF_JMP)
.map(|(n, _)| n)
.collect()
}
#[test]
fn every_jump_lands_inside_the_program() {
for cfg in [
CaptureConfig::default(),
CaptureConfig {
match_field: MatchField::Either,
..Default::default()
},
CaptureConfig {
arp: false,
match_field: MatchField::Src,
..Default::default()
},
] {
let p = program(&cfg);
for n in jumps(&p) {
let i = p[n];
if i.code == (BPF_JMP | 0x80) || i.code == (BPF_JMP | 0x90) {
continue;
}
let target = n as isize + 1 + i.off as isize;
assert!(
target >= 0 && target < p.len() as isize,
"jump at {n} targets {target}, program is {} insns",
p.len()
);
}
}
}
#[test]
fn program_ends_with_the_default_verdict() {
let cfg = CaptureConfig::default();
let p = program(&cfg);
let n = p.len();
assert_eq!(p[n - 1], Insn::exit());
assert_eq!(p[n - 2], Insn::mov64_imm(R0, Action::PASS.0 as i32));
}
#[test]
fn drop_default_is_honoured() {
let cfg = CaptureConfig {
default_action: Action::DROP,
..Default::default()
};
let p = program(&cfg);
assert_eq!(p[p.len() - 2], Insn::mov64_imm(R0, Action::DROP.0 as i32));
}
#[test]
fn redirect_falls_back_to_pass_on_an_unbound_queue() {
let p = program(&CaptureConfig::default());
let call = p
.iter()
.position(|i| *i == Insn::call(BPF_FUNC_REDIRECT_MAP))
.expect("redirect call present");
assert_eq!(p[call - 1], Insn::mov64_imm(R3, Action::PASS.0 as i32));
}
#[test]
fn dst_only_does_one_lookup_per_family() {
let p = program(&CaptureConfig {
match_field: MatchField::Dst,
arp: false,
..Default::default()
});
let n = p
.iter()
.filter(|i| **i == Insn::call(BPF_FUNC_MAP_LOOKUP_ELEM))
.count();
assert_eq!(n, 2, "one v4 + one v6 lookup");
}
#[test]
fn either_doubles_the_lookups() {
let p = program(&CaptureConfig {
match_field: MatchField::Either,
arp: false,
..Default::default()
});
let n = p
.iter()
.filter(|i| **i == Insn::call(BPF_FUNC_MAP_LOOKUP_ELEM))
.count();
assert_eq!(n, 4);
}
#[test]
fn arp_adds_a_third_family_branch() {
let with = program(&CaptureConfig::default());
let without = program(&CaptureConfig {
arp: false,
..Default::default()
});
assert!(with.len() > without.len());
let n = with
.iter()
.filter(|i| **i == Insn::call(BPF_FUNC_MAP_LOOKUP_ELEM))
.count();
assert_eq!(n, 3, "v4 + v6 + arp");
}
#[test]
fn packet_reads_never_follow_a_helper_call() {
let p = program(&CaptureConfig {
match_field: MatchField::Either,
..Default::default()
});
let mut seen_call = false;
for i in &p {
if *i == Insn::call(BPF_FUNC_MAP_LOOKUP_ELEM) {
seen_call = true;
}
if i.code & 0x07 == 0x01 && (i.regs >> 4) == R7 {
assert!(!seen_call, "packet read after a helper call");
}
if i.code & 0x07 == BPF_JMP && i.code != (BPF_JMP | 0x80) && i.off != 0 {
seen_call = false;
}
}
}
#[test]
fn every_staged_key_is_written_before_it_is_read() {
let p = program(&CaptureConfig {
match_field: MatchField::Either,
..Default::default()
});
let add64_imm = BPF_ALU64 | BPF_K | BPF_ADD;
let mut written: Vec<i16> = Vec::new();
for i in &p {
if i.code & 0x07 == BPF_STX && (i.regs & 0x0f) == R10 {
written.push(i.off);
}
if i.code == add64_imm && (i.regs & 0x0f) == R2 && i.imm < 0 {
let slot = i.imm as i16;
assert!(written.contains(&slot), "lookup key at {slot} never staged");
}
}
}
#[test]
fn a_default_route_is_never_capturable() {
let cfg = CaptureConfig::default();
for p in [
IpPrefix::new(Ipv4Addr::UNSPECIFIED.into(), 0),
IpPrefix::new(Ipv6Addr::UNSPECIFIED.into(), 0),
] {
let e = cfg.check_prefix(p).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
assert!(
e.to_string().contains("every packet"),
"error should say why: {e}"
);
}
}
#[test]
fn a_zero_floor_cannot_be_configured() {
for cfg in [
CaptureConfig {
min_prefix_v4: 0,
..Default::default()
},
CaptureConfig {
min_prefix_v6: 0,
..Default::default()
},
] {
assert!(cfg.validate().is_err());
}
let smuggled = CaptureConfig {
min_prefix_v4: 0,
..Default::default()
};
assert!(
smuggled
.check_prefix(IpPrefix::new(Ipv4Addr::UNSPECIFIED.into(), 0))
.is_err()
);
}
#[test]
fn ordinary_prefixes_are_accepted() {
let cfg = CaptureConfig::default();
cfg.check_prefix(v4([10, 0, 0, 7], 32)).unwrap();
cfg.check_prefix(v4([10, 0, 0, 0], 24)).unwrap();
cfg.check_prefix(v4([10, 0, 0, 0], 8)).unwrap();
cfg.check_prefix(v4([0, 0, 0, 0], 1)).unwrap();
}
#[test]
fn a_tighter_floor_is_enforced_per_family() {
let cfg = CaptureConfig {
min_prefix_v4: 24,
min_prefix_v6: 64,
..Default::default()
};
cfg.validate().unwrap();
cfg.check_prefix(v4([10, 0, 0, 0], 24)).unwrap();
assert!(cfg.check_prefix(v4([10, 0, 0, 0], 16)).is_err());
let net: Ipv6Addr = "2001:db8::".parse().unwrap();
cfg.check_prefix(IpPrefix::new(net.into(), 64)).unwrap();
assert!(cfg.check_prefix(IpPrefix::new(net.into(), 48)).is_err());
}
#[test]
fn a_floor_wider_than_the_family_is_rejected() {
assert!(
CaptureConfig {
min_prefix_v4: 33,
..Default::default()
}
.validate()
.is_err()
);
assert!(
CaptureConfig {
min_prefix_v6: 129,
..Default::default()
}
.validate()
.is_err()
);
}
#[test]
fn two_halves_cannot_add_up_to_the_whole_interface() {
let low = v4([0, 0, 0, 0], 1);
let high = v4([128, 0, 0, 0], 1);
check_coverage(&[], low).unwrap();
let e = check_coverage(&[low], high).unwrap_err();
assert!(e.to_string().contains("every IPv4 address"), "{e}");
}
#[test]
fn four_quarters_cannot_either() {
let quarters: Vec<IpPrefix> = [0u8, 64, 128, 192]
.iter()
.map(|&a| v4([a, 0, 0, 0], 2))
.collect();
for i in 0..3 {
check_coverage(&quarters[..i], quarters[i]).unwrap();
}
assert!(check_coverage(&quarters[..3], quarters[3]).is_err());
}
#[test]
fn ipv6_halves_are_caught_without_overflowing() {
let low = IpPrefix::new("::".parse::<Ipv6Addr>().unwrap().into(), 1);
let high = IpPrefix::new("8000::".parse::<Ipv6Addr>().unwrap().into(), 1);
check_coverage(&[], low).unwrap();
assert!(check_coverage(&[low], high).is_err());
}
#[test]
fn coverage_is_counted_per_family() {
let v6_low = IpPrefix::new("::".parse::<Ipv6Addr>().unwrap().into(), 1);
let v6_high = IpPrefix::new("8000::".parse::<Ipv6Addr>().unwrap().into(), 1);
check_coverage(&[v6_low, v6_high], v4([10, 0, 0, 1], 32)).unwrap();
}
#[test]
fn realistic_sets_stay_far_from_the_limit() {
let mut held: Vec<IpPrefix> = (0..1000)
.map(|i| v4([10, (i / 256) as u8, (i % 256) as u8, 1], 32))
.collect();
held.push(v4([192, 168, 0, 0], 16));
held.push(v4([172, 16, 0, 0], 12));
check_coverage(&held, v4([10, 0, 0, 0], 8)).unwrap();
}
#[test]
fn coverage_totals_are_exact_at_the_boundary() {
assert_eq!(coverage(&[v4([10, 0, 0, 1], 32)], true), 1);
assert_eq!(coverage(&[v4([10, 0, 0, 0], 24)], true), 256);
assert_eq!(coverage(&[v4([0, 0, 0, 0], 1)], true), 1 << 31);
assert_eq!(family_total(true), 1u128 << 32);
assert_eq!(coverage(&[], false), 0);
}
#[test]
fn default_action_must_be_a_terminal_verdict() {
for a in [Action::PASS, Action::DROP] {
CaptureConfig {
default_action: a,
..Default::default()
}
.validate()
.unwrap();
}
for a in [Action::REDIRECT, Action::TX, Action::ABORTED] {
assert!(
CaptureConfig {
default_action: a,
..Default::default()
}
.validate()
.is_err()
);
}
}
#[test]
fn the_default_configuration_validates() {
CaptureConfig::default().validate().unwrap();
}
#[test]
fn solicited_node_follows_rfc4291() {
let a: Ipv6Addr = "2001:db8::dead:beef".parse().unwrap();
let sn = solicited_node_multicast(a);
assert_eq!(sn, "ff02::1:ffad:beef".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn solicited_node_only_depends_on_the_low_24_bits() {
let a: Ipv6Addr = "2001:db8::1:2:3".parse().unwrap();
let b: Ipv6Addr = "fe80::ffff:1:2:3".parse().unwrap();
assert_eq!(solicited_node_multicast(a), solicited_node_multicast(b));
}
#[test]
fn ethertype_constants_are_compared_in_wire_order() {
let p = program(&CaptureConfig::default());
let load = p
.iter()
.position(|i| *i == Insn::ldx(Size::H, R2, R7, ETH_TYPE))
.unwrap();
assert_eq!(p[load + 1].imm, host_be16(0x0800));
assert_eq!(p[load + 2].imm, host_be16(0x86DD));
assert_eq!(p[load + 3].imm, host_be16(0x0806));
}
#[test]
fn v4_prefix_round_trips_through_a_key() {
let p = IpPrefix::new(Ipv4Addr::new(198, 51, 100, 7).into(), 32);
assert_eq!(lpm_key(p).as_bytes()[4..], [198, 51, 100, 7]);
}
}