WireGuard ist eine schlanke und umfangreiche VPN-Lösung für verschiedene Betriebssysteme (inkl. einer Android-App und einer Implementation in Go für Windows, macOS und BSD), die ähnlich leicht zu nutzen ist wie SSH. Für Debian gibt es das Paket wireguard-tools, das die entsprechenden Kommandozeilenprogramme bereitstellt.

Das Angenehme an WireGuard ist, dass der Schlüsselaustausch sehr einfach gehalten ist: Jede Seite generiert ein Paar aus öffentlichem und geheimen Schlüssel und gibt den öffentlichen Schlüssel jeweils an die Gegenseite. Wer Server und wer Client bei der Kommunikation ist, entscheidet sich allein durch die Einstellung, welcher Rechner eingehende Verbindungen annimmt.

WireGuard transportiert nur IP-Pakete (kein Ethernet oder andere Layer-3-Pakete) und arbeitet mit Punkt-zu-Punkt-Verbindungen, so als hätte man die Systeme mit virtuellen Kabeln verbunden. An ein WireGuard-Interface können mehrere Gegenstellen angebunden werden, so als hätte man mehrere Kabel in einen Switch gesteckt; ein Client kann Verbindungen zu mehreren Servern aufbauen und ein Server kann Verbindungen von mehreren Clients annehmen. Diese Struktur kann genauso komplex wie eine physische Verkabelung werden und WireGuard setzt an der Stelle keine Grenzen. Das Weiterleiten (Routing) und Filtern der Paket zwischen diesen virtuellen Kabeln kann wiederum mit den bekannten Linux-Mitteln erfolgen.

Eine nette Eigenschaft von WireGuard ist, dass es beim Empfang ungültige Pakete einfach ignoriert. Da der Transport der verschlüsselten Pakete mittels UDP erfolgt, lassen sich die Server von WireGuard daher schwer über einen Portscan erkennen. WireGuard hat noch weitere interessante Eigenschaften und für den Einzug in den Linux-Kernel sind vielversprechende Änderungen geschehen.

Nach einigen Monaten intensiver Nutzung bin ich sehr angetan von WireGuard. Es hat sich als sehr robust erwiesen: Wenn die darunter liegende Verbindung wechselt, wird die VPN-Verbindung sehr schnell wieder hergestellt. Programme bekommen von diesem Wechsel nichts mit und verlorene Pakete werden neu übertragen; besonders bei SSH merkt man dies, wenn eine der VPN-Verbindungen neu gestartet wird. Auch die Latenz und die Bandbreite sind nah an den echten Leitungsparametern; SMB/CIFS und RDP funktionieren angenehm flüssig.

Neben der offiziellen Kurzanleitung für den Einstieg gibt es noch viele weitere Anleitungen zur Einrichtung von WireGuard als VPN im Internet:

Die Entwickler von WireGuard betreiben auch einen Demo-Server über den man seine Konfiguration als Client testen kann.

WireGuard als Bridge für NAT-Systeme

Mein Ziel war es, zwei Systeme miteinander zu verbinden, die sich beide hinter einer Firewall befanden und keines über eine öffentliche IP-Adresse erreichbar war. Die klassische Lösung mit WireGuard war daher nicht möglich. Stattdessen habe ich ein drittes System genutzt, auf dem ich den WireGuard-Server betreibe und über den sich die beiden Clients hinter den Firewalls treffen können. Der Server fungiert also nicht wie der übliche Gateway, sondern wie eine Bridge1, die die Pakete zwischen den Geräten am gleichen WireGuard-Interface vermittelt.

  1. Der Begriff Bridge ist an dieser Stelle falsch, da eine Bridge auf Layer 2 arbeitet und nicht auf IP-Ebene. 

Wie eingangs beschrieben muss auf allen Geräten WireGuard installiert werden: apt install wireguard linux-headers-amd64 bzw. auf aktuellen Systemen nur noch apt install wireguard-tools. Die Konfiguration für WireGuard sollte in einer Datei in /etc/wireguard liegen, wobei der Dateiname wie der Schnittstellenname mit der Endung conf lauten sollte; z. B. /etc/wireguard/wg0.conf und das Netzwerk-Interface wird dann wg0 heißen.

Das Format der Datei hat zwei Abschnitte: Im Abschnitt Interface setzt befinden sich die lokalen Einstellungen und in den Abschnitten Peer jeweils die Einstellungen für die Kommunikationspartner. Jedes System benötigt ein Schlüsselpaar, dass man mit wg genkey erstellen kann. Die Ausgabe ist eine Zeichenfolge aus Buchstaben und Zahlen, die als PrivateKey verwendet wird und aus derer man sich den öffentlichen Schlüssel mit wg pubkey ermitteln kann.[^](Ein kleiner Tipp am Rande: Mit Emacs lässt sich das Feld PrivateKey angenehm mit C-u M-! füllen. Den PublicKey kann man sich für die Region mit M-| ermitteln und dann aus dem Message-Buffer C-h e kopieren.)

Konfiguration des Clients

Innerhalb des Abschnitts Interface bestimmt man für die lokale Seite die (oder mehrere) IP-Adresse bzw. ein Netzwerksegment. Mit einem kompletten Netzwerksegment lassen sich auch mehrere Geräte über diese Verbindung routen. Neben den IPv4-Adressen kann man auch IPv6-Adressen angeben. Im einfachsten Fall die Link-Local-Adressen fe80::…, da alle Verbindungen am selben Netzwerkanschluss (= link-local) hängen.1

  1. Will doch irgendwo das VPN verlassen, kann man Unique-Local-Adressen mit fd… verwenden, wobei man sich mit dd if=/dev/urandom bs=5 count=1 |hd noch einen Netzwerkkennung schaffen muss; z. B. fdf9:d195:9d26::/48 

Im Abschnitt Peer konfiguriert man auf dem Client die entsprechenden Parameter für den Server. Der PublicKey muss den öffentlichen Schlüssel des Servers enthalten und unter Endpoint trägt man die Adresse des Server und den Port ein. Mit den Einträgen AllowedIPs gibt man die Adressen an, die an den Server geschickt werden sollen bzw. die von dort kommen dürfen.

Da die zwischen Client und Server liegende Firewall irgendwann die Weiterleitung für die UDP-Verbindung vergisst, müssen regelmäßig Pakete übertragen werden, um die Verbindung vom Server zum Client zu ermöglichen. Den genauen Wert kann man nur in der Praxis ermitteln und er weicht auch von Firewall zu Firewall ab; auf einem System habe ich 300 auf einem anderen muss es 50 sein.

[Interface]
PrivateKey = des Clients mit `wg genkey` erstellen

Address = 192.168.0.2/32
# Address = fd80::2/128

[Peer]
PublicKey = des Servers aus dessen PrivateKey mit `wg pubkey` ermitteln

Endpoint = relay.example.org:51820
PersistentKeepalive = 300

AllowedIPs = 192.168.0.0/24
# AllowedIPs = fd80::/64

Da die Konfigurationsdatei den geheimen Schlüssel enthält, sollte man kontrollieren, ob das Verzeichnis /etc/wireguard auch nur für root lesbar ist.

Zum Aktivieren der Verbindung kann man wg-quick up wg0 verwenden oder man richtet Systemd ein und kann dessen Befehle verwenden: systemctl enable --now wg-quick@wg0. Zum Aktualisieren der Einstellungen kann man dann systemctl reload verwenden, wenn die bestehenden Verbindungen nicht unterbrechen will. Dahinter verbirgt sich der Befehl wg syncconf wg0 <(wg-quick strip wg0), den man auch manuell ausführen kann.

Konfiguration des Servers

Im Abschnitt Interface auf dem Server steht wiederum dessen PrivateKey und zusätzlich die Angabe des Ports auf dem der WireGuard-Server lauschen soll; bei mehreren VPNs braucht jedes VPN einen eigenen Port. Die Konfiguration einer Adresse ist nicht notwendig, da der Server selbst nicht in Erscheinung treten soll. Zur Fehlersuche kann es aber hilfreich sein, dem Server eine Adresse zu geben, um zu sehen, ob die direkte Verbindung zwischen Server und Client funktioniert.

Damit der Server die ankommenden Pakete weiterleitet, muss man dies für die Schnittstelle aktivieren. Für IPv4 ist dies einfach, jedoch für IPv6 hat der Schalter für die Schnittstelle nur eine Bedeutung, wenn auch ipv6/conf/all/forwarding aktiviert ist. Da mein Server nicht grundsätzlich als Router arbeitet, sondern nur auf diesem einen Interface, habe ich in ipv6/conf/default/forwarding wiederum die Weiterleitung deaktiviert.

Für jeden Client bedarf es eines Abschnitts Peer, in dem man dessen PublicKey und die Adressen bestimmt, die er verwenden darf bzw. für die er zuständig ist.

[Interface]
PrivateKey = des Server mit `wg genkey` erstellen

ListenPort = 51820
#Address = 192.168.0.1/32
#Address = fe80::1/128

PostUp = echo 1 > /proc/sys/net/ipv4/conf/%i/forwarding
PostUp = echo 1 > /proc/sys/net/ipv6/conf/%i/forwarding

# client-1
[Peer]
PublicKey = des Clients aus dessen PrivateKey mit `wg pubkey` ermitteln
AllowedIPs = 192.168.0.2/32
AllowedIPs = fe80::2/128

# client-2
[Peer]
PublicKey = des Clients aus dessen PrivateKey mit `wg pubkey` ermitteln
AllowedIPs = 192.168.0.3/32
AllowedIPs = fe80::3/128

# client-3
[Peer]
PublicKey = des Clients aus dessen PrivateKey mit `wg pubkey` ermitteln
AllowedIPs = 192.168.0.4/32
AllowedIPs = fe80::4/128

… mit Netzwerkweiterleitung/Gateway

Jetzt wird es etwas komplizierter. Die Situation sieht wie folgt aus:

+----------+    +-----------+    +----------+
| client-2 +----+ wg-server +----+ client-1 +--- Netzwerk
+----------+    +-----------+    +----------+

192.168.0.3                      192.168.0.2
                                   10.0.0.2     10.0.0.0/24

Der client-1 soll Gateway für das bei ihm angeschlossene Netzwerk werden, sodass der client-2 (und auch client-3) in das Netzwerk gelangen können. Hierfür muss der client-1 die Pakete entsprechend umschreiben (masquerading), damit sie dann im angeschlossenen Netzwerk gültig sind und so aussehen, als hätte er sie gesendet.

Auf wg-server muss in der WireGuard-Konfiguration wg0.conf für client-1 der Netzwerkbereich erlaubt werden:

# client-1
[Peer]
…
AllowedIPs = 192.168.0.2/32
AllowedIPs = 10.0.0.0/24

Auf client-2 muss ebenfalls der Netzwerkbereich erlaubt werden:

[Peer]
Endpoint = relay.example.org:51820
…
AllowedIPs = 192.168.0.0/24
AllowedIPs = 10.0.0.0/24

Auf client-1 muss der Wireguard-Konfiguration ein Eintrag für die Aktivierung der Weiterleitung hinzugefügt werden:

[Interface]
…
Address = 192.168.0.1/32
PostUp = sysctl net.ipv4.conf.wg0.forwarding=1

In /etc/sysctl.d/local.conf muss die Weiterleitung für den Adapter zum Zielnetzwerk eingetragen und diese Einstellungen danach mit systemctl restart systemd-sysctl aktiviert werden:

net.ipv4.ip_forward = 1
net.ipv4.conf.eth0.forwarding = 1

Und in der Firewall /etc/nftables.conf muss noch die Umschreibung der Adressen eingetragen und ggf. mit systemctl enable --now nftables aktiviert oder mit systemctl reload nftables geladen werden:

table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        oifname "eth0" ifname "wg0" masquerade;
    }
}

Einmalig lässt sich diese Regel auch mit folgendem Befehl hinzufügen:

nft add rule nat postrouting iif wg0 oif eth0 masquerade

Prüfen lässt sich die Konfiguration auf diese Weise:

# nft list chain nat postrouting
table ip nat {
        chain postrouting {
                type nat hook postrouting priority 100; policy accept;
                oifname "eth0" counter packets 156 bytes 14585 masquerade
        }
}

# ip route get 10.0.0.42 from 192.168.0.3 iif wg0
10.0.0.42 from 192.168.0.3 dev eth0
    cache iif wg0

Fehlersuche bei WireGuard

Die Fehlersuche bei WireGuard gestaltet sich einfach. Als erstes sollte man mit wg show immer prüfen, ob die Konfiguration dem entspricht, was man haben will, und wann der letzte Kontakt zur Gegenstelle war. Weiterhin sollte man auch immer die IP-Adressen mit ip a und die Routen mit ip r bzw. ip -6 r prüfen.

Mit tcpdump kann man direkt sehen, was auf der Schnittstelle alles passiert. Dabei allerdings nicht verwirren lassen, denn alle Pakete tauchen zweimal auf: das erste Mal als eingehendes und das zweite Mal als ausgehendes Paket.

# tcpdump -i wg0 -n not port ssh
tcpdump: listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes
10:31:09.408191 IP6 (flowlabel 0x10649, hlim 64, next-header ICMPv6 (58) payload length: 64) fe80::2 > fe80::3: [icmp6 sum ok] ICMP6, echo request, seq 1
10:31:09.408236 IP6 (flowlabel 0x10649, hlim 63, next-header ICMPv6 (58) payload length: 64) fe80::2 > fe80::3: [icmp6 sum ok] ICMP6, echo request, seq 1
10:31:09.444464 IP6 (flowlabel 0x48d7a, hlim 64, next-header ICMPv6 (58) payload length: 64) fe80::3 > fe80::2: [icmp6 sum ok] ICMP6, echo reply, seq 1
10:31:09.444581 IP6 (flowlabel 0x48d7a, hlim 63, next-header ICMPv6 (58) payload length: 64) fe80::3 > fe80::2: [icmp6 sum ok] ICMP6, echo reply, seq 1

Für IPv6 muss man beim Ping eines Link-Local-Systems immer die Schnittstelle angeben, auf die sich die Adresse bezieht, da fe80:: über jede Schnittstelle zu finden ist: ping -c1 -I wg0 fe80::3

Als dritte Option für die Fehlersuche, kann man die Debugmeldungen für WireGuard im Kernel nutzen. Diese lassen sich mit folgenden Befehlen aktivieren bzw. dann später wieder deaktivieren. Mit journalctl -fk --grep=^wireguard kann man diese verfolgen.

# echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
# echo module wireguard -p > /sys/kernel/debug/dynamic_debug/control

Weiteres

Dynamische Adressvergabe an Clients

Es gibt zwar die Idee von wg-dynamic, wie den Clients dynamisch IP-Adressen zugewiesen werden könnten, da DHCP Layer 2 und Layer 3 nutzt und daher die Pakete nicht von WireGuard übertragen werden. Aber in der Anleitung wird auch erklärt, dass eine dynamische Adressvergabe keinen großen Gewinn bringt, da man weiterhin die öffentlichen Schlüssel aller Clients verwalten muss.

Konfiguration über Systemd-Networkd

Networkd bietet auch die Konfiguration von WireGuard-Netzwerken an: WireGuard (via systemd-networkd) — Elouworld. Hierbei muss man darauf achten, dass die Datei mit dem privaten Schlüssel nur für root zugänglich ist.

Wireguard unter Windows automatisch starten

Nachdem das Programm für Wireguard (direkt über die Webseite oder über einen der Paketmanager choco oder winget) installiert wurde, lässt sich die Verbindung auch automatisch für alle Benutzer starten: In der Admin-Powershell (Windows-Taste und x) muss der folgende Befehl ausgeführt werden, wobei die Datei wg0.conf die Konfiguration für den Tunnel enthält.

& 'C:\Program Files\WireGuard\wireguard.exe' /installtunnelservice "C:\Program Files\WireGuard\Data\wg0.conf"

Deinstallieren lässt sich der Dienst danach mit

& 'C:\Program Files\WireGuard\wireguard.exe' /uninstalltunnelservice wg0

Android-App

Die Android-App für WireGuard ist klein und einfach gehalten. Praktisch daran ist, dass man die Konfiguration über einen QR-Code einlesen kann. Wer also auf dem Server die Konfigurationsdatei für den Client erstellt, kann diese einfach mit qrencode -t UTF8 -m 2 -o - -r wg0.conf auf der Kommandozeile anzeigen und in der App laden.

Vollverschlüsselung mit Network-Namespaces

Man kann den kompletten Datenverkehr mit WireGuard absichern, indem die eigentlichen Netzwerkschnittstellen in einen eigenen Namespace verschoben werden und im Hauptsystem der Weg ins Internet nur über WireGuard laufen kann. Eine coole Idee, aber bei DHCP für die Primärverbindung, sollte diese Konfiguration an dem gleichen Problem kranken, wie die Konfiguration über Anpassung der Routen: der DHCP-Client kann sein Lease nicht erneuern und daher ist irgendwann die Verbindung tot.

wireguard-p2p für WireGuard mit DSL

Der Ansatz bei wireguard-p2p ist, dass eines der Systeme eine öffentliche IP-Adresse hat, die sich jedoch ändern kann; typische Fall von DSL. Genau diese Adresse wird über ein öffentliches System (eine Art schwarzes Brett) ausgetauscht und damit ist der Verbindungsaufbau möglich. Ausprobiert habe ich es jedoch nicht.