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. Pick an Ubuntu/BIRD, VyOS, or CHR relay implementation.
Build log · MikroTik RB5009 · VPS-routed /48
$3/mo VPS that routes a /48, WireGuard from the RB5009, eBGP between them. Pick an Ubuntu/BIRD, VyOS, or CHR relay implementation.
The VPS path in the MikroTik RB5009 home-network 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 home router 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 matter 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 home router 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 home router; 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 home router's
forward chain, where it is colocated with the device it protects.wg-quick@wg0, bird, nftables) plus
persistent-keepalive=25s on the home router's 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 home │
└─────────────┬─────────────┘
│ WireGuard / UDP 51820
│ (IPv6 transit + eBGP)
│
home 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> / <HOME_PUBKEY> | WireGuard public keys, one per side. Each is printed during §4. |
<VPS_AS> / <HOME_AS> | Private 2-byte ASNs (RFC 6996, 64512–65534) for the BGP session. |
<VPS_ROUTER_ID> / <HOME_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 ::/0The home router 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 home router 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.
If the VPS itself is running VyOS instead, use the
VyOS relay variant and keep the same
home-side BGP/WireGuard contract.
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 = <HOME_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 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" 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 # home router 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 home {
102 local <LAN_PREFIX>:0::1 as <VPS_AS>;
103 neighbor <LAN_PREFIX>:0::2 as <HOME_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 home router 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.
Home router 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=<HOME_AS> router-id=<HOME_ROUTER_ID>
22/routing/bgp/template add name=tpl-host as=<HOME_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 input rule is needed on the home router. The BGP session is initiated
outbound from the home router 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.
The home router's listen-port=51820 above is the WireGuard source port —
it always initiates from behind CGNAT and never receives an unsolicited WG
packet, so the value is cosmetic and need not match the VPS's port. Pick
anything if you ever stand up a second peer on the same router.
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 RouterOS 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.
If you'd rather run this path alongside Route64 under one announceable
/48 with BGP best-path failover, see
Multi-homing IPv6 over CGNAT on RouterOS
— the series finale. It replaces the WAN-side plumbing of this post with
a self-contained multi-homed build (own ASN, announceable /48 as hard
prerequisites) and supersedes the failover companion's single-session
BFD with a two-session BGP best-path.
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 -> home 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 # home router: 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.