# 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): ```bash linode-cli networking v6-range-create --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 ```bash 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 ```bash 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.forwarding` before troubleshooting > anything else. ### 5. Server config — `/etc/wireguard/wg0.conf` ```ini [Interface] PrivateKey = 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] # PublicKey = AllowedIPs = 10.10.0.2/32, fd10:10:10::2/128, ``` > ⚠️ **Gotcha:** don't add a manual `ip -6 route add dev wg0` line. > `wg-quick` automatically installs a route for every prefix in a peer's > `AllowedIPs` — a duplicate manual route-add fails with "File exists" and can > abort the whole `wg-quick up` (PostUp failures are fatal). ### 6. Bring it up and enable at boot ```bash 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 ```bash wg show ip -6 route show # should show 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: ```ini [Interface] PrivateKey = Address = 10.10.0.2/24, fd10:10:10::2/64 DNS = 1.1.1.1, 2606:4700:4700::1111 [Peer] PublicKey = Endpoint = :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: `` (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..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:** ```bash cat <<'EOF' > /etc/firewall._no_masq6 #!/bin/sh while ip6tables -t nat -C zone__postrouting -m comment --comment "!fw3" -j MASQUERADE 2>/dev/null; do ip6tables -t nat -D zone__postrouting -m comment --comment "!fw3" -j MASQUERADE done exit 0 EOF chmod +x /etc/firewall._no_masq6 uci set firewall._no_masq6=include uci set firewall._no_masq6.path="/etc/firewall._no_masq6" uci set firewall._no_masq6.family='ipv6' uci set firewall._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 `` 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 ```bash ip6tables -t nat -L 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/*.network` for actual live config on Ubuntu Linodes) - Client: GL-MT3000, GL.iNet firmware v4.8.1, OpenWrt 21.02-SNAPSHOT base