VRF troubleshooting — RIB versus FIB, `ip vrf exec`, and the leak that installs nothing
What you'll learn
- Separate FRR's RIB from the kernel FIB and know which command reads which, per VRF
- Run reachability and socket diagnostics inside a VRF with `ip vrf exec`, and explain what that actually does to the process
- Verify the four places VRF state lives — configuration, kernel, FRR, peer — in the order that localises a fault fastest
- Recognise the signature of a route leak that installs nothing, and the signature of a leak that is missing its return direction
- Express a per-VRF firewall policy on VyOS 1.5, where the firewall matches interfaces rather than VRFs
Prerequisites
- VRF concept — L3VPN, the kernel vrf driver, and how VyOS implements routing tables per VRF
- VRF configuration — set vrf name, table ids, attaching interfaces, addresses
- VRF routing protocols — OSPF, BGP, and crossing the VRF boundary on purpose
- Routing table anti-patterns — too many tables, conflicting priorities, FRR not pushing
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)
A VRF-localised routing failure has a distinctive shape. A packet
enters the router on the right interface, the configuration reads
correctly, every show command an operator reaches for by habit
looks healthy — and the traffic goes nowhere. The reason it is hard
is that VRF state lives in four places at once, and the operational
commands most people know only read two of them.
This lesson is the playbook: the four sources of truth, the order to check them in, the one distinction that resolves most of these incidents on its own, and the failure signatures worth memorising.
The distinction that resolves most VRF incidents
Before any of the command sequences: FRR’s RIB and the kernel’s FIB are different things, and there are separate commands for each.
show ip route vrf CUST-Areads FRR’s RIB. It is a statement of what FRR believes and intends.ip route show vrf CUST-Areads the kernel’s table 1001. It is what actually forwards packets.
In a healthy VRF the two agree, which is why the difference rarely
comes up — and why it ambushes people when it matters. A route can
sit in the RIB indefinitely without ever being offered to the kernel:
FRR keeps routes whose next-hop it cannot resolve, marks them
inactive, and never installs them. show ip route prints them.
Traffic never sees them.
flowchart LR
CFG["VyOS configuration<br/>set vrf name CUST-A ..."] --> FRR
FRR["FRR RIB · vrf CUST-A<br/>show ip route vrf CUST-A"] -- "zebra installs<br/>only resolved routes" --> FIB
FIB["kernel table 1001<br/>ip route show vrf CUST-A"] --> PKT["forwarded packets"]
FRR -. "unresolved next-hop:<br/>stays here, inactive" .-> DEAD["never installed"]
Every diagnostic in this lesson eventually lands on this question: is the route in the RIB only, or in the FIB too? Ask it early.
$ ip route show vrf CUST-A10.1.0.0/24 dev eth1 proto kernel scope link src 10.1.0.1
10.250.0.0/16 via 10.99.0.254 dev eth9 proto 196 metric 20
192.168.0.0/16 via 10.1.0.254 dev eth1 proto bgp metric 20Illustrative output
The four sources of truth
A working VRF has four views that agree:
- The VyOS configuration tree — what the operator typed.
- The kernel — the
vrf-NAMEmaster device, the enslaved interfaces under it, and the routes in its table. - FRR — the per-VRF routing instance: its RIB, and the state of OSPF, BGP and static inside it.
- The adjacent peer — the far end’s view of the same session.
Check them in that order. Each step is cheap, and each one rules out everything downstream of it. Jumping straight to the peer because the symptom looks like a peering problem is how a two-minute configuration-placement bug becomes an afternoon.
flowchart TB
subgraph S1["1 · configuration"]
C1["set vrf name CUST-A table 1001"]
C2["set interfaces ethernet eth1 vrf CUST-A"]
C3["set vrf name CUST-A protocols bgp system-as 65000"]
end
subgraph S2["2 · kernel"]
K1["vrf-CUST-A master · table 1001 · UP"]
K2["eth1 · master vrf-CUST-A"]
K3["table 1001 holds the connected routes"]
end
subgraph S3["3 · FRR"]
F1["vrf CUST-A instance exists"]
F2["bgpd · peer Established in this VRF"]
F3["zebra · RIB and FIB agree"]
end
subgraph S4["4 · peer"]
P1["far end sees the session"]
P2["far end advertises the prefixes"]
end
S1 --> S2 --> S3 --> S4
Step 1 — the configuration tree
show configuration commands | match "vrf"
show configuration commands | match "protocols"
The first tells you which interfaces claim a VRF and which VRFs
exist. The second is the one that catches the commonest defect of
all: a protocols stanza that sits at the top of the tree instead of
under vrf name CUST-A.
That defect has a specific history. An engineer converts a working
non-VRF router to a VRF design by adding vrf CUST-A to the
interfaces — and leaves the existing protocols ospf or
protocols bgp block where it was. The adjacency comes up, because
the interface is still reachable. The routes install in table 254.
The customer’s traffic, which is scoped to table 1001, never sees
them. Nothing errors.
# This should return nothing on a router whose routing is all per-VRF
show configuration commands | match "^set protocols"
Step 2 — the kernel
$ show vrfVRF name state mac address flags interfaces
-------- ----- ----------- ----- ----------
CUST-A up 52:54:00:11:11:11 noarp,master,up,lower_up eth1
SHARED up 52:54:00:99:99:99 noarp,master,up,lower_up eth9Illustrative output
Then the two questions that show vrf does not answer directly:
# Is the interface really enslaved? (the configuration tree is not evidence)
ip -d link show eth1
# Does the per-VRF table hold what it should?
ip route show vrf CUST-A
ip -6 route show vrf CUST-A
ip -d link show eth1 prints master vrf-CUST-A on the second line
when the binding took. If it does not, the configuration and the
kernel disagree.
If the master device is missing entirely, ip -d link show vrf-CUST-A reports Device "vrf-CUST-A" does not exist. — that is a
VRF that was never created, not one that is down. Look for a failed
commit, not for a link to bring up.
Step 3 — FRR
FRR keeps a routing instance per VRF. Its own view of which ones exist is the first thing to read:
$ vtysh -c 'show vrf'vrf CUST-A id 5 table 1001
vrf SHARED id 7 table 9999
vrf default id 0 table 254Illustrative output
Then the per-VRF protocol state:
show ip route vrf CUST-A
show ip route vrf CUST-A 192.168.0.0/16
show bgp vrf CUST-A ipv4 unicast summary
show bgp vrf CUST-A ipv4 unicast
vtysh -c 'show ip ospf vrf CUST-A neighbor'
vtysh -c 'show ipv6 ospf6 vrf CUST-A neighbor'
What the common findings mean:
- The VRF is missing from
vtysh -c 'show vrf'. Zebra has no instance for it. The configuration exists and the kernel device may exist, but FRR was not told. Look for a commit-script error. show ip route vrf CUST-Ais empty or thin. Either the routing protocol is not running inside the VRF — the placement bug from step 1 — or its session is not up.- A peer is absent from
show bgp vrf CUST-A ipv4 unicast summary. The neighbour has noaddress-family ipv4-unicastnode, so VyOS never rendered anactivateline for it into FRR. Note the shape: an unactivated peer is missing from the family’s summary, not present in it with a zero count. A peer that is listed with0did negotiate the family and either received nothing or had everything filtered. - A route is in
show ip route vrf CUST-Abut not inip route show vrf CUST-A. Its next-hop did not resolve. This is the leaking failure, and it has its own section below.
Step 4 — the peer, tested from inside the VRF
Reachability tests are only evidence if they were scoped to the VRF.
An unscoped ping from the router uses the global table and can
easily reach a completely different host that happens to share the
address.
ip vrf exec CUST-A ping 10.1.0.254
ip vrf exec CUST-A traceroute 192.168.0.5
ip vrf exec CUST-A ss -tunap
VyOS also wraps the common ones, so ping 10.1.0.254 vrf CUST-A
does the same job for that command. ip vrf exec is the general
form: it works for any program, including ones VyOS never wrapped.
The leak that installs nothing
This is the failure that route leaking between VRFs produces most often, and it is worth recognising on sight because every surface an operator normally trusts says the configuration is fine.
Leaking a prefix from one VRF into another means telling FRR to
resolve the next-hop in a different VRF than the one that holds the
route. On VyOS that is the vrf leaf under the next-hop; it renders
FRR’s nexthop-vrf:
# Correct: resolve 10.99.0.254 in SHARED, install the result in CUST-A
set vrf name CUST-A protocols static route 10.250.0.0/16 next-hop 10.99.0.254 vrf SHARED
Omit that leaf and the command still commits without complaint:
# Commits cleanly. Installs nothing.
set vrf name CUST-A protocols static route 10.250.0.0/16 next-hop 10.99.0.254
FRR now tries to resolve 10.99.0.254 inside CUST-A’s own table,
where the address does not exist. It keeps the route in the RIB,
marks it unusable, and never hands it to the kernel.
$ show ip route vrf CUST-A 10.250.0.0/16Routing entry for 10.250.0.0/16
Known via "static", distance 1, metric 0, vrf CUST-A
Last update 00:04:52 ago
10.99.0.254 inactiveIllustrative output
Compare with the same route when the vrf leaf is present:
$ show ip route vrf CUST-A 10.250.0.0/16Routing entry for 10.250.0.0/16
Known via "static", distance 1, metric 0, vrf CUST-A, best
Last update 00:00:18 ago
* 10.99.0.254, via eth9 (vrf SHARED), weight 1Illustrative output
And the check that settles it either way:
ip route show vrf CUST-A
If the prefix is not in that output, it does not forward traffic,
whatever show ip route said.
The firewall, and what VyOS does not give you
A recurring wish is a firewall rule that matches “traffic in VRF CUST-A”. VyOS does not expose that. The firewall matches interfaces, interface groups, addresses and ports — and VRF membership is a property of an interface, not something the rule set can select on directly.
The supported way to express a per-VRF policy is therefore to name the VRF’s interfaces, and to keep that list in one place so it does not drift as the VRF gains members:
configure
# One group per VRF, maintained alongside the VRF itself
set firewall group interface-group CUST-A-IF interface eth1
set firewall group interface-group CUST-A-IF interface eth1.100
set firewall ipv4 name CUST-A-FWD default-action drop
set firewall ipv4 name CUST-A-FWD rule 10 action accept
set firewall ipv4 name CUST-A-FWD rule 10 state established
set firewall ipv4 name CUST-A-FWD rule 20 action accept
set firewall ipv4 name CUST-A-FWD rule 20 state related
set firewall ipv4 forward filter rule 100 inbound-interface group CUST-A-IF
set firewall ipv4 forward filter rule 100 action jump
set firewall ipv4 forward filter rule 100 jump-target CUST-A-FWD
commit
save
Two things to hold on to:
- The rule set is reached by a
jumpfrom the base chain (forward filterfor transit traffic,input filterfor traffic to the router itself). The per-interfacefirewall in/outbindings that older material uses were removed in 1.4. - The established/related rules are not boilerplate. A stateful
policy that permits the request and forgets the reply is the
commonest way to produce a VRF that “routes but does not work”,
and it is indistinguishable from a routing fault until you look at
conntrack -L.
The inspection commands:
show firewall
show firewall ipv4 name CUST-A-FWD
show firewall group
conntrack -L
The full per-VRF command set
# Configuration
show configuration commands | match "vrf"
show configuration commands | match "^set protocols"
# Kernel
show vrf
ip -d link show vrf-CUST-A
ip -d link show eth1
ip route show vrf CUST-A
ip -6 route show vrf CUST-A
ip neigh show dev eth1
ip -6 neigh show dev eth1
# FRR
vtysh -c 'show vrf'
show ip route vrf CUST-A
show ip route vrf CUST-A 10.250.0.0/16
show ipv6 route vrf CUST-A
show bgp vrf CUST-A ipv4 unicast summary
show bgp vrf CUST-A ipv4 unicast
vtysh -c 'show ip ospf vrf CUST-A neighbor'
vtysh -c 'show running-config'
# Reachability, scoped to the VRF
ip vrf exec CUST-A ping 10.1.0.254
ip vrf exec CUST-A traceroute 192.168.0.5
ip vrf exec CUST-A ss -tunap
# Firewall and state
show firewall ipv4 name CUST-A-FWD
conntrack -L
# The wire
tcpdump -ni eth1 host 10.1.0.254
tcpdump -ni eth9 host 10.99.0.254
The captures are the only commands in that list that prove bytes moved. Everything above them proves intent. When the intent looks right and the customer is still down, go to the wire — on both interfaces, because a leaked flow uses two.
The diagnostic walk
flowchart TD
S["Customer reports CUST-A broken"] --> C["Is any protocols stanza outside vrf name?"]
C -- yes --> CFX["Move it under vrf name · commit · re-check"]
C -- no --> K["show vrf and ip -d link: master up, interface enslaved?"]
K -- no --> KFX["Repair through the configuration, not with ip link"]
K -- yes --> R["Is the prefix in ip route show vrf CUST-A?"]
R -- "in RIB only" --> RFX["Next-hop unresolved: add or correct the vrf leaf on the leak"]
R -- "not in RIB either" --> P["Peer or protocol: session state in this VRF?"]
P -- "peer absent from family summary" --> PFX["Add the address-family node under the neighbour"]
P -- "peer present, 0 prefixes" --> PF2["Peer sent nothing, or inbound policy filtered it"]
R -- yes --> T["ip vrf exec CUST-A ping the next-hop"]
T -- fails --> W["tcpdump both interfaces: are the bytes there?"]
T -- succeeds --> F["Return direction and firewall state"]
W --> F
F --> FFX["Missing return leak, or a stateful rule set that never sees the reply"]
Rollback
Troubleshooting changes need the same discipline as planned ones — more, because they are made under pressure.
# Undo a leak you added while testing
delete vrf name CUST-A protocols static route 10.250.0.0/16
commit
save
# Move a misplaced routing stanza (both halves in one commit)
delete protocols bgp
set vrf name CUST-A protocols bgp system-as 65000
commit
save
# Release an interface from a VRF
delete interfaces ethernet eth1 vrf
commit
save
The sequence, every time:
show configuration commandscaptured off-box before the first change.comparebefore every commit, so you know the candidate contains what you think and nothing else.commit-confirm 10— a whole number of minutes — for anything touching a path your own session traverses.confirmmakes it permanent; doing nothing rolls it back.- Verify with both
show ip route vrfandip route show vrf, not one of them. saveonly after the verification is clean. An unsaved fix is a fix that ends at the next reboot.
Production discipline
Cross-course references
- The Linux course’s
XXII-Linux-NetTroubleshootcoversip route get,ip neighand namespace tooling;ip vrf execis the l3mdev-scoped sibling of those primitives. vyos-xiv-06-table-anti-patternscovers the same RIB/FIB reasoning for policy routing outside VRFs.vyos-xv-04-vrf-ipv6covers the IPv6-specific diagnostics — ND filtering, link-local next-hops,route6leaks.vyos-xvi-05-leaking-troubleshoottakes the leaking failures in this lesson further, including BGP-based leaking.- The BGP course’s
XXXI-BGPTroubleshootand the OSPF course’sXXII-OSPFTroubleshootcover the protocol diagnostics that apply unchanged inside a VRF once you have added thevrfselector.
Quiz
Knowledge check · 4 questions
Q1. A leaked prefix appears in `show ip route vrf CUST-A` but traffic to it is dropped. Which single command best distinguishes 'FRR intends this route' from 'the kernel will forward on this route'?
Q2. FRR will keep a static route in the RIB, and print it in `show ip route vrf CUST-A`, even when it has never installed that route in the kernel because the next-hop did not resolve.
Q3. A shared-services leak into a tenant VRF is confirmed installed in the kernel, and a host on the tenant network still gets no reply from the shared DNS server. The forward direction is verifiably correct. Where is the fault, and how do you prove it rather than assume it?
The topology: - VRF SHARED, table 9999, eth9 enslaved, 10.99.0.1/24. The shared services live in 10.250.0.0/16 behind the services router at 10.99.0.254. - VRF CUST-A, table 1001, eth1 enslaved, 10.1.0.1/24. The tenant CE is at 10.1.0.254 and the tenant's hosts are in 10.1.0.0/16 behind it. - set vrf name CUST-A protocols static route 10.250.0.0/16 next-hop 10.99.0.254 vrf SHARED `ip route show vrf CUST-A` lists 10.250.0.0/16 via 10.99.0.254 dev eth9. A host at 10.1.16.50 gets no reply from 10.250.0.53. Nothing has been configured in SHARED beyond the interface.
Q4. A tenant VRF has a healthy OSPF adjacency and an empty per-VRF routing table. `show ip ospf neighbor` shows the adjacency; `show ip route vrf CUST-A` shows only connected routes. Explain the state and give the safe fix.
The router was converted from a single-table design. The operator added: - set vrf name CUST-A table 1001 - set interfaces ethernet eth1 vrf CUST-A The pre-existing `set protocols ospf area 0 network 10.1.0.0/24` block at the top of the tree was left untouched. The adjacency on eth1 is Full. The customer reports that none of the routes they advertise are reachable.
Passing score: 75%. Answers are checked in this browser.