From 12eff54277baceddbe18e95c75fd9be7bfeca9e3 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 3 May 2026 13:59:58 +0200 Subject: [PATCH] Add nftables apply-needed header action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Note: Track saved nftables configuration changes with Apache-style config/apply timestamp flags, expose the standard restart.cgi header action for themes, and use it as the single apply endpoint. The button expands to “Apply Changes” when saved rules need applying, while the existing Apply Configuration action now routes through restart.cgi and clears the pending state after a successful apply. --- nftables/active.cgi | 3 +- nftables/active_table.cgi | 3 +- nftables/clear_table.cgi | 3 +- nftables/create_table.cgi | 3 +- nftables/delete_table.cgi | 5 ++- nftables/edit_chain.cgi | 7 ++-- nftables/edit_rule.cgi | 6 ++- nftables/edit_set.cgi | 6 ++- nftables/import_table.cgi | 3 +- nftables/index.cgi | 5 ++- nftables/lang/en | 1 + nftables/nftables-lib.pl | 59 ++++++++++++++++++++++++++++- nftables/rename_chain.cgi | 4 +- nftables/{apply.cgi => restart.cgi} | 7 ++-- nftables/setup.cgi | 3 +- 15 files changed, 96 insertions(+), 22 deletions(-) rename nftables/{apply.cgi => restart.cgi} (60%) diff --git a/nftables/active.cgi b/nftables/active.cgi index e8472dc68..ed728396b 100755 --- a/nftables/active.cgi +++ b/nftables/active.cgi @@ -7,7 +7,8 @@ use strict; use warnings; our (%text); -ui_print_header(undef, $text{'active_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'active_title'}, "", "intro", 1, 1, + undef, restart_button()); my ($tables, $err) = get_active_nftables_save(); if ($err) { diff --git a/nftables/active_table.cgi b/nftables/active_table.cgi index 082aada68..5065161fa 100755 --- a/nftables/active_table.cgi +++ b/nftables/active_table.cgi @@ -24,7 +24,8 @@ 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); +ui_print_header(undef, $text{'active_table_title'}, "", "intro", 1, 1, + undef, restart_button()); print ui_table_start($text{'active_table_summary'}, "width=100%", 2); print ui_table_row($text{'active_table'}, html_escape(nft_table_spec($table))); diff --git a/nftables/clear_table.cgi b/nftables/clear_table.cgi index cff049737..76957e6a4 100755 --- a/nftables/clear_table.cgi +++ b/nftables/clear_table.cgi @@ -30,7 +30,8 @@ if ($in{'confirm'}) { return; } -ui_print_header(undef, $text{'clear_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'clear_title'}, "", "intro", 1, 1, + undef, restart_button()); print "
\n"; print ui_form_start("clear_table.cgi"); print ui_hidden("family", $table->{'family'}); diff --git a/nftables/create_table.cgi b/nftables/create_table.cgi index 0456cc481..5a7122d9c 100755 --- a/nftables/create_table.cgi +++ b/nftables/create_table.cgi @@ -50,7 +50,8 @@ if ($in{'create'}) { return; } -ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1, + undef, restart_button()); print ui_form_start("create_table.cgi"); print ui_hidden("create", 1); diff --git a/nftables/delete_table.cgi b/nftables/delete_table.cgi index a0470d69e..70a56435b 100755 --- a/nftables/delete_table.cgi +++ b/nftables/delete_table.cgi @@ -29,18 +29,21 @@ else { $table || error($text{'delete_notable'}); if ($in{'confirm'}) { + my $needs_apply = needs_config_restart(); splice(@tables, $table_idx, 1); my $err = delete_table_configuration($table, @tables); error(text('delete_failed', $err)) if ($err); $err = delete_active_table($table); error(text('delete_failed', $err)) if ($err); + restart_last_restart_time() if (!$needs_apply); webmin_log("delete", "table", $table->{'name'}, { 'family' => $table->{'family'} }); redirect("index.cgi"); return; } -ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1, + undef, restart_button()); print "
\n"; print ui_form_start("delete_table.cgi"); print ui_hidden("table", $table_idx); diff --git a/nftables/edit_chain.cgi b/nftables/edit_chain.cgi index f7580b69e..41fde4db6 100644 --- a/nftables/edit_chain.cgi +++ b/nftables/edit_chain.cgi @@ -17,12 +17,14 @@ my $chain_name = ""; my $is_new = $in{'new'} ? 1 : 0; if ($is_new) { - ui_print_header(undef, $text{'chain_title_new'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'chain_title_new'}, "", "intro", 1, 1, + undef, restart_button()); } else { $chain_name = $in{'chain'}; $chain = $table->{'chains'}->{$chain_name}; $chain || error($text{'chain_nochain'}); - ui_print_header(undef, $text{'chain_title_edit'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'chain_title_edit'}, "", "intro", 1, 1, + undef, restart_button()); } my @type_opts = ( @@ -84,4 +86,3 @@ if (window.addEventListener) { EOF ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); - diff --git a/nftables/edit_rule.cgi b/nftables/edit_rule.cgi index f531a2964..fc849b55b 100755 --- a/nftables/edit_rule.cgi +++ b/nftables/edit_rule.cgi @@ -47,10 +47,12 @@ sub split_multi_value } if ($in{'new'}) { - ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1, + undef, restart_button()); $rule = { 'chain' => $in{'chain'} }; } else { - ui_print_header(undef, $text{'edit_title_edit'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'edit_title_edit'}, "", "intro", 1, 1, + undef, restart_button()); $rule = $table->{'rules'}->[$in{'idx'}]; } if ($table && $rule->{'chain'}) { diff --git a/nftables/edit_set.cgi b/nftables/edit_set.cgi index bccd95787..c34caff5b 100755 --- a/nftables/edit_set.cgi +++ b/nftables/edit_set.cgi @@ -17,13 +17,15 @@ my $set_name = ""; my $is_new = $in{'new'} ? 1 : 0; if ($is_new) { - ui_print_header(undef, $text{'set_title_new'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'set_title_new'}, "", "intro", 1, 1, + undef, restart_button()); } else { $set_name = $in{'set'}; $set = $table->{'sets'}->{$set_name}; $set || error($text{'set_noset'}); - ui_print_header(undef, $text{'set_title_edit'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'set_title_edit'}, "", "intro", 1, 1, + undef, restart_button()); } my $elements_text = set_elements_text($set); diff --git a/nftables/import_table.cgi b/nftables/import_table.cgi index 99d012d36..3dc5a3d26 100755 --- a/nftables/import_table.cgi +++ b/nftables/import_table.cgi @@ -60,7 +60,8 @@ if ($in{'import'}) { return; } -ui_print_header(undef, $text{'import_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'import_title'}, "", "intro", 1, 1, + undef, restart_button()); print ui_form_start("import_table.cgi"); print ui_hidden("family", $source->{'family'}); diff --git a/nftables/index.cgi b/nftables/index.cgi index 0bdae9aab..36f36ab56 100755 --- a/nftables/index.cgi +++ b/nftables/index.cgi @@ -9,7 +9,8 @@ our (%in, %text, %config); ReadParse(); my $partial = $in{'partial'}; if (!$partial) { - ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1); + ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, + undef, restart_button()); } # Check for nft command @@ -279,7 +280,7 @@ print $rules_html; 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("restart.cgi", $text{'index_apply'}, $text{'index_applydesc'}); print ui_buttons_row("active.cgi", $text{'index_active'}, $text{'index_activedesc'}); print ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}); print ui_buttons_end(); diff --git a/nftables/lang/en b/nftables/lang/en index 5e99a4e03..4b837c2f1 100644 --- a/nftables/lang/en +++ b/nftables/lang/en @@ -50,6 +50,7 @@ index_cdeletesel=Delete Selected Chains index_cmovesel=Move Selected index_radd=Add Rule index_apply=Apply Configuration +index_apply_changes=Apply Changes 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. diff --git a/nftables/nftables-lib.pl b/nftables/nftables-lib.pl index fcd2b56e0..d64428852 100644 --- a/nftables/nftables-lib.pl +++ b/nftables/nftables-lib.pl @@ -5,8 +5,63 @@ BEGIN { push(@INC, ".."); }; ## no critic use WebminCore; use strict; use warnings; -our (%config, $module_config_directory); +our (%config, $module_config_directory, $module_var_directory); +our ($last_config_change_flag, $last_restart_time_flag); init_config(); +$last_config_change_flag = $module_var_directory."/config-flag"; +$last_restart_time_flag = $module_var_directory."/restart-flag"; + +# restart_button() +# Returns HTML for the header apply button +sub restart_button +{ +my @tables = get_nftables_save(); +return "" if (!@tables); +my $args = "redir=".urlize(this_url()); +my $needs = needs_config_restart(); +my $apply = text('index_apply_changes'); +my $label = $needs ? "$apply" : $apply; +my $url = "restart.cgi?$args"; +$url .= "&newconfig=1" if ($needs); +return ui_link($url, $label); +} + +# this_url() +# Returns the URL in the nftables module for the current script +sub this_url +{ +my $url = $ENV{'SCRIPT_NAME'} || ""; +my $query = $ENV{'QUERY_STRING'} || ""; +$url .= "?$query" if ($query ne ""); +return $url; +} + +# update_last_config_change() +# Updates the flag file indicating when the saved config was changed +sub update_last_config_change +{ +open_tempfile(my $fh, ">$last_config_change_flag", 0, 1); +close_tempfile($fh); +} + +# restart_last_restart_time() +# Updates the flag file indicating when the saved config was applied +sub restart_last_restart_time +{ +open_tempfile(my $fh, ">$last_restart_time_flag", 0, 1); +close_tempfile($fh); +} + +# needs_config_restart() +# Returns 1 if saved config changes still need to be applied +sub needs_config_restart +{ +my @cst = stat($last_config_change_flag); +my @rst = stat($last_restart_time_flag); +return 0 if (!@cst); +return 1 if (!@rst); +return $cst[9] > $rst[9] ? 1 : 0; +} # get_nft_command() # Returns the configured nft command path, or finds it in PATH @@ -1198,6 +1253,7 @@ open_tempfile(my $fh, ">$file"); print_tempfile($fh, $out); close_tempfile($fh); sync_managed_metadata(@tables); +update_last_config_change(); return; } @@ -1294,6 +1350,7 @@ unlink_file($tmp); if ($?) { return "
$out
"; } +restart_last_restart_time(); return; } diff --git a/nftables/rename_chain.cgi b/nftables/rename_chain.cgi index 4c82e21c3..13ef7fe26 100644 --- a/nftables/rename_chain.cgi +++ b/nftables/rename_chain.cgi @@ -15,7 +15,8 @@ $table || error($text{'chain_notable'}); my $chain = $table->{'chains'}->{$in{'chain'}}; $chain || error($text{'chain_nochain'}); -ui_print_header(undef, $text{'rename_chain_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'rename_chain_title'}, "", "intro", 1, 1, + undef, restart_button()); print ui_form_start("save_chain.cgi"); print ui_hidden("table", $in{'table'}); print ui_hidden("rename", 1); @@ -30,4 +31,3 @@ print ui_table_end(); print ui_form_end([ [ undef, $text{'rename_chain_ok'} ] ]); ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); - diff --git a/nftables/apply.cgi b/nftables/restart.cgi similarity index 60% rename from nftables/apply.cgi rename to nftables/restart.cgi index 4fa246ec1..0ae1f1886 100755 --- a/nftables/apply.cgi +++ b/nftables/restart.cgi @@ -1,6 +1,6 @@ #!/usr/bin/perl -# apply.cgi -# Apply the current configuration +# restart.cgi +# Apply saved nftables configuration from the header action require './nftables-lib.pl'; ## no critic use strict; @@ -12,4 +12,5 @@ error_setup($text{'apply_err'}); my $err = apply_restore(); error($err) if ($err); -redirect("index.cgi"); +webmin_log("apply"); +redirect($in{'redir'} || "index.cgi"); diff --git a/nftables/setup.cgi b/nftables/setup.cgi index 0b0f287ab..8c6867361 100644 --- a/nftables/setup.cgi +++ b/nftables/setup.cgi @@ -47,7 +47,8 @@ if ($in{'action'} eq 'create') { return; } -ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1, + undef, restart_button()); print ui_form_start("setup.cgi"); print ui_hidden("action", "create");