fsm

package
v0.0.0-...-6b8ee43 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

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

View Source
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

type InstallVIPs struct {
	VIPs []netip.Addr `json:"vips"`
}

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

func New(clk clock.Clock) *Instance

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

func (i *Instance) Handle(ev Event) []Action

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.

func (*Instance) Snapshot

func (i *Instance) Snapshot() Snapshot

Snapshot returns the current instance state view.

func (*Instance) State

func (i *Instance) State() State

State returns the current state.

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

type RemoveVIPs struct {
	VIPs []netip.Addr `json:"vips"`
}

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

type StartAdvertTimer struct {
	Interval time.Duration `json:"interval"`
	Gen      uint64        `json:"gen"`
}

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
)

func (State) String

func (s State) String() string

String returns the operator-facing state name.

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL