Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-bgp-prefix~30 min

Outbound BGP Filter Blocks Intended Prefix

Reported symptoms

  • During a planned drain of the ISP-B transit session for carrier maintenance, the new payments API on 203.0.113.0/24 became unreachable from the internet; every other service rode the drain without a packet lost
  • From inside the estate the service is perfectly healthy - the route is in the RIB, the servers answer, the internal synthetic monitor stays green throughout
  • External traceroutes during the drain stop at the last hop before our border and never reach us, which reads as an upstream black hole rather than an origination problem
  • Undoing the drain restored the service in under two minutes. That made it look like an ISP-B fault, and the incident was minutes from being closed against the carrier
  • The prefix passed acceptance testing three weeks ago and has had no complaint since; nothing was changed today except the drain itself
  • Both eBGP sessions are Established with the expected prefix counts, and the ISP-A session has been up for sixty-three days with no flap
  • The security team confirms the ROA and the IRR route object for 203.0.113.0/24 are published and correct, so prefix validation was checked early and ruled out

Evidence

  • · `show ip route 203.0.113.0/24` shows the prefix present and best - the RIB, which is upstream of BGP, is not the problem
  • · `show ip bgp 203.0.113.0/24` shows the prefix in the Loc-RIB, sourced local and best, and lists exactly one entry under "Advertised to non peer-group peers": 192.0.2.5, the ISP-B session
  • · `show ip bgp neighbors 192.0.2.1 advertised-routes` on the ISP-A session does not contain 203.0.113.0/24, while it does contain every prefix the company originated before this year
  • · `show ip bgp neighbors 192.0.2.1` reports an outbound route-map RM-TO-ISP-A, applied yes; the same command against 192.0.2.5 reports no outbound policy of any kind
  • · `show policy route-map RM-TO-ISP-A` shows two permit rules, both matching on the prefix-list PL-OUR-AGGREGATES, and no third rule - so a prefix that is not in that list falls off the end
  • · `show configuration commands | match PL-OUR-AGGREGATES` lists 198.51.100.0/22 and two more-specifics and does not mention 203.0.113.0/24
  • · After `clear route-map` and a soft outbound refresh, `vtysh -c 'show route-map RM-TO-ISP-A'` shows non-zero hit counts on both permit rules and no rule that fired for the missing prefix
  • · `show system commit` shows a revision three weeks old commented "add 203.0.113.0/24 for new service", which added the network statement and edited the inbound policy
Diagnosis and resolutionclick to reveal

Root cause

Nothing denied the prefix. The prefix was never permitted, which produces the same result and none of the evidence. RM-TO-ISP-A is the outbound policy on the ISP-A session, and both of its rules gate on membership of the prefix-list PL-OUR-AGGREGATES: rule 10 permits and prepends for the subset carrying the prepend community, rule 20 permits the rest. There is no rule 30. A route-map that falls off the end is filtered by the implicit deny, so a prefix absent from PL-OUR-AGGREGATES is silently not advertised - no deny rule to find, no log line, no counter that moves. When 203.0.113.0/24 was allocated three weeks ago the change added the network statement, updated the inbound policy and published the ROA and the IRR object. It did not add the prefix to PL-OUR-AGGREGATES, because the provisioning runbook for a new public prefix does not list that step. The prefix was therefore never advertised to ISP-A from the moment it existed. It was reachable anyway, and passed acceptance testing, because the ISP-B session carries no outbound policy at all and advertises the entire Loc-RIB. That is the second defect, and for three weeks it masked the first one perfectly. The drain removed the mask. The service did not break during the maintenance window; it had been single-homed since the day it was built, and the maintenance window was simply the first time anyone looked at it with the other path gone.

Remediation

Fix the missing entry, not the symptom, and resist the rule that would make the symptom impossible: adding a catch-all permit to the end of RM-TO-ISP-A would restore this prefix and simultaneously offer ISP-A every route in the Loc-RIB, including the full table learned from ISP-B, which is a route leak rather than a fix. The correct change is one prefix-list entry - add 203.0.113.0/24 to PL-OUR-AGGREGATES - because that list is the object the policy is built around and keeping it as the single inventory of what this AS originates is the property worth preserving. Read the diff with `compare`, commit with `commit-confirm`, then push the corrected policy to the peer with `clear ip bgp 192.0.2.1 soft out`, which uses route refresh and leaves the session up; a hard clear would withdraw every prefix from a transit session to fix a prefix that was never there. The second defect is a larger and slower piece of work and it is not the same change. Applying an export route-map to a session that has never had one can withdraw everything if the list behind it is incomplete, so build that list from what the estate intends to originate rather than from what happens to be in today's advertised-routes, and apply it in its own window with commit-confirm and someone watching the peer. If the window is gone, hold: leaving ISP-B unfiltered for one more night with a named owner and a fixed end time is a defensible decision, and applying an untested export filter to a live transit session at two in the morning is not.

Verification

`show ip bgp neighbors 192.0.2.1 advertised-routes` must contain 203.0.113.0/24, and the count of advertised prefixes must have risen by exactly one - if it moved by more than one, the change did more than intended and should be read again before anything else. That command states what this router sent, not what ISP-A accepted, so confirm acceptance independently: ask the peer, or read the prefix on a public looking glass from a network that does not transit ISP-B. Confirm the right rule fired rather than merely that the prefix appeared: run `clear route-map`, refresh outbound, and read `vtysh -c 'show route-map RM-TO-ISP-A'` - a prefix that is meant to be prepended and is instead being permitted by the general rule is a different bug wearing the same fix. Confirm the AS path seen externally matches the intended traffic-engineering policy for this prefix rather than assuming it. Then re-run the test that exposed the fault: drain the ISP-B session again, in a window, and confirm that 203.0.113.0/24 stays reachable from outside. That drain is the only check that closes this incident, and it is the one most likely to be skipped because by then the service is working. Finally, reconcile the whole set: compare the advertised prefixes on both sessions against the list of prefixes this AS intends to originate, and treat any difference in either direction as a finding, not as noise.

Prevention

The set of prefixes an AS originates is a single object, and this incident is what happens when it is maintained as four independent copies. The network statement, the outbound policy list, the IRR route objects and the RPKI ROAs must be generated from one source or reconciled against it on a schedule; a change that updates three of the four is the normal outcome of a human checklist, not an unusual one. Add a periodic reconciliation that compares each eBGP session's advertised-routes against the intended origination set and reports differences in both directions, because the missing advertisement here produced no alert of any kind and would have produced none for years. Make "this session has no outbound policy" a monitored state rather than an absence: an empty filter is invisible in every review, and the ISP-B session was both the reason the service worked and the largest leak risk in the estate. Test redundancy on a schedule instead of discovering it during maintenance - the drain that exposed this is a cheap, repeatable exercise that belongs in the calendar, and a path that has never been tested alone is a path that has never been tested. And treat "it works via the other upstream" as a hypothesis to disprove rather than as reassurance: redundancy does not remove defects, it converts loud ones into latent ones that surface at the moment you were relying on it.

Reported symptoms

The maintenance was routine and had been on the calendar for a fortnight. ISP-B was replacing a line card and asked for the session to be drained; the network team shifted traffic to ISP-A at 01:00, watched the graphs move, and confirmed the estate was healthy.

Nine minutes later the payments team escalated. Their new API — the one behind 203.0.113.0/24, live for three weeks — was returning nothing from the internet. Not slow, not erroring: unreachable.

What made it hard:

  • Everything else was fine. Twenty-odd services ride the same border. Not one of them noticed the drain.
  • From inside, the service was healthy. The route was in the RIB, the servers answered, and the internal synthetic monitor stayed green for the entire event. The application team could see nothing wrong and said so.
  • External traceroutes died before reaching us, at the last hop in ISP-A’s network. That reads as an upstream black hole, and it is exactly what a missing origination looks like from outside.
  • Undoing the drain fixed it in under two minutes. This is the dangerous one. Restoring the ISP-B session restored the service instantly and completely, which is powerful evidence for “ISP-B’s maintenance broke it” and no evidence at all for what actually happened.
  • Nothing had changed. The prefix passed acceptance testing three weeks ago and had not been touched since. No commit today.
  • Both sessions look right. Established, expected prefix counts, ISP-A up sixty-three days without a flap.
  • RPKI and IRR were checked first and were clean. The ROA and the route object for 203.0.113.0/24 are published and correct, which took prefix validation off the table early.

The incident was thirty seconds from being closed against the carrier.

The estate

rtr-border-1 is a VyOS 1.5 LTS router in AS 64512 with two transits and one job: originate the company’s address space and carry it to both upstreams.

  • ISP-A, peer 192.0.2.1, AS 64500. Outbound policy is the route-map RM-TO-ISP-A.
  • ISP-B, peer 192.0.2.5, AS 64600.

The company originates 198.51.100.0/22, two more-specifics inside it, and — since three weeks ago — 203.0.113.0/24 for the payments API. Origination is by network statement against prefixes present in the RIB.

RM-TO-ISP-A exists to do traffic engineering: a subset of the prefixes carry a community that means “prepend twice towards ISP-A”, and the rest go out unmodified. Both of its rules gate on membership of the prefix-list PL-OUR-AGGREGATES, which is meant to be the inventory of everything this AS originates.

Evidence provided

Read-only / Safethe RIB is upstream of BGP, and the RIB is fine
vyos@rtr-border-1:~$ show ip route 203.0.113.0/24
Routing entry for 203.0.113.0/24
Known via static, distance 1, metric 0, best
* 10.60.4.2, via eth2

Illustrative output

Read-only / Safeone peer listed under Advertised, and there are two peers
vyos@rtr-border-1:~$ show ip bgp 203.0.113.0/24
BGP routing table entry for 203.0.113.0/24
Paths: (1 available, best #1, table default)
Advertised to non peer-group peers:
192.0.2.5
Local
0.0.0.0 from 0.0.0.0 (10.255.0.1)
Origin IGP, metric 0, weight 32768, valid, sourced, local, best

Illustrative output

Read-only / Safeeverything originated before this year, and nothing from this year
vyos@rtr-border-1:~$ show ip bgp neighbors 192.0.2.1 advertised-routes
   Network            Next Hop     Metric  LocPrf  Weight  Path
*> 198.51.100.0/22    0.0.0.0           0           32768  i
*> 198.51.100.0/24    0.0.0.0           0           32768  i
*> 198.51.101.0/24    0.0.0.0           0           32768  i

Total number of prefixes 3

Illustrative output

Read-only / SafeISP-A has an outbound policy - run the same command against 192.0.2.5
vyos@rtr-border-1:~$ show ip bgp neighbors 192.0.2.1
BGP neighbor is 192.0.2.1, remote AS 64500, local AS 64512, external link
BGP state = Established, up for 63d04h11m
For address family: IPv4 Unicast
Outbound route-map: RM-TO-ISP-A
Outbound route-map applied: yes

Illustrative output

Read-only / Safetwo rules, one gate, and no rule 30
vyos@rtr-border-1:~$ show policy route-map RM-TO-ISP-A
Route-map RM-TO-ISP-A, permit, sequence 10
Match clauses:
ip address prefix-list: PL-OUR-AGGREGATES
community: CL-PREPEND-ISP-A
Set clauses:
as-path prepend 64512 64512
Action: permit

Route-map RM-TO-ISP-A, permit, sequence 20
Match clauses:
ip address prefix-list: PL-OUR-AGGREGATES
Set clauses:
Action: permit

Illustrative output

Read-only / Safethree entries - count them against what this AS originates
vyos@rtr-border-1:~$ show configuration commands | match PL-OUR-AGGREGATES
set policy prefix-list PL-OUR-AGGREGATES rule 10 action 'permit'
set policy prefix-list PL-OUR-AGGREGATES rule 10 prefix '198.51.100.0/22'
set policy prefix-list PL-OUR-AGGREGATES rule 20 action 'permit'
set policy prefix-list PL-OUR-AGGREGATES rule 20 prefix '198.51.100.0/24'
set policy prefix-list PL-OUR-AGGREGATES rule 30 action 'permit'
set policy prefix-list PL-OUR-AGGREGATES rule 30 prefix '198.51.101.0/24'

Illustrative output

Read-only / Saferevision 0 is three weeks old and is the only change that mentions the prefix
vyos@rtr-border-1:~$ show system commit
0   2026-07-28 14:06:52 by vyos via cli   add 203.0.113.0/24 for new service
1   2026-07-11 09:31:20 by vyos via cli   ntp: replace decommissioned server
2   2026-06-30 16:44:08 by vyos via cli   syslog: add secondary collector

Illustrative output

Work the evidence before reading on

The advertisement diagnostic has four boundaries and they are ordered for a reason: the RIB, the BGP table, what we advertise, and what the peer accepts. Walk them.

  1. Boundaries one and two are already answered by the first two blocks. Say out loud which boundary the failure is at before reading further, because that decision eliminates three quarters of the possible causes.
  2. show ip bgp 203.0.113.0/24 lists one peer under “Advertised to non peer-group peers” and this router has two peers. That single line is the whole diagnosis if you read it as an exhaustive list rather than as a fact about the one peer it names.
  3. Read RM-TO-ISP-A and ask what happens to a route that matches neither rule. Then ask what evidence that outcome leaves behind.
  4. Compare the prefix-list entries against the set of prefixes this AS originates. The counting takes five seconds.
  5. Now the harder question, and the one that turns a one-line fix into a real incident review: the prefix has been unadvertised to ISP-A for three weeks. Why did nobody notice, and what does the answer imply about the ISP-B session?

Before continuing: nothing in this configuration denies the prefix. Explain how it is being filtered anyway, and explain why the service worked yesterday.

Root cause

1. A route-map filters by omission, and omission leaves no trace

RM-TO-ISP-A has two rules and both of them gate on membership of PL-OUR-AGGREGATES. A route that is not in that list matches neither rule, falls off the end of the route-map, and is filtered by the implicit deny.

This is the semantic that separates a route-map from a prefix-list in the reader’s head, and it is worth stating plainly: a route-map that does not match a route denies it. There is no “permit by default” at the bottom. So the search for the deny rule that is blocking 203.0.113.0/24 fails, because there is no deny rule. The prefix is not denied. It is unmentioned, and unmentioned is denied.

The consequence for diagnosis matters more than the semantic: a deny rule leaves a hit count, a diff and something to grep for. An omission leaves nothing. Nothing is logged, no counter moves, and show ip bgp 203.0.113.0/24 reports a healthy, best, locally sourced route — because from the Loc-RIB’s point of view it is healthy. The only evidence available is a negative one: the peer is absent from the “Advertised to” list.

2. The provisioning change updated three places out of four

The commit three weeks ago did real work. It added the network statement so the prefix entered the Loc-RIB, adjusted the inbound policy, and the same change ticket covered publishing the ROA and the IRR route object. Every one of those is correct and every one of them was verified.

PL-OUR-AGGREGATES was not touched, because the provisioning runbook for a new public prefix does not mention it. The list is maintained by hand, is referenced from a route-map rather than bound to a neighbour directly, and is therefore two hops away from the place where anyone thinks about origination.

3. The other transit hid it, and hid it perfectly

Run the same show ip bgp neighbors against 192.0.2.5 and there is no outbound policy at all. The ISP-B session advertises the whole Loc-RIB.

That is why the service passed acceptance testing, why external monitoring was green for three weeks, and why nobody saw a problem: 203.0.113.0/24 was globally reachable the entire time, through exactly one upstream. It was single-homed from the day it was built, and nothing in the estate was capable of saying so.

The drain did not break the service. The drain removed the only path the service had ever had.

Resolution

  1. Capture the before state into the ticket: show ip bgp neighbors 192.0.2.1 advertised-routes including the total count, and show ip bgp 203.0.113.0/24. The count is the number you will diff against, and a fix that moves it by more than one did something you did not intend.
  2. Add the prefix to the list, not a rule to the route-map: set policy prefix-list PL-OUR-AGGREGATES rule 40 action permit and set policy prefix-list PL-OUR-AGGREGATES rule 40 prefix 203.0.113.0/24. The list is the inventory of what this AS originates, and keeping it as the single gate is the property that makes the policy reviewable.
  3. Do not add a catch-all permit to the end of RM-TO-ISP-A. It would fix this prefix and simultaneously offer ISP-A everything in the Loc-RIB, including the full table learned from ISP-B. That is not a wider filter, it is a route leak, and unlike this incident it damages other people.
  4. Read the diff with compare, then commit-confirm 10. This is outbound policy on a live transit session; the timer is what recovers you if the change is wrong in the direction you did not anticipate.
  5. Push the corrected policy to the peer with clear ip bgp 192.0.2.1 soft out. That is a route refresh and leaves the session up. A hard clear ip bgp 192.0.2.1 would withdraw every prefix from a working transit to publish one that was never there.
  6. Confirm and save once advertised-routes contains the prefix and the total has risen by exactly one.
  7. Treat the missing outbound policy on ISP-B as a separate change with its own window, its own review and its own owner. Building an export filter for a session that has never had one is not a same-night task: if the list behind it is incomplete you withdraw everything, and the list must be built from what the estate intends to originate rather than from what today happens to be in advertised-routes.
  8. If that window does not exist tonight, hold, explicitly. Leaving ISP-B unfiltered until a scheduled change, with a named owner and a fixed end time, is a defensible decision. Applying an untested export filter to a live transit at 02:00 is not, and the incident does not oblige you to do it.

The change itself, in the 1.5 configuration tree, is two lines:

set policy prefix-list PL-OUR-AGGREGATES rule 40 action 'permit'
set policy prefix-list PL-OUR-AGGREGATES rule 40 prefix '203.0.113.0/24'

The binding that is already in place, and that you should read back rather than assume, is the neighbour’s export route-map:

set protocols bgp neighbor 192.0.2.1 address-family ipv4-unicast route-map export 'RM-TO-ISP-A'

Verification

  1. The prefix is advertised. show ip bgp neighbors 192.0.2.1 advertised-routes contains 203.0.113.0/24, and the total has gone from three to four. Exactly one more.
  2. The right rule fired. Run clear route-map, refresh outbound, then read the hit counts with vtysh. A prefix that is meant to be prepended towards ISP-A and is instead being permitted by the general rule is a different bug wearing the same fix, and the hit counts are the only place it shows.
  3. The peer accepted it. advertised-routes states what this router sent, not what ISP-A installed. Confirm from the other side - ask the peer, or read the prefix on a public looking glass in a network that does not transit ISP-B.
  4. The AS path is the intended one. Read the path as seen externally and check it against the traffic-engineering policy for this prefix rather than assuming the prepend behaved.
  5. The drain test passes. Drain ISP-B again, in a window, and confirm 203.0.113.0/24 stays reachable from outside. This is the only check that closes the incident, and it is the one that gets skipped because the service is working by then.
  6. Nothing else is single-homed. Reconcile the advertised prefixes on both sessions against the list this AS intends to originate, and treat a difference in either direction as a finding.

Prevention

  • One origination set, four consumers. The network statements, the outbound policy list, the IRR route objects and the RPKI ROAs are four copies of one fact. Generate them from a single source or reconcile them on a schedule. A change that updates three of four is the expected output of a human checklist, not an unusual lapse.
  • Reconcile advertised-routes against intent, per session, on a timer. The missing advertisement produced no alert and would have produced none for years. A scheduled diff in both directions catches the prefix you failed to advertise and the prefix you advertised by accident with the same job.
  • Make “no outbound policy” a monitored state. An empty filter is invisible in every configuration review, because there is nothing on the page to review. The ISP-B session was simultaneously the reason the service worked and the largest leak risk in the estate.
  • Put the drain in the calendar, not in the incident. A redundant path that has never been tested alone has never been tested. This entire incident is a scheduled exercise that nobody had scheduled.
  • Distrust “it works via the other path”. Redundancy does not remove defects; it converts loud ones into latent ones, timed to surface at the exact moment you are relying on it.
  • When a new prefix goes live, prove both paths on day one. The acceptance test for a new public prefix should include reading it from a looking glass behind each upstream separately. That test takes five minutes and would have caught this three weeks ago.