Skip to main content
RunBook Academy

VyOSXIII · Policy-Based RoutingPolicy-based routing

PBR rules — the policy route tree, its interface, ip rule, and the table it points at

Advanced⏱ ~20 minset policy routeset policy local-routeset protocols static tableshow configuration commandsip rule showip route show tableip route getnft list ruleset

What you'll learn

  • Build a policy route on VyOS 1.5 and attach it to an interface with the 1.4+ attachment
  • Populate the routing table a policy route selects, and recognise the empty-table failure
  • Read the three views — configuration, nftables render, kernel rule — in the right order
  • Distinguish a policy route from a route-map, and know which one acts on packets
  • Recognise the failure modes that come from a missing attachment, a wrong interface, or locally-originated traffic

Prerequisites

Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling) · 2026-08-19

Not yet marked complete on this device.

PBR rules — the policy route tree, its interface, ip rule, and the table it points at

A policy route with nothing attached to it is configuration with no effect. It commits, it appears in show configuration, and not one packet is diverted by it. That gap between “configured” and “in the forwarding path” is what this lesson is about: what the attachment actually is on VyOS 1.5, what the router builds underneath when you commit it, and how to read the three places the state lives when the diversion is not happening.

What VyOS actually builds

A policy route on VyOS 1.5 has three parts, and all three must be present before a packet changes course:

flowchart TB
  A["set policy route PBR-VOICE rule 10<br/>match the packets"] --> B["set table 100<br/>name the table to consult"]
  B --> C["set policy route PBR-VOICE interface eth1<br/>put the policy in the path"]
  C --> D["set protocols static table 100 route ...<br/>put a route IN table 100"]
  D --> E["Packet from 10.10.0.0/24 on eth1<br/>leaves via the voice provider"]

Miss the third and the rules exist but nothing is evaluated. Miss the fourth and the rules are evaluated, the table is consulted, the table is empty, and the packet falls through to main as though the policy were not there. Both failures are silent. Both look like a working configuration in a review.

The configuration

configure

# 1. Match the packets and name the table they should use.
set policy route PBR-VOICE rule 10 description 'voice subnet out the voice provider'
set policy route PBR-VOICE rule 10 source address '10.10.0.0/24'
set policy route PBR-VOICE rule 10 set table '100'

# 2. Attach the policy to the interface the traffic arrives on.
set policy route PBR-VOICE interface 'eth1'

# 3. Give table 100 something to say.
set protocols static table 100 route 0.0.0.0/0 next-hop '198.51.100.1'

commit
save

Read step 2 carefully, because it is the line that moved. On VyOS 1.3 the attachment lived on the interface — set interfaces ethernet eth1 policy route PBR-VOICE. From 1.4 onward the interface is a property of the policy, and the node under interfaces is gone. A 1.3-era runbook fails here, and it fails loudly: the path is rejected, so at least you find out at commit rather than in production.

Step 3 is the one nobody notices is missing. set table '100' does not create table 100 and does not populate it. It says “consult table 100”, and consulting an empty table returns nothing.

Direction: there is no egress policy route, and that is on purpose

The attachment names an interface, and the match is on arrival. A policy route diverts traffic that comes in through the named interface. There is no output-direction variant of set policy route, and looking for one is the wrong shape of question — by the time a packet is leaving through an interface, the routing decision that chose that interface has already been made. Selecting a table after the fact would have nothing left to decide.

That leaves one real gap, and it is the gap people trip over: traffic the router originates itself never arrives on any interface, so no policy route will ever see it. A ping from the router, a BGP session the router opens, a DNS query from the router’s own resolver, an NTP poll — none of these are diverted by a policy route, no matter which interface it is attached to.

VyOS has a separate tree for exactly this case:

set policy local-route rule 10 set table '100'

policy local-route selects a table for traffic the router itself originates, matched by source address rather than by arrival interface — because there is no arrival interface to match on:

set policy local-route rule 100 source address '203.0.113.254'
set policy local-route rule 100 set table '100'

That pins traffic the router sends from 203.0.113.254 to table 100. It is the mechanism behind a multi-homed router replying out of the provider a request came in on, and behind pinning one VPN tunnel to one uplink. policy local-route6 is its IPv6 counterpart.

The render — what the router builds when you commit

This is where an operator trained on other vendors goes looking in the wrong place, so it is worth being exact.

A policy route is not rendered into FRR. FRR does ship a policy-routing daemon of its own — pbrd, configured with pbr-map — and VyOS does not expose it. There is no set protocols pbr in the VyOS tree. So vtysh -c 'show running-config' shows nothing about your policy route, and neither does any show ip ... command that reaches into FRR. An operator who concludes from that silence that the PBR is broken has just spent an hour proving something that was never going to be there.

What VyOS actually builds is two things, both below FRR:

  1. nftables rules in a mangle table, which match the packets your policy describes and mark them.
  2. an ip rule entry, which sends marked packets to the table you named.

The kernel’s rule list is then walked in priority order for every packet, and the first matching rule decides which routing table the lookup uses.

Reading the three views, in order

When traffic is not being diverted, read these three in this order. Each one answers a question the previous one cannot.

1. The configuration — is the policy attached at all?

show configuration commands | match 'policy route'
set policy route PBR-VOICE interface 'eth1'
set policy route PBR-VOICE rule 10 description 'voice subnet out the voice provider'
set policy route PBR-VOICE rule 10 set table '100'
set policy route PBR-VOICE rule 10 source address '10.10.0.0/24'

The interface line is the one to look for first. Rules without it are the commonest cause of “the PBR does nothing”, and they are invisible in a review that only reads the rules.

2. The kernel rule — did the router install a selector?

ip rule show
0:      from all lookup local
32765:  from all fwmark 0x7fffff9b lookup 100
32766:  from all lookup main
32767:  from all lookup default

The middle line is the policy. The mark follows the documented derivation — 0x7FFFFFFF - 100 is 0x7FFFFF9B — and the priority is assigned by VyOS, so check the shape rather than memorising the numbers. What matters is that a rule selecting table 100 exists and sits above the main rule at 32766. A selector below main is never reached, because the first match wins and from all lookup main matches everything.

3. The table — is there anything in it?

ip route show table 100
default via 198.51.100.1 dev eth2

An empty response here is the diagnosis. The rule fired, the table was consulted, nothing was found, and the lookup continued to main — which is why the traffic went out the default route and why every other view looked healthy.

Finally, prove the whole path end to end rather than inferring it:

SRC=10.10.0.5
DST=203.0.113.20
ip route get "$DST" from "$SRC" iif eth1

ip route get performs a real lookup with the selectors you give it. If it returns the voice provider’s next-hop, the policy works. If it returns the main-table next-hop, it does not — and you now know that before any user tells you.

What a route-map is for, so the distinction stays clean

The previous lesson built route-maps, and they remain the right tool for their own job. The difference is what they are attached to:

set policy prefix-list PL-VOICE rule 10 action 'permit'
set policy prefix-list PL-VOICE rule 10 prefix '10.10.0.0/24'

set policy route-map RM-VOICE-OUT rule 10 action 'permit'
set policy route-map RM-VOICE-OUT rule 10 match ip address prefix-list 'PL-VOICE'
set policy route-map RM-VOICE-OUT rule 10 set ip-next-hop '198.51.100.1'

set protocols bgp neighbor 198.51.100.2 address-family ipv4-unicast route-map export 'RM-VOICE-OUT'

Two details in there correct things that are commonly mistyped. The set clause is set ip-next-hop, one hyphenated node, not set ip next-hop. And the match is match ip address prefix-list, which matches the route’s own prefix — the 10.10.0.0/24 here is a prefix being advertised, not a source subnet.

Attached to a BGP neighbour, that route-map rewrites the next-hop attribute of the matching route as it is announced. It changes what the neighbour is told. It does not change where a single packet on this router goes.

Validation

A policy route is working when all five of these hold:

  • show configuration commands | match 'policy route' shows an interface line for the policy.
  • ip rule show shows a selector for the policy’s table, above the main rule.
  • ip route show table 100 returns at least one route.
  • The mangle counters for the policy’s rules are non-zero after representative traffic.
  • ip route get from a matching source, with iif set to the attached interface, returns the policy’s next-hop.

Check them in that order. Each failing check tells you which of the five to stop reading at.

How it fails

  • No interface line. The rules commit, show configuration looks complete, and nothing is in the path. On 1.4 and 1.5 the attachment is set policy route NAME interface ethX; a runbook that still says set interfaces ethernet ethX policy route NAME is 1.3 and will be rejected outright.
  • Attached to the wrong interface. The selector is installed and never matches, because the traffic arrives somewhere else. The nftables counter for the rule stays at zero — that counter is how you tell this apart from a match that fires and then finds nothing.
  • Empty table. set table '100' selects a table; it does not create routes in one. Without set protocols static table 100 route ... the lookup falls through to main and the policy is a no-op. This is the most common of all of them.
  • Locally-originated traffic. The operator tests the policy by pinging from the router itself, sees the packet take the default route, and concludes the policy is broken. It is not — router-originated traffic never arrives on an interface, so no policy route applies to it. policy local-route is the tree for that case. Test from a host behind the router, or with ip route get ... iif eth1.
  • A route-map where a policy route belongs. The route-map commits, because a route-map is valid configuration; it just has nothing to do with forwarding. The symptom is a configuration that reviews perfectly and diverts nothing, with no error anywhere.
  • A selector below main. from all lookup main at priority 32766 matches every packet. Anything installed with a numerically higher priority is unreachable. Read the priorities in ip rule show, not just the presence of the line.

Rollback

Policy routing changes the path traffic takes, which means a mistake can move the path your own session is riding on. Treat it accordingly.

configure

# Take the policy out of the forwarding path without deleting the work.
# The rules survive; nothing is evaluated.
delete policy route PBR-VOICE interface

# Or remove the policy entirely.
delete policy route PBR-VOICE

compare
commit
save

Deleting the interface node is the useful one during an incident: it disables the diversion in a single line and leaves the rules intact for inspection afterwards, which a full delete does not.

For a change applied over the path it might break, commit-confirm 5 applies the candidate and reverts it automatically unless a confirm follows within five minutes:

commit-confirm 5
# ... verify from a host behind the router ...
confirm

Do not reach for rollback N here. The VyOS documentation states that it applies the stored revision by rebooting the router, which turns a misrouted subnet into an outage for everything else the box carries. Where you want a whole-configuration revert, load a file from /config/archive/, run compare, and commit that.

Production discipline

Cross-course references

The Linux course’s XIX-Linux-NetFoundations covers the kernel FIB and the rule selector these commands drive, and its firewall part covers the mark that connects the two halves. The VyOS lessons vyos-xiii-01-pbr-concept and vyos-xiii-02-route-maps cover the foundation this lesson assumes. vyos-xii-01-static-routes and vyos-xii-03-static-route-options cover the static route primitives that populate a PBR table. vyos-xiii-05-pbr-troubleshoot covers the operational evidence when a policy route does not fire, and vyos-xxxix-04-wan-policy-routing builds a full multi-WAN design on the same three-part model, including the set table 'main' exception rule that keeps internal traffic off the WAN defaults.

Quiz

Knowledge check · 4 questions

  1. Q1. On VyOS 1.5, which line puts a policy route named PBR-VOICE into the forwarding path for traffic arriving on eth1?

  2. Q2. `set policy route PBR-VOICE rule 10 set table '100'` creates routing table 100 and populates it from the rule's match criteria.

  3. Q3. A policy route is configured and its rules look correct, but no traffic is diverted. `vtysh -c 'show running-config'` contains nothing about it. Where should the operator look, and what is the likely finding?

    R1 runs VyOS 1.5. `set policy route PBR-VOICE rule 10 source address 10.10.0.0/24` and `set policy route PBR-VOICE rule 10 set table 100` are both present. Voice traffic from 10.10.0.0/24 still leaves by the default route. The operator has searched the FRR running configuration for the policy and found nothing, and is treating that as the failure.

  4. Q4. A policy route is attached and `ip rule show` contains a selector for table 100, but `ip route get` from a matching source still returns the main-table next-hop. What is the failure, and how does the operator confirm it in one command?

    R1 has `set policy route PBR-VOICE interface eth1` and a rule matching 10.10.0.0/24 with `set table 100`. `ip rule show` shows a selector above the main rule that looks up table 100. Traffic from 10.10.0.5 nonetheless leaves by the default route in the main table. The operator has confirmed the attachment and the selector and is out of ideas.

Passing score: 75%. Answers are checked in this browser.

Why this one needs state, not configuration review

Policy-based routing is the part of the configuration where “it commits” and “it works” diverge the most sharply, because every layer of it is independently valid on its own. Rules with no interface are valid. An interface attachment pointing at a table with no routes is valid. A route-map that has nothing to do with forwarding is valid. None of them produce an error, and all of them produce a router that reviews clean and forwards exactly as it did before.

The discipline that survives contact with production is to stop reading the configuration as evidence. The configuration is the intent. ip rule show, ip route show table, the mangle counters and ip route get are the state, and only the state can tell you whether the intent arrived.