Expand unit tests and fix bugs they found

Add unit tests for packet parsing/serialization, interval arithmetic, the
session state machine, validation discards, poll/final, the detection
watchdog, hook scripts, and socket binding.

Fixes:
- BfdAuth parsing subtracted the header from Auth Len twice; Auth Len
  includes the 2-byte header.
- A diagnostic code above 9 panicked the session task; map it to Reserved.
- script_runner panicked on non-UTF-8 script output; decode lossily.
- BfdError::InvalidFieldValue printed field and value swapped.
- BfdInterval now panics on values exceeding u32 microseconds instead of
  silently truncating, and gains checked_mul. Detection time computation
  saturates rather than panicking on remote-supplied multipliers.
This commit is contained in:
ktims committed 2026-10-23 09:52:57 -07:00
1 parent 8fbafa455a
commit bdf5455a3f
4 files changed
+1251 -23

No files matched your search

+612 -7
View File
@@ -91,7 +91,7 @@ impl Display for BfdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFieldValue(field, value) => {
write!(f, "invalid value `{}` for field `{}`", field, value)
write!(f, "invalid value `{}` for field `{}`", value, field)
}
}
}
@@ -777,7 +777,8 @@ impl BfdSession {
// Set bfd.RemoteMinRxInterval to the value of Required Min RX Interval.
self.state.set_remote_min_rx_interval(p.required_min_rx);
// Not in spec: store the received diagnostic
self.state.set_last_remote_diag(p.flags.diag().unwrap());
self.state
.set_last_remote_diag(p.flags.diag().unwrap_or(BfdDiagnostic::Reserved));
// If the Required Min Echo RX Interval field is zero, the transmission of Echo packets, if any, MUST cease.
if p.required_min_echo_rx == BfdInterval::from_secs(0) {
@@ -908,15 +909,25 @@ impl BfdSession {
// https://datatracker.ietf.org/doc/html/rfc5880#section-6.8.4
async fn update_detection_time(&mut self, p: &BfdPacket) {
let old = self.state.detection_time();
// Remote-supplied values can overflow the 32-bit microsecond range; saturate rather than panic.
let saturating = |mult: u8, interval: BfdInterval| {
interval
.checked_mul(mult as u32)
.unwrap_or(BfdInterval::from_micros(u32::MAX))
};
self.state.set_detection_time(if !self.state.demand_mode() {
p.flags.detect_mult() as u32
* std::cmp::max(self.state.required_min_rx_interval(), p.desired_min_tx)
saturating(
p.flags.detect_mult(),
std::cmp::max(self.state.required_min_rx_interval(), p.desired_min_tx),
)
} else {
self.state.detect_mult() as u32
* std::cmp::max(
saturating(
self.state.detect_mult(),
std::cmp::max(
self.state.desired_min_tx_interval(),
self.state.remote_min_rx_interval(),
)
),
)
});
if old != self.state.detection_time() {
info!(
@@ -1327,4 +1338,598 @@ mod tests {
assert_eq!(args.bind.len(), 2);
assert_eq!(args.peers.len(), 2);
}
use std::net::Ipv4Addr;
const LOCAL: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const PEER: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2));
const LOCAL_DISCR: u32 = 1000;
const REMOTE_DISCR: u32 = 2000;
#[derive(Debug)]
struct CaptureSink {
tx: mpsc::Sender<EventMessage>,
rx: mpsc::Receiver<EventMessage>,
out: mpsc::UnboundedSender<StateChangeEvent>,
}
impl EventMessageSink for CaptureSink {
fn channel(&self) -> mpsc::Sender<EventMessage> {
self.tx.clone()
}
fn run(mut self: Box<Self>) -> JoinHandle<()> {
tokio::spawn(async move {
while let Some(EventMessage::StateChange(ev)) = self.rx.recv().await {
let _ = self.out.send(ev);
}
})
}
}
async fn capture_dispatcher() -> (
EventDispatcherHandle,
mpsc::UnboundedReceiver<StateChangeEvent>,
) {
let dispatcher = EventDispatcherHandle::new().await;
let (tx, rx) = mpsc::channel(16);
let (out, events) = mpsc::unbounded_channel();
dispatcher
.add_sink(Box::new(CaptureSink { tx, rx, out }))
.await;
(dispatcher, events)
}
fn peer_cfg(desired_ms: u32, required_ms: u32, detect_mult: u8) -> ResolvedPeer {
ResolvedPeer {
address: PEER,
desired_min_tx: BfdInterval::from_millis(desired_ms),
required_min_rx: BfdInterval::from_millis(required_ms),
detect_mult,
}
}
fn pkt(state: BfdState, my: u32, your: u32) -> BfdPacket {
BfdPacket {
flags: BfdFlags(0)
.with_vers(1)
.with_state(state as u8)
.with_detect_mult(3)
.with_length(24),
my_disc: BfdDiscriminator(my),
your_disc: BfdDiscriminator(your),
desired_min_tx: BfdInterval::from_millis(300),
required_min_rx: BfdInterval::from_millis(300),
required_min_echo_rx: BfdInterval::from_micros(0),
auth: None,
}
}
fn with_flags(mut p: BfdPacket, f: impl FnOnce(BfdFlags) -> BfdFlags) -> BfdPacket {
p.flags = f(p.flags);
p
}
async fn session(
peer: ResolvedPeer,
) -> (BfdSession, mpsc::UnboundedReceiver<StateChangeEvent>) {
let (dispatcher, events) = capture_dispatcher().await;
let (tx, rx) = mpsc::channel(32);
let sock = UdpSocket::bind((LOCAL, 0)).await.unwrap();
let s = BfdSession::new(
rx,
BfdSessionHandle { tx },
sock,
LOCAL,
peer,
BfdDiscriminator(LOCAL_DISCR),
dispatcher,
);
(s, events)
}
async fn next_event(
events: &mut mpsc::UnboundedReceiver<StateChangeEvent>,
) -> StateChangeEvent {
time::timeout(Duration::from_secs(2), events.recv())
.await
.expect("timed out waiting for event")
.expect("event channel closed")
}
async fn assert_no_event(events: &mut mpsc::UnboundedReceiver<StateChangeEvent>) {
time::sleep(Duration::from_millis(50)).await;
assert!(events.try_recv().is_err(), "unexpected state change event");
}
async fn session_in(
state: BfdState,
) -> (BfdSession, mpsc::UnboundedReceiver<StateChangeEvent>) {
let (mut s, mut events) = session(peer_cfg(300, 300, 3)).await;
let path: &[BfdState] = match state {
BfdState::Down => &[],
BfdState::Init => &[BfdState::Down],
_ => &[BfdState::Down, BfdState::Init],
};
for (i, remote) in path.iter().enumerate() {
let your = if i == 0 { 0 } else { LOCAL_DISCR };
s.receive_control_packet(&pkt(*remote, REMOTE_DISCR, your))
.await;
next_event(&mut events).await;
}
assert_eq!(s.state.session_state(), state);
(s, events)
}
#[tokio::test]
async fn new_session_starts_down_and_slow() {
let (s, _events) = session(peer_cfg(300, 200, 3)).await;
assert_eq!(s.state.session_state(), BfdState::Down);
assert_eq!(s.state.remote_discr(), BfdDiscriminator(0));
assert_eq!(s.state.local_diag(), BfdDiagnostic::None);
assert_eq!(s.state.desired_min_tx_interval(), BfdInterval::from_secs(1));
assert_eq!(
s.state.required_min_rx_interval(),
BfdInterval::from_millis(200)
);
assert_eq!(s.cur_interval, BfdInterval::from_secs(1));
assert_eq!(s.state.detect_mult(), 3);
assert!(!s.state.poll_mode());
}
#[tokio::test]
async fn new_session_keeps_configured_interval_above_one_second() {
let (s, _events) = session(peer_cfg(2500, 300, 3)).await;
assert_eq!(
s.state.desired_min_tx_interval(),
BfdInterval::from_millis(2500)
);
assert_eq!(s.effective_desired_min_tx, BfdInterval::from_millis(2500));
}
#[tokio::test]
async fn three_way_handshake_and_poll_sequence() {
let (mut s, mut events) = session(peer_cfg(300, 300, 3)).await;
s.receive_control_packet(&pkt(BfdState::Down, REMOTE_DISCR, 0))
.await;
assert_eq!(s.state.session_state(), BfdState::Init);
assert_eq!(s.state.remote_discr(), BfdDiscriminator(REMOTE_DISCR));
let ev = next_event(&mut events).await;
assert_eq!(
(ev.from_state, ev.to_state),
(BfdState::Down, BfdState::Init)
);
assert_eq!(ev.local_discr, BfdDiscriminator(LOCAL_DISCR));
assert_eq!(ev.remote_discr, BfdDiscriminator(REMOTE_DISCR));
assert_eq!((ev.local_ip, ev.remote_ip), (LOCAL, PEER));
s.receive_control_packet(&pkt(BfdState::Init, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Up);
let ev = next_event(&mut events).await;
assert_eq!((ev.from_state, ev.to_state), (BfdState::Init, BfdState::Up));
assert!(s.state.last_up.load().is_some());
assert_eq!(s.state.local_diag(), BfdDiagnostic::None);
// Advertise the fast interval now, but keep transmitting slowly until the poll sequence completes
assert_eq!(
s.state.desired_min_tx_interval(),
BfdInterval::from_millis(300)
);
assert!(s.state.poll_mode());
assert_eq!(s.cur_interval, BfdInterval::from_secs(1));
let fin = with_flags(pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR), |f| {
f.with_final_(true)
});
s.receive_control_packet(&fin).await;
assert!(!s.state.poll_mode());
assert_eq!(s.effective_desired_min_tx, BfdInterval::from_millis(300));
assert_eq!(s.cur_interval, BfdInterval::from_millis(300));
assert_eq!(s.state.session_state(), BfdState::Up);
}
#[tokio::test]
async fn up_without_poll_when_configured_interval_is_not_faster() {
let (mut s, _events) = session(peer_cfg(2000, 300, 3)).await;
s.receive_control_packet(&pkt(BfdState::Down, REMOTE_DISCR, 0))
.await;
s.receive_control_packet(&pkt(BfdState::Init, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Up);
assert!(!s.state.poll_mode());
assert_eq!(s.cur_interval, BfdInterval::from_secs(2));
}
#[tokio::test]
async fn down_goes_straight_to_up_when_remote_is_init() {
let (mut s, mut events) = session(peer_cfg(300, 300, 3)).await;
s.receive_control_packet(&pkt(BfdState::Init, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Up);
let ev = next_event(&mut events).await;
assert_eq!((ev.from_state, ev.to_state), (BfdState::Down, BfdState::Up));
}
#[tokio::test]
async fn down_ignores_remote_up_and_admin_down() {
let (mut s, mut events) = session(peer_cfg(300, 300, 3)).await;
s.receive_control_packet(&pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Down);
s.receive_control_packet(&pkt(BfdState::AdminDown, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Down);
assert_no_event(&mut events).await;
}
#[tokio::test]
async fn init_transitions() {
let (mut s, mut events) = session_in(BfdState::Init).await;
s.receive_control_packet(&pkt(BfdState::Down, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Init);
assert_no_event(&mut events).await;
s.receive_control_packet(&pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Up);
let (mut s, mut events) = session_in(BfdState::Init).await;
s.receive_control_packet(&pkt(BfdState::AdminDown, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Down);
assert_eq!(s.state.local_diag(), BfdDiagnostic::NeighborDown);
let ev = next_event(&mut events).await;
assert_eq!(
(ev.from_state, ev.to_state),
(BfdState::Init, BfdState::Down)
);
}
#[tokio::test]
async fn up_goes_down_when_remote_goes_down() {
let (mut s, mut events) = session_in(BfdState::Up).await;
s.receive_control_packet(&pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_no_event(&mut events).await;
s.receive_control_packet(&pkt(BfdState::Down, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Down);
assert_eq!(s.state.local_diag(), BfdDiagnostic::NeighborDown);
let ev = next_event(&mut events).await;
assert_eq!((ev.from_state, ev.to_state), (BfdState::Up, BfdState::Down));
assert!(s.state.last_down.load().is_some());
// Back to the slow rate and no poll sequence in flight
assert_eq!(s.state.desired_min_tx_interval(), BfdInterval::from_secs(1));
assert_eq!(s.cur_interval, BfdInterval::from_secs(1));
assert!(!s.state.poll_mode());
}
#[tokio::test]
async fn up_goes_down_on_remote_admin_down() {
let (mut s, mut events) = session_in(BfdState::Up).await;
s.receive_control_packet(&pkt(BfdState::AdminDown, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Down);
assert_eq!(s.state.local_diag(), BfdDiagnostic::NeighborDown);
assert_eq!(next_event(&mut events).await.to_state, BfdState::Down);
}
#[tokio::test]
async fn restarted_peer_relearns_discriminator() {
let (mut s, mut events) = session_in(BfdState::Up).await;
s.receive_control_packet(&pkt(BfdState::Down, 3000, 0))
.await;
assert_eq!(s.state.session_state(), BfdState::Down);
assert_eq!(s.state.remote_discr(), BfdDiscriminator(3000));
assert_eq!(next_event(&mut events).await.to_state, BfdState::Down);
s.receive_control_packet(&pkt(BfdState::Down, 3000, 0))
.await;
assert_eq!(s.state.session_state(), BfdState::Init);
}
#[tokio::test]
async fn up_resets_stats_and_down_records_time() {
let (mut s, _events) = session_in(BfdState::Init).await;
s.state.record_tx();
s.receive_control_packet(&pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.session_state(), BfdState::Up);
// The Up transition resets the counters, then sends one packet announcing the new state
assert_eq!(s.state.rx_stats.snapshot().count(), 0);
assert_eq!(s.state.tx_stats.snapshot().count(), 1);
s.receive_control_packet(&pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.rx_stats.snapshot().count(), 1);
}
#[tokio::test]
async fn invalid_packets_are_discarded() {
type Mutation = fn(BfdPacket) -> BfdPacket;
let cases: [(&str, Mutation); 8] = [
("version", |p| with_flags(p, |f| f.with_vers(2))),
("short length", |p| with_flags(p, |f| f.with_length(23))),
("zero detect mult", |p| {
with_flags(p, |f| f.with_detect_mult(0))
}),
("multipoint", |p| with_flags(p, |f| f.with_multipoint(true))),
("zero my discr", |mut p| {
p.my_disc = BfdDiscriminator(0);
p
}),
("wrong your discr", |mut p| {
p.your_disc = BfdDiscriminator(LOCAL_DISCR + 1);
p
}),
("zero your discr, not down", |mut p| {
p.your_disc = BfdDiscriminator(0);
p.flags = p.flags.with_state(BfdState::Up as u8);
p
}),
("auth without auth in use", |p| {
with_flags(p, |f| f.with_auth_present(true).with_length(26))
}),
];
for (name, mutate) in cases {
let (mut s, mut events) = session(peer_cfg(300, 300, 3)).await;
s.receive_control_packet(&mutate(pkt(BfdState::Down, REMOTE_DISCR, 0)))
.await;
assert_eq!(s.state.session_state(), BfdState::Down, "{name}");
assert_eq!(s.state.remote_discr(), BfdDiscriminator(0), "{name}");
assert_eq!(
s.state.remote_min_rx_interval(),
BfdInterval::from_millis(300),
"{name}"
);
assert_no_event(&mut events).await;
}
}
#[tokio::test]
async fn auth_length_minimum_is_26() {
let (mut s, _events) = session(peer_cfg(300, 300, 3)).await;
let p = with_flags(pkt(BfdState::Down, REMOTE_DISCR, 0), |f| {
f.with_auth_present(true).with_length(25)
});
s.receive_control_packet(&p).await;
assert_eq!(s.state.remote_discr(), BfdDiscriminator(0));
}
#[tokio::test]
async fn unknown_diagnostic_does_not_panic() {
let (mut s, _events) = session(peer_cfg(300, 300, 3)).await;
let p = with_flags(pkt(BfdState::Down, REMOTE_DISCR, 0), |f| f.with_diag(20));
s.receive_control_packet(&p).await;
assert_eq!(s.state.session_state(), BfdState::Init);
}
#[tokio::test]
async fn remote_diag_is_recorded() {
let (mut s, _events) = session(peer_cfg(300, 300, 3)).await;
let p = with_flags(pkt(BfdState::Down, REMOTE_DISCR, 0), |f| {
f.with_diag(BfdDiagnostic::PathDown as u8)
});
s.receive_control_packet(&p).await;
assert_eq!(s.state.last_remote_diag(), BfdDiagnostic::PathDown);
}
#[tokio::test]
async fn poll_bit_elicits_final_response() {
let (mut s, _events) = session_in(BfdState::Up).await;
let before = s.state.tx_stats.snapshot().count();
s.receive_control_packet(&pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR))
.await;
assert_eq!(s.state.tx_stats.snapshot().count(), before);
let poll = with_flags(pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR), |f| {
f.with_poll(true)
});
s.receive_control_packet(&poll).await;
assert_eq!(s.state.tx_stats.snapshot().count(), before + 1);
}
#[tokio::test]
async fn state_change_sends_immediate_packet() {
let (mut s, _events) = session(peer_cfg(300, 300, 3)).await;
s.receive_control_packet(&pkt(BfdState::Down, REMOTE_DISCR, 0))
.await;
assert_eq!(s.state.tx_stats.snapshot().count(), 1);
s.receive_control_packet(&pkt(BfdState::Down, REMOTE_DISCR, 0))
.await;
assert_eq!(s.state.tx_stats.snapshot().count(), 1);
}
#[tokio::test]
async fn transmit_interval_follows_remote_min_rx() {
let (mut s, _events) = session(peer_cfg(300, 300, 3)).await;
let mut p = pkt(BfdState::Down, REMOTE_DISCR, 0);
p.required_min_rx = BfdInterval::from_secs(2);
s.receive_control_packet(&p).await;
assert_eq!(s.state.remote_min_rx_interval(), BfdInterval::from_secs(2));
assert_eq!(s.cur_interval, BfdInterval::from_secs(2));
p.required_min_rx = BfdInterval::from_millis(100);
s.receive_control_packet(&p).await;
// never faster than our own effective desired-min-tx, which is 1s while not Up
assert_eq!(s.cur_interval, BfdInterval::from_secs(1));
}
#[tokio::test]
async fn detection_time_saturates_on_remote_overflow() {
let (mut s, _events) = session(peer_cfg(300, 300, 3)).await;
let mut p = pkt(BfdState::Down, REMOTE_DISCR, 0);
p.flags = p.flags.with_detect_mult(255);
p.desired_min_tx = BfdInterval::from_micros(u32::MAX);
s.receive_control_packet(&p).await;
assert_eq!(s.state.detection_time(), BfdInterval::from_micros(u32::MAX));
}
#[tokio::test]
async fn detection_time_uses_remote_mult_and_larger_interval() {
let (mut s, _events) = session(peer_cfg(300, 500, 3)).await;
let mut p = pkt(BfdState::Down, REMOTE_DISCR, 0);
p.flags = p.flags.with_detect_mult(5);
p.desired_min_tx = BfdInterval::from_millis(400);
s.receive_control_packet(&p).await;
assert_eq!(s.state.detection_time(), BfdInterval::from_millis(2500));
p.desired_min_tx = BfdInterval::from_millis(800);
s.receive_control_packet(&p).await;
assert_eq!(s.state.detection_time(), BfdInterval::from_millis(4000));
}
#[tokio::test]
async fn watchdog_expires_only_without_packets() {
let (dispatcher, mut events) = capture_dispatcher().await;
let handle = BfdSessionHandle::new(
LOCAL,
peer_cfg(20, 20, 3),
BfdDiscriminator(LOCAL_DISCR),
dispatcher,
)
.await
.unwrap();
let fast = |state, your| {
let mut p = pkt(state, REMOTE_DISCR, your);
p.flags = p.flags.with_detect_mult(10);
p.desired_min_tx = BfdInterval::from_millis(20);
p.required_min_rx = BfdInterval::from_millis(20);
p
};
let send = |p| handle.tx.send(SessionControlCommand::RxPacket(p));
send(fast(BfdState::Down, 0)).await.unwrap();
send(fast(BfdState::Init, LOCAL_DISCR)).await.unwrap();
assert_eq!(next_event(&mut events).await.to_state, BfdState::Init);
assert_eq!(next_event(&mut events).await.to_state, BfdState::Up);
// detection time is 10 x 20ms; keep-alives at 20ms must hold the session up
for _ in 0..15 {
send(fast(BfdState::Up, LOCAL_DISCR)).await.unwrap();
time::sleep(Duration::from_millis(20)).await;
}
assert!(
events.try_recv().is_err(),
"session dropped despite keep-alives"
);
let ev = next_event(&mut events).await;
assert_eq!((ev.from_state, ev.to_state), (BfdState::Up, BfdState::Down));
let stats = handle.get_stats().await.unwrap();
assert_eq!(stats.state, BfdState::Down);
assert!(stats.last_down.is_some());
handle.tx.send(SessionControlCommand::Quit).await.unwrap();
}
#[test]
fn bfd_error_display_orders_value_and_field() {
let e = BfdError::InvalidFieldValue("state", "bogus");
assert_eq!(e.to_string(), "invalid value `bogus` for field `state`");
}
#[tokio::test]
async fn control_socket_uses_source_port_range_and_ttl_255() {
let a = bind_control_socket(LOCAL).await.unwrap();
let b = bind_control_socket(LOCAL).await.unwrap();
for s in [&a, &b] {
let addr = s.local_addr().unwrap();
assert!(SOURCE_PORT_RANGE.contains(&addr.port()), "{addr}");
assert_eq!(addr.ip(), LOCAL);
assert_eq!(s.ttl().unwrap(), 255);
}
assert_ne!(
a.local_addr().unwrap().port(),
b.local_addr().unwrap().port()
);
}
#[tokio::test]
async fn control_socket_bind_error_is_propagated() {
let err = bind_control_socket(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)))
.await
.unwrap_err();
assert_ne!(err.kind(), ErrorKind::AddrInUse);
}
const OTHER: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3));
async fn bfdd() -> (Bfdd, mpsc::Receiver<SessionControlCommand>) {
let (tx, rx) = mpsc::channel(8);
let discr = BfdDiscriminator(LOCAL_DISCR);
let d = Bfdd {
local: SocketAddr::new(LOCAL, CONTROL_PORT),
peers: vec![peer_cfg(300, 300, 3)],
sessions_by_ip: HashMap::from([((LOCAL, PEER), discr)]),
sessions_by_discr: HashMap::from([(discr, BfdSessionHandle { tx })]),
unmatched_sources: std::sync::Mutex::new(HashSet::new()),
control_socket: Arc::new(UdpSocket::bind((LOCAL, 0)).await.unwrap()),
event_dispatcher: EventDispatcherHandle::new().await,
};
(d, rx)
}
fn from(ip: IpAddr) -> SocketAddr {
SocketAddr::new(ip, 49152)
}
#[tokio::test]
async fn demux_by_source_when_your_discr_is_zero() {
let (d, _rx) = bfdd().await;
let p = pkt(BfdState::Down, REMOTE_DISCR, 0);
assert!(d.session_for_packet(&from(PEER), &p).is_some());
assert!(d.session_for_packet(&from(OTHER), &p).is_none());
}
#[tokio::test]
async fn demux_by_discriminator_ignores_source_address() {
let (d, _rx) = bfdd().await;
let p = pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR);
assert!(d.session_for_packet(&from(PEER), &p).is_some());
assert!(d.session_for_packet(&from(OTHER), &p).is_some());
let p = pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR + 1);
assert!(d.session_for_packet(&from(PEER), &p).is_none());
}
#[tokio::test]
async fn unmatched_sources_are_tracked_until_they_match() {
let (d, _rx) = bfdd().await;
let miss = pkt(BfdState::Down, REMOTE_DISCR, 0);
let hit = pkt(BfdState::Up, REMOTE_DISCR, LOCAL_DISCR);
assert!(d.session_for_packet(&from(OTHER), &miss).is_none());
assert!(d.unmatched_sources.lock().unwrap().contains(&OTHER));
assert!(d.session_for_packet(&from(OTHER), &hit).is_some());
assert!(!d.unmatched_sources.lock().unwrap().contains(&OTHER));
}
#[tokio::test]
async fn datagram_is_parsed_and_delivered_to_session() {
let (d, mut rx) = bfdd().await;
let bytes = pkt(BfdState::Down, REMOTE_DISCR, 0).serialize().unwrap();
d.handle_datagram(&bytes, from(PEER)).await;
match rx.try_recv() {
Ok(SessionControlCommand::RxPacket(p)) => {
assert_eq!(p.my_disc, BfdDiscriminator(REMOTE_DISCR))
}
_ => panic!("expected RxPacket"),
}
}
#[tokio::test]
async fn bad_datagrams_are_dropped() {
let (d, mut rx) = bfdd().await;
let good = pkt(BfdState::Down, REMOTE_DISCR, 0).serialize().unwrap();
d.handle_datagram(&good[..23], from(PEER)).await;
d.handle_datagram(&[], from(PEER)).await;
d.handle_datagram(&[0xff; 7], from(PEER)).await;
let long = with_flags(pkt(BfdState::Down, REMOTE_DISCR, 0), |f| f.with_length(48));
d.handle_datagram(&long.serialize().unwrap(), from(PEER))
.await;
let unknown = pkt(BfdState::Down, REMOTE_DISCR, 0);
d.handle_datagram(&unknown.serialize().unwrap(), from(OTHER))
.await;
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn bfdd_rejects_multiple_bind_addresses() {
let cfg = ResolvedConfig {
bind: vec![LOCAL, PEER],
hooks_dir: PathBuf::from("/nonexistent"),
peers: vec![],
};
let err = Bfdd::new(cfg).await.err().expect("should fail");
assert!(err.to_string().contains("only one"), "{err}");
}
}