Firewall-Regel via PHP in config.xml geschrieben. filter_configure() aufgerufen. pfctl -sr zeigt: nichts. Kein Fehler, keine Warnung — die Regel existiert einfach nicht.

Das war mein Nachmittag auf einem pfSense-Gateway.


Die Regel war da. pf kannte sie nicht.

Ich wollte DoT/DoQ auf Port 853 blocken damit LAN-Clients den DNS-Resolver nicht via DNS over TLS umgehen können. PHP-Skript, write_config(), filter_configure() — alles Standard.

$block_rule = [
    'type'        => 'block',
    'disabled'    => '',       // ← der Täter
    'interface'   => 'lan',
    'protocol'    => 'tcp/udp',
    'source'      => ['network' => 'lan'],
    'destination' => ['any' => '', 'port' => '853'],
    'descr'       => 'Block DoT/DoQ Port 853',
    'tracker'     => (string)time(),
];

grep in config.xml — Regel vorhanden. /tmp/rules.debug — Regel fehlt. pfctl -sr — Regel fehlt.


Die Ursache: filter.inc und PHP’s isset()

In /etc/inc/filter.inc steht:

foreach (config_get_path('filter/rule', []) as $rule) {
    if (isset($rule['disabled'])) {
        continue;  // Regel überspringen
    }
    // ... Regel generieren
}

Das Problem: isset('') gibt in PHP true zurück. Ein leerer String ist kein null — der Key existiert, also springt isset() an.

pfSense’s XML-Parser macht daraus wenn du <disabled/> im XML liest: null. Und isset(null) gibt false zurück — Regel wird generiert. Aber mein PHP-Skript hat '' (leerer String) geschrieben, nicht null. Ergebnis: Regel deaktiviert, lautlos.

Das Gleiche gilt für NAT-Regeln:

foreach (config_get_path('nat/rule', []) as $rule) {
    if (isset($rule['disabled'])) {
        continue;  // auch hier
    }

Der Fix

disabled-Key einfach nicht setzen. Kein Wert, kein Key, gar nichts:

$block_rule = [
    'type'        => 'block',
    // 'disabled' => ''  ← raus damit
    'interface'   => 'lan',
    'protocol'    => 'tcp/udp',
    'source'      => ['network' => 'lan'],
    'destination' => ['any' => '', 'port' => '853'],
    'descr'       => 'Block DoT/DoQ Port 853',
    'tracker'     => (string)time(),
    'created'     => ['time' => (string)time(), 'username' => 'admin@localhost'],
    'updated'     => ['time' => (string)time(), 'username' => 'admin@localhost'],
];

Bestehende kaputte Regel reparieren:

<?php
require_once("config.inc");
require_once("filter.inc");
$config = parse_config(true);

foreach ($config['filter']['rule'] as $i => $r) {
    if (($r['destination']['port'] ?? '') === '853' && isset($r['disabled'])) {
        unset($config['filter']['rule'][$i]['disabled']);
        echo "Fixed rule #$i\n";
    }
}
$config['filter']['rule'] = array_values($config['filter']['rule']);
write_config("Fix: disabled-Flag entfernt");
filter_configure();

Danach: pfctl -sr | grep "domain-s" — Regel erscheint.


Merksatz

Beim Schreiben von pfSense-Firewall- oder NAT-Regeln via PHP: disabled-Key darf nicht existieren — nicht mal mit leerem Wert. pfSense prüft isset(), nicht den Wert.

Wer eine Regel wirklich deaktivieren will: 'disabled' => true (nicht leer). Wer eine aktive Regel will: Key komplett weglassen.