diff --git a/readme-vars.yml b/readme-vars.yml index 643bce8e..2bdebfef 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -39,6 +39,7 @@ opt_param_env_vars: - {env_var: "PEERS", env_value: "1", desc: "Number of peers to create confs for. Required for server mode. Can also be a list of names: `myPC,myPhone,myTablet` (alphanumeric only)"} - {env_var: "PEERDNS", env_value: "auto", desc: "DNS server set in peer/client configs (can be set as `8.8.8.8`). Used in server mode. Defaults to `auto`, which uses wireguard docker host's DNS via included CoreDNS forward."} - {env_var: "INTERNAL_SUBNET", env_value: "10.13.13.0", desc: "Internal subnet for the wireguard and server and peers (only change if it clashes). Used in server mode."} + - {env_var: "IP6_SUBNET", env_value: "", desc: "IPv6 subnet for the WireGuard tunnel, enabling dual-stack (IPv4+IPv6) configuration. Must end with ':' (e.g., 2001:db8:b00b:420::). Accepts an optional CIDR prefix /64 through /112 (defaults to /64 if omitted). Server gets ::1/128, peers get sequential ::2/128, ::3/128, etc. Requires SYS_MODULE capability and a static IPv6 route on your router. See IPv6 GUA setup instructions in the application setup section below. Used in server mode."} - {env_var: "ALLOWEDIPS", env_value: "0.0.0.0/0", desc: "The IPs/Ranges that the peers will be able to reach using the VPN connection. If not specified the default value is: '0.0.0.0/0, ::0/0' This will cause ALL traffic to route through the VPN, if you want split tunneling, set this to only the IPs you would like to use the tunnel AND the ip of the server's WG ip, such as 10.13.13.1."} - {env_var: "PERSISTENTKEEPALIVE_PEERS", env_value: "", desc: "Set to `all` or a list of comma separated peers (ie. `1,4,laptop`) for the wireguard server to send keepalive packets to listed peers every 25 seconds. Useful if server is accessed via domain name and has dynamic IP. Used only in server mode."} - {env_var: "LOG_CONFS", env_value: "true", desc: "Generated QR codes will be displayed in the docker log. Set to `false` to skip log output."} @@ -60,7 +61,7 @@ app_setup_block: | If the environment variable `PEERS` is set to a number or a list of strings separated by comma, the container will run in server mode and the necessary server and peer/client confs will be generated. The peer/client config qr codes will be output in the docker log if `LOG_CONFS` is set to `true`. They will also be saved in text and png format under `/config/peerX` in case `PEERS` is a variable and an integer or `/config/peer_X` in case a list of names was provided instead of an integer. - Variables `SERVERURL`, `SERVERPORT`, `INTERNAL_SUBNET`, `PEERDNS`, `INTERFACE`, `ALLOWEDIPS` and `PERSISTENTKEEPALIVE_PEERS` are optional variables used for server mode. Any changes to these environment variables will trigger regeneration of server and peer confs. Peer/client confs will be recreated with existing private/public keys. Delete the peer folders for the keys to be recreated along with the confs. + Variables `SERVERURL`, `SERVERPORT`, `INTERNAL_SUBNET`, `PEERDNS`, `INTERFACE`, `ALLOWEDIPS`, `PERSISTENTKEEPALIVE_PEERS` and `IP6_SUBNET` are optional variables used for server mode. Any changes to these environment variables will trigger regeneration of server and peer confs. Peer/client confs will be recreated with existing private/public keys. Delete the peer folders for the keys to be recreated along with the confs. To add more peers/clients later on, you increment the `PEERS` environment variable or add more elements to the list and recreate the container. @@ -76,6 +77,95 @@ app_setup_block: | If you get IPv6 related errors in the log and connection cannot be established, edit the `AllowedIPs` line in your peer/client wg0.conf to include only `0.0.0.0/0` and not `::/0`; and restart the container. + ## IPv6 GUA (Global Unicast Address) Support + + WireGuard supports optional dual-stack (IPv4+IPv6) tunnel configuration via the `IP6_SUBNET` environment variable. When set, both the server and all peers receive an IPv6 GUA address in addition to their IPv4 address. IPv6 is disabled by default; existing IPv4-only configurations continue to work without modification. + + `SYS_MODULE` capability **must** be added to `cap_add` when using IPv6. A static IPv6 route must also be configured on your router pointing `IP6_SUBNET` to your host — this is required for both host and bridge modes. Without the router route, IPv6 traffic will not reach your WireGuard peers. + + ### Host Mode + + Enable IPv6 forwarding on the host by adding the following sysctls to your docker-compose: + + ```yaml + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.all.forwarding=1 + ``` + + Configure a static IPv6 route on your router pointing `IP6_SUBNET` to the host device IP. + + Example (host mode): + + ```yaml + services: + wireguard: + image: lsio/wireguard + container_name: wireguard + network_mode: host + cap_add: + - NET_ADMIN + - SYS_MODULE + volumes: + - /path/to/wireguard/config:/config + - /lib/modules:/lib/modules + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.all.forwarding=1 + environment: + - PEERS=3 + - SERVERURL=wireguard.domain.com + - IP6_SUBNET=2001:db8:b00b:420:: + - PEERDNS=8.8.8.8,2001:4860:4860::8888 + restart: unless-stopped + ``` + + ### Bridge Mode + + Create an external Docker network with IPv6 support: + + ```bash + docker network create --ipv6 --subnet 2001:db8:1::/64 wireguard_net + ``` + + Add a static IPv6 route on the host pointing `IP6_SUBNET` to the Docker network gateway. **Note:** host routes added via `ip route` are ephemeral — you must make them persistent via your system's network configuration or they will be lost on reboot. + + ```bash + sudo ip -6 route add 2001:db8:b00b:420::/64 via + ``` + + Configure a static IPv6 route on your router pointing `IP6_SUBNET` to the host device IP. + + Example (bridge mode): + + ```yaml + services: + wireguard: + image: lsio/wireguard + container_name: wireguard + cap_add: + - NET_ADMIN + - SYS_MODULE + ports: + - 51820:51820/udp + volumes: + - /path/to/wireguard/config:/config + - /lib/modules:/lib/modules + networks: + wireguard_net: + environment: + - PEERS=3 + - SERVERURL=wireguard.domain.com + - IP6_SUBNET=2001:db8:b00b:420:: + - PEERDNS=8.8.8.8,2001:4860:4860::8888 + restart: unless-stopped + networks: + wireguard_net: + external: true + ``` + ## Road warriors, roaming and returning home If you plan to use Wireguard both remotely and locally, say on your mobile phone, you will need to consider routing. Most firewalls will not route ports forwarded on your WAN interface correctly to the LAN out of the box. This means that when you return home, even though you can see the Wireguard server, the return packets will probably get lost. diff --git a/root/defaults/peer.conf b/root/defaults/peer.conf index d987dba9..3169774d 100644 --- a/root/defaults/peer.conf +++ b/root/defaults/peer.conf @@ -1,5 +1,5 @@ [Interface] -Address = ${CLIENT_IP} +Address = ${CLIENT_IP}${CLIENT_IP6:+,${CLIENT_IP6}} PrivateKey = $(cat /config/${PEER_ID}/privatekey-${PEER_ID}) ListenPort = 51820 DNS = ${PEERDNS} diff --git a/root/defaults/server.conf b/root/defaults/server.conf index 757682d6..b803a74f 100644 --- a/root/defaults/server.conf +++ b/root/defaults/server.conf @@ -1,5 +1,5 @@ [Interface] -Address = ${INTERFACE}.1 +Address = ${INTERFACE}.1${IP6_ADDR_SERVER:+,${IP6_ADDR_SERVER}} ListenPort = 51820 PrivateKey = $(cat /config/server/privatekey-server) PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE diff --git a/root/etc/s6-overlay/s6-rc.d/init-wireguard-confs/run b/root/etc/s6-overlay/s6-rc.d/init-wireguard-confs/run index 4279d315..53d85347 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-wireguard-confs/run +++ b/root/etc/s6-overlay/s6-rc.d/init-wireguard-confs/run @@ -22,6 +22,13 @@ fi if ! grep -q 'PresharedKey' /config/templates/peer.conf; then sed -i 's|^Endpoint|PresharedKey = \$\(cat /config/\${PEER_ID}/presharedkey-\${PEER_ID}\)\nEndpoint|' /config/templates/peer.conf fi +# add IPv6 GUA support to user templates (backwards compatibility) +if ! grep -q 'IP6_ADDR_SERVER' /config/templates/server.conf; then + sed -i 's|^Address = ${INTERFACE}\.1$|Address = ${INTERFACE}.1${IP6_ADDR_SERVER:+,${IP6_ADDR_SERVER}}|' /config/templates/server.conf +fi +if ! grep -q 'CLIENT_IP6' /config/templates/peer.conf; then + sed -i 's|^Address = ${CLIENT_IP}$|Address = ${CLIENT_IP}${CLIENT_IP6:+,${CLIENT_IP6}}|' /config/templates/peer.conf +fi generate_confs () { mkdir -p /config/server @@ -50,10 +57,18 @@ DUDE" wg genpsk > "/config/${PEER_ID}/presharedkey-${PEER_ID}" fi if [[ -f "/config/${PEER_ID}/${PEER_ID}.conf" ]]; then - CLIENT_IP=$(grep "Address" "/config/${PEER_ID}/${PEER_ID}.conf" | awk '{print $NF}') + CLIENT_IP=$(grep "Address" "/config/${PEER_ID}/${PEER_ID}.conf" | awk '{print $NF}' | cut -d, -f1) if [[ -n "${ORIG_INTERFACE}" ]] && [[ "${INTERFACE}" != "${ORIG_INTERFACE}" ]]; then CLIENT_IP="${CLIENT_IP//${ORIG_INTERFACE}/${INTERFACE}}" fi + if [[ -n "${IP6_SUBNET}" ]]; then + CLIENT_IP6=$(grep "Address" "/config/${PEER_ID}/${PEER_ID}.conf" | awk '{print $NF}' | cut -d, -f2) + if [[ -n "${ORIG_IP6_SUBNET}" ]] && [[ "${IP6_SUBNET}" != "${ORIG_IP6_SUBNET}" ]]; then + CLIENT_IP6="" + fi + else + CLIENT_IP6="" + fi else for idx in {2..254}; do PROPOSED_IP="${INTERFACE}.${idx}" @@ -62,6 +77,16 @@ DUDE" break fi done + CLIENT_IP6="" + fi + # IPv6 address conflict detection + if [[ -n "${IP6_SUBNET}" ]] && [[ -z "${CLIENT_IP6}" ]]; then + for idx in {2..254}; do + if ! grep -q -R "${IP6_SUBNET}${idx}" /config/peer*/*.conf 2>/dev/null; then + CLIENT_IP6="${IP6_SUBNET}${idx}/128" + break + fi + done fi if [[ -f "/config/${PEER_ID}/presharedkey-${PEER_ID}" ]]; then # create peer conf with presharedkey @@ -95,11 +120,11 @@ DUDE if [[ -n "${!SERVER_ALLOWEDIPS}" ]]; then echo "Adding ${!SERVER_ALLOWEDIPS} to wg0.conf's AllowedIPs for peer ${i}" cat <> /config/wg_confs/wg0.conf -AllowedIPs = ${CLIENT_IP}/32,${!SERVER_ALLOWEDIPS} +AllowedIPs = ${CLIENT_IP}/32${CLIENT_IP6:+,${CLIENT_IP6}},${!SERVER_ALLOWEDIPS} DUDE else cat <> /config/wg_confs/wg0.conf -AllowedIPs = ${CLIENT_IP}/32 +AllowedIPs = ${CLIENT_IP}/32${CLIENT_IP6:+,${CLIENT_IP6}} DUDE fi # add PersistentKeepalive if the peer is specified @@ -133,6 +158,7 @@ ORIG_PEERS="$PEERS" ORIG_INTERFACE="$INTERFACE" ORIG_ALLOWEDIPS="$ALLOWEDIPS" ORIG_PERSISTENTKEEPALIVE_PEERS="$PERSISTENTKEEPALIVE_PEERS" +ORIG_IP6_SUBNET="$IP6_SUBNET" DUDE } @@ -160,6 +186,35 @@ if [[ -n "$PEERS" ]]; then INTERFACE=$(echo "$INTERNAL_SUBNET" | awk 'BEGIN{FS=OFS="."} NF--') ALLOWEDIPS=${ALLOWEDIPS:-0.0.0.0/0, ::/0} echo "**** AllowedIPs for peers $ALLOWEDIPS ****" + if [[ -n "$IP6_SUBNET" ]]; then + # strip optional CIDR prefix and validate prefix length + if [[ "$IP6_SUBNET" =~ /([0-9]+)$ ]]; then + IP6_PREFIX_LEN="${BASH_REMATCH[1]}" + IP6_SUBNET="${IP6_SUBNET%/*}" + if [[ "$IP6_PREFIX_LEN" -lt 64 ]]; then + echo "**** ERROR: IP6_SUBNET prefix length /${IP6_PREFIX_LEN} is too small. Must be /64 or larger. IPv6 disabled. ****" + IP6_SUBNET="" + fi + fi + fi + if [[ -n "$IP6_SUBNET" ]]; then + # must end with colon (e.g. 2001:db8:b00b:420::) + if [[ ! "$IP6_SUBNET" =~ :$ ]]; then + echo "**** ERROR: IP6_SUBNET must end with ':' (e.g., 2001:db8:b00b:420::). IPv6 disabled. ****" + IP6_SUBNET="" + fi + fi + if [[ -n "$IP6_SUBNET" ]]; then + # basic format: only hex digits and colons + if [[ ! "$IP6_SUBNET" =~ ^[0-9a-fA-F:]+$ ]]; then + echo "**** ERROR: IP6_SUBNET contains invalid characters. IPv6 disabled. ****" + IP6_SUBNET="" + fi + fi + if [[ -n "$IP6_SUBNET" ]]; then + IP6_ADDR_SERVER="${IP6_SUBNET}1/128" + echo "**** IPv6 subnet is set to ${IP6_SUBNET}, server IPv6 will be ${IP6_ADDR_SERVER} ****" + fi if [[ -z "$PEERDNS" ]] || [[ "$PEERDNS" = "auto" ]]; then PEERDNS="${INTERFACE}.1" echo "**** PEERDNS var is either not set or is set to \"auto\", setting peer DNS to ${INTERFACE}.1 to use wireguard docker host's DNS. ****" @@ -175,7 +230,7 @@ if [[ -n "$PEERS" ]]; then if [[ -f /config/.donoteditthisfile ]]; then . /config/.donoteditthisfile fi - if [[ "$SERVERURL" != "$ORIG_SERVERURL" ]] || [[ "$SERVERPORT" != "$ORIG_SERVERPORT" ]] || [[ "$PEERDNS" != "$ORIG_PEERDNS" ]] || [[ "$PEERS" != "$ORIG_PEERS" ]] || [[ "$INTERFACE" != "$ORIG_INTERFACE" ]] || [[ "$ALLOWEDIPS" != "$ORIG_ALLOWEDIPS" ]] || [[ "$PERSISTENTKEEPALIVE_PEERS" != "$ORIG_PERSISTENTKEEPALIVE_PEERS" ]]; then + if [[ "$SERVERURL" != "$ORIG_SERVERURL" ]] || [[ "$SERVERPORT" != "$ORIG_SERVERPORT" ]] || [[ "$PEERDNS" != "$ORIG_PEERDNS" ]] || [[ "$PEERS" != "$ORIG_PEERS" ]] || [[ "$INTERFACE" != "$ORIG_INTERFACE" ]] || [[ "$ALLOWEDIPS" != "$ORIG_ALLOWEDIPS" ]] || [[ "$PERSISTENTKEEPALIVE_PEERS" != "$ORIG_PERSISTENTKEEPALIVE_PEERS" ]] || [[ "$IP6_SUBNET" != "$ORIG_IP6_SUBNET" ]]; then echo "**** Server related environment variables changed, regenerating 1 server and ${PEERS} peer/client confs ****" generate_confs save_vars