Many public networks (hotel WiFi, train WiFi, public hotspots, etc.) are IPv4-only or apply strict filtering policies for UDP traffic.
If a WireGuard endpoint is reachable only via IPv6, clients in such networks cannot establish the tunnel.
Goal of this design:
The actual WireGuard service runs on a dedicated host (called wg-end).
The firewall only provides an IPv4 port forward.
Internet Client
│
│ IPv6 available
▼
wg-end (WireGuard endpoint)
or
Internet Client
│
│ IPv4 only
▼
Firewall (DNAT)
│
▼
wg-end (WireGuard endpoint)
A single endpoint hostname is used that resolves differently depending on the IP protocol.
firewall.random.tld
A → firewall IPv4
AAAA → wg-end IPv6
wg-end.random.tld
AAAA → wg-end IPv6
Resulting connection paths:
| Client network | Connection path |
|---|---|
| IPv6 available | direct connection to wg-end |
| IPv4 only | firewall → DNAT → wg-end |
WireGuard client configuration:
Endpoint = firewall.random.tld:51820
IPv4 forwarding:
WAN UDP 51820 → DNAT → wg-end:51820
IPv6 handling:
No NAT is required. Only a firewall rule permitting access to the endpoint host.
allow udp any → wg-end.random.tld port 51820
The IPv6 DynDNS updater writes two AAAA records simultaneously.
wg-end.random.tld AAAA → wg-end IPv6 firewall.random.tld AAAA → wg-end IPv6
IPv4 DynDNS remains unchanged:
firewall.random.tld A → firewall IPv4
This automatically makes firewall.random.tld (FAKE) dual-stack.
/usr/local/sbin/ovh-dyndns-v6.sh
#!/usr/bin/env bash set -euo pipefail CONF=/etc/ovh-dyndns-v6.conf source "$CONF" GUA="$(/usr/local/sbin/fetch-gua.sh)" [ -n "$GUA" ] || { logger -t ovh-dyndns-v6 "ERROR: no GUA" exit 2 } export OVH_ENDPOINT OVH_APP_KEY OVH_APP_SECRET OVH_CONSUMER_KEY export OVH_ZONE TTL export GUA TARGETS=("${RECORD_FQDNS[@]}") for fqdn in "${TARGETS[@]}"; do export RECORD_FQDN="$fqdn" OUT="$( /opt/ovh-dyndns-v6/venv/bin/python \ /opt/ovh-dyndns-v6/update_aaaa.py )" logger -t ovh-dyndns-v6 "$OUT fqdn=$fqdn" echo "$OUT fqdn=$fqdn" done
/etc/ovh-dyndns-v6.conf
OVH_ENDPOINT="ovh-eu" OVH_APP_KEY="secret" OVH_APP_SECRET="secret" OVH_CONSUMER_KEY="secret" OVH_ZONE="random.tld" TTL=300 RECORD_FQDNS=( "wg-end.random.tld" "firewall.random.tld" ) IFACE="ens18"
/etc/systemd/system/ovh-dyndns-v6.service
[Unit] Description=Update OVH IPv6 DynDNS [Service] Type=oneshot ExecStart=/usr/local/sbin/ovh-dyndns-v6.sh
/etc/systemd/system/ovh-dyndns-v6.timer
[Unit] Description=Run OVH IPv6 DynDNS updater every 5 minutes [Timer] OnBootSec=2min OnUnitActiveSec=5min AccuracySec=30s Persistent=true [Install] WantedBy=timers.target
Enable the timer:
systemctl daemon-reload systemctl enable --now ovh-dyndns-v6.timer
dig +short A firewall.random.tld dig +short AAAA firewall.random.tld dig +short AAAA wg-end.random.tld
Expected result:
firewall.random.tld A → firewall IPv4 firewall.random.tld AAAA → wg-end IPv6 wg-end.random.tld AAAA → wg-end IPv6
wg show
IPv6 transport:
endpoint: [2003:xxxx:...]:51820
IPv4 fallback:
endpoint: 203.x.x.x:51820
This architecture provides:
The actual WireGuard endpoint always remains wg-end, independent of the transport protocol.
For anyone familiar with DNS, this is self-explanatory:
A DNS name may only have one CNAME record, and that record replaces all other record types.
Therefore it is not possible to combine:
using CNAME records.
The correct approach is simply to publish A and AAAA records directly on the endpoint hostname.
Both connection paths ultimately terminate on the same WireGuard endpoint host.
IPv6 path:
Client → wg-end
IPv4 path:
Client → firewall → DNAT → wg-end
Since the WireGuard service always runs on the same machine and uses the same peer identity, the tunnel state remains consistent.
Using one endpoint hostname keeps client configuration simple:
Endpoint = firewall.random.tld:51820
Transport selection happens automatically:
This makes the setup particularly robust in mobile or restricted networks.
In environments where a static public dual-stack address is available on the firewall, this setup can be simplified.
Example:
vpn.random.tld
A → firewall IPv4
AAAA → firewall IPv6
The firewall would then forward both IPv4 and IPv6 traffic to the WireGuard host.
However, in practice this is rarely feasible in residential deployments.
Reasons include:
Very few home users are willing to pay for static addressing just to operate a personal VPN endpoint.
Therefore a DynDNS-based design is typically required.
While DynDNS services are widely available for IPv4, support for automatic IPv6 updates is still inconsistent.
Typical problems include:
Because of this, IPv6 DynDNS solutions are often implemented using custom scripts or API integrations.
The approach described in this document assumes a DynDNS workflow capable of updating multiple AAAA records simultaneously.
This architecture solves three practical problems simultaneously:
The result is a single stable WireGuard endpoint hostname that works reliably across both IPv4 and IPv6 networks.