VRF routing protocols — OSPF, BGP, and crossing the VRF boundary on purpose
What you'll learn
- Place OSPF and BGP stanzas under `vrf name` so FRR builds a per-VRF routing instance
- Verify per-VRF protocol state with the vrf-scoped FRR show commands
- Leak a route between VRFs with a static next-hop in another VRF, and explain what the command renders to in FRR
- Leak routes between VRFs with BGP `import vrf`, and filter what crosses
- Recognise a leak that committed cleanly and installed nothing
- Name what VyOS refuses to accept in the same VRF, and why
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) · 2026-08-19
A VRF is a routing-table boundary. To make it useful, the engineer runs routing protocols inside it, and — sooner than they expect — needs a specific prefix to cross the boundary anyway: a shared DNS resolver, a monitoring collector, an internet default. Both halves of that are in this lesson, because the second half is where VRF designs fail. A leak that is configured wrongly does not error. It commits, it appears in the configuration, and it installs nothing.
The critical configuration-tree fact for the first half: routing-protocol
stanzas must live under vrf name X, not at the top level. A top-level
protocols ospf block installs into the main table regardless of which VRF
the interface is in.
The placement rule
# WRONG - OSPF runs in the main table even though eth1 is in CUST-A
set protocols ospf area 0 network 10.1.0.0/24
set interfaces ethernet eth1 vrf CUST-A
# RIGHT - OSPF runs inside CUST-A
set vrf name CUST-A protocols ospf area 0 network 10.1.0.0/24
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 address 10.1.0.1/24
The VyOS tree mirrors the FRR tree. Anything under vrf name CUST-A protocols is rendered inside an FRR vrf CUST-A block or as a
vrf CUST-A-qualified router instance:
! from the top-level stanza
router ospf
network 10.1.0.0/24 area 0
!
! from the per-VRF stanza
router ospf vrf CUST-A
network 10.1.0.0/24 area 0
The vrf CUST-A qualifier is what makes FRR allocate the OSPF instance
inside that routing context. Without it the process runs against the main
RIB, and the symptom is not a broken adjacency — it is a working adjacency
whose routes land in the wrong table.
flowchart TB
subgraph TOP["Top-level configuration"]
GOSPF["protocols ospf"] --> GRIB["main RIB"]
GBGP["protocols bgp"] --> GRIB
end
subgraph PERVRF["vrf name CUST-A protocols ..."]
VOSPF["ospf"] --> VRIB["CUST-A RIB"]
VBGP["bgp"] --> VRIB
VSTATIC["static"] --> VRIB
end
GRIB --> Z["zebra"]
VRIB --> Z
Z --> KT254["kernel table 254"]
Z --> KT1001["kernel table 1001"]
OSPF inside a VRF
configure
set vrf name CUST-A table 1001
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 address 10.1.0.1/24
# OSPF inside the VRF
set vrf name CUST-A protocols ospf parameters router-id 10.1.0.1
set vrf name CUST-A protocols ospf area 0 network 10.1.0.0/24
# Turn on authentication for the area, then supply the key per interface
set vrf name CUST-A protocols ospf area 0 authentication plaintext-password
set vrf name CUST-A protocols ospf interface eth1 authentication plaintext-password s3cr3t
# Advertise a default into the VRF, but only if we have one to give
set vrf name CUST-A protocols ospf default-information originate metric 10 metric-type 2
commit
save
Three details in that block are the ones people get wrong.
The router id lives under parameters. It is
protocols ospf parameters router-id, not protocols ospf router-id. Set it
explicitly per VRF. If you do not, FRR picks one from the addresses it can
see in that routing context, which means the id can change when an interface
is added or removed — and an OSPF instance that changes router id resets its
adjacencies.
Area authentication and the key are two separate commands. area 0 authentication plaintext-password selects the authentication type for the
area and takes no key. The key is a per-interface value under
protocols ospf interface eth1 authentication plaintext-password. A plain
text key is capped at 8 characters and an MD5 key at 16, so a password
policy that mandates longer will simply not commit; use
interface eth1 authentication md5 key-id 1 md5-key ... when you need the
stronger option.
default-information originate is how a default enters OSPF. Without
always, FRR only originates it when the VRF actually has a default route of
its own. Adding always makes the router advertise a default it may not be
able to honour, which turns every other router in the area into a black-hole
client if the upstream is down.
Validation is the ordinary FRR command set with a vrf qualifier on every
one:
show ip ospf vrf CUST-A
show ip ospf vrf CUST-A neighbor
show ip ospf vrf CUST-A route
show ip route vrf CUST-A
Read show ip ospf vrf CUST-A neighbor for two things: a neighbour in Full
(or Full/DR / Full/BDR on a broadcast segment), and the interface column
naming the interface you expect. A neighbour stuck in Init means hellos are
arriving but yours are not getting back — usually authentication mismatch,
mismatched hello/dead timers, or a unidirectional path. A neighbour stuck in
ExStart means the adjacency negotiated and the database exchange failed,
which on a VRF is very often an MTU mismatch between the VRF-facing interface
and the peer.
BGP inside a VRF
configure
set vrf name CUST-A table 1001
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 address 10.1.0.1/24
# eBGP to the tenant's edge router, inside the VRF
set vrf name CUST-A protocols bgp system-as 65000
set vrf name CUST-A protocols bgp parameters router-id 10.1.0.1
set vrf name CUST-A protocols bgp neighbor 10.1.0.254 remote-as 65001
set vrf name CUST-A protocols bgp neighbor 10.1.0.254 address-family ipv4-unicast
set vrf name CUST-A protocols bgp neighbor 10.1.0.254 address-family ipv4-unicast soft-reconfiguration inbound
# Originate the tenant LAN into the session
set vrf name CUST-A protocols bgp address-family ipv4-unicast network 10.1.0.0/24
commit
save
The ASN is system-as in VyOS 1.4 and 1.5. The older set protocols bgp 65000 ... form, where the ASN was a node name rather than a value, was
removed in 1.4; a configuration written that way will not commit on 1.5.
system-as is mandatory — a BGP stanza without one fails verification with
BGP system-as number must be defined!.
Each VRF gets its own BGP instance, and each instance needs its own
system-as even when the number is identical to the global one. Set a
per-VRF parameters router-id for the same reason as OSPF: the automatic
choice is derived from what is visible in that routing context and can move.
show ip bgp vrf CUST-A summary
show ip bgp vrf CUST-A
show ip route vrf CUST-A
In the summary, the column to read is the state column. It shows a state name
(Idle, Connect, Active, OpenSent, OpenConfirm) while the session is
down and a prefix count once it is Established — a number in that column
means the session is up and that many prefixes were accepted. Active is the
one that misleads people: it means the local side is trying to open a TCP
connection and failing, not that anything is active.
What does not belong inside the VRF
When the VyOS box is a genuine L3VPN PE, the per-VRF address family carries the RD, the route targets, and the instruction to exchange with the VPN RIB:
set vrf name CUST-A protocols bgp system-as 65000
set vrf name CUST-A protocols bgp parameters router-id 10.0.0.1
set vrf name CUST-A protocols bgp address-family ipv4-unicast rd vpn export '65000:1001'
set vrf name CUST-A protocols bgp address-family ipv4-unicast route-target vpn both '65000:1001'
set vrf name CUST-A protocols bgp address-family ipv4-unicast export vpn
set vrf name CUST-A protocols bgp address-family ipv4-unicast import vpn
The PE-to-PE session itself does not. It lives in the default instance,
carries the ipv4-vpn family, and is what actually moves the VPN routes
between routers:
set protocols bgp system-as 65000
set protocols bgp parameters router-id 10.0.0.1
set protocols bgp neighbor 10.0.0.2 remote-as 65000
set protocols bgp neighbor 10.0.0.2 update-source lo
set protocols bgp neighbor 10.0.0.2 address-family ipv4-vpn
That split is the part worth memorising. rd, route-target, export vpn
and import vpn describe how one VRF’s routes are stamped on the way into
the VPN RIB and selected on the way out. The ipv4-vpn neighbour describes
how that RIB reaches the other PE. Putting the VPNv4 neighbour inside the VRF
does not build an L3VPN; it builds an ordinary session in the tenant’s
routing context.
Crossing the VRF boundary on purpose
Every multi-tenant design eventually needs one prefix to cross. VyOS gives you two mechanisms, and it is worth being precise about them, because the command that looks like it should work does not.
Why the obvious command does nothing
Take a shared services subnet 10.99.0.0/24, reachable from the default VRF
via 192.0.2.1 on eth0. The intuitive way to give CUST-A access:
set vrf name CUST-A protocols static route 10.99.0.0/24 next-hop 192.0.2.1
This commits. It appears in show configuration commands. It renders to FRR
as:
vrf CUST-A
ip route 10.99.0.0/24 192.0.2.1
exit-vrf
And it does nothing at all, because next hops are resolved in the routing
context the route belongs to. FRR looks for a way to reach 192.0.2.1 inside
CUST-A. Table 1001 contains the tenant’s connected route and nothing else.
The next hop does not resolve, the route stays inactive, and zebra never
offers it to the kernel.
$ ip route show table 100110.1.0.0/24 dev eth1 proto kernel scope link src 10.1.0.1Illustrative output
FRR will show the route with an unresolved next hop and no selection marker. The exact wording differs between FRR releases, so treat that as a hint and the kernel table as the fact:
$ vtysh -c 'show ip route vrf CUST-A 10.99.0.0/24'VRF CUST-A:
S 10.99.0.0/24 [1/0] via 192.0.2.1 inactiveIllustrative output
Two things distinguish this from a working route: there is no * (selected)
or > (installed in the FIB) in the code column, and the next hop is flagged
as not resolving. A working static in the same table would read
S>* 10.99.0.0/24 [1/0] via 192.0.2.1, eth0.
This is the failure this part of the course exists to teach. Nothing errors. The change review passes. The tenant reports that the shared service is unreachable, and the configuration says it should not be.
Mechanism one: a static route whose next hop lives in another VRF
The static-route tree has a vrf node under both next-hop and interface.
Its help text is exactly what it does: VRF to leak route.
# From CUST-A, reach the shared subnet via a next hop in the default VRF
set vrf name CUST-A protocols static route 10.99.0.0/24 next-hop 192.0.2.1 vrf default
The value is either default — meaning the main table — or the name of
another configured VRF. This renders to FRR as:
vrf CUST-A
ip route 10.99.0.0/24 192.0.2.1 nexthop-vrf default
exit-vrf
nexthop-vrf tells FRR to resolve 192.0.2.1 in the default routing
context while installing the resulting route in CUST-A’s. Now it resolves —
192.0.2.0/24 is connected on eth0 in the main table — and zebra installs
it:
$ ip route show table 100110.1.0.0/24 dev eth1 proto kernel scope link src 10.1.0.1
10.99.0.0/24 via 192.0.2.1 dev eth0 proto staticIllustrative output
The signature of a leaked route in a kernel table is exactly that: an output
device that is not a member of the VRF the table belongs to. eth0 is in the
default VRF; the route is in table 1001. If you can see that line, the leak
is real.
The return path is a separate configuration
A leak is unidirectional. 10.99.0.0/24 is now reachable from CUST-A, but
the shared service’s replies are routed by the main table, which has no route
to 10.1.0.0/24. Traffic goes out and nothing comes back — which reads
exactly like a firewall drop and sends people to the wrong subsystem for an
hour.
The reverse leak is configured in the default context, pointing into the VRF:
set protocols static route 10.1.0.0/24 interface eth1 vrf CUST-A
Rendering to:
ip route 10.1.0.0/24 eth1 nexthop-vrf CUST-A
Here the interface form is the right one, because eth1 is directly
connected inside CUST-A and there is no next-hop address to name. Verify the
same way, in the main table this time:
ip route show table 254
sequenceDiagram
autonumber
participant H as Host 10.1.0.50 (CUST-A)
participant K1 as table 1001
participant K2 as table 254
participant S as Shared service 10.99.0.5
H->>K1: dst 10.99.0.5
Note over K1: leaked route<br/>via 192.0.2.1 dev eth0
K1->>S: forwarded out eth0
S->>K2: reply, dst 10.1.0.50
Note over K2: without the reverse leak<br/>no route to 10.1.0.0/24
K2-->>S: unreachable
Mechanism two: BGP import vrf
Static leaks are one prefix at a time and they do not follow topology changes. When there are many prefixes, or when the set of prefixes is learned rather than known, BGP does the same job dynamically.
# Both instances need an ASN; the number may be the same
set protocols bgp system-as 65000
set protocols bgp parameters router-id 10.0.0.1
set vrf name CUST-A protocols bgp system-as 65000
set vrf name CUST-A protocols bgp parameters router-id 10.1.0.1
# Put the tenant's prefixes into CUST-A's BGP RIB in the first place
set vrf name CUST-A protocols bgp address-family ipv4-unicast redistribute connected
# Import in each direction
set vrf name CUST-A protocols bgp address-family ipv4-unicast import vrf default
set protocols bgp address-family ipv4-unicast import vrf CUST-A
The redistribute connected line is not optional decoration, and leaving it
out is the second-most-common way to configure a leak that does nothing.
import vrf copies routes between BGP RIBs. A connected route in CUST-A is
in zebra’s RIB, not in BGP’s, until something puts it there — a network
statement or a redistribute. Import a VRF whose BGP instance has no routes
and you import nothing, correctly and silently.
Filter what crosses with a route map. The map is applied to the address-family that is doing the importing:
set policy prefix-list SHARED-ONLY rule 10 action permit
set policy prefix-list SHARED-ONLY rule 10 prefix 10.99.0.0/24
set policy route-map LEAK-IN rule 10 action permit
set policy route-map LEAK-IN rule 10 match ip address prefix-list SHARED-ONLY
set vrf name CUST-A protocols bgp address-family ipv4-unicast route-map vrf import LEAK-IN
Without the map, import vrf default imports the whole main table’s BGP
routes into the tenant — including, on an internet-facing router, whatever
the transit session is carrying. That is not a leak, it is a merge.
What VyOS refuses
The VPN mechanism and the import vrf mechanism are two implementations of
the same idea, and they cannot both be configured on one address family. VyOS
rejects the combination outright, with messages that name the pair:
Command "import vrf" conflicts with "rd vpn export" command!, and the same
form for route-target vpn both, import and export.
Two more references block a delete:
- A VRF that another VRF imports from cannot be removed:
Cannot delete VRF instance "CUST-A", unconfigure "import vrf" commands! - The default BGP instance cannot be removed while any VRF has a BGP instance depending on it.
Both are the validator protecting you from a half-built import relationship, and both are cleared by deleting the dependent side in the same commit.
Common configuration mistakes
- Protocol stanza at the top level.
set protocols ospf area 0 network 10.1.0.0/24outsidevrf name CUST-A. The adjacency comes up, the routes install in the main table, and connectivity to directly connected hosts is unaffected — so nothing looks wrong until someone checks which table the learned prefixes are in. - 1.3-era BGP syntax.
set protocols bgp 65000 neighbor ...with the ASN as a node name. Removed in 1.4. The 1.5 form issystem-as. - A leak with no
vrfon the next hop. Commits, installs nothing. This is the one to internalise, because it is invisible in the configuration and only visible in the kernel table. - A leak in one direction. Traffic leaves, replies have no route home. The symptom mimics a firewall drop.
import vrfwith nothing in the source BGP RIB. Nonetworkstatement and noredistributein the source VRF means there is nothing to import.import vrfwith no route map. Imports everything the source instance carries.
How it fails
- The route is inactive. Configured, committed, absent from
ip route show table TABLEID. Cause: the next hop does not resolve in the context the route lives in. Fix: add thevrfvalue to the next hop, or give the VRF a path to the next hop. - The route installs and traffic still fails. Cause: no reverse leak, or a firewall rule that only ever saw traffic in one routing context. Check the return direction’s table before touching the firewall.
- The adjacency is up in the wrong instance.
show ip ospf neighborshows it,show ip ospf vrf CUST-A neighbordoes not. The stanza is at the top level. - BGP is Established and no prefixes arrive. The neighbour has no
address-familyconfigured, so the session negotiated no capability to carry anything. - The commit is rejected naming two commands.
import vrfand the VPN commands were both configured on one address family. Pick one. - The VRF cannot be deleted. Another VRF imports from it, or its interfaces and static routes are still present. Remove the reference in the same commit.
Rollback
# Remove a routing protocol from a VRF
configure
delete vrf name CUST-A protocols ospf
commit
save
# Remove a leak, both directions
configure
delete vrf name CUST-A protocols static route 10.99.0.0/24
delete protocols static route 10.1.0.0/24
commit
save
# Move a misplaced protocol stanza into its VRF, in one commit
configure
delete protocols ospf
set vrf name CUST-A protocols ospf parameters router-id 10.1.0.1
set vrf name CUST-A protocols ospf area 0 network 10.1.0.0/24
commit
save
A safe rollback sequence:
- Capture the running configuration before you start.
compareand read the whole diff, not just the line you meant to change — deleting a VRF’s protocols subtree can trip the delete-reference checks elsewhere.- Use
commit-confirmwhen the change touches a path your session depends on. - After
commit, verify in the kernel first (ip route show table 1001,ip route show table 254) and in FRR second. The kernel view does not depend on knowing the current FRR output format. saveonly once both directions of traffic have been re-tested.
Production discipline
Cross-course references
vyos-xv-01-vrf-conceptcovers the l3mdev model and the routing-policy rules that make a VRF miss fail rather than fall through.vyos-xv-02-vrf-configcovers the VRF itself: names, table ids, and the reference checks that block a delete.vyos-xvi-01-leaking-conceptand the rest of Part XVI take route leaking further, including the firewall and IPv6 dimensions and the anti-patterns.- The BGP course’s
XXIV-BGPSession,XXV-BGPAdvertiseandXXXI-BGPTroubleshootcover the session and policy mechanics that this lesson applies inside a VRF. - The OSPF course’s
XVIII-OSPFFundamentalsthroughXXII-OSPFTroubleshootdo the same for OSPF.
Quiz
Knowledge check · 4 questions
Q1. CUST-A (table 1001) needs to reach 10.99.0.0/24, which is reachable in the default VRF via 192.0.2.1 on eth0. Which command actually installs a route in table 1001?
Q2. A static route configured inside a VRF, whose next-hop address is only reachable through the main table, still installs into the VRF's kernel table.
Q3. An operator configures eBGP for CUST-A but places the stanza at the top level rather than under `vrf name CUST-A`. The session is Established and `show ip route vrf CUST-A` is empty. Where did the tenant's prefixes install, and what is the corrected configuration?
The intent was for the tenant's prefixes to install inside CUST-A. The stanza went in at the top level. eth1 is in CUST-A and addressed, so the peer 10.1.0.254 is reachable and the session comes up - but the BGP instance is the global one, running against the main RIB.
Q4. A BGP `import vrf` leak is configured in both directions between CUST-A and the default VRF. It commits cleanly. The default VRF receives nothing from CUST-A. What is missing?
Both instances have a system-as and a router id. CUST-A has eth1 with 10.1.0.0/24 connected, and no BGP neighbours - it is a pure tenant segment. `set protocols bgp address-family ipv4-unicast import vrf CUST-A` is configured, and the mirror command in CUST-A. `show ip bgp` in the default instance shows no 10.1.0.0/24.
Passing score: 75%. Answers are checked in this browser.