use heapless::LinearMap;
use crate::config::IFACE_NEIGHBOR_CACHE_COUNT;
use crate::time::{Duration, Instant};
use crate::wire::{HardwareAddress, IpAddress};
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Neighbor {
hardware_addr: HardwareAddress,
expires_at: Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub(crate) enum Answer {
Found(HardwareAddress),
NotFound,
RateLimited,
}
impl Answer {
pub(crate) fn found(&self) -> bool {
match self {
Answer::Found(_) => true,
_ => false,
}
}
}
#[derive(Debug)]
pub struct Cache {
storage: LinearMap<IpAddress, Neighbor, IFACE_NEIGHBOR_CACHE_COUNT>,
silent_until: Instant,
}
impl Cache {
pub(crate) const SILENT_TIME: Duration = Duration::from_millis(1_000);
pub(crate) const ENTRY_LIFETIME: Duration = Duration::from_millis(60_000);
pub fn new() -> Self {
Self {
storage: LinearMap::new(),
silent_until: Instant::from_millis(0),
}
}
pub fn fill(
&mut self,
protocol_addr: IpAddress,
hardware_addr: HardwareAddress,
timestamp: Instant,
) {
debug_assert!(protocol_addr.is_unicast());
debug_assert!(hardware_addr.is_unicast());
let expires_at = timestamp + Self::ENTRY_LIFETIME;
self.fill_with_expiration(protocol_addr, hardware_addr, expires_at);
}
pub fn fill_with_expiration(
&mut self,
protocol_addr: IpAddress,
hardware_addr: HardwareAddress,
expires_at: Instant,
) {
debug_assert!(protocol_addr.is_unicast());
debug_assert!(hardware_addr.is_unicast());
let neighbor = Neighbor {
expires_at,
hardware_addr,
};
match self.storage.insert(protocol_addr, neighbor) {
Ok(Some(old_neighbor)) => {
if old_neighbor.hardware_addr != hardware_addr {
net_trace!(
"replaced {} => {} (was {})",
protocol_addr,
hardware_addr,
old_neighbor.hardware_addr
);
}
}
Ok(None) => {
net_trace!("filled {} => {} (was empty)", protocol_addr, hardware_addr);
}
Err((protocol_addr, neighbor)) => {
let old_protocol_addr = *self
.storage
.iter()
.min_by_key(|(_, neighbor)| neighbor.expires_at)
.expect("empty neighbor cache storage")
.0;
let _old_neighbor = self.storage.remove(&old_protocol_addr).unwrap();
match self.storage.insert(protocol_addr, neighbor) {
Ok(None) => {
net_trace!(
"filled {} => {} (evicted {} => {})",
protocol_addr,
hardware_addr,
old_protocol_addr,
_old_neighbor.hardware_addr
);
}
_ => unreachable!(),
}
}
}
}
pub(crate) fn lookup(&self, protocol_addr: &IpAddress, timestamp: Instant) -> Answer {
assert!(protocol_addr.is_unicast());
if let Some(&Neighbor {
expires_at,
hardware_addr,
}) = self.storage.get(protocol_addr)
{
if timestamp < expires_at {
return Answer::Found(hardware_addr);
}
}
if timestamp < self.silent_until {
Answer::RateLimited
} else {
Answer::NotFound
}
}
pub(crate) fn limit_rate(&mut self, timestamp: Instant) {
self.silent_until = timestamp + Self::SILENT_TIME;
}
pub(crate) fn flush(&mut self) {
self.storage.clear()
}
}
#[cfg(feature = "medium-ethernet")]
#[cfg(test)]
mod test {
use super::*;
use crate::wire::ip::test::{MOCK_IP_ADDR_1, MOCK_IP_ADDR_2, MOCK_IP_ADDR_3, MOCK_IP_ADDR_4};
use crate::wire::EthernetAddress;
const HADDR_A: HardwareAddress = HardwareAddress::Ethernet(EthernetAddress([0, 0, 0, 0, 0, 1]));
const HADDR_B: HardwareAddress = HardwareAddress::Ethernet(EthernetAddress([0, 0, 0, 0, 0, 2]));
const HADDR_C: HardwareAddress = HardwareAddress::Ethernet(EthernetAddress([0, 0, 0, 0, 0, 3]));
const HADDR_D: HardwareAddress = HardwareAddress::Ethernet(EthernetAddress([0, 0, 0, 0, 0, 4]));
#[test]
fn test_fill() {
let mut cache = Cache::new();
assert!(!cache
.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0))
.found());
assert!(!cache
.lookup(&MOCK_IP_ADDR_2, Instant::from_millis(0))
.found());
cache.fill(MOCK_IP_ADDR_1, HADDR_A, Instant::from_millis(0));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0)),
Answer::Found(HADDR_A)
);
assert!(!cache
.lookup(&MOCK_IP_ADDR_2, Instant::from_millis(0))
.found());
assert!(!cache
.lookup(
&MOCK_IP_ADDR_1,
Instant::from_millis(0) + Cache::ENTRY_LIFETIME * 2
)
.found(),);
cache.fill(MOCK_IP_ADDR_1, HADDR_A, Instant::from_millis(0));
assert!(!cache
.lookup(&MOCK_IP_ADDR_2, Instant::from_millis(0))
.found());
}
#[test]
fn test_expire() {
let mut cache = Cache::new();
cache.fill(MOCK_IP_ADDR_1, HADDR_A, Instant::from_millis(0));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0)),
Answer::Found(HADDR_A)
);
assert!(!cache
.lookup(
&MOCK_IP_ADDR_1,
Instant::from_millis(0) + Cache::ENTRY_LIFETIME * 2
)
.found(),);
}
#[test]
fn test_replace() {
let mut cache = Cache::new();
cache.fill(MOCK_IP_ADDR_1, HADDR_A, Instant::from_millis(0));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0)),
Answer::Found(HADDR_A)
);
cache.fill(MOCK_IP_ADDR_1, HADDR_B, Instant::from_millis(0));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0)),
Answer::Found(HADDR_B)
);
}
#[test]
fn test_evict() {
let mut cache = Cache::new();
cache.fill(MOCK_IP_ADDR_1, HADDR_A, Instant::from_millis(100));
cache.fill(MOCK_IP_ADDR_2, HADDR_B, Instant::from_millis(50));
cache.fill(MOCK_IP_ADDR_3, HADDR_C, Instant::from_millis(200));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_2, Instant::from_millis(1000)),
Answer::Found(HADDR_B)
);
assert!(!cache
.lookup(&MOCK_IP_ADDR_4, Instant::from_millis(1000))
.found());
cache.fill(MOCK_IP_ADDR_4, HADDR_D, Instant::from_millis(300));
assert!(!cache
.lookup(&MOCK_IP_ADDR_2, Instant::from_millis(1000))
.found());
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_4, Instant::from_millis(1000)),
Answer::Found(HADDR_D)
);
}
#[test]
fn test_hush() {
let mut cache = Cache::new();
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0)),
Answer::NotFound
);
cache.limit_rate(Instant::from_millis(0));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(100)),
Answer::RateLimited
);
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(2000)),
Answer::NotFound
);
}
#[test]
fn test_flush() {
let mut cache = Cache::new();
cache.fill(MOCK_IP_ADDR_1, HADDR_A, Instant::from_millis(0));
assert_eq!(
cache.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0)),
Answer::Found(HADDR_A)
);
assert!(!cache
.lookup(&MOCK_IP_ADDR_2, Instant::from_millis(0))
.found());
cache.flush();
assert!(!cache
.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0))
.found());
assert!(!cache
.lookup(&MOCK_IP_ADDR_1, Instant::from_millis(0))
.found());
}
}