Series finale · MikroTik RB5009 · multi-homed IPv6
Multi-homing IPv6 over CGNAT on RouterOS
Run the VPS and Route64 paths at the same time under one announceable /48, with BGP best-path selecting the active IPv6 default.
Series finale · MikroTik RB5009 · multi-homed IPv6
Run the VPS and Route64 paths at the same time under one announceable /48, with BGP best-path selecting the active IPv6 default.
The series finale of the MikroTik RB5009 home-network series:
run two IPv6 uplinks at once under one announceable provider-assigned
/48, so a dead tunnel on one upstream pivots traffic to the other
without the LAN losing routable IPv6. This post is WAN clean-slate — it
replaces the WAN-side plumbing of the two equal-paths posts
(VPS, Route64)
with a self-contained multi-homed build, so you do not need to have
built those posts first — though §3 below pulls <R64_*> placeholders
from the Route64 post §2
portal walkthrough, so keep that tab open. The LAN side still assumes the
VLAN foundation post — bridge, VLANs,
inter-VLAN IPv4 firewall, per-VLAN DHCPv4, ULA-bound DoH resolver.
The hard prerequisite is a 32-bit ASN (<OWN_AS>) and an announceable
/48 (<OWN_48>) — the assigning provider must authorize you to
originate the /48 from the ASN, with a matching ROA for RPKI. The
same <OWN_48> rides over both upstreams, so consistent origin matters
more than which provider issues it. If you do not yet have either,
follow jf.si's IPv6 ASN + BGP + WireGuard write-up
for the acquisition step — this post is another flavor of the same
idea (MikroTik RouterOS on the home side, Route64 as the second
upstream, BFD on the VPS path) that picks up where it ends. The flavor
I used was NoPkt for both the
ASN and the /48 ($20 one-time + $60/year USD), and
pdxnet.co.uk / getvps.net
(AS 52025) for the transit VPS — $6/month
including a free self-service BGP session, so <TRANSIT_AS> in the
snippets below is 52025 for this flavor.
On top of the jf.si baseline (ASN + /48 + one transit VPS) you're
adding the operational weight of a second BGP session and a BFD layer.
That's worth weighing against what multi-homing actually buys you. It
pays off if you self-host v6 services on <OWN_48> and want a public
prefix that survives an upstream swap; if your two upstreams genuinely
fail independently (different transits, different physical paths to
the home); or if the build itself is the point — BGP, BFD, and RIR
bureaucracy as a weekend project. If none of those apply — one path
plus Happy Eyeballs fail-to-IPv4 already covers your failure modes
invisibly — stop at the VPS path or the
Route64 path and skip this post.
Every numbered section is paste-ready against a defconf RouterOS v7 box that already has the VLAN segmentation from the foundation post. Italicized notes are the rationale.
Internet (IPv4 + IPv6)
│
┌─────────────┴─────────────┐ ┌──────────────────────────┐
│ Transit A — your VPS │ │ Route64 PoP (WireGuard) │
│ AS<OWN_AS>, runs BFD │ │ AS 212895, no BFD │
│ iBGP to home over wg-host│ │ eBGP to home over wg-r64│
└─────────────┬─────────────┘ └────────────┬─────────────┘
│ │
wg-host wg-route64
│ │
│ both transit your <OWN_48>│
│ and return ::/0 to home │
│ │
┌────┴────────────────────────────┴────┐
│ home router │
│ AS<OWN_AS>, originates <OWN_48> │
│ receives ::/0 from both, best-path │
│ installs VPS-learned default │
└──────────────────────────────────────┘
│
┌────────────┴────────────┐
│ per-VLAN /64s from │
│ <OWN_48> — same recipe │
│ as Per-VLAN IPv6 post │
└─────────────────────────┘
Per-VLAN IPv6 from <OWN_48> is plumbed via the
Per-VLAN IPv6 post once both BGP sessions
are up.
Three working pieces:
text
text
1A. VPS — runs BFD with home, announces your /48 up to transit
2 (announce-only — AS 52025 sends nothing back), and re-
3 originates its own default route to home over iBGP as
4 ::/0 with empty AS_PATH
5B. Route64 — eBGPs your /48 from home to AS 212895, advertises ::/0
6 back with AS_PATH=[212895]
7C. Home — receives ::/0 from both sessions, BGP best-path installs the
8 VPS-learned default (shorter AS_PATH), Route64-learned
9 default is the backupHome and VPS share <OWN_AS>, so the session between them is iBGP and
the VPS-side ::/0 reaches home with an empty AS_PATH. That ::/0 is
the VPS's own upstream default — from the public NIC's RA or a static
route, pulled into bird and re-originated locally — not a route learned
over the transit session, which for AS 52025 carries nothing back to
you. Route64 sits in AS 212895, so its ::/0 arrives with
AS_PATH=[212895]. No
local-pref or MED is set on either session, so RouterOS best-path
falls through to AS_PATH length — empty beats [212895] — and the
VPS default wins. The Route64 default sits in BGP as the backup.
Nothing on the home router explicitly marks one path preferred — the
primary/backup ordering falls out of where each AS boundary sits.
The home router is the only place that needs to know about multi-homing.
The VPS and Route64 each see exactly one BGP peer (home) and don't know
the other exists. Both transits accept your <OWN_48> announcement on
their respective sessions and reach <OWN_48> via the WireGuard tunnel.
| Placeholder | Meaning |
|---|---|
<OWN_AS> | Your 32-bit ASN. Both home and the VPS use this; the session is iBGP. |
<OWN_48> | Your assigned /48, written without trailing zeros, e.g. 2001:db8:eff9. |
<TRANSIT_AS> | The VPS provider's ASN (your eBGP peer from the VPS upstream-side). Provider tells you. |
<TRANSIT_PEER_V6> | Provider's IPv6 BGP peer address handed to your VPS. From the provider's BGP onboarding. |
<VPS_IP> | VPS public IPv4 (used as the WireGuard endpoint for the home → VPS tunnel). |
<VPS_NIC> | VPS public interface (ip -o link; typical enp3s0, ens3). |
<VPS_GUA> | The VPS's own IPv6 on its public /64 (e.g. 2001:db8::2b). Used as the BGP source toward transit. |
<VPS_PUBKEY> / <HOME_PUBKEY> | WireGuard public keys, one per side. Printed during §4. |
<VPS_ROUTER_ID> / <HOME_ROUTER_ID> | Unique 32-bit BGP router IDs written like IPv4 addresses. |
<R64_LINK> | The /64 Route64 carves out for the BGP transport (portal calls it the point-to-point /64). ::1 is the PoP, ::2 is your home router. |
<R64_*> (other) | Other Route64 placeholders defined in the Route64 post §2. |
<ULA_PREFIX> | Your locally-generated ULA (same one the DNS post uses). |
<CLIENT_GUA> | A LAN client's GUA from <OWN_48>, used as the source in §8 induced-failover pings. |
The Route64 portal needs your <OWN_AS> set as the tunnel's
BYO ASN — the BGP session requires it. The field is on the
tunnel page next to the WireGuard config.
<OWN_48> substitution mirrors the equal-paths posts: <OWN_48>:0::1/64
on the tunnel link, <OWN_48>:1::/64 for the trusted VLAN, etc.
Two bird2 BGP sessions on the VPS: eBGP upstream to the transit
provider (carries your <OWN_48> outbound and ::/0 inbound), and
iBGP downstream to home (carries <OWN_48> inbound from home and
::/0 outbound). BFD only runs on the home-side session because that's
the one your <OWN_48> rides over and the one that needs sub-second
failure detection.
Each WireGuard tunnel needs the peer's public key in its own config, so
the keygen runs on both sides before either config is pasted. The VPS
script below prints <VPS_PUBKEY> at the end; <HOME_PUBKEY> comes
from running the /interface/wireguard add name=wg-host … line in §5
first and reading the auto-generated public key with
/interface/wireguard print where name=wg-host. The <R64_*> keys are
not generated locally — Route64 hands you <R64_PRIVKEY> and
<R64_SRV_PUBKEY> on the tunnel page.
Assumes a minimal Ubuntu install. bird listens on TCP/179 on all
interfaces by default; the nftables ruleset below scopes which NIC
each session is reachable on — transit NIC for the upstream session,
wg0 for the home session.
VPS — wg0 + bird2 (transit eBGP + home iBGP + BFD) + nftables
bash
1set -e
2apt-get update -qq
3apt-get install -y -qq wireguard bird2 nftables
4
5cat >/etc/sysctl.d/99-multi-homed.conf <<'EOF'
6net.ipv6.conf.all.forwarding = 1
7net.ipv6.conf.default.forwarding = 1
8net.ipv6.conf.<VPS_NIC>.accept_ra = 2
9EOF
10sysctl --system >/dev/null
11
12umask 077
13mkdir -p /etc/wireguard
14wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
15VPS_PRIVKEY=$(cat /etc/wireguard/server.key)
16
17cat >/etc/wireguard/wg0.conf <<EOF
18[Interface]
19PrivateKey = ${VPS_PRIVKEY}
20Address = <OWN_48>:0::1/64, fe80::1/64
21ListenPort = 51820
22MTU = 1420
23Table = off
24
25[Peer]
26PublicKey = <HOME_PUBKEY>
27AllowedIPs = <OWN_48>:0::2/128, <OWN_48>::/48
28PersistentKeepalive = 25
29EOF
30
31systemctl enable --now wg-quick@wg0
32echo "VPS public key: $(cat /etc/wireguard/server.pub)"
33
34cat >/etc/nftables.conf <<'EOF'
35#!/usr/sbin/nft -f
36flush ruleset
37
38table inet filter {
39 chain input {
40 type filter hook input priority filter; policy drop;
41
42 ct state established,related accept
43 ct state invalid drop
44 iif lo accept
45 meta l4proto { icmp, icmpv6 } accept
46
47 tcp dport 22 accept # SSH
48 udp dport 51820 accept # WireGuard from home
49 iifname "<VPS_NIC>" tcp dport 179 accept # BGP from transit
50 iifname "wg0" tcp dport 179 accept # BGP from home, wg0 only
51 iifname "wg0" udp dport 3784 accept # BFD from home, wg0 only
52 }
53
54 chain forward {
55 type filter hook forward priority filter; policy drop;
56
57 ct state established,related accept
58 iifname "wg0" ip6 saddr <OWN_48>::/48 accept # LAN -> internet, source must be ours
59 oifname "wg0" ip6 daddr <OWN_48>::/48 accept # internet -> LAN, dest must be ours
60 }
61
62 chain output {
63 type filter hook output priority filter; policy accept;
64 }
65}
66EOF
67systemctl enable --now nftables
68
69mkdir -p /etc/bird
70cat >/etc/bird/bird.conf <<'EOF'
71log syslog all;
72router id <VPS_ROUTER_ID>;
73
74protocol device { }
75
76protocol direct {
77 ipv6;
78 interface "wg0", "<VPS_NIC>";
79}
80
81protocol kernel kernel6 {
82 metric 32;
83 ipv6 {
84 # Pull only ::/0 from the kernel. This is what becomes locally-
85 # originated in BGP (empty AS_PATH) and what wins best-path on home.
86 import filter {
87 if net = ::/0 then accept;
88 reject;
89 };
90
91 # Install the BGP-learned <OWN_48> (from home) into the kernel so
92 # return traffic to home actually goes via wg0.
93 export filter {
94 if net = <OWN_48>::/48 then accept;
95 reject;
96 };
97 };
98 learn yes;
99}
100
101# Active BFD probing toward home. Without an explicit neighbor, bird is
102# passive; after a flap both sides wait for the other and recovery stalls.
103protocol bfd {
104 interface "wg0" {
105 min rx interval 200 ms;
106 min tx interval 200 ms;
107 idle tx interval 1 s;
108 multiplier 3;
109 };
110 neighbor <OWN_48>:0::2 dev "wg0";
111}
112
113# iBGP downstream to home — both ends in <OWN_AS>.
114protocol bgp home {
115 local <OWN_48>:0::1 as <OWN_AS>;
116 neighbor <OWN_48>:0::2 as <OWN_AS>;
117 bfd on;
118
119 ipv6 {
120 import filter {
121 if net = <OWN_48>::/48 then accept;
122 reject;
123 };
124
125 export filter {
126 if net = ::/0 then accept;
127 reject;
128 };
129
130 next hop self;
131 };
132}
133
134# eBGP upstream to transit — your <OWN_AS> peering with <TRANSIT_AS>.
135# AS 52025 hands you a peer address (<TRANSIT_PEER_V6>) that sits in a
136# different subnet from your on-link /64, so it's reached over your
137# default route and the session is multihop. `local` pins the source to
138# your on-link address (<VPS_GUA>); without it bird may source from the
139# wrong address and the peering won't come up.
140protocol bgp transit {
141 local <VPS_GUA> as <OWN_AS>;
142 neighbor <TRANSIT_PEER_V6> as <TRANSIT_AS>;
143 multihop 2;
144
145 ipv6 {
146 # AS 52025 is announce-only and sends nothing, so this imports no
147 # routes in practice. Kept defensive: the ::/0 home uses is the
148 # kernel-originated one above (empty AS_PATH), not anything learned
149 # here. Anything more-specific would be provider-routed; reject it.
150 import filter {
151 if net = ::/0 then accept;
152 reject;
153 };
154
155 # Originate only <OWN_48> upstream. ROA must already authorize this
156 # origin or transit will drop it via RPKI.
157 export filter {
158 if net = <OWN_48>::/48 then accept;
159 reject;
160 };
161 };
162}
163EOF
164chown -R bird:bird /etc/bird
165
166mkdir -p /etc/systemd/system/bird.service.d
167printf '[Service]\nRestart=on-failure\nRestartSec=2s\n' \
168 > /etc/systemd/system/bird.service.d/restart.conf
169systemctl daemon-reload
170systemctl enable --now birdThe explicit neighbor <OWN_48>:0::2 dev "wg0" line in the protocol bfd block is what turns this side into the active prober. Without
it bird is passive — it answers BFD packets but never originates
them. If the home side is also passive (or briefly drops out), both
ends sit waiting for the other and the session never re-establishes.
The config will work fine on first boot and fail to recover from the
first real outage.
Two separate protocol bgp blocks because the sessions have very
different shapes — iBGP to home over wg0 with BFD, eBGP to transit over
the public NIC without. The transit session has no BFD by default;
whether your transit provider supports it varies and is not required
for the multi-homed story to work. The kernel import filter taking
only ::/0 is what gives home an empty AS_PATH on the route — bird
treats kernel-learned routes as locally originated, and iBGP doesn't
prepend.
The forward chain's ip6 saddr <OWN_48>::/48 and ip6 daddr <OWN_48>::/48
guards are anti-spoof: the VPS will only forward outbound packets sourced
from your prefix and inbound packets destined to it. Without them
iifname "wg0" accept would let the home side egress with any source
address, and oifname "wg0" accept would let the VPS be used as a
generic router toward whatever happens to be reachable through wg0.
If you configure the public NIC's IPv6 by hand rather than via RA
(accept_ra=2 above) — on VyOS, static netplan, etc. — give it the
on-link /64, never the provider's full /48. A /48 on the
interface installs an on-link route for all 65k /64s it covers, so
the box answers its own NDP for addresses that actually live elsewhere
and silently drops them — the multihop transit peer above is exactly
one such address.
The configuration below is RouterOS-only. If your home router is
Linux/bird2 or VyOS, take §4 as the template: it's the same bird2
session shape you need at home, with the BGP roles swapped — home
originates <OWN_48> (kernel-import a static blackhole for it) and
imports ::/0 from both peers; VPS originates ::/0 (already shown)
and imports <OWN_48> from home. The Route64 session is plain eBGP
with no BFD.
Three blocks below — origination + aggregate, VPS session, Route64
session. The first block stands up the BGP instance and the
locally-originated /48 that both sessions advertise; the second and
third blocks are independent and either order works.
Home router — BGP instance + originated /48 (shared by both sessions)
bash
1/ipv6/route add dst-address=<OWN_48>::/48 blackhole distance=254 \
2 comment="aggregate-for-bgp"
3
4/ipv6/firewall/address-list add list=bgp-networks-own \
5 address=<OWN_48>::/48 comment="/48 to both upstreams"
6
7/routing/bgp/instance add name=default-bgp as=<OWN_AS> router-id=<HOME_ROUTER_ID>Home router — VPS path WireGuard + iBGP + BFD
bash
1/interface/wireguard add name=wg-host listen-port=51820 mtu=1420
2/interface/wireguard/peers add interface=wg-host name=vps \
3 public-key="<VPS_PUBKEY>" \
4 endpoint-address=<VPS_IP> endpoint-port=51820 \
5 allowed-address=::/0 \
6 persistent-keepalive=25s
7
8/ipv6/address add address=<OWN_48>:0::2/64 interface=wg-host advertise=no
9
10/routing/filter/rule add chain=bgp-in-vps \
11 rule="if (dst == ::/0) { accept } reject"
12/routing/filter/rule add chain=bgp-out-vps \
13 rule="if (dst == <OWN_48>::/48) { accept } reject"
14
15/routing/bgp/template add name=tpl-host as=<OWN_AS> use-bfd=yes
16/routing/bgp/connection add name=host-vps instance=default-bgp \
17 remote.address=<OWN_48>:0::1 remote.as=<OWN_AS> \
18 local.address=<OWN_48>:0::2 local.role=ibgp \
19 templates=tpl-host afi=ipv6 use-bfd=yes \
20 input.filter=bgp-in-vps \
21 output.network=bgp-networks-own output.filter-chain=bgp-out-vps
22
23/routing/bfd/configuration/add interfaces=wg-host \
24 min-rx=200ms min-tx=200ms multiplier=3
25
26# BFD's UDP/3784 packets arrive unsolicited, so the defconf
27# established,related,untracked rule doesn't cover them. The BGP
28# session itself is dialed out by home and needs no input rule.
29/ipv6/firewall/filter add chain=input action=accept protocol=udp dst-port=3784 \
30 in-interface=wg-host comment="BFD from VPS" \
31 place-before=[find where chain=input and comment="defconf: drop everything else not coming from LAN"]
32# place-before= pins the new accept above the defconf drop. If your
33# RouterOS version's defconf wording differs, [find ...] returns
34# nothing and the rule lands at the bottom (after the drop) — swap to
35# a numeric place-before= in that case.Home router — Route64 path WireGuard + eBGP, no BFD
bash
1/interface/wireguard add name=wg-route64 mtu=1420 \
2 listen-port=<R64_POP_PORT> private-key="<R64_PRIVKEY>"
3/interface/wireguard/peers add interface=wg-route64 \
4 public-key="<R64_SRV_PUBKEY>" \
5 endpoint-address=<R64_POP_IP> endpoint-port=<R64_POP_PORT> \
6 allowed-address=::/0 persistent-keepalive=15s
7/ipv6/address add address=<R64_LINK>::2/64 interface=wg-route64 advertise=no
8
9/routing/filter/rule add chain=bgp-in-r64 \
10 rule="if (dst == ::/0) { accept } reject"
11/routing/filter/rule add chain=bgp-out-r64 \
12 rule="if (dst == <OWN_48>::/48) { accept } reject"
13
14# Route64 portal must have BYO ASN set to <OWN_AS> for this to peer.
15/routing/bgp/connection add name=host-r64 instance=default-bgp \
16 remote.address=<R64_LINK>::1 remote.as=212895 \
17 local.address=<R64_LINK>::2 local.role=ebgp \
18 afi=ipv6 \
19 input.filter=bgp-in-r64 \
20 output.network=bgp-networks-own output.filter-chain=bgp-out-r64No input firewall rule is needed for either WireGuard peer at base —
both BGP sessions dial out from home on TCP/179, so the return traffic
rides the defconf established,related,untracked rule. The only
unsolicited inbound is BFD/3784, which exists only on wg-host.
The Route64 link uses Route64's <R64_LINK> /64 from the portal (the
point-to-point pair, ::1/::2) as the BGP session addressing — your
<OWN_48> does not appear in the Route64 transport at all; it appears
in the BGP advertisement that rides over it.
BFD is enabled where it works (AS 52025-class transits like the
VPS upstream above, where bird2 on a peer you operate runs BFD) and
skipped where it doesn't. Route64 does not reply to BFD —
use-bfd=yes on the host-r64 connection drops the BGP session — so
the home-router BGP configuration in §5 only attaches BFD to the
host-vps connection.
The host-r64 connection runs plain BGP liveness — the only option,
since Route64 doesn't support BFD — and that's fine for the
backup-path role it occupies: the fast-failover work is done by the
primary's BFD. When BFD says the VPS path is dead, RouterOS withdraws
the VPS-learned ::/0 within ~1.5 s and best-path immediately
switches to the Route64-learned ::/0, which is already in BGP, just
not active.
| Failure mode | What detects it | Recovery time |
|---|---|---|
| VPS WG path dead, Route64 still up | BFD on wg-host | ~1.5 s to switch |
| Route64 path dead, VPS still up | irrelevant — VPS is already primary; no client-visible event | 0 |
| Both paths dead simultaneously | netwatch (§7) | ~6–8 s to drain LAN RA |
| VPS path recovers, Route64 was active | BFD comes back up | ~10 s to reinstall |
The second row is the asymmetry worth internalizing. If only Route64
fails, nothing happens at the LAN — Route64's ::/0 is the backup, not
the active path, so its withdrawal is invisible to clients.
If both upstreams fail at once, neither BGP session has a route to
withdraw against — there is no surviving primary to fail over to. The
LAN would happily continue picking the home router as its IPv6 default
even though the router can't reach anything beyond <OWN_48>. The
mitigation, lifted directly from the Route64 post §6,
is a netwatch that drains LAN RA so clients fall back to IPv4 by Happy
Eyeballs.
Home router — netwatch drains LAN RA when both paths are gone
bash
1/tool/netwatch add name=v6health type=tcp-conn \
2 host=2606:4700:4700::1111 port=443 \
3 interval=5s timeout=2s comment=v6health \
4 up-script="/ipv6/nd set [find interface=bridge] ra-lifetime=30m; /ipv6/nd set [find interface=vlan-iot] ra-lifetime=30m; /ipv6/nd set [find interface=vlan-guest] ra-lifetime=30m; :log info v6-up" \
5 down-script="/ipv6/nd set [find interface=bridge] ra-lifetime=0s; /ipv6/nd set [find interface=vlan-iot] ra-lifetime=0s; /ipv6/nd set [find interface=vlan-guest] ra-lifetime=0s; :log info v6-down-failedtoIPv4"The probe is a TCP-connect to Cloudflare's IPv6 anycast on 443 — it
exits via whichever ::/0 is active, so a single netwatch covers both
paths. It does not distinguish VPS-down from Route64-down, by design:
BFD + BGP best-path already handle single-path failures invisibly, so
the netwatch only needs to fire when neither path is forwarding —
the one state where the LAN must drop IPv6 entirely.
RouterOS 7.22's IPv6 ICMP netwatch is broken (type=icmp stays "down"
for v6 targets even when /ping works) — type=tcp-conn is the
working replacement. End-to-end client failover is netwatch detection
(~5–7 s at interval=5s timeout=2s) plus the ~0.6 s
unsolicited RA RouterOS emits whenever ra-lifetime changes ≈
~6–8 s, symmetric on recovery. The set [find interface=...]
lines start working as soon as Per-VLAN IPv6
is applied (substitution: <GUA_LAN>=<OWN_48>:1,
<GUA_IOT>=<OWN_48>:10, <GUA_GUEST>=<OWN_48>:20).
Home router — both sessions healthy
bash
1# Both BGP sessions established
2/routing/bgp/session/print
3# expect: host-vps established
4# host-r64 established
5
6# BFD up on the VPS session only
7/routing/bfd/session/print
8# expect: one entry, state=up, peer=<OWN_48>:0::1
9
10# Active default route
11/routing/route/print where dst-address=::/0
12# expect: two ::/0 candidates; the bgp.as-path=[] one is active
13
14# The active default's gateway is the VPS WG endpoint
15/ipv6/route/print where active and dst-address=::/0
16# expect: gateway=<OWN_48>:0::1%wg-host
17
18# netwatch healthy
19/tool/netwatch/print where comment=v6health
20
21# BFD/3784 input rule lives above the defconf drop.
22# The §5 place-before=[find ...] depends on the defconf comment string
23# in your RouterOS version; if it doesn't match, the rule lands at the
24# bottom of the chain (after the drop) and BFD silently never arrives.
25/ipv6/firewall/filter print
26# expect: the chain=input udp dst-port=3784 rule above the
27# "defconf: drop everything else not coming from LAN" ruleVPS — transit + home sessions healthy
bash
1birdc show protocols
2# expect: home (bgp) Established
3# transit (bgp) Established
4# bfd1 (bfd) Up
5
6birdc show route ::/0 all
7# expect: best path via wg0 to <OWN_48>:0::2 from home (iBGP, AS_PATH empty)
8# plus transit's ::/0 from <TRANSIT_PEER_V6>
9
10birdc show route <OWN_48>::/48 all
11# expect: via wg0 from home (learned by iBGP), exported to transitVerify primary→backup pivot
bash
1# On the VPS, drop BFD packets to simulate a half-dead tunnel.
2# BFD declares down in ~700 ms; bird withdraws ::/0 from home; home's
3# best-path picks the Route64-learned ::/0.
4nft add rule inet filter input iifname wg0 udp dport 3784 drop
5
6# On home router: time it.
7/log/print where message~"BGP|BFD"
8/ipv6/route/print where active and dst-address=::/0
9# expect: gateway now <R64_LINK>::1%wg-route64
10
11ping -c 5 -I <CLIENT_GUA> 2606:4700:4700::1111
12# expect: 1 dropped packet at the switchover, rest reach via Route64
13
14# Undo it — BFD comes back, best-path returns to VPS.
15nft -a list ruleset | grep "udp dport 3784 drop" # note the handle
16nft delete rule inet filter input handle <handle>Verify backup-only failure is invisible to the LAN
bash
1# Disable Route64 alone. Because VPS is already best-path, this should
2# produce no active-route change and no v6-down log line — Route64
3# leaves BGP, the LAN sees nothing.
4/interface/wireguard/peers/disable [find interface=wg-route64]
5/ipv6/route/print where active and dst-address=::/0
6# expect: gateway still <OWN_48>:0::1%wg-host
7/log/print where message~"v6-(up|down)"
8# expect: no new entries
9
10/interface/wireguard/peers/enable [find interface=wg-route64]Verify both-paths-dead fallback
bash
1# Stop WireGuard on the VPS and disable Route64 on home simultaneously.
2# Within ~8 s the netwatch fires; clients drop their v6 default and
3# Happy-Eyeballs to IPv4.
4ssh vps "wg-quick down wg0"
5/interface/wireguard/peers/disable [find interface=wg-route64]
6/log/print where message~"v6-(up|down)"
7
8# Restore.
9/interface/wireguard/peers/enable [find interface=wg-route64]
10ssh vps "wg-quick up wg0"A brief IPv6 miss at the switchover plus a 6–8 s window before clients drop to IPv4 when both paths die — instead of either a hold-time-long blackhole or a permanent LAN-thinks-it-has-v6 stall — is the whole point of this build.
After both BGP sessions are established and verified, continue to
Per-VLAN IPv6 on RouterOS to plumb
<OWN_48> through to every VLAN — per-VLAN GUA + ULA + RA RDNSS, IPv6
forward-chain isolation, and SLAAC anti-spoof, using the substitution
<GUA_LAN>=<OWN_48>:1, <GUA_IOT>=<OWN_48>:10,
<GUA_GUEST>=<OWN_48>:20.
| Acronym | Expansion | Reference |
|---|---|---|
| AS / ASN | Autonomous system (number) | RFC 1930 |
| BFD | Bidirectional Forwarding Detection | RFC 5880 |
| BGP | Border Gateway Protocol | RFC 4271 |
| eBGP | external BGP (across AS boundary) | RFC 4271 §3.2 |
| iBGP | internal BGP (within one AS) | RFC 4271 §3.2 |
| LIR | Local Internet Registry | RIPE NCC |
| ROA | Route Origin Authorization (RPKI) | RFC 6482 |
| RPKI | Resource Public Key Infrastructure | RFC 6480 |
Series-wide acronyms (CGNAT, DoH, GUA, RA, RDNSS, SLAAC, ULA, VLAN, VPS, WireGuard) live in the index glossary.
/48/routing/bgp referenceDisclosure: the NoPkt and pdxnet.co.uk links above are affiliate links — signing up through them earns me a referral credit at no extra cost to you. I am recommending both providers because I use them on this build, not because of the credit.
Comments
Comments are powered by GitHub Discussions and require a free GitHub account to post.