BFD protocol implementation details: FSM state machine, packet format, timer negotiation, jitter, authentication, and Poll Sequences.
- BFD Control Packet Format
- FSM State Machine
- State Transition Table
- State Variables
- Timer Negotiation
- Jitter
- Poll Sequence
- Authentication
- Not Implemented
RFC 5880 Section 4.1 defines a 24-byte mandatory header:
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 1 | Version (3 bits) + Diagnostic (5 bits) | Version MUST be 1 |
| 1 | 1 | State (2 bits) + P F C A D M (6 bits) | P=Poll, F=Final, C=CPI, A=Auth, D=Demand, M=Multipoint |
| 2 | 1 | Detect Multiplier | MUST NOT be zero |
| 3 | 1 | Length | Total packet length in bytes |
| 4-7 | 4 | My Discriminator | MUST NOT be zero |
| 8-11 | 4 | Your Discriminator | Zero until peer discriminator is known |
| 12-15 | 4 | Desired Min TX Interval | Microseconds |
| 16-19 | 4 | Required Min RX Interval | Microseconds |
| 20-23 | 4 | Required Min Echo RX Interval | Microseconds (always 0 -- Echo not implemented) |
| 24+ | var | Authentication Section | Optional, present when A bit is set |
IMPORTANT: All interval fields are in microseconds on the wire. Conversion to
time.Durationhappens at the boundary:interval := time.Duration(pkt.DesiredMinTxInterval) * time.Microsecond
Zero-allocation codec: MarshalControlPacket writes into a pre-allocated buffer (typically from sync.Pool). UnmarshalControlPacket fills a caller-provided ControlPacket struct in-place. Auth section digest/password fields reference the original buffer (zero-copy); callers must copy before returning the buffer to the pool.
RFC 5880 Section 6.8.6 defines four states and the transitions between them:
stateDiagram-v2
[*] --> Down
Down --> Init : Recv Down
Down --> Up : Recv Init
Down --> AdminDown : Admin Down
Init --> Up : Recv Init / Recv Up
Init --> Down : Recv AdminDown / Timer Expired
Init --> AdminDown : Admin Down
Up --> Down : Recv Down / Recv AdminDown / Timer Expired
Up --> AdminDown : Admin Down
AdminDown --> Down : Admin Up
The FSM is implemented as a pure function over a map[stateEvent]transition table. No if-else chains. No side effects -- the caller (Session) executes returned actions.
Derived from RFC 5880 Section 6.8.6 pseudocode:
| Current State | Event | New State | Actions |
|---|---|---|---|
| AdminDown | AdminUp | Down | (none) |
| Down | RecvDown | Init | SendControl |
| Down | RecvInit | Up | SendControl, NotifyUp |
| Down | AdminDown | AdminDown | SetDiagAdminDown |
| Init | RecvAdminDown | Down | SetDiagNeighborDown, NotifyDown |
| Init | RecvDown | Init | (self-loop) |
| Init | RecvInit | Up | SendControl, NotifyUp |
| Init | RecvUp | Up | SendControl, NotifyUp |
| Init | TimerExpired | Down | SetDiagTimeExpired, NotifyDown |
| Init | AdminDown | AdminDown | SetDiagAdminDown |
| Up | RecvAdminDown | Down | SetDiagNeighborDown, NotifyDown |
| Up | RecvDown | Down | SetDiagNeighborDown, NotifyDown |
| Up | RecvInit | Up | (self-loop) |
| Up | RecvUp | Up | (self-loop) |
| Up | TimerExpired | Down | SetDiagTimeExpired, NotifyDown |
| Up | AdminDown | AdminDown | SetDiagAdminDown |
Unlisted (state, event) pairs are silently ignored.
All mandatory state variables from RFC 5880 Section 6.1:
| RFC Variable | Go Field | Notes |
|---|---|---|
| bfd.SessionState | session.state (atomic) |
External reads via atomic.Uint32 |
| bfd.RemoteSessionState | session.remoteState (atomic) |
From received packets |
| bfd.LocalDiscr | session.localDiscr |
Immutable after creation |
| bfd.RemoteDiscr | session.remoteDiscr |
Set from received packets |
| bfd.LocalDiag | session.localDiag (atomic) |
Set by FSM actions |
| bfd.DesiredMinTxInterval | session.desiredMinTxInterval |
Configurable |
| bfd.RequiredMinRxInterval | session.requiredMinRxInterval |
Configurable |
| bfd.RemoteMinRxInterval | session.remoteMinRxInterval |
From received packets |
| bfd.DemandMode | Not implemented | See Not Implemented |
| bfd.RemoteDemandMode | session.remoteDemandMode |
Parsed but ignored |
| bfd.DetectMult | session.detectMult |
Configurable |
| bfd.AuthType | session.auth (interface) |
Via Authenticator interface |
| bfd.RcvAuthSeq | session.authState |
AuthState tracks this |
| bfd.XmitAuthSeq | session.authState |
AuthState tracks this |
| bfd.AuthSeqKnown | session.authState |
AuthState tracks this |
Thread safety: state, remoteState, and localDiag use atomic.Uint32 for lock-free reads from the gRPC server goroutine. All other state is owned exclusively by the session goroutine.
Initialization per RFC 5880 Section 6.8.1:
bfd.SessionState= Downbfd.RemoteSessionState= Downbfd.LocalDiag= 0 (None)bfd.RemoteDiscr= 0bfd.RemoteMinRxInterval= 1 (microsecond)bfd.XmitAuthSeq= random 32-bit value (viacrypto/rand)
RFC 5880 Sections 6.8.1-6.8.4.
The actual transmission interval is the larger of the local desired minimum and the remote required minimum:
ActualTxInterval = max(bfd.DesiredMinTxInterval, bfd.RemoteMinRxInterval)
When the session is not in Up state, the TX interval is enforced to be at least 1 second (1,000,000 microseconds) per RFC 5880 Section 6.8.3.
The detection time is calculated from the remote peer's parameters:
DetectionTime = bfd.RemoteDetectMult * max(bfd.RequiredMinRxInterval, bfd.RemoteDesiredMinTxInterval)
If no packet is received within the Detection Time, the session transitions to Down with Diagnostic = Control Detection Time Expired (1).
RFC 5880 Section 6.8.7. Periodic transmission intervals include random jitter to prevent synchronization:
| Condition | Jitter Range | Effective Interval |
|---|---|---|
| DetectMult > 1 | 0-25% reduction | 75-100% of interval |
| DetectMult == 1 | 10-25% reduction | 75-90% of interval |
Implementation:
func ApplyJitter(interval time.Duration, detectMult uint8) time.Duration {
if detectMult == 1 {
jitterPercent = 10 + rand.IntN(16) // 75-90%
} else {
jitterPercent = rand.IntN(26) // 75-100%
}
return interval - (interval * jitterPercent / 100)
}Uses math/rand/v2 for jitter (not security-sensitive, called on hot path).
RFC 5880 Section 6.5. Parameter changes (TX interval, RX interval) require a Poll Sequence for confirmation:
sequenceDiagram
participant A as Initiator
participant B as Responder
Note over A: Parameter change pending
A->>B: Control(Poll=1, new params)
Note over B: Acknowledge change
B->>A: Control(Final=1)
Note over A: Apply new parameters
Key rules:
- Only one Poll Sequence may be active at a time (RFC 5880 Section 6.5)
- Pending values stored in
pendingDesiredMinTx/pendingRequiredMinRx pollActive = truecauses the Poll (P) bit to be set in outgoing packets- When Final (F) bit is received,
terminatePollSequence()applies pending values
Parameter changes are deferred until poll completion rather than applied immediately. This matches the RFC intent: "A Poll Sequence MUST be used in order to verify that the change has been received."
RFC 5880 Section 6.7. Five authentication types are supported:
| Type ID | Name | RFC Section | Sequence Behavior |
|---|---|---|---|
| 1 | Simple Password | 4.2 | No sequence number |
| 2 | Keyed MD5 | 4.3 | Increment on state change only |
| 3 | Meticulous Keyed MD5 | 4.3 | Increment on every packet |
| 4 | Keyed SHA1 | 4.4 | Increment on state change only |
| 5 | Meticulous Keyed SHA1 | 4.4 | Increment on every packet |
Meticulous vs Non-Meticulous: Meticulous variants increment bfd.XmitAuthSeq on every transmitted packet. Non-meticulous variants increment only on session state changes. This distinction is critical for replay protection.
Sequence number window: For non-meticulous auth, received sequence numbers are accepted if they fall within 3 * DetectMult of bfd.RcvAuthSeq. Meticulous auth requires strict monotonic increment.
Key rotation: AuthKeyStore supports multiple simultaneous keys indexed by Key ID, allowing hitless key rotation per RFC 5880 Section 6.7.1.
WARNING: MD5 and SHA1 are retained despite cryptographic weakness because the RFC mandates them as the only defined hash-based auth types. GoBFD logs a warning at startup when MD5 auth is configured.
| Auth Type | Wire Layout | Auth Len |
|---|---|---|
| Simple Password | Type(1) + Len(1) + KeyID(1) + Password(1-16) | 4-19 |
| Keyed MD5 | Type(1) + Len(1) + KeyID(1) + Reserved(1) + SeqNum(4) + Digest(16) | 24 |
| Meticulous MD5 | Same as Keyed MD5 | 24 |
| Keyed SHA1 | Type(1) + Len(1) + KeyID(1) + Reserved(1) + SeqNum(4) + Hash(20) | 28 |
| Meticulous SHA1 | Same as Keyed SHA1 | 28 |
Not implemented. The Demand (D) bit is always set to zero on transmit. bfd.RemoteDemandMode is parsed from received packets but has no effect.
Rationale: Demand Mode is rarely used in production ISP/DC deployments. The primary use case (reducing BFD traffic) is better served by tuning TX/RX intervals. All major implementations (FRR, Junos, IOS-XR) default to Asynchronous mode.
Not implemented. RequiredMinEchoRxInterval is always set to zero, indicating the local system does not support Echo.
Rationale: Echo Mode requires kernel cooperation for reflecting echo packets. It adds complexity with minimal benefit for the typical GoBFD deployment scenario (BFD-assisted BGP failover).
The Multipoint (M) bit is always zero. Received packets with M=1 are rejected per RFC 5880 Section 6.8.6 step 5.
- 01-architecture.md -- System architecture and packet flow
- 08-rfc-compliance.md -- Full RFC compliance matrix
- RFC 5880 full text -- RFC 5880
Last updated: 2026-02-21