Skip to main content
RunBook Academy

VyOSXV · VRFsVRF

IPv6 inside a VRF — link-local next-hops, OSPFv3 and BGP per VRF, leaking route6

Advanced⏱ ~22 minip -6 addr show dev SLAVEIFip -6 route show vrf VRFNAMEip -6 neigh show dev SLAVEIFshow ipv6 route vrf VRFNAMEshow bgp vrf VRFNAME ipv6 unicast summaryvtysh -c show ipv6 ospf6 vrf VRFNAME neighborvyos

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

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

Not yet marked complete on this device.

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 name rule 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.

Read-only / Safeaddresses on the slave interface
$ ip -6 addr show dev eth1
3: 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 forever

Illustrative output

Where the IPv6 routes land

The interesting output is not the address list — it is the table.

Read-only / Safeper-VRF IPv6 table
$ ip -6 route show vrf CUST-A
2001: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 medium

Illustrative 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 eth1 is in the VRF’s table too. That is the part that matters later: a protocol that resolves a fe80:: next-hop for CUST-A resolves it in table 1001.
  • ip -6 route show with no vrf selector 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

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::/64 connected 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 network statement. Interfaces are placed into an area directly with area 0 interface eth1. The network idiom 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:

Read-only / SafeOSPFv3 adjacency inside CUST-A
$ 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:

Read-only / SafeOSPFv3-learned route in the per-VRF RIB
$ show ipv6 route vrf CUST-A
VRF 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:07

Illustrative 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
Read-only / Safeper-VRF IPv6 BGP summary
$ show bgp vrf CUST-A ipv6 unicast summary
IPv6 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 1

Illustrative 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.

Read-only / Safea leak that resolved
$ show ipv6 route vrf CUST-A 2001:db8:aa::/48
Routing 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 1

Illustrative 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 route does 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 with ping.

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

BehaviourIPv4 in a VRFIPv6 in a VRF
Static route nodeprotocols static routeprotocols static route6
Connected routesper-VRF tableper-VRF table, plus fe80::/64
L2 resolutionARP, invisible to the IP firewallICMPv6 ND, filterable by firewall ipv6
IGPOSPFv2, area 0 network ...OSPFv3, area 0 interface ethN
IGP next-hopsglobal addresseslink-local addresses, bound to an interface
BGP family nodeaddress-family ipv4-unicastaddress-family ipv6-unicast
Router-idderivable from any IPv4 addressmust be set when the VRF has no IPv4 address
Address sourcesstatic, DHCP relaystatic, SLAAC, DHCPv6 where available
Leakingnext-hop ADDR vrf OTHERsame 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 summary does not list the neighbour. The address-family ipv6-unicast node 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-id under the instance inside the VRF.
  • IPv6 routes appear in the global table. The protocols ospfv3 stanza is at the top level instead of under vrf name CUST-A. The adjacency comes up — the interface is reachable — but show ipv6 route vrf CUST-A is empty while show ipv6 route has the prefixes. Move the stanza; the isolation was never in effect.
  • Neighbours stay INCOMPLETE or FAILED. ip -6 neigh show dev eth1 shows 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 eth1 clears 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 vrf leaf was omitted from the next-hop, so the next-hop is unresolvable inside the VRF that holds the route. show ipv6 route vrf CUST-A shows it inactive; ip -6 route show vrf CUST-A does 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-flag is off so SLAAC never runs. Compare the router-advert prefix value against ip -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:

  1. Capture the running configuration first (show configuration commands into a file off-box).
  2. Review the candidate with compare before committing.
  3. Use commit-confirm 10 — the argument is a whole number of minutes — for any change that touches a path your own session depends on. confirm within the window makes it permanent; silence rolls it back.
  4. After commit, verify with show ipv6 route vrf CUST-A, ip -6 route show vrf CUST-A, and the per-VRF protocol summaries.
  5. save only once the verification is clean.

Production discipline

Cross-course references

  • The Linux course’s XXII-Linux-NetTroubleshoot covers the host-side IPv6 primitives — ip -6 route get, ip -6 neigh show — that the per-VRF forms extend.
  • vyos-xi-01-ipv6-fundamentals and vyos-xi-03-ipv6-router-advertisements establish the addressing and RA model this lesson places inside a VRF.
  • vyos-xxi-02-ospfv3-config covers the OSPFv3 tree at the global level; the per-VRF form is the same tree under vrf name.
  • vyos-xv-05-vrf-troubleshoot (next) is the diagnostic playbook for both families.
  • vyos-xvi-04-leaking-ipv6 develops the leaking material this lesson introduces, including the policy controls on a leak.

Quiz

Knowledge check · 4 questions

  1. 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?

  2. 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.

  3. 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.

  4. 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.