Skip to main content
RunBook Academy

VyOSXV · VRFsVRF

VRF troubleshooting — RIB versus FIB, `ip vrf exec`, and the leak that installs nothing

Advanced⏱ ~22 minip vrf exec VRFNAME ping DSTip vrf exec VRFNAME traceroute DSTip vrf exec VRFNAME ss -tunapip route show vrf VRFNAMEip -d link show SLAVEIFshow vrfshow ip route vrf VRFNAMEshow bgp vrf VRFNAME ipv4 unicast summarytcpdump -ni SLAVEIFvyos

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

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)

Not yet marked complete on this device.

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-A reads FRR’s RIB. It is a statement of what FRR believes and intends.
  • ip route show vrf CUST-A reads 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.

Read-only / Safethe kernel's per-VRF forwarding table
$ ip route show vrf CUST-A
10.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 20

Illustrative 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-NAME master 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

Read-only / SafeVyOS view of the VRF masters
$ show vrf
VRF 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  eth9

Illustrative 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:

Read-only / SafeFRR view of the VRFs
$ vtysh -c 'show vrf'
vrf CUST-A id 5 table 1001
vrf SHARED id 7 table 9999
vrf default id 0 table 254

Illustrative 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-A is 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 no address-family ipv4-unicast node, so VyOS never rendered an activate line 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 with 0 did negotiate the family and either received nothing or had everything filtered.
  • A route is in show ip route vrf CUST-A but not in ip 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.

Read-only / Safea leak that did not resolve
$ show ip route vrf CUST-A 10.250.0.0/16
Routing 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 inactive

Illustrative output

Compare with the same route when the vrf leaf is present:

Read-only / Safea leak that resolved
$ show ip route vrf CUST-A 10.250.0.0/16
Routing 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 1

Illustrative 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 jump from the base chain (forward filter for transit traffic, input filter for traffic to the router itself). The per-interface firewall in / out bindings 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:

  1. show configuration commands captured off-box before the first change.
  2. compare before every commit, so you know the candidate contains what you think and nothing else.
  3. commit-confirm 10 — a whole number of minutes — for anything touching a path your own session traverses. confirm makes it permanent; doing nothing rolls it back.
  4. Verify with both show ip route vrf and ip route show vrf, not one of them.
  5. save only 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-NetTroubleshoot covers ip route get, ip neigh and namespace tooling; ip vrf exec is the l3mdev-scoped sibling of those primitives.
  • vyos-xiv-06-table-anti-patterns covers the same RIB/FIB reasoning for policy routing outside VRFs.
  • vyos-xv-04-vrf-ipv6 covers the IPv6-specific diagnostics — ND filtering, link-local next-hops, route6 leaks.
  • vyos-xvi-05-leaking-troubleshoot takes the leaking failures in this lesson further, including BGP-based leaking.
  • The BGP course’s XXXI-BGPTroubleshoot and the OSPF course’s XXII-OSPFTroubleshoot cover the protocol diagnostics that apply unchanged inside a VRF once you have added the vrf selector.

Quiz

Knowledge check · 4 questions

  1. 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'?

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

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

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