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.
Build log · MikroTik RB5009 · VPS-routed /48
$3/mo VPS that routes a /48, WireGuard from the RB5009, eBGP between them. The VPS path of the CGNAT series.
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 post — vlan-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.
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:
/48 aggregate, not per-/64, so adding or removing
subnets under it never triggers reconvergence or VPS re-config.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.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.
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
Series-wide placeholders (<ULA_PREFIX>) are in the
index §2. The VPS path adds:
| Placeholder | Meaning |
|---|---|
<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.
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 ::/0MikroTik 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.
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 birdThe 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-vpsNo 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.
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"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 item | On-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 space | one /64 | /48 — 65 k /64s |
| Extra daemon on VPS | ndppd | none |
| 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.
| Acronym | Expansion | Reference |
|---|---|---|
| AS / ASN | Autonomous system (number) | RFC 1930 |
| BFD | Bidirectional Forwarding Detection | RFC 5880 |
| BGP | Border Gateway Protocol | RFC 4271 |
| GRE | Generic Routing Encapsulation | RFC 2784 |
| IKEv2 | Internet Key Exchange v2 | RFC 7296 |
| IPsec | Internet Protocol Security | RFC 4301 |
| NDP | Neighbor Discovery Protocol | RFC 4861 |
| OpenVPN | OpenVPN tunnel daemon | Wikipedia |
| SA | Security association (IPsec) | RFC 4301 |
| WAN | Wide area network | Wikipedia |
Series-wide acronyms (CGNAT, DoH, GUA, RA, RDNSS, SLAAC, ULA, VLAN, VPS, WireGuard) live in the index glossary.
/routing/bgp referenceComments
Comments are powered by GitHub Discussions and require a free GitHub account to post.