Documentation
¶
Overview ¶
Design: plan/learned/1124-vrrp-first-hop-redundancy.md -- VRRP FSM output actions (closed set) RFC: rfc/short/rfc9568.md (VRRPv3) and rfc/short/rfc3768.md (VRRPv2)
The FSM returns ordered action VALUES instead of performing effects. The engine (spec-vrrp-5) is the sole executor: packet sends via the transport (spec-vrrp-4), VIP install/remove via the iface address-owner registry (spec-vrrp-3), timers via internal/core/clock. Action order is part of the contract; a closed set keeps the executor a dumb dispatcher that cannot corrupt protocol logic.
Design: plan/learned/1124-vrrp-first-hop-redundancy.md -- VRRP instance state machine and timers RFC: rfc/short/rfc9568.md (VRRPv3) and rfc/short/rfc3768.md (VRRPv2)
Package fsm implements the per-group VRRP instance state machine and timer arithmetic for RFC 9568 (VRRPv3, default) and RFC 3768 (VRRPv2, opt-in).
Invariants ¶
The FSM is a pure, deterministic, single-threaded, actions-as-values machine:
- Inputs are typed events (Startup, Shutdown, AdvertReceived, MasterDownExpired, AdvertTimerExpired, PreemptDelayExpired, ConfigUpdated) plus the current time read from an injected clock.Clock (timestamps only, never scheduling).
- Outputs are an ordered slice of action values (SendAdvert, SendAdvertZeroPriority, InstallVIPs, RemoveVIPs, AnnounceFailover, StartMasterDownTimer, StartAdvertTimer, StartPreemptDelayTimer, StopPreemptDelayTimer, StopTimers, EmitStateChange). Action order is part of the contract.
- The FSM performs NO I/O: no sockets, no netlink, no direct wall-clock scheduling, no goroutines, no locks. Every side effect is an action value the engine (spec-vrrp-5) executes.
- It reads clock.Now() only, for state-entry timestamps and last-advert bookkeeping surfaced by Snapshot for `show vrrp`.
The engine owns the three clock.Timer values (master-down, advert, preempt-delay), selects on their channels, and re-enters Handle with the matching expiry event. Because clock.Timer.Reset can leave an already-fired tick queued, every Start* action and every expiry event carries a monotonic Gen; the FSM ignores any expiry whose Gen does not match the currently armed generation for that timer role (see timers.go).
The FSM trusts its caller's threading contract: exactly one goroutine calls Handle. It contains no locks by design and is NOT safe for concurrent use.
Terminology ¶
RFC 9568 renamed "Master" to "Active Router". This package keeps Initialize/Backup/Master to match the operator vocabulary of keepalived, Junos, and the `show vrrp` CLI surface. In the RFC citations below, Master == Active.
Design: plan/learned/1124-vrrp-first-hop-redundancy.md -- VRRP FSM input events and instance config RFC: rfc/short/rfc9568.md (VRRPv3) and rfc/short/rfc3768.md (VRRPv2)
Typed input events consumed by Instance.Handle. Each event is a small value type carrying only decoded, pre-validated fields (no wire bytes, no packet types from spec-vrrp-1); the codec boundary lives entirely in the engine (spec-vrrp-5), which builds these events.
Design: plan/learned/1124-vrrp-first-hop-redundancy.md -- VRRP per-instance state machine RFC: rfc/short/rfc9568.md (VRRPv3 Section 6.4) and rfc/short/rfc3768.md (VRRPv2 Section 6.4)
The per-group VRRP state machine: State type, the Instance struct, the single synchronous Handle method implementing the State Transition Table, and the Snapshot surfaced for `show vrrp`. See doc.go for the purity/threading invariants. Timer arithmetic lives in timers.go.
Design: plan/learned/1124-vrrp-first-hop-redundancy.md -- VRRP Skew_Time / Master_Down_Interval math RFC: rfc/short/rfc9568.md (VRRPv3 Algorithms) and rfc/short/rfc3768.md (VRRPv2 Algorithms)
Timer arithmetic for the VRRP FSM. Unit discipline (spec risk R-2): every interval crossing the FSM boundary is an integer MILLISECOND count; every computed value is a time.Duration (int64 nanoseconds). Multiplication ALWAYS precedes division; division by 256 is the LAST operation. Valid v3 skews are sub-millisecond (priority 254 at a 10 ms interval is 78,125 ns), so an integer-millisecond representation would truncate them to zero -- the exact uvrrpd/holo bug class this file exists to prevent.
Index ¶
- Constants
- type Action
- type AdvertReceived
- type AdvertTimerExpired
- type AnnounceFailover
- type Config
- type ConfigUpdated
- type EmitStateChange
- type Event
- type InstallVIPs
- type Instance
- type MasterDownExpired
- type PreemptDelayExpired
- type RemoveVIPs
- type SendAdvert
- type SendAdvertZeroPriority
- type Shutdown
- type Snapshot
- type StartAdvertTimer
- type StartMasterDownTimer
- type StartPreemptDelayTimer
- type Startup
- type State
- type StopPreemptDelayTimer
- type StopTimers
Constants ¶
const ( // ReasonStartupOwner: Initialize->Master on owner Startup (RFC 9568 Section 6.4.1). ReasonStartupOwner = "startup-owner" // ReasonStartup: Initialize->Backup on non-owner Startup (RFC 9568 Section 6.4.1). ReasonStartup = "startup" // ReasonMasterDownExpired: Backup->Master on down-timer expiry (RFC 9568 Section 6.4.2). ReasonMasterDownExpired = "master-down-expired" // ReasonPreemptDelayExpired: Backup->Master on preempt-delay expiry (no RFC basis). ReasonPreemptDelayExpired = "preempt-delay-expired" // ReasonShutdown: Backup/Master->Initialize on Shutdown (RFC 9568 Section 6.4.2/6.4.3). ReasonShutdown = "shutdown" // ReasonHigherPriority: Master->Backup on a higher-priority advert (RFC 9568 Section 6.4.3). ReasonHigherPriority = "higher-priority" // ReasonTieBreakLost: Master->Backup on equal priority with greater sender IP (RFC 9568 Section 6.4.3). ReasonTieBreakLost = "tie-break-lost" )
EmitStateChange reason tokens. These are fixed, enumerated strings that feed eventbus consumers, metrics labels, and logs in spec-vrrp-5; they are never attacker-controlled and must match the State Transition Table verbatim.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action interface {
// contains filtered or unexported methods
}
Action is the closed set of ordered side-effect values the FSM emits. The engine executes them in the returned order.
type AdvertReceived ¶
type AdvertReceived struct {
// Priority is the sender's advertised priority; 0 means the sender is
// relinquishing (RFC 9568 Section 5.2.4).
Priority uint8 `json:"priority"`
// SrcIP is the sender's primary IPvX source address, the tie-break operand
// consulted only in Master state.
SrcIP netip.Addr `json:"src-ip"`
// IntervalMs is the sender's Max Advertise Interval already converted to
// milliseconds by the codec (spec-vrrp-1); v3 Backups adopt it.
IntervalMs int `json:"interval-ms"`
// VIPCount is the advertised IPvX address count (diagnostic; >= 1 enforced
// upstream per RFC 9568 erratum 8299).
VIPCount int `json:"vip-count"`
}
AdvertReceived carries a decoded, receive-validated VRRP ADVERTISEMENT. The engine (spec-vrrp-5) runs packet.Decode + validation (spec-vrrp-1) over raw packets from the transport (spec-vrrp-4); malformed or failed-validation packets never become events.
type AdvertTimerExpired ¶
type AdvertTimerExpired struct {
Gen uint64 `json:"gen"`
}
AdvertTimerExpired is delivered by the engine's advert clock.Timer, echoing the Gen of the arming StartAdvertTimer action.
RFC 9568 Section 6.4.3 / RFC 3768 Section 6.4.3: Active(Master) sends an advertisement each interval and resets the timer.
type AnnounceFailover ¶
type AnnounceFailover struct{}
AnnounceFailover asks the engine to send the gratuitous ARP (per IPv4 VIP) / unsolicited NA (per IPv6 VIP) burst that repoints learning bridges and host caches (spec-vrrp-4).
RFC 9568 Section 6.4.2 (erratum 7949) / RFC 3768 Section 6.4.2.
type Config ¶
type Config struct {
// Version is the wire protocol version, 2 (RFC 3768) or 3 (RFC 9568),
// fixed per instance at Startup.
Version uint8 `json:"version"`
// IsOwner is true when this router owns the virtual address(es); it forces
// Priority 255 and unconditional preemption (RFC 9568 Section 6.1).
IsOwner bool `json:"is-owner"`
// Priority is 1..254 for a Backup, 255 iff IsOwner. 0 is wire-only
// (Master releasing) and is never a configured local priority (A-5).
Priority uint8 `json:"priority"`
// Preempt defaults true (RFC). Consulted only in Backup.
Preempt bool `json:"preempt"`
// PreemptDelayMs is the Junos-style hold-time in milliseconds; 0 disables
// delayed preemption (the RFC-pure default). No RFC basis; see the spec's
// Preempt-Delay Semantics section.
PreemptDelayMs int `json:"preempt-delay-ms"`
// AdvertIntervalMs is this router's own advertisement interval in
// milliseconds (validated per version: v3 10..40950, v2 1000..255000).
AdvertIntervalMs int `json:"advert-interval-ms"`
// LocalPrimaryIP is the tie-break operand: the primary IPv4 address or the
// link-local IPv6 address of the sending interface (RFC 9568 Section 6.4.3).
LocalPrimaryIP netip.Addr `json:"local-primary-ip"`
// VIPs is the full desired virtual-address set, used for the
// InstallVIPs/RemoveVIPs payloads.
VIPs []netip.Addr `json:"vips"`
// AcceptMode is v3-only, default false; stored for the state snapshot only
// (dataplane enforcement is out of scope, see umbrella Known Limitations).
AcceptMode bool `json:"accept-mode"`
}
Config is the resolved per-instance VRRP configuration embedded in the Startup and ConfigUpdated events. The engine (spec-vrrp-5) builds it from validated YANG config; the FSM treats every field as a precondition (ranges are checked upstream, asserted here only in tests).
RFC 9568 Section 5.2.4 / RFC 3768 Section 5.3.4: "The priority value for the VRRP Router that owns the IPvX address ... MUST be 255"; Backups "MUST use priority values between 1-254".
type ConfigUpdated ¶
type ConfigUpdated struct {
Config Config `json:"config"`
}
ConfigUpdated re-applies configuration to a running instance. Produced by the engine on config apply.
type EmitStateChange ¶
type EmitStateChange struct {
From State `json:"from"`
To State `json:"to"`
Reason string `json:"reason"`
}
EmitStateChange asks the engine to publish a state transition on the eventbus, increment metrics, and log it. Reason is one of the Reason* constants.
type Event ¶
type Event interface {
// contains filtered or unexported methods
}
Event is the closed set of typed inputs the FSM consumes. Exactly one Event is passed to Instance.Handle per call.
type InstallVIPs ¶
InstallVIPs asks the engine to register the full desired virtual-address set via the iface address-owner registry (spec-vrrp-3/5).
type Instance ¶
type Instance struct {
// contains filtered or unexported fields
}
Instance is one VRRP group's state machine (per interface, per family, per VRID). It is pure and single-threaded: only the owning worker goroutine calls Handle. It holds no locks, spawns no goroutines, and performs no I/O; the only use of the injected clock is Now() for snapshot timestamps.
func New ¶
New creates an Instance in Initialize. The clock is used only for Now() timestamps; the FSM never schedules. Configuration arrives with the Startup event.
func (*Instance) Handle ¶
Handle evaluates one event against the current state and returns the ordered action slice the engine must execute. It mutates internal state but performs no side effects itself.
type MasterDownExpired ¶
type MasterDownExpired struct {
Gen uint64 `json:"gen"`
}
MasterDownExpired is delivered by the engine's master-down clock.Timer, echoing the Gen of the arming StartMasterDownTimer action.
RFC 9568 Section 6.4.2 / RFC 3768 Section 6.4.2: Backup promotes when the down timer fires.
type PreemptDelayExpired ¶
type PreemptDelayExpired struct {
Gen uint64 `json:"gen"`
}
PreemptDelayExpired is delivered by the engine's preempt-delay clock.Timer, echoing the Gen of the arming StartPreemptDelayTimer action. No RFC basis; vendor extension (Junos hold-time semantics).
type RemoveVIPs ¶
RemoveVIPs asks the engine to deregister the virtual addresses.
type SendAdvert ¶
type SendAdvert struct {
Priority uint8 `json:"priority"`
AdvertIntervalMs int `json:"advert-interval-ms"`
}
SendAdvert asks the engine to build and send a VRRP ADVERTISEMENT from THESE fields (never from a cached packet, per R-5 / holo bug 8).
RFC 9568 Section 7.2 / RFC 3768 Section 7.2: fill fields from the Virtual Router configuration state and transmit.
type SendAdvertZeroPriority ¶
type SendAdvertZeroPriority struct{}
SendAdvertZeroPriority asks the engine to send a Priority-0 ADVERTISEMENT (Master relinquishing).
RFC 9568 Section 6.4.3 / RFC 3768 Section 6.4.3: on Shutdown, Active(Master) MUST send an ADVERTISEMENT with Priority = 0 before Initialize.
type Shutdown ¶
type Shutdown struct{}
Shutdown stops an instance. Produced by the engine on config removal, plugin stop, or parent-link down.
RFC 9568 Section 6.4.2/6.4.3 / RFC 3768 Section 6.4.2/6.4.3: Backup cancels its timer; Active(Master) sends a Priority-0 advertisement first.
type Snapshot ¶
type Snapshot struct {
State State `json:"state"`
Since time.Time `json:"since"`
Version uint8 `json:"version"`
Priority uint8 `json:"priority"`
IsOwner bool `json:"is-owner"`
Preempt bool `json:"preempt"`
AcceptMode bool `json:"accept-mode"`
ConfiguredIntervalMs int `json:"configured-interval-ms"`
ActiveIntervalMs int `json:"active-interval-ms"`
LastAdvertSrc netip.Addr `json:"last-advert-src"`
LastAdvertAt time.Time `json:"last-advert-at"`
MasterDownArmed bool `json:"master-down-armed"`
AdvertArmed bool `json:"advert-armed"`
PreemptDelayArmed bool `json:"preempt-delay-armed"`
// SkewTime and MasterDownInterval are the DERIVED timers, exported here so
// the operator surface reports the values this FSM actually uses. Computed
// by the same skew()/masterDown() the state machine arms its timers with:
// a second formula in the show path would be free to disagree with the one
// that decides failovers, and the disagreement would be invisible.
//
// time.Duration, not milliseconds: a valid VRRPv3 skew is sub-millisecond
// (78.125us at priority 254 with a 10ms interval), so a millisecond field
// would render the most interesting values as 0.
SkewTime time.Duration `json:"skew-time"`
MasterDownInterval time.Duration `json:"master-down-interval"`
}
Snapshot is the state view surfaced by `show vrrp` (spec-vrrp-5). Deadlines are owned by the engine's clock.Timer set; this reports which timer roles the FSM currently has armed.
type StartAdvertTimer ¶
StartAdvertTimer asks the engine to arm/reset the advert clock.Timer at the router's own configured interval.
type StartMasterDownTimer ¶
type StartMasterDownTimer struct {
Duration time.Duration `json:"duration"`
Gen uint64 `json:"gen"`
}
StartMasterDownTimer asks the engine to arm/reset the master-down clock.Timer. Duration is a time.Duration (nanoseconds); Gen is the staleness generation.
type StartPreemptDelayTimer ¶
type StartPreemptDelayTimer struct {
Duration time.Duration `json:"duration"`
Gen uint64 `json:"gen"`
}
StartPreemptDelayTimer asks the engine to arm the preempt-delay clock.Timer (Junos hold-time; no RFC basis).
type Startup ¶
type Startup struct {
Config Config `json:"config"`
}
Startup begins an instance. Produced by the engine on instance start (config commit/apply and parent-link readiness).
RFC 9568 Section 6.4.1 / RFC 3768 Section 6.4.1: Initialize transitions on Startup, the address owner to Active(Master), a non-owner to Backup.
type State ¶
type State uint8
State is the VRRP instance state. RFC 9568 renamed Master to "Active Router"; this package keeps Master to match operator vocabulary (see doc.go).
RFC 9568 Section 6.4 / RFC 3768 Section 6.4: one state machine instance per Virtual Router, with states Initialize, Backup, and Active(Master).
const ( // StateInitialize: RFC 9568 Section 6.4.1 -- the instance is not // participating; it waits for Startup. StateInitialize State = iota // StateBackup: RFC 9568 Section 6.4.2 -- monitoring the Active Router, // ready to promote when the down-timer fires. StateBackup // StateMaster: RFC 9568 Section 6.4.3 (RFC term "Active") -- forwarding for // the virtual address(es) and advertising. StateMaster )
type StopPreemptDelayTimer ¶
type StopPreemptDelayTimer struct{}
StopPreemptDelayTimer asks the engine to cancel only the preempt-delay timer.
type StopTimers ¶
type StopTimers struct{}
StopTimers asks the engine to cancel all three timers (master-down, advert, preempt-delay).