Firewall: Rules - allow multiple options in source/destination address fields.

o merge src+srcmask, dst+dstmask into a single field
o remove current clunky input and re-use the same javascript hooks as in MVC
o re-use OPNsense\Firewall\Api\FilterController to list available options
This commit is contained in:
Ad Schellevis 2024-12-05 22:00:44 +01:00
parent 918ba63bb5
commit 0dac1d6201
2 changed files with 99 additions and 120 deletions

View File

@ -86,6 +86,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
} elseif (isset($_GET['id']) && isset($a_filter[$_GET['id']])) {
$id = $_GET['id'];
$configId = $id;
} elseif (isset($_GET['get_address_options'])) {
/* XXX: no beauty contest here, we need the same valid options as MVC, just dump them... */
echo json_encode((new OPNsense\Firewall\Api\FilterController())->listNetworkSelectOptionsAction());
exit(0);
}
// define form fields
@ -149,12 +153,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$pconfig['category'] = !empty($pconfig['category']) ? explode(",", $pconfig['category']) : [];
// process fields with some kind of logic
address_to_pconfig($a_filter[$configId]['source'], $pconfig['src'],
$pconfig['srcmask'], $pconfig['srcnot'],
$pconfig['srcbeginport'], $pconfig['srcendport']);
address_to_pconfig($a_filter[$configId]['destination'], $pconfig['dst'],
$pconfig['dstmask'], $pconfig['dstnot'],
$pconfig['dstbeginport'], $pconfig['dstendport']);
address_to_pconfig(
$a_filter[$configId]['source'],
$pconfig['src'],
$ignore, /* XXX: ignored */
$pconfig['srcnot'],
$pconfig['srcbeginport'],
$pconfig['srcendport'],
true
);
address_to_pconfig(
$a_filter[$configId]['destination'],
$pconfig['dst'],
$ignore, /* XXX: ignored */
$pconfig['dstnot'],
$pconfig['dstbeginport'],
$pconfig['dstendport'],
true
);
if (isset($id) && isset($a_filter[$configId]['associated-rule-id'])) {
// do not link on rule copy.
$pconfig['associated-rule-id'] = $a_filter[$configId]['associated-rule-id'];
@ -294,18 +312,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$input_errors[] = gettext('When selecting aliases for destination ports, both from and to fields must be the same');
}
}
if (!is_specialnet($pconfig['src']) && !is_ipaddroralias($pconfig['src'])) {
if (strpos($pconfig['src'], ',') > 0) {
foreach (explode(',', $pconfig['src']) as $tmp) {
if (!is_specialnet($tmp) && !is_alias($tmp)) {
$input_errors[] = sprintf(gettext("%s is not a valid source alias."), $tmp);
}
}
} elseif (!is_specialnet($pconfig['src']) && !is_ipaddroralias($pconfig['src']) && !is_subnet($pconfig['src'])) {
$input_errors[] = sprintf(gettext("%s is not a valid source IP address or alias."),$pconfig['src']);
}
if (!empty($pconfig['srcmask']) && !is_numericint($pconfig['srcmask'])) {
$input_errors[] = gettext("A valid source bit count must be specified.");
}
if (!is_specialnet($pconfig['dst']) && !is_ipaddroralias($pconfig['dst'])) {
if (strpos($pconfig['dst'], ',') > 0) {
foreach (explode(',', $pconfig['dst']) as $tmp) {
if (!is_specialnet($tmp) && !is_alias($tmp)) {
$input_errors[] = sprintf(gettext("%s is not a valid destination alias."), $tmp);
}
}
} elseif (!is_specialnet($pconfig['dst']) && !is_ipaddroralias($pconfig['dst']) && !is_subnet($pconfig['dst'])) {
$input_errors[] = sprintf(gettext("%s is not a valid destination IP address or alias."),$pconfig['dst']);
}
if (!empty($pconfig['dstmask']) && !is_numericint($pconfig['dstmask'])) {
$input_errors[] = gettext("A valid destination bit count must be specified.");
}
if (is_ipaddr($pconfig['src']) && is_ipaddr($pconfig['dst'])) {
if ((is_ipaddrv4($pconfig['src']) && is_ipaddrv6($pconfig['dst'])) || (is_ipaddrv6($pconfig['src']) && is_ipaddrv4($pconfig['dst']))) {
@ -314,21 +338,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
foreach (array('src', 'dst') as $fam) {
if (is_ipaddr($pconfig[$fam])) {
if (is_ipaddrv6($pconfig[$fam]) && $pconfig['ipprotocol'] == "inet") {
if ((is_ipaddrv6($pconfig[$fam]) || is_subnetv6($pconfig[$fam])) && $pconfig['ipprotocol'] == "inet") {
$input_errors[] = gettext("You can not use IPv6 addresses in IPv4 rules.");
} elseif (is_ipaddrv4($pconfig[$fam]) && $pconfig['ipprotocol'] == "inet6") {
} elseif ((is_ipaddrv4($pconfig[$fam]) || is_subnetv4($pconfig[$fam])) && $pconfig['ipprotocol'] == "inet6") {
$input_errors[] = gettext("You can not use IPv4 addresses in IPv6 rules.");
}
}
}
if (is_ipaddrv4($pconfig['src']) && $pconfig['srcmask'] > 32) {
$input_errors[] = gettext("Invalid subnet mask on IPv4 source");
}
if (is_ipaddrv4($pconfig['dst']) && $pconfig['dstmask'] > 32) {
$input_errors[] = gettext("Invalid subnet mask on IPv4 destination");
}
if ((is_ipaddr($pconfig['src']) || is_ipaddr($pconfig['dst'])) && ($pconfig['ipprotocol'] == "inet46")) {
$input_errors[] = gettext('You can not use an IPv4 or IPv6 address in combined IPv4 + IPv6 rules.');
}
@ -395,7 +412,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (is_numeric($pconfig['adaptivestart']) || is_numeric($pconfig['adaptiveend'])) {
$input_errors[] = gettext("You cannot specify the adaptive timeouts (advanced option) if statetype is none.");
}
}
if (!empty($pconfig['max']) && !is_posnumericint($pconfig['max']))
@ -586,10 +602,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
pconfig_to_address($filterent['source'], $pconfig['src'],
$pconfig['srcmask'] ?? '', !empty($pconfig['srcnot']),
'', !empty($pconfig['srcnot']),
$pconfig['srcbeginport'], $pconfig['srcendport']);
pconfig_to_address($filterent['destination'], $pconfig['dst'],
$pconfig['dstmask'] ?? '', !empty($pconfig['dstnot']),
'', !empty($pconfig['dstnot']),
$pconfig['dstbeginport'], $pconfig['dstendport']);
$filterent['updated'] = make_config_revision_entry();
@ -652,10 +668,54 @@ include("head.inc");
$(".advanced_opt_src").toggleClass("hidden visible");
});
$.ajax("/firewall_rules_edit.php?get_address_options",{
type: 'get',
cache: false,
dataType: "json",
success: function(data) {
$(".net_selector_multi").each(function(){
/* replaceInputWithSelector() replaces our input with a clean one, copy relevant attributes after construct */
$(this).replaceInputWithSelector(data, true);
let new_input = $("#" + $(this).attr('id'));
new_input.attr('name', $(this).attr('id'));
if ($(this).is(':disabled')) {
$("select[for='" + $(this).attr('id') + "']").prop('disabled', true);
new_input.prop('disabled', true);
}
$("select[for='" + $(this).attr('id') + "']").on('shown.bs.select', function(){
$(this).data('previousValue', $(this).val());
}).change(function(){
let prev = Array.isArray($(this).data('previousValue')) ? $(this).data('previousValue') : [];
let is_single = $(this).val().includes('') || $(this).val().includes('any');
let was_single = prev.includes('') || prev.includes('any');
let refresh = false;
if (was_single && is_single && $(this).val().length > 1) {
$(this).val($(this).val().filter(value => !prev.includes(value)));
refresh = true;
} else if (is_single && $(this).val().length > 1) {
if ($(this).val().includes('any') && !prev.includes('any')) {
$(this).val('any');
} else{
$(this).val('');
}
refresh = true;
}
if (refresh) {
$(this).selectpicker('refresh');
$(this).trigger('change');
}
$(this).data('previousValue', $(this).val());
});
new_input.val($(this).val()).change();
});
}
});
// select / input combination, link behaviour
// when the data attribute "data-other" is selected, display related input item(s)
// push changes from input back to selected option value
$('[for!=""][for]').each(function(){
$('.portselect').each(function(){
var refObj = $("#"+$(this).attr("for"));
if (refObj.is("select")) {
// connect on change event to select box (show/hide)
@ -689,7 +749,7 @@ include("head.inc");
if ($(this).attr("name") == undefined) {
$(this).change(function(){
var otherOpt = $('#'+$(this).attr('for')+' > option[data-other="true"]') ;
otherOpt.attr("value",$(this).val());
otherOpt.attr("value", $(this).val());
});
}
}
@ -1064,51 +1124,8 @@ include("head.inc");
<tr>
<td><i class="fa fa-info-circle text-muted"></i> <?=gettext("Source"); ?></td>
<td>
<table style="max-width: 348px">
<tr>
<td>
<select <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> name="src" id="src" class="selectpicker" data-live-search="true" data-size="5" data-width="348px">
<option data-other=true value="<?=$pconfig['src'];?>" <?=!is_specialnet($pconfig['src']) ? "selected=\"selected\"" : "";?>><?=gettext("Single host or Network"); ?></option>
<optgroup label="<?=gettext("Aliases");?>">
<?php foreach (legacy_list_aliases("network") as $alias):
?>
<option value="<?=$alias['name'];?>" <?=$alias['name'] == $pconfig['src'] ? "selected=\"selected\"" : "";?>><?=htmlspecialchars($alias['name']);?></option>
<?php endforeach; ?>
</optgroup>
<optgroup label="<?=gettext("Networks");?>">
<?php foreach (get_specialnets(true) as $ifent => $ifdesc):
?>
<option value="<?=$ifent;?>" <?= $pconfig['src'] == $ifent ? "selected=\"selected\"" : ""; ?>><?=$ifdesc;?></option>
<?php endforeach; ?>
</optgroup>
</select>
</td>
</tr>
<tr>
<td>
<div>
<table style="max-width: 348px">
<tbody>
<tr>
<td style="width:285px">
<!-- updates to "other" option in src -->
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" id="src_address" for="src" value="<?=$pconfig['src'];?>" aria-label="<?=gettext("Source address");?>"/>
</td>
<td>
<select <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> name="srcmask" data-network-id="src_address" class="selectpicker ipv4v6net" data-size="5" id="srcmask" data-width="70px" for="src">
<?php for ($i = 128; $i > 0; $i--): ?>
<option value="<?=$i;?>" <?= $i == $pconfig['srcmask'] ? "selected=\"selected\"" : ""; ?>><?=$i;?></option>
<?php endfor ?>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</table>
</td>
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> id="src" name="src" class="net_selector_multi" type="text" value="<?=$pconfig['src'];?>" />
</td>
</tr>
<tr class="advanced_opt_src visible">
<td><?=gettext("Source"); ?></td>
@ -1168,10 +1185,10 @@ include("head.inc");
</tr>
<tr>
<td>
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" value="<?=$pconfig['srcbeginport'];?>" for="srcbeginport"> <!-- updates to "other" option in srcbeginport -->
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" value="<?=$pconfig['srcbeginport'];?>" class="portselect" for="srcbeginport"> <!-- updates to "other" option in srcbeginport -->
</td>
<td>
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" value="<?=$pconfig['srcendport'];?>" for="srcendport"> <!-- updates to "other" option in srcendport -->
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" value="<?=$pconfig['srcendport'];?>" class="portselect" for="srcendport"> <!-- updates to "other" option in srcendport -->
</td>
</tr>
</tbody>
@ -1192,48 +1209,7 @@ include("head.inc");
<tr>
<td><i class="fa fa-info-circle text-muted"></i> <?=gettext("Destination"); ?></td>
<td>
<table style="max-width: 348px">
<tr>
<td>
<select <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> name="dst" id="dst" class="selectpicker" data-live-search="true" data-size="5" data-width="348px">
<option data-other=true value="<?=$pconfig['dst'];?>" <?=!is_specialnet($pconfig['dst']) ? "selected=\"selected\"" : "";?>><?=gettext("Single host or Network"); ?></option>
<optgroup label="<?=gettext("Aliases");?>">
<?php foreach (legacy_list_aliases("network") as $alias):
?>
<option value="<?=$alias['name'];?>" <?=$alias['name'] == $pconfig['dst'] ? "selected=\"selected\"" : "";?>><?=htmlspecialchars($alias['name']);?></option>
<?php endforeach; ?>
</optgroup>
<optgroup label="<?=gettext("Networks");?>">
<?php foreach (get_specialnets(true) as $ifent => $ifdesc):
?>
<option value="<?=$ifent;?>" <?= $pconfig['dst'] == $ifent ? "selected=\"selected\"" : ""; ?>><?=$ifdesc;?></option>
<?php endforeach; ?>
</optgroup>
</select>
</td>
</tr>
<tr>
<td>
<table style="max-width: 348px">
<tbody>
<tr>
<td style="width:285px">
<!-- updates to "other" option in src -->
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" id="dst_address" for="dst" value="<?=$pconfig['dst'];?>" aria-label="<?=gettext("Destination address");?>"/>
</td>
<td>
<select <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> name="dstmask" class="selectpicker ipv4v6net" data-network-id="dst_address" data-size="5" id="dstmask" data-width="70px" for="dst">
<?php for ($i = 128; $i > 0; $i--): ?>
<option value="<?=$i;?>" <?= $i == $pconfig['dstmask'] ? "selected=\"selected\"" : ""; ?>><?=$i;?></option>
<?php endfor; ?>
</select>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> id="dst" name="dst" type="text" value="<?=$pconfig['dst'];?>" class="net_selector_multi" />
</td>
</tr>
<tr>
@ -1285,10 +1261,10 @@ include("head.inc");
</tr>
<tr>
<td>
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" value="<?=$pconfig['dstbeginport'];?>" for="dstbeginport"> <!-- updates to "other" option in dstbeginport -->
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> class="portselect" type="text" value="<?=$pconfig['dstbeginport'];?>" for="dstbeginport"> <!-- updates to "other" option in dstbeginport -->
</td>
<td>
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> type="text" value="<?=$pconfig['dstendport'];?>" for="dstendport"> <!-- updates to "other" option in dstendport -->
<input <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?> class="portselect" type="text" value="<?=$pconfig['dstendport'];?>" for="dstendport"> <!-- updates to "other" option in dstendport -->
</td>
</tr>
</tbody>

View File

@ -385,7 +385,7 @@ function gentitle($breadcrumbs, $navlevelsep = ': ')
return join($navlevelsep, $output) . "$gentitle_suffix";
}
function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendport)
function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendport, $merge_mask=false)
{
if (isset($adr['any'])) {
$padr = "any";
@ -402,6 +402,9 @@ function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendp
$pmask = 32;
}
}
if ($merge_mask && is_ipaddr($padr)) {
$padr = $padr . '/' . $pmask;
}
}
if (isset($adr['not'])) {