diff --git a/src/etc/inc/globals.inc b/src/etc/inc/globals.inc
index bf46112b5fc5159c23406e3216b68a83e848264e..38ee5813856ad3b81e8a1733af819d48820a77f3 100644
--- a/src/etc/inc/globals.inc
+++ b/src/etc/inc/globals.inc
@@ -92,7 +92,7 @@ $g = [
'disablehelpicon' => false,
'disablecrashreporter' => false,
'debug' => false,
- 'latest_config' => '24.7',
+ 'latest_config' => '24.8',
'minimum_ram_warning' => '101',
'minimum_ram_warning_text' => '128 MB',
'wan_interface_name' => INTERFACE_IDENTITY_WAN,
diff --git a/src/etc/inc/upgrade_config.inc b/src/etc/inc/upgrade_config.inc
index 34ade0be76430222265b31721615fef4b2ff8009..53208cb8879fe7573d4b9f766b941cc57d9b47d4 100644
--- a/src/etc/inc/upgrade_config.inc
+++ b/src/etc/inc/upgrade_config.inc
@@ -7492,6 +7492,130 @@ function upgrade_246_to_247() {
}
}
+/**
+ * Filter and Port Forwarding rules may have duplicate or invalid
+ * association IDs. Remove these invalid association IDs. Resolve
+ * duplicate conflicts by only keeping the newest association; only the
+ * association is modified, the rules themselves are retained.
+ * https://redmine.pfsense.org/issues/17021
+ */
+function upgrade_247_to_248() {
+ $changes = [];
+ $changed_filter_rules = false;
+ $changed_rdr_rules = false;
+ $rdr_rules_list = get_anynat_rules_list('rdr');
+ $filter_rules_list = get_filter_rules_list();
+
+ // Remove invalid association IDs from filter rules.
+ // Collect any other associated filter rules.
+ $changed = false;
+ $filter_rules_association_map = [];
+ foreach ($filter_rules_list as &$rule) {
+ if (!isset($rule['associated-rule-id'])) {
+ continue;
+ }
+
+ if (!is_string($rule['associated-rule-id']) || (strlen($rule['associated-rule-id']) < 1) ||
+ ($rule['associated-rule-id'] == 'pass')) {
+ // Invalid association ID.
+ unset($rule['associated-rule-id']);
+ $changed = true;
+ continue;
+ }
+
+ $filter_rules_association_map[$rule['associated-rule-id']][] = &$rule;
+ }
+ if ($changed) {
+ $changed_filter_rules = true;
+ $changes[] = gettext('Removed invalid Port Forwarding rule association from firewall rule');
+ }
+
+ // Check for duplicate associations - keep the newest association.
+ $changed = false;
+ foreach ($filter_rules_association_map as &$rules) {
+ if (count($rules) < 2) {
+ continue;
+ }
+ $newest_associated_rule_idx = array_key_last($rules);
+ foreach ($rules as $idx => &$rule) {
+ if ($idx != $newest_associated_rule_idx) {
+ unset($rule['associated-rule-id']);
+ $changed = true;
+ }
+ }
+ }
+ if ($changed) {
+ $changed_filter_rules = true;
+ $changes[] = gettext('Removed duplicate Port Forwarding rule association from firewall rule');
+ }
+
+ // Remove association from NAT rules with no corresponding filter rule.
+ // Remove invalid associations.
+ // Collect any other associated filter rules.
+ $changed = false;
+ $rdr_rules_association_map = [];
+ foreach ($rdr_rules_list as &$rule) {
+ if (empty($rule['associated-rule-id']) || ($rule['associated-rule-id'] == 'pass')) {
+ // Not an associated rule.
+ continue;
+ }
+
+ if (!is_string($rule['associated-rule-id']) || !str_starts_with($rule['associated-rule-id'], 'nat')) {
+ // Invalid association.
+ $rule['associated-rule-id'] = '';
+ $changed = true;
+ continue;
+ }
+
+ if (!isset($filter_rules_association_map[$rule['associated-rule-id']])) {
+ // No corresponding filter rule.
+ $rule['associated-rule-id'] = '';
+ $changed = true;
+ continue;
+ }
+
+ $rdr_rules_association_map[$rule['associated-rule-id']][] = &$rule;
+ }
+ if ($changed) {
+ $changed_rdr_rules = true;
+ $changes[] = gettext('Removed invalid firewall rule association from Port Forwarding rule');
+ }
+
+ // Check for duplicate associations - keep the newest association.
+ $changed = false;
+ foreach ($rdr_rules_association_map as &$rules) {
+ if (count($rules) < 2) {
+ continue;
+ }
+ $newest_associated_rule_idx = array_key_last($rules);
+ foreach ($rules as $idx => &$rule) {
+ if ($idx != $newest_associated_rule_idx) {
+ $rule['associated-rule-id'] = '';
+ $changed = true;
+ }
+ }
+ }
+ if ($changed) {
+ $changed_rdr_rules = true;
+ $changes[] = gettext('Removed duplicate firewall rule association from Port Forwarding rule');
+ }
+
+ unset($rule, $rules);
+
+ // Save changes.
+ if (!empty($changes)) {
+ if ($changed_rdr_rules) {
+ set_anynat_rules_list('rdr', $rdr_rules_list);
+ }
+ if ($changed_filter_rules) {
+ set_filter_rules_list($filter_rules_list);
+ }
+ foreach ($changes as $message) {
+ logger(LOG_INFO, $message, LOG_PREFIX_UPGRADE);
+ }
+ }
+}
+
/*
* Special function that is called independent of current config version. It's
* a workaround to have config_upgrade running on older versions after next
diff --git a/src/usr/local/pfSense/include/www/firewall_nat.inc b/src/usr/local/pfSense/include/www/firewall_nat.inc
index 8a5e7d5f85958a1bc5afde49ef52a11c0fc4b644..5edce915344b23407c8f368d6f5fab79dae5f4da 100644
--- a/src/usr/local/pfSense/include/www/firewall_nat.inc
+++ b/src/usr/local/pfSense/include/www/firewall_nat.inc
@@ -612,7 +612,7 @@ function saveNATrule($post, $id, $json = false) {
$need_filter_rule = false;
// Updating a rule with a filter rule associated
- if (!empty($natent['associated-rule-id'])) {
+ if (!empty($natent['associated-rule-id']) && ($natent['associated-rule-id'] != "pass")) {
$need_filter_rule = true;
}
// Create a rule or if we want to create a new one
diff --git a/src/usr/local/www/firewall_nat_edit.php b/src/usr/local/www/firewall_nat_edit.php
index ac867c5d735c7344831f957eef667c9ba4421b32..5e1414993716d305da848d2d309e3be304b08b9f 100644
--- a/src/usr/local/www/firewall_nat_edit.php
+++ b/src/usr/local/www/firewall_nat_edit.php
@@ -94,6 +94,32 @@ if ($_POST['save'] && !$input_errors) {
}
}
+$is_new_nat_rule = true;
+if (!is_numericint($_POST['dup']) && isset($id) && config_get_path("nat/rule/{$id}")) {
+ $is_new_nat_rule = false;
+}
+
+if ($is_new_nat_rule) {
+ if ($_POST['save']) {
+ // Match preferred rule settings.
+ $pconfig['associated-rule-id'] = match ($_POST['filter-rule-association']) {
+ 'pass' => 'pass',
+ 'add-unassociated' => 'add-unassociated',
+ '' => '',
+ default => 'add-associated'
+ };
+ } elseif (array_key_exists('associated-rule-id', $pconfig)) {
+ // Match association action to that of the rule being duplicated.
+ $pconfig['associated-rule-id'] = match ($pconfig['associated-rule-id']) {
+ 'pass' => 'pass',
+ '' => 'add-unassociated',
+ default => 'add-associated'
+ };
+ } else {
+ // Set default association action.
+ $pconfig['associated-rule-id'] = 'add-associated';
+ }
+}
function srctype_selected() {
global $pconfig;
@@ -424,42 +450,46 @@ $section->addInput(new Form_Select(
)
));
-if (isset($id) && config_get_path("nat/rule/{$id}") && (!isset($_POST['dup']) || !is_numericint($_POST['dup']))) {
-
- $hlpstr = '';
- $rulelist = array('' => gettext('None'), 'pass' => gettext('Pass'));
- $rule_association = 'associated-rule-id';
-
+// Association action.
+// '': Indicates a non-pass NAT rule without a filter rule association.
+// 'pass': Indicates a pass NAT rule without a filter rule association.
+// 'new': (existing NAT rules only) Completely overwrite the existing associated filter rule.
+// 'add-associated': Create a new associated filter rule.
+// 'add-unassociated': Create a new unassociated filter rule.
+$rulelist = [
+ '' => gettext('None'),
+ 'pass' => gettext('Pass')
+];
+
+$hlpstr = gettext('The "pass" selection does not work properly with Multi-WAN. It will only work on an interface containing the default gateway.');
+
+$has_linked_filter_rule = false;
+if (!$is_new_nat_rule && str_starts_with(array_get_path($pconfig, 'associated-rule-id', ''), 'nat')) {
foreach (get_filter_rules_list() as $filter_id => $filter_rule) {
- if (isset($filter_rule['associated-rule-id'])) {
- $rulelist[$filter_rule['associated-rule-id']] = sprintf(gettext('Rule %s'), $filter_rule['descr']);
-
- if ($filter_rule['associated-rule-id'] == $pconfig['associated-rule-id']) {
- $hlpstr = '<a href="firewall_rules_edit.php?id=' . $filter_id . '">' . gettext("View the filter rule") . '</a><br />';
- }
+ if (!isset($filter_rule['associated-rule-id'])) {
+ continue;
}
- }
-
- if (isset($pconfig['associated-rule-id'])) {
+ if ($filter_rule['associated-rule-id'] != $pconfig['associated-rule-id']) {
+ continue;
+ }
+ $rulelist[$filter_rule['associated-rule-id']] = sprintf(gettext('Rule %s'), $filter_rule['descr']);
$rulelist['new'] = gettext('Create new associated filter rule');
+ $hlpstr = '<a href="firewall_rules_edit.php?id=' . $filter_id . '">' . gettext("View the filter rule") . '</a><br />' . $hlpstr;
+ $has_linked_filter_rule = true;
+ break;
}
-} else {
- $rulelist = array(
- '' => gettext('None'),
- 'add-associated' => gettext('Add associated filter rule'),
- 'add-unassociated' => gettext('Add unassociated filter rule'),
- 'pass' => gettext('Pass')
- );
- $hlpstr = gettext('The "pass" selection does not work properly with Multi-WAN. It will only work on an interface containing the default gateway.');
- $rule_association = 'filter-rule-association';
- if (!isset($pconfig['associated-rule-id']) ||
- (strpos($pconfig['associated-rule-id'], 'nat') !== false)) {
- $pconfig['associated-rule-id'] = 'add-associated';
- }
}
+if (!$has_linked_filter_rule) {
+ $rulelist['add-associated'] = gettext('Add associated filter rule');
+ $rulelist['add-unassociated'] = gettext('Add unassociated filter rule');
+}
+
+// Association action type.
+// 'associated-rule-id': Used for existing NAT rules with a linked filter rule.
+// 'filter-rule-association': Used for new or existing NAT rules with no linked rules.
$section->addInput(new Form_Select(
- $rule_association,
+ $has_linked_filter_rule ? 'associated-rule-id' : 'filter-rule-association',
'Filter rule association',
$pconfig['associated-rule-id'],
$rulelist