Fix to rework nftables management around saved tables

Rework the nftables module so Webmin manages its saved nftables configuration as the source of truth instead of directly editing the live ruleset. Add an active ruleset view for inspecting live tables and importing copies into Webmin-managed config if needed, track managed and imported tables with metadata, and prevent externally managed tables from being overwritten during apply.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Ilia Ross
2026-05-02 19:02:37 +02:00
parent 6825dc11d6
commit 3c9d53109b
14 changed files with 534 additions and 108 deletions

57
nftables/active.cgi Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/perl
# active.cgi
# Show active nftables tables for viewing and import
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%text);
ui_print_header(undef, $text{'active_title'}, "", "intro", 1, 1);
my ($tables, $err) = get_active_nftables_save();
if ($err) {
print text('active_failed', $err);
}
elsif (!@$tables) {
print "<b>$text{'active_none'}</b><p>\n";
}
else {
my @saved_tables = get_nftables_save();
print ui_columns_start(
[ $text{'active_table'}, $text{'active_flags'},
$text{'active_chains'}, $text{'active_sets'},
$text{'active_rules'}, $text{'active_status'},
$text{'index_actions'} ], 100);
foreach my $t (@$tables) {
my $chains = $t->{'chains'} && ref($t->{'chains'}) eq 'HASH' ?
scalar(keys %{$t->{'chains'}}) : 0;
my $sets = $t->{'sets'} && ref($t->{'sets'}) eq 'HASH' ?
scalar(keys %{$t->{'sets'}}) : 0;
my $rules = $t->{'rules'} && ref($t->{'rules'}) eq 'ARRAY' ?
scalar(@{$t->{'rules'}}) : 0;
my $flags = $t->{'flags'} || "-";
my $status_key = active_table_status($t, \@saved_tables);
my $status = $text{'active_'.$status_key};
my $is_saved = table_is_webmin_managed($t, \@saved_tables);
my $table_url = "active_table.cgi?family=".urlize($t->{'family'}).
"&name=".urlize($t->{'name'});
my $actions = $is_saved ? "-" :
ui_link(
"import_table.cgi?family=".urlize($t->{'family'}).
"&name=".urlize($t->{'name'}),
$text{'active_import'});
print ui_columns_row([
ui_link($table_url, html_escape(nft_table_spec($t))),
html_escape($flags),
$chains,
$sets,
$rules,
$status,
$actions,
]);
}
print ui_columns_end();
}
ui_print_footer("index.cgi", $text{'index_return'});

108
nftables/active_table.cgi Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/perl
# active_table.cgi
# Show a read-only active nftables table
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
error_setup($text{'active_table_err'});
my ($tables, $err) = get_active_nftables_save();
error(text('active_failed', $err)) if ($err);
my $table;
foreach my $t (@$tables) {
if ($t->{'family'} eq $in{'family'} && $t->{'name'} eq $in{'name'}) {
$table = $t;
last;
}
}
$table || error($text{'active_table_notable'});
my @saved_tables = get_nftables_save();
my $status_key = active_table_status($table, \@saved_tables);
my $is_saved = table_is_webmin_managed($table, \@saved_tables);
ui_print_header(undef, $text{'active_table_title'}, "", "intro", 1, 1);
print ui_table_start($text{'active_table_summary'}, "width=100%", 2);
print ui_table_row($text{'active_table'}, html_escape(nft_table_spec($table)));
print ui_table_row($text{'active_flags'}, html_escape($table->{'flags'} || "-"));
print ui_table_row($text{'active_status'}, $text{'active_'.$status_key});
print ui_table_end();
if (!$is_saved) {
print ui_buttons_start();
print ui_buttons_row(
"import_table.cgi?family=".urlize($table->{'family'}).
"&name=".urlize($table->{'name'}),
$text{'active_import'}, $text{'active_importdesc'});
print ui_buttons_end();
}
my ($chains_html, $sets_html);
$chains_html .= ui_columns_start(
[ $text{'index_chain_col'}, $text{'index_type'}, $text{'index_hook'},
$text{'index_priority'}, $text{'index_policy_col'}, $text{'index_rules'} ],
100);
foreach my $c (sort keys %{$table->{'chains'}}) {
my $chain_def = $table->{'chains'}->{$c} || { };
my $policy = $chain_def->{'policy'};
my $policy_label = $policy ?
($text{'index_policy_'.lc($policy)} || uc($policy)) : "-";
my @rules = grep { $_->{'chain'} eq $c } @{$table->{'rules'}};
my $rules_html = @rules ?
ui_tag('div',
join("", map {
ui_tag('div', describe_rule($_),
{ 'class' => 'nftables_rule_text' })
} @rules),
{ 'class' => 'nftables_rules_list',
'style' => 'display: grid; row-gap: 0.25em;' }) :
ui_tag('i', $text{'index_rules_none'});
$chains_html .= ui_columns_row([
html_escape($c),
html_escape($chain_def->{'type'} || "-"),
html_escape($chain_def->{'hook'} || "-"),
defined($chain_def->{'priority'}) ?
html_escape($chain_def->{'priority'}) : "-",
html_escape($policy_label),
$rules_html,
]);
}
$chains_html .= ui_columns_end();
$sets_html .= ui_columns_start(
[ $text{'index_set_name'}, $text{'index_set_type'},
$text{'index_set_flags'}, $text{'index_set_elements'} ], 100);
if ($table->{'sets'} && ref($table->{'sets'}) eq 'HASH') {
foreach my $s (sort keys %{$table->{'sets'}}) {
my $set = $table->{'sets'}->{$s} || { };
$sets_html .= ui_columns_row([
html_escape($s),
html_escape($set->{'type'} || "-"),
html_escape($set->{'flags'} || "-"),
html_escape(set_elements_summary($set)),
]);
}
}
$sets_html .= ui_columns_end();
my @tabs = (
[ 'chains', $text{'index_tab_chains'} ],
[ 'sets', $text{'index_tab_sets'} ],
);
my $tab = $in{'view'} && $in{'view'} eq 'sets' ? 'sets' : 'chains';
print ui_hr();
print ui_tabs_start(\@tabs, "view", $tab, 1);
print ui_tabs_start_tab("view", "chains");
print $chains_html;
print ui_tabs_end_tab();
print ui_tabs_start_tab("view", "sets");
print $sets_html;
print ui_tabs_end_tab();
print ui_tabs_end(1);
ui_print_footer("active.cgi", $text{'active_return'});

View File

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

View File

@@ -1,4 +1,3 @@
direct=0
perpage=50
view_condition=1
view_comment=1

View File

@@ -9,4 +9,3 @@ after_apply_cmd=Command to run after applying configuration,3,None
line2=NFTables configuration,11
nft_cmd=Full path to nft command,0
save_file=File to save/edit NFTables rules,3,Use operating system or Webmin default
direct=Directly edit firewall rules instead of save file?,1,1-Yes,0-No

View File

@@ -25,6 +25,15 @@ if ($in{'create'}) {
error($text{'create_edup'});
}
}
my ($active, $active_err) = get_active_nftables_save();
if (!$active_err) {
foreach my $t (@$active) {
if ($t->{'name'} eq $name && $t->{'family'} eq $family &&
table_is_externally_managed($t)) {
error(text('create_eexternal', nft_table_spec($t)));
}
}
}
my $table = { 'name' => $name,
'family' => $family,

102
nftables/import_table.cgi Executable file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/perl
# import_table.cgi
# Import an active nftables table as a Webmin-managed saved table
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
use Storable qw(dclone);
our (%in, %text);
ReadParse();
error_setup($text{'import_err'});
my ($active, $active_err) = get_active_nftables_save();
error(text('active_failed', $active_err)) if ($active_err);
my $source;
foreach my $t (@$active) {
if ($t->{'family'} eq $in{'family'} && $t->{'name'} eq $in{'name'}) {
$source = $t;
last;
}
}
$source || error($text{'import_esource'});
my @tables = get_nftables_save();
if (table_is_webmin_managed($source, \@tables)) {
error(text('import_emanaged', nft_table_spec($source)));
}
if ($in{'import'}) {
my $name = $in{'new_name'};
$name =~ /^\w[\w-]*$/ || error($text{'create_ename'});
foreach my $t (@tables) {
if ($t->{'family'} eq $source->{'family'} && $t->{'name'} eq $name) {
error($text{'create_edup'});
}
}
foreach my $t (@$active) {
if ($t->{'family'} eq $source->{'family'} && $t->{'name'} eq $name &&
table_is_externally_managed($t)) {
error(text('import_eexternal', nft_table_spec($t)));
}
}
my $import = dclone($source);
$import->{'name'} = $name;
delete($import->{'flags'});
push(@tables, $import);
write_configuration(@tables);
register_managed_table($import,
'source' => 'imported',
'imported_from' => nft_table_spec($source),
'imported_from_family' => $source->{'family'},
'imported_from_name' => $source->{'name'},
'imported_at' => time());
webmin_log("import", "table", $source->{'name'},
{ 'family' => $source->{'family'}, 'new' => $name });
redirect("index.cgi?table_family=".urlize($source->{'family'}).
"&table_name=".urlize($name));
}
ui_print_header(undef, $text{'import_title'}, "", "intro", 1, 1);
print ui_form_start("import_table.cgi");
print ui_hidden("family", $source->{'family'});
print ui_hidden("name", $source->{'name'});
print ui_hidden("import", 1);
print ui_table_start($text{'import_header'}, "width=100%", 2);
print ui_table_row($text{'import_source'}, html_escape(nft_table_spec($source)));
print ui_table_row($text{'import_flags'}, html_escape($source->{'flags'} || "-"));
if (table_is_externally_managed($source)) {
print ui_table_row("", $text{'import_external_note'});
}
print ui_table_row($text{'import_new_name'},
ui_textbox("new_name", unique_import_table_name($source, \@tables, $active), 30));
print ui_table_end();
print ui_form_end([ [ undef, $text{'import_ok'} ] ]);
ui_print_footer("active.cgi", $text{'active_return'});
sub unique_import_table_name
{
my ($source, $saved, $active_tables) = @_;
my $base = "imported_".$source->{'name'};
$base =~ s/[^\w-]/_/g;
$base = "imported_table" if ($base !~ /^\w/);
my %used;
foreach my $list ($saved, $active_tables) {
foreach my $t (@$list) {
next if ($t->{'family'} ne $source->{'family'});
$used{$t->{'name'}} = 1;
}
}
my $name = $base;
my $i = 1;
while ($used{$name}) {
$name = $base."_".$i++;
}
return $name;
}

View File

@@ -22,17 +22,6 @@ if (!$cmd) {
exit;
}
# Check if kernel supports it (basic check)
my $out = backquote_command("$cmd list ruleset 2>&1");
if ($? && $out !~ /no ruleset/i) {
# If it fails and not just empty
print text('index_ekernel', "<pre>$out</pre>");
if (!$partial) {
ui_print_footer("/", $text{'index'});
}
exit;
}
# Load tables
my @tables = get_nftables_save();
my $rules_html = "";
@@ -43,6 +32,8 @@ if (!@tables) {
$rules_html .= ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'});
$rules_html .= ui_buttons_row("create_table.cgi", $text{'index_table_create'},
$text{'index_table_createdesc'});
$rules_html .= ui_buttons_row("active.cgi", $text{'index_active'},
$text{'index_activedesc'});
$rules_html .= ui_buttons_end();
} else {
# Select table
@@ -71,7 +62,7 @@ if (!@tables) {
if (!$partial) {
print ui_form_start("index.cgi");
print "<div class='nftables_table_select'>\n";
print text('index_change')," ";
print text('index_change'),"&nbsp;&nbsp;";
print ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1, 0,
"onchange='this.form.querySelector(\"[name=nft_submit]\").click()'");
print ui_submit("", "nft_submit", 0, "style='display:none'");
@@ -267,10 +258,11 @@ if ($partial) {
print $rules_html;
if (@tables && !$config{'direct'}) {
if (@tables) {
print ui_hr();
print ui_buttons_start();
print ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'});
print ui_buttons_row("active.cgi", $text{'index_active'}, $text{'index_activedesc'});
print ui_buttons_end();
}

View File

@@ -16,11 +16,9 @@ sub is_installed
my ($mode) = @_;
return 0 if (&check_nftables());
if ($mode) {
if (!$config{'direct'}) {
my $file = $config{'save_file'} ||
"$module_config_directory/nftables.conf";
return 1 if (!-s $file);
}
my $file = $config{'save_file'} ||
"$module_config_directory/rules.conf";
return 1 if (!-s $file);
return 2;
}
return 1;

View File

@@ -5,7 +5,7 @@ index_editing=Rules file $1
index_ecommand=The NFTables command $1 was not found on your system.
index_ekernel=An error occurred checking your current NFTables configuration: $1
index_header=Existing NFTables Rules
index_change=Show table:
index_change=Managing table
index_table_filter=Packet filtering
index_table_nat=Network address translation
index_table_mangle=Packet alteration
@@ -51,7 +51,9 @@ index_cdeletesel=Delete Selected Chains
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_applydesc=Click this button to replace the saved Webmin-managed tables in the active nftables ruleset.
index_active=View Active Ruleset
index_activedesc=View active nftables tables and import copies into Webmin's 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,6 +67,8 @@ save=Save
delete=Delete
save_err=Failed to save rule
apply_err=Failed to apply configuration
apply_enone=No saved nftables tables were found to apply.
apply_eexternal=Cannot apply configuration because table $1 is currently marked as externally managed.
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'.
@@ -189,6 +193,7 @@ create_failed=Failed to create table: <pre>$1</pre>
create_ename=Table name is invalid
create_edup=A table with that name and family already exists
create_efamily=Invalid table family selected
create_eexternal=Cannot create table $1 because that active table is externally managed.
delete_title=Delete table
delete_err=Failed to delete table
delete_failed=Failed to delete table: <pre>$1</pre>
@@ -228,3 +233,33 @@ 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.
active_title=Active Ruleset
active_failed=Failed to read active nftables ruleset: $1
active_none=No active nftables tables were found.
active_table=Table
active_flags=Flags
active_chains=Chains
active_sets=Sets
active_rules=Rules
active_status=Status
active_webmin=Managed by Webmin
active_external=Externally managed
active_unclaimed=Unclaimed
active_import=Import Copy
active_importdesc=Import this active table as a separate Webmin-managed table.
active_return=active ruleset
active_table_title=Active Table
active_table_err=Failed to view active table
active_table_notable=No such active table selected
active_table_summary=Active table details
import_title=Import active table
import_header=Import table as Webmin-managed copy
import_err=Failed to import active table
import_esource=No such active table selected
import_eexternal=Cannot import as table $1 because that active table is externally managed.
import_emanaged=Table $1 is already managed by Webmin.
import_source=Source table
import_flags=Source flags
import_external_note=This active table is marked as externally managed. Importing creates a separate Webmin-managed copy and does not change the active source table.
import_new_name=New table name
import_ok=Import Copy

View File

@@ -29,16 +29,11 @@ return text('index_ecommand', "<tt>nft</tt>");
sub get_nftables_save
{
my ($file) = @_;
my $cmd = get_nft_command();
if (!$file) {
if ($config{'direct'}) {
return ( ) if (!$cmd);
$file = "$cmd list ruleset |";
} else {
$file = $config{'save_file'} || "$module_config_directory/nftables.conf";
}
$file = $config{'save_file'} || "$module_config_directory/rules.conf";
}
return ( ) if (!$file);
return ( ) if ($file !~ /\|\s*$/ && !-r $file);
my @rv;
my $table;
@@ -129,6 +124,9 @@ for(my $i=0; $i<@lines; $i++) {
push(@rv, $table);
$chain = undef;
}
elsif ($line =~ /^\s*flags\s+(.+?)\s*;?$/ && $table && !$chain) {
$table->{'flags'} = $1;
}
elsif ($line =~ /^\s*set\s+(\S+)\s+\{/) {
# Start of a set
if ($table) {
@@ -186,6 +184,25 @@ for(my $i=0; $i<@lines; $i++) {
return @rv;
}
# get_active_nftables_save()
# Returns an array ref of tables from the active ruleset, and an optional error
sub get_active_nftables_save
{
my $cmd = get_nft_command();
return (undef, text('index_ecommand', "<tt>nft</tt>")) if (!$cmd);
my $out = backquote_command("$cmd list ruleset 2>&1");
return (undef, "<pre>$out</pre>") if ($?);
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
print_tempfile($fh, $out);
close_tempfile($fh);
my @tables = get_nftables_save($tmp);
unlink_file($tmp);
return (\@tables, undef);
}
sub tokenize_nft_rule
{
my ($line) = @_;
@@ -1003,11 +1020,12 @@ sub write_configuration
{
my (@tables) = @_;
my $out = dump_nftables_save(@tables);
my $file = $config{'save_file'} || "$module_config_directory/nftables.conf";
my $file = $config{'save_file'} || "$module_config_directory/rules.conf";
open_tempfile(my $fh, ">$file");
print_tempfile($fh, $out);
close_tempfile($fh);
sync_managed_metadata(@tables);
return;
}
@@ -1024,69 +1042,81 @@ my ($table) = @_;
}
# save_configuration(@tables)
# Writes the configuration to the save file. If direct mode is on, applies it
# without creating a persistent save file.
# Writes the configuration to the save file
sub save_configuration
{
my (@tables) = @_;
if ($config{'direct'}) {
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
# Writes the full configuration after creating a 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
# Writes the full configuration after changing a 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
# Writes the full configuration after deleting a table
sub delete_table_configuration
{
my ($table, @tables) = @_;
if ($config{'direct'}) {
return apply_table_delete($table);
}
write_configuration(@tables);
return;
}
# apply_restore([file])
# Applies the configuration from the save file
# Applies Webmin-managed tables from the save file
sub apply_restore
{
my ($file) = @_;
$file ||= $config{'save_file'} || "$module_config_directory/nftables.conf";
$file ||= $config{'save_file'} || "$module_config_directory/rules.conf";
my $cmd = get_nft_command();
return text('index_ecommand', "<tt>nft</tt>") if (!$cmd);
my $out = backquote_logged("$cmd -f $file 2>&1");
my @tables = get_nftables_save($file);
return text('apply_enone') if (!@tables);
my ($active, $active_err) = get_active_nftables_save();
return $active_err if ($active_err);
my %active;
foreach my $t (@$active) {
$active{table_key($t)} = $t;
}
foreach my $t (@tables) {
my $active_table = $active{table_key($t)};
if ($active_table && table_is_externally_managed($active_table)) {
return text('apply_eexternal', nft_table_spec($t));
}
}
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
foreach my $t (@tables) {
print_tempfile($fh, "delete table ".nft_table_spec($t)."\n")
if ($active{table_key($t)});
}
print_tempfile($fh, dump_nftables_save(@tables));
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>";
}
@@ -1102,54 +1132,148 @@ 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
# table_key(&table)
# Returns a stable key for a table
sub table_key
{
my ($table, $replace) = @_;
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") 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");
my ($table) = @_;
return ($table->{'family'} || '')."\0".($table->{'name'} || '');
}
unlink_file($tmp);
if ($?) {
return "<pre>$out</pre>";
# table_is_externally_managed(&table)
# Returns true if an active table is marked as owned by another program
sub table_is_externally_managed
{
my ($table) = @_;
return 0 if (!$table || !$table->{'flags'});
my %flags = map { $_ => 1 } grep { $_ ne '' } split(/[,\s]+/, $table->{'flags'});
return $flags{'owner'} || $flags{'persist'};
}
# table_is_webmin_managed(&table, [&saved_tables])
# Returns true if an active table is present in Webmin's saved config
sub table_is_webmin_managed
{
my ($table, $saved_tables) = @_;
if (!$saved_tables) {
my @tables = get_nftables_save();
$saved_tables = \@tables;
}
foreach my $t (@$saved_tables) {
return 1 if (table_key($t) eq table_key($table));
}
return 0;
}
# active_table_status(&table, [&saved_tables])
# Returns webmin, external or unclaimed for an active table
sub active_table_status
{
my ($table, $saved_tables) = @_;
return "external" if (table_is_externally_managed($table));
return "webmin" if (table_is_webmin_managed($table, $saved_tables));
return "unclaimed";
}
# managed_metadata_file()
# Returns the path to Webmin's nftables metadata file
sub managed_metadata_file
{
return "$module_config_directory/managed.json";
}
# managed_table_key(&table)
# Returns the key used for managed table metadata
sub managed_table_key
{
my ($table) = @_;
return nft_table_spec($table);
}
# read_managed_metadata()
# Returns metadata about tables managed by this module
sub read_managed_metadata
{
my $file = managed_metadata_file();
return { 'tables' => { } } if (!-r $file);
my $json = read_file_contents($file);
my $meta = eval { convert_from_json($json) };
if (!$meta || ref($meta) ne 'HASH') {
$meta = { };
}
if (!$meta->{'tables'} || ref($meta->{'tables'}) ne 'HASH') {
$meta->{'tables'} = { };
}
return $meta;
}
# write_managed_metadata(&metadata)
# Writes metadata about tables managed by this module
sub write_managed_metadata
{
my ($meta) = @_;
$meta ||= { };
$meta->{'tables'} = { } if (ref($meta->{'tables'}) ne 'HASH');
my $file = managed_metadata_file();
lock_file($file);
write_file_contents($file, convert_to_json($meta, 1));
unlock_file($file);
return;
}
# apply_table_delete(&table)
# Deletes a single active table without touching unrelated tables
sub apply_table_delete
# sync_managed_metadata(@tables)
# Keeps managed metadata aligned with the saved Webmin config
sub sync_managed_metadata
{
my (@tables) = @_;
my $meta = read_managed_metadata();
my %old = %{$meta->{'tables'}};
my %new;
foreach my $t (@tables) {
my $key = managed_table_key($t);
my %entry = $old{$key} && ref($old{$key}) eq 'HASH' ?
%{$old{$key}} : ( );
$entry{'family'} = $t->{'family'};
$entry{'name'} = $t->{'name'};
$entry{'source'} ||= 'webmin';
$entry{'managed_at'} ||= time();
$new{$key} = \%entry;
}
$meta->{'tables'} = \%new;
write_managed_metadata($meta);
return;
}
# register_managed_table(&table, %info)
# Adds or updates metadata for a Webmin-managed table
sub register_managed_table
{
my ($table, %info) = @_;
my $meta = read_managed_metadata();
my $key = managed_table_key($table);
my %entry = $meta->{'tables'}->{$key} &&
ref($meta->{'tables'}->{$key}) eq 'HASH' ?
%{$meta->{'tables'}->{$key}} : ( );
foreach my $k (keys %info) {
$entry{$k} = $info{$k};
}
$entry{'family'} = $table->{'family'};
$entry{'name'} = $table->{'name'};
$entry{'source'} ||= 'webmin';
$entry{'managed_at'} ||= time();
$meta->{'tables'}->{$key} = \%entry;
write_managed_metadata($meta);
return;
}
# unregister_managed_table(&table)
# Removes metadata for a table no longer managed by this module
sub unregister_managed_table
{
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>";
}
my $meta = read_managed_metadata();
delete($meta->{'tables'}->{managed_table_key($table)});
write_managed_metadata($meta);
return;
}

View File

@@ -5,7 +5,7 @@
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text, %config);
our (%in, %text);
ReadParse();
error_setup($text{'save_err'});
my @tables = get_nftables_save();
@@ -165,8 +165,7 @@ if ($in{'delete'}) {
if ($cmd) {
my $tmp = tempname();
open_tempfile(my $fh, ">$tmp");
print_tempfile($fh, dump_nftables_save(
$config{'direct'} ? ($table) : @tables));
print_tempfile($fh, dump_nftables_save(@tables));
close_tempfile($fh);
my $out = backquote_logged("$cmd -c -f $tmp 2>&1");
unlink_file($tmp);

View File

@@ -5,7 +5,7 @@
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text, %config);
our (%in, %text);
ReadParse();
if ($in{'action'} eq 'create') {
my $type = $in{'type'};
@@ -27,11 +27,9 @@ if ($in{'action'} eq 'create') {
if ($error) {
error(text('setup_failed', $error));
}
if (!$config{'direct'}) {
$error = apply_restore();
if ($error) {
error(text('setup_failed', $error));
}
$error = apply_restore();
if ($error) {
error(text('setup_failed', $error));
}
webmin_log("setup", "create", $type);
redirect("index.cgi");

View File

@@ -128,6 +128,14 @@ is($chain->{policy}, 'drop', 'chain policy');
my $ruleset_prio = "$bindir/rulesets/firewalld-priority.nft";
my @tables_prio = get_nftables_save($ruleset_prio);
ok(@tables_prio == 1, 'firewalld priority table count');
is($tables_prio[0]->{flags}, 'owner,persist', 'firewalld table flags');
ok(table_is_externally_managed($tables_prio[0]), 'firewalld table is externally managed');
is(active_table_status($tables_prio[0], []), 'external',
'external active table status');
is(active_table_status({ family => 'inet', name => 'filter' }, [ $t ]), 'webmin',
'saved active table status');
is(active_table_status({ family => 'inet', name => 'loose' }, []), 'unclaimed',
'unclaimed active table status');
my $fw_chain = $tables_prio[0]->{chains}->{filter_INPUT};
ok($fw_chain, 'firewalld priority chain present');
is($fw_chain->{type}, 'filter', 'firewalld priority chain type');