diff --git a/nftables/active.cgi b/nftables/active.cgi new file mode 100755 index 000000000..4f7843b90 --- /dev/null +++ b/nftables/active.cgi @@ -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 "$text{'active_none'}

\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'}); diff --git a/nftables/active_table.cgi b/nftables/active_table.cgi new file mode 100755 index 000000000..082aada68 --- /dev/null +++ b/nftables/active_table.cgi @@ -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'}); diff --git a/nftables/apply.cgi b/nftables/apply.cgi index f00267123..4fa246ec1 100755 --- a/nftables/apply.cgi +++ b/nftables/apply.cgi @@ -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); diff --git a/nftables/config b/nftables/config index 1116a423b..455fdd0d4 100644 --- a/nftables/config +++ b/nftables/config @@ -1,4 +1,3 @@ -direct=0 perpage=50 view_condition=1 view_comment=1 diff --git a/nftables/config.info b/nftables/config.info index 2c5b4ca98..e97a45585 100644 --- a/nftables/config.info +++ b/nftables/config.info @@ -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 diff --git a/nftables/create_table.cgi b/nftables/create_table.cgi index 4c5718354..e6842d749 100755 --- a/nftables/create_table.cgi +++ b/nftables/create_table.cgi @@ -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, diff --git a/nftables/import_table.cgi b/nftables/import_table.cgi new file mode 100755 index 000000000..fd1dbd8ee --- /dev/null +++ b/nftables/import_table.cgi @@ -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; +} diff --git a/nftables/index.cgi b/nftables/index.cgi index ea4cb57c1..15ccf82ba 100755 --- a/nftables/index.cgi +++ b/nftables/index.cgi @@ -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', "

$out
"); - 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 "
\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(); } diff --git a/nftables/install_check.pl b/nftables/install_check.pl index 934cb882d..87ae0fd1d 100644 --- a/nftables/install_check.pl +++ b/nftables/install_check.pl @@ -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; diff --git a/nftables/lang/en b/nftables/lang/en index 92da50ba6..5fb0b0ddd 100644 --- a/nftables/lang/en +++ b/nftables/lang/en @@ -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:
$1
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:
$1
@@ -228,3 +233,33 @@ delete_sets_failed=Failed to delete selected sets:
$1
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 diff --git a/nftables/nftables-lib.pl b/nftables/nftables-lib.pl index 4e5475636..7de4e04c4 100644 --- a/nftables/nftables-lib.pl +++ b/nftables/nftables-lib.pl @@ -29,16 +29,11 @@ return text('index_ecommand', "nft"); 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', "nft")) if (!$cmd); + +my $out = backquote_command("$cmd list ruleset 2>&1"); +return (undef, "
$out
") 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', "nft") 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 "
$out
"; } @@ -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', "nft") 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 "
$out
"; + +# 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', "nft") 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 "
$out
"; -} +my $meta = read_managed_metadata(); +delete($meta->{'tables'}->{managed_table_key($table)}); +write_managed_metadata($meta); return; } diff --git a/nftables/save_rule.cgi b/nftables/save_rule.cgi index e15ce745e..7efc803e2 100755 --- a/nftables/save_rule.cgi +++ b/nftables/save_rule.cgi @@ -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); diff --git a/nftables/setup.cgi b/nftables/setup.cgi index f0f6d09c0..c7809f7de 100644 --- a/nftables/setup.cgi +++ b/nftables/setup.cgi @@ -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"); diff --git a/nftables/t/run-tests.t b/nftables/t/run-tests.t index ace962a93..f56af5d1e 100755 --- a/nftables/t/run-tests.t +++ b/nftables/t/run-tests.t @@ -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');