Wie bereits im letzten Teil beschrieben, funktioniert IPv6 mit pfSense soweit gut, jedoch nur bis zum Zeitpunkt der Verbindungstrennung. Denn von dieser bzw. der anschließenden neuen Prefix Delegation und somit neuen IPv6-Adressen bekommt das LAN-Interface schlicht und einfach nichts mit. Erst nachdem der DHCPv6-Timeout zugeschlagen hat, werden die Adressen des LAN-Interfaces und infolge dessen die IPv6-Adressen der angeschlossenen Clients aktualisiert.
Von der Dynamik der Prefix Delegation
Mit einigem Glück passiert dies innerhalb weniger Minuten, im Worst Case dauert es ungefähr so lange wie der Timeout eingestellt ist. Im Log (hier von OPNsense) sah ein besserer Fall wie folgt aus:
2023-02-18T05:57:22 Notice dhcp6c executes /var/etc/dhcp6c_wan_script.sh 2023-02-18T05:57:22 Notice dhcp6c create a prefix 2a0a:XXXX:YYYY:ZZZZ::/62 pltime=3600, vltime=7200 2023-02-18T05:57:22 Notice dhcp6c make an IA: PD-0 2023-02-18T05:57:22 Notice dhcp6c nameserver[0] fd00::1234:5678:9abc:def0 2023-02-18T05:57:22 Notice dhcp6c IA_PD prefix: 2a0a:AAAA:BBBB:ZZZZ::/62 pltime=3600 vltime=7200 2023-02-18T05:57:20 Notice dhcp6c remove a site prefix 2a0a:AAAA:BBBB:ZZZZ::/62 2023-02-18T05:57:20 Notice dhcp6c prefix timeout for 2a0a:AAAA:BBBB:ZZZZ::/62 2023-02-18T05:50:48 Notice opnsense /usr/local/etc/rc.newwanipv6: No IP change detected for WAN_FritzBox[wan]
Nach der Verbindungstrennung dauerte es in diesem Fall nur wenige Minuten, bis der neue Prefix verteilt wurde. Dennoch eine unbefriedigende Situation. Dass meine Konfiguration kein Einzelfall war, zeigen Forumsbeiträge oder Issues auf GitHub, hier am Beispiel von OPNsense. Ein Reboot des Routers nach der Verbindungstrennung erschien mir jedoch als Lösung mit der Brechstange, genauso wenig sinnvoll hielt ich es, den Timeout so gering zu setzen, dass die Gültigkeit der IPv6-Daten auf wenige Minuten beschränkt wäre. Und trotz zahlreichen Suchens und Probierens zeigte sich sowohl bei OPNsense, als auch bei pfSense genau dasselbe Problem.
Entwickeln statt Stochern im Konfigurationsnebel
Dabei erschien mir eine Lösung eigentlich recht simpel – es müsste nur eine Instanz geben, die periodisch überwacht, ob sich die „echte“, also die vom Prefix des Providers abgeleitete IPv6-Adresse geändert hat. Falls eine Änderung erfolgt ist, müsste das LAN-Interface diese Änderung erfahren, beispielsweise indem eine Neukonfiguration des LAN-Interfaces erfolgt, d.h. es müssten genau dieselben Schritte ausgeführt werden, die beim Speichern und Bestätigen der LAN-Interface-Einstellungen per Web-UI durchlaufen werden.
Die Prüfung der Änderung der IPv6-Adresse verbraucht letztlich so gut wie keine Ressourcen, schließlich handelt es sich nur um die Abfrage, ob die zuvor festgestellte Adresse mit der aktuellen übereinstimmt. Falls ja, wird die Routine sofort beendet. Falls nicht, wird das LAN-Interface neu konfiguriert und die damit verbundenen Dienste neu gestartet. In einem Forums-Beitrag fand sich die Idee, das LAN-Interface im Minutenabstand neu zu starten, dies halte ich jedoch eher für suboptimal.
Nun werden sowohl OPNsense als auch pfSense tatsächlich durch ziemlich viele PHP-Skripte zusammen gehalten, d.h. nicht nur das Web-Interface ist in PHP erstellt, sondern auch viele der Skripte zur Konfiguration des Systems bestehen aus PHP-Code. Insofern erschien es mir als sinnvoll, eine Lösung ebenfalls in PHP zu schreiben, damit ließ sich auch auf bestehenden Code zurückgreifen. Dass mir einmal wieder PHP-Dateien mit mehreren Tausend Zeilen Code begegnen würden, hätte ich zwar auch nicht gedacht, aber irgendwie kommt ja alles wieder zurück, und manchmal darf es auch ein wenig Retro sein… Zum Glück mussten einige der sich darin befindlichen Funktionen ja nur aufgerufen, jedoch nicht geändert werden.
PHP für pfSense, #1: IPv6 prüfen
Die Lösung besteht aus zwei PHP-Skripten, das erste ist davon nur für die Abfrage und den Vergleich der IPv6-Adressen zuständig. Der folgende Code dient dabei nur der Übersicht, die jeweils aktuellen Versionen sind im GitHub-Repository zu finden. Ganz unkreativ habe ich die Skripte „CCW_IPv6“ genannt, was für „Check Current WAN IPv6“ steht.
<?php /** * Check Current WAN IPv6 script (CCW_IPv6) * * Copyright (c) 2023 Ralf Geschke <ralf@kuerbis.org>. * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // config section // WAN interface system identifier, as found in "Interface assignments" section. For example: "igb0", "igb3", "em0", "re0" or similar. $wanInterface = "igb0"; // Full path of files used in this script. In most cases these options don't need to be changed. $currentIpFile = "/tmp/_current_wan_ipv6.txt"; $currentIpFileLock = "/tmp/_current_wan_ipv6.lock"; /////////////// No further configuration necessary below this line //////////////// // define some syslog messages const SYSLOG_PREFIX = "CCW_IPv6: "; const ERR_COULD_NOT_GET_IP = "Could not get IP from interface "; const ERR_COULD_NOT_FIND_CURRENT_IP = "Could not find current IP of WAN interface"; const ERR_COULD_NOT_RECOGNIZE_CURRENT_IP = "Current IP found, but failed to recognize"; const ERR_COULD_NOT_WRITE_IP_FILE = "Could not write current IP into file"; const ERR_PROCESS_RUNNING = "Could not get lock, maybe another process is still running?"; const ERR_LOCK_TIMEOUT = "Lock timeout reached, clear previous locks."; const INFO_STILL_VALID = "Current WAN IP is identical to IP found in file, so still valid: "; const INFO_NEW_IP_APPLIED = "Current IP settings applied successfully: "; /** * getIp reads all IPv6 information of submitted interface */ function getIp($interface) { $interface = escapeshellarg($interface); $pattern = "/.*inet6(.*)/"; $text = shell_exec("ifconfig $interface"); preg_match_all($pattern, $text, $matches); return $matches[1]; } /** * findCurrentIp tries to find current IPv6 address in submitted array by filtering */ function findCurrentIp(array $ips) { $currentIp = ""; foreach ($ips as $ip) { $ip = trim($ip); if (!preg_match("/^f[cdef][0-9a-f][0-9a-f].*$/", $ip) && !preg_match("/^fe80.*$/", $ip) && !preg_match("/^.*\sdeprecated\s.*$/", $ip) ) { $currentIp = $ip; break; } } return $currentIp; } function cleanIp($ip) { preg_match("/^(.*)\s.*$/U",$ip,$ma); if (count($ma)) { return $ma[1]; } return ""; } function saveCurrentIp($filename,$ip) { $res = file_put_contents($filename, $ip); if ($res === false) { return false; } return true; } function readIpFromFile($filename) { if (!file_exists($filename)) { return false; } $ip = file_get_contents($filename); if ($ip === false) { return false; } return $ip; } function applyInterfaceLAN() { exec("php ccw_apply_pfsense.php", $output, $retval); if ($retval != 0) { return false; } return true; } function isLocked($lockFile, $seconds = 600) { if (file_exists($lockFile)) { // older than 10 minutes? $mTime = filemtime($lockFile); $curTime = time(); if (($curTime - $mTime) > $seconds) { syslog(LOG_WARNING, SYSLOG_PREFIX . ERR_LOCK_TIMEOUT); unlink($lockFile); return false; } return true; } return false; } function setLock($lockFile) { return touch($lockFile); } function removeLock($lockFile) { return unlink($lockFile); } function cleanupAndExit($errorCode, $logLevel, $logText) { global $currentIpFileLock; syslog($logLevel, $logText); removeLock($currentIpFileLock); exit($errorCode); } function main($wanInterface, $currentIpFile) { global $currentIpFileLock; if (isLocked($currentIpFileLock)) { syslog(LOG_WARNING, SYSLOG_PREFIX . ERR_PROCESS_RUNNING); exit(10); } setLock($currentIpFileLock); $ips = getIp($wanInterface); if (count($ips) < 1) { cleanupAndExit(1, LOG_WARNING, SYSLOG_PREFIX . ERR_COULD_NOT_GET_IP . $wanInterface); } $currentIp = findCurrentIp($ips); if ($currentIp == "") { cleanupAndExit(2, LOG_WARNING, SYSLOG_PREFIX . ERR_COULD_NOT_FIND_CURRENT_IP); } $currentIp = cleanIp($currentIp); if ($currentIp == "") { cleanupAndExit(3, LOG_WARNING, SYSLOG_PREFIX . ERR_COULD_NOT_RECOGNIZE_CURRENT_IP); } $fileIp = readIpFromFile($currentIpFile); if ($fileIp === false) { applyInterfaceLAN(); $ok = saveCurrentIp($currentIpFile, $currentIp); if (!$ok) { cleanupAndExit(4, LOG_WARNING, SYSLOG_PREFIX . ERR_COULD_NOT_WRITE_IP_FILE); } cleanupAndExit(0, LOG_INFO, SYSLOG_PREFIX . INFO_NEW_IP_APPLIED . $currentIp); } else { if ($currentIp == $fileIp) { cleanupAndExit(0, LOG_INFO, SYSLOG_PREFIX . INFO_STILL_VALID . $currentIp); } else { // current IP differs from file IP, do something and save new ip into file applyInterfaceLAN(); $ok = saveCurrentIp($currentIpFile, $currentIp); if (!$ok) { cleanupAndExit(5, LOG_WARNING, SYSLOG_PREFIX . ERR_COULD_NOT_WRITE_IP_FILE); } cleanupAndExit(0, LOG_INFO, SYSLOG_PREFIX . INFO_NEW_IP_APPLIED . $currentIp); } } } main($wanInterface, $currentIpFile);
In diesem Skript („ccw_ipv6.php
„) erfolgt die Konfiguration. Der Einfachheit halber ohne zusätzliches Include-File, da es in den meisten Fällen ausreichen müsste, das WAN-Interface korrekt zu setzen. Im Beispiel ist dies die Zeile $wanInterface = "igb0";
. Der Name des Interfaces leitet sich dabei vom Chipsatz der Netzwerkkarte ab, so dass hier einfach nur die Bezeichnung des „Network Port“ genutzt werden muss, der dem WAN-Interface zugeordnet ist, wie unter „Interfaces“ -> „Interface Assignments“ angegeben.
Bei den Pfaden für die Speicherung der aktuellen IP-Adresse und des Lockfiles ist eine Anpassung zwar möglich, aber das /tmp/
-Verzeichnis sollte im Normalfall dazu nutzbar sein, denn auch pfSense selbst verwendet dies zur Speicherung von temporären Angaben.
Das Skript macht nichts Anderes, als die aktuelle IPv6-Adresse abzufragen, vergleicht diese mit der zuvor gespeicherten Adresse, falls vorhanden. Bei Abweichung wird die neue Adresse gespeichert und das Skript zur Neukonfiguration des LAN-Interfaces wird gestartet. Der restliche Code implementiert noch einen Locking-Mechanismus, der ein mehrfaches Starten des Skriptes verhindern soll (man weiß ja nie…), außerdem werden im Fehlerfall entsprechende Meldungen ins Syslog geschrieben.
PHP für pfSense, #2: LAN-Interface neu konfigurieren
Der folgende Code („ccw_apply_pfsense.php
„) sorgt schließlich dafür, das LAN-Interface neu zu starten:
<?php /* * ccw_apply_pfsense.php * * Copyright (c) 2023 Ralf Geschke <ralf@kuerbis.org>. * * NOT part of pfSense (https://www.pfsense.org) * * Most parts of this code are extracted from interface.php which is published under the following licenses: * Copyright (c) 2004-2013 BSD Perimeter * Copyright (c) 2013-2016 Electric Sheep Fencing * Copyright (c) 2014-2023 Rubicon Communications, LLC (Netgate) * Copyright (c) 2006 Daniel S. Haischt * All rights reserved. * * originally based on m0n0wall (http://m0n0.ch/wall) * Copyright (c) 2003-2004 Manuel Kasper <mk@neon1.net>. * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* parse the configuration and include all functions used below */ require_once("globals.inc"); require_once("config.inc"); require_once("functions.inc"); require_once("filter.inc"); require_once("shaper.inc"); require_once("ipsec.inc"); require_once("vpn.inc"); require_once("openvpn.inc"); require_once("Net/IPv6.php"); require_once("services.inc"); require_once("rrd.inc"); // prevent execution when booting or configuration in progress if (is_platform_booting()) { syslog(LOG_WARNING, "CCW_IPv6_APPLY: System is booting, exiting."); exit(1); } if (is_subsystem_dirty('interfaces')) { syslog(LOG_WARNING, "CCW_IPv6_APPLY: Interface configuration not applied, it seems that the configuration process is still in work, exiting."); exit(2); } $ifapply = "lan"; // build interface config array as it comes from unserializing the configuration array when performing "apply" in LAN interface UI section $ifConfig = config_get_path("interfaces/{$ifapply}"); $ifConfig['realif'] = $ifConfig['if']; $ifcfgo['ifcfg'] = $ifConfig; // The following part comes from interface.php. // Perform the "apply" action to restart and reconfigure LAN interface. // By doing this, the LAN interface gets the new IPv6 address, so the new // configuration can be distributed with DHCPv6 / RA. $realif = get_real_interface($ifapply); $ifmtu = get_interface_mtu($realif); if (config_path_enabled("interfaces/{$ifapply}")) { interface_bring_down($ifapply, false, $ifcfgo); interface_configure($ifapply, true); if (config_get_path("interfaces/{$ifapply}/ipaddrv6") == "track6") { /* call interface_track6_configure with linkup true so IPv6 IPs are added back. dhcp6c needs a HUP. Can't just call interface_configure with linkup true as that skips bridge membership addition. */ $wancfg = config_get_path("interfaces/{$ifapply}"); interface_track6_configure($ifapply, $wancfg, true); } } else { interface_bring_down($ifapply, true, $ifcfgo); } restart_interface_services($ifapply, $ifcfg['ipaddrv6']); /* sync filter configuration */ setup_gateways_monitor(); clear_subsystem_dirty('interfaces'); $retval |= filter_configure(); enable_rrd_graphing();
Bis auf die Prüfung, ob das System sich gerade im Bootvorgang befindet bzw. ob das LAN-Interface aktuell eine Bearbeitung erfährt, ist dieser Teil aus dem Skript „interface.php
“ von pfSense übernommen. Somit handelt es sich um denselben Code, der nach „Save“ und „Apply“ des LAN-Interfaces dafür sorgt, dies neu zu konfigurieren und die davon abhängigen Dienste neu zu starten. Leider hatte ich ad hoc in den Myriaden Codezeilen keine einzelne Funktion gefunden, die diese Schritte übernimmt, daher habe ich die entsprechenden Teile einfach kopiert.
Cron, übernehmen Sie!
Zur Installation müssen beide Dateien in ein passendes Verzeichnis kopiert werden, in meinem Fall habe ich der Einfachheit halber das /root/-Verzeichnis genutzt, wobei sich sicherlich auch ein besserer Ort findet. Zur Konfiguration ist nur das WAN-Interface – wie oben beschrieben – anzupassen. Die Einrichtung eines Cronjobs kann wiederum per Web-UI erfolgen, dazu findet sich unter „Services“ -> „Cron“ -> „Settings“ eine Übersicht:
Falls Cron nicht bereits installiert ist, kann dies einfach via „System“ -> „Package Manager“ -> „Available Packages“ durch Wahl des Cron-Paketes erfolgen.
Ein neuer Cronjob-Eintrag wird durch Klick auf den „Add„-Button hinzugefügt:
In diesem Beispiel wird das Skript „ccw_ipv6.php
„, das sich im Verzeichnis /root/ befindet, minütlich gestartet.
Der Erfolg – oder auch Misserfolg – lässt sich im Status-Bereich betrachten, und zwar bei „Status“ -> „System Logs“ -> „System“ -> „General„. Kurz nach der Verbindungstrennung und -wiederaufnahme sollte sich in den Logs ein derartiger Eintrag finden:
Mar 29 02:36:06 php 38755 CCW_IPv6: Current IP settings applied successfully: 2a0a:AAAA:BBBB:CCCC:DDDD:EEEE:FFFF:GGGG
Anstelle der Platzhalter würde darin dann die aktualisierte und somit gültige IPv6-Adresse angezeigt.
Workaround oder Würgaround?
Zugegebenermaßen ist dies alles kein Hexenwerk, sondern eher im Gegenteil ein kleiner Workaround, der bei einem Problem hilft, das zumindest meiner Ansicht und meiner Erwartungshaltung nach gar nicht auftreten dürfte. Schließlich ist IPv6 nun auch nicht mehr so neu, und die Verbindungstrennungen der Provider bei DSL-Anschlüssen zumindest hierzulande Standard. Auf meinem aktuellen pfSense-System schieben die Skripte zum Zeitpunkt des Schreibens dieses Artikels seit ungefähr vier Wochen ihren Dienst, und bislang konnte ich keine Probleme feststellen. Neue IPv6-Adressen bzw. -Prefixes werden zuverlässig vergeben, alle Rechner im Netz erhalten kurz nach der Verbindungstrennung die aktuellen IPv6-Daten per DHCPv6 bzw. Router Advertisements.
Wie immer würde ich mich über Feedback und Kommentare sehr freuen, und zum Schluss sei mir noch einmal der Hinweis erlaubt, beim etwaigen Ausprobieren bitte den Code nicht von dieser Seite zu kopieren, sondern aus dem CCW_IPv6-GitHub-Repository zu entnehmen, da dieser ausschließlich dort gepflegt wird.
Super Sache! Nur leider scheinen bei mir die php scripts nicht mit der Pfsense 2.6 CE kompatibel zu sein.
z.b.
amd64
12.3-STABLE
FreeBSD 12.3-STABLE RELENG_2_6_0-n226742-1285d6d205f pfSense
Crash report details:
PHP Errors:
[07-May-2023 12:19:00 Etc/UTC] PHP Fatal error: Uncaught Error: Call to undefined function config_get_path() in /root/ccw_apply_pfsense.php:62
Stack trace:
#0 {main}
thrown in /root/ccw_apply_pfsense.php on line 62
No FreeBSD crash data found.
Ich hatte zuvor einen gleichartigen fehler mit „is_platform_booting()“ was ich danach auf „platform_booting()“ mit scheinbaren erfolg abgeändert habe (zumindtes kam der fehler – Call to undefined function- nicht mehr).
Hallo!
Vielen Dank für Deinen Hinweis. Tatsächlich nutze ich die „pfSense +“ Edition mit Community Support, also nach Registrierung kostenfrei, da befindet sich Folgendes drunter:
Version 23.01-RELEASE (amd64)
built on Fri Feb 10 20:06:33 UTC 2023
FreeBSD 14.0-CURRENT
Anhand des pfSense-Quelltextes bei GitHub ist auch zu erkennen, dass die Funktion platform_booting() genutzt wird, anstatt wie hier is_platform_booting(), verwendet wird diese z.B. in „/etc/rc.newwanipv6“ (siehe https://github.com/pfsense/pfsense/blob/master/src/etc/rc.newwanipv6). Hier war Deine Änderung insofern goldrichtig. 🙂
Der untere Teil des Skriptes ist wie erwähnt eine schamlose Kopie eines Snippets der Datei interfaces.php. Die aktuelle Version müsste somit diese sein: https://github.com/pfsense/pfsense/blob/master/src/usr/local/www/interfaces.php ab Zeile 440.
Die Funktion wird insofern auch bei pfSense ohne „+“ eingesetzt. Allerdings fiel mir auf, dass sich die Include-Dateien unterscheiden. Evtl. kannst Du die require_once()-Aufrufe im Skript ab Zeile 35 durch diejenigen aus der interfaces.php-Datei auf GitHub (ab Zeile 36) ersetzen? Falls das erfolgreich war, freue ich mich sehr über Feedback und würde das entsprechend einarbeiten, zwei Versionen anbieten o.ä.. Falls es nicht erfolgreich war, freue ich mich natürlich auch über einen Hinweis. 🙂
Vielen Dank & Beste Grüße,
Ralf
Hallo Ralf,
dein Hinweis, dass es eine kostenfreie pfSense Plus Edition Version gibt war Goldwert. Damit sollte auch dein Script dann bei mir ohne Änderungen laufen.
Ich denke, ich werde Upgrade durchführen!
Danke für den Tipp!
Danke für den Artikel! Ich frage mich nur, warum pfSense das nicht einfach selbst implementiert…
Guten Morgen,
besten Dank, das wäre Super, habe das gleiche Problem.
Ich starte die ganze OPNSense neu, wenn ipv6 keinen Ping mehr absetzen kann.
Wäre das Skript auch für OPNSense lauffähig?
greets
Byte
Hallo Ralf,
Wow super Artikel vielen Dank dafür.
Und endlich jemand der auch NetCologne nutzt und damit kämpft 😉
Wie finde ich raus ob ich tatsächlich IPV6 oder nur DualStack habe?
Vor ca.4Jahren musste ich von DSLite auf DualStack umstellen um überhaupt die Pfsense zum Laufen zu bringen. Jetzt soll diese neu aufgesetzt werden und es geht nicht mehr?
Das einzige was, interessanterweise klappt, ist eine alte Config einzuspielen. es muss also Code außerhalb der GUI geben in dem die „funktionierenden“ Einstellungen schlummern.
Denn das eintragen der sichtbaren GUI Einstellungen bringt nix mehr.
Und um zu der Eingangs gestellten Fragen zu kommen:
Nach der Neuinstallation und dem Abbruch des Wizard sehe ich WAN UP und Gateway UP allerdings nur für IPV6
WAN ist auf DHCP und ich habe noch keine Credentials eingetragen. Denn das Vigor 165 ist als Bridge dazwischen.
Grüße und danke aus Erftstadt 😃
Dominik
Hallo Ralf,
ich würde gerne lieber OPNsense verwenden anstatt pfSense.
Wäre es möglich das Script auch auf die OPNsense abzustimmen?
Ich habe leider nicht so viel Ahnung von PHP.
Würde mich über eine Rückmeldung freuen.
Lieben Gruß
Janina
Hallo,
super und vielen Dank. Eine tolle Anleitung! Ich habe das für meine Zwecke leicht abgeändert, Zeile 57 statt „lan“: „wan“ und in Z. 77 statt „track6“: „dhcp6“.
Hintergrund ist, dass ich pfsense als NAT für IPv6 benutzte. Fragt sich dann nach dem Sinn von IPv6?! Local Link Adressen über mehrere Standorte via VPN site-to-site. Das ActiveDirectory braucht für die Standorte feste IPv6 Präfixe, ansonsten kommt es bei mir zu einigen Problemen mit den sich dynamischen ändernden Präfixen, bspw. mit „nltest site“.
Viele Grüße
Felix