VyOSXIII · Policy-Based RoutingPolicy-based routing
PBR rules — the policy route tree, its interface, ip rule, and the table it points at
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
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:
- nftables rules in a mangle table, which match the packets your policy describes and mark them.
- an
ip ruleentry, 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 aninterfaceline for the policy.ip rule showshows a selector for the policy’s table, above themainrule.ip route show table 100returns at least one route.- The mangle counters for the policy’s rules are non-zero after representative traffic.
ip route getfrom a matching source, withiifset 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
interfaceline. The rules commit,show configurationlooks complete, and nothing is in the path. On 1.4 and 1.5 the attachment isset policy route NAME interface ethX; a runbook that still saysset interfaces ethernet ethX policy route NAMEis 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. Withoutset protocols static table 100 route ...the lookup falls through tomainand 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-routeis the tree for that case. Test from a host behind the router, or withip 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 mainat priority 32766 matches every packet. Anything installed with a numerically higher priority is unreachable. Read the priorities inip 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
Q1. On VyOS 1.5, which line puts a policy route named PBR-VOICE into the forwarding path for traffic arriving on eth1?
Q2. `set policy route PBR-VOICE rule 10 set table '100'` creates routing table 100 and populates it from the rule's match criteria.
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.
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.