Skip to main content
RunBook Academy

VyOSXVI · Route Leaking Between VRFsLeaking

Route leaking configuration — static next-hop-vrf, BGP import vrf, filtering

Advanced⏱ ~26 minset vrf name protocols static routeset protocols static routeset vrf name protocols bgpset policy prefix-listset policy route-mapshow ip route vrf VRFNAMEshow bgp vrf VRFNAME ipv4 unicastip route show table TABLEIDvtysh -c show running-config

What you'll learn

  • Configure a static leak in both the interface form and the next-hop-address form
  • Configure the reverse leak, and explain why it is a separate statement in a different subtree
  • Configure a BGP leak with import vrf, including originating the prefixes into the source BGP RIB
  • Filter what crosses with a prefix-list and a route-map, and say which side the filter is on
  • Verify a leak in the receiving kernel table before believing anything FRR prints

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

Not yet marked complete on this device.

Route leaking configuration — static next-hop-vrf, BGP import vrf, filtering

Lesson 1 established what a leak is: a route installed in one table whose next hop is deliberately resolved in another. This lesson is the configuration walk-through — the two mechanisms VyOS 1.5 LTS exposes, in both directions, with the filtering and the verification that make them reviewable.

The verification is not an appendix. A leak is the one change in this course where a clean commit proves nothing, so every pattern below ends in the kernel table of the context that is supposed to receive the route.

The reference estate

One VyOS 1.5 router, three routing contexts, carried forward from lesson 1.

Routing contextKernel tableInterfaceAddressing
default (the main table)254eth0192.0.2.2/30, transit to the service router
mgmt1001eth110.10.0.1/24
tenant-a1002eth210.20.0.1/24

eth0 is a transit link to the service router at 192.0.2.1. Behind it sit DNS 198.51.100.53, NTP 198.51.100.123 and syslog 198.51.100.200, and the default context reaches all of 198.51.100.0/24 through one static route:

set protocols static route 198.51.100.0/24 next-hop 192.0.2.1

That the services are behind a router rather than on a segment this box owns is the ordinary case, and it decides which form of the static leak each direction needs.

flowchart TB
  subgraph VyOS["VyOS 1.5 LTS - single chassis"]
    G["default context<br/>table 254<br/>eth0 192.0.2.2/30"]
    M["vrf mgmt<br/>table 1001<br/>eth1 10.10.0.1/24"]
    T["vrf tenant-a<br/>table 1002<br/>eth2 10.20.0.1/24"]
  end
  CORE["service router<br/>192.0.2.1"]
  SEG["198.51.100.0/24<br/>DNS .53 - NTP .123 - syslog .200"]
  G --- CORE
  CORE --- SEG

Pattern 1 — the static leak

1a. The forward leak: a next-hop address, resolved elsewhere

The services are behind 192.0.2.1, so the leak has to name that address — and the vrf value tells FRR which context to resolve it in. Three services, two consumer VRFs, six statements.

configure
set vrf name mgmt protocols static route 198.51.100.53/32 next-hop 192.0.2.1 vrf default
set vrf name mgmt protocols static route 198.51.100.123/32 next-hop 192.0.2.1 vrf default
set vrf name mgmt protocols static route 198.51.100.200/32 next-hop 192.0.2.1 vrf default

set vrf name tenant-a protocols static route 198.51.100.53/32 next-hop 192.0.2.1 vrf default
set vrf name tenant-a protocols static route 198.51.100.123/32 next-hop 192.0.2.1 vrf default
set vrf name tenant-a protocols static route 198.51.100.200/32 next-hop 192.0.2.1 vrf default
commit
save

Each statement renders to FRR as one ip route line inside the VRF’s block:

vrf mgmt
 ip route 198.51.100.53/32 192.0.2.1 nexthop-vrf default
 ip route 198.51.100.123/32 192.0.2.1 nexthop-vrf default
 ip route 198.51.100.200/32 192.0.2.1 nexthop-vrf default
exit-vrf

nexthop-vrf default is the entire mechanism: resolve 192.0.2.1 in the default context — where 192.0.2.0/30 is connected on eth0 — and install the resulting route in mgmt’s table.

Note what happens if the vrf default is dropped. The statement still commits. It still reads correctly. FRR resolves 192.0.2.1 inside mgmt, finds table 1001 holding only the tenant’s connected route, holds the static inactive, and never offers it to the kernel.

1b. When there is no next-hop address worth naming

The vrf leaf sits under interface as well as under next-hop, and the interface form is the right one whenever the far side is directly connected in the other context. That is not the case for the forward leak here — the services sit behind 192.0.2.1 rather than on the link — but it is exactly the case for the reverse leak, which is the next section.

# The shape, for reference: install in this VRF, egress an interface that
# belongs to another one
set vrf name VRFNAME protocols static route PREFIX interface INTERFACE vrf CONTEXT

The two forms are not interchangeable. Naming interface eth0 for 198.51.100.53/32 would tell the kernel the service is on the link, which it is not, and ARP for it would go unanswered.

1c. The reverse leak

The forward leak makes the service reachable from mgmt. The service’s replies are resolved in table 254, which has never heard of 10.10.0.0/24. That is a second statement, and it lives in the top-level tree because the default context has no vrf name node.

configure
set protocols static route 10.10.0.0/24 interface eth1 vrf mgmt
set protocols static route 10.20.0.0/24 interface eth2 vrf tenant-a
commit
save
ip route 10.10.0.0/24 eth1 nexthop-vrf mgmt
ip route 10.20.0.0/24 eth2 nexthop-vrf tenant-a

Here the interface form is right: 10.10.0.0/24 is directly connected on eth1 inside mgmt, and there is no next-hop address that would mean anything. The same vrf leaf, the other subtree, the other spelling.

1d. Verifying pattern 1

Kernel table first. This is the only view that does not depend on knowing the current FRR output format, and it is the one that settles the question.

Read-only / Safekernel truth - mgmt
$ ip route show table 1001
10.10.0.0/24 dev eth1 proto kernel scope link src 10.10.0.1
198.51.100.53 via 192.0.2.1 dev eth0 proto static
198.51.100.123 via 192.0.2.1 dev eth0 proto static
198.51.100.200 via 192.0.2.1 dev eth0 proto static

Illustrative output

That is the signature of a leaked route and nothing else produces it: a route in table 1001 whose output device is eth0, an interface that belongs to the default context. ip route show vrf mgmt prints the same table if you prefer the VRF name to the table id.

The reverse direction has the mirror signature in table 254:

Read-only / Safekernel truth - default
$ ip route show table 254
192.0.2.0/30 dev eth0 proto kernel scope link src 192.0.2.2
198.51.100.0/24 via 192.0.2.1 dev eth0 proto static
10.10.0.0/24 dev eth1 proto static
10.20.0.0/24 dev eth2 proto static

Illustrative output

FRR’s view comes second, and it is worth learning mainly for the failure case. In show ip route vrf mgmt, a route that is selected and installed carries > and * in its code column; a route FRR is holding because the next hop did not resolve carries neither, and the next hop is flagged as not resolving. The exact wording of that flag differs between FRR releases, so read the markers rather than the sentence.

Read-only / Safecontrol-plane view
$ vtysh -c 'show ip route vrf mgmt 198.51.100.53/32'
VRF mgmt:
S>* 198.51.100.53/32 [1/0] via 192.0.2.1, eth0 (vrf default)

Illustrative output

A leak that did not install shows the same prefix without > and *. If you are ever unsure which you are looking at, go back to ip route show table 1001; the route is either in the table or it is not.

What you cannot do to a static leak

The natural next thought is to leak one broad prefix and filter it down — leak 0.0.0.0/0 and attach a route map that permits only the three service /32s. That does not exist, and it could not work.

A route map applied to a route can permit it, deny it, or change its attributes. It cannot turn one route into a different set of routes. A permitted 0.0.0.0/0 is still 0.0.0.0/0; a denied one is nothing. There is no intermediate outcome where a default route becomes three host routes, and no route-map leaf under a static route in the VyOS tree that would offer one.

Pattern 2 — BGP import vrf

When the set of prefixes is large, or learned rather than known, BGP does the same job dynamically. Each routing context gets its own BGP instance, and the importing instance copies paths out of the exporting instance’s RIB.

flowchart LR
  subgraph GlobalCtx["default context"]
    BG["bgp instance<br/>system-as 65000<br/>network 198.51.100.0/24"]
  end
  subgraph MgmtCtx["vrf mgmt"]
    BM["bgp instance<br/>system-as 65000<br/>network 10.10.0.0/24<br/>import vrf default"]
  end
  BG -->|"import vrf default"| BM
  BM -->|"import vrf mgmt"| BG

There is no BGP session here and no neighbour statement. Both instances live in the same FRR process and the import is an internal copy between RIBs. That is worth saying explicitly because material that describes inter-VRF leaking as “an iBGP session between the two VRFs over an internal link” is describing a topology nobody builds on a single chassis and that VyOS does not require.

configure
# An instance in each context. Both need a system-as; the number may be identical.
set protocols bgp system-as 65000
set protocols bgp parameters router-id 192.0.2.2
set vrf name mgmt protocols bgp system-as 65000
set vrf name mgmt protocols bgp parameters router-id 10.10.0.1

# Put the prefixes into each instance's BGP RIB in the first place
set protocols bgp address-family ipv4-unicast network 198.51.100.0/24
set vrf name mgmt protocols bgp address-family ipv4-unicast network 10.10.0.0/24

# Import, in each direction
set vrf name mgmt protocols bgp address-family ipv4-unicast import vrf default
set protocols bgp address-family ipv4-unicast import vrf mgmt
commit
save

The network statements are the part that gets left out, and leaving them out is the second-most-common way to build a leak that does nothing. import vrf copies BGP paths. A connected route lives in zebra’s RIB, not in BGP’s, until a network statement or a redistribute puts it there. Import a context whose BGP instance holds nothing and you import nothing — correctly, and without an error, because an empty import is a valid state.

Filtering what crosses

import vrf default on an internet-facing router imports whatever the default instance’s RIB holds, which on a box with a transit session is the whole table. That is not a leak, it is a merge. Two filters are available, and they sit on different sides.

Filter at the source, by originating exactly what may cross. This is the filter that cannot be renamed out from under you, because it is the absence of a route rather than the presence of a policy:

set protocols bgp address-family ipv4-unicast network 198.51.100.0/24

One statement, one prefix, and nothing else in the default instance’s RIB is a candidate for import unless something else put it there.

Filter on redistribution, when network is impractical. A redistribute widens the RIB the moment a new route of that kind appears, so pair it with a route map:

set policy prefix-list SHARED-SERVICES rule 10 action permit
set policy prefix-list SHARED-SERVICES rule 10 prefix 198.51.100.0/24

set policy route-map SHARED-SERVICES-ONLY rule 10 action permit
set policy route-map SHARED-SERVICES-ONLY rule 10 match ip address prefix-list SHARED-SERVICES

set protocols bgp address-family ipv4-unicast redistribute static route-map SHARED-SERVICES-ONLY

A VyOS prefix-list rule is two statements — an action and a prefix — and the list ends in an implicit deny, so there is no final deny any rule to write. A route map is the same shape: action on the rule, then the match conditions.

Verifying pattern 2

The order is: is the path in the source instance, is it in the receiving instance, is it in the receiving kernel table. Skipping to the third and finding nothing tells you the leak failed but not where.

show bgp vrf default ipv4 unicast
show bgp vrf mgmt ipv4 unicast
show ip route vrf mgmt

then the kernel:

ip route show table 1001
Read-only / Safereceiving BGP RIB
$ vtysh -c 'show bgp vrf mgmt ipv4 unicast'
   Network          Next Hop            Metric LocPrf Weight Path
*> 10.10.0.0/24     0.0.0.0                  0         32768 i
*> 198.51.100.0/24  0.0.0.0@1                0    100      0 i

Illustrative output

The imported prefix is the one whose next hop carries an instance qualifier. FRR annotates the next hop of an imported path with the routing context it came from, so an entry that otherwise looks locally originated is an import rather than something this instance learned. The exact notation differs between FRR releases, so read it as a hint and settle the question in the kernel table.

An empty show bgp vrf mgmt ipv4 unicast after a clean commit almost always means the source instance had nothing to give. Check show bgp vrf default ipv4 unicast before you touch the import statement.

Pattern 3 — RD and RT, and when they belong

The RFC 4364 mechanism is the right answer when the VRFs live on different routers that exchange routes over an MPLS or VXLAN underlay, and the VyOS box is a genuine provider-edge router.

# Per-VRF: how this VRF's routes are stamped into and selected out of the VPN RIB
set vrf name tenant-a protocols bgp system-as 65000
set vrf name tenant-a protocols bgp parameters router-id 10.20.0.1
set vrf name tenant-a protocols bgp address-family ipv4-unicast rd vpn export '65000:1002'
set vrf name tenant-a protocols bgp address-family ipv4-unicast route-target vpn both '65000:1002'
set vrf name tenant-a protocols bgp address-family ipv4-unicast export vpn
set vrf name tenant-a protocols bgp address-family ipv4-unicast import vpn

# Top level: the session that actually carries the VPN routes to the other PE
set protocols bgp system-as 65000
set protocols bgp parameters router-id 192.0.2.2
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

The split is the part to memorise. rd, route-target, export vpn and import vpn describe how one VRF’s routes enter and leave the VPN RIB. The ipv4-vpn neighbour describes how that RIB reaches another router, and it lives in the default instance. Putting the VPN neighbour inside the VRF does not build an L3VPN; it builds an ordinary session in the tenant’s routing context.

How it fails

The production failure modes, in the order they are worth checking:

  • The vrf value is missing from a static leak. Commits, reads correctly, installs nothing. ip route show table 1001 has no line for the prefix. The only fix is the vrf value; nothing about the firewall or the next hop’s reachability is relevant yet.
  • The vrf value names the wrong context. vrf tenant-a where default was meant. The next hop does not resolve there either, so the symptom is identical to the previous one and the configuration looks even more convincing.
  • Only one direction was configured. The forward route is in the receiving table and traffic still fails. Check the other context’s table before the firewall.
  • import vrf with an empty source RIB. No network, no redistribute in the exporting context. show bgp vrf default ipv4 unicast is empty and the import is doing exactly what it was asked.
  • import vrf with no filter. The default context’s whole BGP RIB lands in the tenant. On a router with a transit session that is a full table in a VRF that was supposed to see three services.
  • A network statement with no matching route. network 198.51.100.53/32 when only a /24 is connected. BGP advertises nothing, and the import that depends on it is empty.
  • A route map that references a renamed prefix-list. The match never succeeds, so a permit rule behaves as a deny and the leak silently empties. show policy route-map and show policy prefix-list are the two commands that catch it.
  • VPN commands and import vrf on one address family. The commit is rejected, naming both commands. Pick one; on a single chassis it is import vrf.
  • A VRF that cannot be deleted. Another context imports from it, or its static leaks still reference it. Remove the dependent side in the same commit.

Rollback

configure
# Remove one static leak and its reverse, in one commit
delete vrf name mgmt protocols static route 198.51.100.53/32
delete protocols static route 10.10.0.0/24

# Remove a BGP leak - both sides in the same commit, or the remaining
# side references a context it can no longer import
delete vrf name mgmt protocols bgp address-family ipv4-unicast import vrf default
delete protocols bgp address-family ipv4-unicast import vrf mgmt

commit
save
  • compare before every commit, and read the whole diff. A leak removal frequently trips a reference check somewhere else in the tree.
  • commit-confirm when the leak carries the path your own session uses.
  • After commit, verify the absence the same way you verified the presence: the receiving kernel table should no longer carry a route with a foreign output device.
  • rollback N inside configure when the change is larger than a couple of statements. Roll back to a revision that was tested, not merely to the most recent one.

Production discipline

Cross-course references

vyos-xv-02-vrf-config covers VRF creation, table ids and the reference checks that block a delete. vyos-xv-03-vrf-routing-protocols introduces both leak mechanisms alongside per-VRF OSPF and BGP. vyos-xvi-03-leaking-firewall is the firewall half of every pattern here. The BGP parts XXIV-BGPSession, XXV-BGPAdvertise and XXVIII-BGPFiltering cover origination and policy in depth; the Linux course’s XIX-Linux-NetFoundations covers the kernel routing tables the VRF tree wraps.

Quiz

Knowledge check · 4 questions

  1. Q1. The syslog collector 198.51.100.200 is reached from the default context via the service router 192.0.2.1 on eth0. Which statement leaks it into the mgmt VRF?

  2. Q2. `import vrf default` moves BGP paths between BGP RIBs, so a connected route in the default context is not importable until a `network` statement or a `redistribute` puts it into that context's BGP instance.

  3. Q3. An operator wants the three shared-service /32s in mgmt but not the rest of 198.51.100.0/24. They configure `set vrf name mgmt protocols static route 0.0.0.0/0 next-hop 192.0.2.1 vrf default` and a route map permitting only the three /32s. Nothing filters. What went wrong?

    The route map was written and committed, and the leak works - too well. Table 1001 now has a default route whose output device is eth0, so every destination the default context can route to is reachable from mgmt, including the service router itself, the rest of 198.51.100.0/24, and whatever lies beyond it. The route map is attached to nothing that could act on it, and even if it were, permitting 0.0.0.0/0 would still leave 0.0.0.0/0.

  4. Q4. A BGP leak is configured in both directions between the default context and mgmt. Both instances have a system-as and a router id, both `import vrf` statements are present, and the commit was clean. `show bgp vrf mgmt ipv4 unicast` is empty. What is missing, and how do you tell it apart from a broken import?

    The default context has 192.0.2.0/30 connected on eth0 and a static route to 198.51.100.0/24 via 192.0.2.1. No `network` statement and no `redistribute` was configured in the default instance. The mgmt instance is importing correctly from a context whose BGP RIB holds nothing at all.

Passing score: 75%. Answers are checked in this browser.