IPv6-over-WireGuard: Linode Gateway + GL.iNet AP — Setup Reference
Part 1: Linode Server
1. Provision
- Spin up a Nanode (or similar), Ubuntu/Debian.
- Note the Linode ID (visible on the Linode's overview page).
2. Allocate a routed IPv6 /64
Via API/CLI (the Cloud Manager GUI's "Add an IP Address" button doesn't exist on the newer Linode Interfaces networking model — this is the reliable path regardless of UI version):
linode-cli networking v6-range-create --linode_id <LINODE_ID> --prefix_length 64
Output gives you a range (your routed /64) and route_target (should match the
Linode's existing SLAAC address). No interface-level config is needed on the VM
for this to work — Linode's fabric already routes the range to the VM; you just
need forwarding enabled (next step) and WireGuard's AllowedIPs to claim it.
3. Install WireGuard, generate keys
apt install wireguard
wg genkey | tee server_private.key | wg pubkey > server_public.key
wg genkey | tee client_private.key | wg pubkey > client_public.key
4. Enable IP forwarding — persistently
cat <<'EOF' > /etc/sysctl.d/99-wireguard-forwarding.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
sysctl --system
⚠️ Gotcha: this is easy to forget and won't produce an obvious error — traffic just silently doesn't forward. Always verify with
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwardingbefore troubleshooting anything else.
5. Server config — /etc/wireguard/wg0.conf
[Interface]
PrivateKey = <SERVER_PRIVKEY>
Address = 10.10.0.1/24, fd10:10:10::1/64
ListenPort = 51820
PostUp = iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = ip6tables -A FORWARD -i wg0 -j ACCEPT
PostUp = ip6tables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
PostDown = ip6tables -D FORWARD -i wg0 -j ACCEPT
PostDown = ip6tables -D FORWARD -o wg0 -j ACCEPT
[Peer]
# <device name>
PublicKey = <CLIENT_PUBKEY>
AllowedIPs = 10.10.0.2/32, fd10:10:10::2/128, <YOUR_ROUTED_V6_PREFIX>
⚠️ Gotcha: don't add a manual
ip -6 route add <prefix> dev wg0line.wg-quickautomatically installs a route for every prefix in a peer'sAllowedIPs— a duplicate manual route-add fails with "File exists" and can abort the wholewg-quick up(PostUp failures are fatal).
6. Bring it up and enable at boot
systemctl enable --now wg-quick@wg0
active (exited) is the correct healthy status for this oneshot unit — not a failure.
7. Firewall
Open UDP 51820 inbound in the Linode Cloud Firewall (Cloud Manager → Linode → Firewalls tab).
8. Verify
wg show
ip -6 route show # should show <routed prefix> dev wg0, added automatically
Part 2: GL.iNet Router (Stock Firmware — Client Side)
Applies to GL.iNet's own OpenWrt-based dashboard firmware. If a future device runs actual DD-WRT or vanilla OpenWrt instead, the WireGuard/UCI mechanics differ — check firmware type first via the Sysinfo/status page.
1. Confirm firmware type before doing anything else
Check for a GL.iNet-branded admin panel (192.168.8.1) vs. LuCI vs. DD-WRT GUI —
determines which of this guide applies.
2. Add the WireGuard client config
GL.iNet dashboard → VPN → WireGuard Client → Import. Paste:
[Interface]
PrivateKey = <CLIENT_PRIVKEY>
Address = 10.10.0.2/24, fd10:10:10::2/64
DNS = 1.1.1.1, 2606:4700:4700::1111
[Peer]
PublicKey = <SERVER_PUBKEY>
Endpoint = <LINODE_PUBLIC_IPV4>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Set Global Mode (not per-app/split), turn the client ON.
3. Configure LAN IPv6 to advertise the routed /64
Dashboard → NETWORK → IPv6:
- Enabled IPv6: ON
- LAN Mode: Static IPv6
- IPv6 Address:
<YOUR_ROUTED_V6_PREFIX>(e.g.2600:3c06:e001:2ec::/64) - DNS acquisition: Automatic
- Apply.
This single toggle handles RA/SLAAC advertisement to LAN clients — no manual radvd config needed on this firmware.
4. ⚠️ Critical gotcha — disable IPv6 NAT66 on the WireGuard zone
GL.iNet's firmware defaults every WireGuard client zone to masq6='1' (IPv6
masquerade), which breaks routed-/64 setups: it rewrites LAN clients' real global
addresses to the tunnel's private link address, so return traffic has nowhere to
go (symptom: outbound ping reaches the server, but replies never come back;
conntrack shows replies addressed to the tunnel's fd... address instead of the
client's real address).
Worse: GL.iNet's setup_instance_via.lua hardcodes masq and masq6 to always
match each other — you can't just uncheck IPv6 NAT anywhere in the UI, and a plain
uci set firewall.<zone>.masq6=0 gets silently overwritten. There's also a
separate vendor script (/etc/firewall.nat6, registered with reload='1') that
re-asserts the masquerade rule on every firewall reload, including the routine
ones triggered by each interface coming up at boot.
The fix — a persistent include script that strips the rule after every reload:
cat <<'EOF' > /etc/firewall.<zone>_no_masq6
#!/bin/sh
while ip6tables -t nat -C zone_<zone>_postrouting -m comment --comment "!fw3" -j MASQUERADE 2>/dev/null; do
ip6tables -t nat -D zone_<zone>_postrouting -m comment --comment "!fw3" -j MASQUERADE
done
exit 0
EOF
chmod +x /etc/firewall.<zone>_no_masq6
uci set firewall.<zone>_no_masq6=include
uci set firewall.<zone>_no_masq6.path="/etc/firewall.<zone>_no_masq6"
uci set firewall.<zone>_no_masq6.family='ipv6'
uci set firewall.<zone>_no_masq6.reload='1' # <- easy to miss; without this it
# only runs once at initial boot,
# not on later interface-up reloads
uci commit firewall
service firewall restart
Replace <zone> with the actual WireGuard client zone name (check via
uci show firewall | grep masq6 to find it — it'll match the network/interface
name shown in ip a, e.g. wgclient1).
5. Verify, in this order
ip6tables -t nat -L zone_<zone>_postrouting -v -n # confirm no MASQUERADE line
Then from a real LAN client (not the router itself — the router's own outbound traffic follows a different policy path and isn't a valid test):
ping -6 2606:4700:4700::1111
or visit test-ipv6.com.
Then reboot the router and re-check both — this setup has enough moving parts (vendor scripts, reload ordering, policy routing) that "works now" and "works after reboot" are genuinely different questions worth testing separately.
Known-good reference values from this build (for comparison, not to reuse)
- Server: Linode Nanode, Chicago, Ubuntu 24.04, networking via
systemd-networkd(not netplan, not/etc/network/interfaces— check/etc/systemd/network/*.networkfor actual live config on Ubuntu Linodes) - Client: GL-MT3000, GL.iNet firmware v4.8.1, OpenWrt 21.02-SNAPSHOT base