Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dns/bind: Allow negation in ACL definitions (#4435) #4520

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
<field>
<id>acl.networks</id>
<label>Network List</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>List of networks for this ACL.</help>
<type>textbox</type>
<help>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.</help>
</field>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,8 @@
<field>
<id>general.filteraaaaacl</id>
<label>ACL for filter-aaaa</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Specifies a list of client addresses for which AAAA filtering is to be applied.</help>
<type>textbox</type>
<help>Specifies a list of client addresses, one per line, for which AAAA filtering is to be applied.</help>
</field>
<field>
<id>general.logsize</id>
Expand Down
4 changes: 1 addition & 3 deletions dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
</check001>
</Constraints>
</name>
<networks type="NetworkField">
<FieldSeparator>,</FieldSeparator>
<networks type=".\BindAddressMatchField">
<Required>Y</Required>
<asList>Y</asList>
</networks>
</acl>
</acls>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

/*
* Copyright (C) 2015-2025 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

namespace OPNsense\Bind\FieldTypes;

use OPNsense\Base\FieldTypes\BaseField;
use OPNsense\Bind\Validators\BindAddressMatchValidator;

/**
* @package OPNsense\Bind\FieldTypes
*/
class BindAddressMatchField extends BaseField
{
/**
* @var bool marks if this is a data node or a container
*/
protected $internalIsContainer = false;

/**
* @var bool marks if net mask is required
*/
protected $internalNetMaskRequired = false;

/**
* @var bool marks if net mask is (dis)allowed
*/
protected $internalNetMaskAllowed = true;

/**
* @var string when multiple values could be provided at once, specify the split character
*/
protected $internalFieldSeparator = ',';

/**
* @var string Network family (ipv4, ipv6)
*/
protected $internalAddressFamily = null;

/**
* @var bool when set, host bits with a value other than zero are not allowed in the notation if a mask is provided
*/
private $internalStrict = false;

/**
* always lowercase / trim networks
* @param string $value
*/
public function setValue($value)
{
parent::setValue(join($this->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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@
<Default>0</Default>
<Required>Y</Required>
</filteraaaav6>
<filteraaaaacl type="NetworkField">
<FieldSeparator>,</FieldSeparator>
<asList>Y</asList>
</filteraaaaacl>
<filteraaaaacl type=".\BindAddressMatchField"/>
<logsize type="IntegerField">
<Default>5</Default>
<Required>Y</Required>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

/*
* Copyright (C) 2015-2025 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

namespace OPNsense\Bind\Validators;

use OPNsense\Base\BaseValidator;
use OPNsense\Firewall\Util;
use OPNsense\Base\Messages\Message;

/**
* Class NetworkValidator validate networks and ip addresses
* @package OPNsense\Base\Validators
*/
class BindAddressMatchValidator extends BaseValidator
{
/**
* @var array List of built in ACLs
*/
protected $builtinAcls = ['any','none','localhost','localnets'];

/**
* Executes network / ip validation for a subset of Bind Address Match Lists
* version : ipv4, ipv6, all (default)
* netMaskAllowed : true (default), false)
* netMaskRequired : true, false (default)
* strict: : true, false (default)
*
* Address match list elements which are supported:
* - ip_address: an IP address (IPv4 or IPv6)
* - netprefix: an IP prefix (in / notation)
* - negation with a leading exclamation mark (!)
* - built in ACL names of: any, none, localhost, and localnets
*
* Address match list elements which are NOT supported:
* - server_key: a key ID, as defined by the key statement
* - acl_name: the name of an address match list defined with the acl statement
* - a nested address match list enclosed in braces
*
* Reference: https://bind9.readthedocs.io/en/v9.20.4/reference.html#address-match-lists
*
* @param $validator
* @param string $attribute
* @return boolean
*/
public function validate($validator, $attribute): bool
{
$result = true;
$msg = $this->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;
}
}
Loading