Skip to content

BFD timer negotiation is non-existent #798

Description

@taspelund

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugbfdBidirectional Forwarding DetectionmgdMaghemite daemon

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions