While working on adding juniper to falcon-lab, I noticed that the BFD tests were consistently due to the incorrect next-hops in the FIB after stopping FRR and EOS peers (meaning juniper should be the only active next-hop). On closer examination, the failures are inconsistent and show 1, 2 or 3 next-hops depending on the timing in which the dpd query occurs.
The inconsistency in the output is actually a result of the BFD session between mgd and juniper flapping indefinitely, and the contents of the dpd query change depending on what point of convergence we happen to get a result back during (when all next-hops for a route are bfd-shutdown, they all become valid again).
This looks to be happening due to mgd not negotiating BFD timers with the peer.
While Juniper cRPD is configured with matching timers (300ms tx/rx * 3 detect multiplier), there seems to be a bug where the config is ignored and default transmit/receive values of 2s are applied:
root@crpd1> show configuration routing-options
rib inet6.0 {
static {
route 3ffe::/64 {
next-hop fd00:3::1;
bfd-liveness-detection {
minimum-interval 300; <<<<<
multiplier 3; <<<<<
}
}
}
}
static {
route 100.65.0.0/24 {
next-hop 10.0.2.1;
bfd-liveness-detection {
minimum-interval 300; <<<<<
multiplier 3; <<<<<
}
}
}
root@crpd1> show bfd session
Detect Transmit
Address State Interface Time Interval Multiplier
10.0.2.1 Up eth1 6.000 2.000 3 <<<<< 2s interval * 3 (multiplier) = 6s time
fd00:3::1 Up eth1 6.000 2.000 3 <<<<< 2s interval * 3 (multiplier) = 6s time
2 sessions, 2 clients
Cumulative transmit rate 1.0 pps, cumulative receive rate 1.0 pps
When this mismatch is happening (mgd using 300ms * 3, junos using 2000ms * 3), an indefinite cycle between Up/Down states occurs as mgd marks the peer session down at 900ms instead of the 6000ms it should have calculated.
RFC 5880 Section 6.8.4 defines detection time calculation:
6.8.4. Calculating the Detection Time
The Detection Time (the period of time without receiving BFD packets
after which the session is determined to have failed) is not carried
explicitly in the protocol. Rather, it is calculated independently
in each direction by the receiving system based on the negotiated
transmit interval and the detection multiplier. Note that there may
be different Detection Times in each direction.
The calculation of the Detection Time is slightly different when in
Demand mode versus Asynchronous mode.
In Asynchronous mode, the Detection Time calculated in the local <<<<<
system is equal to the value of Detect Mult received from the remote <<<<<
system, multiplied by the agreed transmit interval of the remote <<<<<
system (the greater of bfd.RequiredMinRxInterval and the last <<<<<
received Desired Min TX Interval). The Detect Mult value is (roughly <<<<<
speaking, due to jitter) the number of packets that have to be missed <<<<<
in a row to declare the session to be down. <<<<<
If Demand mode is not active, and a period of time equal to the
Detection Time passes without receiving a BFD Control packet from the
remote system, and bfd.SessionState is Init or Up, the session has
gone down -- the local system MUST set bfd.SessionState to Down and
bfd.LocalDiag to 1 (Control Detection Time Expired).
In Demand mode, the Detection Time calculated in the local system is
equal to bfd.DetectMult, multiplied by the agreed transmit interval
of the local system (the greater of bfd.DesiredMinTxInterval and
bfd.RemoteMinRxInterval). bfd.DetectMult is (roughly speaking, due
to jitter) the number of packets that have to be missed in a row to
declare the session to be down.
If Demand mode is active, and a period of time equal to the Detection
Time passes after the initiation of a Poll Sequence (the transmission
of the first BFD Control packet with the Poll bit set), the session
has gone down -- the local system MUST set bfd.SessionState to Down,
and bfd.LocalDiag to 1 (Control Detection Time Expired).
(Note that a packet is considered to have been received, for the
purposes of Detection Time expiration, only if it has not been
"discarded" according to the rules of section 6.8.6).
We are not using demand mode, so that puts us in async mode.
Paraphrasing the async calculation, it is remoteDetectMult * max(localRequiredMinRx, remoteDesiredMinTx).
Our code always calculates it using our local interval * our local multiplier -- never considering the peer's value:
endpoint.rx.recv_timeout(
local.required_min_rx * local.detection_multiplier.into(),
)
If we encounter a timeout from this call, we declare the peer has timed out and transition out of the Up state.
A simple diff to ostensibly address Detection Time negotiation looks like this:
diff --git a/bfd/src/sm.rs b/bfd/src/sm.rs
index 0f16915aa7..5d87c07b7e 100644
--- a/bfd/src/sm.rs
+++ b/bfd/src/sm.rs
@@ -9,6 +9,7 @@
use anyhow::{Result, anyhow};
use mg_common::lock;
use slog::Logger;
+use std::cmp::max;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Sender;
@@ -340,8 +341,13 @@
log: Logger,
counters: Arc<SessionCounters>,
) -> Result<RecvResult> {
+ let (remote_detection_multiplier, remote_desired_min_tx) = {
+ let remote = lock!(remote);
+ (remote.detection_multiplier, remote.desired_min_tx)
+ };
match endpoint.rx.recv_timeout(
- local.required_min_rx * local.detection_multiplier.into(),
+ u32::from(remote_detection_multiplier)
+ * (max(local.required_min_rx, remote_desired_min_tx)),
) {
Ok((addr, msg)) => {
sm_log!(
but I don't think this would fully bring us into spec. There are poll modes that start/stop in the middle of a session, and timers can change mid session too. I think this will need a more thorough review of the BFD state machine implementation.
While working on adding juniper to falcon-lab, I noticed that the BFD tests were consistently due to the incorrect next-hops in the FIB after stopping FRR and EOS peers (meaning juniper should be the only active next-hop). On closer examination, the failures are inconsistent and show 1, 2 or 3 next-hops depending on the timing in which the dpd query occurs.
The inconsistency in the output is actually a result of the BFD session between mgd and juniper flapping indefinitely, and the contents of the dpd query change depending on what point of convergence we happen to get a result back during (when all next-hops for a route are bfd-shutdown, they all become valid again).
This looks to be happening due to mgd not negotiating BFD timers with the peer.
While Juniper cRPD is configured with matching timers (300ms tx/rx * 3 detect multiplier), there seems to be a bug where the config is ignored and default transmit/receive values of 2s are applied:
When this mismatch is happening (mgd using 300ms * 3, junos using 2000ms * 3), an indefinite cycle between Up/Down states occurs as mgd marks the peer session down at 900ms instead of the 6000ms it should have calculated.
RFC 5880 Section 6.8.4 defines detection time calculation:
We are not using demand mode, so that puts us in async mode.
Paraphrasing the async calculation, it is
remoteDetectMult * max(localRequiredMinRx, remoteDesiredMinTx).Our code always calculates it using our local interval * our local multiplier -- never considering the peer's value:
If we encounter a timeout from this call, we declare the peer has timed out and transition out of the Up state.
A simple diff to ostensibly address Detection Time negotiation looks like this:
but I don't think this would fully bring us into spec. There are poll modes that start/stop in the middle of a session, and timers can change mid session too. I think this will need a more thorough review of the BFD state machine implementation.