vyos: move PPPoE off the config plane onto a gated systemd unit
Some checks failed
CI/CD / lint (push) Failing after 25s
CI/CD / typecheck (push) Failing after 23s
CI/CD / test (push) Failing after 23s
CI/CD / build (push) Has been skipped
CI/CD / publish-rpm (push) Has been skipped
CI/CD / publish-deb (push) Has been skipped

PPPoE HA could not work as written, and the reason is structural rather than a
bug: `set interfaces pppoe pppoe0 disable` and `delete` are handled identically
by interfaces_pppoe.py -- both UNLINK /etc/ppp/peers/pppoe0. That path is pppd's
own options file, so the resting state destroyed exactly what the promotion path
needed, and `ppp@pppoe0` restart-looped against it (observed: 47 restarts, zero
sessions at the access concentrator). It also made op-mode `connect interface
pppoe0` unusable, and put every failover behind a priority-322 commit where one
unrelated invalid node fails the whole thing -- which has already taken the
10 gig down once.

pppoe0 is now configured identically and ENABLED on both routers, so the peers
file always exists, and dialling is gated by a drop-in on the unit:

    ConditionPathExists=/run/vrrp-wan/may-dial
    ConditionPathExists=/etc/ppp/peers/pppoe0

/run is tmpfs, so the gate is shut at boot and neither box can dial before VRRP
has decided. That matters more than it first appears: with the node enabled,
interfaces_pppoe.py restarts ppp on EVERY commit touching the pppoe subtree when
the daemon is not running -- so the backup actively tries to dial whenever
anything commits. The gate is the only thing making that a no-op, which is why
vrrp-wan-reconcile now refuses to bless a box whose drop-in is missing: /etc is
per-image, and a VyOS upgrade would otherwise silently remove the protection.

may-dial is a LEASE, not a flag. ConditionPathExists is evaluated at start only
-- it can prevent a dial, never revoke one -- so a reconciler that stops running
while its box is demoted would keep the one ISP session for ever. The reconciler
renews the lease; a new 5s vrrp-wan-guard revokes it, and only ever revokes. It
fired correctly first time: "GUARD: lease stale (81s > 75s)".

Also: remove-then-stop on release (the file's absence blocks a NEW start that a
concurrent commit would trigger); a flap damper, because two routers that both
believe they hold the VIP will both dial and each dial kills the other's session
-- against a real ISP that is how an account gets rate-limited; and a guard on
`cfg` returning empty under commit-lock contention, which had already produced
one spurious "releasing" on a box that needed nothing.

GRACE 90 -> 180. accel-ppp's dead-peer budget is lcp-echo-interval(30) x
failure(3) = 90s, so the old value sat exactly on the boundary: a hard failover
into an AC that does not replace the stale session would fail its own check,
shed the VIPs, and leave both routers in FAULT.

The sim could not have tested any of this. Both routers now get the identical
WAN -- the secondary had none "because two PPPoE clients sharing one credential
is a different failure mode than anything production has", which is backwards:
that IS production. It also left the pair incomparable, ten NAT rules against
none. Safety now comes from resting state, not asymmetry.

Three more things the sim was hiding:
  - the drift check's secondary regex omitted interfaces pppoe/bonding, nat
    source and protocols failover, so it reported "in sync" for a box with no
    WAN at all;
  - the VRRP health-check and transition-script hooks existed on both live VMs
    and in NEITHER generator -- the mechanism under test was pure undetected
    drift;
  - labsim-vyos's only default route was the libvirt-NAT scaffold, so every
    "the LAN still has internet" verdict on it was answered by eth2 rather than
    the WAN. --drop-scaffold applied; the earlier DHCP-failover proof is being
    re-run because of it.

vrrp-wan-install ends the other half of that: the sim's previous proof came from
scripts hand-`sed`-ed in place, so the tested behaviour was not the committed
behaviour. `--check` now makes that a hard failure.

First green run: master holds both WANs, backup released, and the AC reports
exactly ONE session. sim-net-apply.sh check: all four in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
This commit is contained in:
Michal
2026-09-05 18:50:16 +01:00
parent 2e828b8af2
commit 4efd70c987
14 changed files with 543 additions and 124 deletions

View File

@@ -1,46 +1,50 @@
#!/bin/vbash
# Enable or disable the WAN. Split out from vrrp-wan-reconcile for one reason:
# `source /opt/vyatta/etc/functions/script-template` must be the FIRST thing the
# script does. Sourced after a few statements -- an if, an exec, a mkdir -- it
# silently terminated the script; `set -x` showed execution stopping inside the
# source with no error and rc=0, so the reconciler reported success having done
# nothing. Only a single assignment may precede it (the template resets the
# positional parameters, so the mode is captured first), which is the same shape
# /config/vyos-known-good uses.
# Enable or disable the DHCP WAN (bond0.53). The ONLY part of the failover that
# touches VyOS configuration.
#
# vrrp-wan-apply enable take the WAN
# PPPoE deliberately does NOT appear here any more, and should not be added back
# for symmetry. `set interfaces pppoe pppoe0 disable` unlinks
# /etc/ppp/peers/pppoe0 -- interfaces_pppoe.py treats `disable` and `delete`
# identically -- and that path is pppd's options file, so the resting state
# destroyed what the promotion path needed and the unit restart-looped (47 times,
# zero sessions at the AC). It also made a pppoe node able to fail this commit
# and take the 10 gig down with it: `interfaces pppoe` is priority 322 and one
# invalid node fails the whole commit. PPPoE is now gated at the systemd unit
# instead; see migration/ppp-vrrp-gate.conf and vrrp-wan-reconcile.
#
# bond0.53 stays here because its lease is bound to a cloned MAC and only VyOS
# config can move a MAC between boxes.
#
# `source /opt/vyatta/etc/functions/script-template` must be the FIRST thing the
# script does. Sourced after an if, an exec and a mkdir it terminated the script
# inside the source, rc=0, no output -- the caller reported success having done
# nothing. Only a single assignment may precede it (the template resets the
# positional parameters), which is the shape /config/vyos-known-good uses.
#
# vrrp-wan-apply enable take the DHCP WAN
# vrrp-wan-apply disable release it
MODE="${1:-}"
source /opt/vyatta/etc/functions/script-template
WAN_VIF=53
CONF=/config/vrrp-wan.conf
[ -r "$CONF" ] && . "$CONF"
WAN_VIF="${WAN_VIF:-53}"
cfg() { /opt/vyatta/bin/vyatta-op-cmd-wrapper show configuration commands 2>/dev/null; }
wan_disabled(){ cfg | grep -q "vif ${WAN_VIF} disable"; }
ppp_disabled(){ cfg | grep -q "pppoe pppoe0 disable"; }
# Only touch pppoe0 if it is actually configured. `set interfaces pppoe pppoe0
# disable` on a box that has no pppoe0 CREATES the node with nothing but
# `disable`, and VyOS then refuses the commit with "Physical source-interface
# required for pppoe0!" -- taking the bond0.53 change down with it, because one
# invalid node fails the whole commit. Seen on the labsim secondary, which has
# no PPPoE; production has it on both, so this would have been an untested path
# that only ever ran during a failover.
ppp_exists(){ cfg | grep -q "pppoe pppoe0 source-interface"; }
configure
if [ "$MODE" = enable ]; then
# Guarded: `delete` of an absent node aborts the whole batch with
# "Nothing to delete", which left the box detected-but-unfixed.
# "Nothing to delete", which once left the box detected-but-unfixed.
wan_disabled && delete interfaces bonding bond0 vif ${WAN_VIF} disable
ppp_exists && ppp_disabled && delete interfaces pppoe pppoe0 disable
else
wan_disabled || set interfaces bonding bond0 vif ${WAN_VIF} disable
ppp_exists && { ppp_disabled || set interfaces pppoe pppoe0 disable; }
fi
# Report the commit's verdict. The script previously ended on `exit` (a
# Report the commit's verdict. This script previously ended on `exit` (a
# script-template function) and returned 0 even after "Commit failed", so the
# reconciler logged a successful release that had not happened -- the worst kind
# of failure for something whose whole job is to keep two routers from holding
# one WAN.
# reconciler logged a release that had not happened -- the worst kind of failure
# for something whose job is to keep two routers from holding one WAN.
if commit 2>&1 | tee /tmp/vrrp-wan-commit.log | grep -qi "commit failed"; then
logger -t vrrp-wan "COMMIT FAILED applying '$MODE' -- see /tmp/vrrp-wan-commit.log"
exit 1