Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-upgrade~30 min

Upgrade Breaks Routing Daemon Behaviour

Reported symptoms

  • Provider B has carried no traffic in either direction since 22:14 on Sunday, the minute `edge2` finished rebooting onto its new image. The NOC opened a ticket with the provider
  • Provider B's NOC replies that their side is up and they are receiving zero prefixes from us, so from their view we withdrew everything
  • Provider A is now carrying all of the estate transit and is congesting at peak, which is being investigated as a separate capacity problem
  • `show ip bgp summary` on `edge2` shows both eBGP sessions Established with an uptime matching the reboot, and iBGP to `edge1` is healthy
  • The upgrade validation passed at 23:10 on Sunday: configuration committed clean, sessions Established, OSPF adjacencies Full, pings fine
  • The configuration is unchanged. A diff of the pre-upgrade and post-upgrade `show configuration commands` captures contains nothing in the BGP tree
  • No alert fired at any point. The BGP alert watches session state, and the session never left Established

Evidence

  • · `show ip bgp summary` — provider A shows a prefix count in the hundreds of thousands, provider B shows `0` in the `State/PfxRcd` column, both Established for the same eleven hours
  • · `show ip bgp neighbors 203.0.113.1 received-routes` and `... advertised-routes` — nothing in either direction
  • · `/config/pre-upgrade-bgp.txt`, captured before the reboot — provider B carried a full table at the same moment on the previous image
  • · `show configuration commands | match "protocols bgp neighbor 198.51.100.1"` — the provider A neighbour has an import and an export route-map attached
  • · `show configuration commands | match "protocols bgp neighbor 203.0.113.1"` — the provider B neighbour has neither
  • · `diff /config/pre-upgrade-frr.txt /config/post-upgrade-frr.txt` — the rendered FRR configuration gained a policy-enforcement line under `router bgp` that the previous image did not emit
  • · `diff /config/pre-upgrade-cli.txt /config/post-upgrade-cli.txt` — no difference anywhere in the BGP tree
  • · `show version` and `vtysh -c "show version"` — the VyOS image and the routing engine inside it, read off the box rather than inferred
  • · `journalctl -u frr --since "2026-08-16 22:00"` — nothing about the peer beyond the ordinary session establishment. Nothing was invalid, so nothing warned
  • · `show system image` — the previous image is still installed and bootable
Diagnosis and resolutionclick to reveal

Root cause

Nothing in the configuration changed and the router changed anyway, because the thing that moved was a default underneath it. FRR implements RFC 8212: an eBGP session with no inbound and no outbound policy attached can establish normally and exchange no prefixes at all in either direction, and VyOS exposes the switch as `set protocols bgp parameters ebgp-requires-policy`. On the image that was running until Sunday night, the rendered FRR configuration did not have that enforcement in force; on the new image it does, and the before-and-after diff of `vtysh -c 'show running-config'` is where that is visible. The VyOS configuration is not where an FRR default lives, which is why the CLI diff shows nothing: the operator writes the VyOS tree, the image renders it into FRR's own syntax at commit time, and the rendering templates ship with the image. An upgrade therefore replaces the half of the routing configuration that nobody wrote down. Provider A survived because its neighbour was built with a route-map in each direction — a prefix filter and a local-preference setting inbound, an originate-only filter outbound — which satisfies the enforcement as a side effect of being a properly built transit session. Provider B was added in a hurry, works fine with no policy at all on the old default, and has been running that way since. "No policy" quietly meant "permit everything" until Sunday, and now means what the RFC says it should mean. Three separate things then made this survivable for eleven hours and invisible for all of them. The commit was clean, because the commit engine validates the configuration against the schema and has no opinion about whether the router still does what it did yesterday. The session was Established, because policy enforcement is not a session failure — the TCP connection, the OPEN exchange and the keepalives are all unaffected, and the peer is a perfectly healthy peer with nothing to say. And the alert watched the session state rather than the prefix count, so the one number that changed was the one number nothing was looking at. The upgrade validation at 23:10 asked whether things were up. Everything was up.

Remediation

Attach the policy the session should always have had, and resist the one-liner that makes the symptom go away. An inbound route-map that filters what a transit provider may hand you and sets the local preference you intend, and an outbound route-map that advertises your own prefixes and nothing else, are the two objects this neighbour has been missing since the day it was built; they satisfy the enforcement as a by-product of being correct. Refuse a pair of permit-any route-maps, which satisfies the enforcement and nothing else — a transit session with an unfiltered outbound policy is a route leak waiting for its moment, and the day it fires you are transit between two providers with your own router's CPU as the only limit. Refuse `ebgp-requires-policy` being turned off, which is the tempting fix and the worst one: it restores Sunday's behaviour on every eBGP session on the box, including the ones you have not audited yet, and removes the only guard standing between a future mistake and that same leak. Apply the change under `commit-confirm` so an error rolls itself back, expect the CPU and convergence cost of a full table arriving, and pick the moment deliberately — if the remaining transit is merely carrying more than usual this can wait for a window, and if it is saturating it cannot. Rolling back the image is a legitimate option and should be named rather than assumed away: `set system image default-boot` with the previous image and a reboot restores the old default in one step, at the cost of another outage window, of reverting everything else the upgrade brought, and of leaving you to meet this trap again on the next attempt with nothing learned. Prefer it only if the policy work cannot be done safely right now. Whichever path you take, finish by sweeping the estate: every router upgraded in that window, and on each of them every eBGP neighbour with no route-map in one or both directions. That list is a grep of the configuration, not an investigation.

Verification

Read the prefix count, not the session state — the state word was Established throughout the outage and is the reason nobody noticed. `show ip bgp summary` must show provider B's `State/PfxRcd` back within a sane band of the pre-upgrade capture in `/config/pre-upgrade-bgp.txt`, and that comparison is the point of having taken the capture. Then check the direction almost nobody checks: `show ip bgp neighbors 203.0.113.1 advertised-routes` must list exactly the prefixes you intend to originate and nothing else, because the outbound policy you just wrote is the object that decides whether this router can leak the internet between two providers, and an enforcement that is satisfied is not the same claim as a policy that is correct. Confirm from outside: ask provider B for the prefix count they receive from you, since their view is the only evidence in this incident not produced by the router that produced the fault, and confirm traffic has actually returned by reading interface counters and the ratio between the two transits rather than the routing table. Prove the check can fail — on a lab router, remove one direction's route-map and confirm the count falls to zero and that the monitoring you have just added fires — because a threshold nobody has watched trip is an assumption. Finally, verify that you fixed this rather than hid it: `vtysh -c 'show running-config'` must still show the policy enforcement in force, and the tooling that scrapes this router must be run by hand against the upgraded image and read, since an exporter that silently stopped parsing is the other half of this upgrade's blast radius.

Prevention

Build every eBGP session with an inbound and an outbound policy, on every platform, whatever the current default permits. An option you do not set is a value somebody else owns and may change in a release that is entirely correct. Make upgrade validation a diff rather than a checklist of things that are up: capture `show configuration commands`, `vtysh -c 'show running-config'`, route counts, prefix counts per session and adjacency counts before the reboot, take the same captures after it, and compare them — the numbers are the test, and "sessions Established, adjacencies Full, pings fine" is a description of a router that has not been checked. Alert on prefix count per session with a floor, not on session state alone; a session Established at zero prefixes is a distinct and alertable condition, and here it was the entire incident. Record both versions in the change record by reading them off the box — `show version` for the image and `vtysh -c 'show version'` for the routing engine — because the second one is what settles an argument about a behaviour that changed. Canary the upgrade on a router that is not carrying half your transit, and run the monitoring and the automation against it before the fleet, since output formats move between releases and a scraper that stopped parsing produces silence rather than an error. Keep the pre-upgrade configuration file off the box; a configuration saved after an upgrade is a new-schema file that the previous image has no migration to read, so it is not the rollback artefact people assume it is. And treat the asymmetry itself as the finding: one session was built properly and one was built in a hurry, and the upgrade only exposed which was which.

Reported symptoms

edge2 is one of two internet edge routers. It holds eBGP sessions to two transit providers — A at 198.51.100.1 and B at 203.0.113.1 — and an iBGP session to edge1. On Sunday night it was upgraded to a new image with add system image, set system image default-boot and a reboot, in a change window that ran from 22:00 to 23:10.

The post-upgrade validation passed. The configuration committed cleanly, both eBGP sessions came up Established, the OSPF adjacencies reached Full, and test traffic worked. The window was closed on time.

By Monday morning there were three tickets, and none of them was about the upgrade:

  • Provider B’s link has carried nothing in either direction since 22:14 on Sunday. The NOC raised it with the provider, who replied that their side is up and that they are receiving zero prefixes from us — from their view, we withdrew our entire estate.
  • Provider A is carrying all of the transit on its own and is congesting at peak. That is being worked as a capacity problem, with a request to the provider for a link upgrade.
  • Nothing has alerted. The BGP monitoring watches session state, and neither session has left Established for a moment.

On the router, the first checks all come back clean. Both sessions are up. The configuration is byte-identical to Friday’s — the diff between the pre-upgrade and post-upgrade show configuration commands captures contains nothing in the BGP tree at all. And the iBGP session to edge1, on the same box, on the same image, works perfectly.

Evidence provided

Read-only / Safetwo sessions Established for the same eleven hours, and one of them has said nothing
vyos@edge2:~$ show ip bgp summary
IPv4 Unicast Summary (VRF default):
BGP router identifier 10.255.0.2, local AS number 64512

Neighbor          V     AS   MsgRcvd   MsgSent   Up/Down  State/PfxRcd
198.51.100.1      4  64500   1841022      1193  11:04:18        951204
203.0.113.1       4  64510       412       409  11:04:12             0
10.255.0.1        4  64512      2214      2209  11:03:57          1180

Total number of neighbors 3

Illustrative output

Read-only / Safethe capture taken before the reboot — provider B carried a full table an hour earlier
vyos@edge2:~$ cat /config/pre-upgrade-bgp.txt
Neighbor          V     AS   MsgRcvd   MsgSent   Up/Down  State/PfxRcd
198.51.100.1      4  64500  22841901    418822  27w1d          950877
203.0.113.1       4  64510  19022145    311904  19w3d          950812
10.255.0.1        4  64512    884101    883217  27w1d            1179

Illustrative output

Read-only / Safethe direction the provider is complaining about
vyos@edge2:~$ show ip bgp neighbors 203.0.113.1 advertised-routes
Total number of prefixes 0

Illustrative output

Read-only / Safethe session that still works, and the two lines that are the difference
vyos@edge2:~$ show configuration commands | match 'protocols bgp neighbor 198.51.100.1'
set protocols bgp neighbor 198.51.100.1 remote-as '64500'
set protocols bgp neighbor 198.51.100.1 address-family ipv4-unicast route-map import 'RM-TRANSIT-A-IN'
set protocols bgp neighbor 198.51.100.1 address-family ipv4-unicast route-map export 'RM-ORIGINATE-ONLY'

Illustrative output

Read-only / Safethe session that does not, built in a hurry nineteen weeks ago
vyos@edge2:~$ show configuration commands | match 'protocols bgp neighbor 203.0.113.1'
set protocols bgp neighbor 203.0.113.1 remote-as '64510'
set protocols bgp neighbor 203.0.113.1 address-family ipv4-unicast

Illustrative output

Read-only / Safethe configuration the operator owns did not move
vyos@edge2:~$ diff /config/pre-upgrade-cli.txt /config/post-upgrade-cli.txt
Read-only / Safethe configuration the image owns did
vyos@edge2:~$ diff /config/pre-upgrade-frr.txt /config/post-upgrade-frr.txt
--- /config/pre-upgrade-frr.txt
+++ /config/post-upgrade-frr.txt
@@ router bgp 64512 @@
-  no bgp ebgp-requires-policy
+  bgp ebgp-requires-policy

Illustrative output

Work the evidence before reading on

An unchanged configuration produced a changed router, and both halves of that sentence are provably true.

  1. The CLI diff is empty and the FRR diff is not. Who writes each of those two files, when is each of them produced, and which one did the upgrade replace?
  2. Provider A and provider B are the same kind of session on the same router with the same image. List what differs between them, using the two configuration extracts, and note that the difference is two lines that are present on one and absent on the other.
  3. The session to provider B is Established and has exchanged nothing in either direction. Which parts of BGP have to work for a session to reach Established, and is deciding what to advertise one of them?
  4. advertised-routes is zero as well as received-routes. A fault that silenced one direction would be a filter; what kind of rule silences both at once, and what does it use as its trigger?
  5. Sunday’s validation checked that the sessions were up. What would it have had to compare against to catch this, and does that artefact exist on this router?

Then the question that resolves it: nothing in the configuration says what should happen to an eBGP session that has no policy attached. So who decided, and where does their decision live?

Root cause

1. The operator owns half the routing configuration

On VyOS you do not write FRR configuration. You write the VyOS tree, and the image renders it into FRR’s own syntax at commit time using templates that ship with that image. There are therefore two configuration files behind every routing decision this box makes, and only one of them is in your repository.

That is why the two diffs disagree. The CLI capture is unchanged because the migration had nothing to rewrite in the BGP tree. The rendered FRR capture gained a line under router bgp that the previous image did not emit, and that line is the whole incident.

2. RFC 8212, and what “no policy” used to mean

FRR implements RFC 8212: an eBGP session with no inbound and no outbound policy attached may establish and exchange nothing at all. VyOS exposes the switch as set protocols bgp parameters ebgp-requires-policy.

Until Sunday, the rendered configuration on this box did not have the enforcement in force, and a neighbour with no route-map behaved as though it had a permit-everything policy in both directions. Since Sunday it does have it, and the same neighbour behaves as the RFC says it should.

Provider A is unaffected for a reason worth stating plainly: it was built properly. It has an inbound route-map that filters what a transit provider is allowed to hand you and sets the local preference the design intends, and an outbound route-map that advertises your own prefixes and nothing else. Satisfying the enforcement is a side effect of that, not the reason for it.

Provider B was turned up in a hurry nineteen weeks ago, worked immediately with no policy, and was never revisited. The upgrade did not break it. The upgrade stopped concealing that it had been built without the two objects a transit session needs.

3. Every check that was run was true, and none of them was about this

The commit was clean because the commit engine validates the configuration against the schema. It has no opinion about whether the router still does what it did on Friday, and it cannot have one.

The session was Established because policy enforcement is not a session failure. TCP came up, the OPEN messages were exchanged, capabilities were negotiated, keepalives flow. The peer is entirely healthy and has nothing to say.

The alert did not fire because it watches session state. The one number that changed — the prefix count — was the number nothing was measuring, and it sits in the same line of the same command that the on-call engineer read at 23:05 and again at 09:20.

4. The blast radius arrived as somebody else’s ticket

With no prefixes from B, every route is learned from A, so all outbound traffic follows A. With nothing advertised to B, the return path collapses onto A as well. The estate did not lose connectivity; it lost half its capacity and all of its redundancy, and both losses presented as third-party problems — a provider who has stopped sending traffic, and a provider whose link is congested.

That is the characteristic shape of this failure class. It does not produce an outage on the router that caused it. It produces two plausible tickets pointing outward.

Resolution

  1. Decide the objective first: this is a redundancy and capacity outage rather than a connectivity one, so the choice is between a fast configuration change on a live router and a reboot back onto the previous image. Name both costs out loud before choosing.
  2. Prefer attaching policy. Write an inbound route-map that filters what a transit provider may hand you and sets the local preference the design intends, and an outbound route-map that advertises your own prefixes and nothing else, then attach both to the provider B neighbour address-family.
  3. Refuse a pair of permit-any route-maps. They satisfy the enforcement, restore the prefix counts, and leave you one mistake away from re-advertising a full table from one provider to the other with your own CPU as the only limit.
  4. Refuse turning the enforcement off. ebgp-requires-policy disabled is a one-line fix that restores Sunday behaviour on every eBGP session on the box, including the ones nobody has audited, and removes the guard that would catch the next session built in a hurry.
  5. Apply under commit-confirm so a mistake rolls itself back without a console visit, and expect the CPU and convergence cost of a full table arriving on a router that is currently carrying all of the estate transit.
  6. Keep the image rollback as a named option rather than a fallback nobody discussed: set system image default-boot with the previous image and a reboot restores the old default in one step, at the cost of a second outage window, of reverting everything else the upgrade brought, and of meeting this again on the next attempt.
  7. Once B is exchanging prefixes, re-check the traffic ratio between the two transits and close the provider A capacity ticket if the congestion was purely a consequence of this.
  8. Sweep the rest of the fleet: every router upgraded in that window, and on each one every eBGP neighbour with no route-map in one or both directions. That list is a grep of the configuration, not an investigation.

Verification

  1. show ip bgp summary shows provider B back within a sane band of the count in /config/pre-upgrade-bgp.txt. The prefix column is the test; the state word was Established throughout the outage.
  2. show ip bgp neighbors 203.0.113.1 advertised-routes lists exactly the prefixes you intend to originate and nothing else. An enforcement that is satisfied and a policy that is correct are different claims, and this is the one that decides whether you can leak the internet between two providers.
  3. Provider B confirms the prefix count they receive from you. That is the only evidence in this incident not produced by the router that produced the fault.
  4. Traffic has actually returned, read from interface counters and the ratio between the two transits rather than from the routing table. Routes present and traffic flowing are separate facts.
  5. vtysh -c "show running-config" still shows the policy enforcement in force, confirming you fixed the session rather than disabled the guard.
  6. The check can fail. On a lab router, remove one direction of policy and confirm the prefix count falls to zero and the new alert fires, so the threshold has been watched tripping rather than assumed.
  7. The monitoring and automation are run by hand against the upgraded image and their output read. An exporter that stopped parsing a changed output format is the other half of this upgrade blast radius and produces silence rather than an error.

Prevention

  • Build every eBGP session with an inbound and an outbound policy, whatever the current default permits. An option you do not set is a value somebody else owns, and they are allowed to change it in a release that is entirely correct and properly documented.
  • Make upgrade validation a diff, not a checklist. Capture the configuration, the rendered FRR configuration, route counts, per-session prefix counts and adjacency counts before the reboot; take the same captures after; compare the numbers. “Sessions Established, adjacencies Full, pings fine” describes a router nobody has checked.
  • Alert on prefix count per session with a floor, not on session state alone. A session Established at zero prefixes is a distinct condition and it was this entire incident.
  • Read both versions off the box and put them in the change record — show version for the image and vtysh -c 'show version' for the routing engine. When a behaviour argument turns on which routing engine you are running, the second command is what settles it.
  • Canary the upgrade on a router that is not carrying half your transit, and run the monitoring and automation against it before the fleet, because output formats move between releases and a scraper that stopped parsing fails quietly.
  • Keep the pre-upgrade configuration file off the box. A configuration saved after an upgrade is a new-schema file that the previous image has no migration to read, so it is not the rollback artefact it is assumed to be.
  • Treat the asymmetry as a finding in its own right. One session was built properly and one was built in a hurry; the upgrade did not create that difference, it published it.