BFD Explained: Catching a Silent Network Failure in Under a Second

This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.

In Lab #34, OSPF reconverged fast because bringing an interface down is detected instantly. Real failures are not always so polite: a link can stay up (carrier present) while forwarding is silently broken — a dumb switch in the middle, a one-way fault, a wedged neighbor. Then OSPF notices only when its dead timer expires (default 40 seconds), black-holing traffic the whole time.

BFD (Bidirectional Forwarding Detection) fixes this. It runs a tiny hello between neighbors several times a second; when a few go missing it declares the path down and tells OSPF immediately.

Reading guide: rfc-notes/bfd.md

Prerequisite: Lab 34: OSPF — Flood the Map, Compute the Shortest Path

Expected time: 40–55 minutes.

The Goal

  • r1/r2/r3 run OSPF plus BFD (300 ms × 3 ≈ 900 ms detection) on every adjacency.
  • r1 reaches the target over the direct r1–r3 link.
  • We simulate a silent failure — drop all packets on r1's eth2 while the link stays UP.
  • BFD catches it and OSPF reconverges onto r2 in ~900 ms, versus 40 s with OSPF alone.

By the end, you should be able to explain this table:

detection of a silent failure time
OSPF alone dead timer expires 40 s
OSPF + BFD missed BFD packets (300 ms × 3) ~0.9 s

What You Will Learn

  • What BFD is and why routing protocols' own hellos are too slow.
  • How detection time = receive interval × detect multiplier (300 ms × 3 ≈ 900 ms).
  • The difference between a link-down failure and a silent forwarding failure.
  • How OSPF registers with BFD (ip ospf bfd) and reacts to a BFD "down".
  • Why BFD only reports up/down — the routing protocol still computes the paths.

This lab does not cover:

  • BFD echo mode, or micro-BFD on LAG members.
  • Multihop BFD (RFC 5883).
  • In-depth timer tuning against flap/false-positive trade-offs.

Where to Read in the RFCs

Reference What to focus on
RFC 5880 BFD itself (sessions, timers, detection time)
RFC 5881 Single-hop BFD between adjacent neighbors
RFC 5882 How OSPF/BGP consume a BFD "down"
RFC 2328 OSPF's dead interval (the slow detection BFD replaces)

The Big Picture

Same OSPF area-0 triangle as Lab 34, with BFD added on every adjacency.

              r1
    (OSPF+BFD)/  \(OSPF+BFD)
             r2 -- r3 --- target 10.0.30.1
                (OSPF+BFD)

r1 reaches the target over the direct r1–r3 link. We turn eth2 into a silent failure with iptables DROP — the link stays up.

sequenceDiagram
  participant r1
  participant r3
  Note over r1,r3: BFD control packets ~3/sec (300ms)
  r1->>r3: BFD (up)
  r3->>r1: BFD (up)
  Note over r1,r3: iptables DROP on eth2 (link stays UP)
  r1--xr3: (packets silently dropped)
  Note over r1: 3 missed packets ≈ 900ms → session DOWN
  Note over r1: OSPF drops adjacency → SPF → route via r2

10.0.0.0/8 is a local, closed range.

Note: Everything here uses local/documentation address space (RFC 1918 / RFC 5737), so nothing in this lab touches the real internet.

What You Need

Recommended environment:

  • Linux / WSL2 / a Linux VM
  • Docker
  • containerlab

Images used:

  • frrouting/frr:latestr1/r2/r3, running ospfd + bfdd.
  • nicolaka/netshoot:latest — the target.

No additional images are required.

Running the Lab

The quick path, which deploys, verifies, and tears down for you:

./scripts/labctl.sh run bfd-35

Or step through it manually:

1. Move into the working directory

cd protocol-lab/examples/bfd-35

2. Deploy

sudo containerlab deploy -t bfd-35.clab.yml

Each router runs OSPF (area 0) and additionally enables BFD on its transit interfaces via ip ospf bfd.

3. Check the BFD sessions and timers

docker exec clab-bfd-35-r1 vtysh -c "show bfd peers"
docker exec clab-bfd-35-r1 vtysh -c "show ip ospf interface eth2" | grep "Timer intervals"

You should see Status: up, Receive/Transmission interval: 300ms, and Detect-multiplier: 3 (≈900 ms detection). OSPF still shows Dead 40s.

docker exec clab-bfd-35-r1 ip route get 10.0.30.1     # via 10.0.13.2 (r3 direct)
docker exec clab-bfd-35-r1 ping -c2 10.0.30.1
docker exec clab-bfd-35-r1 sh -c 'iptables -A INPUT -i eth2 -j DROP; iptables -A OUTPUT -o eth2 -j DROP'
sleep 2
docker exec clab-bfd-35-r1 ip -br link show eth2       # still UP,LOWER_UP
docker exec clab-bfd-35-r1 ip route get 10.0.30.1      # already via 10.0.12.2 (r2)
docker exec clab-bfd-35-r1 ping -c2 10.0.30.1          # still reachable

BFD detects the loss in about 900 ms, and OSPF reconverges onto r2 — while the link is still UP.

6. Restore it

docker exec clab-bfd-35-r1 sh -c 'iptables -D INPUT -i eth2 -j DROP; iptables -D OUTPUT -o eth2 -j DROP'

Expected Output

  • show bfd peers: two sessions with Status: up, 300 ms intervals, multiplier 3.
  • OSPF interface: Dead 40s — the time you would be waiting without BFD.
  • After the silent failure: eth2 stays UP,LOWER_UP, the route moves to via 10.0.12.2 (r2), and reconvergence takes under one second (about 900 ms in this environment).
  • The target stays reachable throughout.

Why It Works

BFD is a fast dead-man's switch for your next hop. Routing protocols detect failures with their own hellos too, but the timers are slow (OSPF's dead interval is 40 seconds).

  • Why hellos alone are too slow. OSPF sends a Hello every 10 seconds with a 40-second dead interval. Cranking those timers down increases load and false positives. So the standard answer is to run a separate, lightweight, detection-only protocol (BFD) and have the routing protocol register with it: "tell me when this path goes down."
  • Detection time. BFD exchanges control packets at a negotiated interval (300 ms here) and declares the session down after detect multiplier (default 3) consecutive packets go missing. That's ≈ 300 ms × 3 = 900 ms — roughly 1/40th of OSPF's 40 seconds.
  • Silent failures are the whole point. If a link goes down, the OS detects carrier loss instantly and routing reacts right away (that's why Lab 34's veth failover was fast). But real networks also suffer link-up, forwarding-dead failures — a dumb switch in the middle, a one-way fault, a wedged neighbor. The carrier is alive, so routing can't tell until the dead interval expires. This lab recreates that with iptables DROP: the link stays up while forwarding is killed. BFD's control packets get dropped too, so r1 declares the session down after ~900 ms.
  • Coupling with OSPF. ip ospf bfd ties BFD to that adjacency. When BFD reports "down", OSPF immediately treats the neighbor as down, reruns SPF, and reconverges via r2. BFD does not choose paths — it only reports up/down; OSPF computes the routes.

The key insight: instead of relying on slow routing hellos, lightweight BFD detects silent failures in under a second and lets routing reconverge fast. Detection (BFD) and path computation (OSPF/BGP) are a division of labor.

Common Pitfalls

  • Thinking BFD chooses routes. It doesn't. It only reports up/down; OSPF/BGP pick the paths.
  • Thinking you need BFD for link-down failures. The OS detects link-down instantly. BFD's value is the silent failure (link up, forwarding dead) — exactly what this lab's iptables DROP recreates.
  • Thinking tighter routing hellos are enough. Aggressively shortening routing-protocol hellos increases load and false positives. Running a separate lightweight BFD is the established pattern.
  • Thinking faster is always better. Push the timers too hard and a momentary blip causes a false "down" (flapping). Tune timer/multiplier to your environment.
  • Thinking BFD works on its own. Reconvergence only happens once BFD is coupled to a routing protocol.

Cleanup

sudo containerlab destroy -t bfd-35.clab.yml --cleanup

If you used labctl.sh run bfd-35, the script runs destroy for you at the end.

Check Your Understanding

  1. What does BFD do? Does it perform path selection?
  2. How is the detection time determined? With 300 ms intervals and a multiplier of 3, how many milliseconds is it?
  3. What's the difference between a link-down failure and a silent failure? Which one does BFD help with?
  4. Why is BFD an order of magnitude faster than OSPF's 40-second dead interval?
  5. How does OSPF use a BFD "down"? What does ip ospf bfd do?
  6. What can go wrong if you push BFD's timers too aggressively?

References

Verified Run Log (2026-07-07)

This lab has been confirmed reproducible on real hardware.

Environment:

  • Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
  • Docker 29.1.3
  • containerlab 0.77.0
  • r1 / r2 / r3: frrouting/frr:latest (ospfd + bfdd)
  • target: nicolaka/netshoot:latest

Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run bfd-35 performed deploy → verify → destroy, and verification.json returned "status": "verified".

BFD sessions (sub-second timers) next to OSPF's 40-second dead interval

peer 10.0.13.2 vrf default interface eth2
    Status: up
    Detect-multiplier: 3
    Receive interval: 300ms
    Transmission interval: 300ms

OSPF: Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5

BFD's detection time ≈ 300 ms × 3 = about 900 ms. OSPF alone would wait a 40-second dead interval — BFD is roughly 40× faster.

The silent failure: detected and reconverged in 918 ms

We applied iptables DROP (both in and out) on r1's eth2, killing forwarding while the link stayed UP:

reconverged: 1
elapsed_ms: 918
link_at_failure: eth2@if1136  UP  <BROADCAST,MULTICAST,UP,LOWER_UP>
  • At the moment of failure, eth2 was still UP,LOWER_UP (carrier present) — from the OS's point of view the link looked alive. A textbook silent failure.
  • BFD nonetheless detected the loss of control packets in about 900 ms, and OSPF reconverged via r2 in 918 ms.
  • Without BFD, in the same situation OSPF would have kept using the broken direct link until Hellos went missing for the full dead interval (40 seconds), black-holing traffic the whole time.
  • After reconvergence, the target (10.0.30.1) remained reachable via r2.

Cleanup

containerlab destroy -t bfd-35.clab.yml --cleanup

That's BFD: a tiny, dedicated hello that turns a 40-second blind spot into a sub-second detection, while OSPF keeps doing the actual path computation.

Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.

Next up, we'll keep building on this fast-failover foundation and explore more ways protocols detect and route around trouble.

Read more