Feste IPv4 trotz CGNAT: NAT-Bridge über einen günstigen VPS mit WireGuard

Dein Provider gibt dir nur eine CGNAT-Adresse? So baust du dir mit einem günstigen VPS und WireGuard eine NAT-Bridge und erreichst dein Heimnetz über eine feste öffentliche IPv4.

Feste IPv4 trotz CGNAT: NAT-Bridge über einen günstigen VPS mit WireGuard

Problem

Du möchtest eine feste IPv4-Adresse für deine selbst gehosteten Dienste, bekommst von deinem Provider aber nur eine CGNAT-Adresse (Carrier-grade NAT)? Wenn im Router bei IPv4 eine Adresse wie 100.64.74.132 steht, hast du keine eigene öffentliche IPv4 zugewiesen bekommen, sondern teilst sie dir mit anderen Kunden. Was du hingegen fast immer bekommst, ist ein eigenes IPv6-Subnetz.

Lösung

Du baust dir mit einem günstigen vServer (ab ca. 1 €) eine NAT-Bridge und leitest jeglichen IPv4-Traffic über diesen Server weiter. Alternativ bekommst du über Oracle Free Tier (AMD Compute Instance) einen kostenlosen VPS. Der Zugriff auf dein Heimnetz erfolgt anschließend über die öffentliche IPv4 deines VPS. Der Datenverkehr läuft dabei verschlüsselt durch einen WireGuard-VPN-Tunnel.

Einrichtung auf dem VPS

Die Installation wurde auf einem Ubuntu-24.04-Host durchgeführt.

System aktualisieren:

apt-get update && apt-get upgrade -y

PiVPN installieren:

curl -L https://install.pivpn.io | bash

Namen des Netzwerk-Interfaces herausfinden:

ip a

Die WireGuard-Konfiguration bearbeiten:

nano /etc/wireguard/wg0.conf

Direkt nach der Zeile ListenPort folgende Zeile einfügen (bei dein_interface den zuvor ermittelten Interface-Namen eintragen):

PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o dein_interface -j MASQUERADE

Danach das System neu starten und ein Client-Profil anlegen:

pivpn -a

Einen Namen für das Profil vergeben, z. B. heimnetzwerk.

Die Konfiguration erneut öffnen:

nano /etc/wireguard/wg0.conf

Bei AllowedIPs den IP-Bereich deines Heimnetzwerks ergänzen:

AllowedIPs = 10.70.193.2/32, 192.168.8.0/24

Den Inhalt der erzeugten Client-Konfiguration vorübergehend zwischenspeichern (wird später auf der VM gebraucht):

cat /etc/wireguard/configs/heimnetzwerk.conf

IP-Forwarding aktivieren – dazu die sysctl-Konfiguration bearbeiten:

nano /etc/sysctl.conf

Die Zeile #net.ipv4.ip_forward = 1 auskommentieren, sodass daraus net.ipv4.ip_forward = 1 wird.

Ein Skript für die NAT-Regeln anlegen:

nano /ip.sh

Folgenden Inhalt eintragen:

#!/bin/bash
iptables -t nat -A POSTROUTING -j MASQUERADE

Um alle Ports an 192.168.8.14 weiterzuleiten (in diesem Fall der Nginx Proxy Manager):

iptables -A PREROUTING -t nat -p tcp --match multiport ! --dports 22,51820 -j DNAT --to-destination 192.168.8.14
iptables -A PREROUTING -t nat -p udp --match multiport ! --dports 22,51820 -j DNAT --to-destination 192.168.8.14

Um nur einen einzelnen Port (80) an 192.168.8.14 weiterzuleiten (in diesem Fall ein Webserver):

iptables -t nat -A PREROUTING -p "tcp" --dport "80" -j DNAT --to-destination "192.168.8.14":"80"

iptables-Regeln dauerhaft speichern:

sudo apt install netfilter-persistent
sudo netfilter-persistent save
sudo systemctl enable netfilter-persistent

Das Skript ausführbar machen:

chmod +x /ip.sh

Crontab öffnen:

crontab -e

Und folgende Zeile eintragen, damit die Regeln bei jedem Neustart gesetzt werden:

@reboot /ip.sh

Nach einem finalen Reboot ist die Einrichtung auf dem VPS abgeschlossen.

Einrichtung auf der VM (Heimnetz)

System aktualisieren und WireGuard installieren:

apt-get update && apt-get upgrade -y && apt-get install -y wireguard openresolv wireguard-dkms

Die WireGuard-Konfiguration anlegen und den Inhalt aus /etc/wireguard/configs/heimnetzwerk.conf (vom VPS) einfügen:

nano /etc/wireguard/wg0.conf

Vor dem Abschnitt [Peer] folgende Zeilen einfügen und das Interface anpassen (bei dein_interface):

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o dein_interface -j MASQUERADE
PostUp = echo 1 > /proc/sys/net/ipv4/ip_forward

Am Ende der Datei außerdem folgende Zeile ergänzen, damit der Tunnel dauerhaft offen bleibt:

PersistentKeepalive = 25

WireGuard aktivieren:

systemctl enable wg-quick@wg0

Nach einem Reboot ist die VM fertig eingerichtet.

IONOS – Firewall-Anpassungen

Im Beispiel mit IONOS muss in der Firewall eine Regel für den WireGuard-Port hinterlegt werden.

Optionale Anpassungen

DNS für den WireGuard-Tunnel aktivieren

DNS in /etc/wireguard/wg0.conf hinterlegen:

DNS = 9.9.9.9

Anschließend die Datei /etc/systemd/resolved.conf.d/disable-stub.conf mit folgendem Inhalt anlegen:

[Resolve]
DNSStubListener=no

Danach systemd-resolved neu starten:

sudo systemctl restart systemd-resolved

WireGuard-Tunnel abschalten

Für Updates muss der VPN-Tunnel temporär deaktiviert werden.

PiVPN ausschalten:

pivpn off

WireGuard wieder starten:

service wg-quick@wg0 start

SSH-Zugang auf den NAT-VPS absichern

Um unbefugten Zugriff auf den VPS zu verhindern, empfiehlt sich ein SSH-Keypair. In diesem Zuge habe ich ein Skript erstellt, das per DNS-Lookup deine DynDNS auflöst und deine aktuelle IP automatisch in iptables hinterlegt.

#!/bin/bash
# Name: dynamic-ssh-access
# Description: Automatically manage iptables rules for SSH access based on DNS
# --------------------------
# Configuration (Edit these)
# --------------------------
DYNAMIC_DNS="your-dynamic-dns.example.com"  # Replace with your DNS name
HOSTFILE="/var/lib/ssh-access/current_ips.txt"
SSH_PORT="22"

# --------------------------
# Constants
# --------------------------
IPTABLES="/sbin/iptables"
IP6TABLES="/sbin/ip6tables"
CHAIN_V4="SSH_ACCESS"
CHAIN_V6="SSH_ACCESS6"

# --------------------------
# Functions
# --------------------------

validate_ip() {
  local ip=$1
  # IPv4 validation
  [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && return 0
  # IPv6 validation
  [[ $ip =~ ^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$ ]] && return 0
  return 1
}

resolve_ips() {
  # Get IPv4 (A-record)
  local ipv4=$(dig +short "$DYNAMIC_DNS" A 2>/dev/null | \
    grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | tail -n1)

  # Get IPv6 (AAAA-record)
  local ipv6=$(dig +short "$DYNAMIC_DNS" AAAA 2>/dev/null | \
    grep -Eo '([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}' | tail -n1)

  echo "$ipv4 $ipv6"
}

# --------------------------
# Main Script
# --------------------------

# Resolve current IPs
read -r NEW_IPV4 NEW_IPV6 <<< $(resolve_ips)

# Validate at least one IP exists
if [ -z "$NEW_IPV4" ] && [ -z "$NEW_IPV6" ]; then
  echo "Error: No valid IPs found for $DYNAMIC_DNS" >&2
  exit 1
fi

# Load previous IPs
declare -A OLD_IPS
if [ -f "$HOSTFILE" ]; then
  while IFS= read -r line; do
    if [[ $line =~ ^IPv4=(.*) ]]; then
      OLD_IPS["v4"]="${BASH_REMATCH[1]}"
    elif [[ $line =~ ^IPv6=(.*) ]]; then
      OLD_IPS["v6"]="${BASH_REMATCH[1]}"
    fi
  done < "$HOSTFILE"
fi

# Process IPv4 changes
if [ -n "$NEW_IPV4" ]; then
  if ! validate_ip "$NEW_IPV4"; then
    echo "Invalid IPv4: $NEW_IPV4" >&2
    exit 1
  fi

  if [ "${OLD_IPS[v4]}" != "$NEW_IPV4" ]; then
    echo "Updating IPv4 rules for $NEW_IPV4"

    # Create chain if missing
    $IPTABLES -N "$CHAIN_V4" 2>/dev/null

    # Ensure chain is linked
    if ! $IPTABLES -C INPUT -p tcp --dport "$SSH_PORT" -j "$CHAIN_V4" 2>/dev/null; then
      $IPTABLES -I INPUT 1 -p tcp --dport "$SSH_PORT" -j "$CHAIN_V4"
    fi

    # Update rules
    $IPTABLES -F "$CHAIN_V4"
    $IPTABLES -A "$CHAIN_V4" -s "$NEW_IPV4/32" -p tcp --dport "$SSH_PORT" \
      -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
    $IPTABLES -A "$CHAIN_V4" -p tcp --dport "$SSH_PORT" -j DROP
  fi
fi

# Process IPv6 changes
if [ -n "$NEW_IPV6" ]; then
  if ! validate_ip "$NEW_IPV6"; then
    echo "Invalid IPv6: $NEW_IPV6" >&2
    exit 1
  fi

  if [ "${OLD_IPS[v6]}" != "$NEW_IPV6" ]; then
    echo "Updating IPv6 rules for $NEW_IPV6"

    # Create chain if missing
    $IP6TABLES -N "$CHAIN_V6" 2>/dev/null

    # Ensure chain is linked
    if ! $IP6TABLES -C INPUT -p tcp --dport "$SSH_PORT" -j "$CHAIN_V6" 2>/dev/null; then
      $IP6TABLES -I INPUT 1 -p tcp --dport "$SSH_PORT" -j "$CHAIN_V6"
    fi

    # Update rules
    $IP6TABLES -F "$CHAIN_V6"
    $IP6TABLES -A "$CHAIN_V6" -s "$NEW_IPV6/128" -p tcp --dport "$SSH_PORT" \
      -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
    $IP6TABLES -A "$CHAIN_V6" -p tcp --dport "$SSH_PORT" -j DROP
  fi
fi

# Store current IPs
mkdir -p "$(dirname "$HOSTFILE")"
> "$HOSTFILE"
[ -n "$NEW_IPV4" ] && echo "IPv4=$NEW_IPV4" >> "$HOSTFILE"
[ -n "$NEW_IPV6" ] && echo "IPv6=$NEW_IPV6" >> "$HOSTFILE"

echo "Rules updated successfully:"
[ -n "$NEW_IPV4" ] && echo "- IPv4: $NEW_IPV4"
[ -n "$NEW_IPV6" ] && echo "- IPv6: $NEW_IPV6"

Das Aktualisierungs-Intervall wird per Crontab gesteuert:

# Beispiel-Cronjob (alle 5 Minuten)
*/5 * * * * root /pfad/zum/update-ssh-access.sh