mirror of
https://github.com/webmin/webmin.git
synced 2026-05-06 15:20:29 +01:00
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:
57
nftables/active.cgi
Executable file
57
nftables/active.cgi
Executable 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
108
nftables/active_table.cgi
Executable 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'});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
direct=0
|
||||
perpage=50
|
||||
view_condition=1
|
||||
view_comment=1
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
102
nftables/import_table.cgi
Executable 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;
|
||||
}
|
||||
@@ -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')," ";
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user