Setting Up WireGuard with IPv6 Support in Docker on Ubuntu

Setting Up WireGuard with IPv6 Support in Docker on Ubuntu

This guide provides a streamlined method to deploy WireGuard in a Docker container on Ubuntu Server 24.04, enabling unique global IPv6 addresses for clients. It addresses a common gap in existing documentation, particularly for IPv6 delegation. The setup uses host networking for simplicity in managing static IPv6 routes, though a bridge networking alternative is included.

Key Goals:

  • Assign unique global IPv6 addresses from a delegated prefix to each WireGuard client.
  • Support delegation to downstream devices (e.g., via a travel router client).

Assumptions and Scope:

  • Ubuntu Server 24.04 host.
  • At least one /64 IPv6 subnet available from your delegated prefix (e.g., /56 or /48; /60 works but limits subnets).
  • Static IPv6 prefix preferred; dynamic prefixes require manual reconfiguration (see IPv6 Prefix Changes).
  • Use the LinuxServer.io WireGuard image, which lacks official IPv6 support but works with manual tweaks.
  • Commands are for Ubuntu; adapt for other distributions.

Security Notes:

  • Keep your IPv6 prefix private.
  • Use a DDNS service or static IPv4 for the endpoint if your public IP changes.

Prerequisites

IPv6 Setup

  1. Obtain a delegated IPv6 prefix from your ISP (e.g., via router WAN settings).
  2. Request a /56 or /48 for flexibility; a /60 suffices but yields fewer /64 subnets.
  3. Carve out /64 subnets: one for WireGuard clients (e.g., 2001:db8:b00b:42a::/64), and optionally another for delegation (e.g., 2001:db8:b00b:42b::/64).

Enable IPv6 Packet Forwarding

Edit /etc/sysctl.conf as root to enable forwarding:

net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1

Apply changes:

sudo sysctl -p

Install Dependencies

Update packages and install WireGuard tools (with optional qrencode for QR codes):

sudo apt update
sudo apt install wireguard-tools qrencode

Install Docker (official repository recommended over Ubuntu’s version):

# Add GPG key
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install docker-ce docker-compose-plugin

Add your user to the Docker group (log out and back in after):

sudo usermod -aG docker $USER

Deploy the WireGuard Container

Create a working directory:

sudo mkdir -p /srv/wireguard/config
cd /srv/wireguard

Create docker-compose.yaml:

services:
  wireguard:
    image: linuxserver/wireguard:latest
    container_name: wireguard
    network_mode: host
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Los_Angeles  # Adjust to your timezone
      - SERVERURL=your.ddns.example.com  # Your public endpoint
      - SERVERPORT=51820
      - PEERS=phone,workphone,tablet,laptop,travelrouter  # Comma-separated peer names
      - PEERDNS=8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844
      - INTERNAL_SUBNET=10.13.13.0/24
      - ALLOWEDIPS=0.0.0.0/0,::/0
      - PERSISTENTKEEPALIVE_PEERS=all
    volumes:
      - ./config:/config
      - /lib/modules:/lib/modules
    privileged: true
    restart: unless-stopped

Launch the container:

sudo docker compose up -d
sudo docker compose logs wireguard  # Check for errors

Note: If using Ubuntu’s Docker (docker-compose with hyphen), adjust commands accordingly.

Test IPv4 Connectivity

  1. Forward UDP port 51820 on your router to the host’s static IPv4.
  2. Allow the port on the host firewall (if using UFW):
    sudo ufw allow 51820/udp
    
  3. Generate and scan a client QR code (e.g., from /srv/wireguard/config/peer_phone/peer_phone.png) using the WireGuard app.
  4. Connect via mobile data (disable WiFi) and verify IPv4 internet access.

Enable IPv6 Support

Edit /srv/wireguard/config/wg_confs/wg0.conf to add IPv6. Include ip6tables rules in PostUp/PostDown for IPv6 forwarding (e.g., ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -A FORWARD -o %i -j ACCEPT).

Server Interface Example:

[Interface]
Address = 10.13.13.1/32, 2001:db8:b00b:42a::1/128
ListenPort = 51820
PrivateKey = <server-private-key>
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE; ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE; ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -D FORWARD -o %i -j ACCEPT

Peer Examples (add IPv6 to AllowedIPs):

[Peer]
# peer_phone
PublicKey = <public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.13.13.2/32, 2001:db8:b00b:42a::2/128
PersistentKeepalive = 25

[Peer]
# peer_travelrouter
PublicKey = <public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.13.13.6/32, 2001:db8:b00b:42a::6/128, 2001:db8:b00b:42b::/64
PersistentKeepalive = 25

Edit each client config in /srv/wireguard/config/peer_*/peer_*.conf (add IPv6 to Address):

Client Example:

[Interface]
Address = 10.13.13.2, 2001:db8:b00b:42a::2
PrivateKey = <client-private-key>
DNS = 8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844

[Peer]
PublicKey = <server-public-key>
PresharedKey = <preshared-key>
Endpoint = your.ddns.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0

Restart and verify:

sudo docker compose restart wireguard
sudo docker compose logs wireguard

Regenerate QR codes (originals lack IPv6):

qrencode -o peer_phone.png < /srv/wireguard/config/peer_phone/peer_phone.conf
qrencode -t ANSI < /srv/wireguard/config/peer_phone/peer_phone.conf  # Terminal display

Warning: Changes to docker-compose.yaml (e.g., peers, DNS) will overwrite configs; back them up post-edits.

Configure Static IPv6 Routes

  1. Identify the host’s link-local IPv6 on the LAN interface (e.g., eth0):

    ip -c -6 -brief addr show dev eth0 | grep 'fe80::'
    

    Output: e.g., fe80::1234:5678:9abc:def0/64 scope link.

  2. On your router, add static routes for client subnets (e.g., 2001:db8:b00b:42a::/64, 2001:db8:b00b:42b::/64) via the link-local address on the LAN interface.

Test: Reconnect a client and verify IPv6 connectivity (e.g., ping6 ipv6.google.com).

Alternative: Docker Bridge Networking

For bridge mode (avoids host networking but adds route complexity):

Update docker-compose.yaml (add sysctls for IPv6):

networks:
  wg6:
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: "2001:db8:b00b:421::/64"

services:
  wireguard:
    # ... (remove network_mode: host)
    networks:
      - wg6
    ports:
      - 51820:51820/udp
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv6.conf.all.forwarding=1
      - net.ipv6.conf.default.forwarding=1
      - net.ipv6.conf.eth0.proxy_ndp=1
    # ... (rest unchanged)
  1. Enable IPv6 in Docker daemon (/etc/docker/daemon.json):

    {
      "ipv6": true,
      "fixed-cidr-v6": "2001:db8:b00b:421::/64"
    }
    

    Restart Docker: sudo systemctl restart docker.

  2. Restart: sudo docker compose up -d.

  3. Get container IPv6: sudo docker exec wireguard ip -c -6 -brief addr show dev eth0 (e.g., 2001:db8:b00b:421::2).

  4. Add host routes (non-persistent):

    sudo ip -6 route add 2001:db8:b00b:42a::/64 via 2001:db8:b00b:421::2
    sudo ip -6 route add 2001:db8:b00b:42b::/64 via 2001:db8:b00b:421::2
    
  5. Persist routes via systemd (create /etc/systemd/system/wg-routes.service):

    [Unit]
    Description=WireGuard IPv6 Routes
    After=docker.service
    
    [Service]
    Type=oneshot
    RemainAfterExit=yes
    ExecStart=/bin/sh -c 'ip -6 route add 2001:db8:b00b:42a::/64 via 2001:db8:b00b:421::2; ip -6 route add 2001:db8:b00b:42b::/64 via 2001:db8:b00b:421::2'
    
    [Install]
    WantedBy=multi-user.target
    

    Enable: sudo systemctl enable wg-routes.service.

Handling IPv6 Prefix Changes

If your prefix changes (e.g., Xfinity residential), update:

  • /srv/wireguard/config/wg_confs/wg0.conf
  • All /srv/wireguard/config/peer_*/peer_*.conf
  • docker-compose.yaml (bridge mode only)
  • Router static routes
  • Host/container routes (if bridge mode)

Restart the container and regenerate QR codes.

Troubleshooting

Common issues stem from Docker’s limited IPv6 handling, routing misconfigurations, or firewall rules. Use these steps to diagnose and resolve.

Container Startup Failures

  • Privileges/Capabilities Missing: Ensure privileged: true, cap_add: [NET_ADMIN, SYS_MODULE], and /lib/modules volume are set. Check logs for kernel module errors: sudo docker compose logs wireguard | grep module.
  • Docker Version Conflicts: Recent versions (e.g., 28.0.0+) may break IPv6 on Ubuntu 24.04. Verify with docker --version; rollback if needed: sudo apt install docker-ce=5:27.5.1-1~ubuntu.24.04~noble (adjust per repo).
  • Module Loading: Run sudo modprobe wireguard on host; restart container.

No IPv6 Connectivity in Container

  • IPv6 Disabled: For bridge mode, confirm /etc/docker/daemon.json has "ipv6": true and restart Docker. Inside container: sudo docker exec wireguard ip -6 route—should show routes like default via <gateway> dev eth0.
  • Sysctl Issues: Add net.ipv6.conf.all.disable_ipv6=0 to sysctls; verify host forwarding: sysctl net.ipv6.conf.all.forwarding.
  • Route Table Check: Run sudo docker exec wireguard ip -c -6 route—look for wg0 entries and defaults. If missing, add ip6tables forwarding rules to PostUp.

Clients Not Receiving/Using IPv6 Addresses

  • Config Overwrites: After edits, regenerate QR codes and rescan; verify client Address includes IPv6 (e.g., 10.13.13.2, 2001:db8:b00b:42a::2).
  • AllowedIPs Errors: If clients fail to connect, temporarily remove ::/0 from client AllowedIPs, test IPv4, then re-add.
  • DNS Resolution: Ensure PEERDNS includes IPv6 servers; test client: nslookup ipv6.google.com over tunnel.

Routing and Forwarding Problems

  • Static Routes Missing: Verify router routes via link-local; on host: ip -6 route show. For bridge, add host-to-container routes.
  • Packet Tracing: Use tcpdump -i any ip6 on host/container/router to trace IPv6 packets—check if requests reach destination and replies return.
  • Policy-Based Routing for Endpoints: If tunnel traffic loops to endpoints, add to wg0.conf PostUp (symmetric on peers):
    ip -6 rule add to <peer-endpoint-ipv6>/128 ipproto udp dport 51820 table 200
    ip -6 route add default via fe80::<router-link-local> dev <lan-iface> table 200
    
    PreDown: Delete rules/routes. This bypasses tunnel for UDP handshakes.
  • NDP Proxy: For bridge mode, add net.ipv6.conf.eth0.proxy_ndp=1 sysctl; install radvd on host to announce client prefixes.

Firewall and NAT Conflicts

  • UFW/ip6tables Blocks: Run sudo ufw status verbose and sudo ip6tables -L -v -n—allow 51820/udp and ::/0 forwarding. Add ufw allow in on wg0 if using UFW.
  • ISP/Router Blocks: Confirm prefix delegation; test host IPv6: ping6 ipv6.google.com.

General Diagnostics

  • WireGuard Status: sudo docker exec -it wireguard wg show—check handshakes, allowed IPs.
  • Full Logs: sudo docker compose logs -f wireguard for real-time errors.
  • Reboot and Test: After changes, reboot host/router; incrementally test IPv4 → full IPv6.

If issues persist, share ip -6 route, wg show output, and logs on comments below will try to help