Featured image of post OPNsense Performance Tweaks für 10G und WireGuard

OPNsense Performance Tweaks für 10G und WireGuard

Einleitung

Eigentlich wollte ich nur wissen, warum die Synology Snapshot Replication über WireGuard mit gemütlichen 8 MB/s vor sich hin dümpelt. Bei symmetrischen 10 Gbit/s und potenter OPNsense-Hardware auf beiden Seiten wäre da eigentlich noch deutlich Luft nach oben. 🤨

Also etwas genauer unter die Haube geschaut. Dabei fielen mir Packetloss beziehungsweise Drops auf - und aus dem kurzen Nachschauen wurde, wie so oft, eine etwas längere Tuning-Session. 🔧

Der Vorzustand

Die 8 MB/s der Snapshot Replication waren der Auslöser. Bevor ich an OPNsense etwas verändert habe, wollte ich deshalb erst einmal wissen, was der Tunnel unabhängig von Synology überhaupt hergibt.

Die ersten iPerf3-Tests waren ziemlich ernüchternd. Ein einzelner TCP-Stream in Reverse-Richtung startete noch mit knapp 400 bis 550 Mbit/s, brach während des Tests aber immer weiter ein:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ iperf3 -c site-a -R

[  5]   0.00-1.00   sec  46.8 MBytes   392 Mbits/sec
[  5]   1.00-2.00   sec  61.1 MBytes   513 Mbits/sec
[  5]   2.00-3.00   sec  65.6 MBytes   550 Mbits/sec
[  5]   3.00-4.00   sec  59.2 MBytes   497 Mbits/sec
[  5]   4.00-5.00   sec  36.4 MBytes   305 Mbits/sec
[  5]   5.00-6.00   sec  22.4 MBytes   188 Mbits/sec
[  5]   6.00-7.00   sec  20.5 MBytes   172 Mbits/sec
[  5]   7.00-8.00   sec  16.0 MBytes   134 Mbits/sec
[  5]   8.00-9.00   sec  14.1 MBytes   118 Mbits/sec
[  5]   9.00-10.00  sec  9.12 MBytes  76.5 Mbits/sec

[  5]   0.00-10.00  sec   351 MBytes   295 Mbits/sec  receiver

Auch vier parallele gaben ein ähnliches Bild ab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ iperf3 -c site-a -R -P 4

[SUM]   0.00-1.00   sec   118 MBytes   993 Mbits/sec
[SUM]   1.00-2.00   sec  95.5 MBytes   801 Mbits/sec
[SUM]   2.00-3.00   sec  70.8 MBytes   593 Mbits/sec
[SUM]   3.00-4.00   sec  60.9 MBytes   511 Mbits/sec
[SUM]   4.00-5.00   sec  57.9 MBytes   485 Mbits/sec
...
[SUM]   9.00-10.00  sec  40.2 MBytes   338 Mbits/sec

[SUM]   0.00-10.00  sec   660 MBytes   553 Mbits/sec  receiver

Dazu kamen 4154 Retransmits auf Senderseite.

In der Gegenrichtung sah es ebenfalls nicht wirklich nach einem 10G-Setup aus:

1
2
3
4
$ iperf3 -c site-a -P 4

[SUM]   0.00-10.00  sec   486 MBytes   408 Mbits/sec  9052  sender
[SUM]   0.00-10.03  sec   475 MBytes   397 Mbits/sec        receiver

Ein einzelner Stream lag bei etwa 356 Mbit/s:

1
2
3
4
$ iperf3 -c site-a

[  5]   0.00-10.00  sec   427 MBytes   358 Mbits/sec  1557  sender
[  5]   0.00-10.03  sec   425 MBytes   356 Mbits/sec        receiver

Damit war zumindest klar: Das Problem war nicht nur die Snapshot Replication. Im Netzwerkpfad selbst gab es ebenfalls etwas zu untersuchen.

Hardware

Hardware Site A Site B
Mainboard Supermicro X11SDV-4C-TP8F Supermicro X10SDV-8C-TLN4F
CPU-Generation Xeon D, Skylake Xeon D, Broadwell
CPU-Kerne 4 8
RAM 32 GB ECC 32 GB ECC
Anschluss 10 Gbit/s 10 Gbit/s
WireGuard Dual Stack (IPv4 + IPv6) Dual Stack (IPv4 + IPv6)

Site A setzt mit dem X11SDV-4C-TP8F zwar nur auf vier CPU-Kerne, dafür aber auf die neuere Skylake-basierte Xeon-D-Generation. Site B bringt mit dem X10SDV-8C-TLN4F acht Kerne mit, basiert jedoch noch auf den älteren und pro Kern langsameren Broadwell-Xeon-Ds.

OPNsense

Auf beiden Seiten sind aktuelle Versionen von OPNsense im Einsatz:

Software Site A Site B
OPNsense Business Edition 26.4.1p2 Community 26.7.1_1

Drops und netisr

Auf Site A wurde es dann konkreter. netstat -Q zeigte, dass ein IPv4-Workstream seine Queue tatsächlich bis zum Limit von 1000 gefüllt hatte:

1
2
WSID CPU   Name   Len WMark   QDrops
   2   2   ip       0  1000      589

Passend dazu:

1
2
net.inet.ip.intr_queue_maxlen: 1000
net.inet.ip.intr_queue_drops: 589

Die 589 Drops waren für mich der erste handfeste Hinweis, dass die Paketverarbeitung unter Last tatsächlich an eine Grenze gelaufen war.

RSS

Eine der Änderungen war das Aktivieren von Receive Side Scaling(RSS). Gerade bei 10-Gbit-Interfaces ist es sinnvoll, die Paketverarbeitung auf mehrere CPUs beziehungsweise Queues verteilen zu können.

Auf beiden Firewalls ist nun RSS aktiviert:

1
2
3
4
5
6
7
# Site A – 4 Cores
net.inet.rss.enabled=1
net.inet.rss.bits=2

# Site B – 8 Cores
net.inet.rss.enabled=1
net.inet.rss.bits=3

Dazu laufen auf beiden Seiten:

1
2
3
net.isr.maxthreads=-1
net.isr.bindthreads=1
net.isr.defaultqlimit=2048

maxthreads=-1 lässt FreeBSD die Anzahl der verwendeten CPUs automatisch bestimmen. Mit bindthreads=1 werden die netisr-Threads an CPUs gebunden.

Queues

Die relevanten Queues habe ich nach diversen Tests auf beiden Firewalls auf 2048 gesetzt:

1
2
3
4
net.link.ifqmaxlen=2048
net.inet.ip.intr_queue_maxlen=2048
net.inet6.ip6.intr_queue_maxlen=2048
net.isr.defaultqlimit=2048

IPv4 und IPv6 sind damit an dieser Stelle gleich behandelt. Nach der Änderung zeigte netstat -Q für IPv4 entsprechend:

1
2
Name   Proto QLimit Policy Dispatch Flags
ip         1   2048    cpu   hybrid   C--

Der bereits aufgelaufene Drop-Counter wird dadurch natürlich nicht zurückgesetzt. Entscheidend war, dass er bei den anschliessenden Tests nicht weiter hochlief.

Das Ganze ist übrigens nicht WireGuard-spezifisch. Die Queue- und RSS-Anpassungen betreffen den normalen Netzwerk-Stack und sind damit auch für den übrigen Traffic interessant.

Tunables

Tunable Site A Site B Zweck
net.inet.rss.enabled 1 1 RSS aktivieren
net.inet.rss.bits 2 3 4 bzw. 8 RSS-Buckets
net.isr.maxthreads -1 -1 Thread-Anzahl automatisch
net.isr.bindthreads 1 1 netisr-Threads an CPUs binden
net.isr.defaultqlimit 2048 2048 netisr Queue-Limit
net.link.ifqmaxlen 2048 2048 Interface Send Queue
net.inet.ip.intr_queue_maxlen 2048 2048 IPv4 Input Queue
net.inet6.ip6.intr_queue_maxlen 2048 2048 IPv6 Input Queue
hw.ixl.enable_head_writeback 0 0 ixl Head Writeback
hw.vtnet.csum_disable 1 1 vtnet Checksum-Offload deaktivieren
vm.numa.disabled 1 0 NUMA-Verhalten der Plattform

Die unterschiedlichen RSS-Bits sind Absicht: 2 entspricht vier RSS-Buckets auf Site A, 3 entsprechend acht auf Site B.

Nicht jede Zeile in dieser Tabelle war die Lösung für mein Problem. Sie dokumentiert aber den relevanten Stand der beiden Firewalls.

MTU & MSS

An der WireGuard-MTU selbst habe ich bewusst nicht herumoptimiert. Sie steht auf beiden Seiten beim Default von:

1
MTU = 1420

Statt die MTU des Tunnels zu verändern, habe ich mich bei den Tests auf MSS Clamping über die Firewall-Normalization-Regeln von OPNsense konzentriert.

Mit MSS 1360 wurde das Bild bereits deutlich besser. Bei vier Reverse-Streams waren über 1 Gbit/s drin:

1
[SUM]   0.00-10.00  sec  1.36 GBytes  1.17 Gbits/sec  receiver

Danach habe ich noch MSS 1340 getestet. Nicht jeder einzelne Lauf war damit schneller – die Resultate schwankten teilweise deutlich –, insgesamt gefiel mir das Verhalten aber besser.

Aktuell laufen deshalb beide Seiten mit:

1
2
WireGuard MTU = 1420
Max MSS       = 1340

Die WireGuard-MTU bleibt damit unangetastet. Mit XGS-PON irgendwo im Pfad nehme ich beim MSS lieber ein paar Bytes Reserve mit.

UDP war unschuldig

Da WireGuard UDP verwendet, hatte ich zwischendurch auch die UDP-Buffer im Verdacht.

Die Statistik war allerdings ziemlich eindeutig:

1
0 dropped due to full socket buffers

Auch bei den mbufs gab es keinen Hinweis auf einen Engpass:

1
2
0 requests for mbufs denied
0 requests for mbufs delayed

An den UDP- und Socket-Buffern habe ich deshalb nichts verändert. Man muss ja nicht an jeder Schraube drehen, nur weil sie da ist.

Nach den Änderungen

Ein guter Single-Stream-Lauf mit MSS 1340 sah danach beispielsweise so aus:

1
2
3
4
5
6
7
8
$ iperf3 -c site-a -R

[  5]   0.00-1.00   sec  82.6 MBytes   693 Mbits/sec
[  5]   1.00-2.00   sec  94.4 MBytes   791 Mbits/sec
[  5]   2.00-3.00   sec  75.0 MBytes   629 Mbits/sec
[  5]   3.00-4.00   sec  54.5 MBytes   457 Mbits/sec
...
[  5]   0.00-10.00  sec   453 MBytes   380 Mbits/sec  receiver

Der Durchschnitt erzählt hier nur einen Teil der Geschichte. Vorher war schon bei einem einzelnen Stream gut zu sehen, wie der Durchsatz im Test bis auf 76 Mbit/s absackte. Nach den Anpassungen waren Peaks von 700 bis knapp 800 Mbit/s möglich.

Noch deutlicher wird der Unterschied mit vier parallelen Streams:

1
2
3
4
5
6
7
8
$ iperf3 -c site-a -R -P 4

[SUM]   0.00-1.00   sec   164 MBytes  1.38 Gbits/sec
[SUM]   1.00-2.00   sec   200 MBytes  1.68 Gbits/sec
[SUM]   2.00-3.00   sec   210 MBytes  1.76 Gbits/sec
[SUM]   3.00-4.00   sec   184 MBytes  1.55 Gbits/sec
...
[SUM]   0.00-10.00  sec  1.49 GBytes  1.28 Gbits/sec  receiver

Zum Vergleich: Einer der frühen -R -P 4-Tests kam auf 553 Mbit/s im Mittel. Nach den Anpassungen waren es in diesem Lauf 1.28 Gbit/s.

Ganz verschwunden sind die TCP-Retransmits allerdings nicht. Das Setup ist also nicht plötzlich perfekt – aber das Bild ist ein deutlich anderes als am Anfang. 😎

Fazit

Auslöser für die ganze Aktion waren die 8 MB/s bei der Synology Snapshot Replication. Dabei kamen mit Packetloss, Queue-Drops und vielen TCP-Retransmits allerdings noch ein paar andere Baustellen zum Vorschein.

Nach RSS, grösseren Queues und MSS-Clamping sieht der Netzwerkpfad deutlich besser aus. Aus rund 553 Mbit/s mit vier Streams wurden in späteren Tests bis zu 1.28 Gbit/s über WireGuard.

Das eigentliche Problem konnte ich damit allerdings nicht lösen: Die Synology Snapshot Replication blieb langsam.

Für die Replikation bin ich deshalb schliesslich auf rclone mit mehreren parallelen Streams umgestiegen. Damit stieg der Durchsatz massiv – und die optimierte Netzwerkverbindung kann nun auch tatsächlich besser ausgenutzt werden.

Unterm Strich also nicht ganz die Lösung, nach der ich ursprünglich gesucht hatte, aber immerhin läuft der 10G-/WireGuard-Pfad jetzt deutlich besser. 🙂

Formerly known as struband.net
Build 10.08.2026