Policy-Based Routing on VyOS: Practical Patterns for Split Routing

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:

  1. Routing table: Alternative routes for specific traffic
  2. Policy route: Match traffic and direct to the appropriate table
Packet arrives → Policy route matches → Routed via alternate table

VyOS 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
Terminal window
configure
# Create a separate routing table for VPN traffic
set protocols static table 10 route 0.0.0.0/0 interface wg0
# Policy route: match source and set table
set 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 interface
set policy route PBR interface 'eth1'
commit

Validation:

Terminal window
# Check routing table 10 exists
show ip route table 10
# From a work device, check public IP
# Should show VPN exit IP, not ISP IP
curl ifconfig.me

Scenario 2: Route by Destination (Specific Sites Through VPN)

Route only certain destinations through VPN, everything else direct.

Terminal window
configure
# Routing table for VPN
set 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 table
set 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'
commit

Scenario 3: Route by Domain (Using DNS-Based Groups)

VyOS 1.4+ supports domain groups. Traffic to specific domains can be routed differently.

Terminal window
configure
# Create domain group
set 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 default
set protocols static table 30 route 0.0.0.0/0 next-hop 192.168.1.1
# Policy route: match domain group and set table
set 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'
commit

Important: 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.”

Terminal window
configure
# Groups
set 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 VPN
set protocols static table 40 route 0.0.0.0/0 interface wg0
# Policy route: match source AND destination, set table
set 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'
commit

Debugging PBR

When PBR doesn’t work as expected, debug systematically:

1. Verify Policy Route is Matching

Terminal window
# Check policy route statistics
show policy route statistics

If rules show zero packets, the match criteria isn’t hitting. Check source/destination groups.

2. Verify the Routing Table Exists

Terminal window
show ip route table 10

Should show the route (e.g., default via wg0). If empty, the table wasn’t configured correctly.

3. Verify Policy Route is Applied

Terminal window
show policy route

Confirms which interfaces have policy routing and what rules exist.

4. Trace a Specific Packet

Terminal window
# From VyOS, simulate routing decision
ip route get 8.8.8.8 mark 10

Shows which route would be used for marked traffic.

5. Check Actual Traffic Flow

Terminal window
# Monitor traffic on interfaces
sudo tcpdump -i wg0 -n host 8.8.8.8

If traffic appears on the expected interface, PBR is working.

Common Issues

ProblemCauseFix
Traffic not matchingSource/dest mismatchVerify group contents, check rule order
Matched but wrong routeTable number mismatchEnsure table exists with correct routes
Works then failsGateway down in alternate tableAdd gateway monitoring
DNS traffic bypasses PBRDNS resolves before routingUse 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:

Terminal window
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:

Terminal window
# Fallback route in VPN table
set protocols static table 10 route 0.0.0.0/0 next-hop 192.168.1.1 distance 10

Lower 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.

Terminal window
# === 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 else
set 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:

  1. Define where traffic should go (routing tables)
  2. 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.