Skip to main content
RunBook Academy

← All runbooks in VyOS

medium riskservice affecting~45 min

Runbook: Investigate a Policy Route

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · The complaint is reduced to a tuple before any command is run: a source address, a destination, the interface the traffic enters on, and the path it is supposed to take. "PBR is broken" is not an investigation; "10.10.0.5 to 8.8.8.8 arriving on eth1 should leave via eth2 and is leaving via eth0" is.
  • · The intent of the policy is written down from the configuration or the change record, not inferred from its name. A route-map called RM-VOICE-OUT tells you nothing about which prefixes it matches or which next-hop it sets.
  • · The test source address is confirmed to be inside the matched prefix. Testing from the router itself, or from a management address, is the single most common way this investigation produces a false negative — router-originated traffic does not arrive on the bound interface and the iif match never fires.
  • · Read-only baseline captured to a file first: show configuration commands | match policy, vtysh -c "show running-config", ip rule show, ip route show table all. Every diagnostic that changes anything destroys part of this.
  • · vtysh -c "show route-map RM-NAME" counters read and recorded BEFORE anything is cleared. A counter that has been zero since boot is the strongest single piece of evidence in this runbook, and clear route-map counters erases it permanently.
  • · Recent change checked with show system commit. A policy route that worked yesterday and does not today is a change, and the commit log names it faster than any packet capture.
  • · Out-of-band console confirmed if the interface under investigation also carries your session. A PBR rule bound to the interface you are logged in over can strand you on the next commit.
  • · The table IDs in use on this router established from ip route show table all, and checked against the reserved IDs (0 local, 253 default, 254 main, 255 local). A PBR table that collides with a VRF table ID is a different fault from a PBR table that is simply empty.
  • · The owner of the far side identified — the upstream that owns the next-hop, and whoever owns the stateful firewall on the return path. Roughly half of PBR investigations end as a handover rather than a local change.

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Capture the baseline to a file before anything else: show configuration commands | match policy, show policy route-map, vtysh -c "show running-config", ip rule show, ip route show table all, and the route-map counters. Everything after this step degrades the evidence.
  2. 2State the expected path explicitly in the ticket — source prefix, ingress interface, table ID, next-hop, egress interface — before running a single diagnostic. Half of these investigations end when the operator discovers the expected path was never what the configuration says.
  3. 3Rung 1, configuration: show policy route-map and show configuration commands | match policy. Confirm the route-map exists, that at least one rule has action permit with a set ip next-hop, that the prefix-list contains the prefix you are testing from, and that the interface binding line is present and names the interface the traffic actually arrives on.
  4. 4Rung 2, FRR render: vtysh -c "show running-config" filtered for the route-map, for ip rule, and for table. The commit engine turns the VyOS tree into FRR configuration; if the rule and the table route are absent here, the configuration never became policy and no amount of packet capture will show why.
  5. 5Rung 3, kernel installation: ip rule show. The PBR rule must appear at a priority numerically below 32766, with the expected source prefix and iif. If the FRR render has it and the kernel does not, the fault is netlink — check systemctl status frr, vtysh -c "show zebra client summary" and journalctl -u frr.
  6. 6Rung 3b, the table: ip route show table <id> for the table the rule points at. An installed rule pointing at an empty table is a silent no-op — the kernel consults the table, finds nothing, and falls through to main. This looks identical from the outside to "the rule was never installed" and has a different fix.
  7. 7Rung 4, route resolution: ip route get <dst> from <src> and, where the ingress interface matters, ip route get <dst> from <src> iif <ifname>. This performs the full rule-database walk for those selectors and names the table, next-hop and egress interface that the kernel would actually use. It is the most precise local check available and it costs nothing.
  8. 8Read the route-map counters: vtysh -c "show route-map RM-NAME". A Statistics line at zero means the rule has never matched anything — a match-clause or binding problem — while a counter that climbs while traffic still takes the wrong path means the rule fires and the table it selects is wrong.
  9. 9Rung 5, the packet: capture on the expected egress interface with tcpdump -ni <ifname> "src <address>" while a real host generates traffic from the matching source. Traffic sourced on the router with ping -I exercises the local output path, not the forwarding path, and is a different test — use it knowingly or not at all.
  10. 10If the forward path is correct, test the return path before concluding the policy works. traceroute -s <src> <dst> from a host inside the source prefix, plus a capture on the interface the replies come back on. Asymmetry is the most common PBR outage and it looks like a working forward path.
  11. 11If the rule fires, the table has a route, and packets still vanish, test the next-hop itself: ping <next-hop> and show ip route <next-hop>. A hard-coded next-hop in a route-map does not fail over when the upstream dies — the kernel keeps forwarding to a dead address.
  12. 12Decide whether the fix belongs on this router at all. A policy that is installed, matching, and resolving to a live next-hop, with the packets leaving the expected interface, is a working policy — and the fault is then the return path, the far-end firewall, or the expectation itself.
  13. 13Apply at most one change, with commit-confirm 5 if the interface under investigation carries your session, then confirm and save only after the verification below passes. Do not fix three things at once; you will not know which one worked.
  14. 14Close with the rung that failed, the evidence at that rung, and the fix recorded together. "Rule was installed, table 100 was empty because the route-map rule had action deny" is reusable; "PBR fixed" guarantees the next person repeats the whole ladder.

4 · Verification

Confirm the procedure actually fixed the problem.

  • ip rule show contains the PBR rule at the expected priority, below 32766, with the source prefix and iif that the intent calls for
  • ip route show table <id> for the PBR table contains the expected route, and the next-hop in it answers ping
  • ip route get <dst> from <src> returns the PBR table, the intended next-hop, and the intended egress interface — and ip route get for a source deliberately OUTSIDE the matched prefix returns the main-table path, proving the rule is selective rather than universal
  • vtysh -c "show route-map RM-NAME" shows the Statistics counter climbing while test traffic runs, having been read before any clear
  • tcpdump on the expected egress interface shows the test traffic, and a simultaneous capture on the previously-wrong interface shows none of it
  • The return path is confirmed symmetric: replies arrive on the interface the policy intends, verified by capture rather than assumed from the forward result
  • A real application transaction from a real host inside the source prefix completes end to end — not just an ICMP echo, which many stateful paths treat differently from TCP
  • Traffic from every OTHER source prefix on the same ingress interface is confirmed unaffected, by ip route get from one of those addresses
  • The IPv6 side is checked with ip -6 rule show and ip -6 route show table all if the interface is dual-stack, because policy route-map and policy route-map6 are separate bindings and fixing one does not touch the other

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • The ladder is read-only from rung 1 to rung 5 and there is nothing to roll back. Everything below needs undoing deliberately.
  • vtysh -c "clear route-map counters" has no undo and destroys the cumulative match history. Read the counters and record them before you clear, and clear only when you need a clean window to observe fresh traffic.
  • vtysh -c "debug zebra kernel" writes at volume to the system log. Turn it off explicitly when finished rather than leaving it on — it can fill a small /var/log on a router that is already having a bad day.
  • A wrong interface binding: delete interfaces ethernet <wrong-ifname> policy route-map <name>, then set interfaces ethernet <correct-ifname> policy route-map <name>, then compare, commit, save.
  • A binding added during the investigation and now unwanted: delete interfaces ethernet <ifname> policy route-map <name>, compare, commit, save. Deleting the binding removes the ip rule and the generated table entry with it, which is the whole point — but it also returns every matched source to the main-table path in one step, so confirm that path is healthy first.
  • A next-hop corrected on a route-map rule: set policy route-map <name> rule <n> set ip next-hop <address>, compare, commit, save. Read the compare output; a typo here silently blackholes the whole matched prefix.
  • Whole-tree revert: rollback N, then compare, then commit, then save. rollback loads the archived configuration into the candidate and changes nothing until commit — and it reverts the entire tree, not only the line you were thinking about.
  • Anything committed with commit-confirm reverts itself when the timer expires. If verification will take longer than the window, extend the window rather than confirming early to buy time.
  • Removing a PBR rule is itself a service-affecting change for the traffic class it carried. There is no read-only way to withdraw a policy route; the matched traffic moves to whatever the main table says the moment the rule leaves the kernel.

6 · Escalation

When the runbook isn't enough, contact:

  • · Escalate to the upstream that owns the next-hop when the rule is installed, the table has a route, and the next-hop does not answer. A hard-coded next-hop that has failed is not a routing bug on this box and no local change will restore the path.
  • · Escalate to whoever owns the stateful firewall on the return path when the forward path is correct and the replies are dropped. Asymmetric PBR is a design conflict between two correct configurations, and it is resolved by agreement, not by a knob on this router.
  • · Escalate to network engineering when the router has multiple route-maps bound to the same interface. Which of them wins is a property of the generated rule priorities rather than of operator intent, and re-ordering them is a design change with its own change window.
  • · Escalate to network engineering when the estate has reached PBR-everywhere — more policy rules than the kernel walks cheaply, or a forwarding path that no one can predict from the configuration. That is a routing-design problem wearing an incident costume, and patching one more rule into it makes the next incident worse.
  • · Escalate to the VRF owner when a PBR table ID collides with a VRF table ID. The two namespaces share the same numeric space and the resolution belongs to whoever planned the ID layout.
  • · Escalate to the change owner when the policy is installed and working exactly as configured, and the configuration does not match what anyone expected. That is a requirements failure, and changing the router before the requirement is agreed just moves the argument to a worse place.
  • · Hold rather than force: a policy route that is degraded but carrying traffic, with a named owner and an agreed end time, is a better position at 03:00 than a rushed change to a forwarding policy nobody has fully read.

“The policy route is not working” is one sentence covering four faults that share almost no evidence and no fix:

  • The policy was never installed. The route-map exists in the configuration and no rule exists in the kernel, usually because the interface binding is missing.
  • The policy is installed and never matches. The rule is in the kernel and the traffic does not meet its selectors — wrong source prefix, wrong ingress interface, or a test run from the router itself.
  • The policy matches and selects nowhere. The rule fires, the table it points at is empty, and the kernel quietly falls through to the main table. This is the one that wastes the most time, because from the outside it is indistinguishable from the first fault.
  • The policy works perfectly, and the return path does not follow it. The forward packets take the intended provider, the replies come back another way, and a stateful firewall in the middle drops them.

The ladder below separates them in about ten minutes, and its order is the whole technique: configuration, FRR render, kernel rule, route resolution, packet. Start at the top. Most policy-route failures are configuration typos, and no amount of packet capture reveals a typo.

When to use this runbook

  • Traffic from a particular source is leaving via the wrong interface or the wrong provider.
  • A policy route was configured, committed without error, and appears to have had no effect at all.
  • A policy route worked and stopped, with or without a recent commit.
  • Connections from one subnet establish and then stall, while the same destination is fine from every other subnet.

When not to use it

  • A destination that is unreachable from everywhere. That is a missing route, and it is a destination-based problem with a different walk. Policy routing changes which table answers, not whether an answer exists.
  • Traffic that is being dropped rather than misrouted. If the packets never leave on any interface, look at the firewall before the policy. A policy route selects a path; it does not permit or deny.
  • A NAT problem. Traffic leaving the right interface with the wrong source address is translation, not routing.
  • Multi-WAN failover that did not fail over. If the question is “why did the backup not take over”, start there — although a hard-coded next-hop inside a policy route is a common reason a failover design does not work, and Step 7 covers it.

Blast radius

Rungs 1 to 5 are read-only. Nothing on the ladder changes forwarding until you commit.

Two diagnostics are not free. clear route-map counters destroys the cumulative match history — which is often the single most informative number available, because a counter that has been zero since the last reboot proves the rule has never matched anything. And debug zebra kernel writes netlink traffic to the system log at volume.

Removing or repairing a policy route is service-affecting for the traffic class it carries, in both directions. The moment the rule leaves the kernel, every source it matched reverts to whatever the main table says — which may be correct, may be a different provider, or may be nothing.

Inputs

  • The source address or prefix, the destination, and the ingress interface.
  • The path the traffic is supposed to take, from the change record or the design, in the form “table, next-hop, egress interface”.
  • What was committed on this router recently: show system commit.
  • Whether the interface under investigation carries your own session.
  • Whether the estate uses VRFs, and which table IDs they occupy.

Step 1: The RPDB, and why order is everything

Before the ladder, know what you are reading. Linux resolves every packet through the routing policy database — an ordered list of rules, walked from the lowest priority number upward, first match wins.

Read-only / Safea router with no policy routing at all
vyos@vyos:~$ ip rule show
0:      from all lookup local
32766:  from all lookup main
32767:  from all lookup default

Illustrative output

That is the baseline every VyOS router starts from. A policy route works by inserting a rule above 32766, so that matching packets consult a different table before the main table is ever reached. A rule inserted below 32766 will never be reached for anything the main table can answer, which is nearly everything.

So the first thing to look at in ip rule show is not whether your rule is there. It is where it sits relative to 32766, and what sits above it.

Step 2: Rung 1 — the configuration

Read-only / Safewhat the operator asked for
show policy route-map
show configuration commands | match policy
show system commit

Three things must all be true and any one of them can be missing:

  1. A rule with action permit and a set ip next-hop clause. A route-map made only of action deny rules produces no next-hop at all — matching traffic is dropped rather than steered, which is a different outcome from the one the name of the route-map suggests.
  2. A prefix-list that actually contains the address you are testing from. Read the prefix, not the prefix-list’s name.
  3. An interface binding line. set interfaces ethernet <ifname> policy route-map <name> is what turns a definition into a rule; without it, the route-map is a paragraph of configuration with no effect anywhere.

Step 3: Rung 2 — the FRR render

VyOS configuration is not what the router runs. The commit engine renders it into FRR configuration, and FRR pushes that into the kernel. Two translations, two places to lose a policy.

Read-only / Safewhat FRR was actually told
vtysh -c 'show running-config'
vtysh -c 'show route-map RM-VOICE-OUT'

A rendered policy route appears as two things: a rule line that names a source, an ingress interface and a table to look up, and a route line that puts something in that table. Both must be present. A rule with no route behind it is Step 5’s fault, and the render is where you first see it coming.

show route-map also carries the per-rule counters. Read them now, while they still hold the whole history:

Read-only / Safethe most informative zero in this runbook
vyos@vyos:~$ vtysh -c 'show route-map RM-VOICE-OUT'
route-map RM-VOICE-OUT, permit, sequence 10
Match clauses:
  ip source-address prefix-list PL-VOICE-SRC
Set clauses:
  ip next-hop 198.51.100.1
Statistics:
  Matching: 0 packets, 0 bytes

Illustrative output

Zero matches since boot means the rule has never fired. That eliminates everything below it on the ladder in one line and sends you back to the selectors. A counter that is climbing while traffic still takes the wrong path means the opposite: the match is fine and the table it selects is wrong.

Step 4: Rung 3 — the kernel

Read-only / Safewhat the kernel installed
ip rule show
ip route show table all
ip -6 rule show

If FRR has the rule and the kernel does not, the failure is in the netlink handoff, and the router will usually say so:

Read-only / Safewhen the rule did not reach the kernel
systemctl status frr
vtysh -c 'show zebra client summary'
journalctl -u frr --since '30 min ago'

Include ip -6 rule show even when the complaint is about IPv4. The IPv4 and IPv6 policy bindings are separate — policy route-map and policy route-map6 — and a dual-stack subnet whose IPv4 is steered and whose IPv6 is not produces a symptom that looks intermittent and is not: it depends entirely on which address family the application picked.

Step 5: Rung 3b — the table the rule points at

Read-only / Safean installed rule is not a working policy
ip route show table 100
ip route show table all

While you are here, check the ID itself against the reserved values: 0 is local, 253 is default, 254 is main, 255 is local again on some kernels. A policy table sharing an ID with a VRF is a namespace collision rather than a routing fault, and it belongs to whoever planned the layout.

Step 6: Rung 4 — ask the kernel directly

This is the highest-value command in the runbook and it is completely free. ip route get performs the full rule walk for the selectors you give it and tells you the table, the next-hop and the egress interface the kernel would use.

Read-only / Safethe answer, without sending a packet
vyos@vyos:~$ ip route get 8.8.8.8 from 10.10.0.5
8.8.8.8 from 10.10.0.5 via 198.51.100.1 dev eth2 table 100

Illustrative output

Add iif <ifname> when the rule matches on ingress interface, because without it the kernel resolves as though the packet were locally generated — and a rule with an iif selector will not match locally generated traffic. This is the same reason ping -I from the router is a misleading test.

Then run it twice more, deliberately:

  • From an address inside the matched prefix. It must return the policy table.
  • From an address outside it. It must return the main-table path.

The second test is the one people skip, and it is what distinguishes a selective policy from a rule that is quietly steering more traffic than anyone intended.

Step 7: Rung 5 — the packet, and the next-hop

Read-only / Safewatch both candidate exits at once
tcpdump -ni eth2 -c 20 'src 10.10.0.5'
tcpdump -ni eth0 -c 20 'src 10.10.0.5'

Run these in two sessions while a real host inside the source prefix generates traffic. Capturing on only the expected interface tells you the traffic is not there; capturing on both tells you where it went, which is a much shorter path to the cause.

If the packets are leaving correctly and still not arriving, test the next-hop as a thing in its own right:

Read-only / Safeis the next-hop actually alive
ping -c 3 198.51.100.1
show ip route 198.51.100.1
show interfaces ethernet eth2

Step 8: The return path

Read-only / Safefrom a host in the source prefix, not from the router
traceroute -s 10.10.0.5 8.8.8.8
tracepath -n -s 10.10.0.5 8.8.8.8

If the forward path is correct and the traffic still fails, the policy is probably working and the return path is not following it. Policy routing steers one direction; nothing about it obliges the replies to come back the same way.

Step 9: Source, or spectator

State the conclusion explicitly in the ticket, using this shape:

EvidenceWhat it meansWhere it goes
No rule in ip rule showNever installedFix the binding here
Rule present, counters zeroNever matchedFix the selectors here
Rule present, table emptyMatched, selects nowhereFix the route-map action or the table here
Correct table, next-hop deadPath pinned to a failed upstreamUpstream owner, plus a design fix
Correct egress, replies droppedAsymmetryFirewall owner, jointly
Everything correct, wrong expectationRequirements mismatchChange owner, before any config edit

The last row is not a joke. A policy route that does precisely what its configuration says, while doing something nobody wanted, is common — and editing the router before the requirement is agreed converts a disagreement into an outage.

Rollback

What you didHow to undo it
Rungs 1 to 5Nothing to undo; all read-only
clear route-map countersNo undo. The history is gone; record it first.
debug zebra kernelTurn it off explicitly; do not leave it logging
Changed the interface bindingdelete the wrong binding, set the right one, compare, commit, save
Corrected a next-hopset policy route-map <name> rule <n> set ip next-hop <address>, compare, commit, save
Anything more involvedrollback N, compare, commit, save — reverts the whole tree
Anything under commit-confirmLet the timer fire, or extend it; do not confirm to buy time

rollback N loads an archived configuration into the candidate and changes nothing until commit. Read compare before committing: it reverts every difference between now and then, including changes made by someone else in the same window.

Common patterns

ObservationLikely causeNext step
Route-map shown by show policy route-map, absent from ip rule showNo interface bindingAdd the binding on the ingress interface
Rule present, counters zero, traffic on the right interfaceSource not in the prefix-list, or bound to the wrong interfaceRe-read the prefix and the binding, not the packets
Rule present, counters climbing, path unchangedTable selected by the rule is emptyip route show table <id>; check for a deny-only route-map
Works from a host, not from the routerThe rule matches on iif; router-originated traffic has noneTest from a real host, or add iif to ip route get
IPv4 steered, IPv6 notpolicy route-map6 binding missingCheck ip -6 rule show; the two families are separate bindings
Worked for months, then one class blackholedHard-coded next-hop failed, no trackingPing the next-hop; escalate the design
Connection establishes, then stallsAsymmetric return path meets a stateful firewallCapture the return path; escalate jointly
Two route-maps on one interface, behaviour variesGenerated rule priorities decide, not intentConsolidate into one route-map; network engineering
Policy table ID also used by a VRFTable ID namespace collisionVRF owner; this is a layout decision

Escalation

Escalate when:

  • The next-hop is dead. That is the upstream’s problem and a local design fix, not an incident fix.
  • The forward path is right and the replies are dropped. Two teams own that outcome.
  • More than one policy is bound to the same interface, or the estate has more policy rules than anyone can trace by hand.
  • A policy table ID collides with a VRF table ID.
  • The configuration is doing exactly what it says and nobody wanted it to.

References

  1. VyOS — Policy route-map
  2. FRRouting — route-map
  3. FRRouting — zebra
  4. ip-rule(8)
  5. rt_tables(5)
  6. RFC 3704 — Ingress Filtering for Multihomed Networks