OpenVPN-DCO@pfSense 2.9.0 CE
Überblick
Es werden zwei VMs verwendet, beide im libvirt-NAT-Netz default (192.168.122.0/24):
| VM | System | Zweck |
|---|---|---|
| Build-VM | FreeBSD 15.1-RELEASE | Kompiliert das Modul; dient später auch als OpenVPN-Testclient |
| Test-VM | pfSense CE 2.9.0 | Liefert Kernel-Informationen; dort wird das Modul getestet |
Ablauf:
- Build-VM aufsetzen
- pfSense-Test-VM aufsetzen
- Kernel-Informationen der pfSense auslesen
- Passenden FreeBSD-Quellstand bestimmen
- Modul bauen
- Modul auf der pfSense laden und prüfen
- OpenVPN-Server DCO-tauglich konfigurieren
- pfSense-Patch für das Interface-Problem einspielen
- Funktionstest mit Client
Referenzwerte für pfSense CE 2.9.0
| Merkmal | Wert |
|---|---|
Kernel (uname -a)
|
FreeBSD 16.0-CURRENT #12 RELENG_2_9_0-n256132-d8e3138ecf52, gebaut am 17.08.2026
|
kern.osreldate
|
1600018
|
| Kernelkonfiguration | pfSense, vollständig in kern.conftxt eingebettet (417 Zeilen)
|
FreeBSD-main-Fenster mit 1600018
|
1569e2334ab (30.04.2026) bis 255538cd906 (19.06.2026, Sprung auf 1600019)
|
| Verwendeter Build-Stand | 7386c0675c8 (= 255538cd906^, letzter Commit mit 1600018)
|
Abweichung der Konfiguration zu main
|
nur options NETGRAPH_ETF (für if_ovpn irrelevant)
|
Bei einem pfSense-Update ändern sich diese Werte, die Schritte 3 bis 6 sind dann zu wiederholen.
1. Build-VM aufsetzen (FreeBSD 15.1)
1.1 Image und VM
Von der FreeBSD-Downloadseite das VM-Image für amd64 laden (Spalte „VM“, Format .qcow2 für virt-manager/KVM). Die UFS- oder ZFS-Variante ist gleichermaßen geeignet.
Platte vor dem ersten Start vergrößern, da das Image nur ca. 6 GB groß ist:
xz -d FreeBSD-15.1-RELEASE-amd64-ufs.qcow2.xz qemu-img resize FreeBSD-15.1-RELEASE-amd64-ufs.qcow2 60G
In virt-manager:
- „Vorhandenes Laufwerksabbild importieren“ und das
.qcow2-Image auswählen - Betriebssystem: „Generic OS“ bzw. „Unknown OS“ wählen, da FreeBSD 15.1 in der Liste nicht vorhanden ist
- Speicher: 8192 MB, CPUs: 4–8
- Vor dem letzten Schritt „Konfiguration vor der Installation bearbeiten“ anhaken, dann:
- Festplatte → Erweiterte Optionen → Festplattenbus: VirtIO
- NIC → Gerätemodell: virtio
- „Übernehmen“, danach „Installation beginnen“
1.2 Erster Start
Login: root, kein Passwort. Die Konsole hat US-Tastaturbelegung (Y/Z vertauscht), umstellen mit kbdcontrol -l de und dauerhaft mit sysrc keymap=de.
1.3 SSH (für bequemes Kopieren von Befehlen)
Nur für Labor-VMs im NAT-Netz geeignet:
passwd
sysrc sshd_enable=YES
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
ssh-keygen -A # Hostschlüssel erzeugen, sonst startet sshd nicht
service sshd start
ifconfig vtnet0 | grep inet
Vom Host: ssh root@<IP der Build-VM>
Hängt die VM nach einem Neustart im Single-User-Modus (Prompt root@:~ # ohne Hostnamen, kein Netz), mit exit den normalen Start fortsetzen, ggf. vorher fsck -y /.
1.4 Werkzeuge
pkg install -y git tmux
Lange Builds in tmux ausführen (tmux new -s build, lösen mit Strg+b d, zurück mit tmux attach -t build).
2. pfSense-Test-VM aufsetzen (pfSense CE 2.9.0)
2.1 Installer
pfSense CE wird nur noch über den Netgate Installer verteilt: kostenlos im Netgate-Shop, Variante „AMD64 ISO IPMI/Virtual Machines“. Es ist ein Online-Installer, die VM braucht beim Installieren Internet.
Die Datei im Terminal entpacken, nicht mit dem Archivprogramm des Desktops (das versucht auch die ISO selbst auszupacken):
gunzip netgate-installer-v1.2-RELEASE-amd64.iso.gz
2.2 VM
- „Lokales Installationsmedium (ISO)“ und die entpackte ISO auswählen
- Betriebssystem: „Generic OS“ bzw. „Unknown OS“
- Speicher: 4096 MB, CPUs: 2, Platte: 20 GB, eine NIC im Netz
default - „Konfiguration vor der Installation bearbeiten“ anhaken und wie bei der Build-VM Festplattenbus VirtIO und NIC-Gerätemodell virtio einstellen
- Im Installer: pfSense CE, Version 2.9.0
- WAN =
vtnet0(DHCP), LAN überspringen
2.3 Zugriff im Labor
Konsolenmenü 8 (Shell). Für SSH und Webinterface über das WAN-Interface:
pfSsh.php playback enablesshd
pfctl -d # Firewall aus, bis zum nächsten Regel-Reload/Neustart
- SSH:
ssh admin@<WAN-IP>, danach Menüpunkt 8 - Web:
https://<WAN-IP>, Loginadmin/pfsense - Im Setup-Wizard unter „Configure WAN Interface“ „Block RFC1918 Private Networks“ abwählen
3. Kernel-Informationen der pfSense auslesen
Die eingebettete Kernelkonfiguration der pfSense auf die Build-VM holen (nur lesend, auch auf Produktivsystemen unbedenklich):
# auf der pfSense (Shell, Menüpunkt 8) sysctl -n kern.conftxt > /tmp/pfSense.conf # auf der Build-VM scp admin@<pfSense-IP>:/tmp/pfSense.conf /root/pfSense.conf
4. Passenden FreeBSD-Quellstand bestimmen
Alle Befehle in diesem Abschnitt auf der Build-VM (FreeBSD 15.1, als
root). Der Quellbaum liegt danach unter/usr/src-main.
Abkürzung für pfSense CE 2.9.0: Der passende Stand ist bekannt (
7386c0675c8, siehe Referenzwerte). Dann genügt 4.1 (Klonen), danach direkt weiter mit Abschnitt 5. Alle Blöcke, die mit# OPTIONALbeginnen, dienen nur dazu, den Stand für eine neue pfSense-Version herzuleiten oder zu kontrollieren.
4.1 FreeBSD main klonen
# Build-VM
git clone --filter=blob:none https://github.com/freebsd/freebsd-src.git /usr/src-main
cd /usr/src-main
--filter=blob:none lädt Dateiinhalte erst bei Bedarf und spart mehrere GB.
4.2 Zeitfenster der __FreeBSD_version finden (optional bei bekannter Version)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
git --no-pager log --since=2026-01-01 --format='%h %cd %s' --date=short \
-G'#define __FreeBSD_version 16000(18|19)' -- sys/sys/param.h
--since begrenzt die Suche auf neuere Commits. Ohne die Begrenzung durchsucht -G die gesamte Geschichte von param.h, und Git lädt wegen --filter=blob:none jede alte Version einzeln von GitHub nach; das kann mehrere Minuten dauern. Bei einer späteren pfSense-Version das Datum entsprechend anpassen (etwa ein halbes Jahr vor dem Build-Datum aus uname -a).
Das Ergebnis zeigt den Commit, der 1600018 setzt, und den, der auf 1600019 erhöht. Kontrolle:
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
git --no-pager show 255538cd906 -- sys/sys/param.h | grep FreeBSD_version
Der pfSense-Zweig wurde innerhalb dieses Fensters von main abgezweigt (das Build-Datum liegt später, die Basis ist älter).
Den Hash des letzten Commits mit 1600018 liefert:
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
git rev-parse --short 255538cd906^ # ergibt 7386c0675c8
^ bedeutet in Git „der Commit direkt davor“.
4.3 Prüfen, ob die Wahl des Commits im Fenster eine Rolle spielt (optional bei bekannter Version)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
# Änderungen am Modul selbst
git --no-pager log --oneline 1569e2334ab..255538cd906 -- \
sys/net/if_ovpn.c sys/net/if_ovpn.h sys/modules/if_ovpn
# Änderungen an Headern, auf die das Modul zugreift
git --no-pager log -p 1569e2334ab..255538cd906 -- \
sys/net/if_var.h sys/net/if_private.h sys/sys/mbuf.h \
sys/netinet/in_pcb.h sys/netinet/udp_var.h sys/opencrypto/cryptodev.h \
sys/net/vnet.h sys/sys/nv.h
Kritisch wären neue oder entfernte Felder in Strukturen (z. B. struct ifnet, struct mbuf), weil sich dann Speicher-Offsets verschieben. Bei 2.9.0 gab es im Fenster keine Änderung am Modul und nur zwei harmlose Header-Commits (ein Kommentar in struct ifnet, eine neue Inline-Funktion in mbuf.h).
Restrisiko: Netgates nicht veröffentlichte Kerneländerungen können nicht geprüft werden. Ob das Modul binär passt, zeigt erst der Lade- und Lasttest.
5. Modul bauen
Alle Befehle in diesem Abschnitt auf der Build-VM (FreeBSD 15.1, als
root), im Quellbaum/usr/src-main.
# Build-VM cd /usr/src-main git checkout 7386c0675c8 # = 255538cd906^, der Commit vor dem Sprung auf 1600019 ls sys/net/if_ovpn.c sys/modules/if_ovpn # OPTIONAL: Kontrolle, ob die Modulquellen da sind cp /root/pfSense.conf sys/amd64/conf/PFSENSE
Unbekannte Optionen/Geräte in der pfSense-Konfiguration finden (optional bei bekannter Version: für 2.9.0 ist das Ergebnis NETGRAPH_ETF):
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM
cd /usr/src-main/sys
for o in $(awk '$1=="options"{sub(/=.*/,"",$2); print $2}' amd64/conf/PFSENSE); do
grep -qwE "^$o" conf/options conf/options.amd64 || echo "option: $o"
done
for d in $(awk '$1=="device"{print $2}' amd64/conf/PFSENSE); do
grep -qwE "$d" conf/files conf/files.amd64 conf/files.x86 || echo "device: $d"
done
CONFIG_AUTOGENERATED ist ein Fehlalarm (wird von config(8) akzeptiert). Bei 2.9.0 ist nur NETGRAPH_ETF unbekannt; auskommentieren:
# Build-VM cd /usr/src-main sed -i '' 's/^options[[:space:]]*NETGRAPH_ETF/#&/' sys/amd64/conf/PFSENSE
Bauen (im tmux, damit der Build weiterläuft, falls die SSH-Verbindung abbricht):
# Build-VM: tmux-Sitzung starten (oder mit "tmux attach -t build" wieder aufnehmen) tmux new -s build
Lösen der Sitzung, ohne den Build abzubrechen: Strg+b, dann d. Wieder verbinden: tmux attach -t build. Alle Sitzungen anzeigen: tmux ls.
In der tmux-Sitzung:
# Build-VM, in /usr/src-main
cd /usr/src-main
make -j$(sysctl -n hw.ncpu) kernel-toolchain 2>&1 | tee /root/toolchain.log
make -j$(sysctl -n hw.ncpu) buildkernel KERNCONF=PFSENSE MODULES_OVERRIDE=if_ovpn \
2>&1 | tee /root/buildkernel.log
Nach dem Build die tmux-Sitzung beenden:
# Build-VM, in der tmux-Sitzung exit
exit schließt die Sitzung endgültig. Soll sie für spätere Builds erhalten bleiben, stattdessen mit Strg+b, dann d lösen.
kernel-toolchain baut den passenden Compiler aus dem Quellbaum (einmalig, 30–90 min). buildkernel baut den Kernel mit der pfSense-Konfiguration, damit das Modul mit identischen Optionen (RSS, RATELIMIT, VIMAGE …) kompiliert wird; mit vorhandener Toolchain dauert das wenige Minuten.
Ergebnis:
# Build-VM
find /usr/obj/usr/src-main -name if_ovpn.ko -exec ls -l {} \;
# …/sys/PFSENSE/modules/usr/src-main/sys/modules/if_ovpn/if_ovpn.ko (ca. 90 KB)
6. Modul auf der pfSense laden
# Build-VM
scp /usr/obj/usr/src-main/amd64.amd64/sys/PFSENSE/modules/usr/src-main/sys/modules/if_ovpn/if_ovpn.ko \
admin@<pfSense-IP>:/tmp/
# pfSense cp /tmp/if_ovpn.ko /boot/modules/ kldload -v /boot/modules/if_ovpn.ko kldstat | grep ovpn dmesg | tail -20
Nicht in /boot/loader.conf(.local) eintragen. Nach einem Neustart oder Update startet das System so garantiert ohne das Modul. Nach einem Neustart bei Bedarf erneut kldload if_ovpn.
7. OpenVPN-Server DCO-tauglich konfigurieren
Die CE-Oberfläche bietet keine DCO-Option. OpenVPN 2.7 nutzt DCO automatisch, sobald das Modul geladen ist und die Konfiguration verträglich ist; andernfalls schaltet es DCO still ab (disable_dco = ENABLED im Log, bei Verbosity ≥ 4). Mit dem Patch aus Abschnitt 8 trifft pfSense diese Entscheidung selbst nach denselben Regeln und schreibt sie eindeutig in die Konfiguration.
Voraussetzungen für DCO:
| Einstellung | Wert |
|---|---|
| Protocol | UDP |
| Data Encryption Algorithms | nur AES-GCM und/oder CHACHA20-POLY1305 |
| Fallback Data Encryption Algorithm | AES-256-GCM (Standard ist AES-256-CBC, das verhindert DCO) |
| Modus | TLS-basiert (SSL/TLS, ggf. mit User Auth), kein Shared Key |
| Tunnelnetz | z. B. /24 mit Topology Subnet, kein /30
|
| Compression | aus (Refuse any non-stub compression) |
| Client Specific Overrides | keine iroute-Einträge (unter FreeBSD mit DCO nicht unterstützt)
|
Der Fallback-Algorithmus wird automatisch in data-ciphers aufgenommen. Typische Logmeldung, wenn er falsch steht:
Note: --data-ciphers-fallback with cipher 'AES-256-CBC' disables data channel offload.
Kontrolle der erzeugten Konfiguration (wird aus der GUI generiert, nicht von Hand bearbeiten):
grep -iE 'cipher|topology|dev' /var/etc/openvpn/server1/config.ovpn grep -iE 'disable_dco|offload|note|dco|error' /var/log/openvpn.log | tail -20
Optional, für einen hybriden Post-Quanten-Handshake im Kontrollkanal (unabhängig von DCO), unter Custom options:
tls-version-min 1.3;tls-groups SecP384r1MLKEM1024:secp521r1
8. Interface-Problem und pfSense-Patch
8.1 Das Problem
pfSense CE legt vor dem Start von OpenVPN ein klassisches tun-Interface an und benennt es in ovpnsN um (dev-node /dev/tunN in der Konfiguration). DCO benötigt aber ein if_ovpn-Interface. Ist das Modul geladen und die Konfiguration DCO-tauglich, übernimmt OpenVPN das vorhandene tun-Interface und scheitert:
dco_set_ifmode: failed to set ifmode=00008002: Invalid argument (errno=22) DCO device ovpns1 already exists, won't be destroyed at shutdown
ifconfig ovpns1 zeigt dann weiterhin groups: tun openvpn. Dieser Zustand ist defekt. OpenVPN läuft, kann aber keine Daten übertragen.
Unterscheidung der Modi am Interface:
| Modus | ifconfig ovpnsN
|
|---|---|
klassisch (tun)
|
groups: tun openvpn, Opened by PID …
|
DCO (if_ovpn)
|
groups: openvpn
|
8.2 Die Lösung: Patch für /etc/inc/openvpn.inc
Die Datei pfsense-openvpn-dco.patch (vollständig im Anhang) erweitert die Funktion, in der pfSense die OpenVPN-Konfiguration schreibt und das Interface anlegt:
openvpn_dco_usable()entscheidet pro Instanz, ob DCO genutzt wird. Bedingungen:if_ovpngeladen, Device-Modetun, UDP, kein Shared-Key-Modus, alle Data-Ciphers und der Fallback aus AES-128-GCM / AES-256-GCM / CHACHA20-POLY1305, keine Kompression, Tunnelnetz größer als /30, und keindisable-dco(bzw.fragment,comp-lzo,compress) in den Custom options.- Hat das vorhandene
ovpnsNden falschen Typ, wird die Instanz beendet (mit Warten auf das Prozessende) und das Interface entfernt (mit Warten, bis es wirklich weg ist).tap-Interfaces werden nie angefasst. - Mit DCO wird
ifconfig ovpn createstatttunverwendet unddev-nodeweggelassen. Schlägt das Anlegen fehl, fällt der Code mit einer Warnung auftunzurück. - Ohne DCO wird explizit
disable-dcoin die Konfiguration geschrieben, damit OpenVPN nie eigenmächtig umschaltet.
Verhalten:
| Situation | Interface | Konfiguration |
|---|---|---|
| Modul nicht geladen (z. B. nach Neustart) | tun
|
dev-node + disable-dco
|
| Modul geladen, Instanz DCO-tauglich | ovpn (DCO)
|
ohne dev-node, ohne disable-dco
|
Modul geladen, aber disable-dco in Custom options
|
tun
|
dev-node + disable-dco
|
| Modul geladen, Instanz nicht DCO-tauglich (z. B. CBC-Fallback, TCP) | tun
|
dev-node + disable-dco
|
disable-dco in den Custom options dient damit als Notschalter pro Server, ganz ohne Änderung an der Oberfläche.
Im Systemlog erscheint bei DCO der Hinweis:
php-cgi[…]: OpenVPN ovpns1: using data channel offload (if_ovpn)
8.3 Einspielen über das Webinterface
Das Paket System Patches ist in pfSense CE 2.9.0 vorinstalliert und taucht deshalb nicht unter „Available Packages“ auf.
- System → Patches → „+ Add New Patch“
- Description:
OpenVPN DCO (if_ovpn) - Patch Contents: kompletter Inhalt von
pfsense-openvpn-dco.patch - Path Strip Count:
1, Base Directory:/ - Auto Apply: vorerst nicht anhaken
- Description:
- Save, dann in der Liste Debug (Probelauf), danach Apply.
Eine neuere Fassung des Patches einspielen: zuerst Revert, dann über das Bleistift-Symbol den Inhalt ersetzen, Save, Debug, Apply.
Alternativ in der Shell: cd / && patch -p1 -b < /tmp/pfsense-openvpn-dco.patch (zurück mit patch -R -p1 < …).
8.4 Testen
kldstat | grep ovpn || kldload if_ovpn pfSsh.php playback svc restart openvpn server 1 ifconfig ovpns1 | grep groups # openvpn grep -E 'dev-node|disable-dco' /var/etc/openvpn/server1/config.ovpn # keine Ausgabe grep 'if_ovpn' /var/log/system.log | tail -2
Laborergebnis mit pfSense CE 2.9.0 (Patch-Fassung v4):
| Test | Ergebnis |
|---|---|
| Neustart ohne Modul | tun openvpn, dev-node + disable-dco ✔
|
| Modul laden, Dienst neu starten | automatisch openvpn (DCO) ✔
|
disable-dco eintragen → entfernen → wieder eintragen (je Save)
|
tun → openvpn → tun ✔, mehrfach wiederholt
|
9. Funktionstest mit Client (Build-VM)
FreeBSD 15.1 bringt if_ovpn im Standardkernel mit; die Build-VM nutzt als Client damit ebenfalls DCO.
pkg install -y openvpn iperf3 kldload if_ovpn # dauerhaft: sysrc kld_list+=if_ovpn mkdir -p /usr/local/etc/openvpn cd /usr/local/etc/openvpn scp admin@<pfSense-IP>:/var/etc/openvpn/server1/tls-auth ta.key scp 'admin@<pfSense-IP>:/var/etc/openvpn/server1/ca/*.0' ca.crt
Client-Zertifikat und -Schlüssel im Webinterface exportieren (System → Certificates → Certificates, Symbole „Export Certificate“ und „Export Key“) und als client.crt / client.key ablegen; chmod 600 client.key.
/usr/local/etc/openvpn/client.conf:
client dev tun proto udp4 remote <pfSense-IP> 1194 nobind ca /usr/local/etc/openvpn/ca.crt cert /usr/local/etc/openvpn/client.crt key /usr/local/etc/openvpn/client.key tls-auth /usr/local/etc/openvpn/ta.key 1 auth SHA256 remote-cert-tls server data-ciphers AES-256-GCM:CHACHA20-POLY1305 tls-version-min 1.3 tls-groups SecP384r1MLKEM1024:secp521r1 explicit-exit-notify 1 verb 4
Die Richtung bei tls-auth ist 1 (Server: 0); auth muss zum Server passen. Verwendet der Server tls-crypt, entsprechend tls-crypt … ohne Richtung.
openvpn --config /usr/local/etc/openvpn/client.conf # zweites tmux-Fenster: ping -c 5 10.8.0.1
Erwartete Logzeilen:
Control Channel: TLSv1.3, … key agreement: SecP384r1MLKEM1024 DCO device tun0 opened Initialization Sequence Completed Data Channel: cipher 'AES-256-GCM', peer-id: 0
Laborergebnis (pfSense CE 2.9.0): Tunnel aufgebaut, DCO auf Server und Client aktiv, hybrider PQ-Handshake, Ping ohne Verluste (ca. 1,5 ms). Kein Absturz.
Wichtige Hinweise für den Betrieb
- Nach jedem pfSense-Update muss das Modul neu gebaut werden. Ein unpassendes Modul kann den Kernel zum Absturz bringen.
- Modul und Patch gehören zusammen. Ohne Patch schaltet ein DCO-tauglicher Server (z. B. mit GCM-Fallback) beim Laden des Moduls in den defekten Zustand aus Abschnitt 8.1.
- Nach einem Update zuerst prüfen, ob der Patch noch angewendet werden kann (System → Patches → Debug). Ändert Netgate die betroffenen Codestellen, muss der Patch angepasst werden.
- Einzelne Server lassen sich jederzeit mit
disable-dcoin den Custom options vom DCO-Betrieb ausnehmen.
Anhang: pfsense-openvpn-dco.patch
--- a/etc/inc/openvpn.inc
+++ b/etc/inc/openvpn.inc
@@ -209,6 +209,134 @@
return($list);
}
+/*
+ * DCO patch (local, not from Netgate): helpers for OpenVPN Data Channel
+ * Offload with a self-built if_ovpn.ko on pfSense CE.
+ */
+
+/* Is the if_ovpn kernel module loaded? */
+function openvpn_dco_module_loaded() : bool {
+ $out = [];
+ $rc = 1;
+ exec('/sbin/kldstat -q -n if_ovpn', $out, $rc);
+ return ($rc === 0);
+}
+
+/*
+ * Can this instance use DCO? Mirrors the restrictions of OpenVPN and the
+ * FreeBSD if_ovpn driver. If anything is unclear, answer "no" so the
+ * instance keeps working in classic tun mode.
+ */
+function openvpn_dco_usable(string $mode, array $settings) : bool {
+ if (!openvpn_dco_module_loaded()) {
+ return false;
+ }
+ /* DCO only supports layer 3 (tun) */
+ if (array_get_path($settings, 'dev_mode', 'tun') !== 'tun') {
+ return false;
+ }
+ /* DCO only supports UDP */
+ if (strncasecmp(array_get_path($settings, 'protocol', ''), 'UDP', 3) !== 0) {
+ return false;
+ }
+ /* DCO requires a TLS-based mode, no static shared key */
+ if (array_get_path($settings, 'mode', '') === 'p2p_shared_key') {
+ return false;
+ }
+ /* All data ciphers and the fallback cipher must be supported by DCO */
+ $dco_ciphers = ['AES-128-GCM', 'AES-256-GCM', 'CHACHA20-POLY1305'];
+ $ciphers = preg_split('/[,:]/', array_get_path($settings, 'data_ciphers', ''), -1, PREG_SPLIT_NO_EMPTY);
+ $ciphers[] = array_get_path($settings, 'data_ciphers_fallback', '');
+ foreach ($ciphers as $cipher) {
+ if (!in_array(strtoupper(trim($cipher)), $dco_ciphers, true)) {
+ return false;
+ }
+ }
+ /* No compression */
+ if (array_get_path($settings, 'allow_compression', 'no') !== 'no') {
+ return false;
+ }
+ /* /30 or smaller peer-to-peer tunnel networks are not DCO compatible */
+ $tunnel_network = array_get_path($settings, 'tunnel_network', '');
+ if (!empty($tunnel_network)) {
+ list($ip, $cidr) = openvpn_gen_tunnel_network($tunnel_network);
+ if (is_numeric($cidr) && ((int)$cidr >= 30)) {
+ return false;
+ }
+ }
+ /* Per-instance opt-out and options that disable DCO anyway */
+ if (preg_match('/(^|[;\s])(disable-dco|fragment|comp-lzo|compress)\b/',
+ array_get_path($settings, 'custom_options', ''))) {
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Does the interface exist right now? Asks the kernel directly instead of
+ * does_interface_exist(), which may answer from a cached interface list
+ * and then still reports an interface that was just destroyed.
+ */
+function openvpn_dco_if_exists(string $ifname) : bool {
+ $out = [];
+ $rc = 1;
+ exec('/sbin/ifconfig ' . escapeshellarg($ifname) . ' >/dev/null 2>&1', $out, $rc);
+ return ($rc === 0);
+}
+
+/*
+ * Stop the OpenVPN instance and remove its interface, waiting until both
+ * are really gone. Without waiting, the exiting process can still hold or
+ * remove the interface while the new one is being created.
+ */
+function openvpn_dco_stop_and_destroy(string $ifname, string $pidfile) : void {
+ $pf = escapeshellarg($pidfile);
+ if (file_exists($pidfile)) {
+ mwexec("/bin/pkill -TERM -F {$pf}");
+ /* wait up to 10 s for the process to exit */
+ for ($i = 0; $i < 100; $i++) {
+ $out = [];
+ $rc = 1;
+ exec("/bin/pgrep -F {$pf}", $out, $rc);
+ if ($rc !== 0) {
+ break;
+ }
+ usleep(100000);
+ }
+ if ($rc === 0) {
+ mwexec("/bin/pkill -KILL -F {$pf}");
+ usleep(500000);
+ }
+ }
+ /* remove the interface and wait up to 5 s until it is gone */
+ for ($i = 0; $i < 50 && openvpn_dco_if_exists($ifname); $i++) {
+ if (($i % 10) == 0) {
+ exec("/sbin/ifconfig " . escapeshellarg($ifname) . " destroy >/dev/null 2>&1");
+ }
+ usleep(100000);
+ }
+ if (openvpn_dco_if_exists($ifname)) {
+ syslog(LOG_WARNING, "OpenVPN {$ifname}: could not remove interface to switch DCO mode");
+ }
+ /* refresh pfSense's cached interface list */
+ does_interface_exist($ifname, true);
+}
+
+/* Kind of an existing interface: 'tun', 'tap', 'ovpn' or null if missing */
+function openvpn_if_kind(string $ifname) : ?string {
+ if (!openvpn_dco_if_exists($ifname)) {
+ return null;
+ }
+ foreach (['tun', 'tap'] as $group) {
+ $out = [];
+ exec('/sbin/ifconfig -g ' . escapeshellarg($group) . ' 2>/dev/null', $out);
+ if (in_array($ifname, array_map('trim', $out), true)) {
+ return $group;
+ }
+ }
+ return 'ovpn';
+}
+
function openvpn_name(string $mode, array $settings, string $type = 'if') : string {
switch ($type) {
case 'if':
@@ -836,16 +964,47 @@
$tunname = openvpn_name($mode, $settings, 'dev');
$devname = openvpn_name($mode, $settings);
- /* is our device already configured */
- if (!does_interface_exist($devname)) {
+ /* DCO patch: decide between if_ovpn (DCO) and classic tun */
+ $use_dco = openvpn_dco_usable($mode, $settings);
+
+ /*
+ * DCO patch: if the existing interface has the wrong kind (tun while
+ * DCO is wanted, or if_ovpn while it is not), stop the instance and
+ * remove the interface so it is recreated below. tap is never touched.
+ */
+ $cur_kind = openvpn_if_kind($devname);
+ if (($use_dco && ($cur_kind === 'tun')) ||
+ (!$use_dco && ($cur_kind === 'ovpn'))) {
+ openvpn_dco_stop_and_destroy($devname,
+ g_get('varrun_path') . "/openvpn_{$mode_id}.pid");
+ }
+
+ /* is our device already configured (DCO patch: uncached check) */
+ if (!openvpn_dco_if_exists($devname)) {
+
+ $srcif = $tunname;
+ if ($use_dco) {
+ /* DCO patch: create an if_ovpn interface instead of tun */
+ $out = [];
+ $rc = 1;
+ exec("/sbin/ifconfig ovpn create", $out, $rc);
+ $srcif = trim($out[0] ?? '');
+ if (($rc !== 0) || empty($srcif)) {
+ syslog(LOG_WARNING, "OpenVPN {$devname}: could not create if_ovpn interface, falling back to tun (no DCO)");
+ $use_dco = false;
+ $srcif = $tunname;
+ }
+ }
/* create the tap device if required */
- if (!file_exists("/dev/{$tunname}")) {
+ if (!$use_dco && !file_exists("/dev/{$tunname}")) {
exec("/sbin/ifconfig " . escapeshellarg($tunname) . " create");
}
/* rename the device */
- mwexec("/sbin/ifconfig " . escapeshellarg($tunname) . " name " . escapeshellarg($devname));
+ mwexec("/sbin/ifconfig " . escapeshellarg($srcif) . " name " . escapeshellarg($devname));
+ /* DCO patch: refresh pfSense's cached interface list */
+ does_interface_exist($devname, true);
/* add the device to the openvpn group */
mwexec("/sbin/ifconfig " . escapeshellarg($devname) . " group openvpn");
@@ -898,7 +1057,16 @@
}
$conf .= "dev-type {$settings['dev_mode']}\n";
- $conf .= "dev-node /dev/{$tunname}\n";
+ if ($use_dco) {
+ /* DCO patch: OpenVPN takes over the existing if_ovpn interface */
+ syslog(LOG_NOTICE, "OpenVPN {$devname}: using data channel offload (if_ovpn)");
+ } else {
+ $conf .= "dev-node /dev/{$tunname}\n";
+ /* DCO patch: never switch to DCO on its own on a tun interface */
+ if (!preg_match('/(^|[;\s])disable-dco\b/', array_get_path($settings, 'custom_options', ''))) {
+ $conf .= "disable-dco\n";
+ }
+ }
$conf .= "writepid {$pfile}\n";
$conf .= "#user nobody\n";
$conf .= "#group nobody\n";