Linux VRFs: Route Isolation Without the Namespace Overhead

Network namespaces give complete isolation — separate routing tables, separate interfaces, separate everything, down to separate iptables/nftables rule sets. That is sometimes more than you need. Linux VRFs (Virtual Routing and Forwarding) give you routing table separation — multiple independent routing tables on the same network stack — without the overhead of a full namespace.

If you have configured FortiGate VDOMs or VRF-lite on Cisco IOS, the concept will be immediately familiar. This post covers the Linux implementation: the l3mdev model, creating and binding VRFs, route leaking between them, and when a VRF is the right tool versus reaching for a namespace instead.

The l3mdev Model

A Linux VRF is implemented as a special interface type — vrf — that acts as an L3 master device. Other interfaces are enslaved to it. Once enslaved, an interface’s traffic is routed using the VRF’s private routing table rather than the main table.

This is the same underlying mechanism as a regular Linux bridge enslaving interfaces for L2, except the VRF device operates at L3 and routes rather than switches.

Each VRF device is tied to its own kernel routing table, identified by a table ID you choose:

# Create a VRF device bound to table 10
ip link add vrf-blue type vrf table 10
ip link set dev vrf-blue up

Enslaving Interfaces

# Bring an interface under the VRF
ip link set dev eth1 master vrf-blue

# Verify
ip link show vrf-blue
ip -d link show eth1 | grep master

Once enslaved, eth1’s routes live in table 10, not the main table:

ip route add default via 192.168.10.1 dev eth1 table 10
ip route show table 10

Traffic arriving on or destined for eth1 is routed using table 10’s contents — completely independent of whatever the main table says about 0.0.0.0/0.

Running Commands Inside a VRF Context

Unlike namespaces, there is no separate process view — all processes on the host share one process table. But you can force a single command to use a specific VRF’s routing table with ip vrf exec:

ip vrf exec vrf-blue ping 8.8.8.8
ip vrf exec vrf-blue ssh [email protected]
ip vrf exec vrf-blue curl https://api.example.com

This works via cgroup-based socket binding under the hood — ip vrf exec runs the command in a cgroup tagged with the VRF, and the kernel consults the VRF’s table for any socket created within that cgroup.

Listing VRFs and their members

ip vrf show
Name              Table
-----------------------
vrf-blue          10
vrf-green         20
ip vrf show vrf-blue

A Practical Multi-Tenant Example

Three VRFs replicating a small MSSP-style setup — three customers, each with their own default route, sharing one physical host:

# Create three VRFs
for i in 10 20 30; do
    ip link add vrf-cust$i type vrf table $i
    ip link set dev vrf-cust$i up
done

# Enslave a dedicated interface per customer
ip link set dev eth1 master vrf-cust10
ip link set dev eth2 master vrf-cust20
ip link set dev eth3 master vrf-cust30

# Assign addresses
ip addr add 192.168.10.2/24 dev eth1
ip addr add 192.168.20.2/24 dev eth2
ip addr add 192.168.30.2/24 dev eth3

# Per-customer default routes
ip route add default via 192.168.10.1 dev eth1 table 10
ip route add default via 192.168.20.1 dev eth2 table 20
ip route add default via 192.168.30.1 dev eth3 table 30

Each customer’s traffic is fully routed in isolation. A misconfigured static route in vrf-cust10 cannot leak into vrf-cust20’s table — they are entirely separate routing domains, even though all three share the same kernel, the same nftables process namespace, and the same physical CPU and memory.

Route Leaking Between VRFs

Sometimes isolation needs an exception — a shared services VRF that every customer VRF needs to reach (DNS, NTP, a monitoring collector). Two mechanisms:

Static route leaking with table references

# In vrf-cust10's table, add a route to the shared-services subnet
# pointing at the shared VRF's interface
ip route add 10.100.0.0/24 dev vrf-shared table 10

This requires the destination subnet to be reachable on the target VRF’s table and a route back for the response — leaking is bidirectional by necessity.

Route leaking via a Linux VRF + BGP (FRRouting)

For anything beyond a couple of static exceptions, run FRRouting with VRF-aware BGP — each VRF gets its own BGP instance (or a single multi-VRF instance with import/export route-target policies), structurally identical to how MPLS L3VPN or FortiGate inter-VDOM BGP peering leaks routes between routing domains.

! In FRR's vtysh, per-VRF BGP instance
router bgp 65010 vrf vrf-cust10
 address-family ipv4 unicast
  redistribute connected
  import vrf vrf-shared
exit-address-family

import vrf vrf-shared pulls routes from the shared VRF’s BGP table into this one — the Linux equivalent of an FortiGate VDOM-to-VDOM BGP leak, or an MPLS VRF importing a route-target from a shared services VRF.

VRF-Aware Services

A VRF only affects routing, not application binding — by default, a service bound to 0.0.0.0 listens across every VRF, which usually is not what you want for a multi-tenant box. Two fixes:

Bind explicitly to a VRF-enslaved interface’s address

# sshd listening only on the vrf-cust10 interface address
ListenAddress 192.168.10.2

Use SO_BINDTODEVICE / VRF-aware sockets

Some daemons (recent OpenSSH, FRRouting, BIRD) understand VRFs natively and accept a vrf parameter directly:

# OpenSSH 8.0+ — bind to a VRF
sshd -o BindInterface=vrf-cust10

Where native support does not exist, ip vrf exec wrapping the daemon’s startup is the fallback — though this only affects outbound sockets the process creates after exec, not necessarily a pre-bound listening socket created before the VRF context was applied.

VRF vs Namespace: When to Use Which

VRFNetwork Namespace
Routing table isolationYesYes
Interface/link isolationNo — shared link namespaceYes — fully separate
Separate nftables/iptables rulesNo — shares the host’s tables (use table matches)Yes — fully separate rule sets
Separate process viewNoYes (with PID namespace)
OverheadMinimal — just a routing table and an l3mdev deviceHigher — full network stack per namespace
Cross-domain route leakingNative and straightforward (shared kernel, just routing)Requires veth pairs + explicit forwarding
Closest real-world analogyVRF-lite on Cisco IOS, FortiGate VDOM (routing aspect only)FortiGate VDOM (full isolation aspect)

The rule of thumb: if the goal is purely “these subnets must not be able to route to each other by accident,” a VRF is lighter-weight and gives you native, simple route leaking when you need a controlled exception. If the goal is full separation — including firewall rules, namespaced interfaces, or even separate processes — use a network namespace, or a VRF nested inside one for defense in depth.

Cleanup

ip link set dev eth1 nomaster
ip link set dev vrf-blue down
ip link delete vrf-blue

Unsetting master returns the interface to the main routing table immediately — useful for testing without committing to a permanent topology change.

  • Linux network namespaces — stronger isolation than VRFs; the right choice when you need fully independent network stacks rather than just routing table separation
  • Linux ip command — the ip rule and ip route table mechanics that underpin VRF routing; VRFs automate what you would otherwise build by hand with policy routing rules
  • FortiGate VDOMs deep dive — the FortiOS equivalent of full namespace-style isolation; understanding the Linux VRF vs namespace distinction maps directly onto the VRF vs VDOM distinction in FortiOS