Das Symptom
Eine pfSense (gw-tatooine, FreeBSD 15, pfSense 2.8.1) routet das LAN über einen NordVPN WireGuard-Tunnel. Auf den ersten Blick alles in Ordnung:
- Handshake aktuell, 152 GiB durchgepumpt
- dpinger Monitor-Target steht korrekt auf
10.5.0.1(kein Self-Ping, das war ein anderes Pitfall) - Internet funktioniert, Site-to-Site VPN nach Hetzner steht
- Gateway-Group hält den Tunnel als Tier 1
Trotzdem feuert Zabbix einen Trigger:
Interface tun_wg0: High output error rate (>2/s über 5 min)
Diagnose
netstat auf der pfSense zeigt das Ausmaß:
[2.8.1-RELEASE][root@gw-tatooine]/root: netstat -idn | grep tun_wg0
tun_wg0 1420 <Link#11> 65878214 0 0 66019447 730517 0 0
Heißt: 730.517 Output-Errors auf 66 Mio gesendete Pakete — rund 1.1% Drop-Rate. Bei 60 Mbit Upload merkt man das nicht direkt, aber bei jedem TCP-Retransmit eben doch.
Ein Blick aufs Interface:
[2.8.1-RELEASE][root@gw-tatooine]/root: ifconfig tun_wg0
tun_wg0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1420
inet 10.5.0.2 netmask 0xffffffff
netmask 0xffffffff ist /32. Das ist genau das, wovor die NordVPN-WireGuard-Doku für pfSense warnt — eigentlich gehört da eine /16 hin. Bekannt war: Mit /32 leitet der NordVPN-Server kein Internet weiter. Aber der Tunnel funktionierte ja, Internet lief. Warum dann die Out-Errors?
Warum eine /32 trotz funktionierendem Internet Pakete dropt
Die Logik dahinter:
- /32 sagt: nur die eigene Adresse
10.5.0.2ist im Interface-Subnetz. Pakete an andere10.5.0.x-Adressen haben keine direkte Route übertun_wg0. - WireGuard-Daemon schickt Keepalives ans Gateway
10.5.0.1— das wird über die explizite WireGuard-Peer-Allowed-IPs geroutet, also OK. - NordVPN-interne Reply-Pakete oder Multicast-/Broadcast-Frames an andere Peers im 10.5.0.0/16 finden aber keine Route und werden vom Stack als Output-Error gezählt.
- Bei /16 ist das ganze 10.5.0.0/16-Subnetz auf dem Interface erreichbar, der Stack wirft die Pakete einfach raus, der Tunnel-Verschlüsselungs-Layer entscheidet was Endpoint-seitig passiert.
Internet-Routing über die Default-Route bleibt davon unberührt — deswegen “läuft alles”, aber das Side-Traffic dropt im Hintergrund.
Die UI-Falle
In der pfSense-Konfig ist die WireGuard-Tunnel-Adresse zweimal hinterlegt:
Im WireGuard-Block selbst (
Status > WireGuard > Tunnels):<wireguard> <tunnels> <item> <name>tun_wg0</name> <addresses> <row> <address>10.5.0.2</address> <mask>16</mask> </row> </addresses>Hier stand korrekt /16.
In der Interface-Zuweisung (
Interfaces > Assignments, hierOPT3):<interfaces> <opt3> <if>tun_wg0</if> <ipaddr>10.5.0.2</ipaddr> <subnet>32</subnet> </opt3>Hier stand /32.
pfSense betrachtet WireGuard-Tunnel und die Interface-Zuweisung als zwei getrennte Konfigurations-Welten. Was aufs Interface gebunden wird, ist der Wert aus <interfaces>, nicht der aus <wireguard>. Die WG-GUI zeigt /16, der Kernel sieht /32 — und keiner warnt.
Fix
Über die pfSsh.php-Konsole oder direkt per Skript:
require_once("config.inc");
require_once("interfaces.inc");
$config["interfaces"]["opt3"]["subnet"] = "16";
write_config("Fix tun_wg0 subnet mask 32 -> 16");
interface_configure("opt3", true);
Danach:
[2.8.1-RELEASE][root@gw-tatooine]/root: ifconfig tun_wg0 | grep inet
inet 10.5.0.2 netmask 0xffff0000
0xffff0000 = /16, passt.
Die zweite Falle: weggesprungene Default-Route
Direkt nach interface_configure("opt3", true) kam Internet kurz nicht mehr durch. netstat -rn zeigte:
default 192.168.178.1 UGS pppoe0
Statt über den Tunnel ging die Default-Route plötzlich über WAN (FritzBox via PPPoE). Was passiert war:
interface_configure()reißt das Interface kurz runter und neu hoch.- dpinger verliert während der ~2 Sekunden den Ping zum 10.5.0.1, markiert das Gateway als “down”.
- Die Gateway-Group failovert auf Tier 2 (WAN_PPPOE).
- Selbst nachdem dpinger das Tunnel-Gateway wieder als “up” erkannte, blieb die installierte Default-Route auf WAN hängen —
setup_gateways_monitor()undfilter_configure()reichten nicht.
Sichtbar an der Außen-IP:
curl -s https://api.ipify.org
# vorher: 89.246.x.x (NordVPN)
# jetzt: 91.32.x.x (FritzBox-WAN-IP)
Das ist der Moment, in dem 50 LAN-Geräte unverschlüsselt über die FritzBox ins Internet gehen — nicht das, was man bei einer “wir-routen-alles-über-VPN”-Policy haben will.
Manueller Fix:
route delete default
route add default 10.5.0.2
pfctl -F states
Alle States flushen ist wichtig, sonst bleiben bestehende Verbindungen über WAN gepinnt, neue gehen über WG, und man hat asymmetrisches Routing.
Verifikation
curl -s https://api.ipify.org
# 89.246.x.x (NordVPN, korrekt)
Output-Errors über die folgenden 12 Stunden:
| Metrik | Vorher (/32) | Nachher (/16) |
|---|---|---|
| Output-Errors/Sekunde | ~3 | ~0.03 |
| Drop-Rate | 1.1% | 0.01% |
| Zabbix-Trigger | feuert | quiet |
Faktor 100 besser. Der Rest sind echte einzelne Drops, die sich bei 60 Mbit Upload nicht vermeiden lassen.
Was hilft / Was nicht hilft
| Maßnahme | Hilft? |
|---|---|
<wireguard> mask auf 16 prüfen | nein, war schon korrekt |
<interfaces><optX><subnet> auf 16 setzen | ja, das war der Fix |
| WireGuard-Service neustarten | nein, Subnetz ist Interface-Layer |
| NordVPN-Endpoint rotieren | nein, Tunnel war gesund |
| dpinger Monitor-IP anpassen | nein, war schon 10.5.0.1 |
route add default 10.5.0.2 nach Re-Configure | ja, sonst hängt Default auf WAN |
pfctl -F states nach Route-Switch | ja, gegen asymmetrisches Routing |
Reboot statt interface_configure() | wäre auch gegangen, dauert nur länger |
Lessons Learned
- pfSense hält die Subnetzmaske eines WireGuard-Interfaces an zwei Stellen in der Config. Die WG-GUI zeigt nicht zwingend, was am Kernel-Interface anliegt. Im Zweifel
ifconfigschauen. - Output-Errors auf einem WireGuard-Tunnel sind ein guter Indikator für Routing-Probleme im Tunnel-Subnetz, auch wenn das Internet darüber augenscheinlich funktioniert.
- Jeder Reconfigure eines Gateway-Interfaces kann eine Gateway-Group zum Failover auf Tier 2 zwingen — und die installierte Default-Route bleibt danach hängen, bis sie aktiv neu gesetzt wird. Bei VPN-Policies mit Kill-Switch-Anspruch lieber kurz die Connectivity-IP prüfen, bevor man weitergeht.
- Zabbix-Trigger auf
ifOutErrorslohnen sich auch auf Tunnel-Interfaces. Ohne den Trigger wäre das hier wahrscheinlich nie aufgefallen.