diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php
index b7a213ef0e..179d802f8f 100644
--- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php
+++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php
@@ -34,6 +34,7 @@ class AclController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'acl';
protected static $internalModelClass = '\OPNsense\Bind\Acl';
+ protected static $internalModelUseSafeDelete = true;
public function searchAclAction()
{
@@ -64,4 +65,9 @@ public function toggleAclAction($uuid)
{
return $this->toggleBase('acls.acl', $uuid);
}
+
+ protected function checkAndThrowValueInUse($token, $contains = true, $only_mvc = false, $exclude_refs = [])
+ {
+ parent::checkAndThrowValueInUse($token, $contains, $only_mvc, $exclude_refs);
+ }
}
diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindAcl.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindAcl.xml
index 1931d97ef1..00aa2bb76f 100644
--- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindAcl.xml
+++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindAcl.xml
@@ -14,9 +14,7 @@
acl.networks
-
- select_multiple
- true
- List of networks for this ACL.
+ textbox
+ List of addresses and network prefixes, one address or prefix per line. Use a leading exclamation mark (!) for negation. These built in ACLs may also be used: any, none, localhost, and localnets. If more than one element in an ACL is found to match a given IP address or prefix, preference is given to the one that came first in the ACL definition.
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 23e9c92026..f4b472a43f 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
@@ -84,10 +84,8 @@
general.filteraaaaacl
-
- select_multiple
- true
- Specifies a list of client addresses for which AAAA filtering is to be applied.
+ textbox
+ Specifies a list of client addresses, one per line, for which AAAA filtering is to be applied.
general.logsize
diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml
index 870ee9ec03..9619569c23 100644
--- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml
+++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml
@@ -20,10 +20,8 @@
-
- ,
+
Y
- Y
diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/FieldTypes/BindAddressMatchField.php b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/FieldTypes/BindAddressMatchField.php
new file mode 100644
index 0000000000..5cbcb9a8c3
--- /dev/null
+++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/FieldTypes/BindAddressMatchField.php
@@ -0,0 +1,158 @@
+internalFieldSeparator, array_map('trim', explode("\n", trim(strtolower($value))))));
+ }
+
+ /**
+ * setter for net mask required
+ * @param integer $value
+ */
+ public function setNetMaskRequired($value)
+ {
+ if (trim(strtoupper($value)) == "Y") {
+ $this->internalNetMaskRequired = true;
+ } else {
+ $this->internalNetMaskRequired = false;
+ }
+ }
+
+ /**
+ * setter for net mask required
+ * @param integer $value
+ */
+ public function setNetMaskAllowed($value)
+ {
+ $this->internalNetMaskAllowed = (trim(strtoupper($value)) == "Y");
+ }
+
+ /**
+ * setter for address family
+ * @param $value address family [ipv4, ipv6, empty for all]
+ */
+ public function setAddressFamily($value)
+ {
+ $this->internalAddressFamily = trim(strtolower($value));
+ }
+
+ /**
+ * select if host bits are allowed in the notation
+ * @param $value
+ */
+ public function setStrict($value)
+ {
+ if (trim(strtoupper($value)) == "Y") {
+ $this->internalStrict = true;
+ } else {
+ $this->internalStrict = false;
+ }
+ }
+
+ /**
+ * get valid options, descriptions and selected value
+ * @return array
+ */
+ public function getNodeData()
+ {
+ return join("\n", array_map('trim', explode($this->internalFieldSeparator, $this->internalValue)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defaultValidationMessage()
+ {
+ return gettext('Please specify a valid network segment or IP address.');
+ }
+
+ /**
+ * retrieve field validators for this field type
+ * @return array returns Text/regex validator
+ */
+ public function getValidators()
+ {
+ $validators = parent::getValidators();
+ if ($this->internalValue != null) {
+ $validators[] = new BindAddressMatchValidator([
+ 'message' => $this->getValidationMessage(),
+ 'split' => $this->internalFieldSeparator,
+ 'netMaskRequired' => $this->internalNetMaskRequired,
+ 'netMaskAllowed' => $this->internalNetMaskAllowed,
+ 'version' => $this->internalAddressFamily,
+ 'strict' => $this->internalStrict
+ ]);
+ }
+ return $validators;
+ }
+}
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 72e771fbc8..8a23f84f95 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
@@ -59,10 +59,7 @@
0
Y
-
- ,
- Y
-
+
5
Y
diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Validators/BindAddressMatchValidator.php b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Validators/BindAddressMatchValidator.php
new file mode 100644
index 0000000000..83f0ca9e55
--- /dev/null
+++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Validators/BindAddressMatchValidator.php
@@ -0,0 +1,149 @@
+getOption('message');
+ $fieldSplit = $this->getOption('split', null);
+ if ($fieldSplit == null) {
+ $values = array($validator->getValue($attribute));
+ } else {
+ $values = explode($fieldSplit, $validator->getValue($attribute));
+ }
+ foreach ($values as $value) {
+ // strip off negation before address validation
+ $value = ltrim($value, '!');
+
+ // short-circuit on built-in ACLs
+ if (in_array($value, $this->builtinAcls)) {
+ continue;
+ }
+
+ // parse filter options
+ $filterOpt = 0;
+ switch (strtolower($this->getOption('version') ?? '')) {
+ case "ipv4":
+ $filterOpt |= FILTER_FLAG_IPV4;
+ break;
+ case "ipv6":
+ $filterOpt |= FILTER_FLAG_IPV6;
+ break;
+ default:
+ $filterOpt |= FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
+ }
+
+ // split network
+ if (strpos($value, "/") !== false) {
+ if ($this->getOption('netMaskAllowed') === false) {
+ $result = false;
+ } else {
+ $cidr = $value;
+ $parts = explode("/", $value);
+ if (count($parts) > 2 || !ctype_digit($parts[1])) {
+ // more parts then expected or second part is not numeric
+ $result = false;
+ } else {
+ $mask = $parts[1];
+ $value = $parts[0];
+ if (strpos($parts[0], ":") !== false) {
+ // probably ipv6, mask must be between 0..128
+ if ($mask < 0 || $mask > 128) {
+ $result = false;
+ }
+ } else {
+ // most likely ipv4 address, mask must be between 0..32
+ if ($mask < 0 || $mask > 32) {
+ $result = false;
+ }
+ }
+ }
+
+ if ($this->getOption('strict') === true && !Util::isSubnetStrict($cidr)) {
+ $result = false;
+ }
+ }
+ } elseif ($this->getOption('netMaskRequired') === true) {
+ $result = false;
+ }
+
+
+ if (filter_var($value, FILTER_VALIDATE_IP, $filterOpt) === false) {
+ $result = false;
+ }
+
+ if (!$result) {
+ // append validation message
+ $validator->appendMessage(new Message($msg, $attribute, 'BindAddressMatchValidator'));
+ }
+ }
+
+ return $result;
+ }
+}
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 6b833e19a9..e645e94f3b 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,36 @@ acl "{{ acl_list.name }}" { {{ acl_list.networks.replace(',', '; ') }}; };
{% endfor %}
{% endif %}
+{#
+Given a comma separated list of ACL UUIDs, if any named ACLs in the list are enabled,
+evaluate the call block, otherwise skip. Use this to wrap configuration block that don't
+make sense to include if there are no enabled ACLs.
+#}
+{% macro ACLListEnabled(aclList) %}
+{% if aclList is defined and aclList != '' %}
+{% set ns = namespace() %}
+{% set ns.found = 0 %}
+{% for aclUUID in aclList.split(',') if helpers.getUUID(aclUUID).enabled == '1' %}
+{% set ns.found = ns.found + 1 %}
+{% endfor %}
+{% if ns.found > 0 %}
+{{ caller() }}
+{% endif %}
+{% endif %}
+{% endmacro %}
+
+{#
+Given a comma separated list of ACL UUIDs, return a semicolon separated
+list of the enabled ACL names for use in a an address match field context.
+#}
+{% macro ACLNames(aclList, joiner = "; ") %}
+{% set ns = namespace() %}
+{% set ns.names = [] %}
+{% if aclList is defined and aclList != '' %}
+{% for aclUUID in aclList.split(',') %}{% do ns.names.append(helpers.getUUID(aclUUID)) %}{% endfor %}
+{% endif %}
+{{ ns.names | selectattr("enabled", "eq", "1") | map(attribute="name") | join(joiner) }}{% endmacro %}
+
options {
directory "/usr/local/etc/namedb/working";
@@ -46,33 +76,24 @@ options {
response-policy { {% if helpers.exists('OPNsense.bind.dnsbl.type') and OPNsense.bind.dnsbl.type != '' %}zone "whitelist.localdomain"; zone "blacklist.localdomain";{% endif %}{% if helpers.exists('OPNsense.bind.dnsbl.forcesafegoogle') and OPNsense.bind.dnsbl.forcesafegoogle == '1' %}zone "rpzgoogle";{% endif %}{% if helpers.exists('OPNsense.bind.dnsbl.forcesafeduckduckgo') and OPNsense.bind.dnsbl.forcesafeduckduckgo == '1' %}zone "rpzduckduckgo";{% endif %}{% if helpers.exists('OPNsense.bind.dnsbl.forcesafeyoutube') and OPNsense.bind.dnsbl.forcesafeyoutube == '1' %}zone "rpzyoutube";{% endif %}{% if helpers.exists('OPNsense.bind.dnsbl.forcestrictbing') and OPNsense.bind.dnsbl.forcestrictbing == '1' %}zone "rpzbing";{% endif %} };
{% endif %}
-{% if helpers.exists('OPNsense.bind.general.recursion') and OPNsense.bind.general.recursion != '' %}
+{% if helpers.exists('OPNsense.bind.general.recursion') %}
+{% call ACLListEnabled(OPNsense.bind.general.recursion) %}
recursion yes;
- allow-recursion {
-{% for acl in OPNsense.bind.general.recursion.split(',') %}
-{% set recursion_acl = helpers.getUUID(acl) %}
- {{ recursion_acl.name }};
-{% endfor %}
- };
-{% endif %}
+ allow-recursion { {{ ACLNames(OPNsense.bind.general.recursion) }}; };
+{% endcall %}
+{% endif -%}
-{% if helpers.exists('OPNsense.bind.general.allowtransfer') and OPNsense.bind.general.allowtransfer != '' %}
- allow-transfer {
-{% for acl in OPNsense.bind.general.allowtransfer.split(',') %}
-{% set transfer_acl = helpers.getUUID(acl) %}
- {{ transfer_acl.name }};
-{% endfor %}
- };
-{% endif %}
+{% if helpers.exists('OPNsense.bind.general.allowtransfer') %}
+{% call ACLListEnabled(OPNsense.bind.general.allowtransfer) %}
+ allow-transfer { {{ ACLNames(OPNsense.bind.general.allowtransfer) }}; };
+{% endcall %}
+{% endif -%}
-{% if helpers.exists('OPNsense.bind.general.allowquery') and OPNsense.bind.general.allowquery != '' %}
- allow-query {
-{% for acl in OPNsense.bind.general.allowquery.split(',') %}
-{% set query_acl = helpers.getUUID(acl) %}
- {{ query_acl.name }};
-{% endfor %}
- };
-{% endif %}
+{% if helpers.exists('OPNsense.bind.general.allowquery') %}
+{% call ACLListEnabled(OPNsense.bind.general.allowquery) %}
+ allow-query { {{ ACLNames(OPNsense.bind.general.allowquery) }}; };
+{% endcall %}
+{% endif -%}
{% if helpers.exists('OPNsense.bind.general.maxcachesize') and OPNsense.bind.general.maxcachesize != '' %}
max-cache-size {{ OPNsense.bind.general.maxcachesize }}%;
@@ -91,7 +112,7 @@ options {
{% endif %}
{% if helpers.exists('OPNsense.bind.general.enableratelimiting') and OPNsense.bind.general.enableratelimiting == '1' %}
{% if helpers.exists('OPNsense.bind.general.ratelimitcount') and OPNsense.bind.general.ratelimitcount != '' %}
- rate-limit {
+ rate-limit {
responses-per-second {{ OPNsense.bind.general.ratelimitcount }};
{% if helpers.exists('OPNsense.bind.general.ratelimitexcept') and OPNsense.bind.general.ratelimitexcept != '' %}
exempt-clients { {{ OPNsense.bind.general.ratelimitexcept.replace(',', '; ') }}; };
@@ -108,7 +129,7 @@ key "rndc-key" {
};
controls {
inet 127.0.0.1 port 9530
- allow { 127.0.0.1; } keys { "rndc-key"; };
+ allow { 127.0.0.1; } keys { "rndc-key"; };
};
{% endif %}
@@ -164,30 +185,24 @@ zone "{{ domain.domainname }}" {
{% else %}
file "/usr/local/etc/namedb/primary/{{ domain.domainname }}.db";
{% endif %}
-{% if domain.allowtransfer is defined or (domain.allowrndctransfer is defined and domain.allowrndctransfer == "1") %}
+{% if domain.allowrndctransfer is defined and domain.allowrndctransfer == "1" %}
allow-transfer {
-{% if domain.allowrndctransfer is defined and domain.allowrndctransfer == "1" %}
- key "rndc-key";
-{% endif %}
-{% if domain.allowtransfer is defined %}
-{% for acl in domain.allowtransfer.split(',') %}
-{% set transfer_acl = helpers.getUUID(acl) %}
- {{ transfer_acl.name }};
-{% endfor %}
-{% endif %}
- };
-{% endif %}
-{% if domain.allowquery is defined %}
- allow-query {
-{% for acl in domain.allowquery.split(',') %}
-{% set query_acl = helpers.getUUID(acl) %}
- {{ query_acl.name }};
-{% endfor %}
+ key "rndc-key";
+{% call ACLListEnabled(domain.allowtransfer) %}
+ {{ ACLNames(domain.allowtransfer) }};
+{% endcall %}
};
+{% else %}
+{% call ACLListEnabled(domain.allowtransfer) %}
+ allow-transfer { {{ ACLNames(domain.allowtransfer) }}; };
+{% endcall %}
{% endif %}
+{% call ACLListEnabled(domain.allowquery) %}
+ allow-query { {{ ACLNames(domain.allowquery) }}; };
+{% endcall %}
{% if domain.allowrndcupdate is defined and domain.allowrndcupdate == "1" and domain.type != 'secondary' %}
update-policy {
- grant rndc-key zonesub ANY;
+ grant rndc-key zonesub ANY;
};
{% endif %}
};
@@ -244,20 +259,20 @@ logging {
plugin query "/usr/local/lib/bind/filter-aaaa.so" {
{% if helpers.exists('OPNsense.bind.general.filteraaaav4') and OPNsense.bind.general.filteraaaav4 == '1' %}
{% if OPNsense.bind.general.dnssecvalidation == 'no' %}
- filter-aaaa-on-v4 break-dnssec;
+ filter-aaaa-on-v4 break-dnssec;
{% else %}
- filter-aaaa-on-v4 yes;
+ filter-aaaa-on-v4 yes;
{% endif %}
{% endif %}
{% if helpers.exists('OPNsense.bind.general.filteraaaav6') and OPNsense.bind.general.filteraaaav6 == '1' %}
{% if OPNsense.bind.general.dnssecvalidation == 'no' %}
- filter-aaaa-on-v6 break-dnssec;
+ filter-aaaa-on-v6 break-dnssec;
{% else %}
- filter-aaaa-on-v6 yes;
+ filter-aaaa-on-v6 yes;
{% endif %}
{% endif %}
{% if helpers.exists('OPNsense.bind.general.filteraaaaacl') and OPNsense.bind.general.filteraaaaacl != '' %}
filter-aaaa { {{ OPNsense.bind.general.filteraaaaacl.replace(',', '; ') }}; };
{% endif %}
- };
+};
{% endif %}