Inhaltsverzeichnis

Reverse WireGuard Endpoint Fallback (extended version)

This article only makes sense if you refer to the initial one first (incase you havn't, here the link)

Blueprint: IPv6 WireGuard Endpoint with Dynamic ISP Prefix (/56) + OVH DynDNS (AAAA)

Problem

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:

Architecture

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)

DNS Design

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

Firewall

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

DynDNS Concept

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.

DynDNS Script

/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

DynDNS Configuration

/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"

systemd Service

/etc/systemd/system/ovh-dyndns-v6.service

[Unit]
Description=Update OVH IPv6 DynDNS

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ovh-dyndns-v6.sh

systemd Timer

/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

DNS Verification

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

WireGuard Verification

wg show

IPv6 transport:

endpoint: [2003:xxxx:...]:51820

IPv4 fallback:

endpoint: 203.x.x.x:51820

Result

This architecture provides:

The actual WireGuard endpoint always remains wg-end, independent of the transport protocol.

Appendix

Why CNAME is not used

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.

Why this does not create asymmetric routing

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.

Why a single hostname is preferred

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.


Static IP Alternative

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.

Why IPv6 DynDNS is still uncommon

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.

Design Summary

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.