Skip to main content
RunBook Academy

VyOSXVI · Route Leaking Between VRFsLeaking

IPv6 leaking — dual-stack leaks, RDNSS sharing, prefix delegation

Advanced⏱ ~22 minset vrf name protocols static route6set service router-advert interfaceset interfaces ethernet dhcpv6-options pdset service dhcpv6-server shared-network-nameshow ipv6 route vrfip -6 route show tablepingtcpdump

What you'll learn

  • Configure an IPv6 leak with route6 and the vrf leaf, alongside the IPv4 leak for the same service
  • Count the statements a bidirectional dual-stack leak actually needs, and verify each one in the kernel table
  • Advertise a shared RDNSS into a tenant VRF and explain why the advertisement is not the reachability
  • State what VyOS exposes for DHCPv6 prefix delegation across a VRF boundary, and what it does not
  • Recognise the IPv6-specific leak failure modes: link-local next-hops, ICMPv6 PTB filtering, multicast scope

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.

IPv6 leaking — dual-stack leaks, RDNSS sharing, prefix delegation

IPv6 leaking is the same mechanism as IPv4 leaking with a different address family bolted to it: route6 instead of route, an IPv6 next-hop instead of a v4 one, RDNSS instead of a DHCPv4 option, prefix delegation instead of a server-side address pool. Everything Part XVI has said about direction, resolution and evidence still holds.

What changes is the failure surface. IPv6 has an address scope IPv4 does not have, it has no in-path fragmentation, and its neighbour discovery is multicast on the local link. Each of those three interacts with a leak in a way that produces a symptom the operator has not seen on the IPv4 side, and this lesson is mostly about those three.

The topology, restated

Every command in this lesson runs against the same three routing contexts Part XVI has used throughout:

  • default — table 254. eth0 is the transit to the shared-service block: 192.0.2.2/30 and 2001:db8:ff::2/64, with the service router on 192.0.2.1 / 2001:db8:ff::1. Behind it live DNS on 198.51.100.53 / 2001:db8:100::53, NTP on 198.51.100.123 / 2001:db8:100::123, syslog on 198.51.100.200 / 2001:db8:100::200.
  • mgmt — table 1001. eth1, 10.10.0.1/24 and 2001:db8:10::1/64.
  • tenant-a — table 1002. eth2, 10.20.0.1/24 and 2001:db8:20::1/64.

default is not a VRF you create. It is the main routing table, and it is the word the leak commands use when the other side of the leak is that table.

flowchart TB
  subgraph D["default (table 254)"]
    E0["eth0<br/>2001:db8:ff::2/64"]
    SVC["shared services<br/>2001:db8:100::53 DNS<br/>2001:db8:100::123 NTP"]
  end
  subgraph M["vrf mgmt (table 1001)"]
    E1["eth1<br/>2001:db8:10::1/64"]
  end
  subgraph T["vrf tenant-a (table 1002)"]
    E2["eth2<br/>2001:db8:20::1/64"]
  end
  E0 --- SVC
  E1 -. "route6 leak toward services" .-> E0
  E2 -. "route6 leak toward services" .-> E0
  E0 -. "return leak toward each tenant" .-> E1

The IPv6 leak is route6 plus the vrf leaf

The whole of route leaking on VyOS sits on one idea: a route is installed in one table and its next-hop is resolved in another. The leaf that separates the two is vrf, and the static-route tree carries it under both next-hop and interface, for route and route6 alike.

configure
# Leak the DNS and NTP service addresses into mgmt.
# The next-hop 2001:db8:ff::1 exists only in the default table.
set vrf name mgmt protocols static route6 2001:db8:100::53/128 next-hop 2001:db8:ff::1 vrf default
set vrf name mgmt protocols static route6 2001:db8:100::123/128 next-hop 2001:db8:ff::1 vrf default

# The same two services into tenant-a.
set vrf name tenant-a protocols static route6 2001:db8:100::53/128 next-hop 2001:db8:ff::1 vrf default
set vrf name tenant-a protocols static route6 2001:db8:100::123/128 next-hop 2001:db8:ff::1 vrf default

commit
save

Read next-hop 2001:db8:ff::1 vrf default as one phrase: resolve this address in the default table, install the result in mine. VyOS renders it into FRR as an ipv6 route statement inside the VRF’s stanza:

vrf mgmt
 ipv6 route 2001:db8:100::53/128 2001:db8:ff::1 nexthop-vrf default
exit-vrf

Drop the two words vrf default and the statement still commits. Zebra then tries to resolve 2001:db8:ff::1 inside table 1001, where nothing matches, and the route sits in the RIB unresolved and is never offered to the kernel. That is the characteristic route-leaking failure and it is the reason this part of the course exists: nothing errors, and nothing forwards.

Read-only / Safea leak that resolved
$ show ipv6 route vrf mgmt 2001:db8:100::53/128
Routing entry for 2001:db8:100::53/128
Known via "static", distance 1, metric 0, vrf mgmt, best
Last update 00:02:11 ago
* 2001:db8:ff::1, via eth0 (vrf default), weight 1

Illustrative output

The line to read is the last one. It names an egress interface, eth0, and the routing context that interface belongs to, (vrf default). An ordinary intra-VRF static route names no VRF there. If the parenthesis is absent, you are not looking at a leak.

The control plane can still be optimistic, so confirm in the kernel:

Read-only / Safekernel truth
$ ip -6 route show table 1001
2001:db8:100::53 via 2001:db8:ff::1 dev eth0 proto static metric 20 pref medium
2001:db8:100::123 via 2001:db8:ff::1 dev eth0 proto static metric 20 pref medium
2001:db8:10::/64 dev eth1 proto kernel metric 256 pref medium
fe80::/64 dev eth1 proto kernel metric 256 pref medium

Illustrative output

The signature of a leak in a kernel table is an output device that does not belong to the VRF that table represents. eth1 belongs to mgmt; eth0 does not; table 1001 is mgmt’s. Those first two lines could not have been produced by an ordinary static route.

The fe80::/64 line is not a leak and never becomes one. It is the link-local scope of eth1, installed by the kernel with the address, and it is the IPv6 counterpart of the on-link behaviour ARP provides for v4.

A bidirectional dual-stack pair is four statements

A leak is unidirectional. It is also per address family. Multiply the two and a dual-stack service that must be reachable and able to answer needs four statements, not two.

configure
# mgmt -> services, IPv4 and IPv6
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 route6 2001:db8:100::53/128 next-hop 2001:db8:ff::1 vrf default

# services -> mgmt, IPv4 and IPv6. Configured in the default context,
# pointing into the VRF. The mgmt LAN is on-link on eth1 inside mgmt,
# so the interface form is the natural one here.
set protocols static route 10.10.0.0/24 interface eth1 vrf mgmt
set protocols static route6 2001:db8:10::/64 interface eth1 vrf mgmt

commit
save

The two forms in that block are both documented and each fits a different case. Use next-hop <address> vrf X when the destination is reached through a router that lives in X. Use interface <name> vrf X when the destination is directly connected on an interface that is a member of X, and there is no next-hop address worth naming.

Missing one of the four is the most common dual-stack leak defect in production, and each omission has its own signature:

Missing statementWhat the operator sees
mgmt to services, v6v4 works, v6 connections hang until the client falls back
mgmt to services, v4v6 works; anything v4-only, such as an old agent, fails outright
services to mgmt, v6v6 request leaves, reply is dropped for want of a route; reads like a firewall drop
services to mgmt, v4same, on the v4 path

The two return-path rows are the expensive ones. A missing return leak does not look like a routing fault from the tenant’s side. The query leaves, nothing comes back, and the first instinct of everyone in the incident channel is the firewall. Part XVI’s troubleshooting lesson opens with that failure for exactly this reason.

An IPv6 next-hop may be link-local, and inside a single routing context that is ordinary: fe80::1 plus the interface it was heard on is a complete instruction. The static-route tree has an interface leaf under next-hop precisely so the pair can be expressed.

A leak breaks the pair apart. The whole point of vrf default is to hand the address to another table and ask it to resolve. A link-local address carries no information about which link it belongs to, so there is nothing for the other table to resolve — the same fe80::1 may exist on every interface on the box, in every VRF.

The supported shape is the one already used above: give the transit link a global address and leak toward that.

Sharing an RDNSS across VRFs

RFC 8106 lets a Router Advertisement carry the addresses of recursive DNS servers, which is how an IPv6 host on a SLAAC network learns a resolver without DHCPv6. On VyOS the option lives under service router-advert, per interface:

configure
set service router-advert interface eth1 prefix 2001:db8:10::/64
set service router-advert interface eth1 name-server 2001:db8:100::53
set service router-advert interface eth1 name-server-lifetime 600

set service router-advert interface eth2 prefix 2001:db8:20::/64
set service router-advert interface eth2 name-server 2001:db8:100::53
set service router-advert interface eth2 name-server-lifetime 600

commit
save

There is no VRF selector in that tree, and none is needed. Router Advertisements are link-local multicast sent out of an interface; which routing table that interface consults is irrelevant to whether the advertisement reaches the link. eth1 is in mgmt and eth2 is in tenant-a, and both links get the same resolver address.

sequenceDiagram
  participant R as VyOS
  participant H as host on the mgmt LAN
  R->>H: RA on eth1: prefix 2001:db8:10::/64,<br/>RDNSS 2001:db8:100::53
  Note over H: host sets resolver to<br/>2001:db8:100::53
  H->>R: DNS query to 2001:db8:100::53
  Note over R: table 1001 lookup —<br/>answered only if the leak exists

The sequence is the lesson. The advertisement and the reachability are two independent facts, produced by two independent subsystems, and the router will happily deliver the first without the second.

DHCPv6 prefix delegation across a VRF boundary

Prefix delegation is where operators most often expect a leak and should not configure one, so it is worth being precise about what VyOS actually exposes.

The DHCPv6 client requests a prefix on the upstream interface and assigns sub-prefixes of it onto interfaces you name:

configure
# Ask upstream for a /56 on eth0, and hand SLA-ID 0 to eth2.
set interfaces ethernet eth0 dhcpv6-options pd 0 length 56
set interfaces ethernet eth0 dhcpv6-options pd 0 interface eth2 address 1
set interfaces ethernet eth0 dhcpv6-options pd 0 interface eth2 sla-id 0
commit
save

eth2 is a member of tenant-a. When the delegation arrives, the client configures the sub-prefix on eth2, the kernel installs the connected route for it, and — because eth2 is enslaved to the VRF — that connected route lands in table 1002. No leak was involved and none is needed: the prefix is in the tenant VRF because the interface is in the tenant VRF.

Read-only / Safethe delegated prefix, as configured on the delegatee
$ ip -6 addr show dev eth2
4: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master tenant-a state UP qlen 1000
  inet6 2001:db8:abcd:ab00::1/64 scope global
     valid_lft forever preferred_lft forever
  inet6 fe80::5054:ff:fe12:3456/64 scope link
     valid_lft forever preferred_lft forever

Illustrative output

What the tenant still needs is a route out, and that is a leak like any other — or an upstream of its own. What it does not need is a static route for its own delegated prefix.

Option 2 in configuration, for the case where the default table needs to reach the tenant’s delegated prefix:

configure
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 ipv6-unicast redistribute connected

set protocols bgp system-as 65000
set protocols bgp parameters router-id 192.0.2.2
set protocols bgp address-family ipv6-unicast import vrf tenant-a
commit
save

import vrf copies paths between BGP RIBs inside the one FRR process; there is no session between the two contexts and none to watch. The redistribute connected line is not decoration — a connected route lives in zebra’s RIB, not BGP’s, until something puts it there, and importing a VRF whose BGP instance holds nothing imports nothing, correctly and silently.

Filter it. import vrf with no route-map brings across everything that VRF’s BGP instance holds, which on a tenant with an upstream is more than the delegated prefix.

Serving addresses inside the tenant

If the tenant LAN is served by DHCPv6 rather than SLAAC, the server is configured per shared-network and bound to an interface:

configure
set service dhcpv6-server shared-network-name TENANT-A interface eth2
set service dhcpv6-server shared-network-name TENANT-A subnet 2001:db8:abcd:ab00::/64 subnet-id 1
set service dhcpv6-server shared-network-name TENANT-A subnet 2001:db8:abcd:ab00::/64 range 1 start 2001:db8:abcd:ab00::100
set service dhcpv6-server shared-network-name TENANT-A subnet 2001:db8:abcd:ab00::/64 range 1 stop 2001:db8:abcd:ab00::199
set service dhcpv6-server shared-network-name TENANT-A subnet 2001:db8:abcd:ab00::/64 option name-server 2001:db8:100::53
commit
save

Two things are worth naming. subnet-id is mandatory and must be unique across the server; the commit fails without it. And the binding is interface, not a VRF — the server tree has no VRF selector, so per-tenant servers are distinguished by the interface they listen on, which in a VRF estate amounts to the same thing.

The option name-server here has the same property the RDNSS did: it hands out an address, and the leak is what makes that address answer.

The three IPv6-specific failure modes

Neighbour discovery does not cross the leak, and does not need to

ND is link-scoped by construction: solicitations go to a solicited-node multicast address on the local link. A host on the mgmt LAN never performs ND for 2001:db8:100::53, and it should not — that address is not on its link. The host resolves its default router, sends the packet there, and the router forwards it using the leaked route.

Where this bites is multicast. A leak is a unicast route, and unicast routes do not carry ff02:: traffic anywhere. Anything built on link-local multicast — mDNS, LLMNR, SSDP, service discovery protocols generally — does not reach across a leak and cannot be made to by adding routes. If a shared service depends on discovery rather than a configured address, the leak is the wrong tool: give the clients a unicast address to talk to, or run a relay.

There is no in-path fragmentation

An IPv6 router never fragments a packet it is forwarding. When a packet is too large for the next link, the router drops it and sends ICMPv6 Packet Too Big to the source, which is expected to lower its path MTU for that destination. Path MTU discovery is not an optimisation in IPv6; it is load-bearing.

A leak crosses between interfaces that may have been provisioned by different teams with different MTUs. If the ICMPv6 PTB cannot get back to the sender — because a firewall on the leak path drops it — then small packets work, large ones vanish, and the failure looks like an application bug.

configure
# Let PTB through on the forward path, both directions of the leak.
set firewall ipv6 forward filter rule 5 action accept
set firewall ipv6 forward filter rule 5 description 'ICMPv6 PTB - required for PMTUD across the leak'
set firewall ipv6 forward filter rule 5 protocol icmpv6
set firewall ipv6 forward filter rule 5 icmpv6 type-name packet-too-big
commit
save

Where the sender cannot be relied on to react — or where the traffic is TCP and you would rather not depend on PTB at all — clamp instead, on the interface:

set interfaces ethernet eth1 ipv6 adjust-mss clamp-mss-to-pmtu

Clamping only helps TCP. UDP over a leak with a broken PTB path stays broken, which is why the firewall rule is the first fix and the clamp is the second.

Scope is a property of the address, not of the VRF

An operator who has internalised “the VRF is the isolation boundary” will sometimes reason that a link-local or unique-local address is “more contained” and therefore safer to leak. Neither scope has anything to do with VRFs. Link-local is bounded by the link; unique-local is bounded by administrative convention and routes as ordinary unicast. A ULA prefix leaked between two VRFs is exactly as reachable as a global one, and the only thing keeping it off the internet is that nobody advertises it.

How the result is validated

show ipv6 route vrf mgmt
show ipv6 route vrf tenant-a
ip -6 route show table 1001
ip -6 route show table 254
ping 2001:db8:100::53 vrf mgmt count 3
ping 2001:db8:100::53 vrf mgmt count 3 size 1400 do-not-fragment
show configuration commands | match router-advert

The first two show intent as the control plane understood it. The next two show what will forward, in both directions — table 254 is where the return leak has to appear, and checking only the tenant side is how one-way leaks reach production. The two ping runs differ in one thing, size: the first proves reachability, the second probes the MTU path that PMTUD depends on. do-not-fragment matters on the second one even though IPv6 has no in-path fragmentation — without it the sender is entitled to fragment locally, and a 1400-byte echo will succeed over a path that cannot carry a 1400-byte packet. The last command prints what the router is promising the hosts.

Then leave the router. A tcpdump -i eth2 -n icmp6 while a host on the tenant LAN boots shows the RA it actually receives, and a DNS query from that host is the only test that exercises the advertisement and the leak together.

How it fails

  • The v6 half of a dual-stack leak was never configured. Clients that prefer IPv6 stall on the AAAA and fall back. The ticket says “slow”, and every v4 test passes.
  • Only one direction was leaked. Queries leave, replies have no route home and are dropped in the default table. It reads as a firewall problem and the investigation goes to the wrong team.
  • The vrf leaf was omitted. The commit succeeds, the route appears in show ipv6 route, and the kernel table never receives it. Reviewing the configuration diff does not reveal this; only the kernel table does.
  • RDNSS advertised without the matching leak. The whole segment configures a resolver it cannot reach, at the next RA.
  • A PD lease changed under a static leak. The upstream re-delegated, the hard-coded prefix is stale, and the tenant is unreachable from everywhere the static leak was serving.
  • ICMPv6 PTB filtered on the leak path. Small packets work, large ones disappear, and the failure is blamed on the application for as long as nobody tests with size.
  • A discovery protocol was expected to cross. mDNS and friends are link multicast; no route will carry them between VRFs.

Rollback

  • Remove one leak: delete vrf name mgmt protocols static route6 2001:db8:100::53/128, then commit.
  • Remove every IPv6 leak from a VRF: delete vrf name mgmt protocols static route6, then commit. Confirm with ip -6 route show table 1001 that only connected routes remain.
  • Undo a return leak: delete protocols static route6 2001:db8:10::/64, then commit.
  • Withdraw an RDNSS that has no backing route: delete service router-advert interface eth2 name-server 2001:db8:100::53, then commit. Hosts stop being told about a resolver they cannot reach, though those that already hold it keep it until the lifetime expires — which is what name-server-lifetime is for.
  • Back out a BGP import: delete protocols bgp address-family ipv6-unicast import vrf tenant-a, then commit. VyOS refuses to delete a VRF that another context imports from, so remove the import before the VRF.

Nothing in this lesson is destructive in the storage sense, and all of it is reversible with rollback plus commit — but every one of these commands can black-hole a tenant’s traffic on the way past, so they belong in a change window like any other routing change.

Production discipline

Cross-course references

vyos-xv-04-vrf-ipv6 establishes IPv6 inside a VRF and introduces the vrf leaf this lesson applies; vyos-xi-04-dhcpv6 and vyos-xi-05-ipv6-routing cover the DHCPv6 and IPv6 routing fundamentals. vyos-xvi-05-leaking-troubleshoot takes the failure modes listed above and turns them into an evidence order. The Linux course’s XIX-Linux-NetFoundations covers ND and PMTUD as kernel behaviour rather than as router configuration.

Quiz

Knowledge check · 4 questions

  1. Q1. Which command leaks the IPv6 service address `2001:db8:100::53/128` into the `mgmt` VRF, where the next-hop `2001:db8:ff::1` is reachable only in the default table?

  2. Q2. Advertising an RDNSS on a VRF's interface with `set service router-advert interface eth2 name-server 2001:db8:100::53` makes that resolver reachable from hosts on that VRF.

  3. Q3. An operator configures `set vrf name mgmt protocols static route6 2001:db8:100::53/128 next-hop fe80::1 vrf default` because `fe80::1` is what `show ipv6 neighbors` shows for the upstream router. The commit succeeds and nothing forwards. Why is a link-local next-hop the wrong tool for a leak, and what should replace it?

    A link-local address is only meaningful together with the link it was heard on; the same fe80::1 can exist on every interface on the box. The `vrf default` leaf asks another routing table to resolve the address on its own, and there is nothing in a bare link-local address for that table to resolve against. The fix is not a more specific next-hop - it is a global address on the transit link, leaked toward normally.

  4. Q4. A tenant VRF gets its addressing from DHCPv6 prefix delegation on the upstream interface. An operator writes a static leak in the default table for the delegated /64 so that a monitoring host can reach the tenant. Twenty-four hours later the upstream re-delegates a different prefix and monitoring goes dark. What is the underlying mismatch, and what does VyOS actually offer?

    A static route is a constant and a delegation is a lease. VyOS has no CLI node that binds a static route to a DHCPv6-PD lease - the static tree can take a next-hop from a DHCPv4 lease, but nothing takes a prefix from a PD lease. When the delegation changes, the client re-addresses the delegatee interface and the connected route follows it, while the hand-written leak in the other table keeps pointing at a prefix that no longer exists.

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