OpenVPN-DCO@pfSense 2.9.0 CE: Difference between revisions

From
Jump to navigation Jump to search
Content deleted Content added
Wolfm (talk | contribs)
Wolfm (talk | contribs)
 
(13 intermediate revisions by the same user not shown)
Line 1: Line 1:
<sub><small>Der '''relevante Inhalt der Wiki-Page''' wurde von [[User:Ole.vautz|Ole]] erstellt und lediglich aus redaktionellen Gründen von [[User:Wolfm|Wolfm]] hinsichtlich der Formatierung überarbeitet und von [[W2026-ITS]] in eine separate Seite verschoben.</small></sub>
== Überblick ==


== Überblick ==
Es werden zwei VMs verwendet, beide im libvirt-NAT-Netz <code>default</code> (192.168.122.0/24):
Das obige [[W2026-ITS#OpenVPN-DCO@pfSense_2.9.0_CE|Thema]] wurde im Rahmen des im IT Security Workshops 2026 bearbeitet und mit [[Media:OpenVPN_DCO_pfSense.pdf | Folien (PDF)]].
Dazu werden zwei VMs verwendet, beide im libvirt-NAT-Netz <code>default</code> (192.168.122.0/24):


{| class="wikitable"
{| class="wikitable"
Line 17: Line 19:
| Liefert Kernel-Informationen; dort wird das Modul getestet
| 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 ===
=== Referenzwerte für pfSense CE 2.9.0 ===
Line 56: Line 46:
|}
|}


Bei einem pfSense-Update ändern sich diese Werte, die Schritte 3 bis 6 sind dann zu wiederholen.
Bei einem pfSense-Update ändern sich diese Werte, die Schritte [[#Kernel-Informationen_der_pfSense_auslesen|2.3]] bis [[#Modul_auf_der_pfSense_laden|2.6]] sind dann zu wiederholen.



-----
-----


== Ablauf: ==
=== 1. Build-VM aufsetzen (FreeBSD 15.1) ===
__TOC__


==== 1.1 Image und VM ====
=== Build-VM aufsetzen (FreeBSD 15.1) ===

==== Image und VM ====


Von der FreeBSD-Downloadseite das '''VM-Image''' für amd64 laden (Spalte „VM“, Format <code>.qcow2</code> für virt-manager/KVM). Die UFS- oder ZFS-Variante ist gleichermaßen geeignet.
Von der FreeBSD-Downloadseite das '''VM-Image''' für amd64 laden (Spalte „VM“, Format <code>.qcow2</code> für virt-manager/KVM). Die UFS- oder ZFS-Variante ist gleichermaßen geeignet.
Line 69: Line 61:
Platte '''vor dem ersten Start''' vergrößern, da das Image nur ca. 6 GB groß ist:
Platte '''vor dem ersten Start''' vergrößern, da das Image nur ca. 6 GB groß ist:


<syntaxhighlight lang="bash">
<pre>xz -d FreeBSD-15.1-RELEASE-amd64-ufs.qcow2.xz
qemu-img resize FreeBSD-15.1-RELEASE-amd64-ufs.qcow2 60G</pre>
xz -d FreeBSD-15.1-RELEASE-amd64-ufs.qcow2.xz
qemu-img resize FreeBSD-15.1-RELEASE-amd64-ufs.qcow2 60G
</syntaxhighlight>
In virt-manager:
In virt-manager:


Line 81: Line 75:
** „Übernehmen“, danach „Installation beginnen“
** „Übernehmen“, danach „Installation beginnen“


==== 1.2 Erster Start ====
==== Erster Start ====


Login: <code>root</code>, kein Passwort. Die Konsole hat US-Tastaturbelegung (Y/Z vertauscht), umstellen mit <code>kbdcontrol -l de</code> und dauerhaft mit <code>sysrc keymap=de</code>.
Login: <code>root</code>, kein Passwort. Die Konsole hat US-Tastaturbelegung (Y/Z vertauscht), umstellen mit <code>kbdcontrol -l de</code> und dauerhaft mit <code>sysrc keymap=de</code>.


==== 1.3 SSH (für bequemes Kopieren von Befehlen) ====
==== SSH (für bequemes Kopieren von Befehlen) ====


Nur für Labor-VMs im NAT-Netz geeignet:
Nur für Labor-VMs im NAT-Netz geeignet:
Line 100: Line 94:
Hängt die VM nach einem Neustart im Single-User-Modus (Prompt <code>root@:~ #</code> ohne Hostnamen, kein Netz), mit <code>exit</code> den normalen Start fortsetzen, ggf. vorher <code>fsck -y /</code>.
Hängt die VM nach einem Neustart im Single-User-Modus (Prompt <code>root@:~ #</code> ohne Hostnamen, kein Netz), mit <code>exit</code> den normalen Start fortsetzen, ggf. vorher <code>fsck -y /</code>.


==== 1.4 Werkzeuge ====
==== Werkzeuge ====


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
pkg install -y git tmux
pkg install -y git tmux
</syntaxhighlight>


Lange Builds in <code>tmux</code> ausführen (<code>tmux new -s build</code>, lösen mit <code>Strg+b d</code>, zurück mit <code>tmux attach -t build</code>).
Lange Builds in <code>tmux</code> ausführen (<code>tmux new -s build</code>, lösen mit <code>Strg+b d</code>, zurück mit <code>tmux attach -t build</code>).
</syntaxhighlight>


-----
-----


=== 2. pfSense-Test-VM aufsetzen (pfSense CE 2.9.0) ===
=== pfSense-Test-VM aufsetzen (pfSense CE 2.9.0) ===


==== 2.1 Installer ====
==== 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.
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.
Line 118: Line 112:
Die Datei im Terminal entpacken, nicht mit dem Archivprogramm des Desktops (das versucht auch die ISO selbst auszupacken):
Die Datei im Terminal entpacken, nicht mit dem Archivprogramm des Desktops (das versucht auch die ISO selbst auszupacken):


<syntaxhighlight lang="bash">
<pre>gunzip netgate-installer-v1.2-RELEASE-amd64.iso.gz</pre>
gunzip netgate-installer-v1.2-RELEASE-amd64.iso.gz
==== 2.2 VM ====
</syntaxhighlight>
==== VM ====


* „Lokales Installationsmedium (ISO)“ und die entpackte ISO auswählen
* „Lokales Installationsmedium (ISO)“ und die entpackte ISO auswählen
Line 128: Line 124:
* WAN = <code>vtnet0</code> (DHCP), LAN überspringen
* WAN = <code>vtnet0</code> (DHCP), LAN überspringen


==== 2.3 Zugriff im Labor ====
==== Zugriff im Labor ====


Konsolenmenü '''8 (Shell)'''. Für SSH und Webinterface über das WAN-Interface:
Konsolenmenü '''8 (Shell)'''. Für SSH und Webinterface über das WAN-Interface:


<syntaxhighlight lang="bash">
<pre>pfSsh.php playback enablesshd
pfSsh.php playback enablesshd
pfctl -d # Firewall aus, bis zum nächsten Regel-Reload/Neustart</pre>
pfctl -d # Firewall aus, bis zum nächsten Regel-Reload/Neustart
</syntaxhighlight>
* SSH: <code>ssh admin@&lt;WAN-IP&gt;</code>, danach Menüpunkt 8
* SSH: <code>ssh admin@&lt;WAN-IP&gt;</code>, danach Menüpunkt 8
* Web: <code>https://&lt;WAN-IP&gt;</code>, Login <code>admin</code> / <code>pfsense</code>
* Web: <code>https://&lt;WAN-IP&gt;</code>, Login <code>admin</code> / <code>pfsense</code>
Line 141: Line 139:
-----
-----


=== 3. Kernel-Informationen der pfSense auslesen ===
=== Kernel-Informationen der pfSense auslesen ===


Die eingebettete Kernelkonfiguration der pfSense auf die Build-VM holen (nur lesend, auch auf Produktivsystemen unbedenklich):
Die eingebettete Kernelkonfiguration der pfSense auf die Build-VM holen (nur lesend, auch auf Produktivsystemen unbedenklich):


<syntaxhighlight lang="bash">
<pre># auf der pfSense (Shell, Menüpunkt 8)
# auf der pfSense (Shell, Menüpunkt 8)
sysctl -n kern.conftxt > /tmp/pfSense.conf
sysctl -n kern.conftxt > /tmp/pfSense.conf


# auf der Build-VM
# auf der Build-VM
scp admin@<pfSense-IP>:/tmp/pfSense.conf /root/pfSense.conf</pre>
scp admin@<pfSense-IP>:/tmp/pfSense.conf /root/pfSense.conf
</syntaxhighlight>


-----
-----


=== 4. Passenden FreeBSD-Quellstand bestimmen ===
=== Passenden FreeBSD-Quellstand bestimmen ===


<blockquote>'''Alle Befehle in diesem Abschnitt auf der Build-VM''' (FreeBSD 15.1, als <code>root</code>). Der Quellbaum liegt danach unter <code>/usr/src-main</code>.
<blockquote>'''Alle Befehle in diesem Abschnitt auf der Build-VM''' (FreeBSD 15.1, als <code>root</code>). Der Quellbaum liegt danach unter <code>/usr/src-main</code>.
Line 159: Line 159:
<blockquote>'''Abkürzung für pfSense CE 2.9.0:''' Der passende Stand ist bekannt (<code>7386c0675c8</code>, siehe Referenzwerte). Dann genügt '''4.1 (Klonen)''', danach direkt weiter mit Abschnitt 5. Alle Blöcke, die mit <code># OPTIONAL</code> beginnen, dienen nur dazu, den Stand für eine neue pfSense-Version herzuleiten oder zu kontrollieren.
<blockquote>'''Abkürzung für pfSense CE 2.9.0:''' Der passende Stand ist bekannt (<code>7386c0675c8</code>, siehe Referenzwerte). Dann genügt '''4.1 (Klonen)''', danach direkt weiter mit Abschnitt 5. Alle Blöcke, die mit <code># OPTIONAL</code> beginnen, dienen nur dazu, den Stand für eine neue pfSense-Version herzuleiten oder zu kontrollieren.
</blockquote>
</blockquote>
==== 4.1 FreeBSD <code>main</code> klonen ====
==== FreeBSD <code>main</code> klonen ====
<syntaxhighlight lang="bash">

<pre># Build-VM
# Build-VM
git clone --filter=blob:none https://github.com/freebsd/freebsd-src.git /usr/src-main
git clone --filter=blob:none https://github.com/freebsd/freebsd-src.git /usr/src-main
cd /usr/src-main</pre>
cd /usr/src-main
</syntaxhighlight>
<code>--filter=blob:none</code> lädt Dateiinhalte erst bei Bedarf und spart mehrere GB.
<code>--filter=blob:none</code> lädt Dateiinhalte erst bei Bedarf und spart mehrere GB.


==== 4.2 Zeitfenster der <code>__FreeBSD_version</code> finden ''(optional bei bekannter Version)'' ====
==== Zeitfenster der <code>__FreeBSD_version</code> finden ''(optional bei bekannter Version)'' ====


<syntaxhighlight lang="bash">
<pre># OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
# Build-VM, in /usr/src-main
git --no-pager log --since=2026-01-01 --format='%h %cd %s' --date=short \
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</pre>
-G'#define __FreeBSD_version 16000(18|19)' -- sys/sys/param.h
</syntaxhighlight>
<code>--since</code> begrenzt die Suche auf neuere Commits. Ohne die Begrenzung durchsucht <code>-G</code> die gesamte Geschichte von <code>param.h</code>, und Git lädt wegen <code>--filter=blob:none</code> 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 <code>uname -a</code>).
<code>--since</code> begrenzt die Suche auf neuere Commits. Ohne die Begrenzung durchsucht <code>-G</code> die gesamte Geschichte von <code>param.h</code>, und Git lädt wegen <code>--filter=blob:none</code> 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 <code>uname -a</code>).


Das Ergebnis zeigt den Commit, der 1600018 setzt, und den, der auf 1600019 erhöht. Kontrolle:
Das Ergebnis zeigt den Commit, der 1600018 setzt, und den, der auf 1600019 erhöht. Kontrolle:


<syntaxhighlight lang="bash">
<pre># OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
# Build-VM, in /usr/src-main
git --no-pager show 255538cd906 -- sys/sys/param.h | grep FreeBSD_version</pre>
git --no-pager show 255538cd906 -- sys/sys/param.h | grep FreeBSD_version
</syntaxhighlight>
Der pfSense-Zweig wurde innerhalb dieses Fensters von <code>main</code> abgezweigt (das Build-Datum liegt später, die Basis ist älter).
Der pfSense-Zweig wurde innerhalb dieses Fensters von <code>main</code> abgezweigt (das Build-Datum liegt später, die Basis ist älter).


Den Hash des letzten Commits mit 1600018 liefert:
Den Hash des letzten Commits mit 1600018 liefert:


<syntaxhighlight lang="bash">
<pre># OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
# Build-VM, in /usr/src-main
git rev-parse --short 255538cd906^ # ergibt 7386c0675c8</pre>
git rev-parse --short 255538cd906^ # ergibt 7386c0675c8
</syntaxhighlight>
<code>^</code> bedeutet in Git „der Commit direkt davor“.
<code>^</code> 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)'' ====
==== Prüfen, ob die Wahl des Commits im Fenster eine Rolle spielt ''(optional bei bekannter Version)'' ====


<syntaxhighlight lang="bash">
<pre># OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM, in /usr/src-main
# Build-VM, in /usr/src-main


Line 201: Line 209:
sys/net/if_var.h sys/net/if_private.h sys/sys/mbuf.h \
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/netinet/in_pcb.h sys/netinet/udp_var.h sys/opencrypto/cryptodev.h \
sys/net/vnet.h sys/sys/nv.h</pre>
sys/net/vnet.h sys/sys/nv.h
</syntaxhighlight>
Kritisch wären neue oder entfernte Felder in Strukturen (z. B. <code>struct ifnet</code>, <code>struct mbuf</code>), 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 <code>struct ifnet</code>, eine neue Inline-Funktion in <code>mbuf.h</code>).
Kritisch wären neue oder entfernte Felder in Strukturen (z. B. <code>struct ifnet</code>, <code>struct mbuf</code>), 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 <code>struct ifnet</code>, eine neue Inline-Funktion in <code>mbuf.h</code>).


Line 209: Line 218:
-----
-----


=== 5. Modul bauen ===
=== Modul bauen ===


<blockquote>'''Alle Befehle in diesem Abschnitt auf der Build-VM''' (FreeBSD 15.1, als <code>root</code>), im Quellbaum <code>/usr/src-main</code>.
<blockquote>'''Alle Befehle in diesem Abschnitt auf der Build-VM''' (FreeBSD 15.1, als <code>root</code>), im Quellbaum <code>/usr/src-main</code>.
</blockquote>
</blockquote>
<syntaxhighlight lang="bash">
<pre># Build-VM
# Build-VM
cd /usr/src-main
cd /usr/src-main
git checkout 7386c0675c8 # = 255538cd906^, der Commit vor dem Sprung auf 1600019
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
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</pre>
cp /root/pfSense.conf sys/amd64/conf/PFSENSE
</syntaxhighlight>
Unbekannte Optionen/Geräte in der pfSense-Konfiguration finden ''(optional bei bekannter Version: für 2.9.0 ist das Ergebnis <code>NETGRAPH_ETF</code>)'':
Unbekannte Optionen/Geräte in der pfSense-Konfiguration finden ''(optional bei bekannter Version: für 2.9.0 ist das Ergebnis <code>NETGRAPH_ETF</code>)'':


<syntaxhighlight lang="bash">
<pre># OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# OPTIONAL bei bekannter Version (nur zur Herleitung/Kontrolle)
# Build-VM
# Build-VM
cd /usr/src-main/sys
cd /usr/src-main/sys
Line 229: Line 241:
for d in $(awk '$1=="device"{print $2}' amd64/conf/PFSENSE); do
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"
grep -qwE "$d" conf/files conf/files.amd64 conf/files.x86 || echo "device: $d"
done</pre>
done
</syntaxhighlight>
<code>CONFIG_AUTOGENERATED</code> ist ein Fehlalarm (wird von <code>config(8)</code> akzeptiert). Bei 2.9.0 ist nur <code>NETGRAPH_ETF</code> unbekannt; auskommentieren:
<code>CONFIG_AUTOGENERATED</code> ist ein Fehlalarm (wird von <code>config(8)</code> akzeptiert). Bei 2.9.0 ist nur <code>NETGRAPH_ETF</code> unbekannt; auskommentieren:


<syntaxhighlight lang="bash">
<pre># Build-VM
# Build-VM
cd /usr/src-main
cd /usr/src-main
sed -i '' 's/^options[[:space:]]*NETGRAPH_ETF/#&/' sys/amd64/conf/PFSENSE</pre>
sed -i '' 's/^options[[:space:]]*NETGRAPH_ETF/#&/' sys/amd64/conf/PFSENSE
</syntaxhighlight>
Bauen (im <code>tmux</code>, damit der Build weiterläuft, falls die SSH-Verbindung abbricht):
Bauen (im <code>tmux</code>, damit der Build weiterläuft, falls die SSH-Verbindung abbricht):


<syntaxhighlight lang="bash">
<pre># Build-VM: tmux-Sitzung starten (oder mit "tmux attach -t build" wieder aufnehmen)
# Build-VM: tmux-Sitzung starten (oder mit "tmux attach -t build" wieder aufnehmen)
tmux new -s build</pre>
tmux new -s build
</syntaxhighlight>
Lösen der Sitzung, ohne den Build abzubrechen: <code>Strg+b</code>, dann <code>d</code>. Wieder verbinden: <code>tmux attach -t build</code>. Alle Sitzungen anzeigen: <code>tmux ls</code>.
Lösen der Sitzung, ohne den Build abzubrechen: <code>Strg+b</code>, dann <code>d</code>. Wieder verbinden: <code>tmux attach -t build</code>. Alle Sitzungen anzeigen: <code>tmux ls</code>.


In der tmux-Sitzung:
In der tmux-Sitzung:


<syntaxhighlight lang="bash">
<pre># Build-VM, in /usr/src-main
# Build-VM, in /usr/src-main
cd /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) kernel-toolchain 2>&1 | tee /root/toolchain.log
make -j$(sysctl -n hw.ncpu) buildkernel KERNCONF=PFSENSE MODULES_OVERRIDE=if_ovpn \
make -j$(sysctl -n hw.ncpu) buildkernel KERNCONF=PFSENSE MODULES_OVERRIDE=if_ovpn \
2>&1 | tee /root/buildkernel.log</pre>
2>&1 | tee /root/buildkernel.log
</syntaxhighlight>
Nach dem Build die tmux-Sitzung beenden:
Nach dem Build die tmux-Sitzung beenden:


<syntaxhighlight lang="bash">
<pre># Build-VM, in der tmux-Sitzung
# Build-VM, in der tmux-Sitzung
exit</pre>
exit
</syntaxhighlight>
<code>exit</code> schließt die Sitzung endgültig. Soll sie für spätere Builds erhalten bleiben, stattdessen mit <code>Strg+b</code>, dann <code>d</code> lösen.
<code>exit</code> schließt die Sitzung endgültig. Soll sie für spätere Builds erhalten bleiben, stattdessen mit <code>Strg+b</code>, dann <code>d</code> lösen.


Line 258: Line 279:
Ergebnis:
Ergebnis:


<syntaxhighlight lang="bash">
<pre># Build-VM
# Build-VM
find /usr/obj/usr/src-main -name if_ovpn.ko -exec ls -l {} \;
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)</pre>
# …/sys/PFSENSE/modules/usr/src-main/sys/modules/if_ovpn/if_ovpn.ko (ca. 90 KB)
</syntaxhighlight>


-----
-----


=== 6. Modul auf der pfSense laden ===
=== Modul auf der pfSense laden ===


<syntaxhighlight lang="bash">
<pre># Build-VM
# Build-VM
scp /usr/obj/usr/src-main/amd64.amd64/sys/PFSENSE/modules/usr/src-main/sys/modules/if_ovpn/if_ovpn.ko \
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/</pre>
admin@<pfSense-IP>:/tmp/
</syntaxhighlight>
<pre># pfSense
<syntaxhighlight lang="bash">
# pfSense
cp /tmp/if_ovpn.ko /boot/modules/
cp /tmp/if_ovpn.ko /boot/modules/
kldload -v /boot/modules/if_ovpn.ko
kldload -v /boot/modules/if_ovpn.ko
kldstat | grep ovpn
kldstat | grep ovpn
dmesg | tail -20</pre>
dmesg | tail -20
</syntaxhighlight>
'''Nicht in <code>/boot/loader.conf(.local)</code> eintragen.''' Nach einem Neustart oder Update startet das System so garantiert ohne das Modul. Nach einem Neustart bei Bedarf erneut <code>kldload if_ovpn</code>.
'''Nicht in <code>/boot/loader.conf(.local)</code> eintragen.''' Nach einem Neustart oder Update startet das System so garantiert ohne das Modul. Nach einem Neustart bei Bedarf erneut <code>kldload if_ovpn</code>.


Line 279: Line 306:
-----
-----


=== 7. OpenVPN-Server DCO-tauglich konfigurieren ===
=== 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 (<code>disable_dco = ENABLED</code> 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.
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 (<code>disable_dco = ENABLED</code> 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.
Line 314: Line 341:
Der Fallback-Algorithmus wird automatisch in <code>data-ciphers</code> aufgenommen. Typische Logmeldung, wenn er falsch steht:
Der Fallback-Algorithmus wird automatisch in <code>data-ciphers</code> aufgenommen. Typische Logmeldung, wenn er falsch steht:


<syntaxhighlight lang="bash">
<pre>Note: --data-ciphers-fallback with cipher 'AES-256-CBC' disables data channel offload.</pre>
Note: --data-ciphers-fallback with cipher 'AES-256-CBC' disables data channel offload.
</syntaxhighlight>
Kontrolle der erzeugten Konfiguration (wird aus der GUI generiert, nicht von Hand bearbeiten):
Kontrolle der erzeugten Konfiguration (wird aus der GUI generiert, nicht von Hand bearbeiten):


<syntaxhighlight lang="bash">
<pre>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</pre>
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
</syntaxhighlight>
Optional, für einen hybriden Post-Quanten-Handshake im Kontrollkanal (unabhängig von DCO), unter ''Custom options'':
Optional, für einen hybriden Post-Quanten-Handshake im Kontrollkanal (unabhängig von DCO), unter ''Custom options'':


<syntaxhighlight lang="bash">
<pre>tls-version-min 1.3;tls-groups SecP384r1MLKEM1024:secp521r1</pre>
tls-version-min 1.3;tls-groups SecP384r1MLKEM1024:secp521r1
</syntaxhighlight>


-----
-----


=== 8. Interface-Problem und pfSense-Patch ===
=== Interface-Problem und pfSense-Patch ===


==== 8.1 Das Problem ====
==== Das Problem ====


pfSense CE legt vor dem Start von OpenVPN ein klassisches '''<code>tun</code>-Interface''' an und benennt es in <code>ovpnsN</code> um (<code>dev-node /dev/tunN</code> in der Konfiguration). DCO benötigt aber ein <code>if_ovpn</code>-Interface. Ist das Modul geladen und die Konfiguration DCO-tauglich, übernimmt OpenVPN das vorhandene <code>tun</code>-Interface und scheitert:
pfSense CE legt vor dem Start von OpenVPN ein klassisches '''<code>tun</code>-Interface''' an und benennt es in <code>ovpnsN</code> um (<code>dev-node /dev/tunN</code> in der Konfiguration). DCO benötigt aber ein <code>if_ovpn</code>-Interface. Ist das Modul geladen und die Konfiguration DCO-tauglich, übernimmt OpenVPN das vorhandene <code>tun</code>-Interface und scheitert:


<syntaxhighlight lang="bash">
<pre>dco_set_ifmode: failed to set ifmode=00008002: Invalid argument (errno=22)
dco_set_ifmode: failed to set ifmode=00008002: Invalid argument (errno=22)
DCO device ovpns1 already exists, won't be destroyed at shutdown</pre>
DCO device ovpns1 already exists, won't be destroyed at shutdown
</syntaxhighlight>
<code>ifconfig ovpns1</code> zeigt dann weiterhin <code>groups: tun openvpn</code>. '''Dieser Zustand ist defekt.''' OpenVPN läuft, kann aber keine Daten übertragen.
<code>ifconfig ovpns1</code> zeigt dann weiterhin <code>groups: tun openvpn</code>. '''Dieser Zustand ist defekt.''' OpenVPN läuft, kann aber keine Daten übertragen.


Line 349: Line 384:
|}
|}


==== 8.2 Die Lösung: Patch für <code>/etc/inc/openvpn.inc</code> ====
==== Die Lösung: Patch für <code>/etc/inc/openvpn.inc</code> ====


Die Datei <code>pfsense-openvpn-dco.patch</code> (vollständig im Anhang) erweitert die Funktion, in der pfSense die OpenVPN-Konfiguration schreibt und das Interface anlegt:
Die Datei <code>pfsense-openvpn-dco.patch</code> (vollständig im Anhang) erweitert die Funktion, in der pfSense die OpenVPN-Konfiguration schreibt und das Interface anlegt:
Line 387: Line 422:
Im Systemlog erscheint bei DCO der Hinweis:
Im Systemlog erscheint bei DCO der Hinweis:


<syntaxhighlight lang="bash">
<pre>php-cgi[…]: OpenVPN ovpns1: using data channel offload (if_ovpn)</pre>
php-cgi[…]: OpenVPN ovpns1: using data channel offload (if_ovpn)
==== 8.3 Einspielen über das Webinterface ====
</syntaxhighlight>
==== Einspielen über das Webinterface ====


Das Paket ''System Patches'' ist in pfSense CE 2.9.0 '''vorinstalliert''' und taucht deshalb nicht unter „Available Packages“ auf.
Das Paket ''System Patches'' ist in pfSense CE 2.9.0 '''vorinstalliert''' und taucht deshalb nicht unter „Available Packages“ auf.
Line 403: Line 440:
Alternativ in der Shell: <code>cd / &amp;&amp; patch -p1 -b &lt; /tmp/pfsense-openvpn-dco.patch</code> (zurück mit <code>patch -R -p1 &lt; …</code>).
Alternativ in der Shell: <code>cd / &amp;&amp; patch -p1 -b &lt; /tmp/pfsense-openvpn-dco.patch</code> (zurück mit <code>patch -R -p1 &lt; …</code>).


==== 8.4 Testen ====
==== Testen ====


<syntaxhighlight lang="bash">
<pre>kldstat | grep ovpn || kldload if_ovpn
kldstat | grep ovpn || kldload if_ovpn
pfSsh.php playback svc restart openvpn server 1
pfSsh.php playback svc restart openvpn server 1
ifconfig ovpns1 | grep groups # openvpn
ifconfig ovpns1 | grep groups # openvpn
grep -E 'dev-node|disable-dco' /var/etc/openvpn/server1/config.ovpn # keine Ausgabe
grep -E 'dev-node|disable-dco' /var/etc/openvpn/server1/config.ovpn # keine Ausgabe
grep 'if_ovpn' /var/log/system.log | tail -2</pre>
grep 'if_ovpn' /var/log/system.log | tail -2
</syntaxhighlight>
Laborergebnis mit pfSense CE 2.9.0 (Patch-Fassung v4):
Laborergebnis mit pfSense CE 2.9.0 (Patch-Fassung v4):


Line 430: Line 469:
-----
-----


=== 9. Funktionstest mit Client (Build-VM) ===
=== Funktionstest mit Client (Build-VM) ===


FreeBSD 15.1 bringt <code>if_ovpn</code> im Standardkernel mit; die Build-VM nutzt als Client damit ebenfalls DCO.
FreeBSD 15.1 bringt <code>if_ovpn</code> im Standardkernel mit; die Build-VM nutzt als Client damit ebenfalls DCO.


<syntaxhighlight lang="bash">
<pre>pkg install -y openvpn iperf3
pkg install -y openvpn iperf3
kldload if_ovpn # dauerhaft: sysrc kld_list+=if_ovpn
kldload if_ovpn # dauerhaft: sysrc kld_list+=if_ovpn
mkdir -p /usr/local/etc/openvpn
mkdir -p /usr/local/etc/openvpn
cd /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/tls-auth ta.key
scp 'admin@<pfSense-IP>:/var/etc/openvpn/server1/ca/*.0' ca.crt</pre>
scp 'admin@<pfSense-IP>:/var/etc/openvpn/server1/ca/*.0' ca.crt
</syntaxhighlight>
Client-Zertifikat und -Schlüssel im Webinterface exportieren (''System → Certificates → Certificates'', Symbole „Export Certificate“ und „Export Key“) und als <code>client.crt</code> / <code>client.key</code> ablegen; <code>chmod 600 client.key</code>.
Client-Zertifikat und -Schlüssel im Webinterface exportieren (''System → Certificates → Certificates'', Symbole „Export Certificate“ und „Export Key“) und als <code>client.crt</code> / <code>client.key</code> ablegen; <code>chmod 600 client.key</code>.


<code>/usr/local/etc/openvpn/client.conf</code>:
<code>/usr/local/etc/openvpn/client.conf</code>:


<syntaxhighlight lang=text>
<pre>client
client
dev tun
dev tun
proto udp4
proto udp4
remote &lt;pfSense-IP&gt; 1194
remote <pfSense-IP> 1194 udp
nobind
nobind
ca /usr/local/etc/openvpn/ca.crt
ca /usr/local/etc/openvpn/ca.crt
Line 459: Line 501:
tls-groups SecP384r1MLKEM1024:secp521r1
tls-groups SecP384r1MLKEM1024:secp521r1
explicit-exit-notify 1
explicit-exit-notify 1
verb 4</pre>
verb 4
</syntaxhighlight>
Die Richtung bei <code>tls-auth</code> ist <code>1</code> (Server: <code>0</code>); <code>auth</code> muss zum Server passen. Verwendet der Server <code>tls-crypt</code>, entsprechend <code>tls-crypt …</code> ohne Richtung.
Die Richtung bei <code>tls-auth</code> ist <code>1</code> (Server: <code>0</code>); <code>auth</code> muss zum Server passen. Verwendet der Server <code>tls-crypt</code>, entsprechend <code>tls-crypt …</code> ohne Richtung.


<syntaxhighlight lang="bash">
<pre>openvpn --config /usr/local/etc/openvpn/client.conf
openvpn --config /usr/local/etc/openvpn/client.conf
# zweites tmux-Fenster:
# zweites tmux-Fenster:
ping -c 5 10.8.0.1</pre>
ping -c 5 10.8.0.1
</syntaxhighlight>
Erwartete Logzeilen:
Erwartete Logzeilen:


<syntaxhighlight lang="bash">
<pre>Control Channel: TLSv1.3, … key agreement: SecP384r1MLKEM1024
Control Channel: TLSv1.3, … key agreement: SecP384r1MLKEM1024
DCO device tun0 opened
DCO device tun0 opened
Initialization Sequence Completed
Initialization Sequence Completed
Data Channel: cipher 'AES-256-GCM', peer-id: 0</pre>
Data Channel: cipher 'AES-256-GCM', peer-id: 0
</syntaxhighlight>
'''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.
'''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.



Latest revision as of 15:54, 28 October 2026

Der relevante Inhalt der Wiki-Page wurde von Ole erstellt und lediglich aus redaktionellen Gründen von Wolfm hinsichtlich der Formatierung überarbeitet und von W2026-ITS in eine separate Seite verschoben.

Überblick

Das obige Thema wurde im Rahmen des im IT Security Workshops 2026 bearbeitet und mit Folien (PDF). Dazu 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

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 2.3 bis 2.6 sind dann zu wiederholen.


Ablauf:

Build-VM aufsetzen (FreeBSD 15.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“

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.

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

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


pfSense-Test-VM aufsetzen (pfSense CE 2.9.0)

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

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

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>, Login admin / pfsense
  • Im Setup-Wizard unter „Configure WAN Interface“ „Block RFC1918 Private Networks“ abwählen



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

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 # OPTIONAL beginnen, dienen nur dazu, den Stand für eine neue pfSense-Version herzuleiten oder zu kontrollieren.

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.

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

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.


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)

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.



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

Interface-Problem und pfSense-Patch

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

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_ovpn geladen, Device-Mode tun, 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 kein disable-dco (bzw. fragment, comp-lzo, compress) in den Custom options.
  • Hat das vorhandene ovpnsN den 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 create statt tun verwendet und dev-node weggelassen. Schlägt das Anlegen fehl, fällt der Code mit einer Warnung auf tun zurück.
  • Ohne DCO wird explizit disable-dco in 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)

Einspielen über das Webinterface

Das Paket System Patches ist in pfSense CE 2.9.0 vorinstalliert und taucht deshalb nicht unter „Available Packages“ auf.

  1. 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
  2. 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 < …).

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



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 udp
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-dco in 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";