Policy-Based Routing on Cisco IOS-XE: Steering Traffic by Source
You have two internet links. The expensive one is fast and carries the business traffic, the cheap one sits mostly idle as a backup, and someone in finance has just asked why the guest Wi-Fi is eating bandwidth on the link that costs the most. The routing table cannot help you here, because it only looks at the destination and every internet destination is reached through the same default route. What you want is a decision based on who is sending the traffic, not where it is going. That is what policy-based routing does, and on Cisco routers it is one route-map away from working, or from causing an outage nobody can explain.
Where PBR sits in the forwarding decision
Normal IP forwarding is destination-based: the router takes the destination address, finds the longest matching prefix in the FIB and forwards the packet to that next hop. Nothing else about the packet matters.
Policy-based routing inserts a step in front of that lookup. A route-map applied to an interface is evaluated for every packet arriving on that interface, and if the packet matches a permit sequence with a set clause, the set clause decides where it goes. If it does not match, the packet carries on to the normal routing lookup as if PBR did not exist. The topology at the top of this article shows the effect: the users and the guests share the same router and the same default route, but guest traffic is peeled off to ISP-B before the routing table is ever consulted.
PBR is applied on ingress
The route-map goes on the interface where the traffic arrives, not the one where it leaves. This trips up people who think of it as an outbound filter. If you apply it to the WAN interface, it will act on return traffic from the internet, which is almost never what you want.
Permit and deny do not mean what they do in an ACL
A permit sequence in the route-map, with a matching ACL permit, means "policy route this packet". A deny sequence, or an ACL deny, means "do not policy route this packet, use the routing table". Nothing is dropped. The ACL in a PBR route-map is a classifier, not a firewall, and treating it as a firewall is how people end up with guests reaching internal servers they thought were blocked.
Router-originated traffic is not covered
An ip policy statement on an interface only touches transit traffic arriving on that interface. Pings, syslog, NTP and BGP sessions sourced from the router itself are untouched. If you need those policy-routed as well, you use ip local policy route-map, which is a separate command with a separate route-map.
The set clauses and their order
The set clause is where the decision is made, and the four common ones behave differently in a way that matters when a link fails.
set ip next-hop
This forwards matching traffic to the given next hop regardless of what the routing table says. It is the one you use when the policy must win over a more specific route. If the next hop is not reachable because the connected interface is down, IOS-XE falls back to normal routing, but if the interface is up and the ISP's network behind it has failed, the router happily keeps sending traffic into a black hole.
set ip default next-hop
This only applies when the routing table has no explicit route for the destination. The default route does not count as an explicit route, so for internet traffic it behaves much like set ip next-hop, but a more specific route learned through OSPF or BGP will win. It is the safer choice when you only want to influence traffic that would otherwise follow the default route.
set interface and set default interface
These point traffic at an exit interface instead of a next-hop address. Use them only on point-to-point links. On Ethernet, forwarding to an interface without a next hop makes the router ARP for every destination address, which works only if the ISP runs proxy ARP and quietly fills your ARP table.
verify-availability with object tracking
The fix for the black-hole problem is to tie the next hop to a tracked object. With set ip next-hop verify-availability, the next hop is only used while the associated track object is up. When an IP SLA probe through ISP-B fails, the track goes down, the set clause is skipped, and the packet falls through to normal routing over ISP-A. This is the version you should deploy in production.
Configuring it on IOS-XE
The example uses a branch router R1 with users on 192.0.2.0/24 behind GigabitEthernet0/0/2 and guests on 198.51.100.0/24 behind GigabitEthernet0/0/3. ISP-A is reached on GigabitEthernet0/0/0 with next hop 203.0.113.1, and ISP-B on GigabitEthernet0/0/1 with next hop 203.0.113.5. The default route points to ISP-A.
Track the path through ISP-B
Start with an IP SLA probe sourced from the ISP-B interface, and a track object that follows its reachability. Probing the ISP's gateway only proves the link is up; in production, probe something further upstream through that link so you catch failures inside the ISP too.
ip sla 20icmp-echo 203.0.113.5 source-interface GigabitEthernet0/0/1frequency 5ip sla schedule 20 life forever start-time nowtrack 20 ip sla 20 reachabilitydelay down 10 up 30
The delay stops a single lost probe from flapping the policy, and the longer up delay makes sure ISP-B is stable before guests move back onto it.
Classify the traffic
The ACL matches guest traffic heading for the internet. The first line is the important one: guest traffic to the internal user subnet must not be policy routed, otherwise it would be sent to ISP-B instead of being routed locally.
ip access-list extended GUEST-TO-INTERNETdeny ip 198.51.100.0 0.0.0.255 192.0.2.0 0.0.0.255permit ip 198.51.100.0 0.0.0.255 any
Build the route-map and apply it
route-map PBR-GUEST permit 10match ip address GUEST-TO-INTERNETset ip next-hop verify-availability 203.0.113.5 10 track 20interface GigabitEthernet0/0/3ip policy route-map PBR-GUEST
The number 10 in the set clause is the sequence of the next hop within that clause, so you can list several tracked next hops in order of preference.
Do not forget NAT
Guest traffic now leaves through ISP-B, so it needs to be translated to the ISP-B address. If your NAT rule is written against a single interface or uses a plain ACL, guest packets leave ISP-B carrying an ISP-A address or no translation at all, and the ISP drops them. Use a route-map NAT rule that matches the exit interface:
route-map NAT-ISP-B permit 10match interface GigabitEthernet0/0/1ip nat inside source route-map NAT-ISP-B interface GigabitEthernet0/0/1 overload
Build the equivalent for ISP-A, so each exit translates to its own address.
The Junos equivalent
Junos does not use route-maps for this. The same result comes from filter-based forwarding: a firewall filter on the ingress interface sends matching traffic into a forwarding routing instance that has its own default route.
set routing-instances ISP-B instance-type forwardingset routing-instances ISP-B routing-options static route 0.0.0.0/0 next-hop 203.0.113.5set routing-instances ISP-B routing-options static route 0.0.0.0/0 qualified-next-hop 203.0.113.1 preference 10set routing-options rib-groups IFRG import-rib [ inet.0 ISP-B.inet.0 ]set routing-options interface-routes rib-group inet IFRGset firewall family inet filter PBF-GUEST term LOCAL from destination-address 192.0.2.0/24set firewall family inet filter PBF-GUEST term LOCAL then acceptset firewall family inet filter PBF-GUEST term GUEST from source-address 198.51.100.0/24set firewall family inet filter PBF-GUEST term GUEST then count GUEST-PBFset firewall family inet filter PBF-GUEST term GUEST then routing-instance ISP-Bset firewall family inet filter PBF-GUEST term DEFAULT then acceptset interfaces ge-0/0/3 unit 0 family inet filter input PBF-GUEST
Two differences are worth knowing. First, the forwarding instance starts empty, so it cannot resolve 203.0.113.5 until the rib-group copies the interface routes into ISP-B.inet.0. Leave that out and the static route stays hidden. Second, the final accept term matters far more than on Cisco: a Junos firewall filter ends with an implicit discard, so forgetting the DEFAULT term drops every packet that is not guest traffic, where on IOS-XE the same mistake simply means normal routing. For probe-based failover rather than link-state failover, pair the qualified-next-hop with RPM probes and ip-monitoring.
Verification
Is the route-map matching?
show route-map PBR-GUEST
Look for the policy routing matches counter on sequence 10. It should increase while guests are browsing. A counter stuck at zero usually means the route-map is on the wrong interface or the ACL does not match what you think.
Is the next hop considered available?
show track 20show ip sla statistics 20
The track should be Up and the SLA return code OK. If the track is down, the set clause is skipped and all guest traffic is following the routing table to ISP-A, which looks exactly like PBR not working.
Is the policy on the right interface?
show ip policy
This lists every interface with a policy route-map applied. It should show GigabitEthernet0/0/3 with PBR-GUEST and nothing on the WAN interfaces. Keep in mind that CEF lookup tools such as show ip cef exact-route only show the destination-based path and do not account for PBR. For a real path test, run a traceroute from a guest client, not from the router. A traceroute from R1 is locally generated and ignores the interface policy entirely, so it will always show the ISP-A path and send you hunting for a problem that does not exist. On Junos, check the GUEST-PBF counter with show firewall filter PBF-GUEST and the instance table with show route table ISP-B.inet.0.
Mistakes that bite in production
Internal traffic gets hijacked
The classic outage: the ACL says permit ip 198.51.100.0 0.0.0.255 any, and suddenly guests cannot reach the captive portal or the local DNS server, because that traffic is now being thrown at ISP-B. Always deny internal destinations first.
The ISP fails and the link stays up
Without tracking, set ip next-hop only notices a failure when the interface goes down. An upstream failure inside the ISP leaves the link up and your guests offline. Use verify-availability with a probe that tests beyond the first hop.
A deny is read as a drop
Someone adds a deny line to the PBR ACL expecting to block a destination, and the traffic just follows the routing table instead. If you need to block something, do it with an interface ACL or a zone-based firewall, not with the PBR classifier.
Return traffic does not come back the same way
PBR only steers outbound packets. Return traffic follows whatever the internet decides, which is fine when NAT is translating to the ISP-B address, but broken when it is not. On inter-site PBR with no NAT, check that the return path goes through the same stateful firewall, or asymmetric flows will be dropped.
The takeaway
Policy-based routing lets you choose a path based on the source, protocol or ports of a packet instead of only its destination. On IOS-XE it is an ingress route-map with a match ACL and a set clause, and in production that set clause should always be set ip next-hop verify-availability tied to an IP SLA track so a failed ISP does not black-hole the traffic. Deny internal destinations first, fix NAT for the new exit, and remember that router-originated traffic needs ip local policy. On Junos the same design becomes filter-based forwarding with a forwarding instance and a rib-group.
For a lab exercise, build R1 with two upstream routers in CML or EVE-NG and two LAN subnets. Apply the PBR-GUEST policy, confirm with show route-map that guest traffic takes the second ISP, then shut an interface on the far side of ISP-B so the link stays up. Watch the IP SLA fail, the track go down and guest traffic move to ISP-A, and then bring it back and time how long the delay up setting holds it off.