VRRP transition scripts never run — two defects in src/system/keepalived-fifo.py
Report body for vyos.dev. Addresses, hostnames and identities replaced with documentation
equivalents. Line numbers refer to rolling at b7e948397 (2026-08-27).
Summary
transition-script was configured on a VRRP instance for months and never executed once.
Two independent defects in src/system/keepalived-fifo.py combine to produce this. Both are
still present in rolling, and both are visible by inspection.
Neither produces any error, warning or log line. The configuration is accepted, the daemon
runs, the transitions happen — and nothing fires.
Defect 1 — the notify regex cannot match names containing :
src/system/keepalived-fifo.py:109
regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE)
The name group accepts [\w-]+ — letters, digits, underscore, hyphen. It does not
accept a colon.
VyOS itself accepts colons in VRRP group names: set high-availability vrrp group foo:11 ...
commits without complaint, and foo:11 is a widespread naming convention. For any such
instance the regex never matches, notify_message is None, and the block at lines 122-142
that dispatches transition-script is never reached.
Observed, with instances named <cluster>:<vrid> and a sync group named <cluster>:
Received message: INSTANCE "cluster:999v6" MASTER 100 <- nothing follows Received message: GROUP "cluster" FAULT 0 GROUP cluster changed state to FAULT <- regex matched
The logger.info at line 126 only appears for the sync group, whose name has no colon.
Instance transitions are silently dropped.
This is the worst kind of failure: the CLI accepts a configuration that its own dispatcher
cannot process, and says nothing at any point.
Design question for the maintainers
Two directions, and the choice belongs to the project:
- Widen the regex to the character set the CLI actually accepts for VRRP group names.
- Reject those names at configuration time.
Option 1 is non-breaking and fixes existing deployments silently affected today. Option 2 is
arguably more correct but breaks working configurations on upgrade. Either is better than the
current silent mismatch. I have not sent a patch precisely because this needs deciding first.
Defect 2 — the FIFO reader truncates messages
src/system/keepalived-fifo.py:150-164
def pipe_wait(self): self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK) while self.stopme.is_set() is False: time.sleep(0.250) try: message = os.read(self.pipe_read, 500) if message: for line in message.decode().strip().splitlines(): self.message_queue.put(line) self.message_event.set()
os.read() is bounded to 500 bytes and no residual buffer is kept between iterations. A
transition involving enough VRRP instances produces more than that in one burst: the read
splits mid-line, the tail of the chunk is queued as an incomplete fragment, and the head of
the next chunk is queued as another.
Observed on a pair with 14 instances in one sync group:
ROUP cluster changed state to MASTER <- the "G" ended the previous read Received message: INSTANCE "cl uster:999v6" BACKUP 200
n_type then holds ROUP, the elif n_type == 'GROUP' at line 136 fails, and no script
runs. Whether a given transition fires is decided by where the 500-byte boundary happens to
fall, which makes it look intermittent.
This affects any deployment with enough VRRP instances to exceed 500 bytes of notifications in
a single transition — it is not specific to unusual configurations.
Suggested fix
Keep the incomplete trailing line and process only complete ones:
self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK) buf = str() # residual buffer: keeps the incomplete trailing line between reads while self.stopme.is_set() is False: time.sleep(0.250) try: message = os.read(self.pipe_read, 500) if message: buf += message.decode() # only complete lines are processed, the tail is kept for the next read lines = buf.split('\n') buf = lines.pop() queued = False for line in lines: line = line.strip() if line: self.message_queue.put(line) queued = True if queued: self.message_event.set()
Running in production since 2026-08-27. Transition scripts fire reliably in both directions;
before the change they fired perhaps one time in three.
I can open a PR for this one if it is useful — it is self-contained and does not depend on how
defect 1 is resolved.
Interaction between the two
They must both be fixed. Working around defect 1 alone — by moving transition-script from
the instance to the sync group, whose name has no colon — makes scripts fire *sometimes*,
because defect 2 then truncates GROUP into ROUP on roughly two transitions out of three.
That intermittency is what makes this hard to diagnose from the outside.
Environment
| Affected code | rolling @ b7e948397, unchanged in this area for a long time |
| Observed on | 2026.06.20-0050-rolling and 2026.08.14-0025-rolling |
| Topology | HA pair, 14 VRRP instances (IPv4 + IPv6) in a single sync group, no-preempt, rfc3768-compatibility |
| Instance naming | <cluster>:<vrid> — the colon that defect 1 chokes on |
Both defects are demonstrable by reading the source; the logs above are the observed
manifestation rather than the proof.