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.2 ist im Interface-Subnetz. Pakete an andere 10.5.0.x-Adressen haben keine direkte Route über tun_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:

  1. 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.

  2. In der Interface-Zuweisung (Interfaces > Assignments, hier OPT3):

    <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:

  1. interface_configure() reißt das Interface kurz runter und neu hoch.
  2. dpinger verliert während der ~2 Sekunden den Ping zum 10.5.0.1, markiert das Gateway als “down”.
  3. Die Gateway-Group failovert auf Tier 2 (WAN_PPPOE).
  4. Selbst nachdem dpinger das Tunnel-Gateway wieder als “up” erkannte, blieb die installierte Default-Route auf WAN hängen — setup_gateways_monitor() und filter_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:

MetrikVorher (/32)Nachher (/16)
Output-Errors/Sekunde~3~0.03
Drop-Rate1.1%0.01%
Zabbix-Triggerfeuertquiet

Faktor 100 besser. Der Rest sind echte einzelne Drops, die sich bei 60 Mbit Upload nicht vermeiden lassen.

Was hilft / Was nicht hilft

MaßnahmeHilft?
<wireguard> mask auf 16 prüfennein, war schon korrekt
<interfaces><optX><subnet> auf 16 setzenja, das war der Fix
WireGuard-Service neustartennein, Subnetz ist Interface-Layer
NordVPN-Endpoint rotierennein, Tunnel war gesund
dpinger Monitor-IP anpassennein, war schon 10.5.0.1
route add default 10.5.0.2 nach Re-Configureja, sonst hängt Default auf WAN
pfctl -F states nach Route-Switchja, 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 ifconfig schauen.
  • 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 ifOutErrors lohnen sich auch auf Tunnel-Interfaces. Ohne den Trigger wäre das hier wahrscheinlich nie aufgefallen.