Core Question
Core Question
BFD has to agree on "the session is up" between two independent sides — how does a three-state machine reach agreement without a central decision-maker?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name the three
BfdStatevalues and which one the state machine can never assign as a first move - walk the DOWN -> INIT -> UP progression step by step, quoting which combination of local/remote state produces which transition
- explain why loss of the peer drops straight to DOWN with no INIT stopover
- state
detection_timeas a formula and compute it for a given multiplier and interval
Read Order
Read Order
- Read the module docstring at the top of the file
- Read
BfdState - Read
BfdSession - Read
on_packet() - Read
detection_time() - Read
check_timeout() - Run
examples/ha/session_03_walkthrough.py
Read It Like Code
Read It Like Code
BfdSession(
local_state,
remote_state_last_heard,
detect_multiplier,
interval_ms,
last_packet_at,
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
local_state | What this side currently believes about the session. The value every caller reads. |
remote_state_last_heard | What the peer last reported about *its* state. on_packet()'s whole decision rests on the pair (local_state, remote_state_last_heard). |
detect_multiplier | How many missed intervals count as a failure. Multiplies with interval_ms in detection_time(). |
interval_ms | The expected spacing between control packets, in milliseconds — the scale this session runs at. |
last_packet_at | The silence clock. check_timeout() measures from here, the same shape as vrrp_timeout.py's last_heard_at. |
Decision Flow
Decision Flow
on_packet(session, remote_state, now): always: remote_state_last_heard = remote_state; last_packet_at = now local DOWN + remote DOWN -> local becomes INIT local DOWN + remote INIT -> local becomes UP local INIT + remote INIT -> local becomes UP local INIT + remote UP -> local becomes UP anything else -> local_state unchanged check_timeout(session, now): now >= last_packet_at + detection_time(session) -> local becomes DOWN otherwise -> local_state unchanged
Reading Lens
Reading Lens
The important move in this session is to stop asking "what state is the session in?" and start asking, at every on_packet() call:
- what did *I* believe before this packet arrived?
- what does the packet say the *peer* believes?
- does that pair of beliefs appear in the table, and if not, why does the session hold its ground instead of guessing?
BFD is vrrp_timeout.py's silence-means-failure again, but the questions are different. VRRP infers failure in one direction only — the backup listens for the master, never the reverse. BFD negotiates in both directions at once: each side's local_state climbs only as far as what it has heard the other side currently claims, and the local state is one field, not a shared one — the two sides can genuinely disagree for a call or two before they converge.
Toy Model Boundary
Toy Model Boundary
Real BFD supports echo mode (looping packets off the peer's forwarding plane without involving its control plane) and optional authentication on control packets — neither exists here. This module also collapses the RFC's AdminDown state into nothing; BfdState only has DOWN, INIT, and UP. on_packet() additionally assumes every packet is well-formed and addressed to this session — there's no demultiplexing by discriminator, which real BFD relies on to route packets to the right session at all.
Code Landmarks
Code Landmarks
on_packet()'s docstring table
Five lines, cheapest case first: DOWN+DOWN -> INIT, DOWN+INIT -> UP, INIT+INIT -> UP, INIT+UP -> UP, everything else unchanged. This is RFC 5880 Section 6.2's three-way handshake compressed into one ordered if/elif chain — reading the docstring before the code tells you what each branch is *for* before you read what it *does*.
The two unconditional lines at the top of on_packet()
session.remote_state_last_heard = remote_state and session.last_packet_at = now run before any of the state-transition branches, every single call. Even a packet that doesn't advance local_state still updates what the session knows about the peer and resets the silence clock — a packet that "does nothing" to the state machine still counts as proof of life.
check_timeout()'s single branch
Loss of the peer never passes through INIT — local_state goes straight to DOWN. Contrast this with on_packet()'s graduated climb (DOWN -> INIT -> UP, sometimes two calls): going up is negotiated in one or two steps, going down is immediate and total.
detection_time()
detect_multiplier * interval_ms — one multiplication, no addition, no skew. Unlike VRRP's master_down_interval(), there's no per-session variation analogous to priority; every BFD session's detection budget depends only on its own configured multiplier and interval.
Failure Questions
Failure Questions
Use the source file to answer these:
- A session starts with
local_state=DOWNand receives a packet reportingremote_state=UP. Which branch ofon_packet()fires, and what doeslocal_statebecome? Is this combination even listed as one of the four transition branches? on_packet()'s branches checkINIT + remote in (INIT, UP)but there is no branch forUP + remote DOWN. What doeslocal_statedo in that case, and which line in the function is responsible for that outcome?check_timeout()never setslocal_statetoINIT. Given the threeBfdStatevalues, isINITreachable at all fromcheck_timeout(), or only fromon_packet()?- For
detect_multiplier=3andinterval_ms=50, what doesdetection_time()return? At exactly that many milliseconds of silence, doescheck_timeout()changelocal_state, or does it need one more millisecond? on_packet()updateslast_packet_aton every call, including calls that fall into the "anything else, unchanged" branch. Why does even a packet that doesn't changelocal_statestill reset the timeout clock thatcheck_timeout()reads?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/ha/session_03_walkthrough.py
The walkthrough drives one session through DOWN -> INIT -> UP step by step, shows a session that reaches UP via the direct DOWN + remote INIT path, computes detection_time() from detect_multiplier=3 and interval_ms=50, and checks check_timeout() one millisecond before and exactly at that threshold.
Done When
Done When
The learner can say all of the following without looking at notes:
- "BfdSession negotiates in both directions — local_state only climbs as far as what the peer last reported."
- "Going up is graduated through INIT; going down from silence is immediate, straight to DOWN."
- "detection_time is detect_multiplier times interval_ms, and check_timeout uses the same >= boundary convention as VRRP's silence check."
References
References
- RFC 5880 Section 6.2 (State Variables, the three-way handshake table)
- RFC 5880 Section 6.8.4 (Detection Time calculation)
Continue