VRF anti-patterns — VRFs for non-routing problems, overlapping space, leaks that install nothing, MTU
What you'll learn
- Decide whether a requirement calls for a VRF or for a VLAN plus firewall policy, and justify the answer in routing terms
- Explain why overlapping tenant address space is legal, why it works, and exactly what it costs the first time two tenants must share something
- Write a shared-service leak that installs a route, using the `vrf` next-hop leaf, and design one that does not need editing per tenant
- Set MTU and MSS on the interfaces that actually carry the overhead, and verify the result from inside the VRF
- Apply a review discipline to a multi-VRF estate before drift makes its behaviour unpredictable
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
- VRF troubleshooting — RIB versus FIB, `ip vrf exec`, and the leak that installs nothing
- Tunnel overhead — WireGuard 32-80, IPsec 50-66, GRE 24, VXLAN 50
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-15
A multi-VRF estate works on the day it is built. What breaks it is accretion: one more VRF, one more hand-written leak, one tunnel whose overhead nobody accounted for, one routing instance that drifted from the others. No single change is wrong enough to argue about, and the sum of them is an estate where every incident starts with twenty minutes of working out which VRF owns the failure.
This lesson catalogues the anti-patterns worth recognising early. Each one has the failure it produces, the command that exposes it, and the discipline that prevents it.
Anti-pattern 1: a VRF for a problem that is not about routing
The classic version: an edge router serving several tenants is rebuilt with a VRF per tenant because “we want isolation”. The requirement, examined, turns out to be that tenant A must not reach tenant B — which is a firewall requirement. VRFs solve routing-table separation. If the tenants share a routing topology and differ only in policy, a VRF is a heavyweight way to get a result a rule set already gives you.
flowchart TD
Q["Several tenants on one VyOS router"] --> Q1{"Different routing topology?<br/>Own default route, own peers, own ASN?"}
Q1 -- yes --> V["VRF is the right primitive"]
Q1 -- no --> Q2{"Requirement is<br/>strict routing-table isolation,<br/>or policy between known subnets?"}
Q2 -- "routing-table isolation" --> V
Q2 -- "policy" --> F["VLANs plus firewall groups"]
Q2 -- "one host or one small subnet" --> FG["A rule matching source address"]
A VRF earns its place when tenants need different default gateways, different BGP topology — their own peers, their own ASN, their own multihoming — or genuine routing-table isolation where no leakage to the global table is acceptable. It does not earn its place when the answer to “what is the routing-table reason this exists?” is “isolation”, unqualified.
The reason to be strict about this is that the cost is not paid once at build time. Every VRF adds a permanent obligation:
| Obligation | Per-VRF estate | VLANs plus firewall |
|---|---|---|
| Address space | must be planned to avoid overlap between tenants | one plan, one space |
| Routing | a routing instance per VRF, each with its own router-id and policy | one instance |
| Shared services | a leak per tenant per direction | ordinary routing |
| Firewall | interface groups per VRF, plus the inter-VRF policy | one chain |
| Monitoring | per-VRF route counters and session state | one view |
| On-call knowledge | every engineer must hold the per-VRF model | none extra |
For two tenants and a clear policy requirement, that table is the argument. For a service provider carrying tenants with their own routing, the same table is just the cost of the correct design.
Anti-pattern 2: overlapping address space across tenants
Two tenants both number their networks out of 10.0.0.0/24. They
are on different interfaces in different VRFs. The configuration is
symmetric and entirely legal:
set vrf name CUST-A table 1001
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 address 10.0.0.1/24
set vrf name CUST-B table 1002
set interfaces ethernet eth2 vrf CUST-B
set interfaces ethernet eth2 address 10.0.0.1/24
Nothing rejects this and nothing misbehaves. Each tenant’s hosts see
10.0.0.1 as their gateway, each lookup is scoped to its own table,
and the two never meet. Isolation is exactly what VRFs do.
The bill arrives the day something has to be shared. Overlapping space is not a problem inside isolation; it is a problem the moment isolation is relaxed, and relaxing isolation is what every shared-service, monitoring or migration project eventually asks for.
The discipline is a documented per-tenant allocation, assigned by the operator at onboarding rather than inherited from whatever the customer was already using:
# Tenant transit and routed space, allocated by the operator
# CUST-A 10.1.0.0/16 transit 10.1.0.0/24, CE at 10.1.0.254
# CUST-B 10.2.0.0/16 transit 10.2.0.0/24, CE at 10.2.0.254
# CUST-C 10.3.0.0/16 transit 10.3.0.0/24, CE at 10.3.0.254
#
# Shared services (VRF SHARED)
# transit 10.99.0.0/24 eth9, services router at 10.99.0.254
# services 10.250.0.0/16 DNS .0.53, NTP .0.123, monitoring .0.200
#
# IPv6
# CUST-A 2001:db8:1::/48
# CUST-B 2001:db8:2::/48
# SHARED 2001:db8:99::/48
Overlaps caught at design time cost a spreadsheet row. Overlaps caught in production cost a renumbering window on somebody else’s network.
Anti-pattern 3: a shared-service leak that installs nothing
Every tenant needs DNS, NTP and monitoring. Those live in one place. So each tenant VRF needs a route to them — and this is where the single most common VRF defect in production gets written:
# Commits without error. Forwards nothing.
set vrf name CUST-A protocols static route 10.250.0.0/16 next-hop 10.99.0.254
10.99.0.254 lives on eth9, which belongs to VRF SHARED. FRR is
being asked to resolve it inside CUST-A’s table, where it does not
exist. FRR keeps the route in the RIB, marks the next-hop
inactive, and never offers it to the kernel. There is no commit
error and no log line an operator would notice.
The correct statement names the VRF the next-hop should be resolved
in. On VyOS that is the vrf leaf under next-hop, and it renders
FRR’s nexthop-vrf:
configure
# The shared-services VRF
set vrf name SHARED table 9999
set interfaces ethernet eth9 vrf SHARED
set interfaces ethernet eth9 address 10.99.0.1/24
# Forward: CUST-A reaches the shared-service block through SHARED
set vrf name CUST-A protocols static route 10.250.0.0/16 next-hop 10.99.0.254 vrf SHARED
# Return: SHARED reaches CUST-A's routed block through CUST-A
set vrf name SHARED protocols static route 10.1.0.0/16 next-hop 10.1.0.254 vrf CUST-A
commit
save
Both directions, every time. A leak is one prefix in one direction; the reverse is a separate statement in the other VRF. Configuring only the forward direction produces a failure that looks like a remote problem — the request arrives at the service, the service answers, and the router has no route to send the answer back on.
$ 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:01:44 ago
* 10.99.0.254, via eth9 (vrf SHARED), weight 1Illustrative output
Confirm in the kernel, not only in FRR:
ip route show vrf CUST-A
ip route show vrf SHARED
If the prefix is not in that output, the leak does not exist as far as forwarded packets are concerned. The previous lesson covers the three distinct leak failures and how to tell them apart.
The part that does not scale
Written this way, the leak is correct. It is also one pair of statements per tenant. At fifty tenants you maintain a hundred statements, and adding a shared-service prefix means editing fifty VRFs — which is not a syntax problem, it is a design problem, and the anti-pattern is treating hand-written statics as the destination rather than the starting point.
Two things make it manageable, in order of how much machinery they cost:
- Leak one aggregate, not one prefix per service. If the shared
services all live inside
10.250.0.0/16, every tenant leaks that one prefix and a new service is added by numbering it inside the aggregate. Nothing on the tenant side changes. This is free, and it is the step most estates skip. - Let BGP do the leaking. FRR can import one VRF’s routes into
another’s BGP table with
import vrf NAMEinside the importing instance’saddress-family ipv4 unicast, filtered by a route-map, which turns “leak the shared services everywhere” into a policy rather than a per-tenant edit. VyOS surfaces this underset vrf name CUST-A protocols bgp address-family ipv4-unicast import vrf SHARED. It needs a BGP instance in each VRF involved. Before you build a design on it, commit it on your own 1.5 image and read back what FRR was actually given withvtysh -c 'show running-config'— the renderedimport vrfline is the thing to confirm, not the fact that the CLI accepted the node.
Part XVI develops the BGP-based form, including the filtering that stops a leak becoming a full-mesh.
Anti-pattern 4: leaking into the default VRF because it is easier
There is always a moment where the shortest path from a tenant to something it needs runs through the global table. The global table has the internet default, it has the management network, and — unlike a purpose-built SHARED VRF — it already exists.
The monitoring collector is the usual first case. It sits on the management network, which the router already reaches through the global table — so instead of routing it into SHARED, someone leaks straight to the global table’s gateway:
# Reaches the collector today. Also the shortest route to regretting it.
set vrf name CUST-A protocols static route 203.0.113.0/24 next-hop 198.51.100.1 vrf default
default is the name the leaf uses for the global table, and the
mechanism is identical to any other leak, so this works exactly as
well as leaking to SHARED. The problem is what default contains. A tenant VRF
leaked into default for one prefix is one careless summary away
from reaching the management network, the other tenants’ transit
links, and anything else the global table has learned. The blast
radius of a future mistake in the global table now includes every
tenant that has a leg in it.
The discipline:
- Shared services get their own VRF.
defaultis not a shared-services VRF; it is the router’s own operational context. - Leak the narrowest prefix that satisfies the requirement — the service aggregate, never a default route or a summary that happens to cover it.
- Write down, per tenant, which prefixes are leaked in each direction. The pair of statements is the mechanism; the document is the policy, and only one of the two gets reviewed.
- Re-derive that document from the router periodically rather than trusting it:
show configuration commands | match "protocols static route" | match "vrf"
Anti-pattern 5: MTU set on the interface that does not carry the overhead
The most operationally expensive anti-pattern, because it fails partially. Small packets work. Interactive sessions work. Large transfers stall, and the customer’s description — “the network is slow sometimes” — points at everything except the cause.
A tenant’s traffic crosses a tunnel somewhere on its path — the tunnel interface may be bound into the tenant’s VRF or reached from it, and the arithmetic is identical either way. The tunnel adds overhead: roughly 60 bytes for WireGuard over IPv4, 50 for VXLAN, 24 for GRE, and enough for IPsec that it has to be worked out per configuration rather than remembered. If the tunnel interface still carries the default MTU of 1500, a full-size packet entering it cannot leave it.
flowchart LR
H["Host in CUST-A<br/>MTU 1500"] --> E1["eth1 · VRF CUST-A<br/>MTU 1500"]
E1 --> W["wg0 · MTU 1500<br/>overhead ~60 bytes"]
W --> E2["eth0 · MTU 1500"]
E2 --> I["Internet"]
W -.-> X["1500-byte payload plus 60 bytes<br/>does not fit in a 1500-byte underlay"]
The reflex fix is to lower the LAN interface’s MTU. That is the wrong
interface. Lowering eth1 does not change what a host on the segment
sends unless the host learns the smaller MTU — and a host that
ignores the RA or DHCP option keeps sending 1500-byte frames. The
overhead is added by the tunnel, so the tunnel interface is where the
MTU belongs:
configure
# The tunnel carries the overhead, so the tunnel gets the MTU
set interfaces wireguard wg0 mtu 1420
# Clamp TCP where the flows enter, so sessions negotiate a workable MSS
set interfaces wireguard wg0 ip adjust-mss 1380
set interfaces wireguard wg0 ipv6 adjust-mss 1360
commit
save
MSS clamping handles TCP, which is most of what a customer notices. It does nothing for UDP or for anything that sets DF and expects Path MTU Discovery to work — which needs ICMP Fragmentation Needed and ICMPv6 Packet Too Big to survive the rule sets at both ends.
Verify from inside the VRF, which is the only place the answer is meaningful:
# A destination on the far side of the tunnel, reached the way the
# tenant reaches it
ip vrf exec CUST-A tracepath 192.0.2.50
ip vrf exec CUST-A ping -M do -s 1392 192.0.2.50
The second is the one that gives a straight answer: with -M do the
packet is not allowed to fragment, so it either arrives or reports
back the MTU that would have worked.
Anti-pattern 6: drift across per-VRF routing instances
A multi-VRF estate has one routing instance per VRF, and they start identical. Over a year, one gains a redistribution the others do not have, one has authentication, two have timers that were tuned during an incident and never normalised. The result is an estate where the answer to “what will this change do?” is “it depends which VRF”.
Drift is not a syntax problem, so no check catches it. The discipline is a written standard per estate and a periodic diff against it:
# Same shape in every tenant VRF; only the identifiers differ
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
set vrf name CUST-B protocols ospf parameters router-id 10.2.0.1
set vrf name CUST-B protocols ospf area 0 network 10.2.0.0/24
The standard should fix, at minimum:
- A unique router-id per instance, always set explicitly. Never left for FRR to derive — that is how two VRFs end up sharing one, and a router-id collision produces an adjacency that forms and then will not stay up.
- The same area layout in every tenant VRF. Multi-area OSPF inside a tenant VRF is rare enough that its presence in one VRF and not the others is usually drift rather than design.
- The same authentication posture everywhere, with the syntax
taken from
vyos-liii-01-routing-protocol-authenticationrather than copied between VRFs by hand. - The same redistribution. A
redistribute connectedin one tenant VRF and not another means one tenant sees your transit links and the rest do not.
Re-derive the comparison from the running configuration, not from what you remember deploying:
show configuration commands | match "vrf name" | match "protocols ospf"
Anti-pattern 7: a rollback that has never been run
Every VRF change in this course carries a rollback. A rollback that has not been executed in a lab is a plan, not a rollback, and the difference shows up at the worst possible time.
# Remove a leak
delete vrf name CUST-A protocols static route 10.250.0.0/16
commit
save
# Remove a routing instance, keep the VRF
delete vrf name CUST-A protocols ospf
commit
save
# Remove a VRF: release its interfaces in the same candidate configuration,
# or the commit is rejected while an interface still names it
delete interfaces ethernet eth1 vrf
delete vrf name CUST-D
commit
save
How these compound
The progression is the same in most estates, and each step is a consequence of the previous one going unaddressed:
- Build. VRFs added per tenant. Configuration is uniform and correct.
- First tunnel. MTU set on the LAN interface instead of the tunnel. Large transfers stall intermittently; the tickets say “slow”, and nobody connects them.
- First shared service. A leak per tenant, hand-written. Half of
them omit the
vrfleaf and are quietly inert; the tenants that work mask the ones that do not. - Expedient leak. One tenant is leaked into
defaultbecause the SHARED VRF did not have what it needed that week. Nobody writes it down. - New tenant. Their existing addressing overlaps an established tenant. Because there is no allocation document, this is discovered during the onboarding window.
- Drift. A routing change works in most VRFs and fails in a few. The estate now has behaviour nobody can predict from reading the configuration.
Each stage is cheap to prevent at the stage before, and expensive at every stage after.
Production discipline
Cross-course references
- The Linux course’s
V-Linux-NetConfigcovers the same l3mdev primitives from the host side. - The OPNsense course’s VLAN-plus-firewall isolation pattern is the alternative anti-pattern 1 points at.
vyos-xv-05-vrf-troubleshootcovers the three leak failure signatures and the RIB-versus-FIB check this lesson relies on.vyos-xvi-01-leaking-conceptand the rest of Part XVI develop route leaking properly, including BGP-based leaking and its security implications.vyos-li-02-tunnel-overheadandvyos-li-04-mss-clampingcarry the MTU arithmetic anti-pattern 5 summarises.vyos-liii-01-routing-protocol-authenticationis the reference for the authentication syntax anti-pattern 6 says to standardise on.
Quiz
Knowledge check · 4 questions
Q1. One VyOS router serves two small offices that share the same upstream, the same default route and the same address plan, and differ only in what each is allowed to reach. An engineer proposes a VRF per office. What is the right critique?
Q2. If CUST-A already has 10.0.0.0/24 as a connected route and you leak CUST-B's overlapping 10.0.0.0/24 into CUST-A, the leak commits successfully and then has no effect, because the connected route has a lower administrative distance and stays best.
Q3. A ten-tenant estate has a shared-service leak configured in every tenant VRF. Six tenants can reach DNS and NTP; four cannot, with no configuration error and no alarm. Find the defect, fix it, and propose the design that stops it recurring.
The shared services live in 10.250.0.0/16, reachable via the services router at 10.99.0.254 on eth9, which is enslaved to VRF SHARED. The six working tenants have: - set vrf name CUST-A protocols static route 10.250.0.0/16 next-hop 10.99.0.254 vrf SHARED The four failing tenants have: - set vrf name CUST-G protocols static route 10.250.0.0/16 next-hop 10.99.0.254 All ten committed without error when they were written, months apart, by different engineers.
Q4. Tenant traffic crosses a WireGuard tunnel. Large transfers stall while interactive sessions are fine. An engineer lowers the tenant LAN interface's MTU and the problem persists for some hosts. Explain why that fix was incomplete and what the correct change is.
The configuration: - set vrf name CUST-A table 1001 - set interfaces ethernet eth1 vrf CUST-A - set interfaces ethernet eth1 address 10.1.0.1/24 - set interfaces wireguard wg0 address 10.255.0.1/32 - set interfaces wireguard wg0 mtu 1500 The engineer then adds `set interfaces ethernet eth1 mtu 1420`. Some hosts improve; several, including the ones doing the large transfers, do not.
Passing score: 75%. Answers are checked in this browser.