IPv6-over-WireGuard relay — VPS with a routed /48 + MikroTik
This is the simpler counterpart of the Vultr/on-link /64 recipe. Use this gist when your VPS provider routes a real prefix to your instance (e.g. WebHorizon). Use the older gist when your provider only assigns on-link IPv6 (e.g. Vultr) and you have to NDP-proxy.
When the prefix is routed, ndppd disappears, the reserved-IP fee disappears, and the address plan opens up — you have 65k /64s to carve up however you want.
| Item | Vultr (on-link /64) | This (routed /48) |
|---|---|---|
| VPS plan | $5/mo | $3/mo (e.g. WebHorizon SG, 1 TB) |
| Reserved IPv6 | $3/mo | $0 (already routed) |
| Bandwidth overage | see Vultr's published rate | $0.0025/GB |
| LAN address plan | one /64 | one /48 — 65k /64s |
| Extra daemon on VPS | ndppd |
none |
| Total fixed | $8/mo | $3/mo |
Internet
│
┌──────────┴──────────┐
│ VPS (Ubuntu) │
│ <VPS_IP> │
│ enp3s0: │
│ <UPSTREAM /128> ← provider link addr (untouched)
│ default v6 via provider gateway
│ wg0: <LAN_PREFIX>:0::1/64 ← WG transit /64 from your /48
└──────────┬──────────┘
│ WireGuard, UDP/51820
│
┌──────────┴──────────┐
│ MikroTik v7 │
│ wg-host: │
│ <LAN_PREFIX>:0::2/64
│ bridge (LAN): │
│ <LAN_PREFIX>:1::1/64 (SLAAC to clients)
│ <ULA_PREFIX>::1/64 (ULA, SLAAC + DNS)
└──────────┬──────────┘
│
LAN clients
(SLAAC <LAN_PREFIX>:1::/64 + ULA + RDNSS)
For multiple VLANs/segments, allocate further /64s from the same /48:
<LAN_PREFIX>:0::/64 — WG transport
<LAN_PREFIX>:1::/64 — main LAN
<LAN_PREFIX>:10::/64 — guest VLAN
<LAN_PREFIX>:11::/64 — IoT VLAN
…
Substitute these before pasting:
| Placeholder | Where to get it |
|---|---|
<VPS_IP> |
VPS provider panel → instance public IPv4 |
<VPS_NIC> |
Provider's NIC name (enp3s0, ens3, etc. — ip -o link to find) |
<UPSTREAM_ADDR> / <UPSTREAM_GW> |
The /128 link address + gateway the provider configured (if not auto-configured via RA) |
<LAN_PREFIX> |
The /48 (or /56) routed to your VPS — e.g. 2001:db8 if /48 is 2001:db8::/48 |
<ULA_PREFIX> |
A locally-generated ULA /64, e.g. fdXX:XXXX:XXXX:1. Generate the /48 with python3 -c 'import secrets; h=secrets.token_hex(5); print(f"fd{h[:2]}:{h[2:6]}:{h[6:]}".lower())' and append :1 (or any subnet id) for the /64. |
<VPS_PRIVKEY> / <VPS_PUBKEY> |
wg genkey | tee server.key | wg pubkey on the VPS |
<MT_PUBKEY> |
After creating the wireguard interface on MikroTik, /interface/wireguard/print shows it |
<LAN_BRIDGE> |
MikroTik LAN bridge name — bridge in defconf |
Examples below use
<LAN_PREFIX>=2001:db8,<ULA_PREFIX>=fd00:dead:beef:1. These are documentation prefixes from RFC 3849 / RFC 4193 — substitute your own.
This recipe assumes the provider routes a prefix to your instance. Confirm in the panel that your IPv6 allocation says "routed /48" or "routed /56" (or that the prefix is a different /48 than the on-link transport subnet). If it's a single on-link /64 you can't subnet, you're in Vultr-land — see the other gist.
Reference setup used to test this recipe:
- WebHorizon SG — routed /48, $3/mo, 1 TB included, $2.50/TB overage
Other providers may also route prefixes on certain plans. Check your provider's panel and docs (look for phrasing like "routed /48", "routed /56", or "delegated prefix") before assuming. Don't infer behavior from the on-link /64 they hand you for the instance itself.
set -e
apt-get update -qq
apt-get install -y -qq wireguard
cat >/etc/sysctl.d/99-wg-relay.conf <<'EOF'
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
net.ipv6.conf.<VPS_NIC>.accept_ra = 2
EOF
sysctl --system >/dev/null
umask 077
mkdir -p /etc/wireguard
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
VPS_PRIVKEY=$(cat /etc/wireguard/server.key)
cat >/etc/wireguard/wg0.conf <<EOF
[Interface]
PrivateKey = ${VPS_PRIVKEY}
Address = <LAN_PREFIX>:0::1/64
ListenPort = 51820
MTU = 1420
[Peer]
# MikroTik — the NAT-traversing side runs PersistentKeepalive; not needed here.
PublicKey = <MT_PUBKEY>
AllowedIPs = <LAN_PREFIX>:0::2/128, <LAN_PREFIX>:1::/64
EOF
# Firewall: allow WG, allow forwarding for the LAN prefix
ufw allow 51820/udp comment "WireGuard"
ufw route allow in on wg0
ufw route allow out on wg0
ufw reload || true
systemctl enable --now wg-quick@wg0
# Print VPS public key — you need it on the MikroTik side
echo "VPS public key: $(cat /etc/wireguard/server.pub)"No ndppd. Because the /48 is routed, the provider's gateway sends packets for any address in it directly to your VPS. The kernel then forwards them through wg0 based on AllowedIPs. Done.
# Create WG interface (RouterOS auto-generates its private key)
/interface/wireguard add name=wg-host listen-port=51820 mtu=1420
# Get the MikroTik's public key, then add the VPS as a peer
/interface/wireguard/peers add interface=wg-host name=vps \
public-key="<VPS_PUBKEY>" \
endpoint-address=<VPS_IP> endpoint-port=51820 \
allowed-address=::/0 \
persistent-keepalive=25s
# Address on WG transport /64
/ipv6/address add address=<LAN_PREFIX>:0::2/64 interface=wg-host advertise=no
# Address on LAN bridge — RA will pick this up automatically
/ipv6/address add address=<LAN_PREFIX>:1::1/64 interface=<LAN_BRIDGE> advertise=yes
/ipv6/address add address=<ULA_PREFIX>::1/64 interface=<LAN_BRIDGE> advertise=yes comment="ULA RFC 4193"
# Default v6 route via the VPS
/ipv6/route add dst-address=::/0 gateway=<LAN_PREFIX>:0::1%wg-host \
comment="webhorizon primary"
# RA on bridge (skip if defconf RA is already covering bridge)
/ipv6/nd add interface=<LAN_BRIDGE> advertise-dns=yes dns=<ULA_PREFIX>::1 \
managed-address-configuration=no other-configuration=no
# Allow WG-side inbound on input chain (defconf drops non-LAN input)
/ipv6/firewall/filter add chain=input action=accept in-interface=wg-host \
comment="accept input from VPS WG peer" \
place-before=[find where chain=input and comment="defconf: drop everything else not coming from LAN"]
On the VPS:
wg show # latest handshake should be < 3 min
ping6 -c 2 <LAN_PREFIX>:0::2 # WG transit reachable (MT side)
ping6 -c 2 <LAN_PREFIX>:1::1 # MT LAN gateway reachable via wg0On the MikroTik:
/ping 2606:4700:4700::1111 count=3 # Cloudflare via VPS
On a LAN client:
ip -6 addr | grep <LAN_PREFIX> # confirm SLAAC picked up the /64
ping6 -c 3 2606:4700:4700::1111
curl -6 -s https://test-ipv6.com/json/ # expect "score":"10/10"| Line item | Cost |
|---|---|
| VPS (routed /48, SG, 1 TB transfer) | $3.00 / mo |
| Bandwidth overage if exceeded | $0.0025/GB |
| Total | $3.00 / mo |
Compare with the Vultr variant ($8/mo) — $5/mo saved and a bigger prefix.
| Situation | Use |
|---|---|
| Provider gives a routed /48 or /56 | this gist |
| Provider only does on-link /64 (Vultr) | Vultr gist |
| You need SLAAC on more than one VLAN | this gist |
| You want a low-cost 24/7 SG relay with bundled bandwidth and a routed prefix | this gist |
| You're stuck with Vultr for other reasons | Vultr gist |