IPv6 inside a VRF — link-local next-hops, OSPFv3 and BGP per VRF, leaking route6
What you'll learn
- Configure IPv6 on a VRF-bound interface and prove the connected routes land in the per-VRF table
- Explain why the interface, not the VRF, is what disambiguates an IPv6 link-local address, and what the VRF actually changes
- Configure OSPFv3 and BGP IPv6 under `vrf name` and read the per-VRF `show` output they produce
- Leak an IPv6 prefix between VRFs with `protocols static route6 ... next-hop ADDR vrf OTHER`, and recognise the leak that installs nothing
- Recognise the IPv6-specific failure modes that have no IPv4 equivalent (ND filtering, missing router-id, link-local next-hops)
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
- IPv6 fundamentals — 128-bit addressing, headers, ICMPv6, NDP
- IPv6 addressing — global unicast, ULA, link-local, multicast, anycast, prefix delegation
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
IPv6 in a VRF uses the same machinery as IPv4 in a VRF. An interface is enslaved to an l3mdev master, the master carries a table id, and every lookup for a packet on that interface is scoped to that table. Nothing about that is protocol-specific.
What is protocol-specific is the set of things IPv6 does that IPv4 does not, and each of them changes what you see in the per-VRF table:
- IPv6 IGP next-hops are link-local. OSPFv3 installs routes whose
next-hop is
fe80::..., not a global address. A link-local next-hop is only meaningful together with its interface — which has direct consequences for what you can and cannot leak into another VRF. - Neighbour Discovery is ICMPv6, so the firewall can break it.
IPv4 gets its L2 resolution from ARP, which the IP firewall never
sees. IPv6 gets it from ICMPv6 packets that an
ipv6 namerule set can drop, inside the VRF, silently. - An IPv6-only VRF has no IPv4 address to borrow a router-id from. FRR router-ids are 32-bit values written in dotted-quad. A VRF that carries only IPv6 gives FRR nothing to derive one from, and the routing instance refuses to start until you set one.
This lesson covers the configuration, the per-VRF verification, IPv6 leaking between VRFs, and the failure modes.
IPv6 addresses on a VRF-bound interface
The grammar mirrors IPv4 exactly — ipv6 address instead of
address:
configure
set vrf name CUST-A table 1001
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 ipv6 address 2001:db8:1::1/64
set interfaces ethernet eth1 ipv6 address 2001:db8:2::1/64
commit
save
The commit renders roughly these kernel operations, in this order:
ip link add vrf-CUST-A type vrf table 1001
ip link set vrf-CUST-A up
ip link set eth1 master vrf-CUST-A
ip -6 addr add 2001:db8:1::1/64 dev eth1
ip -6 addr add 2001:db8:2::1/64 dev eth1
The order is not cosmetic. Enslaving a device to a master is a link
event: the kernel tears the interface’s IPv6 addrconf state down and
rebuilds it around the change. Configure the VRF and the binding
first, the addresses after — which is what VyOS does for you when
the whole change is in one commit. If you ever move an
already-addressed interface into a VRF, re-check ip -6 addr show dev eth1 before you believe the addresses survived.
$ ip -6 addr show dev eth13: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 2001:db8:1::1/64 scope global
valid_lft forever preferred_lft forever
inet6 2001:db8:2::1/64 scope global
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe11:1111/64 scope link
valid_lft forever preferred_lft foreverIllustrative output
Where the IPv6 routes land
The interesting output is not the address list — it is the table.
$ ip -6 route show vrf CUST-A2001:db8:1::/64 dev eth1 proto kernel metric 256 pref medium
2001:db8:2::/64 dev eth1 proto kernel metric 256 pref medium
fe80::/64 dev eth1 proto kernel metric 256 pref mediumIllustrative output
Three things are worth naming in that output:
- Both configured /64s install as connected routes in the VRF’s table, exactly as IPv4 would.
fe80::/64 dev eth1is in the VRF’s table too. That is the part that matters later: a protocol that resolves afe80::next-hop for CUST-A resolves it in table 1001.ip -6 route showwith novrfselector shows the global table and none of this. Reaching for the unscoped command is the single most common reason an operator concludes “the address did not configure”.
The FRR view of the same state is show ipv6 route vrf CUST-A.
flowchart TB
subgraph VRFA["vrf CUST-A · table 1001"]
V1["2001:db8:1::/64 · connected · eth1"]
V2["2001:db8:2::/64 · connected · eth1"]
VL["fe80::/64 · connected · eth1"]
end
subgraph GLOB["global · table 254"]
G["2001:db8:ffff::/64 · connected · eth0"]
GD["default via 2001:db8:ffff::1 · eth0"]
end
E1["eth1 · master vrf-CUST-A"] --> VRFA
E0["eth0 · no master"] --> GLOB
Link-local scope: the interface disambiguates, not the VRF
It is tempting to say that a VRF gives each interface its own link-local namespace. It does not, and believing it will send you looking for the wrong fix.
Link-local scope is per link. That was true before VRFs existed
and it is still true. Two interfaces on the same router may carry the
same fe80:: address whether or not they are in different VRFs,
because a link-local address is only ever interpreted together with
the interface it was seen on — the scope-id. The kernel disambiguates
by ifindex.
What the VRF changes is narrower and more useful:
- The
fe80::/64connected route is installed in the VRF’s table rather than table 254. - Therefore an IGP next-hop of
fe80::...learned inside CUST-A is resolved against table 1001, and the resulting FIB entry names an interface that belongs to CUST-A.
For the operator, the practical rule is that you always name the interface when you touch a link-local address, and you use the VRF when the destination is global:
# Link-local destination — the scope-id names the link
ping fe80::5054:ff:fe22:2222%eth1
# Global destination inside the VRF — the VRF selects the table
ping 2001:db8:1::254 vrf CUST-A
OSPFv3 inside a VRF
OSPFv3 is configured under vrf name, with the same tree the course
used at the global level in Part XXI:
configure
set vrf name CUST-A table 1001
set interfaces ethernet eth1 vrf CUST-A
set interfaces ethernet eth1 ipv6 address 2001:db8:1::1/64
set vrf name CUST-A protocols ospfv3 parameters router-id 10.1.0.1
set vrf name CUST-A protocols ospfv3 area 0 interface eth1
commit
save
Two points that are easy to get wrong:
- OSPFv3 has no
networkstatement. Interfaces are placed into an area directly witharea 0 interface eth1. Thenetworkidiom is OSPFv2’s. - The router-id is mandatory in practice. It is a 32-bit number that happens to be written in dotted-quad notation; it is not an address and it is not reachable. In a VRF that carries no IPv4 address at all, FRR has nothing to derive it from, so leaving it out means the instance does not come up.
Verification goes through vtysh, because the vrf selector on the
OSPFv3 show commands is FRR’s:
$ vtysh -c 'show ipv6 ospf6 vrf CUST-A neighbor'Neighbor ID Pri DeadTime State/IfState Duration I/F[State]
10.1.0.254 1 00:00:35 Full/BDR 00:12:41 eth1[DR]Illustrative output
The routes that adjacency installs make the link-local point concrete:
$ show ipv6 route vrf CUST-AVRF CUST-A:
C>* 2001:db8:1::/64 is directly connected, eth1, 00:31:07
O>* 2001:db8:9::/64 [110/20] via fe80::5054:ff:fe22:2222, eth1, weight 1, 00:12:38
C>* fe80::/64 is directly connected, eth1, 00:31:07Illustrative output
Remember that shape. A route whose next-hop is a link-local address is bound to the interface printed beside it. That is the constraint that governs what you can leak, later in this lesson.
BGP IPv6 inside a VRF
configure
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 2001:db8:1::254 remote-as 65001
set vrf name CUST-A protocols bgp neighbor 2001:db8:1::254 address-family ipv6-unicast
set vrf name CUST-A protocols bgp neighbor 2001:db8:1::254 address-family ipv6-unicast soft-reconfiguration inbound
commit
save
If the session should source from a stable address rather than the
LAN address, use a dummy interface enslaved to the VRF. The host
loopback lo cannot be used here: lo is not enslavable to a VRF
master, and VyOS’s interfaces loopback lo node has no vrf leaf to
set. A dummy interface is the supported per-VRF loopback:
set interfaces dummy dum1 vrf CUST-A
set interfaces dummy dum1 address 10.10.10.1/32
set interfaces dummy dum1 ipv6 address 2001:db8:ff::1/128
set vrf name CUST-A protocols bgp neighbor 2001:db8:1::254 update-source dum1
$ show bgp vrf CUST-A ipv6 unicast summaryIPv6 Unicast Summary (VRF CUST-A):
BGP router identifier 10.1.0.1, local AS number 65000 vrf-id 5
BGP table version 12
RIB entries 23, using 4416 bytes of memory
Peers 1, using 21 KiB of memory
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt
2001:db8:1::254 4 65001 142 141 0 0 0 01:52:03 12 3
Total number of neighbors 1Illustrative output
Leaking an IPv6 prefix into another VRF
A tenant VRF that needs a shared service has to import a route for it, and the mechanism is the same one Part XVI develops in full: a static route whose next-hop is resolved in a different VRF.
On VyOS the IPv6 static route node is route6, and the leak is the
vrf leaf under the next-hop. It renders FRR’s nexthop-vrf:
configure
# Shared-services VRF: the transit LAN and the router that fronts the services
set vrf name SHARED table 9999
set interfaces ethernet eth9 vrf SHARED
set interfaces ethernet eth9 ipv6 address 2001:db8:99::1/64
# CUST-A reaches the shared-service block 2001:db8:aa::/48
# via a next-hop that only exists in SHARED
set vrf name CUST-A protocols static route6 2001:db8:aa::/48 next-hop 2001:db8:99::254 vrf SHARED
# The return path is a separate leak. CUST-A's routed block, from SHARED.
set vrf name SHARED protocols static route6 2001:db8:1:100::/56 next-hop 2001:db8:1::254 vrf CUST-A
commit
save
Read next-hop 2001:db8:99::254 vrf SHARED as “resolve this
next-hop in SHARED’s table, install the result in mine”. Zebra looks
2001:db8:99::254 up in table 9999, finds it connected on eth9,
and installs into table 1001 a route that egresses eth9 — an
interface that belongs to a different VRF.
$ show ipv6 route vrf CUST-A 2001:db8:aa::/48Routing entry for 2001:db8:aa::/48
Known via "static", distance 1, metric 0, vrf CUST-A, best
Last update 00:02:11 ago
* 2001:db8:99::254, via eth9 (vrf SHARED), weight 1Illustrative output
Two IPv6-specific constraints on leaking:
- The next-hop must be global scope. A
fe80::next-hop only has meaning together with its interface, and the leak asks another VRF to resolve the address. Leaking to a link-local next-hop is not a thing you can express; if the far side has no global address on the transit link, give it one. - Leaking is directional, and per address family. The IPv4 leak
you configured with
protocols static routedoes not carry IPv6 with it. Each family needs its own leak in each direction, which is four statements for a bidirectional dual-stack pair. Part XVI’s troubleshooting lesson exists largely because people configure one of the four and test withping.
DHCPv6 and other services inside a VRF
A handful of VyOS services take a vrf node so the daemon binds
inside a VRF — set service ssh vrf CUST-A is the one you will meet
first. That set is small and explicit, and the DHCPv6 server is not
documented as one of its members.
This is a real gap rather than a syntax you have not found yet: there
is no supported set service dhcpv6-server ... vrf leaf to write,
and no per-interface dhcpv6-server binding on the interface tree
either. Before you design a per-VRF DHCPv6 deployment, check
set service dhcpv6-server with tab-completion on your own 1.5 image
and confirm what the daemon actually listens on, rather than
assuming the VRF is respected. Where DHCPv6 is not available inside
a VRF, SLAAC via Router Advertisements is — and it is configured on
the interface, so it follows the interface into the VRF.
Router Advertisements on a VRF-bound interface
RAs are configured per interface, exactly as Part XI covered them, and the VRF binding does not appear in the RA configuration at all:
configure
set interfaces ethernet eth1 ipv6 router-advert prefix '2001:db8:1::/64'
set interfaces ethernet eth1 ipv6 router-advert prefix '2001:db8:1::/64' autonomous-flag 'on'
set interfaces ethernet eth1 ipv6 router-advert prefix '2001:db8:1::/64' on-link-flag 'on'
set interfaces ethernet eth1 ipv6 router-advert interval 200
set interfaces ethernet eth1 ipv6 router-advert lifetime 1800
commit
save
VyOS renders this into a configuration for radvd, a userspace
daemon, which sends the advertisements out of eth1. Sending an RA
is a link operation — the daemon writes to the interface — so it
neither knows nor cares about the VRF.
What the VRF does own is the consequence. A host that autoconfigures
from this RA installs a default route towards the router’s link-local
address on eth1. When its traffic arrives, it arrives on an
interface enslaved to CUST-A, so the router forwards it out of table
1001. The host has no way to tell that the router is multi-tenant;
from its side this is an ordinary IPv6 gateway.
The prefix you advertise must be the prefix that is actually on the link. Advertising a shorter prefix than the interface’s /64 does not “cover” the /64 — SLAAC derives the host address from the advertised prefix, so the host ends up numbered outside the subnet its gateway is on, and the first packet to the gateway has nowhere to go.
IPv4 in a VRF versus IPv6 in a VRF
| Behaviour | IPv4 in a VRF | IPv6 in a VRF |
|---|---|---|
| Static route node | protocols static route | protocols static route6 |
| Connected routes | per-VRF table | per-VRF table, plus fe80::/64 |
| L2 resolution | ARP, invisible to the IP firewall | ICMPv6 ND, filterable by firewall ipv6 |
| IGP | OSPFv2, area 0 network ... | OSPFv3, area 0 interface ethN |
| IGP next-hops | global addresses | link-local addresses, bound to an interface |
| BGP family node | address-family ipv4-unicast | address-family ipv6-unicast |
| Router-id | derivable from any IPv4 address | must be set when the VRF has no IPv4 address |
| Address sources | static, DHCP relay | static, SLAAC, DHCPv6 where available |
| Leaking | next-hop ADDR vrf OTHER | same leaf, and the next-hop must be global scope |
An IPv6-only VRF is a supported design and needs no IPv4 addressing to forward traffic. The one thing it does need is an explicit router-id on every routing instance you run inside it, because that is the single place where FRR still wants a 32-bit dotted-quad value it cannot invent.
How it fails
The failure modes that are specific to IPv6 inside a VRF, and what each one looks like before you know the cause:
- The BGP peer is missing from the family summary.
show bgp vrf CUST-A ipv6 unicast summarydoes not list the neighbour. Theaddress-family ipv6-unicastnode under that neighbour is missing. Add it, commit, re-check. - The routing instance never starts in an IPv6-only VRF. No
adjacency, no peers, and FRR’s log complains that the router-id is
not set. Set
parameters router-idunder the instance inside the VRF. - IPv6 routes appear in the global table. The
protocols ospfv3stanza is at the top level instead of undervrf name CUST-A. The adjacency comes up — the interface is reachable — butshow ipv6 route vrf CUST-Ais empty whileshow ipv6 routehas the prefixes. Move the stanza; the isolation was never in effect. - Neighbours stay
INCOMPLETEorFAILED.ip -6 neigh show dev eth1shows unresolved entries and nothing forwards. An IPv6 rule set is dropping ICMPv6 Neighbour Solicitation and Advertisement. Unlike ARP, IPv6’s L2 resolution runs over IP and the firewall can and will drop it. Permit ND, Router Solicitation and Advertisement, and Packet Too Big. - A stale neighbour entry survives a renumber.
ip -6 neigh flush dev eth1clears it. Do not go looking for a sysctl to tune first; flush, confirm the entry re-resolves, and only then investigate why it went stale. - A leaked prefix is in the RIB but not the FIB. The
vrfleaf was omitted from the next-hop, so the next-hop is unresolvable inside the VRF that holds the route.show ipv6 route vrf CUST-Ashows itinactive;ip -6 route show vrf CUST-Adoes not show it at all. - Hosts autoconfigure an address they cannot use. The advertised
prefix does not match the /64 on the interface, or
autonomous-flagis off so SLAAC never runs. Compare therouter-advert prefixvalue againstip -6 addr show dev eth1.
Rollback
# Remove one IPv6 address from a VRF-bound interface
delete interfaces ethernet eth1 ipv6 address 2001:db8:1::1/64
commit
save
# Remove an IPv6 leak
delete vrf name CUST-A protocols static route6 2001:db8:aa::/48
commit
save
# Remove the routing instance but keep the VRF
delete vrf name CUST-A protocols ospfv3
commit
save
Removing the VRF itself needs the member interfaces released in the
same candidate configuration — VyOS refuses to commit a delete vrf name CUST-A while an interface still names it:
delete interfaces ethernet eth1 vrf
delete vrf name CUST-A
commit
save
A safe sequence for any of these:
- Capture the running configuration first
(
show configuration commandsinto a file off-box). - Review the candidate with
comparebefore committing. - Use
commit-confirm 10— the argument is a whole number of minutes — for any change that touches a path your own session depends on.confirmwithin the window makes it permanent; silence rolls it back. - After
commit, verify withshow ipv6 route vrf CUST-A,ip -6 route show vrf CUST-A, and the per-VRF protocol summaries. saveonly once the verification is clean.
Production discipline
Cross-course references
- The Linux course’s
XXII-Linux-NetTroubleshootcovers the host-side IPv6 primitives —ip -6 route get,ip -6 neigh show— that the per-VRF forms extend. vyos-xi-01-ipv6-fundamentalsandvyos-xi-03-ipv6-router-advertisementsestablish the addressing and RA model this lesson places inside a VRF.vyos-xxi-02-ospfv3-configcovers the OSPFv3 tree at the global level; the per-VRF form is the same tree undervrf name.vyos-xv-05-vrf-troubleshoot(next) is the diagnostic playbook for both families.vyos-xvi-04-leaking-ipv6develops the leaking material this lesson introduces, including the policy controls on a leak.
Quiz
Knowledge check · 4 questions
Q1. Two interfaces on one VyOS router, eth1 in VRF CUST-A and eth2 in VRF CUST-B, both carry a link-local address. What does the VRF binding actually change about how those link-local addresses behave?
Q2. A VRF that carries only IPv6 addresses still needs an explicit router-id on each FRR routing instance inside it, because FRR router-ids are 32-bit values written in dotted-quad and there is no IPv4 address in the VRF for FRR to derive one from.
Q3. An operator leaks a shared-service IPv6 block into a tenant VRF. The commit succeeds with no error, `show ipv6 route vrf CUST-A` shows the static route, and yet nothing in CUST-A can reach the block. Walk the diagnostic and name the defect.
The configuration is: - set vrf name SHARED table 9999 - set interfaces ethernet eth9 vrf SHARED - set interfaces ethernet eth9 ipv6 address 2001:db8:99::1/64 - set vrf name CUST-A protocols static route6 2001:db8:aa::/48 next-hop 2001:db8:99::254 The shared-service router 2001:db8:99::254 sits on the eth9 LAN, which belongs to VRF SHARED. Nothing in CUST-A can reach 2001:db8:aa::/48. There was no commit error.
Q4. IPv6 hosts on a VRF-bound LAN cannot reach their gateway. The addresses are correct, the connected route is in the per-VRF table, and the same segment works for IPv4. What is the most likely cause, and how does the IPv4/IPv6 difference explain why it only affects one family?
eth1 is enslaved to CUST-A and addressed 2001:db8:1::1/64 and 10.1.0.1/24. Hosts on the segment have working IPv4 to 10.1.0.1. IPv6 to 2001:db8:1::1 times out. `show ipv6 route vrf CUST-A` shows 2001:db8:1::/64 connected on eth1. An IPv6 rule set was added to the VRF's interfaces the previous week.
Passing score: 75%. Answers are checked in this browser.