Selektives VPN-Routing: Bestimmte IPs über ProtonVPN tunneln
Du betreibst ein WireGuard-VPN mit wg-easy und willst, dass bestimmter Traffic deiner Clients nicht über deine Heim-IP rausgeht, sondern anonym über einen kommerziellen VPN-Provider? Genau dafür gibt es Policy-Based Routing (PBR). Damit routest du gezielt einzelne IPs oder Domains durch einen zweiten WireGuard-Tunnel - zum Beispiel ProtonVPN - während der Rest normal weiterläuft.
Falls du WireGuard und wg-easy noch nicht kennst: Im WireGuard-Grundlagen-Post findest du alles zum Einstieg, und im v15-Migrations-Post die Neuerungen der aktuellen Version.
Warum selektives Routing?
Stell dir vor: Deine Familie und Freunde nutzen dein WireGuard-VPN, um auf Plex, Jellyfin und Overseerr zuzugreifen. Für 99% des Traffics ist das perfekt - die Pakete fließen direkt von deinem Homelab zum Ziel.
Aber es gibt Situationen, in denen du nicht willst, dass die Ziel-Website deine echte WAN-IP sieht. Beispiele:
- Tracker-Foren mit Registrierung
- Download-Seiten, die IPs loggen
- Services, bei denen du deine Privatsphäre schützen willst
Die naive Lösung wäre ein Full Tunnel über ProtonVPN. Problem: Aller Traffic deiner VPN-Clients würde dann durch den kommerziellen VPN laufen - langsamer, unnötig, und Plex-Streams würden über die Schweiz geroutet statt direkt.
Policy-Based Routing löst das elegant: Du definierst eine Liste von IPs und Domains. Nur Traffic zu diesen Zielen geht durch den ProtonVPN-Tunnel. Alles andere fließt normal.
flowchart TB
Client([VPN-Client]) --> WG[wg-easy Server<br>wg0 Interface]
WG --> Check{Ziel-IP in<br>Route-Liste?}
Check -->|Nein| Direct[Direktes Routing<br>über Heim-IP]
Check -->|Ja| Proton[ProtonVPN-Tunnel<br>proton0 Interface]
Direct --> Target1[Plex, Jellyfin<br>Homelab-Services]
Direct --> Target2[Normales Internet<br>deine WAN-IP]
Proton --> Target3[Tracker-Forum<br>sieht ProtonVPN-IP]
style Client fill:#e1f5ff
style Proton fill:#ffe1e1
style Direct fill:#e1ffe1
style Check fill:#fff3e0
Wie funktioniert PBR?
Policy-Based Routing klingt kompliziert, ist aber im Kern ein einfacher Dreischritt:
- Markieren: Pakete, deren Ziel-IP in einer Liste steht, bekommen ein “Etikett” (fwmark)
- Routen: Pakete mit diesem Etikett werden über eine separate Routing-Tabelle geleitet
- Tunneln: Die separate Routing-Tabelle zeigt auf das ProtonVPN-Interface
Die Komponenten im Überblick
| Komponente | Tool | Aufgabe |
|---|---|---|
| IP-Liste | ipset |
Hält alle Ziel-IPs in einer performanten Hash-Tabelle |
| Paket-Markierung | iptables mangle |
Markiert Pakete mit fwmark wenn Ziel im ipset |
| Routing-Regel | ip rule |
Leitet markierte Pakete an Tabelle 100 |
| Routing-Tabelle | ip route table 100 |
Default-Route über proton0 |
| NAT | iptables nat |
MASQUERADE auf dem proton0 Interface |
| VPN-Tunnel | wg-quick |
WireGuard-Tunnel zu ProtonVPN |
Paketfluss im Detail
So läuft ein Paket durch den Stack, wenn ein VPN-Client eine IP aus der Route-Liste aufruft:
flowchart LR
A[VPN-Client<br>10.8.0.3] -->|Paket an<br>178.17.x.x| B[wg0]
B -->|mangle<br>PREROUTING| C{ipset<br>vpn-routes}
C -->|Match!| D[fwmark 0x1]
D -->|ip rule| E[Table 100]
E -->|default dev<br>proton0| F[MASQUERADE]
F --> G[proton0<br>Tunnel]
G --> H[ProtonVPN<br>Server CH]
H --> I[Ziel-Website<br>sieht CH-IP]
style C fill:#fff3e0
style D fill:#ffe1e1
style G fill:#e1f5ff
Und für normalen Traffic (Ziel-IP nicht in der Liste):
flowchart LR
A[VPN-Client<br>10.8.0.3] -->|Paket an<br>192.168.60.x| B[wg0]
B -->|mangle<br>PREROUTING| C{ipset<br>vpn-routes}
C -->|Kein Match| D[Kein fwmark]
D -->|Main Table| E[eth0<br>normales Routing]
E --> F[Homelab<br>Service]
style C fill:#fff3e0
style D fill:#e1ffe1
Der entscheidende Punkt: Das ipset-Lookup in der mangle-Chain ist extrem schnell (O(1) dank Hash-Tabelle). Selbst mit hunderten Einträgen merkst du keine Latenz.
Voraussetzungen
Bevor du loslegst, brauchst du:
- wg-easy läuft bereits (v14 oder v15, egal)
- ProtonVPN-Account (kostenlos reicht, aber Paid hat mehr Server und Port-Forwarding)
- Root-Zugang auf der wg-easy VM
- Pakete:
wireguard-tools,ipset,dnsutils,iptables,cron
Pakete installieren
1
2
3
sudo apt update
sudo apt install -y wireguard-tools ipset dnsutils iptables cron
sudo systemctl enable cron
ProtonVPN WireGuard-Config holen
- Öffne ProtonVPN → Downloads → WireGuard
- Wähle einen Server (z.B. Schweiz, Niederlande, Island)
- NAT-PMP (Port-Weiterleitung): An (falls du P2P nutzt)
- Klicke auf “Config generieren” und lade die
.confherunter
Die Config sieht ungefähr so aus:
1
2
3
4
5
6
7
8
9
10
11
[Interface]
PrivateKey = abc123...
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
# CH#937
PublicKey = xyz789...
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 62.169.136.3:51820
PersistentKeepalive = 25
Schritt 1: Verzeichnis und Config anlegen
1
sudo mkdir -p /opt/wg-easy/proton-pbr
Kopiere die ProtonVPN-Config und passe sie an:
1
2
3
4
5
6
7
8
9
10
11
12
[Interface]
PrivateKey = abc123...
Address = 10.2.0.2/32
# DNS = 10.2.0.1
Table = off
[Peer]
# CH#937
PublicKey = xyz789...
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 62.169.136.3:51820
PersistentKeepalive = 25
Zwei wichtige Änderungen:
| Änderung | Warum |
|---|---|
DNS auskommentiert |
Wir wollen den DNS-Server des Homelabs nutzen (z.B. Pi-hole), nicht den von ProtonVPN. Ohne diese Änderung würde wg-quick die /etc/resolv.conf überschreiben. |
Table = off hinzugefügt |
Verhindert, dass wg-quick automatisch Routing-Regeln anlegt. Ohne Table = off würde aller Traffic durch den Tunnel gehen - das Gegenteil von dem, was wir wollen. Das Routing übernimmt stattdessen unser PBR-Setup mit fwmark und ipset. |
1
2
# Config-Permissions einschränken (enthält Private Key!)
sudo chmod 600 /opt/wg-easy/proton-pbr/proton0.conf
Der Private Key in
proton0.confist sensibel. Stelle sicher, dass nur root die Datei lesen kann.
Schritt 2: Route-Liste erstellen
Die routes.conf definiert, welche IPs und Domains durch den Tunnel gehen sollen:
1
2
3
4
5
6
7
8
9
10
11
12
13
# PBR Routes - IPs und Domains die über ProtonVPN geroutet werden
# Eine IP oder Domain pro Zeile
# Leerzeilen und Kommentare (#) werden ignoriert
# Nach Änderungen: pbr-update.sh ausführen oder auf nächsten Cron warten
# === IPs ===
178.17.166.194
185.195.236.11
103.214.7.28
# === Domains (werden per dig aufgelöst) ===
# example-tracker.org
# some-forum.net
Format-Regeln:
| Typ | Beispiel | Verhalten |
|---|---|---|
| Kommentar | # Das ist ein Kommentar |
Wird ignoriert |
| Leerzeile | Wird übersprungen | |
| IPv4-Adresse | 178.17.166.194 |
Wird direkt ins ipset übernommen |
| Domain | example-tracker.org |
Wird per dig aufgelöst, alle A-Records werden ins ipset übernommen |
Warum Domains unterstützt werden: Manche Websites wechseln ihre IPs (CDN, Load Balancer). Mit Domain-Einträgen löst das Update-Script die IPs alle 15 Minuten neu auf. Wenn sich die IP ändert, wird das ipset automatisch aktualisiert.
Trage hier nur Ziele ein, die wirklich über ProtonVPN laufen sollen. Je weniger Einträge, desto geringer die Komplexität.
Schritt 3: Setup-Script
Das Setup-Script wird beim Systemstart ausgeführt und richtet den kompletten PBR-Stack ein:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/bin/bash
# pbr-setup.sh - Richtet ProtonVPN PBR-Tunnel ein
# Wird beim Systemstart ausgeführt (@reboot cron)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONF="$SCRIPT_DIR/proton0.conf"
ROUTES="$SCRIPT_DIR/routes.conf"
IPSET_NAME="vpn-routes"
TABLE=100
MARK=0x1
IFACE="proton0"
WG_IFACE="wg0"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# Config-Dateien prüfen
if [[ ! -f "$CONF" ]]; then
log "FEHLER: $CONF nicht gefunden"
exit 1
fi
if [[ ! -f "$ROUTES" ]]; then
log "FEHLER: $ROUTES nicht gefunden"
exit 1
fi
# --- 1. ProtonVPN Tunnel starten ---
if ip link show "$IFACE" &>/dev/null; then
log "Tunnel $IFACE läuft bereits, wird neu gestartet..."
wg-quick down "$CONF" 2>/dev/null || true
fi
log "Starte ProtonVPN Tunnel..."
wg-quick up "$CONF"
sleep 2
if ! ip link show "$IFACE" &>/dev/null; then
log "FEHLER: $IFACE nicht gestartet"
exit 1
fi
log "Tunnel $IFACE aktiv"
# --- 2. ipset anlegen ---
if ipset list "$IPSET_NAME" &>/dev/null; then
ipset flush "$IPSET_NAME"
else
ipset create "$IPSET_NAME" hash:ip
fi
# IPs und Domains aus routes.conf laden
while IFS= read -r line; do
line=$(echo "$line" | sed 's/#.*//' | xargs)
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
ipset add "$IPSET_NAME" "$line" -exist
log "IP hinzugefügt: $line"
else
ips=$(dig +short "$line" A 2>/dev/null | grep -E '^[0-9]+\.' || true)
if [[ -n "$ips" ]]; then
while IFS= read -r ip; do
ipset add "$IPSET_NAME" "$ip" -exist
log "Domain $line -> $ip"
done <<< "$ips"
else
log "WARNUNG: Domain $line konnte nicht aufgelöst werden"
fi
fi
done < "$ROUTES"
# --- 3. Routing-Tabelle 100 ---
ip route replace default dev "$IFACE" table "$TABLE"
log "Routing-Tabelle $TABLE: default via $IFACE"
# --- 4. IP Rule: fwmark → Tabelle 100 ---
if ! ip rule show | grep -q "fwmark $MARK.*lookup $TABLE"; then
ip rule add fwmark "$MARK" table "$TABLE" priority 100
log "IP Rule: fwmark $MARK -> table $TABLE"
fi
# --- 5. iptables mangle: Pakete markieren ---
if ! iptables -t mangle -C PREROUTING -i "$WG_IFACE" \
-m set --match-set "$IPSET_NAME" dst \
-j MARK --set-mark "$MARK" 2>/dev/null; then
iptables -t mangle -A PREROUTING -i "$WG_IFACE" \
-m set --match-set "$IPSET_NAME" dst \
-j MARK --set-mark "$MARK"
log "Mangle PREROUTING Regel gesetzt"
fi
# --- 6. NAT/MASQUERADE ---
if ! iptables -t nat -C POSTROUTING -o "$IFACE" -j MASQUERADE 2>/dev/null; then
iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE
log "MASQUERADE für $IFACE gesetzt"
fi
# --- 7. FORWARD-Regeln ---
# Markierten Traffic durchlassen
if ! iptables -C FORWARD -i "$WG_IFACE" \
-m set --match-set "$IPSET_NAME" dst -j ACCEPT 2>/dev/null; then
# Vor einer eventuellen DROP-Regel einfügen
DROP_POS=$(iptables -L FORWARD --line-numbers -n 2>/dev/null \
| awk '/^[0-9].*DROP/{print $1; exit}')
if [[ -n "$DROP_POS" ]]; then
iptables -I FORWARD "$DROP_POS" -i "$WG_IFACE" \
-m set --match-set "$IPSET_NAME" dst -j ACCEPT
else
iptables -A FORWARD -i "$WG_IFACE" \
-m set --match-set "$IPSET_NAME" dst -j ACCEPT
fi
log "FORWARD ACCEPT für vpn-routes gesetzt"
fi
# Antwort-Traffic zurück zum Client
if ! iptables -C FORWARD -i "$IFACE" -o "$WG_IFACE" \
-m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
iptables -I FORWARD 1 -i "$IFACE" -o "$WG_IFACE" \
-m state --state RELATED,ESTABLISHED -j ACCEPT
log "FORWARD RELATED/ESTABLISHED gesetzt"
fi
log "PBR-Setup abgeschlossen"
log "Verifizierung: curl --interface $IFACE https://ifconfig.me"
Was das Script Schritt für Schritt macht:
| Schritt | Befehl | Erklärung |
|---|---|---|
| 1 | wg-quick up |
Startet den WireGuard-Tunnel zu ProtonVPN |
| 2 | ipset create |
Erstellt eine Hash-Tabelle für die Ziel-IPs |
| 3 | ip route table 100 |
Eigene Routing-Tabelle die alles über proton0 schickt |
| 4 | ip rule fwmark |
Pakete mit Mark 0x1 nutzen Tabelle 100 statt Main |
| 5 | iptables mangle |
Markiert Pakete von wg0 deren Ziel im ipset ist |
| 6 | iptables nat |
MASQUERADE: Quell-IP wird zu ProtonVPN-Tunnel-IP |
| 7 | iptables FORWARD |
Erlaubt den markierten Traffic und die Antworten |
Idempotenz: Jeder Schritt prüft vorher, ob die Regel bereits existiert. Das Script kann gefahrlos mehrfach ausgeführt werden - es dupliziert keine Regeln.
Die FORWARD-Regeln müssen vor einer eventuellen DROP-Regel stehen. Das Script erkennt automatisch die Position einer existierenden DROP-Regel und fügt die ACCEPT-Regel davor ein.
Schritt 4: Update-Script
Das Update-Script hält die IP-Liste aktuell - besonders wichtig für Domain-Einträge, deren IPs sich ändern können:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/bin/bash
# pbr-update.sh - Aktualisiert ipset mit IPs/Domains aus routes.conf
# Cron: */15 * * * *
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROUTES="$SCRIPT_DIR/routes.conf"
IPSET_NAME="vpn-routes"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
if [[ ! -f "$ROUTES" ]]; then
log "FEHLER: $ROUTES nicht gefunden"
exit 1
fi
if ! ipset list "$IPSET_NAME" &>/dev/null; then
log "FEHLER: ipset $IPSET_NAME existiert nicht. pbr-setup.sh zuerst ausführen."
exit 1
fi
# Soll-Zustand aus routes.conf berechnen
declare -A WANTED_IPS
while IFS= read -r line; do
line=$(echo "$line" | sed 's/#.*//' | xargs)
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
WANTED_IPS["$line"]=1
else
ips=$(dig +short "$line" A 2>/dev/null | grep -E '^[0-9]+\.' || true)
if [[ -n "$ips" ]]; then
while IFS= read -r ip; do
WANTED_IPS["$ip"]=1
log "Domain $line -> $ip"
done <<< "$ips"
fi
fi
done < "$ROUTES"
# Neue IPs hinzufügen
for ip in "${!WANTED_IPS[@]}"; do
ipset add "$IPSET_NAME" "$ip" -exist
done
# Stale IPs entfernen (im ipset aber nicht mehr in routes.conf)
CURRENT_IPS=$(ipset list "$IPSET_NAME" -output plain 2>/dev/null \
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true)
removed=0
if [[ -n "$CURRENT_IPS" ]]; then
while IFS= read -r ip; do
if [[ -z "${WANTED_IPS[$ip]+x}" ]]; then
ipset del "$IPSET_NAME" "$ip" 2>/dev/null || true
log "Stale IP entfernt: $ip"
((removed++))
fi
done <<< "$CURRENT_IPS"
fi
log "Update: ${#WANTED_IPS[@]} IPs aktiv, $removed entfernt"
Was passiert hier?
- Soll-Zustand berechnen: Alle IPs und aufgelösten Domains aus
routes.confsammeln - Hinzufügen: Neue IPs ins ipset eintragen (existierende werden ignoriert dank
-exist) - Bereinigen: IPs die nicht mehr in
routes.confstehen, aus dem ipset entfernen
Das Bereinigen ist wichtig: Wenn du eine IP oder Domain aus routes.conf entfernst, wird sie beim nächsten Cron-Lauf automatisch aus dem ipset gelöscht. Ohne diesen Schritt würde die alte IP ewig im ipset bleiben.
Schritt 5: Aktivieren
1
2
3
4
5
6
# Scripts ausführbar machen
sudo chmod +x /opt/wg-easy/proton-pbr/pbr-setup.sh
sudo chmod +x /opt/wg-easy/proton-pbr/pbr-update.sh
# PBR jetzt starten
sudo /opt/wg-easy/proton-pbr/pbr-setup.sh
Erwartete Ausgabe:
1
2
3
4
5
6
7
8
9
10
11
12
[2026-02-03 20:15:00] Starte ProtonVPN Tunnel...
[2026-02-03 20:15:02] Tunnel proton0 aktiv
[2026-02-03 20:15:02] IP hinzugefügt: 178.17.166.194
[2026-02-03 20:15:02] IP hinzugefügt: 185.195.236.11
[2026-02-03 20:15:02] IP hinzugefügt: 103.214.7.28
[2026-02-03 20:15:02] Routing-Tabelle 100: default via proton0
[2026-02-03 20:15:02] IP Rule: fwmark 0x1 -> table 100
[2026-02-03 20:15:02] Mangle PREROUTING Regel gesetzt
[2026-02-03 20:15:02] MASQUERADE für proton0 gesetzt
[2026-02-03 20:15:02] FORWARD ACCEPT für vpn-routes gesetzt
[2026-02-03 20:15:02] FORWARD RELATED/ESTABLISHED gesetzt
[2026-02-03 20:15:02] PBR-Setup abgeschlossen
Tunnel verifizieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Ist der Tunnel aktiv?
sudo wg show proton0
# Welche Exit-IP hat der Tunnel?
sudo curl --interface proton0 https://ifconfig.me
# Erwartung: Eine ProtonVPN-IP (NICHT deine WAN-IP)
# Welche IPs sind im ipset?
sudo ipset list vpn-routes
# Ist die Routing-Tabelle korrekt?
sudo ip route show table 100
# Erwartung: default dev proton0 scope link
# Ist die IP Rule gesetzt?
sudo ip rule show | grep fwmark
# Erwartung: 100: from all fwmark 0x1 lookup 100
Schritt 6: Autostart per Cron
Damit das PBR nach einem Reboot automatisch startet:
1
sudo crontab -e
Füge diese zwei Zeilen ein:
1
2
@reboot sleep 30 && /opt/wg-easy/proton-pbr/pbr-setup.sh >> /var/log/pbr-setup.log 2>&1
*/15 * * * * /opt/wg-easy/proton-pbr/pbr-update.sh >> /var/log/pbr-update.log 2>&1
| Eintrag | Wann | Was |
|---|---|---|
@reboot sleep 30 |
30 Sekunden nach Boot | PBR-Stack komplett einrichten. Das sleep 30 gibt Docker und wg-easy Zeit zum Starten. |
*/15 * * * * |
Alle 15 Minuten | IP-Liste aktualisieren (Domain-Auflösung, Stale-IPs entfernen) |
Die Logs landen in
/var/log/pbr-setup.logund/var/log/pbr-update.log. Praktisch fürs Debugging.
IPs und Domains verwalten
Neuen Eintrag hinzufügen
1
2
3
4
5
6
7
8
# IP hinzufügen
echo "203.0.113.42" | sudo tee -a /opt/wg-easy/proton-pbr/routes.conf
# Domain hinzufügen
echo "some-tracker-forum.net" | sudo tee -a /opt/wg-easy/proton-pbr/routes.conf
# Sofort anwenden (statt auf Cron zu warten)
sudo /opt/wg-easy/proton-pbr/pbr-update.sh
Eintrag entfernen
Lösche die Zeile aus routes.conf, dann:
1
sudo /opt/wg-easy/proton-pbr/pbr-update.sh
Das Update-Script erkennt, dass die IP nicht mehr im Soll-Zustand ist, und entfernt sie aus dem ipset.
Aktuelle Liste anzeigen
1
2
3
4
5
# Was steht in routes.conf?
cat /opt/wg-easy/proton-pbr/routes.conf
# Was ist aktuell im ipset aktiv?
sudo ipset list vpn-routes
Zusammenspiel mit wg-easy Hooks
PBR und die wg-easy Firewall-Hooks (PostUp/PostDown) ergänzen sich. Die Hooks steuern, welche Dienste ein Client erreichen darf. PBR steuert, über welchen Weg der Traffic zu bestimmten Zielen fließt.
Reihenfolge der Paketverarbeitung:
1
2
3
4
5
6
7
8
9
10
11
12
13
Paket von VPN-Client (wg0)
│
├── 1. mangle PREROUTING: fwmark setzen (PBR)
│
├── 2. FORWARD-Chain: Erlaubt? (wg-easy Hooks)
│ ├── ACCEPT → weiter
│ └── DROP → Paket verworfen
│
├── 3. Routing-Entscheidung
│ ├── fwmark 0x1 → Table 100 → proton0 (PBR)
│ └── kein fwmark → Main Table → eth0 (normal)
│
└── 4. NAT POSTROUTING: MASQUERADE
Das bedeutet: Wenn ein Client per Whitelist-Hook nur Plex und Jellyfin erreichen darf, wird Traffic zu einer IP in routes.conf trotzdem erst durch die FORWARD-Chain geprüft. Ist die IP nicht in der Whitelist, wird das Paket verworfen - egal ob PBR es markiert hat.
PBR ändert nur den Weg, nicht die Berechtigung. Deine Firewall-Regeln greifen wie gewohnt.
Troubleshooting
Tunnel steht nicht
1
2
3
4
5
6
7
8
# Interface vorhanden?
ip link show proton0
# Falls nicht: manuell starten
sudo wg-quick up /opt/wg-easy/proton-pbr/proton0.conf
# Handshake prüfen (sollte < 2 Min sein)
sudo wg show proton0 | grep "latest handshake"
Kein Handshake? Die ProtonVPN-Config ist abgelaufen oder der Server nicht erreichbar. Generiere eine neue Config in der ProtonVPN-Web-UI.
Traffic geht nicht durch den Tunnel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. Ist die IP im ipset?
sudo ipset test vpn-routes 178.17.166.194
# 2. Wird das Paket markiert?
sudo iptables -t mangle -L PREROUTING -n -v
# Zähler sollten bei Traffic hochgehen
# 3. Ist die Rule aktiv?
sudo ip rule show | grep fwmark
# 4. Geht Table 100 über proton0?
sudo ip route show table 100
# 5. MASQUERADE aktiv?
sudo iptables -t nat -L POSTROUTING -n -v
DNS-Probleme nach Setup
Wenn nach dem Start von pbr-setup.sh keine Domains mehr aufgelöst werden:
- Prüfe ob
proton0.conftatsächlich# DNS = 10.2.0.1(auskommentiert) hat - Ohne Auskommentieren überschreibt
wg-quickdie/etc/resolv.conf
1
2
cat /etc/resolv.conf
# Sollte deinen normalen DNS zeigen, NICHT 10.2.0.1
Alles zurücksetzen
Falls etwas komplett schiefgeht:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Tunnel stoppen
sudo wg-quick down /opt/wg-easy/proton-pbr/proton0.conf
# iptables-Regeln entfernen
sudo iptables -t mangle -D PREROUTING -i wg0 -m set --match-set vpn-routes dst -j MARK --set-mark 0x1
sudo iptables -t nat -D POSTROUTING -o proton0 -j MASQUERADE
sudo iptables -D FORWARD -i wg0 -m set --match-set vpn-routes dst -j ACCEPT
sudo iptables -D FORWARD -i proton0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# IP Rule und Routing-Tabelle entfernen
sudo ip rule del fwmark 0x1 table 100
sudo ip route flush table 100
# ipset löschen
sudo ipset destroy vpn-routes
# Crontab-Einträge entfernen
sudo crontab -e
# Die zwei PBR-Zeilen löschen
Danach ist alles wieder im Ausgangszustand. Dein wg-easy läuft normal weiter - nur ohne das selektive ProtonVPN-Routing.
Sicherheitshinweise
Ein paar Punkte, die du im Hinterkopf behalten solltest:
Private Keys: Die proton0.conf enthält deinen ProtonVPN Private Key. Die Datei sollte nur für root lesbar sein (chmod 600).
DNS-Leaks: Wenn du Table = off vergisst, geht aller Traffic (auch DNS) durch ProtonVPN. Wenn du DNS auskommentierst aber Table = off vergisst, hast du potenziell DNS-Leaks in die andere Richtung. Beides zusammen ist die richtige Konfiguration.
WebRTC-Leaks: PBR schützt auf Netzwerkebene. Browser-basierte Leaks (WebRTC) sind ein Client-seitiges Problem und werden durch PBR nicht adressiert.
Kill Switch: Es gibt keinen automatischen Kill Switch. Wenn der ProtonVPN-Tunnel abbricht, werden markierte Pakete trotzdem über Table 100 geroutet - aber da kein Interface mehr da ist, werden sie verworfen. Der Traffic “fällt” also nicht auf die normale Route zurück. Das ist in der Praxis ein impliziter Kill Switch.
Teste dein Setup immer von einem tatsächlichen VPN-Client aus (Smartphone über mobile Daten). Lokale Tests vom Server selbst gehen nicht durch die mangle-Chain, weil die Pakete nicht über wg0 reinkommen.
Fazit
Policy-Based Routing mit ProtonVPN und wg-easy ist eine elegante Lösung für ein reales Problem: Du willst nicht deinen gesamten Traffic durch einen kommerziellen VPN jagen, aber für bestimmte Ziele soll deine echte IP verborgen bleiben.
Was du bekommst:
- Selektives Routing: Nur definierte IPs/Domains gehen über ProtonVPN
- Keine Auswirkung auf den Rest: Plex-Streams, DNS-Anfragen und normaler Traffic laufen direkt
- Automatische Aktualisierung: Domain-IPs werden alle 15 Minuten neu aufgelöst
- Einfache Verwaltung: IPs/Domains in einer Textdatei hinzufügen oder entfernen
- Überlebt Reboots: Cron-basierter Autostart
Was du brauchst:
- Eine laufende wg-easy Instanz
- Einen ProtonVPN-Account
- 4 Dateien:
proton0.conf,routes.conf,pbr-setup.sh,pbr-update.sh - 2 Crontab-Einträge
Das Setup ist in wenigen Minuten erledigt und läuft danach wartungsfrei. Wenn du einen neuen Eintrag zur Route-Liste hinzufügen willst, ist das eine Zeile in einer Textdatei - fertig.
Ressourcen
- ProtonVPN: protonvpn.com - WireGuard-Configs im Account-Dashboard
- wg-easy: github.com/wg-easy/wg-easy - WireGuard mit Web-UI
- ipset: ipset.netfilter.org - Dokumentation zu IP-Sets
- Linux PBR: Policy Routing (Kernel Docs) - Hintergrund zu fwmark und Routing-Tabellen
- WireGuard: wireguard.com - Protokoll-Dokumentation