Standard routing is simple: packets go to the destination via the best route in the table. But what if you need specific traffic to take a different path? Work traffic through the VPN, streaming through the ISP, certain devices always through a specific gateway?
That’s Policy-Based Routing (PBR). VyOS implements PBR through policy routes and routing tables. It sounds complex, but the pattern is simple: match the traffic, then route it to a specific table.
The PBR Mental Model
Two components work together:
- Routing table: Alternative routes for specific traffic
- Policy route: Match traffic and direct to the appropriate table
Packet arrives → Policy route matches → Routed via alternate tableVyOS 1.4 policy route can match traffic directly (by source, destination, protocol, etc.) and route it to a specific table — no firewall marks needed for most cases.
Scenario 1: Route Specific Subnet Through VPN
Let’s say you have:
- Default internet via eth0 (ISP)
- WireGuard VPN on wg0
- Want 10.0.0.0/24 (work devices) to use VPN
configure
# Create a separate routing table for VPN trafficset protocols static table 10 route 0.0.0.0/0 interface wg0
# Policy route: match source and set tableset policy route PBR rule 10 source address '10.0.0.0/24'set policy route PBR rule 10 set table '10'
# Apply policy to LAN interfaceset policy route PBR interface 'eth1'
commitValidation:
# Check routing table 10 existsshow ip route table 10
# From a work device, check public IP# Should show VPN exit IP, not ISP IPcurl ifconfig.meScenario 2: Route by Destination (Specific Sites Through VPN)
Route only certain destinations through VPN, everything else direct.
configure
# Routing table for VPNset protocols static table 20 route 0.0.0.0/0 interface wg0
# Define destinations (IP ranges of services you want through VPN)set firewall group network-group VPN-DESTINATIONS network '203.0.113.0/24'set firewall group network-group VPN-DESTINATIONS network '198.51.100.0/24'
# Policy route: match destination and set tableset policy route PBR-DEST rule 10 destination group network-group 'VPN-DESTINATIONS'set policy route PBR-DEST rule 10 set table '20'set policy route PBR-DEST interface 'eth1'
commitScenario 3: Route by Domain (Using DNS-Based Groups)
VyOS 1.4+ supports domain groups. Traffic to specific domains can be routed differently.
configure
# Create domain groupset firewall group domain-group STREAMING domain 'netflix.com'set firewall group domain-group STREAMING domain 'nflxvideo.net'set firewall group domain-group STREAMING domain 'hulu.com'
# Route streaming through ISP (not VPN) even if VPN is defaultset protocols static table 30 route 0.0.0.0/0 next-hop 192.168.1.1
# Policy route: match domain group and set tableset policy route PBR-DOMAIN rule 10 destination group domain-group 'STREAMING'set policy route PBR-DOMAIN rule 10 set table '30'set policy route PBR-DOMAIN interface 'eth1'
commitImportant: Domain groups rely on DNS resolution. VyOS maintains a cache of IP addresses for the domains. This isn’t perfect — CDNs change IPs, some services use many domains. But for common use cases, it works well.
Scenario 4: Combined Rules (Source + Destination)
Real-world often needs combinations: “Work devices accessing work servers go through VPN, but their general browsing goes direct.”
configure
# Groupsset firewall group network-group WORK-DEVICES network '10.0.0.0/24'set firewall group network-group WORK-SERVERS network '10.100.0.0/16'
# Table for VPNset protocols static table 40 route 0.0.0.0/0 interface wg0
# Policy route: match source AND destination, set tableset policy route PBR-WORK rule 10 source group network-group 'WORK-DEVICES'set policy route PBR-WORK rule 10 destination group network-group 'WORK-SERVERS'set policy route PBR-WORK rule 10 set table '40'
# Everything else from work devices goes direct (no matching rule = main table)
set policy route PBR-WORK interface 'eth1'
commitDebugging PBR
When PBR doesn’t work as expected, debug systematically:
1. Verify Policy Route is Matching
# Check policy route statisticsshow policy route statisticsIf rules show zero packets, the match criteria isn’t hitting. Check source/destination groups.
2. Verify the Routing Table Exists
show ip route table 10Should show the route (e.g., default via wg0). If empty, the table wasn’t configured correctly.
3. Verify Policy Route is Applied
show policy routeConfirms which interfaces have policy routing and what rules exist.
4. Trace a Specific Packet
# From VyOS, simulate routing decisionip route get 8.8.8.8 mark 10Shows which route would be used for marked traffic.
5. Check Actual Traffic Flow
# Monitor traffic on interfacessudo tcpdump -i wg0 -n host 8.8.8.8If traffic appears on the expected interface, PBR is working.
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Traffic not matching | Source/dest mismatch | Verify group contents, check rule order |
| Matched but wrong route | Table number mismatch | Ensure table exists with correct routes |
| Works then fails | Gateway down in alternate table | Add gateway monitoring |
| DNS traffic bypasses PBR | DNS resolves before routing | Use domain groups or DNS on VPN |
Best Practices
Use meaningful table numbers: 10 for VPN, 20 for backup ISP, etc. Document what each table is for.
Keep firewall groups organized:
set firewall group network-group VPN-CLIENTS description 'Devices that always use VPN'set firewall group network-group BYPASS-VPN description 'Devices that never use VPN'Test each component separately: First verify the routing table works (manually add a route and test), then verify policy rules are matching, then check traffic flows correctly.
Have a fallback: If VPN goes down, marked traffic will blackhole. Consider adding:
# Fallback route in VPN tableset protocols static table 10 route 0.0.0.0/0 next-hop 192.168.1.1 distance 10Lower distance = preferred. If wg0 route (default distance 1) fails, traffic falls back to ISP.
Complete Example: Split-Tunnel VPN
Here’s a realistic full configuration. Certain devices always use VPN, streaming services bypass VPN, everything else goes direct.
# === Routing Tables ===set protocols static table 10 route 0.0.0.0/0 interface wg0
# === Firewall Groups ===set firewall group network-group VPN-CLIENTS network '10.0.0.50/32'set firewall group network-group VPN-CLIENTS network '10.0.0.51/32'set firewall group domain-group STREAMING domain 'netflix.com'set firewall group domain-group STREAMING domain 'nflxvideo.net'
# === Policy Route Rules ===# Rule order matters - exceptions first!
# Streaming from VPN clients goes direct (not through VPN)set policy route PBR rule 5 source group network-group 'VPN-CLIENTS'set policy route PBR rule 5 destination group domain-group 'STREAMING'# No table set = uses main routing table
# VPN clients use VPN for everything elseset policy route PBR rule 10 source group network-group 'VPN-CLIENTS'set policy route PBR rule 10 set table '10'
# === Apply ===set policy route PBR interface 'eth1'Rule order matters: rule 5 (streaming exception) is checked before rule 10 (VPN routing). Streaming traffic matches rule 5 with no table override, uses default routing. Everything else from VPN clients matches rule 10, uses VPN table.
The Lesson
PBR isn’t magic incantations. It’s two clear steps:
- Define where traffic should go (routing tables)
- Define what traffic to affect (policy route rules with matching criteria)
When debugging, check each step independently. Are rules matching traffic? Does the table have the right routes? Is the policy applied to the correct interface?
Clear criteria + systematic debugging = PBR that works reliably.