From 5d48eb3e8b2466a1a87b8636e6038dc26f940e13 Mon Sep 17 00:00:00 2001 From: mbedworth Date: Sat, 2 May 2026 19:10:48 -0400 Subject: [PATCH] dns/bind: add native DNS-over-TLS (DoT) server support Add GUI configuration for incoming DNS-over-TLS connections (RFC 7858). BIND 9.18+ supports a native tls clause, but the plugin had no way to configure it. This adds three fields to the general settings model and form: dotenable, dotport (default 853), and dotcertificate. A new generate_certs.php script reads the selected certificate from the OPNsense trust store and writes the PEM files to /var/etc/named/ before named starts. The configd [start] and [restart] actions are updated to call this script first, ensuring cert files are present on every boot (since /var/etc/ is a tmpfs ramdisk). A standalone [certsetup] action is also added for on-demand use. The named.conf template emits a tls dot-tls stanza and listen-on lines for port 853 when dotenable is set, reusing the existing listen addresses. Requires BIND 9.18+. --- .../OPNsense/Bind/forms/general.xml | 22 ++++++ .../mvc/app/models/OPNsense/Bind/General.xml | 14 +++- .../scripts/OPNsense/Bind/generate_certs.php | 69 +++++++++++++++++++ .../service/conf/actions.d/actions_bind.conf | 11 ++- .../templates/OPNsense/Bind/named.conf | 15 ++++ 5 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 dns/bind/src/opnsense/scripts/OPNsense/Bind/generate_certs.php diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml index 23e9c9202..89e11ab84 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml @@ -195,4 +195,26 @@ true The base64-encoded RNDC key. This requires a restart of the Bind Service. + + header + + + + general.dotenable + + checkbox + Accept incoming DNS-over-TLS (port 853) connections. Requires BIND 9.18+ and a valid certificate. The same listen addresses used for plain DNS are also used for DoT. + + + general.dotport + + text + Port to listen on for DNS-over-TLS connections. Default is 853. + + + general.dotcertificate + + dropdown + TLS certificate to present to DNS-over-TLS clients. Select a certificate from System > Trust > Certificates. + diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml index 238c9dc24..201ad79d4 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml @@ -1,7 +1,7 @@ //OPNsense/bind/general BIND configuration - 1.0.12 + 1.0.13 0 @@ -167,5 +167,17 @@ Y VxtIzJevSQXqnr7h2qerrcwjnZlMWSGGFBndKeNIDfw= + + 0 + Y + + + 853 + Y + + + N + Please select a valid certificate for DNS over TLS. + diff --git a/dns/bind/src/opnsense/scripts/OPNsense/Bind/generate_certs.php b/dns/bind/src/opnsense/scripts/OPNsense/Bind/generate_certs.php new file mode 100644 index 000000000..df76502b9 --- /dev/null +++ b/dns/bind/src/opnsense/scripts/OPNsense/Bind/generate_certs.php @@ -0,0 +1,69 @@ +#!/usr/local/bin/php +object(); +$general = $cfg->OPNsense->bind->general ?? new \stdClass(); + +$dotenable = (string)($general->dotenable ?? '0'); +$certref = (string)($general->dotcertificate ?? ''); + +if ($dotenable === '1' && $certref !== '') { + $cert = Store::getCertificate($certref); + if ($cert && isset($cert['prv'])) { + File::file_update_contents($certfile, $cert['crt'], 0640); + File::file_update_contents($keyfile, $cert['prv'], 0640); + chown($certfile, 'bind'); + chgrp($certfile, 'bind'); + chown($keyfile, 'bind'); + chgrp($keyfile, 'bind'); + exit(0); + } +} + +foreach ([$certfile, $keyfile] as $f) { + if (file_exists($f)) { + unlink($f); + } +} diff --git a/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf b/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf index c3d4a47a5..ff9f2e7f7 100644 --- a/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf +++ b/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf @@ -1,5 +1,12 @@ +[certsetup] +command:/usr/local/bin/php /usr/local/opnsense/scripts/OPNsense/Bind/generate_certs.php +parameters: +type:script +message:setting up BIND TLS certificates +description: Write BIND DoT certificate files from OPNsense trust store + [start] -command:/usr/local/etc/rc.d/named start +command:/usr/local/bin/php /usr/local/opnsense/scripts/OPNsense/Bind/generate_certs.php && /usr/local/etc/rc.d/named start parameters: type:script message:starting BIND @@ -11,7 +18,7 @@ type:script message:stopping BIND [restart] -command:/usr/local/etc/rc.d/named restart +command:/usr/local/bin/php /usr/local/opnsense/scripts/OPNsense/Bind/generate_certs.php && /usr/local/etc/rc.d/named restart parameters: type:script message:restarting BIND diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf index 9196b5de3..1bcdd0129 100644 --- a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf @@ -8,6 +8,13 @@ acl "{{ acl_list.name }}" { {{ acl_list.networks.replace(',', '; ') }}; }; {% endfor %} {% endif %} +{% if helpers.exists('OPNsense.bind.general.dotenable') and OPNsense.bind.general.dotenable == '1' %} +tls dot-tls { + cert-file "/var/etc/named/dot.crt"; + key-file "/var/etc/named/dot.key"; +}; +{% endif %} + options { directory "/usr/local/etc/namedb/working"; @@ -21,6 +28,14 @@ options { {% for listenv6 in OPNsense.bind.general.listenv6.split(',') %} listen-on-v6 port {{ OPNsense.bind.general.port }} { {% if listenv6 == '::' %}any{% else %}{{ listenv6 }}{% endif %}; }; {% endfor %} +{% if helpers.exists('OPNsense.bind.general.dotenable') and OPNsense.bind.general.dotenable == '1' %} +{% for listenv4 in OPNsense.bind.general.listenv4.split(',') %} + listen-on port {{ OPNsense.bind.general.dotport }} tls dot-tls { {% if listenv4 == '0.0.0.0' %}any{% else %}{{ listenv4 }}{% endif %}; }; +{% endfor %} +{% for listenv6 in OPNsense.bind.general.listenv6.split(',') %} + listen-on-v6 port {{ OPNsense.bind.general.dotport }} tls dot-tls { {% if listenv6 == '::' %}any{% else %}{{ listenv6 }}{% endif %}; }; +{% endfor %} +{% endif %} {% if helpers.exists('OPNsense.bind.general.querysource') and OPNsense.bind.general.querysource != '' %} query-source {{ OPNsense.bind.general.querysource }};