VRF concept — L3VPN, the kernel vrf driver, and how VyOS implements routing tables per VRF
What you'll learn
- Define a VRF in operational terms and contrast it with a separate router
- Explain the L3VPN architectural pattern and name the VyOS commands that carry a route distinguisher and a route target
- Describe the Linux kernel vrf driver, the l3mdev model, and the master/slave relationship
- Map a VyOS vrf name to a kernel interface, a kernel table id, and an FRR routing instance
- Explain the ip rule chain that makes a VRF miss fail instead of falling back to the main table
- Recognise the failure modes that appear when an interface is not bound to the expected VRF
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 routable slice of a single router. To packets inside the slice, the router looks identical to a stand-alone device with its own routing table, its own connected networks, and its own view of every external destination. To packets in a different slice, that router does not even exist. The Linux kernel implements this with the vrf driver (an l3mdev master), and VyOS 1.5 LTS sits FRR’s per-VRF routing instances on top of the kernel tables so the control plane matches what the data plane is actually doing.
This lesson is the foundation for the rest of Part XV. xv-02 covers the configuration commands. xv-03 covers running OSPF and BGP inside a VRF and moving routes across the VRF boundary on purpose. xv-04 covers IPv6 inside a VRF. xv-05 covers the operational command set the engineer uses when a VRF is misbehaving. xv-06 covers the anti-patterns that turn a multi-VRF estate into an incident factory.
What a VRF actually is
In operational terms, a VRF is:
- A routing table that is independent of the main routing table.
- A set of interfaces whose packets are looked up in that table.
- A control-plane context in which routing protocols (OSPF, BGP, IS-IS, static) install routes into that table and not the main table.
A packet that arrives on an interface bound to VRF CUST-A is looked up
only in CUST-A’s routing table. The kernel does not consult the main table
for that packet, even when CUST-A’s table has no route for the destination.
The packet is rejected as unreachable, not silently forwarded out the global
default route. The section “How traffic actually flows” below shows the exact
rule chain that produces that behaviour, because it is a set of ip rule
entries VyOS installs rather than an intrinsic property of the lookup.
flowchart TB
subgraph ROUTER["VyOS router (one device, multiple VRFs)"]
direction TB
subgraph VA["VRF CUST-A"]
TA["table 1001"]
IA["eth1"]
SA["OSPF / BGP / static"]
TA --- IA
TA --- SA
end
subgraph VB["VRF CUST-B"]
TB["table 1002"]
IB["eth2"]
SB["OSPF / BGP / static"]
TB --- IB
TB --- SB
end
subgraph GL["default VRF (main table)"]
TG["table 254"]
IG["eth0 (uplink)"]
SG["default route / eBGP to ISP"]
TG --- IG
TG --- SG
end
end
HA["Host A 10.0.1.0/24"] --> IA
HB["Host B 10.0.2.0/24"] --> IB
ISP["ISP"] <--> IG
A VRF is not a virtual router. A virtual router separates the control plane AND the data plane AND the management plane — it consumes its own CPU, its own memory, its own configuration, and its own copy of the OSPF process tree. A VRF separates the routing table. The kernel vrf driver multiplexes the data plane across VRFs in software; FRR runs one zebra process that maintains a separate RIB per VRF.
The distinction matters for resource planning. A site router can run dozens of VRFs inside the memory footprint of one FRR process. The same router with a per-tenant virtualisation plan would consume one VM or one container per tenant, which is a different order of cost in CPU and memory.
L3VPN and where VRFs come from
The L3VPN (Layer 3 Virtual Private Network) is the architectural pattern that made VRFs operationally important. RFC 4364 codifies it for service providers: a provider offers a “VPN” service to many customers, where each customer’s sites appear to be on a private network even though they share the provider’s MPLS backbone.
The primitives are:
- Customer Edge (CE) router — the customer-side router at each site.
- Provider Edge (PE) router — the provider-side router that carries the customer’s routes in a VRF.
- VRF — one VRF per customer on each PE.
- Route Distinguisher (RD) — an 8-byte value prepended to the customer’s
IPv4 or IPv6 routes so two customers who both use
10.0.0.0/8produce distinct VPNv4 / VPNv6 NLRI in BGP. - Route Target (RT) — an extended community that controls which VRFs import which routes out of the VPN RIB.
- MP-BGP VPNv4 / VPNv6 address family — the BGP address family that carries customer routes between PEs across the core.
On VyOS those last two are not free-standing commands. They live under the per-VRF BGP address family, and xv-03 configures them in full:
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
A VyOS router at the customer edge is usually not an MPLS L3VPN PE. It is, however, routinely expected to separate tenants on a single device: the same kernel model, the same FRR per-VRF routing instance, with no MPLS, no RD and no RT. Read the RD/RT vocabulary as the provider dialect of the same idea, and do not configure it when there is no VPN RIB to export into.
flowchart LR
subgraph PE1["PE-1"]
PA["VRF CUST-A<br/>rd 65000:1001"]
PB["VRF CUST-B<br/>rd 65000:1002"]
end
subgraph CORE["Provider core"]
MPLS["label switching only<br/>no per-customer state"]
end
subgraph PE2["PE-2"]
PA2["VRF CUST-A<br/>rt import 65000:1001"]
PB2["VRF CUST-B<br/>rt import 65000:1002"]
end
PA -->|"MP-BGP VPNv4"| MPLS
PB -->|"MP-BGP VPNv4"| MPLS
MPLS --> PA2
MPLS --> PB2
The Linux kernel vrf driver
The kernel implements VRFs as an l3mdev master. The model is:
- A master device of type
vrf, created withip link add NAME type vrf table ID. - Slave interfaces enslaved to that master. A slave’s packets carry the master’s table id into the FIB lookup.
- Routes installed with that table id are the VRF’s routes.
VyOS creates the master device with the VRF name as the interface name. There
is no prefix and no decoration: set vrf name CUST-A table 1001 produces a
Linux interface literally called CUST-A.
# What the commit performs, at the kernel level
ip link add CUST-A type vrf table 1001
ip link set dev eth1 master CUST-A
That naming is why the VyOS validator constrains VRF names the way it does. A
VRF name must be 15 characters or fewer (the kernel’s IFNAMSIZ limit minus
the terminator), must consist of letters, digits, - and _, and must not
begin with a string the box would read as an interface type — eth, bond,
br, dum, tun, vti, wg, lo and the rest. set vrf name eth-mgmt is
rejected for that reason, not for a policy reason.
A separate list of words is reserved outright, because they are ip command
keywords or the name of the global instance: add, all, broadcast,
default, delete, dev, down, get, inet, link, mtu, type,
up, vrf. default matters most of the three dozen rules in this lesson —
it is the name FRR uses for the global routing instance, and it is the value
you will pass to a route-leaking command in xv-03 to mean “the main table”.
The name is a label; the kernel’s identity for the VRF is the table id. VyOS accepts a table id in the range 100 to 65535, requires it on every VRF, rejects 254 explicitly because that is the main table, and refuses to change an id once the interface exists — the kernel does not allow a live vrf device to be re-tabled, so a change means deleting and recreating the VRF.
set vrf name CUST-A table 1001
set vrf name CUST-A description "Customer A tenant"
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 address 10.1.0.1/24
The set vrf name CUST-A table 1001 line creates the CUST-A l3mdev master
with table 1001. The set interfaces ethernet eth1 vrf CUST-A line enslaves
eth1 to it. The address line then installs its connected route in table
1001 rather than table 254, because the kernel derives the table from the
slave’s master.
How VyOS wires a vrf name to an FRR routing instance
The full chain when the operator commits set vrf name CUST-A table 1001 and
set interfaces ethernet eth1 vrf CUST-A:
- The
vrfnode commits early — it carries priority 11, ahead of every interface node — so the master device exists before any interface is asked to join it. - The commit script runs
ip link add CUST-A type vrf table 1001, adds127.0.0.1/8and::1/128to the VRF device so services bound to the loopback are reachable inside the VRF, applies the description as the interface alias, and brings the device up. - It adds an nftables element mapping the VRF name to its table id in
inet vrf_zones ct_iface_map, which gives each VRF its own conntrack zone. That is what allows two VRFs to use overlapping address space without their connection-tracking state colliding. - The interface node then enslaves
eth1, and the connected route for10.1.0.1/24lands in table 1001. - FRR receives the interface’s VRF membership from zebra and maintains a
CUST-Arouting instance. Anything the operator configures underset vrf name CUST-A protocols ...is rendered inside FRR’svrf CUST-Ablock and installs into table 1001.
No prefix from CUST-A reaches the main table unless somebody configures a
leak on purpose. xv-03 covers how that is done and, more usefully, what it
looks like when a leak is configured and quietly does nothing.
Why the engineering boundary exists
VRFs are a primitive rather than a convenience because they enforce isolation
without depending on operator discipline. An engineer cannot accidentally
route a packet from eth1 (bound to CUST-A) into CUST-B’s subnet,
because the lookup for an eth1 packet is directed at table 1001 by the
l3mdev rule and stopped by the l3mdev unreachable rule if it misses. The
boundary is enforced by the rule chain, not by somebody reading a config
carefully.
sequenceDiagram
autonumber
participant H as Host in 10.1.0.0/24
participant E as eth1 (slave of CUST-A)
participant K as Linux routing policy
participant V as table 1001
participant Z as zebra (FRR)
participant F as OSPF instance in CUST-A
H->>E: packet 10.1.0.50 to 10.1.0.99
E->>K: skb on eth1, master CUST-A
K->>V: rule 1000 l3mdev, lookup table 1001
V-->>K: connected route on eth1
K->>E: forward
E->>H: deliver
F-->>Z: OSPF Hello / LSA inside vrf CUST-A
Z->>V: install route in table 1001
Two separate physical routers enforce the same isolation through wiring: a packet on router A’s interface cannot reach router B’s interface. A VRF delivers the same guarantee on one device, with the l3mdev rules as the enforcement point.
How traffic actually flows
The decision path for a packet arriving on an interface, given the rule chain from the “Under the hood” box above:
flowchart TD
A["Packet arrives on an interface"] --> B{"Interface enslaved to a VRF?"}
B -- "master CUST-A" --> C["rule 1000 l3mdev: look up table 1001"]
B -- "no master" --> M["rule 32765 local, then 32766 main"]
C --> D{"Hit in table 1001?"}
D -- yes --> F["Forward per table 1001"]
D -- no --> E["rule 2000 l3mdev unreachable"]
E --> G["Reject: unreachable"]
M --> H{"Hit in local or main?"}
H -- yes --> I["Forward per the main table"]
H -- no --> G
The packet in a VRF context is never offered to the main table, because rule 2000 terminates the walk before rule 32766 is reached. There is no implicit “fall back to the global table” — building one is a deliberate act, and xv-03 shows the two supported ways to do it.
How the result is validated
# VyOS view
show vrf
show vrf CUST-A
show vrf CUST-A processes
show interfaces ethernet eth1
# Kernel view
ip vrf show
ip -d link show dev CUST-A
ip link show master CUST-A
ip route show table 1001
ip -6 route show table 1001
ip -4 rule show
# FRR view
vtysh -c 'show vrf'
vtysh -c 'show ip route vrf CUST-A'
vtysh -c 'show ip ospf vrf CUST-A neighbor'
The operational-mode form is show vrf CUST-A, with the VRF name as a
positional value. There is no name keyword in the operational tree even
though the configuration tree has one — show vrf name CUST-A is not a
command.
A working VRF has:
- The master device present and up.
- Every intended slave interface reporting
master CUST-A. - The VRF listed by
vtysh -c 'show vrf'as an FRR routing instance. - Connected routes present in the expected table id and absent from table 254.
- Where routing protocols are configured, adjacencies visible only in the per-VRF view.
How it fails
The failure modes an engineer must recognise:
- The interface is not enslaved. The configuration says
vrf CUST-Abutip -d link show dev eth1reports no master. Traffic on the interface is routed by the main table. The isolation is a fiction, and nothing in the VyOS view says so. - The table id is already taken. Two VRFs configured with the same
tablevalue fail validation withtable id is not unique. The commit is rejected as a whole, so the box is left on the previous configuration. - Somebody tries to re-table a live VRF. Changing
tableon an existing VRF is rejected: the id cannot be modified once the kernel device exists. The only route is to delete the VRF and add it back, which drops every interface binding and every route inside it. - A routing protocol is configured at the top level.
set protocols ospf area 0 network 10.1.0.0/24outsidevrf name CUST-Abuilds the adjacency oneth1but installs into the main table. Packets still flow — the connected route is in table 1001 either way — so the symptom is not loss of connectivity but loss of isolation. xv-03 covers this in detail. - A VRF is deleted while something still references it. VyOS refuses to remove a VRF that still has member interfaces, static routes, or policy routes, and refuses to remove one that another VRF imports from. The commit fails with the reference named; delete the reference in the same commit.
- Overlapping address space without the conntrack zone. Two VRFs using the same prefix rely on the per-VRF conntrack zone VyOS installs. If connection tracking is not in play for a flow, nothing separates two identical 5-tuples in different VRFs — which is a reason to keep NAT and stateful firewalling explicitly VRF-aware rather than assuming it.
Rollback
# Detach a single interface from a VRF
delete interfaces ethernet eth1 vrf
# Remove a VRF - the references must go in the same commit
delete interfaces ethernet eth1 vrf
delete vrf name CUST-A
commit
A safe rollback sequence:
compareto confirm the candidate diff is exactly the inverse of the forward change.committo apply the rollback.- Verify with
ip -d link showthat the master device is gone and no interface still reports it. - Verify with
ip -4 rule showthat the rule chain matches the box’s new state — with the last VRF removed,localreturns to priority 0 and the l3mdev rules disappear. saveonly after the rollback is confirmed working.
Production discipline
Cross-course references
- The Linux course’s
V-Linux-NetConfigandXXI-Linux-NetAdvancedcover the same l3mdev primitives from the host perspective — the vrf master device, thetableargument, andip vrf exec. - The OPNsense course covers a different isolation model (VLAN + interface + per-VLAN firewall) on FreeBSD; the operational goal is similar, the primitives are not.
- The VyOS lessons
vyos-xiv-01-routing-table-conceptthroughvyos-xiv-06-table-anti-patternscover the multi-table primitives that a per-VRF table id shares a namespace with. - The BGP course’s
XXVII-BGP-BestPathandXXX-BGP-RouteReflectorsdiscuss the MP-BGP VPNv4 / VPNv6 family that consumes the VRF concept in an L3VPN environment.
Quiz
Knowledge check · 4 questions
Q1. A packet arrives on an interface enslaved to VRF CUST-A and the destination is not in table 1001. What stops the kernel from resolving it out of the main table instead?
Q2. After `set vrf name CUST-A table 1001`, the Linux interface that represents the VRF is called CUST-A.
Q3. A VRF was built with `table 1001`. The address plan changed and the operator now wants it on `table 2001`. They edit the table id and commit. What happens, and what is the safe way to make the change?
CUST-A exists with table 1001, eth1 enslaved to it, OSPF running inside it. The operator runs `set vrf name CUST-A table 2001` and commits. The commit is rejected. The kernel device CUST-A is still present and still using table 1001, and the running configuration is unchanged.
Q4. An operator configures `set protocols ospf area 0 network 10.1.0.0/24` at the top level rather than under `vrf name CUST-A`. eth1 is enslaved to CUST-A. Where do the OSPF-learned routes install, and why is the symptom so easy to miss?
The intent was to run OSPF inside VRF CUST-A. The stanza was placed at the top of the configuration tree instead of under `vrf name CUST-A protocols ospf`. The adjacency comes up on eth1 and traffic to directly connected hosts works, so the change looks successful.
Passing score: 75%. Answers are checked in this browser.