Build log · MikroTik RB5009 · VPS-routed /48

Routed IPv6 for a segmented IPv4-only LAN behind CGNAT

$3/mo VPS that routes a /48, WireGuard from the RB5009, eBGP between them. The VPS path of the CGNAT series.

Overview

The VPS path in the RB5009 CGNAT series: recover real, routable IPv6 over a CGNAT'd line by paying a $3/month VPS to route a /48 to its instance, terminating a WireGuard tunnel from the RB5009 on it, and exchanging routing intent over eBGP. You operate the endpoint, so the prefix, the return routing, and the failure mode are yours to define.

This post assumes you already have a working, segmented IPv4 LAN from the VLAN companion postvlan-iot, vlan-guest, the inter-VLAN firewall, per-VLAN DHCPv4 — and that you have read the index's path-choice matrix and decided the VPS path's trade-offs are the right ones for you. The sibling — Route64's free /56, no server to run — is its own post: Routed IPv6 over CGNAT via Route64.

Every numbered section below is paste-ready against a defconf RouterOS v7 setup with the VLAN layer already in place; italicized notes are the rationale.

Design decisions

Two choices in this path are load-bearing enough that a reader substituting parts will want a sentence of justification. The series-wide WireGuard rationale is in the index; the ones below are specific to running your own relay.

eBGP rather than static routes between the RB5009 and the VPS. The provider routes the /48 to the VPS and configures one address from it on the public NIC at /48 mask, which installs a connected route for the entire /48 on the public interface. If WireGuard also installs the same /48 from AllowedIPs, the two routes compete and return traffic falls out the public NIC. The fix splits the responsibilities: WireGuard's AllowedIPs stays broad enough for its cryptokey checks while Table=off keeps that from turning into a Linux route; BGP then carries the home aggregate one way and ::/0 the other. This is the one detail that silently breaks the setup — egress works, ping works, then the first packet to a SLAAC client falls off the public NIC and the network looks broken in a way no log line explains.

The VPS is configured once and never touched for new home services. All gating lives on the MikroTik; the VPS is pure transit. The relay transports prefixes, not applications. Three specifics keep it that way:

  • BGP advertises the /48 aggregate, not per-/64, so adding or removing subnets under it never triggers reconvergence or VPS re-config.
  • The VPS nftables forward chain is interface-based (iifname "wg0" accept / oifname "wg0" accept), not address- or port-based, so a new host, port, or VLAN inside the /48 transits without a VPS change. Per-service policy belongs on the MikroTik forward chain, where it is colocated with the device it protects.
  • Persistence is service-enable (wg-quick@wg0, bird, nftables) plus persistent-keepalive=25s on the MikroTik peer, so a VPS reboot re-establishes the tunnel and BGP without operator intervention.

The narrow exceptions — a service running on the VPS itself, adding BFD (see the failover companion), a second WG peer, a public-NIC rename, or the provider re-allocating the prefix — all touch named config that the snippets below call out by line.

1. Topology recap

The series-wide topology lives in the index §1. The VPS-specific piece:

                       Internet (IPv4 + IPv6)
                              │
                ┌─────────────┴─────────────┐
                │  VPS — Ubuntu, routed /48 │
                │  <VPS_IP>                 │
                │  enp3s0: provider GUA     │
                │  wg0:    <LAN_PREFIX>:0::1│
                │  bird2:  eBGP to RB5009   │
                └─────────────┬─────────────┘
                              │ WireGuard / UDP 51820
                              │ (IPv6 transit + eBGP)
                              │
                          RB5009 wg-host
                          <LAN_PREFIX>:0::2

2. Conventions and placeholders

Series-wide placeholders (<ULA_PREFIX>) are in the index §2. The VPS path adds:

PlaceholderMeaning
<LAN_PREFIX>Routed IPv6 /48 the VPS hands you. Drop the trailing zeros, e.g. 2001:db8.
<VPS_IP>VPS public IPv4 from the provider panel.
<VPS_NIC>VPS public interface. ip -o link shows it (typical: enp3s0, ens3).
<VPS_PUBKEY> / <MT_PUBKEY>WireGuard public keys, one per side. Each is printed during §4.
<VPS_AS> / <MT_AS>Private 2-byte ASNs (RFC 6996, 64512–65534) for the BGP session.
<VPS_ROUTER_ID> / <MT_ROUTER_ID>Any unique 32-bit router IDs written like IPv4 addresses.

The <LAN_PREFIX> convention lets <LAN_PREFIX>:1::/64 expand to 2001:db8:0:1::/64 and so on, mirroring VLAN numbers in the slice ID.

3. Return routing: WireGuard authorizes, BGP routes

The provider routes the /48 to the VPS, then configures one address from it on the public NIC at /48 mask. That installs a connected route for the entire /48 on the public interface. If WireGuard also installs the same /48 from AllowedIPs, the two routes compete and return traffic can fall out the public NIC.

The fix is to split the responsibilities:

text

text

1WireGuard AllowedIPs = peer authorization for the tunnel and LAN prefix 2Table = off = do not turn AllowedIPs into Linux routes 3BGP = exchange <LAN_PREFIX>::/48 and ::/0

MikroTik advertises the home aggregate to the VPS over plain BGP. The VPS advertises only ::/0 back. Without BFD, liveness is normal BGP liveness: clean and dynamic, but not sub-second. The BGP+BFD companion adds fast failure detection to this same session.

BGP gives the VPS one explicit route for the whole home prefix and gives the MikroTik one explicit default route, while AllowedIPs stays broad enough for WireGuard's cryptokey checks.

4. VPS — WireGuard relay and BGP

Assumes a minimal Ubuntu install — nothing layered on top, no host firewall yet. bird listens on TCP/179 wildcard (one listener per address family, shared across all bgp protocol blocks; local <addr> only constrains source dispatch, not the listener bind), so leaving the box unfirewalled puts a BGP parser on the public internet. The nftables ruleset below restricts TCP/179 to wg0 while keeping SSH, WireGuard, and ICMP reachable.

VPS wg0 + bird2 BGP + nftables

bash

1set -e 2apt-get update -qq 3apt-get install -y -qq wireguard bird2 nftables 4 5cat >/etc/sysctl.d/99-wg-relay.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 = <LAN_PREFIX>:0::1/64, fe80::1/64 21ListenPort = 51820 22MTU = 1420 23Table = off 24 25[Peer] 26PublicKey = <MT_PUBKEY> 27# Authorize the tunnel peer and LAN prefix. Table=off keeps this from 28# becoming a Linux route; bird installs the kernel route learned by BGP. 29AllowedIPs = <LAN_PREFIX>:0::2/128, <LAN_PREFIX>::/48 30PersistentKeepalive = 25 31EOF 32 33systemctl enable --now wg-quick@wg0 34echo "VPS public key: $(cat /etc/wireguard/server.pub)" 35 36cat >/etc/nftables.conf <<'EOF' 37#!/usr/sbin/nft -f 38flush ruleset 39 40table inet filter { 41 chain input { 42 type filter hook input priority filter; policy drop; 43 44 ct state established,related accept 45 ct state invalid drop 46 iif lo accept 47 meta l4proto { icmp, icmpv6 } accept 48 49 tcp dport 22 accept # SSH 50 udp dport 51820 accept # WireGuard 51 iifname "wg0" tcp dport 179 accept # BGP from MikroTik, 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" accept # LAN -> internet via VPS 59 oifname "wg0" accept # internet -> LAN return 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"; 79} 80 81protocol kernel kernel6 { 82 metric 32; 83 ipv6 { 84 # Learn only the VPS default route from Linux. Do not import the 85 # provider's connected <LAN_PREFIX>::/48 from the public NIC; the 86 # MikroTik will advertise the home aggregate over BGP. 87 import filter { 88 if net = ::/0 then accept; 89 reject; 90 }; 91 92 # Install the BGP-learned home aggregate into the VPS kernel table. 93 export filter { 94 if net = <LAN_PREFIX>::/48 then accept; 95 reject; 96 }; 97 }; 98 learn yes; 99} 100 101protocol bgp mikrotik { 102 local <LAN_PREFIX>:0::1 as <VPS_AS>; 103 neighbor <LAN_PREFIX>:0::2 as <MT_AS>; 104 105 ipv6 { 106 import filter { 107 if net = <LAN_PREFIX>::/48 then accept; 108 reject; 109 }; 110 111 export filter { 112 if net = ::/0 then accept; 113 reject; 114 }; 115 116 next hop self; 117 }; 118} 119EOF 120chown -R bird:bird /etc/bird 121systemctl enable --now bird

5. MikroTik — WireGuard client and BGP

The MikroTik side has two jobs: advertise the home aggregate back to the VPS, and learn the IPv6 default route from the VPS. This keeps WireGuard as transport and BGP as routing intent. The companion post Fast IPv6 failover on RouterOS adds BFD for faster failure detection if normal BGP liveness is not fast enough.

MikroTik WireGuard + BGP

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=<LAN_PREFIX>:0::2/64 interface=wg-host advertise=no 9 10/ipv6/route add dst-address=<LAN_PREFIX>::/48 blackhole distance=254 \ 11 comment="aggregate-for-bgp" 12 13/ipv6/firewall/address-list add list=bgp-networks-vps \ 14 address=<LAN_PREFIX>::/48 comment="aggregate to VPS" 15 16/routing/filter/rule add chain=bgp-in-vps \ 17 rule="if (dst == ::/0) { accept } reject" 18/routing/filter/rule add chain=bgp-out-vps \ 19 rule="if (dst == <LAN_PREFIX>::/48) { accept } reject" 20 21/routing/bgp/instance add name=default-bgp as=<MT_AS> router-id=<MT_ROUTER_ID> 22/routing/bgp/template add name=tpl-host as=<MT_AS> 23/routing/bgp/connection add name=host-vps instance=default-bgp \ 24 remote.address=<LAN_PREFIX>:0::1 remote.as=<VPS_AS> \ 25 local.address=<LAN_PREFIX>:0::2 local.role=ebgp \ 26 templates=tpl-host afi=ipv6 \ 27 input.filter=bgp-in-vps \ 28 output.network=bgp-networks-vps output.filter-chain=bgp-out-vps

No new MikroTik input rule is needed here. The BGP session is initiated outbound from the MikroTik on TCP/179, so the return traffic is accepted by the defconf established,related,untracked rule. ICMPv6 over the tunnel is already covered by the defconf accept-ICMPv6 rule.

At this point the router has a working IPv6 default route over WireGuard, but no LAN client has a GUA yet. Continue to Per-VLAN IPv6 on the RB5009 to plumb the /48 through to every VLAN — per-VLAN GUA + ULA + RA RDNSS, IPv6 forward-chain isolation, and SLAAC anti-spoof, using the substitution <GUA_LAN>=<LAN_PREFIX>:1, <GUA_IOT>=<LAN_PREFIX>:10, <GUA_GUEST>=<LAN_PREFIX>:20.

6. Relay-side verification

The VPS-specific checks are below — they prove the WireGuard tunnel, the BGP session, and the kernel route on the VPS are all correct. LAN-side checks (client GUA, ping6 from a VLAN, anti-spoof drop counter) live in the Per-VLAN IPv6 post; the one-glance path-agnostic check is the screenshot at the index §7.

VPS-side and tunnel smoke tests

bash

1# Tunnel and BGP 2wg show # on VPS: handshake < 3 min 3ping6 -c 2 <LAN_PREFIX>:0::2 # VPS -> MikroTik WG endpoint 4birdc show route <LAN_PREFIX>::/48 # VPS: BGP-learned return route 5ip -6 route show <LAN_PREFIX>::/48 # VPS: proto bird, metric 32, dev wg0 6/routing/bgp/session/print # MikroTik: established 7ip -6 route get <client-IPv6-addr> # on VPS: expect "dev wg0"

A. Appendix — Cost and provider notes

The IPv6 leg is the only recurring cost in this path, and it depends entirely on whether the VPS provider routes a prefix to the instance or only hands out an on-link /64. Providers known to route prefixes (verify on your specific plan): WebHorizon SG (/48), Hetzner Cloud (/64 effectively routed via NDP-proxy), Linode/Akamai (/64 default, /56 on request), Oracle Cloud (/56).

Line itemOn-link /64 (e.g. Vultr)Routed /48 (this build)
VPS plan$5 / mo$3 / mo (routed /48, 1 TB)
Reserved IPv6 fee$3 / mo$0
Bandwidth overage$0.01 / GB$0.0025 / GB
LAN address spaceone /64/48 — 65 k /64s
Extra daemon on VPSndppdnone
Fixed total$8 / mo$3 / mo

The metered 1 TB cap is what motivates the index §5 ULA-only update — without it, streaming on the main SSID burns through the cap for no reason.

Glossary

AcronymExpansionReference
AS / ASNAutonomous system (number)RFC 1930
BFDBidirectional Forwarding DetectionRFC 5880
BGPBorder Gateway ProtocolRFC 4271
GREGeneric Routing EncapsulationRFC 2784
IKEv2Internet Key Exchange v2RFC 7296
IPsecInternet Protocol SecurityRFC 4301
NDPNeighbor Discovery ProtocolRFC 4861
OpenVPNOpenVPN tunnel daemonWikipedia
SASecurity association (IPsec)RFC 4301
WANWide area networkWikipedia

Series-wide acronyms (CGNAT, DoH, GUA, RA, RDNSS, SLAAC, ULA, VLAN, VPS, WireGuard) live in the index glossary.

B. References

Share

Comments

Comments are powered by GitHub Discussions and require a free GitHub account to post.