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:
1 parent
8fbafa455a
commit
bdf5455a3f
4 files changed
+1251
-23
No files matched your search
+612
-7
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user