Fix to scope direct-mode changes to selected tables

Fix nftables direct-mode operations so create, edit, delete, and move actions apply only the selected table instead of rewriting or applying the full ruleset. This avoids copying firewalld-owned rules, or any other externally managed rules, into Webmin’s save file and prevents operations from failing against externally managed tables. Also remove previously added unsafe full-ruleset flush action and keep Apply Configuration out of direct mode (will be further reworked).
This commit is contained in:
Ilia Ross
2026-05-02 17:08:35 +02:00
parent 6fb304e384
commit 7f2b4b00aa
16 changed files with 298 additions and 81 deletions

View File

@@ -5,11 +5,12 @@
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
our (%config, %in, %text);
ReadParse();
error_setup($text{'apply_err'});
my @tables = get_nftables_save();
redirect("index.cgi") if ($config{'direct'});
my $err = apply_restore();
error($err) if ($err);

View File

@@ -26,17 +26,18 @@ if ($in{'create'}) {
}
}
push(@tables, { 'name' => $name,
'family' => $family,
'rules' => [],
'chains' => {},
'sets' => {} });
my $err = save_configuration(@tables);
my $table = { 'name' => $name,
'family' => $family,
'rules' => [],
'chains' => {},
'sets' => {} };
push(@tables, $table);
my $err = create_table_configuration($table, @tables);
error(text('create_failed', $err)) if ($err);
webmin_log("create", "table", $name, { 'family' => $family });
my $idx = $#tables;
redirect("index.cgi?table=$idx");
redirect("index.cgi?table_family=".urlize($family).
"&table_name=".urlize($name));
}
ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1);

View File

@@ -27,7 +27,7 @@ if ($in{'confirm'}) {
@{$table->{'rules'}} = grep { $_->{'chain'} ne $in{'chain'} } @{$table->{'rules'}};
delete($table->{'chains'}->{$in{'chain'}});
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('delete_chain_failed', $err)) if ($err);
webmin_log("delete", "chain", $in{'chain'},
{ 'table' => $table->{'name'}, 'family' => $table->{'family'} });

56
nftables/delete_chains.cgi Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/perl
# delete_chains.cgi
# Delete selected nftables chains
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
error_setup($text{'delete_chains_err'});
my @tables = get_nftables_save();
my $table_idx = $in{'table'};
my $table;
if (defined($in{'table_family'}) && defined($in{'table_name'})) {
for (my $i = 0; $i <= $#tables; $i++) {
if ($tables[$i]->{'family'} eq $in{'table_family'} &&
$tables[$i]->{'name'} eq $in{'table_name'}) {
$table_idx = $i;
$table = $tables[$i];
last;
}
}
}
$table ||= $tables[$table_idx];
$table || error($text{'chain_notable'});
my @chains = split(/\0/, $in{'d'} || "");
my %seen;
@chains = grep { defined($_) && $_ ne '' && !$seen{$_}++ } @chains;
@chains || error($text{'delete_chains_enone'});
foreach my $c (@chains) {
$table->{'chains'}->{$c} || error(text('delete_chains_nochain', $c));
}
my %delete = map { $_ => 1 } @chains;
my @refs = grep {
!$delete{$_->{'chain'}} &&
(($_->{'jump'} && $delete{$_->{'jump'}}) ||
($_->{'goto'} && $delete{$_->{'goto'}}))
} @{$table->{'rules'}};
@refs && error(text('delete_chains_inuse', scalar(@refs)));
@{$table->{'rules'}} = grep { !$delete{$_->{'chain'}} } @{$table->{'rules'}};
foreach my $c (@chains) {
delete($table->{'chains'}->{$c});
}
my $err = save_table_configuration($table, @tables);
error(text('delete_chains_failed', $err)) if ($err);
webmin_log("delete", "chains", scalar(@chains),
{ 'table' => $table->{'name'},
'family' => $table->{'family'} });
redirect("index.cgi?table_family=".urlize($table->{'family'}).
"&table_name=".urlize($table->{'name'}));

View File

@@ -22,11 +22,11 @@ if ($in{'confirm'}) {
$refs && error(text('delete_set_inuse', $in{'set'}, $refs));
delete($table->{'sets'}->{$in{'set'}});
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('delete_set_failed', $err)) if ($err);
webmin_log("delete", "set", $in{'set'},
{ 'table' => $table->{'name'}, 'family' => $table->{'family'} });
redirect("index.cgi?table=$in{'table'}");
redirect("index.cgi?table=$in{'table'}&view=sets");
}
ui_print_header(undef, $text{'delete_set_title'}, "", "intro", 1, 1);
@@ -45,4 +45,4 @@ print "<p>\n";
print ui_submit($text{'delete'}, "confirm");
print "</center>\n";
print ui_form_end();
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});
ui_print_footer("index.cgi?table=$in{'table'}&view=sets", $text{'index_return'});

53
nftables/delete_sets.cgi Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/perl
# delete_sets.cgi
# Delete selected nftables sets
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
error_setup($text{'delete_sets_err'});
my @tables = get_nftables_save();
my $table_idx = $in{'table'};
my $table;
if (defined($in{'table_family'}) && defined($in{'table_name'})) {
for (my $i = 0; $i <= $#tables; $i++) {
if ($tables[$i]->{'family'} eq $in{'table_family'} &&
$tables[$i]->{'name'} eq $in{'table_name'}) {
$table_idx = $i;
$table = $tables[$i];
last;
}
}
}
$table ||= $tables[$table_idx];
$table || error($text{'set_notable'});
my @sets = split(/\0/, $in{'s'} || "");
my %seen;
@sets = grep { defined($_) && $_ ne '' && !$seen{$_}++ } @sets;
@sets || error($text{'delete_sets_enone'});
foreach my $s (@sets) {
$table->{'sets'}->{$s} || error(text('delete_sets_noset', $s));
}
my $refs = 0;
foreach my $s (@sets) {
$refs += count_set_references($table, $s);
}
$refs && error(text('delete_sets_inuse', $refs));
foreach my $s (@sets) {
delete($table->{'sets'}->{$s});
}
my $err = save_table_configuration($table, @tables);
error(text('delete_sets_failed', $err)) if ($err);
webmin_log("delete", "sets", scalar(@sets),
{ 'table' => $table->{'name'},
'family' => $table->{'family'} });
redirect("index.cgi?table_family=".urlize($table->{'family'}).
"&table_name=".urlize($table->{'name'})."&view=sets");

View File

@@ -10,12 +10,27 @@ ReadParse();
error_setup($text{'delete_err'});
my @tables = get_nftables_save();
my $table = $tables[$in{'table'}];
my $table_idx = $in{'table'};
my $table;
if (defined($in{'table_family'}) && defined($in{'table_name'})) {
for (my $i = 0; $i <= $#tables; $i++) {
if ($tables[$i]->{'family'} eq $in{'table_family'} &&
$tables[$i]->{'name'} eq $in{'table_name'}) {
$table_idx = $i;
$table = $tables[$i];
last;
}
}
$table || error($text{'delete_notable'});
}
else {
$table = $tables[$table_idx];
}
$table || error($text{'delete_notable'});
if ($in{'confirm'}) {
splice(@tables, $in{'table'}, 1);
my $err = save_configuration(@tables);
splice(@tables, $table_idx, 1);
my $err = delete_table_configuration($table, @tables);
error(text('delete_failed', $err)) if ($err);
webmin_log("delete", "table", $table->{'name'},
{ 'family' => $table->{'family'} });
@@ -24,7 +39,9 @@ if ($in{'confirm'}) {
ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1);
print ui_form_start("delete_table.cgi");
print ui_hidden("table", $in{'table'});
print ui_hidden("table", $table_idx);
print ui_hidden("table_family", $table->{'family'});
print ui_hidden("table_name", $table->{'name'});
print "<center><b>",
text('delete_confirm',
"<tt>$table->{'family'} $table->{'name'}</tt>"),
@@ -32,5 +49,5 @@ print "<center><b>",
print ui_submit($text{'delete'}, "confirm");
print "</center>\n";
print ui_form_end();
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});
ui_print_footer("index.cgi?table_family=".urlize($table->{'family'}).
"&table_name=".urlize($table->{'name'}), $text{'index_return'});

View File

@@ -1,25 +0,0 @@
#!/usr/bin/perl
# flush.cgi
# Flush the active nftables ruleset
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
error_setup($text{'flush_err'});
if ($in{'confirm'}) {
my $err = flush_ruleset();
error(text('flush_failed', $err)) if ($err);
webmin_log("flush", "ruleset");
redirect("index.cgi");
}
ui_print_header(undef, $text{'flush_title'}, "", "intro", 1, 1);
print ui_form_start("flush.cgi");
print "<center><b>$text{'flush_confirm'}</b><p>\n";
print ui_submit($text{'flush_ok'}, "confirm");
print "</center>\n";
print ui_form_end();
ui_print_footer("index.cgi", $text{'index_return'});

View File

@@ -46,9 +46,21 @@ if (!@tables) {
$rules_html .= ui_buttons_end();
} else {
# Select table
if (!defined($in{'table'}) || $in{'table'} !~ /^\d+$/ ||
$in{'table'} > $#tables) {
$in{'table'} = 0;
my $found_table;
if (defined($in{'table_family'}) && defined($in{'table_name'})) {
for (my $i = 0; $i <= $#tables; $i++) {
if ($tables[$i]->{'family'} eq $in{'table_family'} &&
$tables[$i]->{'name'} eq $in{'table_name'}) {
$in{'table'} = $i;
$found_table = 1;
last;
}
}
}
if (!$found_table &&
(!defined($in{'table'}) || $in{'table'} !~ /^\d+$/ ||
$in{'table'} > $#tables)) {
$in{'table'} = 0;
}
my @table_opts;
for (my $i = 0; $i <= $#tables; $i++) {
@@ -194,13 +206,12 @@ print "<div id='nftables_ruleset'>\n";
print $rules_html;
print "</div>\n";
if (@tables) {
if (@tables && !$config{'direct'}) {
print ui_hr();
print ui_buttons_start();
print ui_buttons_row("create_table.cgi", $text{'index_table_create'},
$text{'index_table_createdesc'});
print ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'});
print ui_buttons_row("flush.cgi", $text{'index_flush'}, $text{'index_flushdesc'});
print ui_buttons_end();
}

View File

@@ -50,8 +50,6 @@ index_cmovesel=Move Selected
index_radd=Add Rule
index_apply=Apply Configuration
index_applydesc=Click this button to load the saved firewall configuration into the active nftables ruleset.
index_flush=Flush Active Ruleset
index_flushdesc=Click this button to remove all active nftables tables, chains, sets and rules without changing the saved configuration.
index_unapply=Revert Configuration
index_unapplydesc=Click this button to reset the configuration listed above to the one that is currently active.
index_bootup=Activate at Boot
@@ -65,11 +63,6 @@ save=Save
delete=Delete
save_err=Failed to save rule
apply_err=Failed to apply configuration
flush_title=Flush active ruleset
flush_err=Failed to flush active ruleset
flush_failed=Failed to flush active ruleset: $1
flush_confirm=Are you sure you want to remove all active nftables tables, chains, sets and rules?
flush_ok=Flush Ruleset
setup_title=Setup Default Ruleset
setup_header=Create Default Ruleset
setup_desc=This page allows you to create a default nftables ruleset. Select one of the options below and click 'Create'.
@@ -175,6 +168,11 @@ delete_chain_err=Failed to delete chain
delete_chain_failed=Failed to delete chain: <pre>$1</pre>
delete_chain_confirm=Are you sure you want to delete chain $1 from table $2?
delete_chain_inuse=Chain $1 is referenced by $2 rule(s) via jump/goto. Remove those rules first.
delete_chains_err=Failed to delete selected chains
delete_chains_failed=Failed to delete selected chains: <pre>$1</pre>
delete_chains_enone=No chains selected
delete_chains_nochain=No such chain selected: $1
delete_chains_inuse=The selected chains are referenced by $1 rule(s) in chains that are not being deleted. Remove those rules first.
rename_chain_title=Rename chain
rename_chain_header=Rename chain
rename_chain_old=Current name
@@ -227,3 +225,8 @@ delete_set_err=Failed to delete set
delete_set_failed=Failed to delete set: <pre>$1</pre>
delete_set_confirm=Are you sure you want to delete set $1 from table $2?
delete_set_inuse=Set $1 is referenced by $2 rule(s). Remove those rules first.
delete_sets_err=Failed to delete selected sets
delete_sets_failed=Failed to delete selected sets: <pre>$1</pre>
delete_sets_enone=No sets selected
delete_sets_noset=No such set selected: $1
delete_sets_inuse=The selected sets are referenced by $1 rule(s). Remove those rules first.

View File

@@ -28,7 +28,7 @@ if (!defined($rv)) {
}
if ($rv) {
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('move_failed', $err)) if ($err);
webmin_log("move", "rule", undef,
{ 'table' => $table->{'name'},

View File

@@ -997,6 +997,20 @@ foreach my $t (@tables) {
return $rv;
}
# write_configuration(@tables)
# Writes the configuration to the save file
sub write_configuration
{
my (@tables) = @_;
my $out = dump_nftables_save(@tables);
my $file = $config{'save_file'} || "$module_config_directory/nftables.conf";
open_tempfile(my $fh, ">$file");
print_tempfile($fh, $out);
close_tempfile($fh);
return;
}
# save_table(&table)
# Saves a single table to the save file or applies it
sub save_table
@@ -1010,21 +1024,57 @@ my ($table) = @_;
}
# save_configuration(@tables)
# Writes the configuration to the save file. If direct mode is on, applies it.
# Writes the configuration to the save file. If direct mode is on, applies it
# without creating a persistent save file.
sub save_configuration
{
my (@tables) = @_;
my $out = dump_nftables_save(@tables);
my $file = $config{'save_file'} || "$module_config_directory/nftables.conf";
# Write to file
open_tempfile(my $fh, ">$file");
print_tempfile($fh, $out);
close_tempfile($fh);
if ($config{'direct'}) {
return apply_restore($file);
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
print_tempfile($fh, dump_nftables_save(@tables));
close_tempfile($fh);
my $err = apply_restore($tmp);
unlink_file($tmp);
return $err;
}
write_configuration(@tables);
return;
}
# create_table_configuration(&table, @tables)
# Writes the full configuration, but in direct mode creates only the selected table
sub create_table_configuration
{
my ($table, @tables) = @_;
if ($config{'direct'}) {
return apply_table_restore($table, 0);
}
write_configuration(@tables);
return;
}
# save_table_configuration(&table, @tables)
# Writes the full configuration, but in direct mode replaces only the selected table
sub save_table_configuration
{
my ($table, @tables) = @_;
if ($config{'direct'}) {
return apply_table_restore($table, 1);
}
write_configuration(@tables);
return;
}
# delete_table_configuration(&table, @tables)
# Writes the full configuration, but in direct mode deletes only the selected table
sub delete_table_configuration
{
my ($table, @tables) = @_;
if ($config{'direct'}) {
return apply_table_delete($table);
}
write_configuration(@tables);
return;
}
@@ -1043,13 +1093,60 @@ if ($?) {
return;
}
# flush_ruleset()
# Flushes all active nftables tables, chains, sets and rules
sub flush_ruleset
# nft_table_spec(&table)
# Returns a table spec for nft commands
sub nft_table_spec
{
my ($table) = @_;
return $table->{'family'} ? "$table->{'family'} $table->{'name'}" :
$table->{'name'};
}
# apply_table_restore(&table, [replace-existing])
# Applies a single table without touching unrelated tables
sub apply_table_restore
{
my ($table, $replace) = @_;
my $cmd = get_nft_command();
return text('index_ecommand', "<tt>nft</tt>") if (!$cmd);
my $out = backquote_logged("$cmd flush ruleset 2>&1");
my $spec = nft_table_spec($table);
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
print_tempfile($fh, "delete table $spec\n") if ($replace);
print_tempfile($fh, dump_nftables_save($table));
close_tempfile($fh);
my $out = backquote_logged("$cmd -c -f $tmp 2>&1");
if (!$?) {
$out = backquote_logged("$cmd -f $tmp 2>&1");
}
unlink_file($tmp);
if ($?) {
return "<pre>$out</pre>";
}
return;
}
# apply_table_delete(&table)
# Deletes a single active table without touching unrelated tables
sub apply_table_delete
{
my ($table) = @_;
my $cmd = get_nft_command();
return text('index_ecommand', "<tt>nft</tt>") if (!$cmd);
my $spec = nft_table_spec($table);
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
print_tempfile($fh, "delete table $spec\n");
close_tempfile($fh);
my $out = backquote_logged("$cmd -c -f $tmp 2>&1");
if (!$?) {
$out = backquote_logged("$cmd -f $tmp 2>&1");
}
unlink_file($tmp);
if ($?) {
return "<pre>$out</pre>";
}

View File

@@ -58,7 +58,7 @@ if ($is_rename) {
}
}
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('rename_chain_failed', $err)) if ($err);
webmin_log("rename", "chain", $old,
{ 'new' => $name,
@@ -91,7 +91,7 @@ $chain->{'priority'} = $priority;
$chain->{'policy'} = $policy;
$table->{'chains'}->{$name} = $chain;
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('chain_failed', $err)) if ($err);
webmin_log($is_new ? "create" : "modify", "chain", $name,

View File

@@ -165,7 +165,8 @@ if ($in{'delete'}) {
if ($cmd) {
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
print_tempfile($fh, dump_nftables_save(@tables));
print_tempfile($fh, dump_nftables_save(
$config{'direct'} ? ($table) : @tables));
close_tempfile($fh);
my $out = backquote_logged("$cmd -c -f $tmp 2>&1");
unlink_file($tmp);
@@ -175,6 +176,6 @@ if ($in{'delete'}) {
webmin_log("save", $in{'new'} ? "create" : "modify", $rule->{'text'});
}
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('save_failed', $err)) if ($err);
redirect("index.cgi?table=$in{'table'}");

View File

@@ -54,9 +54,9 @@ $set->{'raw_lines'} ||= [ ];
$table->{'sets'}->{$name} = $set;
my $err = save_configuration(@tables);
my $err = save_table_configuration($table, @tables);
error(text('set_failed', $err)) if ($err);
webmin_log($is_new ? "create" : "save", "set", $name,
{ 'table' => $table->{'name'}, 'family' => $table->{'family'} });
redirect("index.cgi?table=$in{'table'}");
redirect("index.cgi?table=$in{'table'}&view=sets");

View File

@@ -27,9 +27,11 @@ if ($in{'action'} eq 'create') {
if ($error) {
error(text('setup_failed', $error));
}
$error = apply_restore();
if ($error) {
error(text('setup_failed', $error));
if (!$config{'direct'}) {
$error = apply_restore();
if ($error) {
error(text('setup_failed', $error));
}
}
webmin_log("setup", "create", $type);
redirect("index.cgi");