From d89f1ee1de66170923d5d8f9339fa61e4d996c3b Mon Sep 17 00:00:00 2001 From: Super User Date: Thu, 22 Jan 2026 00:23:17 -0600 Subject: [PATCH 01/13] Initial commit --- apply.cgi | 16 ++++ config.info | 12 +++ edit_rule.cgi | 90 +++++++++++++++++++ index.cgi | 107 ++++++++++++++++++++++ install_check.pl | 18 ++++ lang/en | 77 ++++++++++++++++ module.info | 5 ++ nftables-lib.pl | 227 +++++++++++++++++++++++++++++++++++++++++++++++ save_rule.cgi | 65 ++++++++++++++ setup.cgi | 156 ++++++++++++++++++++++++++++++++ 10 files changed, 773 insertions(+) create mode 100755 apply.cgi create mode 100644 config.info create mode 100755 edit_rule.cgi create mode 100755 index.cgi create mode 100644 install_check.pl create mode 100644 lang/en create mode 100644 module.info create mode 100644 nftables-lib.pl create mode 100755 save_rule.cgi create mode 100644 setup.cgi diff --git a/apply.cgi b/apply.cgi new file mode 100755 index 000000000..f01eccebc --- /dev/null +++ b/apply.cgi @@ -0,0 +1,16 @@ +#!/usr/bin/perl +# apply.cgi +# Apply the current configuration + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text); +&ReadParse(); +&error_setup($text{'apply_err'}); + +my @tables = &get_nftables_save(); +my $err = &apply_restore(); +&error($err) if ($err); + +&redirect("index.cgi"); diff --git a/config.info b/config.info new file mode 100644 index 000000000..2c5b4ca98 --- /dev/null +++ b/config.info @@ -0,0 +1,12 @@ +line0=Configurable global options,11 +perpage=Number of rules to display per page,3,Default (50) +view_condition=Display condition in rules list?,1,1-Yes,0-No +view_comment=Display comment in rules list?,1,1-Yes,0-No +before_cmd=Command to run before changing rules,3,None +after_cmd=Command to run after changing rules,3,None +before_apply_cmd=Command to run before applying configuration,3,None +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/edit_rule.cgi b/edit_rule.cgi new file mode 100755 index 000000000..1c33919db --- /dev/null +++ b/edit_rule.cgi @@ -0,0 +1,90 @@ +#!/usr/bin/perl +# edit_rule.cgi +# Display a form for creating or editing a rule + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text, %config); +&ReadParse(); +my @tables = &get_nftables_save(); +my $table = $tables[$in{'table'}]; +my $rule; +my $chain_def; +my $chain_hook; + +if ($in{'new'}) { + &ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1); + $rule = { 'chain' => $in{'chain'} }; +} else { + &ui_print_header(undef, $text{'edit_title_edit'}, "", "intro", 1, 1); + $rule = $table->{'rules'}->[$in{'idx'}]; +} +if ($table && $rule->{'chain'}) { + $chain_def = $table->{'chains'}->{$rule->{'chain'}}; + $chain_hook = $chain_def ? $chain_def->{'hook'} : undef; +} + +print &ui_form_start("save_rule.cgi"); +print &ui_hidden("table", $in{'table'}); +print &ui_hidden("idx", $in{'idx'}); +print &ui_hidden("chain", $rule->{'chain'}); +print &ui_hidden("new", $in{'new'}); + +print &ui_table_start($text{'edit_header'}, "width=100%", 2); + +# Rule comment +print &ui_table_row($text{'edit_comment'}, + &ui_textbox("comment", $rule->{'comment'}, 50)); + +# Action +print &ui_table_row($text{'edit_action'}, + &ui_select("action", $rule->{'action'}, + [ + [ "accept", $text{'index_accept'} ], + [ "drop", $text{'index_drop'} ], + [ "reject", $text{'index_reject'} ], + ])); + +# Protocol +print &ui_table_row($text{'edit_proto'}, + &ui_select("proto", $rule->{'proto'}, + [ + [ "tcp", "TCP" ], + [ "udp", "UDP" ], + [ "icmp", "ICMP" ], + ])); + +# Destination port +print &ui_table_row($text{'edit_dport'}, + &ui_textbox("dport", $rule->{'dport'}, 10)); + +if ($chain_hook && $chain_hook eq 'input') { + # Incoming interface + print &ui_table_row($text{'edit_iif'}, + &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); +} +elsif ($chain_hook && $chain_hook eq 'output') { + # Outgoing interface + print &ui_table_row($text{'edit_oif'}, + &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); +} +else { + # Forward or unknown chain - allow both + print &ui_table_row($text{'edit_iif'}, + &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); + print &ui_table_row($text{'edit_oif'}, + &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); +} + +print &ui_table_end(); +my @buttons; +if ($in{'new'}) { + push(@buttons, [ undef, $text{'create'} ]); +} else { + push(@buttons, [ undef, $text{'save'} ]); + push(@buttons, [ 'delete', $text{'delete'} ]); +} +print &ui_form_end(\@buttons); + +&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/index.cgi b/index.cgi new file mode 100755 index 000000000..8c0724ca7 --- /dev/null +++ b/index.cgi @@ -0,0 +1,107 @@ +#!/usr/bin/perl +# index.cgi +# Display current nftables configuration + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text, %config); +&ReadParse(); +&ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1); + +# Check for nft command +my $cmd = $config{'nft_cmd'} || &has_command("nft"); +if (!$cmd) { + print &text('index_ecommand', "nft"); + &ui_print_footer("/", $text{'index'}); + 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
"); + &ui_print_footer("/", $text{'index'}); + exit; +} + +# Load tables +my @tables = &get_nftables_save(); + +if (!@tables) { + print "$text{'index_none'}

\n"; + print &ui_buttons_start(); + print &ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}); + print &ui_buttons_end(); +} else { + # Select table + if (!defined($in{'table'}) || $in{'table'} !~ /^\d+$/ || + $in{'table'} > $#tables) { + $in{'table'} = 0; + } + my @table_opts; + for (my $i = 0; $i <= $#tables; $i++) { + my $t = $tables[$i]; + push(@table_opts, [ $i, $t->{'family'}." ".$t->{'name'} ]); + } + + print &ui_form_start("index.cgi"); + print &text('index_change')," "; + print &ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1); + print &ui_form_end(); + + # Identify current table + my $curr = $tables[$in{'table'}]; + + if ($curr) { + # Show chains and rules + print &ui_hr(); + print &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 %{$curr->{'chains'}}) { + my $chain_def = $curr->{'chains'}->{$c} || { }; + my $policy = $chain_def->{'policy'}; + my $policy_label = $policy ? + ($text{'index_policy_'.lc($policy)} || uc($policy)) : "-"; + my @rules = grep { $_->{'chain'} eq $c } @{$curr->{'rules'}}; + my $rules_html; + if (@rules) { + my @rows; + foreach my $r (@rules) { + my $desc = &describe_rule($r); + push(@rows, &ui_link( + "edit_rule.cgi?table=$in{'table'}&chain=". + &urlize($c)."&idx=$r->{'index'}", + $desc)); + } + $rules_html = join("
", @rows); + } else { + $rules_html = "$text{'index_rules_none'}"; + } + $rules_html .= "
". + &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + &urlize($c)."&new=1", $text{'index_radd'}); + + print &ui_columns_row([ + $c, + $chain_def->{'type'} || "-", + $chain_def->{'hook'} || "-", + defined($chain_def->{'priority'}) ? $chain_def->{'priority'} : "-", + $policy_label, + $rules_html + ]); + } + print &ui_columns_end(); + } +} + +print &ui_hr(); +print &ui_buttons_start(); +print &ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'}); +print &ui_buttons_end(); + +&ui_print_footer("/", $text{'index'}); diff --git a/install_check.pl b/install_check.pl new file mode 100644 index 000000000..78dd5a6f9 --- /dev/null +++ b/install_check.pl @@ -0,0 +1,18 @@ +# install_check.pl +use strict; +use warnings; + +our %config; +do './nftables-lib.pl'; + +# is_installed(mode) +# For mode 1, returns 2 if the server is installed and configured for use by +# Webmin, 1 if installed but not configured, or 0 otherwise. +# For mode 0, returns 1 if installed, 0 if not +sub is_installed { + my ($mode) = @_; + +# Available config file in the default location? + return 0 if (!-x $config{'nft_cmd'}); + return $mode ? 2 : 0; +} diff --git a/lang/en b/lang/en new file mode 100644 index 000000000..2f4565554 --- /dev/null +++ b/lang/en @@ -0,0 +1,77 @@ +index_title=NFTables Firewall +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 IP table: +index_table_filter=Packet filtering +index_table_nat=Network address translation +index_table_mangle=Packet alteration +index_chain=Chain $1 +index_action=Action +index_desc=Condition +index_comm=Comment +index_move=Move +index_add=Add +index_none=No rules defined for this chain. +index_policy=Set default action: +index_policy_accept=Accept +index_policy_drop=Drop +index_policy_queue=Userspace +index_policy_return=Return +index_chain_col=Chain +index_type=Type +index_hook=Hook +index_priority=Priority +index_policy_col=Policy +index_rules=Rules +index_rules_none=No rules +index_cdelete=Delete Chain +index_crename=Rename Chain +index_cclear=Clear All Rules +index_cdeletesel=Delete Selected +index_cmovesel=Move Selected +index_radd=Add Rule +index_apply=Apply Configuration +index_applydesc=Click this button to make the firewall configuration listed above active. Any current firewall rules will be flushed and replaced +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 +index_bootupdesc=Change whether this firewall is activated at boot time or not. +index_return=rules list +edit_title=Edit Rule +edit_header=Rule Details +edit_rule=Rule definition +create=Create +save=Save +delete=Delete +save_err=Failed to save rule +apply_err=Failed to apply configuration +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'. +setup_allow_all=Allow all traffic +setup_deny_incoming=Deny all incoming traffic, allow all outgoing +setup_deny_all=Deny all traffic +setup_create=Create +setup_invalid_type=Invalid ruleset type selected. +setup_failed=Failed to create default ruleset:

$1
+index_setup=Create Default Ruleset +index_setupdesc=Create a default set of rules, for example to allow all traffic. +index_rule_desc=Action $1 for protocol $2 and destination port $3 +index_rule_desc2=Action $1 for outgoing interface $2 +index_rule_desc3=Action $1 for incoming interface $2 +index_rule_desc4=Action $1 for incoming interface $2 and outgoing interface $3 +index_accept=Accept +index_drop=Drop +index_reject=Reject +edit_title_new=Create Rule +edit_title_edit=Edit Rule +edit_comment=Comment +edit_action=Action +edit_proto=Protocol +edit_dport=Destination Port +edit_oif=Outgoing Interface +edit_iif=Incoming Interface +edit_if_any=Any +save_failed=Failed to save rule:
$1
diff --git a/module.info b/module.info new file mode 100644 index 000000000..3aeb690fe --- /dev/null +++ b/module.info @@ -0,0 +1,5 @@ +name=Nftables +desc=NFTables Firewall +os_support=*-linux +category=net +longdesc=Configure a Linux firewall using NFTables. Allows the editing of all tables, chains, and rules. diff --git a/nftables-lib.pl b/nftables-lib.pl new file mode 100644 index 000000000..9609ef8fd --- /dev/null +++ b/nftables-lib.pl @@ -0,0 +1,227 @@ +# nftables-lib.pl +# Functions for reading and writing nftables rules + +BEGIN { push(@INC, ".."); }; +use WebminCore; +use strict; +use warnings; +our (%config, $module_config_directory); +&init_config(); + +# get_nftables_save([file]) +# Returns a list of tables and their chains/rules +sub get_nftables_save +{ +my ($file) = @_; +my $cmd = $config{'nft_cmd'} || &has_command("nft"); +if (!$file) { + if ($config{'direct'}) { + $file = "$cmd list ruleset |"; + } else { + $file = $config{'save_file'} || "$module_config_directory/nftables.conf"; + } +} +return ( ) if (!$file); + +my @rv; +my $table; +my $chain; +my $lnum = 0; +my $content; +open(my $fh, $file); +$content = do { local $/; <$fh> }; +close($fh); + +my @lines = split /\r?\n/, $content; +for(my $i=0; $i<@lines; $i++) { + my $line = $lines[$i]; + $lnum++; + $line =~ s/#.*$//; # Ignore comments for now + + if ($line =~ /^table\s+(\S+)\s+(\S+)\s+\{/) { + # Start of a table + $table = { 'name' => $2, + 'family' => $1, + 'line' => $lnum, + 'rules' => [ ], + 'chains' => { } }; + push(@rv, $table); + $chain = undef; + } + elsif ($line =~ /^\s*chain\s+(\S+)\s+\{/) { + # Start of a chain + if ($table) { + $chain = $1; + $table->{'chains'}->{$chain} = { }; + + # Look at next line for chain definition + if ($lines[$i+1] =~ /^\s*type\s+(\S+)\s+hook\s+(\S+)\s+priority\s+([a-zA-Z0-9_-]+);\s+policy\s+(\S+);/) { + $table->{'chains'}->{$chain}->{'type'} = $1; + $table->{'chains'}->{$chain}->{'hook'} = $2; + $table->{'chains'}->{$chain}->{'priority'} = $3; + $table->{'chains'}->{$chain}->{'policy'} = $4; + $i++; # Skip next line + } + } + } + elsif ($line =~ /^\s*(.*?)$/ && $table && $chain && $1 ne "}") { + # A rule + my $rule_str = $1; + if ($rule_str =~ /\S/) { + my $rule = { + 'text' => $rule_str, + 'chain' => $chain, + 'index' => scalar(@{$table->{'rules'}}), + 'line' => $lnum + }; + if ($rule_str =~ /\bcomment\s+"((?:\\.|[^"\\])*)"/) { + my $c = $1; + $c =~ s/\\"/"/g; + $c =~ s/\\\\/\\/g; + $rule->{'comment'} = $c; + } + if ($rule_str =~ /(\S+)\s+dport\s+(\d+)/) { + $rule->{'proto'} = $1; + $rule->{'dport'} = $2; + } + if ($rule_str =~ /\biif\s+"([^"]+)"/) { + $rule->{'iif'} = $1; + } + elsif ($rule_str =~ /\biif\s+(\S+)/) { + $rule->{'iif'} = $1; + } + if ($rule_str =~ /\boif\s+"([^"]+)"/) { + $rule->{'oif'} = $1; + } + elsif ($rule_str =~ /\boif\s+(\S+)/) { + $rule->{'oif'} = $1; + } + my @actions = ($rule_str =~ /\b(accept|drop|reject)\b/g); + if (@actions) { + $rule->{'action'} = $actions[-1]; + } + push(@{$table->{'rules'}}, $rule); + } + } +} + +return @rv; +} + + +# dump_nftables_save(@tables) +# Returns a string representation of the firewall rules +sub dump_nftables_save +{ +my (@tables) = @_; +my $rv; +foreach my $t (@tables) { + if ($t->{'family'}) { + $rv .= "table $t->{'family'} $t->{'name'} {\n"; + } else { + $rv .= "table $t->{'name'} {\n"; + } + + foreach my $c (keys %{$t->{'chains'}}) { + my $chain = $t->{'chains'}->{$c}; + $rv .= "\tchain $c {\n"; + if ($chain->{'type'}) { + $rv .= "\t\ttype $chain->{'type'} hook $chain->{'hook'} priority $chain->{'priority'}; policy $chain->{'policy'};\n"; + } + + # Add rules for this chain + my @rules = sort { $a->{'index'} <=> $b->{'index'} } + grep { ref($_) eq 'HASH' && $_->{'chain'} eq $c } @{$t->{'rules'}}; + foreach my $r (@rules) { + $rv .= "\t\t$r->{'text'}\n"; + } + $rv .= "\t}\n"; + } + $rv .= "}\n"; +} +return $rv; +} + +# save_table(&table) +# Saves a single table to the save file or applies it +sub save_table +{ +my ($table) = @_; +# Re-read all tables to ensure we have the full picture if we are overwriting the file +# But here we probably just want to update the specific table in the list of tables we have. +# Since we usually operate on a list of tables, we might need to pass the full list or +# re-read the state. +# For simplicity, we usually load all, modify one, and save all. +} + +# save_configuration(@tables) +# Writes the configuration to the save file. If direct mode is on, applies it. +sub save_configuration +{ +my (@tables) = @_; +my $out = &dump_nftables_save(@tables); +my $file = $config{'save_file'} || "$module_config_directory/nftables.conf"; + +# Write to file +&open_tempfile(my $fh, ">$file"); +&print_tempfile($fh, $out); +&close_tempfile($fh); + +if ($config{'direct'}) { + return &apply_restore($file); +} +return undef; +} + +# apply_restore([file]) +# Applies the configuration from the save file +sub apply_restore +{ +my ($file) = @_; +$file ||= $config{'save_file'} || "$module_config_directory/nftables.conf"; +my $cmd = $config{'nft_cmd'} || &has_command("nft"); +my $out = &backquote_logged("$cmd -f $file 2>&1"); +if ($?) { + return "
$out
"; +} +return undef; +} + +# describe_rule(&rule) +sub describe_rule +{ +my ($r) = @_; +my $desc; +if ($r->{'proto'} && $r->{'dport'} && $r->{'action'}) { + $desc = &text('index_rule_desc', $r->{'action'}, $r->{'proto'}, $r->{'dport'}); +} +elsif ($r->{'iif'} && $r->{'oif'} && $r->{'action'}) { + $desc = &text('index_rule_desc4', $r->{'action'}, $r->{'iif'}, $r->{'oif'}); +} +elsif ($r->{'iif'} && $r->{'action'}) { + $desc = &text('index_rule_desc3', $r->{'action'}, $r->{'iif'}); +} +elsif ($r->{'oif'} && $r->{'action'}) { + $desc = &text('index_rule_desc2', $r->{'action'}, $r->{'oif'}); +} +else { + $desc = &html_escape($r->{'text'}); +} +return $desc; +} + +# interface_choice(name, value, blanktext) +# Returns HTML for an interface chooser menu +sub interface_choice +{ +my ($name, $value, $blanktext) = @_; +if (&foreign_check("net")) { + &foreign_require("net", "net-lib.pl"); + return &net::interface_choice($name, $value, $blanktext, 0, 1); +} +else { + return &ui_textbox($name, $value, 20); +} +} + +1; diff --git a/save_rule.cgi b/save_rule.cgi new file mode 100755 index 000000000..812ae89c0 --- /dev/null +++ b/save_rule.cgi @@ -0,0 +1,65 @@ +#!/usr/bin/perl +# save_rule.cgi +# Save a new or existing rule + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text, %config); +&ReadParse(); +&error_setup($text{'save_err'}); +my @tables = &get_nftables_save(); +my $table = $tables[$in{'table'}]; + +if ($in{'delete'}) { + # Delete the rule + my $rule = $table->{'rules'}->[$in{'idx'}]; + splice(@{$table->{'rules'}}, $in{'idx'}, 1); + &webmin_log("delete", "rule", $rule ? $rule->{'text'} : undef); +} else { + my $rule = {}; + if ($in{'new'}) { + $rule->{'chain'} = $in{'chain'}; + $rule->{'index'} = scalar(@{$table->{'rules'}}); + } else { + $rule = $table->{'rules'}->[$in{'idx'}]; + } + + $rule->{'comment'} = $in{'comment'}; + $rule->{'action'} = $in{'action'}; + $rule->{'proto'} = $in{'proto'}; + $rule->{'dport'} = $in{'dport'}; + my $iif = $in{'iif'}; + my $oif = $in{'oif'}; + $iif = $in{'iif_other'} if (defined($iif) && $iif eq 'other'); + $oif = $in{'oif_other'} if (defined($oif) && $oif eq 'other'); + $rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef; + $rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef; + + my $rule_text = ""; + if ($rule->{'proto'} && $rule->{'dport'}) { + $rule_text .= "$rule->{'proto'} dport $rule->{'dport'} "; + } + if ($rule->{'iif'}) { + $rule_text .= "iif \"$rule->{'iif'}\" "; + } + if ($rule->{'oif'}) { + $rule_text .= "oif \"$rule->{'oif'}\" "; + } + $rule_text .= $rule->{'action'}; + if ($rule->{'comment'}) { + my $comment = $rule->{'comment'}; + $comment =~ s/\\/\\\\/g; + $comment =~ s/"/\\"/g; + $rule_text .= " comment \"$comment\""; + } + $rule->{'text'} = $rule_text; + + if ($in{'new'}) { + push(@{$table->{'rules'}}, $rule); + } + &webmin_log("save", $in{'new'} ? "create" : "modify", $rule->{'text'}); +} +my $err = &save_configuration(@tables); +&error(&text('save_failed', $err)) if ($err); +&redirect("index.cgi?table=$in{'table'}"); diff --git a/setup.cgi b/setup.cgi new file mode 100644 index 000000000..254785570 --- /dev/null +++ b/setup.cgi @@ -0,0 +1,156 @@ +#!/usr/bin/perl +# setup.cgi +# Create a default nftables ruleset + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text, %config); +&ReadParse(); +if ($in{'action'} eq 'create') { + my $type = $in{'type'}; + my @tables; + if ($type eq 'allow_all') { + @tables = &create_allow_all_ruleset(); + } + elsif ($type eq 'deny_incoming') { + @tables = &create_deny_incoming_ruleset(); + } + elsif ($type eq 'deny_all') { + @tables = &create_deny_all_ruleset(); + } + else { + &error($text{'setup_invalid_type'}); + } + + my $error = &save_configuration(@tables); + 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"); +} + +&ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1); + +print "

$text{'setup_header'}

"; +print "

$text{'setup_desc'}

"; + +print &ui_form_start("setup.cgi"); +print &ui_hidden("action", "create"); + +my @type_opts = ( + [ 'allow_all', $text{'setup_allow_all'} . "
" ], + [ 'deny_incoming', $text{'setup_deny_incoming'} . "
" ], + [ 'deny_all', $text{'setup_deny_all'} ], +); +print &ui_radio("type", "allow_all", \@type_opts); + +print &ui_form_end([ [ undef, $text{'setup_create'} ] ]); + +sub create_allow_all_ruleset +{ + my @tables; + my $table = { + 'name' => 'inet_filter', + 'family' => 'inet', + 'rules' => [], + 'chains' => { + 'input' => { + 'type' => 'filter', + 'hook' => 'input', + 'priority' => 0, + 'policy' => 'accept' + }, + 'forward' => { + 'type' => 'filter', + 'hook' => 'forward', + 'priority' => 0, + 'policy' => 'accept' + }, + 'output' => { + 'type' => 'filter', + 'hook' => 'output', + 'priority' => 0, + 'policy' => 'accept' + } + } + }; + push(@tables, $table); + return @tables; +} + +sub create_deny_incoming_ruleset +{ + my @tables; + my $table = { + 'name' => 'inet_filter', + 'family' => 'inet', + 'rules' => [ + { + 'text' => 'ct state established,related accept', + 'chain' => 'input' + } + ], + 'chains' => { + 'input' => { + 'type' => 'filter', + 'hook' => 'input', + 'priority' => 0, + 'policy' => 'drop' + }, + 'forward' => { + 'type' => 'filter', + 'hook' => 'forward', + 'priority' => 0, + 'policy' => 'accept' + }, + 'output' => { + 'type' => 'filter', + 'hook' => 'output', + 'priority' => 0, + 'policy' => 'accept' + } + } + }; + push(@tables, $table); + return @tables; +} + +sub create_deny_all_ruleset +{ + my @tables; + my $table = { + 'name' => 'inet_filter', + 'family' => 'inet', + 'rules' => [], + 'chains' => { + 'input' => { + 'type' => 'filter', + 'hook' => 'input', + 'priority' => 0, + 'policy' => 'drop' + }, + 'forward' => { + 'type' => 'filter', + 'hook' => 'forward', + 'priority' => 0, + 'policy' => 'drop' + }, + 'output' => { + 'type' => 'filter', + 'hook' => 'output', + 'priority' => 0, + 'policy' => 'drop' + } + } + }; + push(@tables, $table); + return @tables; +} + +&ui_print_footer("/", $text{'index'}); From 0d70782330e2ff588d46ed3e46340b42ee3e0c25 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Thu, 22 Jan 2026 20:45:07 -0600 Subject: [PATCH 02/13] Always allow Webmin/ssh ports on setup --- lang/en | 7 ++++--- nftables-lib.pl | 11 +++++++++++ setup.cgi | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lang/en b/lang/en index 2f4565554..2bc1204a8 100644 --- a/lang/en +++ b/lang/en @@ -3,7 +3,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 IP table: +index_change=Show table: index_table_filter=Packet filtering index_table_nat=Network address translation index_table_mangle=Packet alteration @@ -50,9 +50,10 @@ apply_err=Failed to apply configuration 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'. +setup_deny_note=Deny options will still allow SSH (port 22), Webmin (port $1), and localhost (loopback). setup_allow_all=Allow all traffic -setup_deny_incoming=Deny all incoming traffic, allow all outgoing -setup_deny_all=Deny all traffic +setup_deny_incoming=Deny all incoming traffic, allow all outgoing (except SSH and Webmin) +setup_deny_all=Deny all traffic (except SSH and Webmin) setup_create=Create setup_invalid_type=Invalid ruleset type selected. setup_failed=Failed to create default ruleset:
$1
diff --git a/nftables-lib.pl b/nftables-lib.pl index 9609ef8fd..e86dbd918 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -224,4 +224,15 @@ else { } } +# get_webmin_port() +# Returns the configured Webmin port, or 10000 if unknown +sub get_webmin_port +{ +my %miniserv; +if (&get_miniserv_config(\%miniserv) && $miniserv{'port'} =~ /^\d+$/) { + return $miniserv{'port'}; +} +return 10000; +} + 1; diff --git a/setup.cgi b/setup.cgi index 254785570..d5e623e8a 100644 --- a/setup.cgi +++ b/setup.cgi @@ -38,7 +38,9 @@ if ($in{'action'} eq 'create') { &ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1); print "

$text{'setup_header'}

"; +my $webmin_port = &get_webmin_port(); print "

$text{'setup_desc'}

"; +print "

",&text('setup_deny_note', $webmin_port),"

"; print &ui_form_start("setup.cgi"); print &ui_hidden("action", "create"); @@ -87,6 +89,7 @@ sub create_allow_all_ruleset sub create_deny_incoming_ruleset { my @tables; + my $webmin_port = &get_webmin_port(); my $table = { 'name' => 'inet_filter', 'family' => 'inet', @@ -94,6 +97,18 @@ sub create_deny_incoming_ruleset { 'text' => 'ct state established,related accept', 'chain' => 'input' + }, + { + 'text' => 'iif "lo" accept', + 'chain' => 'input' + }, + { + 'text' => 'tcp dport 22 accept', + 'chain' => 'input' + }, + { + 'text' => "tcp dport $webmin_port accept", + 'chain' => 'input' } ], 'chains' => { @@ -124,6 +139,7 @@ sub create_deny_incoming_ruleset sub create_deny_all_ruleset { my @tables; + my $webmin_port = &get_webmin_port(); my $table = { 'name' => 'inet_filter', 'family' => 'inet', @@ -149,6 +165,28 @@ sub create_deny_all_ruleset } } }; + $table->{'rules'} = [ + { + 'text' => 'ct state established,related accept', + 'chain' => 'output' + }, + { + 'text' => 'iif "lo" accept', + 'chain' => 'input' + }, + { + 'text' => 'oif "lo" accept', + 'chain' => 'output' + }, + { + 'text' => 'tcp dport 22 accept', + 'chain' => 'input' + }, + { + 'text' => "tcp dport $webmin_port accept", + 'chain' => 'input' + } + ]; push(@tables, $table); return @tables; } From d546116ab7d8dd7495a296ae71f7511a23f4815c Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 31 Jan 2026 02:21:33 -0600 Subject: [PATCH 03/13] Add/delete table, and select table --- create_table.cgi | 55 ++++++++++++++++++++++++++++++++ delete_table.cgi | 36 +++++++++++++++++++++ index.cgi | 82 ++++++++++++++++++++++++++++++++++-------------- lang/en | 19 +++++++++++ 4 files changed, 168 insertions(+), 24 deletions(-) create mode 100755 create_table.cgi create mode 100755 delete_table.cgi diff --git a/create_table.cgi b/create_table.cgi new file mode 100755 index 000000000..be0aa3a52 --- /dev/null +++ b/create_table.cgi @@ -0,0 +1,55 @@ +#!/usr/bin/perl +# create_table.cgi +# Create a new nftables table + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text); +&ReadParse(); +&error_setup($text{'create_err'}); + +my @families = qw(ip ip6 inet arp bridge netdev); +my %family_ok = map { $_ => 1 } @families; + +if ($in{'create'}) { + my $name = $in{'name'}; + my $family = $in{'family'}; + + $name =~ /^\w[\w-]*$/ || &error($text{'create_ename'}); + $family_ok{$family} || &error($text{'create_efamily'}); + + my @tables = &get_nftables_save(); + foreach my $t (@tables) { + if ($t->{'name'} eq $name && $t->{'family'} eq $family) { + &error($text{'create_edup'}); + } + } + + push(@tables, { 'name' => $name, + 'family' => $family, + 'rules' => [], + 'chains' => {} }); + my $err = &save_configuration(@tables); + &error(&text('create_failed', $err)) if ($err); + &webmin_log("create", "table", $name, { 'family' => $family }); + + my $idx = $#tables; + &redirect("index.cgi?table=$idx"); +} + +&ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1); +print &ui_form_start("create_table.cgi"); +print &ui_hidden("create", 1); + +print &ui_table_start($text{'create_header'}, "width=100%", 2); +print &ui_table_row($text{'create_family'}, + &ui_select("family", $in{'family'} || "inet", + [ map { [ $_, $_ ] } @families ])); +print &ui_table_row($text{'create_name'}, + &ui_textbox("name", $in{'name'}, 20)); +print &ui_table_end(); + +print &ui_form_end([ [ undef, $text{'create_ok'} ] ]); +&ui_print_footer("index.cgi", $text{'index_return'}); + diff --git a/delete_table.cgi b/delete_table.cgi new file mode 100755 index 000000000..9ebc69488 --- /dev/null +++ b/delete_table.cgi @@ -0,0 +1,36 @@ +#!/usr/bin/perl +# delete_table.cgi +# Delete an existing nftables table + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text); +&ReadParse(); +&error_setup($text{'delete_err'}); + +my @tables = &get_nftables_save(); +my $table = $tables[$in{'table'}]; +$table || &error($text{'delete_notable'}); + +if ($in{'confirm'}) { + splice(@tables, $in{'table'}, 1); + my $err = &save_configuration(@tables); + &error(&text('delete_failed', $err)) if ($err); + &webmin_log("delete", "table", $table->{'name'}, + { 'family' => $table->{'family'} }); + &redirect("index.cgi"); +} + +&ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1); +print &ui_form_start("delete_table.cgi"); +print &ui_hidden("table", $in{'table'}); +print "
", + &text('delete_confirm', + "$table->{'family'} $table->{'name'}"), + "

\n"; +print &ui_submit($text{'delete'}, "confirm"); +print "

\n"; +print &ui_form_end(); +&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); + diff --git a/index.cgi b/index.cgi index 8c0724ca7..aec1660f7 100755 --- a/index.cgi +++ b/index.cgi @@ -7,13 +7,18 @@ use strict; use warnings; our (%in, %text, %config); &ReadParse(); -&ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1); +my $partial = $in{'partial'}; +if (!$partial) { + &ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1); +} # Check for nft command my $cmd = $config{'nft_cmd'} || &has_command("nft"); if (!$cmd) { print &text('index_ecommand', "nft"); - &ui_print_footer("/", $text{'index'}); + if (!$partial) { + &ui_print_footer("/", $text{'index'}); + } exit; } @@ -22,18 +27,23 @@ 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
"); - &ui_print_footer("/", $text{'index'}); + if (!$partial) { + &ui_print_footer("/", $text{'index'}); + } exit; } # Load tables my @tables = &get_nftables_save(); +my $rules_html = ""; if (!@tables) { - print "$text{'index_none'}

\n"; - print &ui_buttons_start(); - print &ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'}); - print &ui_buttons_end(); + $rules_html .= "$text{'index_none'}

\n"; + $rules_html .= &ui_buttons_start(); + $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_end(); } else { # Select table if (!defined($in{'table'}) || $in{'table'} !~ /^\d+$/ || @@ -46,18 +56,23 @@ if (!@tables) { push(@table_opts, [ $i, $t->{'family'}." ".$t->{'name'} ]); } - print &ui_form_start("index.cgi"); - print &text('index_change')," "; - print &ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1); - print &ui_form_end(); + if (!$partial) { + print &ui_form_start("index.cgi"); + print "

\n"; + print &text('index_change')," "; + print &ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1, 0, + "onchange='form.submit()'"); + print "
\n"; + print &ui_form_end(); + } # Identify current table my $curr = $tables[$in{'table'}]; if ($curr) { # Show chains and rules - print &ui_hr(); - print &ui_columns_start( + $rules_html .= &ui_hr(); + $rules_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); @@ -68,7 +83,7 @@ if (!@tables) { my $policy_label = $policy ? ($text{'index_policy_'.lc($policy)} || uc($policy)) : "-"; my @rules = grep { $_->{'chain'} eq $c } @{$curr->{'rules'}}; - my $rules_html; + my $rules_html_row; if (@rules) { my @rows; foreach my $r (@rules) { @@ -78,30 +93,49 @@ if (!@tables) { &urlize($c)."&idx=$r->{'index'}", $desc)); } - $rules_html = join("
", @rows); + $rules_html_row = join("
", @rows); } else { - $rules_html = "$text{'index_rules_none'}"; + $rules_html_row = "$text{'index_rules_none'}"; } - $rules_html .= "
". + $rules_html_row .= "
". &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". &urlize($c)."&new=1", $text{'index_radd'}); - print &ui_columns_row([ + $rules_html .= &ui_columns_row([ $c, $chain_def->{'type'} || "-", $chain_def->{'hook'} || "-", defined($chain_def->{'priority'}) ? $chain_def->{'priority'} : "-", $policy_label, - $rules_html + $rules_html_row ]); } - print &ui_columns_end(); + $rules_html .= &ui_columns_end(); + $rules_html .= &ui_hr(); + $rules_html .= &ui_buttons_start(); + $rules_html .= &ui_buttons_row("delete_table.cgi?table=$in{'table'}", + $text{'index_table_delete'}, + $text{'index_table_deletedesc'}); + $rules_html .= &ui_buttons_end(); } } -print &ui_hr(); -print &ui_buttons_start(); -print &ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'}); -print &ui_buttons_end(); +if ($partial) { + print $rules_html; + exit; +} + +print "
\n"; +print $rules_html; +print "
\n"; + +if (@tables) { + print &ui_hr(); + print &ui_buttons_start(); + print &ui_buttons_row("create_table.cgi", $text{'index_table_create'}, + $text{'index_table_createdesc'}); + print &ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'}); + print &ui_buttons_end(); +} &ui_print_footer("/", $text{'index'}); diff --git a/lang/en b/lang/en index 2bc1204a8..3c95a1c00 100644 --- a/lang/en +++ b/lang/en @@ -59,6 +59,10 @@ setup_invalid_type=Invalid ruleset type selected. setup_failed=Failed to create default ruleset:
$1
index_setup=Create Default Ruleset index_setupdesc=Create a default set of rules, for example to allow all traffic. +index_table_create=Create table +index_table_createdesc=Add a new nftables table. +index_table_delete=Delete table +index_table_deletedesc=Remove the currently selected table. index_rule_desc=Action $1 for protocol $2 and destination port $3 index_rule_desc2=Action $1 for outgoing interface $2 index_rule_desc3=Action $1 for incoming interface $2 @@ -75,4 +79,19 @@ edit_dport=Destination Port edit_oif=Outgoing Interface edit_iif=Incoming Interface edit_if_any=Any +create_title=Create table +create_header=Create a new table +create_family=Table family +create_name=Table name +create_ok=Create +create_err=Failed to create table +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 +delete_title=Delete table +delete_err=Failed to delete table +delete_failed=Failed to delete table:
$1
+delete_confirm=Are you sure you want to delete table $1? +delete_notable=No such table selected save_failed=Failed to save rule:
$1
From e92fc730eeb427ab51db2f656673f9f2f098ff4a Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 31 Jan 2026 13:16:07 -0600 Subject: [PATCH 04/13] Make selecting table work without a submit button --- index.cgi | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.cgi b/index.cgi index aec1660f7..f7d60d507 100755 --- a/index.cgi +++ b/index.cgi @@ -60,8 +60,9 @@ if (!@tables) { print &ui_form_start("index.cgi"); print "
\n"; print &text('index_change')," "; - print &ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1, 0, - "onchange='form.submit()'"); + 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'"); print "
\n"; print &ui_form_end(); } From 179ddf751b4e6cc24437b6bf2b43a751c8ee3d12 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 31 Jan 2026 17:00:57 -0600 Subject: [PATCH 05/13] Expand parser, cover more kinds of rules --- edit_rule.cgi | 105 ++++++- lang/en | 44 +++ nftables-lib.pl | 716 ++++++++++++++++++++++++++++++++++++++++--- save_rule.cgi | 97 ++++-- t/rulesets/basic.nft | 8 + t/run-tests.t | 131 ++++++++ 6 files changed, 1030 insertions(+), 71 deletions(-) create mode 100644 t/rulesets/basic.nft create mode 100755 t/run-tests.t diff --git a/edit_rule.cgi b/edit_rule.cgi index 1c33919db..c4f3663df 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -12,6 +12,10 @@ my $table = $tables[$in{'table'}]; my $rule; my $chain_def; my $chain_hook; +my $action_sel; +my $proto_sel; +my $icmp_type; +my $log_enabled; if ($in{'new'}) { &ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1); @@ -24,6 +28,26 @@ if ($table && $rule->{'chain'}) { $chain_def = $table->{'chains'}->{$rule->{'chain'}}; $chain_hook = $chain_def ? $chain_def->{'hook'} : undef; } +if ($rule) { + if ($rule->{'jump'}) { + $action_sel = 'jump'; + } + elsif ($rule->{'goto'}) { + $action_sel = 'goto'; + } + else { + $action_sel = $rule->{'action'}; + } + $action_sel ||= 'accept'; + $proto_sel = $rule->{'proto'} || $rule->{'l4proto'}; + if (!$proto_sel) { + $proto_sel = 'icmp' if ($rule->{'icmp_type'}); + $proto_sel = 'icmpv6' if ($rule->{'icmpv6_type'}); + } + $proto_sel ||= 'tcp' if ($in{'new'}); + $icmp_type = $rule->{'icmp_type'} || $rule->{'icmpv6_type'}; + $log_enabled = $rule->{'log'} || $rule->{'log_prefix'} || $rule->{'log_level'}; +} print &ui_form_start("save_rule.cgi"); print &ui_hidden("table", $in{'table'}); @@ -39,26 +63,23 @@ print &ui_table_row($text{'edit_comment'}, # Action print &ui_table_row($text{'edit_action'}, - &ui_select("action", $rule->{'action'}, + &ui_select("action", $action_sel, [ [ "accept", $text{'index_accept'} ], [ "drop", $text{'index_drop'} ], [ "reject", $text{'index_reject'} ], + [ "return", $text{'edit_return'} ], + [ "jump", $text{'edit_jump_action'} ], + [ "goto", $text{'edit_goto_action'} ], ])); -# Protocol -print &ui_table_row($text{'edit_proto'}, - &ui_select("proto", $rule->{'proto'}, - [ - [ "tcp", "TCP" ], - [ "udp", "UDP" ], - [ "icmp", "ICMP" ], - ])); - -# Destination port -print &ui_table_row($text{'edit_dport'}, - &ui_textbox("dport", $rule->{'dport'}, 10)); +# Jump/Goto target chain +print &ui_table_row($text{'edit_jump'}, + &ui_textbox("jump", $rule->{'jump'}, 20)); +print &ui_table_row($text{'edit_goto'}, + &ui_textbox("goto", $rule->{'goto'}, 20)); +# Interfaces if ($chain_hook && $chain_hook eq 'input') { # Incoming interface print &ui_table_row($text{'edit_iif'}, @@ -77,6 +98,64 @@ else { &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); } +# Addresses +print &ui_table_row($text{'edit_saddr'}, + &ui_textbox("saddr", $rule->{'saddr'}, 30)); +print &ui_table_row($text{'edit_daddr'}, + &ui_textbox("daddr", $rule->{'daddr'}, 30)); + +# Protocol +print &ui_table_row($text{'edit_proto'}, + &ui_select("proto", $proto_sel, + [ + [ "", $text{'edit_proto_any'} ], + [ "tcp", "TCP" ], + [ "udp", "UDP" ], + [ "icmp", "ICMP" ], + [ "icmpv6", "ICMPv6" ], + ])); + +# Ports +print &ui_table_row($text{'edit_sport'}, + &ui_textbox("sport", $rule->{'sport'}, 10)); +print &ui_table_row($text{'edit_dport'}, + &ui_textbox("dport", $rule->{'dport'}, 10)); + +# ICMP type +print &ui_table_row($text{'edit_icmp_type'}, + &ui_textbox("icmp_type", $icmp_type, 20)); + +# Conntrack state +print &ui_table_row($text{'edit_ct_state'}, + &ui_textbox("ct_state", $rule->{'ct_state'}, 30)); + +# TCP flags +print &ui_table_row($text{'edit_tcp_flags'}, + &ui_textbox("tcp_flags", $rule->{'tcp_flags'}, 20)); +print &ui_table_row($text{'edit_tcp_flags_mask'}, + &ui_textbox("tcp_flags_mask", $rule->{'tcp_flags_mask'}, 20)); + +# Limit +print &ui_table_row($text{'edit_limit_rate'}, + &ui_textbox("limit_rate", $rule->{'limit_rate'}, 20)); +print &ui_table_row($text{'edit_limit_burst'}, + &ui_textbox("limit_burst", $rule->{'limit_burst'}, 10)); + +# Log +my $log_row = &ui_checkbox("log", 1, $text{'edit_log_enable'}, $log_enabled); +$log_row .= "
".&text('edit_log_prefix', &ui_textbox("log_prefix", $rule->{'log_prefix'}, 20)); +$log_row .= " ".&text('edit_log_level', &ui_textbox("log_level", $rule->{'log_level'}, 10)); +print &ui_table_row($text{'edit_log'}, $log_row); + +# Counter +print &ui_table_row($text{'edit_counter'}, + &ui_checkbox("counter", 1, $text{'edit_counter_enable'}, $rule->{'counter'})); + +# Raw rule (read-only) +print &ui_table_row($text{'edit_raw_rule'}, + &ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, + "readonly='true'")); + print &ui_table_end(); my @buttons; if ($in{'new'}) { diff --git a/lang/en b/lang/en index 3c95a1c00..36c40722f 100644 --- a/lang/en +++ b/lang/en @@ -7,6 +7,7 @@ index_change=Show table: index_table_filter=Packet filtering index_table_nat=Network address translation index_table_mangle=Packet alteration +index_table_ok=Display table index_chain=Chain $1 index_action=Action index_desc=Condition @@ -67,15 +68,58 @@ index_rule_desc=Action $1 for protocol $2 and destination port $3 index_rule_desc2=Action $1 for outgoing interface $2 index_rule_desc3=Action $1 for incoming interface $2 index_rule_desc4=Action $1 for incoming interface $2 and outgoing interface $3 +index_rule_desc_generic=Action $1 for $2 +index_rule_desc_action=Action $1 +index_rule_iif=Incoming interface $1 +index_rule_oif=Outgoing interface $1 +index_rule_saddr=Source $1 +index_rule_daddr=Destination $1 +index_rule_proto=Protocol $1 +index_rule_sport=Source port $1 +index_rule_dport=Destination port $1 +index_rule_icmp=ICMP type $1 +index_rule_icmpv6=ICMPv6 type $1 +index_rule_ct=Conntrack state $1 +index_rule_tcpflags=TCP flags $1 +index_rule_limit=Limit $1 +index_rule_log=Log +index_rule_log_prefix=Log prefix $1 +index_rule_log_level=Log level $1 +index_rule_counter=Counter +index_rule_jump=Jump $1 +index_rule_goto=Goto $1 index_accept=Accept index_drop=Drop index_reject=Reject +index_return_action=Return edit_title_new=Create Rule edit_title_edit=Edit Rule edit_comment=Comment edit_action=Action +edit_return=Return +edit_jump_action=Jump +edit_goto_action=Goto +edit_jump=Jump target chain +edit_goto=Goto target chain edit_proto=Protocol +edit_proto_any=Any +edit_saddr=Source address +edit_daddr=Destination address +edit_sport=Source Port edit_dport=Destination Port +edit_icmp_type=ICMP/ICMPv6 type +edit_ct_state=Conntrack state +edit_tcp_flags=TCP flags +edit_tcp_flags_mask=TCP flags mask +edit_limit_rate=Limit rate +edit_limit_burst=Limit burst +edit_log=Logging +edit_log_enable=Enable logging +edit_log_prefix=Prefix $1 +edit_log_level=Level $1 +edit_counter=Counter +edit_counter_enable=Enable counter +edit_raw_rule=Raw rule edit_oif=Outgoing Interface edit_iif=Incoming Interface edit_if_any=Any diff --git a/nftables-lib.pl b/nftables-lib.pl index e86dbd918..8af072a05 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -74,31 +74,11 @@ for(my $i=0; $i<@lines; $i++) { 'index' => scalar(@{$table->{'rules'}}), 'line' => $lnum }; - if ($rule_str =~ /\bcomment\s+"((?:\\.|[^"\\])*)"/) { - my $c = $1; - $c =~ s/\\"/"/g; - $c =~ s/\\\\/\\/g; - $rule->{'comment'} = $c; - } - if ($rule_str =~ /(\S+)\s+dport\s+(\d+)/) { - $rule->{'proto'} = $1; - $rule->{'dport'} = $2; - } - if ($rule_str =~ /\biif\s+"([^"]+)"/) { - $rule->{'iif'} = $1; - } - elsif ($rule_str =~ /\biif\s+(\S+)/) { - $rule->{'iif'} = $1; - } - if ($rule_str =~ /\boif\s+"([^"]+)"/) { - $rule->{'oif'} = $1; - } - elsif ($rule_str =~ /\boif\s+(\S+)/) { - $rule->{'oif'} = $1; - } - my @actions = ($rule_str =~ /\b(accept|drop|reject)\b/g); - if (@actions) { - $rule->{'action'} = $actions[-1]; + my $parsed = &parse_rule_text($rule_str); + if ($parsed) { + foreach my $k (keys %$parsed) { + $rule->{$k} = $parsed->{$k}; + } } push(@{$table->{'rules'}}, $rule); } @@ -108,6 +88,604 @@ for(my $i=0; $i<@lines; $i++) { return @rv; } +sub tokenize_nft_rule +{ +my ($line) = @_; +my @tokens; +my $i = 0; +my $len = length($line); +while ($i < $len) { + my $ch = substr($line, $i, 1); + if ($ch =~ /\s/) { + $i++; + next; + } + if ($ch eq '"' || $ch eq "'") { + my $q = $ch; + my $j = $i + 1; + my $esc = 0; + while ($j < $len) { + my $c = substr($line, $j, 1); + if ($esc) { + $esc = 0; + } + elsif ($c eq "\\") { + $esc = 1; + } + elsif ($c eq $q) { + $j++; + last; + } + $j++; + } + push(@tokens, substr($line, $i, $j-$i)); + $i = $j; + next; + } + if ($ch eq '{') { + my $j = $i + 1; + my $depth = 1; + while ($j < $len && $depth > 0) { + my $c = substr($line, $j, 1); + if ($c eq '{') { + $depth++; + } + elsif ($c eq '}') { + $depth--; + } + $j++; + } + push(@tokens, substr($line, $i, $j-$i)); + $i = $j; + next; + } + my $j = $i; + while ($j < $len && substr($line, $j, 1) !~ /\s/) { + $j++; + } + push(@tokens, substr($line, $i, $j-$i)); + $i = $j; +} +return @tokens; +} + +sub unquote_nft_string +{ +my ($s) = @_; +return $s if (!defined($s)); +if ($s =~ /^"(.*)"$/s) { + $s = $1; + $s =~ s/\\(["\\])/$1/g; +} +elsif ($s =~ /^'(.*)'$/s) { + $s = $1; + $s =~ s/\\(['\\])/$1/g; +} +return $s; +} + +sub escape_nft_string +{ +my ($s) = @_; +return "" if (!defined($s)); +$s =~ s/\\/\\\\/g; +$s =~ s/"/\\"/g; +return $s; +} + +sub guess_addr_family +{ +my ($addr, $fallback) = @_; +return $fallback if ($fallback); +return "ip6" if (defined($addr) && $addr =~ /:/); +return "ip"; +} + +sub format_addr_expr +{ +my ($dir, $rule) = @_; +my $val = $rule->{$dir}; +return undef if (!defined($val) || $val eq ''); +my $fam = &guess_addr_family($val, $rule->{$dir."_family"}); +return $fam." ".$dir." ".$val; +} + +sub format_l4proto_expr +{ +my ($rule) = @_; +my $proto = $rule->{'l4proto'}; +return undef if (!defined($proto) || $proto eq ''); +my $fam = $rule->{'l4proto_family'} || 'meta'; +if ($fam eq 'ip' || $fam eq 'ip6') { + return $fam." protocol ".$proto; +} +return "meta l4proto ".$proto; +} + +sub format_port_expr +{ +my ($dir, $rule) = @_; +my $val = $rule->{$dir}; +return undef if (!defined($val) || $val eq ''); +my $proto; +if ($dir eq 'sport') { + $proto = $rule->{'sport_proto'} || $rule->{'proto'} || $rule->{'l4proto'}; +} +else { + $proto = $rule->{'proto'} || $rule->{'l4proto'}; +} +return undef if (!defined($proto) || $proto eq ''); +return $proto." ".$dir." ".$val; +} + +sub format_tcp_flags_expr +{ +my ($rule) = @_; +return undef if (!defined($rule->{'tcp_flags'}) || $rule->{'tcp_flags'} eq ''); +my $val = $rule->{'tcp_flags'}; +if (defined($rule->{'tcp_flags_mask'}) && $rule->{'tcp_flags_mask'} ne '') { + return "tcp flags & ".$rule->{'tcp_flags_mask'}." == ".$val; +} +return "tcp flags ".$val; +} + +sub format_limit_expr +{ +my ($rule) = @_; +return undef if (!defined($rule->{'limit_rate'}) || $rule->{'limit_rate'} eq ''); +my $out = "limit rate ".$rule->{'limit_rate'}; +if (defined($rule->{'limit_burst'}) && $rule->{'limit_burst'} ne '') { + my $burst = $rule->{'limit_burst'}; + $out .= " burst ".$burst; + $out .= " packets" if ($burst =~ /^\d+$/); +} +return $out; +} + +sub format_log_expr +{ +my ($rule) = @_; +return undef if (!$rule->{'log'} && !$rule->{'log_prefix'} && !$rule->{'log_level'}); +my @p = ("log"); +if (defined($rule->{'log_prefix'}) && $rule->{'log_prefix'} ne '') { + my $pfx = &escape_nft_string($rule->{'log_prefix'}); + push(@p, "prefix", "\"".$pfx."\""); +} +if (defined($rule->{'log_level'}) && $rule->{'log_level'} ne '') { + push(@p, "level", $rule->{'log_level'}); +} +return join(" ", @p); +} + +sub parse_rule_text +{ +my ($line) = @_; +return { } if (!defined($line)); +my %rule; +my @tokens = &tokenize_nft_rule($line); +my @exprs; +my $i = 0; +while ($i < @tokens) { + my $tok = $tokens[$i]; + + if ($tok eq 'comment' && $i+1 < @tokens) { + my $raw = $tokens[$i]." ".$tokens[$i+1]; + $rule{'comment'} = &unquote_nft_string($tokens[$i+1]); + push(@exprs, { 'type' => 'comment', 'text' => $raw }); + $i += 2; + next; + } + if (($tok eq 'iif' || $tok eq 'iifname') && $i+1 < @tokens) { + my $raw = $tok." ".$tokens[$i+1]; + $rule{'iif'} = &unquote_nft_string($tokens[$i+1]); + $rule{'iif_type'} = $tok; + push(@exprs, { 'type' => 'iif', 'text' => $raw }); + $i += 2; + next; + } + if (($tok eq 'oif' || $tok eq 'oifname') && $i+1 < @tokens) { + my $raw = $tok." ".$tokens[$i+1]; + $rule{'oif'} = &unquote_nft_string($tokens[$i+1]); + $rule{'oif_type'} = $tok; + push(@exprs, { 'type' => 'oif', 'text' => $raw }); + $i += 2; + next; + } + if (($tok eq 'ip' || $tok eq 'ip6') && $i+2 < @tokens && + ($tokens[$i+1] eq 'saddr' || $tokens[$i+1] eq 'daddr')) { + my $which = $tokens[$i+1]; + my $val = $tokens[$i+2]; + my $raw = $tok." ".$which." ".$val; + $rule{$which} = $val; + $rule{$which."_family"} = $tok; + push(@exprs, { 'type' => $which, 'text' => $raw }); + $i += 3; + next; + } + if (($tok eq 'ip' || $tok eq 'ip6') && $i+2 < @tokens && + $tokens[$i+1] eq 'protocol') { + my $val = $tokens[$i+2]; + my $raw = $tok." protocol ".$val; + $rule{'l4proto'} = $val; + $rule{'l4proto_family'} = $tok; + push(@exprs, { 'type' => 'l4proto', 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'meta' && $i+2 < @tokens && + $tokens[$i+1] eq 'l4proto') { + my $val = $tokens[$i+2]; + my $raw = "meta l4proto ".$val; + $rule{'l4proto'} = $val; + $rule{'l4proto_family'} = 'meta'; + push(@exprs, { 'type' => 'l4proto', 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'tcp' && $i+1 < @tokens && $tokens[$i+1] eq 'flags') { + my $j = $i + 2; + my $mask; + my $val; + if ($j < @tokens && $tokens[$j] eq '&' && $j+1 < @tokens) { + $mask = $tokens[$j+1]; + $j += 2; + } + if ($j < @tokens && $tokens[$j] eq '==' && $j+1 < @tokens) { + $val = $tokens[$j+1]; + $j += 2; + } + elsif ($j < @tokens) { + $val = $tokens[$j]; + $j++; + } + my $raw = join(" ", @tokens[$i..($j-1)]); + $rule{'tcp_flags'} = $val if (defined($val)); + $rule{'tcp_flags_mask'} = $mask if (defined($mask)); + push(@exprs, { 'type' => 'tcp_flags', 'text' => $raw }); + $i = $j; + next; + } + if (($tok eq 'tcp' || $tok eq 'udp') && $i+2 < @tokens && + ($tokens[$i+1] eq 'dport' || $tokens[$i+1] eq 'sport')) { + my $dir = $tokens[$i+1]; + my $val = $tokens[$i+2]; + my $raw = $tok." ".$dir." ".$val; + if ($dir eq 'dport') { + $rule{'proto'} = $tok; + $rule{'dport'} = $val; + } + else { + $rule{'sport'} = $val; + $rule{'sport_proto'} = $tok; + } + push(@exprs, { 'type' => $dir, 'text' => $raw, 'proto' => $tok }); + $i += 3; + next; + } + if (($tok eq 'icmp' || $tok eq 'icmpv6') && $i+2 < @tokens && + $tokens[$i+1] eq 'type') { + my $val = $tokens[$i+2]; + my $raw = $tok." type ".$val; + if ($tok eq 'icmp') { + $rule{'icmp_type'} = $val; + } + else { + $rule{'icmpv6_type'} = $val; + } + push(@exprs, { 'type' => $tok, 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'ct' && $i+2 < @tokens && $tokens[$i+1] eq 'state') { + my $val = $tokens[$i+2]; + my $raw = "ct state ".$val; + $rule{'ct_state'} = $val; + push(@exprs, { 'type' => 'ct_state', 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'limit') { + my $j = $i + 1; + my @lt = ($tok); + if ($j < @tokens && $tokens[$j] eq 'rate' && $j+1 < @tokens) { + push(@lt, $tokens[$j], $tokens[$j+1]); + $rule{'limit_rate'} = $tokens[$j+1]; + $j += 2; + if ($j < @tokens && $tokens[$j] eq 'burst' && $j+1 < @tokens) { + push(@lt, $tokens[$j], $tokens[$j+1]); + $rule{'limit_burst'} = $tokens[$j+1]; + $j += 2; + if ($j < @tokens && $tokens[$j] eq 'packets') { + push(@lt, $tokens[$j]); + $j++; + } + } + } + my $raw = join(" ", @lt); + push(@exprs, { 'type' => 'limit', 'text' => $raw }); + $i = $j; + next; + } + if ($tok eq 'log') { + my $j = $i + 1; + my @lt = ($tok); + while ($j < @tokens) { + if ($tokens[$j] eq 'prefix' && $j+1 < @tokens) { + $rule{'log_prefix'} = &unquote_nft_string($tokens[$j+1]); + push(@lt, $tokens[$j], $tokens[$j+1]); + $j += 2; + next; + } + if ($tokens[$j] eq 'level' && $j+1 < @tokens) { + $rule{'log_level'} = $tokens[$j+1]; + push(@lt, $tokens[$j], $tokens[$j+1]); + $j += 2; + next; + } + last; + } + $rule{'log'} = 1; + my $raw = join(" ", @lt); + push(@exprs, { 'type' => 'log', 'text' => $raw }); + $i = $j; + next; + } + if ($tok eq 'counter') { + $rule{'counter'} = 1; + push(@exprs, { 'type' => 'counter', 'text' => $tok }); + $i++; + next; + } + if ($tok =~ /^(accept|drop|reject|return)$/) { + $rule{'action'} = $tok; + push(@exprs, { 'type' => 'action', 'text' => $tok }); + $i++; + next; + } + if (($tok eq 'jump' || $tok eq 'goto') && $i+1 < @tokens) { + my $raw = $tok." ".$tokens[$i+1]; + $rule{$tok} = $tokens[$i+1]; + push(@exprs, { 'type' => $tok, 'text' => $raw }); + $i += 2; + next; + } + + push(@exprs, { 'type' => 'raw', 'text' => $tok }); + $i++; +} +$rule{'exprs'} = \@exprs; +return \%rule; +} + +sub format_rule_text +{ +my ($rule) = @_; +return "" if (!$rule || ref($rule) ne 'HASH'); +my @parts; +my %used; +my $exprs = $rule->{'exprs'}; +if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { + foreach my $e (@$exprs) { + my $type = $e->{'type'} || 'raw'; + if ($type eq 'action' || $type eq 'comment') { + next; + } + if ($type eq 'iif') { + if (!$used{'iif'} && defined($rule->{'iif'}) && $rule->{'iif'} ne '') { + my $iftype = $rule->{'iif_type'} || 'iif'; + my $ival = &escape_nft_string($rule->{'iif'}); + push(@parts, $iftype." \"".$ival."\""); + $used{'iif'} = 1; + } + next; + } + if ($type eq 'oif') { + if (!$used{'oif'} && defined($rule->{'oif'}) && $rule->{'oif'} ne '') { + my $oftype = $rule->{'oif_type'} || 'oif'; + my $oval = &escape_nft_string($rule->{'oif'}); + push(@parts, $oftype." \"".$oval."\""); + $used{'oif'} = 1; + } + next; + } + if ($type eq 'saddr') { + if (!$used{'saddr'}) { + my $addr = &format_addr_expr('saddr', $rule); + if ($addr) { + push(@parts, $addr); + $used{'saddr'} = 1; + } + } + next; + } + if ($type eq 'daddr') { + if (!$used{'daddr'}) { + my $addr = &format_addr_expr('daddr', $rule); + if ($addr) { + push(@parts, $addr); + $used{'daddr'} = 1; + } + } + next; + } + if ($type eq 'l4proto') { + if (!$used{'l4proto'}) { + my $lp = &format_l4proto_expr($rule); + if ($lp) { + push(@parts, $lp); + $used{'l4proto'} = 1; + } + } + next; + } + if ($type eq 'sport') { + if (!$used{'sport'}) { + my $sp = &format_port_expr('sport', $rule); + if ($sp) { + push(@parts, $sp); + $used{'sport'} = 1; + } + } + next; + } + if ($type eq 'dport') { + if (!$used{'dport'} && $rule->{'proto'} && $rule->{'dport'}) { + my $dp = &format_port_expr('dport', $rule); + if ($dp) { + push(@parts, $dp); + $used{'dport'} = 1; + } + } + next; + } + if ($type eq 'icmp') { + if (!$used{'icmp'} && $rule->{'icmp_type'}) { + push(@parts, "icmp type ".$rule->{'icmp_type'}); + $used{'icmp'} = 1; + } + next; + } + if ($type eq 'icmpv6') { + if (!$used{'icmpv6'} && $rule->{'icmpv6_type'}) { + push(@parts, "icmpv6 type ".$rule->{'icmpv6_type'}); + $used{'icmpv6'} = 1; + } + next; + } + if ($type eq 'ct_state') { + if (!$used{'ct_state'} && $rule->{'ct_state'}) { + push(@parts, "ct state ".$rule->{'ct_state'}); + $used{'ct_state'} = 1; + } + next; + } + if ($type eq 'tcp_flags') { + if (!$used{'tcp_flags'}) { + my $tf = &format_tcp_flags_expr($rule); + if ($tf) { + push(@parts, $tf); + $used{'tcp_flags'} = 1; + } + } + next; + } + if ($type eq 'limit') { + if (!$used{'limit'}) { + my $lim = &format_limit_expr($rule); + if ($lim) { + push(@parts, $lim); + $used{'limit'} = 1; + } + } + next; + } + if ($type eq 'log') { + if (!$used{'log'}) { + my $lg = &format_log_expr($rule); + if ($lg) { + push(@parts, $lg); + $used{'log'} = 1; + } + } + next; + } + if ($type eq 'counter') { + if (!$used{'counter'} && $rule->{'counter'}) { + push(@parts, "counter"); + $used{'counter'} = 1; + } + next; + } + if ($type eq 'jump') { + if (!$used{'jump'} && $rule->{'jump'}) { + push(@parts, "jump ".$rule->{'jump'}); + $used{'jump'} = 1; + } + next; + } + if ($type eq 'goto') { + if (!$used{'goto'} && $rule->{'goto'}) { + push(@parts, "goto ".$rule->{'goto'}); + $used{'goto'} = 1; + } + next; + } + push(@parts, $e->{'text'}) if ($e->{'text'}); + } +} +if (!$used{'iif'} && defined($rule->{'iif'}) && $rule->{'iif'} ne '') { + my $iftype = $rule->{'iif_type'} || 'iif'; + my $ival = &escape_nft_string($rule->{'iif'}); + push(@parts, $iftype." \"".$ival."\""); +} +if (!$used{'oif'} && defined($rule->{'oif'}) && $rule->{'oif'} ne '') { + my $oftype = $rule->{'oif_type'} || 'oif'; + my $oval = &escape_nft_string($rule->{'oif'}); + push(@parts, $oftype." \"".$oval."\""); +} +if (!$used{'saddr'}) { + my $addr = &format_addr_expr('saddr', $rule); + push(@parts, $addr) if ($addr); +} +if (!$used{'daddr'}) { + my $addr = &format_addr_expr('daddr', $rule); + push(@parts, $addr) if ($addr); +} +if (!$used{'l4proto'}) { + my $lp = &format_l4proto_expr($rule); + push(@parts, $lp) if ($lp); +} +if (!$used{'sport'}) { + my $sp = &format_port_expr('sport', $rule); + push(@parts, $sp) if ($sp); +} +if (!$used{'dport'}) { + my $dp = &format_port_expr('dport', $rule); + push(@parts, $dp) if ($dp); +} +if (!$used{'icmp'} && $rule->{'icmp_type'}) { + push(@parts, "icmp type ".$rule->{'icmp_type'}); +} +if (!$used{'icmpv6'} && $rule->{'icmpv6_type'}) { + push(@parts, "icmpv6 type ".$rule->{'icmpv6_type'}); +} +if (!$used{'tcp_flags'}) { + my $tf = &format_tcp_flags_expr($rule); + push(@parts, $tf) if ($tf); +} +if (!$used{'ct_state'} && $rule->{'ct_state'}) { + push(@parts, "ct state ".$rule->{'ct_state'}); +} +if (!$used{'limit'}) { + my $lim = &format_limit_expr($rule); + push(@parts, $lim) if ($lim); +} +if (!$used{'log'}) { + my $lg = &format_log_expr($rule); + push(@parts, $lg) if ($lg); +} +if (!$used{'counter'} && $rule->{'counter'}) { + push(@parts, "counter"); +} +if (!$used{'jump'} && $rule->{'jump'}) { + push(@parts, "jump ".$rule->{'jump'}); +} +if (!$used{'goto'} && $rule->{'goto'}) { + push(@parts, "goto ".$rule->{'goto'}); +} +if ($rule->{'action'} && !$rule->{'jump'} && !$rule->{'goto'}) { + push(@parts, $rule->{'action'}); +} +if (defined($rule->{'comment'}) && $rule->{'comment'} ne '') { + my $c = &escape_nft_string($rule->{'comment'}); + push(@parts, "comment \"".$c."\""); +} +my $text = join(" ", grep { defined($_) && $_ ne '' } @parts); +$text =~ s/^\s+//; +$text =~ s/\s+$//; +return $text; +} + # dump_nftables_save(@tables) # Returns a string representation of the firewall rules @@ -191,23 +769,87 @@ return undef; sub describe_rule { my ($r) = @_; -my $desc; -if ($r->{'proto'} && $r->{'dport'} && $r->{'action'}) { - $desc = &text('index_rule_desc', $r->{'action'}, $r->{'proto'}, $r->{'dport'}); +my @conds; +if ($r->{'iif'}) { + push(@conds, &text('index_rule_iif', &html_escape($r->{'iif'}))); } -elsif ($r->{'iif'} && $r->{'oif'} && $r->{'action'}) { - $desc = &text('index_rule_desc4', $r->{'action'}, $r->{'iif'}, $r->{'oif'}); +if ($r->{'oif'}) { + push(@conds, &text('index_rule_oif', &html_escape($r->{'oif'}))); } -elsif ($r->{'iif'} && $r->{'action'}) { - $desc = &text('index_rule_desc3', $r->{'action'}, $r->{'iif'}); +if ($r->{'saddr'}) { + push(@conds, &text('index_rule_saddr', &html_escape($r->{'saddr'}))); } -elsif ($r->{'oif'} && $r->{'action'}) { - $desc = &text('index_rule_desc2', $r->{'action'}, $r->{'oif'}); +if ($r->{'daddr'}) { + push(@conds, &text('index_rule_daddr', &html_escape($r->{'daddr'}))); } -else { - $desc = &html_escape($r->{'text'}); +if ($r->{'l4proto'} || ($r->{'proto'} && !$r->{'dport'} && !$r->{'sport'})) { + my $p = $r->{'l4proto'} || $r->{'proto'}; + push(@conds, &text('index_rule_proto', &html_escape($p))); } -return $desc; +if ($r->{'sport'}) { + push(@conds, &text('index_rule_sport', &html_escape($r->{'sport'}))); +} +if ($r->{'dport'}) { + push(@conds, &text('index_rule_dport', &html_escape($r->{'dport'}))); +} +if ($r->{'icmp_type'}) { + push(@conds, &text('index_rule_icmp', &html_escape($r->{'icmp_type'}))); +} +if ($r->{'icmpv6_type'}) { + push(@conds, &text('index_rule_icmpv6', &html_escape($r->{'icmpv6_type'}))); +} +if ($r->{'ct_state'}) { + push(@conds, &text('index_rule_ct', &html_escape($r->{'ct_state'}))); +} +if ($r->{'tcp_flags'}) { + my $tf = $r->{'tcp_flags'}; + if ($r->{'tcp_flags_mask'}) { + $tf = $r->{'tcp_flags_mask'}."==".$r->{'tcp_flags'}; + } + push(@conds, &text('index_rule_tcpflags', &html_escape($tf))); +} +if ($r->{'limit_rate'}) { + my $lim = $r->{'limit_rate'}; + if ($r->{'limit_burst'}) { + $lim .= " burst ".$r->{'limit_burst'}; + } + push(@conds, &text('index_rule_limit', &html_escape($lim))); +} +if ($r->{'log_prefix'}) { + push(@conds, &text('index_rule_log_prefix', &html_escape($r->{'log_prefix'}))); +} +if ($r->{'log_level'}) { + push(@conds, &text('index_rule_log_level', &html_escape($r->{'log_level'}))); +} +if ($r->{'log'} && !$r->{'log_prefix'} && !$r->{'log_level'}) { + push(@conds, &text('index_rule_log')); +} +if ($r->{'counter'}) { + push(@conds, &text('index_rule_counter')); +} + +my $action_label; +if ($r->{'jump'}) { + $action_label = &text('index_rule_jump', &html_escape($r->{'jump'})); +} +elsif ($r->{'goto'}) { + $action_label = &text('index_rule_goto', &html_escape($r->{'goto'})); +} +elsif ($r->{'action'}) { + if ($r->{'action'} eq 'return') { + $action_label = &text('index_return_action'); + } + else { + $action_label = &text('index_'.lc($r->{'action'})); + } +} +if ($action_label) { + if (@conds) { + return &text('index_rule_desc_generic', $action_label, join(", ", @conds)); + } + return &text('index_rule_desc_action', $action_label); +} +return &html_escape($r->{'text'}); } # interface_choice(name, value, blanktext) diff --git a/save_rule.cgi b/save_rule.cgi index 812ae89c0..49f8be47a 100755 --- a/save_rule.cgi +++ b/save_rule.cgi @@ -26,9 +26,81 @@ if ($in{'delete'}) { } $rule->{'comment'} = $in{'comment'}; - $rule->{'action'} = $in{'action'}; - $rule->{'proto'} = $in{'proto'}; - $rule->{'dport'} = $in{'dport'}; + my $action = $in{'action'} || 'accept'; + $rule->{'action'} = undef; + $rule->{'jump'} = undef; + $rule->{'goto'} = undef; + if ($action eq 'jump') { + $rule->{'jump'} = $in{'jump'}; + } + elsif ($action eq 'goto') { + $rule->{'goto'} = $in{'goto'}; + } + else { + $rule->{'action'} = $action; + } + + $rule->{'saddr'} = (defined($in{'saddr'}) && $in{'saddr'} ne '') ? $in{'saddr'} : undef; + $rule->{'daddr'} = (defined($in{'daddr'}) && $in{'daddr'} ne '') ? $in{'daddr'} : undef; + $rule->{'saddr_family'} = $rule->{'saddr'} ? &guess_addr_family($rule->{'saddr'}) : undef; + $rule->{'daddr_family'} = $rule->{'daddr'} ? &guess_addr_family($rule->{'daddr'}) : undef; + + my $proto = $in{'proto'}; + $proto = undef if (defined($proto) && $proto eq ''); + $rule->{'sport'} = (defined($in{'sport'}) && $in{'sport'} ne '') ? $in{'sport'} : undef; + $rule->{'dport'} = (defined($in{'dport'}) && $in{'dport'} ne '') ? $in{'dport'} : undef; + if (!$proto && ($rule->{'sport'} || $rule->{'dport'})) { + $proto = 'tcp'; + } + $rule->{'l4proto'} = undef; + $rule->{'l4proto_family'} = undef; + $rule->{'proto'} = undef; + $rule->{'sport_proto'} = undef; + if ($proto && ($proto eq 'tcp' || $proto eq 'udp')) { + $rule->{'proto'} = $proto if ($rule->{'sport'} || $rule->{'dport'}); + $rule->{'sport_proto'} = $proto if ($rule->{'sport'}); + } + elsif ($proto && $proto !~ /^(tcp|udp)$/) { + $rule->{'sport'} = undef; + $rule->{'dport'} = undef; + } + if ($proto) { + if (($proto eq 'tcp' || $proto eq 'udp') && ($rule->{'sport'} || $rule->{'dport'})) { + # L4 proto implied by port match + } + else { + $rule->{'l4proto'} = $proto; + $rule->{'l4proto_family'} = 'meta'; + } + } + + my $icmp_type = $in{'icmp_type'}; + $rule->{'icmp_type'} = undef; + $rule->{'icmpv6_type'} = undef; + if ($proto && $proto eq 'icmp') { + $rule->{'icmp_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); + } + elsif ($proto && $proto eq 'icmpv6') { + $rule->{'icmpv6_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); + } + elsif (!$proto && defined($icmp_type) && $icmp_type ne '') { + $rule->{'icmp_type'} = $icmp_type; + $rule->{'l4proto'} = 'icmp'; + $rule->{'l4proto_family'} = 'meta'; + } + + $rule->{'ct_state'} = (defined($in{'ct_state'}) && $in{'ct_state'} ne '') ? $in{'ct_state'} : undef; + $rule->{'tcp_flags'} = (defined($in{'tcp_flags'}) && $in{'tcp_flags'} ne '') ? $in{'tcp_flags'} : undef; + $rule->{'tcp_flags_mask'} = (defined($in{'tcp_flags_mask'}) && $in{'tcp_flags_mask'} ne '') ? $in{'tcp_flags_mask'} : undef; + $rule->{'limit_rate'} = (defined($in{'limit_rate'}) && $in{'limit_rate'} ne '') ? $in{'limit_rate'} : undef; + $rule->{'limit_burst'} = (defined($in{'limit_burst'}) && $in{'limit_burst'} ne '') ? $in{'limit_burst'} : undef; + + my $log_enabled = $in{'log'} || $in{'log_prefix'} || $in{'log_level'}; + $rule->{'log'} = $log_enabled ? 1 : undef; + $rule->{'log_prefix'} = $log_enabled && defined($in{'log_prefix'}) && $in{'log_prefix'} ne '' ? $in{'log_prefix'} : undef; + $rule->{'log_level'} = $log_enabled && defined($in{'log_level'}) && $in{'log_level'} ne '' ? $in{'log_level'} : undef; + $rule->{'counter'} = $in{'counter'} ? 1 : undef; + my $iif = $in{'iif'}; my $oif = $in{'oif'}; $iif = $in{'iif_other'} if (defined($iif) && $iif eq 'other'); @@ -36,24 +108,7 @@ if ($in{'delete'}) { $rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef; $rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef; - my $rule_text = ""; - if ($rule->{'proto'} && $rule->{'dport'}) { - $rule_text .= "$rule->{'proto'} dport $rule->{'dport'} "; - } - if ($rule->{'iif'}) { - $rule_text .= "iif \"$rule->{'iif'}\" "; - } - if ($rule->{'oif'}) { - $rule_text .= "oif \"$rule->{'oif'}\" "; - } - $rule_text .= $rule->{'action'}; - if ($rule->{'comment'}) { - my $comment = $rule->{'comment'}; - $comment =~ s/\\/\\\\/g; - $comment =~ s/"/\\"/g; - $rule_text .= " comment \"$comment\""; - } - $rule->{'text'} = $rule_text; + $rule->{'text'} = &format_rule_text($rule); if ($in{'new'}) { push(@{$table->{'rules'}}, $rule); diff --git a/t/rulesets/basic.nft b/t/rulesets/basic.nft new file mode 100644 index 000000000..2d43483b6 --- /dev/null +++ b/t/rulesets/basic.nft @@ -0,0 +1,8 @@ +table inet filter { + chain input { + type filter hook input priority 0; policy drop; + iif "lo" accept + ip saddr 192.168.1.0/24 tcp dport 22 accept comment "ssh" + ct state established,related accept + } +} diff --git a/t/run-tests.t b/t/run-tests.t new file mode 100755 index 000000000..a7c4634f4 --- /dev/null +++ b/t/run-tests.t @@ -0,0 +1,131 @@ +#!/usr/bin/perl +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); + +sub script_dir +{ + my $path = $0; + if ($path =~ m{^/}) { + $path =~ s{/[^/]+$}{}; + return $path; + } + my $cwd = `pwd`; + chomp($cwd); + if ($path =~ m{/}) { + $path =~ s{/[^/]+$}{}; + return $cwd.'/'.$path; + } + return $cwd; +} + +my $bindir = &script_dir(); + +my $confdir = tempdir(CLEANUP => 1); +my $vardir = tempdir(CLEANUP => 1); +open(my $cfh, ">", "$confdir/config") or die "config: $!"; +print $cfh "os_type=linux\nos_version=0\n"; +close($cfh); +open(my $vfh, ">", "$confdir/var-path") or die "var-path: $!"; +print $vfh "$vardir\n"; +close($vfh); +$ENV{'WEBMIN_CONFIG'} = $confdir; +$ENV{'WEBMIN_VAR'} = $vardir; +$ENV{'FOREIGN_MODULE_NAME'} = 'nftables'; +$ENV{'FOREIGN_ROOT_DIRECTORY'} = '/usr/libexec/webmin'; + +chdir("$bindir/..") or die "chdir: $!"; + +require "$bindir/../nftables-lib.pl"; + +sub check_fields +{ + my ($name, $got, $expect) = @_; + foreach my $k (sort keys %$expect) { + is($got->{$k}, $expect->{$k}, "$name $k"); + } +} + +my @cases = ( + { + name => 'tcp dport accept', + line => 'tcp dport 22 accept', + expect => { proto => 'tcp', dport => '22', action => 'accept' }, + }, + { + name => 'iif oif drop', + line => 'iif "eth0" oif "eth1" drop', + expect => { iif => 'eth0', oif => 'eth1', action => 'drop' }, + }, + { + name => 'comment with quotes', + line => 'tcp dport 80 accept comment "a \\"quote\\""', + expect => { proto => 'tcp', dport => '80', action => 'accept', comment => 'a "quote"' }, + }, + { + name => 'ct state', + line => 'ct state established,related accept', + expect => { ct_state => 'established,related', action => 'accept' }, + }, + { + name => 'icmp type', + line => 'icmp type echo-request accept', + expect => { icmp_type => 'echo-request', action => 'accept' }, + }, + { + name => 'limit log counter', + line => 'tcp dport 22 limit rate 10/second burst 20 packets log prefix "ssh" level info counter accept', + expect => { + proto => 'tcp', + dport => '22', + limit_rate => '10/second', + limit_burst => '20', + log_prefix => 'ssh', + log_level => 'info', + counter => 1, + action => 'accept', + }, + }, + { + name => 'unknown tokens preserved', + line => 'tcp dport 22 meta skgid 1000 accept', + expect => { proto => 'tcp', dport => '22', action => 'accept' }, + preserve => 'meta skgid 1000', + }, +); + +foreach my $c (@cases) { + my $r = &parse_rule_text($c->{line}); + ok($r && ref($r) eq 'HASH', "$c->{name} parse hash"); + check_fields($c->{name}, $r, $c->{expect}); + + my $out = &format_rule_text($r); + ok($out =~ /\S/, "$c->{name} formatted non-empty"); + if ($c->{preserve}) { + like($out, qr/\Q$c->{preserve}\E/, "$c->{name} preserves unknowns"); + } + + my $r2 = &parse_rule_text($out); + check_fields($c->{name}.' roundtrip', $r2, $c->{expect}); +} + +my $ruleset = "$bindir/rulesets/basic.nft"; +my @tables = &get_nftables_save($ruleset); +ok(@tables == 1, 'ruleset table count'); +my $t = $tables[0]; +is($t->{family}, 'inet', 'ruleset family'); +is($t->{name}, 'filter', 'ruleset name'); +my $chain = $t->{chains}->{input}; +ok($chain, 'input chain present'); +is($chain->{type}, 'filter', 'chain type'); +is($chain->{hook}, 'input', 'chain hook'); +is($chain->{priority}, '0', 'chain priority'); +is($chain->{policy}, 'drop', 'chain policy'); + +my @rules = @{$t->{rules}}; +check_fields('ruleset r1', $rules[0], { iif => 'lo', action => 'accept' }); +check_fields('ruleset r2', $rules[1], { saddr => '192.168.1.0/24', proto => 'tcp', dport => '22', action => 'accept', comment => 'ssh' }); +check_fields('ruleset r3', $rules[2], { ct_state => 'established,related', action => 'accept' }); + +done_testing(); From 8242714b99a7a99cf073bccc2571b2f9ef8d7f32 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sat, 31 Jan 2026 18:01:23 -0600 Subject: [PATCH 06/13] Update raw rule in realtime, allow editing directly --- edit_rule.cgi | 193 ++++++++++++++++++++++++++++++++++++++++++++++++-- lang/en | 4 ++ save_rule.cgi | 179 ++++++++++++++++++++++++++-------------------- 3 files changed, 295 insertions(+), 81 deletions(-) diff --git a/edit_rule.cgi b/edit_rule.cgi index c4f3663df..17b41dca9 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -16,6 +16,7 @@ my $action_sel; my $proto_sel; my $icmp_type; my $log_enabled; +my $raw_extra = ""; if ($in{'new'}) { &ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1); @@ -29,6 +30,12 @@ if ($table && $rule->{'chain'}) { $chain_hook = $chain_def ? $chain_def->{'hook'} : undef; } if ($rule) { + if ($rule->{'exprs'} && ref($rule->{'exprs'}) eq 'ARRAY') { + my @raw = map { $_->{'text'} } + grep { $_->{'type'} && $_->{'type'} eq 'raw' } + @{$rule->{'exprs'}}; + $raw_extra = join(" ", @raw); + } if ($rule->{'jump'}) { $action_sel = 'jump'; } @@ -54,6 +61,7 @@ print &ui_hidden("table", $in{'table'}); print &ui_hidden("idx", $in{'idx'}); print &ui_hidden("chain", $rule->{'chain'}); print &ui_hidden("new", $in{'new'}); +print &ui_hidden("raw_extra", $raw_extra); print &ui_table_start($text{'edit_header'}, "width=100%", 2); @@ -151,10 +159,11 @@ print &ui_table_row($text{'edit_log'}, $log_row); print &ui_table_row($text{'edit_counter'}, &ui_checkbox("counter", 1, $text{'edit_counter_enable'}, $rule->{'counter'})); -# Raw rule (read-only) -print &ui_table_row($text{'edit_raw_rule'}, - &ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, - "readonly='true'")); +# Raw rule (read-only unless edit direct is checked) +my $raw_controls = &ui_checkbox("edit_direct", 1, $text{'edit_raw_rule_direct'}, 0); +my $raw_area = &ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, + "readonly='true'"); +print &ui_table_row($text{'edit_raw_rule'}, $raw_controls."
".$raw_area); print &ui_table_end(); my @buttons; @@ -166,4 +175,180 @@ if ($in{'new'}) { } print &ui_form_end(\@buttons); +print <<'EOF'; + +EOF + &ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/lang/en b/lang/en index 36c40722f..20a6792a4 100644 --- a/lang/en +++ b/lang/en @@ -120,6 +120,7 @@ edit_log_level=Level $1 edit_counter=Counter edit_counter_enable=Enable counter edit_raw_rule=Raw rule +edit_raw_rule_direct=Edit directly edit_oif=Outgoing Interface edit_iif=Incoming Interface edit_if_any=Any @@ -139,3 +140,6 @@ delete_failed=Failed to delete table:
$1
delete_confirm=Are you sure you want to delete table $1? delete_notable=No such table selected save_failed=Failed to save rule:
$1
+save_raw_empty=Raw rule cannot be empty. +save_raw_multiline=Raw rule must be a single line. +save_invalid_rule=Raw rule is invalid: $1 diff --git a/save_rule.cgi b/save_rule.cgi index 49f8be47a..90b5f21f4 100755 --- a/save_rule.cgi +++ b/save_rule.cgi @@ -25,94 +25,119 @@ if ($in{'delete'}) { $rule = $table->{'rules'}->[$in{'idx'}]; } - $rule->{'comment'} = $in{'comment'}; - my $action = $in{'action'} || 'accept'; - $rule->{'action'} = undef; - $rule->{'jump'} = undef; - $rule->{'goto'} = undef; - if ($action eq 'jump') { - $rule->{'jump'} = $in{'jump'}; - } - elsif ($action eq 'goto') { - $rule->{'goto'} = $in{'goto'}; + if ($in{'edit_direct'}) { + my $raw = $in{'raw_rule'}; + $raw =~ s/\r//g if (defined($raw)); + $raw =~ s/^\s+// if (defined($raw)); + $raw =~ s/\s+$// if (defined($raw)); + &error($text{'save_raw_empty'}) if (!defined($raw) || $raw eq ''); + &error($text{'save_raw_multiline'}) if ($raw =~ /[\r\n]/); + $rule->{'text'} = $raw; } else { - $rule->{'action'} = $action; - } - - $rule->{'saddr'} = (defined($in{'saddr'}) && $in{'saddr'} ne '') ? $in{'saddr'} : undef; - $rule->{'daddr'} = (defined($in{'daddr'}) && $in{'daddr'} ne '') ? $in{'daddr'} : undef; - $rule->{'saddr_family'} = $rule->{'saddr'} ? &guess_addr_family($rule->{'saddr'}) : undef; - $rule->{'daddr_family'} = $rule->{'daddr'} ? &guess_addr_family($rule->{'daddr'}) : undef; - - my $proto = $in{'proto'}; - $proto = undef if (defined($proto) && $proto eq ''); - $rule->{'sport'} = (defined($in{'sport'}) && $in{'sport'} ne '') ? $in{'sport'} : undef; - $rule->{'dport'} = (defined($in{'dport'}) && $in{'dport'} ne '') ? $in{'dport'} : undef; - if (!$proto && ($rule->{'sport'} || $rule->{'dport'})) { - $proto = 'tcp'; - } - $rule->{'l4proto'} = undef; - $rule->{'l4proto_family'} = undef; - $rule->{'proto'} = undef; - $rule->{'sport_proto'} = undef; - if ($proto && ($proto eq 'tcp' || $proto eq 'udp')) { - $rule->{'proto'} = $proto if ($rule->{'sport'} || $rule->{'dport'}); - $rule->{'sport_proto'} = $proto if ($rule->{'sport'}); - } - elsif ($proto && $proto !~ /^(tcp|udp)$/) { - $rule->{'sport'} = undef; - $rule->{'dport'} = undef; - } - if ($proto) { - if (($proto eq 'tcp' || $proto eq 'udp') && ($rule->{'sport'} || $rule->{'dport'})) { - # L4 proto implied by port match + $rule->{'comment'} = $in{'comment'}; + my $action = $in{'action'} || 'accept'; + $rule->{'action'} = undef; + $rule->{'jump'} = undef; + $rule->{'goto'} = undef; + if ($action eq 'jump') { + $rule->{'jump'} = $in{'jump'}; + } + elsif ($action eq 'goto') { + $rule->{'goto'} = $in{'goto'}; } else { - $rule->{'l4proto'} = $proto; + $rule->{'action'} = $action; + } + + $rule->{'saddr'} = (defined($in{'saddr'}) && $in{'saddr'} ne '') ? $in{'saddr'} : undef; + $rule->{'daddr'} = (defined($in{'daddr'}) && $in{'daddr'} ne '') ? $in{'daddr'} : undef; + $rule->{'saddr_family'} = $rule->{'saddr'} ? &guess_addr_family($rule->{'saddr'}) : undef; + $rule->{'daddr_family'} = $rule->{'daddr'} ? &guess_addr_family($rule->{'daddr'}) : undef; + + my $proto = $in{'proto'}; + $proto = undef if (defined($proto) && $proto eq ''); + $rule->{'sport'} = (defined($in{'sport'}) && $in{'sport'} ne '') ? $in{'sport'} : undef; + $rule->{'dport'} = (defined($in{'dport'}) && $in{'dport'} ne '') ? $in{'dport'} : undef; + if (!$proto && ($rule->{'sport'} || $rule->{'dport'})) { + $proto = 'tcp'; + } + $rule->{'l4proto'} = undef; + $rule->{'l4proto_family'} = undef; + $rule->{'proto'} = undef; + $rule->{'sport_proto'} = undef; + if ($proto && ($proto eq 'tcp' || $proto eq 'udp')) { + $rule->{'proto'} = $proto if ($rule->{'sport'} || $rule->{'dport'}); + $rule->{'sport_proto'} = $proto if ($rule->{'sport'}); + } + elsif ($proto && $proto !~ /^(tcp|udp)$/) { + $rule->{'sport'} = undef; + $rule->{'dport'} = undef; + } + if ($proto) { + if (($proto eq 'tcp' || $proto eq 'udp') && ($rule->{'sport'} || $rule->{'dport'})) { + # L4 proto implied by port match + } + else { + $rule->{'l4proto'} = $proto; + $rule->{'l4proto_family'} = 'meta'; + } + } + + my $icmp_type = $in{'icmp_type'}; + $rule->{'icmp_type'} = undef; + $rule->{'icmpv6_type'} = undef; + if ($proto && $proto eq 'icmp') { + $rule->{'icmp_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); + } + elsif ($proto && $proto eq 'icmpv6') { + $rule->{'icmpv6_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); + } + elsif (!$proto && defined($icmp_type) && $icmp_type ne '') { + $rule->{'icmp_type'} = $icmp_type; + $rule->{'l4proto'} = 'icmp'; $rule->{'l4proto_family'} = 'meta'; } + + $rule->{'ct_state'} = (defined($in{'ct_state'}) && $in{'ct_state'} ne '') ? $in{'ct_state'} : undef; + $rule->{'tcp_flags'} = (defined($in{'tcp_flags'}) && $in{'tcp_flags'} ne '') ? $in{'tcp_flags'} : undef; + $rule->{'tcp_flags_mask'} = (defined($in{'tcp_flags_mask'}) && $in{'tcp_flags_mask'} ne '') ? $in{'tcp_flags_mask'} : undef; + $rule->{'limit_rate'} = (defined($in{'limit_rate'}) && $in{'limit_rate'} ne '') ? $in{'limit_rate'} : undef; + $rule->{'limit_burst'} = (defined($in{'limit_burst'}) && $in{'limit_burst'} ne '') ? $in{'limit_burst'} : undef; + + my $log_enabled = $in{'log'} || $in{'log_prefix'} || $in{'log_level'}; + $rule->{'log'} = $log_enabled ? 1 : undef; + $rule->{'log_prefix'} = $log_enabled && defined($in{'log_prefix'}) && $in{'log_prefix'} ne '' ? $in{'log_prefix'} : undef; + $rule->{'log_level'} = $log_enabled && defined($in{'log_level'}) && $in{'log_level'} ne '' ? $in{'log_level'} : undef; + $rule->{'counter'} = $in{'counter'} ? 1 : undef; + + my $iif = $in{'iif'}; + my $oif = $in{'oif'}; + $iif = $in{'iif_other'} if (defined($iif) && $iif eq 'other'); + $oif = $in{'oif_other'} if (defined($oif) && $oif eq 'other'); + $rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef; + $rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef; + + $rule->{'text'} = &format_rule_text($rule); } - my $icmp_type = $in{'icmp_type'}; - $rule->{'icmp_type'} = undef; - $rule->{'icmpv6_type'} = undef; - if ($proto && $proto eq 'icmp') { - $rule->{'icmp_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); - } - elsif ($proto && $proto eq 'icmpv6') { - $rule->{'icmpv6_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); - } - elsif (!$proto && defined($icmp_type) && $icmp_type ne '') { - $rule->{'icmp_type'} = $icmp_type; - $rule->{'l4proto'} = 'icmp'; - $rule->{'l4proto_family'} = 'meta'; - } - - $rule->{'ct_state'} = (defined($in{'ct_state'}) && $in{'ct_state'} ne '') ? $in{'ct_state'} : undef; - $rule->{'tcp_flags'} = (defined($in{'tcp_flags'}) && $in{'tcp_flags'} ne '') ? $in{'tcp_flags'} : undef; - $rule->{'tcp_flags_mask'} = (defined($in{'tcp_flags_mask'}) && $in{'tcp_flags_mask'} ne '') ? $in{'tcp_flags_mask'} : undef; - $rule->{'limit_rate'} = (defined($in{'limit_rate'}) && $in{'limit_rate'} ne '') ? $in{'limit_rate'} : undef; - $rule->{'limit_burst'} = (defined($in{'limit_burst'}) && $in{'limit_burst'} ne '') ? $in{'limit_burst'} : undef; - - my $log_enabled = $in{'log'} || $in{'log_prefix'} || $in{'log_level'}; - $rule->{'log'} = $log_enabled ? 1 : undef; - $rule->{'log_prefix'} = $log_enabled && defined($in{'log_prefix'}) && $in{'log_prefix'} ne '' ? $in{'log_prefix'} : undef; - $rule->{'log_level'} = $log_enabled && defined($in{'log_level'}) && $in{'log_level'} ne '' ? $in{'log_level'} : undef; - $rule->{'counter'} = $in{'counter'} ? 1 : undef; - - my $iif = $in{'iif'}; - my $oif = $in{'oif'}; - $iif = $in{'iif_other'} if (defined($iif) && $iif eq 'other'); - $oif = $in{'oif_other'} if (defined($oif) && $oif eq 'other'); - $rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef; - $rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef; - - $rule->{'text'} = &format_rule_text($rule); - if ($in{'new'}) { push(@{$table->{'rules'}}, $rule); } + + if ($in{'edit_direct'}) { + my $cmd = $config{'nft_cmd'} || &has_command("nft"); + if ($cmd) { + my $tmp = &tempname(); + &open_tempfile(my $fh, ">$tmp"); + &print_tempfile($fh, &dump_nftables_save(@tables)); + &close_tempfile($fh); + my $out = &backquote_logged("$cmd -c -f $tmp 2>&1"); + &unlink_file($tmp); + &error(&text('save_invalid_rule', "
$out
")) if ($?); + } + } + &webmin_log("save", $in{'new'} ? "create" : "modify", $rule->{'text'}); } my $err = &save_configuration(@tables); From 3f96fb8adb1ab9f4dff92a90f6cc3b3e4eef5a9a Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sun, 1 Feb 2026 18:36:13 -0600 Subject: [PATCH 07/13] Add tooltips to edit_rule, hide advanced options by default --- edit_rule.cgi | 275 ++++++++++++++++++++++++++++++++------- help/action.html | 7 + help/comment.html | 6 + help/counter.html | 3 + help/counter_1.html | 3 + help/ct_state.html | 10 ++ help/daddr.html | 3 + help/dport.html | 3 + help/edit_direct.html | 4 + help/edit_direct_1.html | 4 + help/goto.html | 4 + help/icmp_type.html | 5 + help/iif.html | 3 + help/jump.html | 4 + help/limit_burst.html | 3 + help/limit_rate.html | 3 + help/log.html | 4 + help/log_1.html | 4 + help/log_level.html | 3 + help/log_prefix.html | 3 + help/oif.html | 3 + help/proto.html | 7 + help/raw_rule.html | 4 + help/saddr.html | 3 + help/sport.html | 3 + help/tcp_flags.html | 14 ++ help/tcp_flags_mask.html | 4 + lang/en | 1 + save_rule.cgi | 16 ++- 29 files changed, 359 insertions(+), 50 deletions(-) create mode 100644 help/action.html create mode 100644 help/comment.html create mode 100644 help/counter.html create mode 100644 help/counter_1.html create mode 100644 help/ct_state.html create mode 100644 help/daddr.html create mode 100644 help/dport.html create mode 100644 help/edit_direct.html create mode 100644 help/edit_direct_1.html create mode 100644 help/goto.html create mode 100644 help/icmp_type.html create mode 100644 help/iif.html create mode 100644 help/jump.html create mode 100644 help/limit_burst.html create mode 100644 help/limit_rate.html create mode 100644 help/log.html create mode 100644 help/log_1.html create mode 100644 help/log_level.html create mode 100644 help/log_prefix.html create mode 100644 help/oif.html create mode 100644 help/proto.html create mode 100644 help/raw_rule.html create mode 100644 help/saddr.html create mode 100644 help/sport.html create mode 100644 help/tcp_flags.html create mode 100644 help/tcp_flags_mask.html diff --git a/edit_rule.cgi b/edit_rule.cgi index 17b41dca9..142ace248 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -17,6 +17,23 @@ my $proto_sel; my $icmp_type; my $log_enabled; my $raw_extra = ""; +my $ct_state_sel; +my $tcp_flags_sel; +my $advanced_open; + +sub split_multi_value +{ + my ($v) = @_; + return undef if (!defined($v) || $v eq ''); + $v =~ s/^\s*\{//; + $v =~ s/\}\s*$//; + $v =~ s/^\s+//; + $v =~ s/\s+$//; + return undef if ($v eq ''); + my @vals = split(/\s*,\s*/, $v); + @vals = grep { $_ ne '' } @vals; + return \@vals; +} if ($in{'new'}) { &ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1); @@ -53,8 +70,50 @@ if ($rule) { } $proto_sel ||= 'tcp' if ($in{'new'}); $icmp_type = $rule->{'icmp_type'} || $rule->{'icmpv6_type'}; + $ct_state_sel = &split_multi_value($rule->{'ct_state'}); + $tcp_flags_sel = &split_multi_value($rule->{'tcp_flags'}); $log_enabled = $rule->{'log'} || $rule->{'log_prefix'} || $rule->{'log_level'}; } +$advanced_open = 1 if ($action_sel && ($action_sel eq 'jump' || $action_sel eq 'goto')); +$advanced_open = 1 if ($rule && ( + $rule->{'jump'} || $rule->{'goto'} || + $rule->{'iif'} || $rule->{'oif'} || + $icmp_type || + $rule->{'ct_state'} || + $rule->{'tcp_flags'} || $rule->{'tcp_flags_mask'} || + $rule->{'limit_rate'} || $rule->{'limit_burst'} || + $log_enabled || + $rule->{'counter'} +)); + +my @icmp_types = qw( + echo-reply destination-unreachable source-quench redirect echo-request + router-advertisement router-solicitation time-exceeded parameter-problem + timestamp-request timestamp-reply info-request info-reply + address-mask-request address-mask-reply +); +my @icmpv6_types = qw( + destination-unreachable packet-too-big time-exceeded parameter-problem + echo-request echo-reply mld-listener-query mld-listener-report + mld-listener-done mld-listener-reduction nd-router-solicit + nd-router-advert nd-neighbor-solicit nd-neighbor-advert nd-redirect + router-renumbering ind-neighbor-solicit ind-neighbor-advert + mld2-listener-report +); +my %icmp_seen; +my @icmp_type_opts = ( [ "", $text{'edit_proto_any'} ] ); +foreach my $t (@icmp_types, @icmpv6_types) { + next if ($icmp_seen{$t}++); + push(@icmp_type_opts, [ $t, $t ]); +} +my @ct_state_opts = ( + [ "", $text{'edit_proto_any'} ], + map { [ $_, $_ ] } qw(invalid new established related untracked), +); +my @tcp_flags_opts = ( + [ "", $text{'edit_proto_any'} ], + map { [ $_, $_ ] } qw(fin syn rst psh ack urg ecn cwr), +); print &ui_form_start("save_rule.cgi"); print &ui_hidden("table", $in{'table'}); @@ -66,11 +125,11 @@ print &ui_hidden("raw_extra", $raw_extra); print &ui_table_start($text{'edit_header'}, "width=100%", 2); # Rule comment -print &ui_table_row($text{'edit_comment'}, +print &ui_table_row(hlink($text{'edit_comment'}, "comment"), &ui_textbox("comment", $rule->{'comment'}, 50)); # Action -print &ui_table_row($text{'edit_action'}, +print &ui_table_row(hlink($text{'edit_action'}, "action"), &ui_select("action", $action_sel, [ [ "accept", $text{'index_accept'} ], @@ -81,39 +140,14 @@ print &ui_table_row($text{'edit_action'}, [ "goto", $text{'edit_goto_action'} ], ])); -# Jump/Goto target chain -print &ui_table_row($text{'edit_jump'}, - &ui_textbox("jump", $rule->{'jump'}, 20)); -print &ui_table_row($text{'edit_goto'}, - &ui_textbox("goto", $rule->{'goto'}, 20)); - -# Interfaces -if ($chain_hook && $chain_hook eq 'input') { - # Incoming interface - print &ui_table_row($text{'edit_iif'}, - &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); -} -elsif ($chain_hook && $chain_hook eq 'output') { - # Outgoing interface - print &ui_table_row($text{'edit_oif'}, - &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); -} -else { - # Forward or unknown chain - allow both - print &ui_table_row($text{'edit_iif'}, - &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); - print &ui_table_row($text{'edit_oif'}, - &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); -} - # Addresses -print &ui_table_row($text{'edit_saddr'}, +print &ui_table_row(hlink($text{'edit_saddr'}, "saddr"), &ui_textbox("saddr", $rule->{'saddr'}, 30)); -print &ui_table_row($text{'edit_daddr'}, +print &ui_table_row(hlink($text{'edit_daddr'}, "daddr"), &ui_textbox("daddr", $rule->{'daddr'}, 30)); # Protocol -print &ui_table_row($text{'edit_proto'}, +print &ui_table_row(hlink($text{'edit_proto'}, "proto"), &ui_select("proto", $proto_sel, [ [ "", $text{'edit_proto_any'} ], @@ -124,46 +158,81 @@ print &ui_table_row($text{'edit_proto'}, ])); # Ports -print &ui_table_row($text{'edit_sport'}, +print &ui_table_row(hlink($text{'edit_sport'}, "sport"), &ui_textbox("sport", $rule->{'sport'}, 10)); -print &ui_table_row($text{'edit_dport'}, +print &ui_table_row(hlink($text{'edit_dport'}, "dport"), &ui_textbox("dport", $rule->{'dport'}, 10)); +print &ui_table_end(); + +print &ui_hidden_table_start($text{'edit_advanced'}, "width=100%", 2, + "advanced", $advanced_open ? 1 : 0); + +# Jump/Goto target chain +print &ui_table_row(hlink($text{'edit_jump'}, "jump"), + &ui_textbox("jump", $rule->{'jump'}, 20)); +print &ui_table_row(hlink($text{'edit_goto'}, "goto"), + &ui_textbox("goto", $rule->{'goto'}, 20)); + +# Interfaces +if ($chain_hook && $chain_hook eq 'input') { + # Incoming interface + print &ui_table_row(hlink($text{'edit_iif'}, "iif"), + &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); +} +elsif ($chain_hook && $chain_hook eq 'output') { + # Outgoing interface + print &ui_table_row(hlink($text{'edit_oif'}, "oif"), + &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); +} +else { + # Forward or unknown chain - allow both + print &ui_table_row(hlink($text{'edit_iif'}, "iif"), + &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); + print &ui_table_row(hlink($text{'edit_oif'}, "oif"), + &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); +} + # ICMP type -print &ui_table_row($text{'edit_icmp_type'}, - &ui_textbox("icmp_type", $icmp_type, 20)); +print &ui_table_row(hlink($text{'edit_icmp_type'}, "icmp_type"), + &ui_select("icmp_type", $icmp_type, \@icmp_type_opts, 1, 0, 1)); # Conntrack state -print &ui_table_row($text{'edit_ct_state'}, - &ui_textbox("ct_state", $rule->{'ct_state'}, 30)); +print &ui_table_row(hlink($text{'edit_ct_state'}, "ct_state"), + &ui_select("ct_state", $ct_state_sel, \@ct_state_opts, 5, 1, 1)); # TCP flags -print &ui_table_row($text{'edit_tcp_flags'}, - &ui_textbox("tcp_flags", $rule->{'tcp_flags'}, 20)); -print &ui_table_row($text{'edit_tcp_flags_mask'}, +print &ui_table_row(hlink($text{'edit_tcp_flags'}, "tcp_flags"), + &ui_select("tcp_flags", $tcp_flags_sel, \@tcp_flags_opts, 8, 1, 1)); +print &ui_table_row(hlink($text{'edit_tcp_flags_mask'}, "tcp_flags_mask"), &ui_textbox("tcp_flags_mask", $rule->{'tcp_flags_mask'}, 20)); # Limit -print &ui_table_row($text{'edit_limit_rate'}, +print &ui_table_row(hlink($text{'edit_limit_rate'}, "limit_rate"), &ui_textbox("limit_rate", $rule->{'limit_rate'}, 20)); -print &ui_table_row($text{'edit_limit_burst'}, +print &ui_table_row(hlink($text{'edit_limit_burst'}, "limit_burst"), &ui_textbox("limit_burst", $rule->{'limit_burst'}, 10)); # Log -my $log_row = &ui_checkbox("log", 1, $text{'edit_log_enable'}, $log_enabled); +my $log_row = &ui_checkbox("log", 1, hlink($text{'edit_log_enable'}, "log_enable"), $log_enabled); $log_row .= "
".&text('edit_log_prefix', &ui_textbox("log_prefix", $rule->{'log_prefix'}, 20)); $log_row .= " ".&text('edit_log_level', &ui_textbox("log_level", $rule->{'log_level'}, 10)); print &ui_table_row($text{'edit_log'}, $log_row); # Counter -print &ui_table_row($text{'edit_counter'}, +print &ui_table_row(hlink($text{'edit_counter'}, "counter"), &ui_checkbox("counter", 1, $text{'edit_counter_enable'}, $rule->{'counter'})); +print &ui_hidden_table_end("advanced"); + +print &ui_table_start($text{'edit_rule'}, "width=100%", 2); + # Raw rule (read-only unless edit direct is checked) my $raw_controls = &ui_checkbox("edit_direct", 1, $text{'edit_raw_rule_direct'}, 0); my $raw_area = &ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, "readonly='true'"); -print &ui_table_row($text{'edit_raw_rule'}, $raw_controls."
".$raw_area); +print &ui_table_row(hlink($text{'edit_raw_rule'}, "raw_rule"), $raw_controls."
".$raw_area, + undef, undef, ["data-column-span='all' data-column-locked='1'"]); print &ui_table_end(); my @buttons; @@ -175,9 +244,29 @@ if ($in{'new'}) { } print &ui_form_end(\@buttons); +sub js_array +{ + my (@vals) = @_; + return "[".join(",", map { + my $v = $_; + $v =~ s/\\/\\\\/g; + $v =~ s/"/\\"/g; + "\"$v\""; + } @vals)."]"; +} + +my $icmp_js = &js_array(@icmp_types); +my $icmpv6_js = &js_array(@icmpv6_types); +my $icmp_any = $text{'edit_proto_any'}; +$icmp_any =~ s/\\/\\\\/g; +$icmp_any =~ s/"/\\"/g; + +print " +EOF + +&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); + diff --git a/help/chain_hook.html b/help/chain_hook.html new file mode 100644 index 000000000..4ba86aec1 --- /dev/null +++ b/help/chain_hook.html @@ -0,0 +1,3 @@ +
Hook
+

Base chain hook point, such as prerouting, input, forward, output, postrouting, or ingress.

+
nft(8)
diff --git a/help/chain_name.html b/help/chain_name.html new file mode 100644 index 000000000..bcc7e69e2 --- /dev/null +++ b/help/chain_name.html @@ -0,0 +1,3 @@ +
Chain name
+

Unique name for the chain within this table. Use letters, numbers, underscores, and dashes.

+
nft(8)
diff --git a/help/chain_policy.html b/help/chain_policy.html new file mode 100644 index 000000000..c0397985d --- /dev/null +++ b/help/chain_policy.html @@ -0,0 +1,3 @@ +
Policy
+

Default action for this base chain, such as accept, drop, reject, queue, or continue.

+
nft(8)
diff --git a/help/chain_priority.html b/help/chain_priority.html new file mode 100644 index 000000000..1b09c3730 --- /dev/null +++ b/help/chain_priority.html @@ -0,0 +1,3 @@ +
Priority
+

Priority for this base chain. Lower values run earlier. Common values include -300, -150, 0, or 100.

+
nft(8)
diff --git a/help/chain_type.html b/help/chain_type.html new file mode 100644 index 000000000..0279d9871 --- /dev/null +++ b/help/chain_type.html @@ -0,0 +1,3 @@ +
Chain type
+

Base chain type, such as filter, nat, or route. Leave blank to create a regular chain with no hook.

+
nft(8)
diff --git a/index.cgi b/index.cgi index f7d60d507..605973ffc 100755 --- a/index.cgi +++ b/index.cgi @@ -76,7 +76,8 @@ if (!@tables) { $rules_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); + $text{'index_policy_col'}, $text{'index_rules'}, + $text{'index_actions'} ], 100); foreach my $c (sort keys %{$curr->{'chains'}}) { my $chain_def = $curr->{'chains'}->{$c} || { }; @@ -102,18 +103,30 @@ if (!@tables) { &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". &urlize($c)."&new=1", $text{'index_radd'}); + my $actions_html = + &ui_link("edit_chain.cgi?table=$in{'table'}&chain=". + &urlize($c), $text{'index_cedit'})."
". + &ui_link("rename_chain.cgi?table=$in{'table'}&chain=". + &urlize($c), $text{'index_crename'})."
". + &ui_link("delete_chain.cgi?table=$in{'table'}&chain=". + &urlize($c), $text{'index_cdelete'}); $rules_html .= &ui_columns_row([ $c, $chain_def->{'type'} || "-", $chain_def->{'hook'} || "-", defined($chain_def->{'priority'}) ? $chain_def->{'priority'} : "-", $policy_label, - $rules_html_row + $rules_html_row, + $actions_html ]); } $rules_html .= &ui_columns_end(); $rules_html .= &ui_hr(); $rules_html .= &ui_buttons_start(); + $rules_html .= &ui_buttons_row( + "edit_chain.cgi?table=$in{'table'}&new=1", + $text{'index_chain_create'}, + $text{'index_chain_createdesc'}); $rules_html .= &ui_buttons_row("delete_table.cgi?table=$in{'table'}", $text{'index_table_delete'}, $text{'index_table_deletedesc'}); diff --git a/lang/en b/lang/en index 585e1c60f..838a0a52d 100644 --- a/lang/en +++ b/lang/en @@ -27,6 +27,10 @@ index_priority=Priority index_policy_col=Policy index_rules=Rules index_rules_none=No rules +index_actions=Actions +index_chain_create=Create chain +index_chain_createdesc=Add a new chain to this table. +index_cedit=Edit Chain index_cdelete=Delete Chain index_crename=Rename Chain index_cclear=Clear All Rules @@ -125,6 +129,35 @@ edit_advanced=Advanced options edit_oif=Outgoing Interface edit_iif=Incoming Interface edit_if_any=Any +chain_title_new=Create chain +chain_title_edit=Edit chain +chain_header=Chain Details +chain_name=Chain name +chain_type=Type +chain_hook=Hook +chain_priority=Priority +chain_policy=Policy +chain_type_none=Regular (no hook) +chain_hook_none=None +chain_policy_none=None +chain_err=Failed to save chain +chain_failed=Failed to save chain:
$1
+chain_ename=Chain name is invalid +chain_edup=A chain with that name already exists +chain_notable=No such table selected +chain_nochain=No such chain selected +chain_ebase=Base chains require type, hook, priority, and policy. +delete_chain_title=Delete chain +delete_chain_err=Failed to delete chain +delete_chain_failed=Failed to delete chain:
$1
+delete_chain_confirm=Are you sure you want to delete chain $1 from table $2? +delete_chain_inuse=Chain $1 is referenced by $2 rule(s) via jump/goto. Remove those rules first. +rename_chain_title=Rename chain +rename_chain_header=Rename chain +rename_chain_old=Current name +rename_chain_new=New name +rename_chain_ok=Rename +rename_chain_failed=Failed to rename chain:
$1
create_title=Create table create_header=Create a new table create_family=Table family diff --git a/nftables-lib.pl b/nftables-lib.pl index 8af072a05..bc3d1a352 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -181,6 +181,16 @@ return "ip6" if (defined($addr) && $addr =~ /:/); return "ip"; } +sub validate_chain_base +{ +my ($type, $hook, $priority, $policy) = @_; +if (defined($type) || defined($hook) || defined($priority) || defined($policy)) { + return 0 if (!defined($type) || !defined($hook) || + !defined($priority) || !defined($policy)); +} +return 1; +} + sub format_addr_expr { my ($dir, $rule) = @_; diff --git a/rename_chain.cgi b/rename_chain.cgi new file mode 100644 index 000000000..30ff58496 --- /dev/null +++ b/rename_chain.cgi @@ -0,0 +1,33 @@ +#!/usr/bin/perl +# rename_chain.cgi +# Rename an existing chain + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text); +&ReadParse(); + +my @tables = &get_nftables_save(); +my $table = $tables[$in{'table'}]; +$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); +print &ui_form_start("save_chain.cgi"); +print &ui_hidden("table", $in{'table'}); +print &ui_hidden("rename", 1); +print &ui_hidden("chain_old", $in{'chain'}); + +print &ui_table_start($text{'rename_chain_header'}, "width=100%", 2); +print &ui_table_row($text{'rename_chain_old'}, + "".&html_escape($in{'chain'}).""); +print &ui_table_row(hlink($text{'rename_chain_new'}, "chain_name"), + &ui_textbox("chain_name", $in{'chain'}, 20)); +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/save_chain.cgi b/save_chain.cgi new file mode 100644 index 000000000..ff865eca6 --- /dev/null +++ b/save_chain.cgi @@ -0,0 +1,99 @@ +#!/usr/bin/perl +# save_chain.cgi +# Save a new or existing chain + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text); +&ReadParse(); +&error_setup($text{'chain_err'}); + +my @tables = &get_nftables_save(); +my $table = $tables[$in{'table'}]; +$table || &error($text{'chain_notable'}); + +my $is_new = $in{'new'} ? 1 : 0; +my $is_rename = $in{'rename'} ? 1 : 0; +my $name = $in{'chain_name'}; +$name =~ s/^\s+// if (defined($name)); +$name =~ s/\s+$// if (defined($name)); +$name =~ /^\w[\w-]*$/ || &error($text{'chain_ename'}); + +my $old = $is_rename ? $in{'chain_old'} : $name; +$old =~ s/^\s+// if (defined($old)); +$old =~ s/\s+$// if (defined($old)); + +if ($is_new) { + $table->{'chains'}->{$name} && &error($text{'chain_edup'}); +} elsif ($is_rename) { + $table->{'chains'}->{$old} || &error($text{'chain_nochain'}); + if ($name ne $old && $table->{'chains'}->{$name}) { + &error($text{'chain_edup'}); + } +} else { + $table->{'chains'}->{$name} || &error($text{'chain_nochain'}); +} + +if ($is_rename) { + if ($name eq $old) { + &redirect("index.cgi?table=$in{'table'}"); + } + if ($name ne $old) { + $table->{'chains'}->{$name} = $table->{'chains'}->{$old}; + delete($table->{'chains'}->{$old}); + + foreach my $r (@{$table->{'rules'}}) { + $r->{'chain'} = $name if ($r->{'chain'} && $r->{'chain'} eq $old); + my $changed = 0; + if ($r->{'jump'} && $r->{'jump'} eq $old) { + $r->{'jump'} = $name; + $changed = 1; + } + if ($r->{'goto'} && $r->{'goto'} eq $old) { + $r->{'goto'} = $name; + $changed = 1; + } + $r->{'text'} = &format_rule_text($r) if ($changed); + } + } + + my $err = &save_configuration(@tables); + &error(&text('rename_chain_failed', $err)) if ($err); + &webmin_log("rename", "chain", $old, + { 'new' => $name, + 'table' => $table->{'name'}, + 'family' => $table->{'family'} }); + &redirect("index.cgi?table=$in{'table'}"); +} + +my $type = $in{'chain_type'}; +my $hook = $in{'chain_hook'}; +my $priority = $in{'chain_priority'}; +my $policy = $in{'chain_policy'}; + +for my $v (\$type, \$hook, \$priority, \$policy) { + $$v =~ s/^\s+// if (defined($$v)); + $$v =~ s/\s+$// if (defined($$v)); +} +$type = undef if (!defined($type) || $type eq ''); +$hook = undef if (!defined($hook) || $hook eq ''); +$priority = undef if (!defined($priority) || $priority eq ''); +$policy = undef if (!defined($policy) || $policy eq ''); + +&validate_chain_base($type, $hook, $priority, $policy) || + &error($text{'chain_ebase'}); + +my $chain = $table->{'chains'}->{$name} || { }; +$chain->{'type'} = $type; +$chain->{'hook'} = $hook; +$chain->{'priority'} = $priority; +$chain->{'policy'} = $policy; +$table->{'chains'}->{$name} = $chain; + +my $err = &save_configuration(@tables); +&error(&text('chain_failed', $err)) if ($err); + +&webmin_log($is_new ? "create" : "modify", "chain", $name, + { 'table' => $table->{'name'}, 'family' => $table->{'family'} }); +&redirect("index.cgi?table=$in{'table'}"); diff --git a/t/run-tests.t b/t/run-tests.t index a7c4634f4..2e79c8b12 100755 --- a/t/run-tests.t +++ b/t/run-tests.t @@ -128,4 +128,11 @@ check_fields('ruleset r1', $rules[0], { iif => 'lo', action => 'accept' }); check_fields('ruleset r2', $rules[1], { saddr => '192.168.1.0/24', proto => 'tcp', dport => '22', action => 'accept', comment => 'ssh' }); check_fields('ruleset r3', $rules[2], { ct_state => 'established,related', action => 'accept' }); +ok(&validate_chain_base('filter', 'input', '0', 'accept'), + 'chain base allows zero priority'); +ok(!&validate_chain_base('filter', 'input', undef, 'accept'), + 'chain base missing priority invalid'); +ok(&validate_chain_base(undef, undef, undef, undef), + 'chain base none set valid'); + done_testing(); From 31d4b6dfd65cb74a083511aae54a0f2e245615c2 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Mon, 2 Feb 2026 18:07:42 -0600 Subject: [PATCH 09/13] Re-order rules support --- images/icon.gif | Bin 0 -> 2918 bytes index.cgi | 29 +++++++++++++++++++++------- lang/en | 5 +++++ move_rule.cgi | 40 ++++++++++++++++++++++++++++++++++++++ nftables-lib.pl | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ t/run-tests.t | 24 +++++++++++++++++++++++ 6 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 images/icon.gif create mode 100755 move_rule.cgi diff --git a/images/icon.gif b/images/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..650a15379bd59079f5b03223c3ae11d5cc541764 GIT binary patch literal 2918 zcmWlak3Z9iAICp?Z^ni-%r-w_ekP43zqQkhYzYZTPL3(RQlS#A(>9D)Yvfl-jpWpI zLM6%h&iqKqH6^K}&DACSqASt&&V7Aff5GeVd_7*T7iW{dhi4oEVIZ#&Bp6`&ELOBH zGA-P%6Cjr5k_*rkgg)lBs>>fErDHw-e0dQhXJZ-w$|C&#;|-DSbb%keJypNJ8s_dh zba|nF%}tyHXxL_P?d;K)KX06_h6}b310Y%n)d0LT77dor7D||(j5!1_3hsmw*oRW14scn9icP9aDI} z&pAN03ETr16A+t>F;B}Jd&4frm5Ot7ul+2VaXfIpk}cDI++=tF@%^klF3^wfKH zz_!5^bBGLWCHrS)x;uIJvEqQ?y_#1!o}reeDFB}U@(IwG;@Ax!1ZX6~KC`^!`PWlVW~cJ@6;ZtY09UGVGGhWSx!DhNRW%Uc5`f}+`0$}S6rlEK!pu<2 z`zzYz!K{&^wmE3H8wLpkm@3CNld&x<zr82iX2KzVq4GFZ%#m_B++Av&>v}8%^l<2SsJhln}($pa3WasK>*hs7-Bt*e+RK z3d?0UmnAz8HQnU$G!rT~HZ{E9eO#}52^w@{S^w_>qP2uK+mGh>Lz={w_+kxo)|gfUx<7 z{-=YL)1!9~8b?U|wxg33cQGGx^&M|sis%x->pi`$6+@53)Wb)@HKxX9S|utt-2~*Vy69Jxna}8~ z^J%0i%X_jiN*<`c+h@FgNBE@8)yE@oc}wx>@e_HP-?z6_EZAKMDzV-rE-gJ6Tzug` zSvU2^&)m~ay8eF)?wF6r*2$55q!5-6->Htp5ZjPC6SSYfapYDt>-2%Lj*5+e&Rf}e zG>qR%{!aGdp?s6+Y!xM^JsS89`=)I|eZ9l|S%uU<8v}|^$>Oirq-5c2T$8Si=sA-4 zlO^qE4&Sk9|NGj*AZ?FPEfhwoMmuK=N1XCd+UPZ)eT&jZfgPm}Nkjff+zzq5@J@(oqj>fm4U_eWPc~v@Ve)M2s_4{u4r%srYz_)Q z_kQrZJP()Yl+zwt`{KWxt4e<5d~#jr4(iBi%I#A%j(eET`)ngO-Wizda5fqx92qqv ze{Fd(l{igJyu0+B9|u49LZT#Y=_?(%u;$96J}ZgeO;r7#!C$jPH+I+?zvtaGe?-f& zvfI6=Z0>4gBQ~Vt-)=+2_&t}VFNc;u8s>Zs=B`zT9sjqg5?oT z5){ta|pllSZUm{YXDsxu^c(RcJcF792}ky@G;X@g-a$KDt$J= zGhcP@yQPH#0R=}FI9U7~KL3_)H$tjP$t)F5_*gg4`gOb+c}6O=!R-!qI``_y`oUhk zDXy^kjI*?yp=o>rN0K#{l^&Y#(H1Bbuz`ibe@5`<277+!b0(pqLx`KkAMDklRa{`4 z*ISCv(m%G8Z+#vEsLhcI_`V6D%+vHN__Cqm3UF1ZO7UP#EMZU(I40$P6Q^=ZD8ICO=OjYZE zzdcQdBxPy8=#bP>WtM~@1+{lj!(mZwU*b_}Wop6Y1hO^bw9p$=dLq{W8rC<9tux?6 zV9csYuW;+PK6u4Ha>h=4Tuy41%^x=yAz&m(p?(B|V0Kxk$t?15IY$)7POEF)DD&vO zv&AbepU1udlzTmg@6WaTQQ0DN>7F~D{<)sgry8IqlnQd@)n|WNLVV5JrIHAS`dlEj zU-z37mD_}%9#&)Fa@_GCr5Y}_a3m@>y)?TpTs(p%M$oj~qW{8eQT4d_NeMfzokzVV z3IpEOX(wNz=|<5Cn6lSJ*L@Og51rD|lB5bxgdS^w*%A(9 zToZ?pB@;3ld>U1kSD#9Vx2_#v zsW+|rxk-s-LFsW~(#4+bM~CuOi7ip&7C~rSI{&(Noas+Wjj(@5#JlA6D~U?7`j<800I^)hH?qJ8TZg6CS#}aV1oT{!i8TJnOxJ%% zs0@SPUEs5#hbza7s*ZiLJ#RF;aGQjU6TQuNtZ{@02@W*vlp>*VMs=Ul!wZkJ#cOrE zB72SbX+>C5mfp6(7W=Zf5_=?%=vXb+ZInw^j|y?uLweUm?Ln0*c$f&geAl9$6%kCC z`6BBLPWv)*}e@+rDEM*fNE~*=itXIFd1dv>55!_##@n*k#cPs3;B`Cd{`AU zb=zm(z}opa?QP|Y6ggw#lG!KuG4nHZ_&_;!RgzDM-P%V6!yy`_htvfznEhAWFZM8{ zBNFDnQ!8aEzWb+HLRgm-WwPj+yHF}*(m*d% zZ#s-}@=%j+jQ2W!-YSlw_?q*Sx3O-sLJBF*RQ|h0?NL^$`}R0Yz0!xi4vHeBJY2jI zUHD=oWtEEnWz8t`)ERlZ&t&RZ5>dn$lIiUJ+l#U%=4xv}$O)$o%Pci*qwxSD*wtmF zfl~TuQ2BdJlb#XpXZc_GifE>PLO3_p0jOInnnCVWiMAd1cSjK>;DHnT(Q{z literal 0 HcmV?d00001 diff --git a/index.cgi b/index.cgi index 605973ffc..af3797db3 100755 --- a/index.cgi +++ b/index.cgi @@ -87,21 +87,36 @@ if (!@tables) { my @rules = grep { $_->{'chain'} eq $c } @{$curr->{'rules'}}; my $rules_html_row; if (@rules) { - my @rows; + my $ri = 0; + $rules_html_row = "\n"; foreach my $r (@rules) { my $desc = &describe_rule($r); - push(@rows, &ui_link( + my $rule_link = &ui_link( "edit_rule.cgi?table=$in{'table'}&chain=". &urlize($c)."&idx=$r->{'index'}", - $desc)); + $desc); + my $move = &ui_up_down_arrows( + "move_rule.cgi?table=$in{'table'}&chain=". + &urlize($c)."&idx=$r->{'index'}&dir=up", + "move_rule.cgi?table=$in{'table'}&chain=". + &urlize($c)."&idx=$r->{'index'}&dir=down", + $ri > 0, + $ri < $#rules); + $rules_html_row .= "". + "\n"; + $ri++; } - $rules_html_row = join("
", @rows); + $rules_html_row .= "\n"; + $rules_html_row .= "
$rule_link$move
". + &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + &urlize($c)."&new=1", $text{'index_radd'}). + "
"; } else { $rules_html_row = "$text{'index_rules_none'}"; + $rules_html_row .= "
". + &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + &urlize($c)."&new=1", $text{'index_radd'}); } - $rules_html_row .= "
". - &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". - &urlize($c)."&new=1", $text{'index_radd'}); my $actions_html = &ui_link("edit_chain.cgi?table=$in{'table'}&chain=". diff --git a/lang/en b/lang/en index 838a0a52d..4ad0b702f 100644 --- a/lang/en +++ b/lang/en @@ -177,3 +177,8 @@ save_failed=Failed to save rule:
$1
save_raw_empty=Raw rule cannot be empty. save_raw_multiline=Raw rule must be a single line. save_invalid_rule=Raw rule is invalid: $1 +move_err=Failed to move rule +move_failed=Failed to move rule:
$1
+move_notable=No such table selected +move_nochain=No such chain selected +move_norule=No such rule selected diff --git a/move_rule.cgi b/move_rule.cgi new file mode 100755 index 000000000..fef1ededb --- /dev/null +++ b/move_rule.cgi @@ -0,0 +1,40 @@ +#!/usr/bin/perl +# move_rule.cgi +# Move a rule up or down within a chain + +require './nftables-lib.pl'; +use strict; +use warnings; +our (%in, %text); +&ReadParse(); +&error_setup($text{'move_err'}); + +my @tables = &get_nftables_save(); +my $table = $tables[$in{'table'}]; +$table || &error($text{'move_notable'}); + +my $chain = $in{'chain'}; +$chain || &error($text{'move_nochain'}); + +my $dir = $in{'dir'}; +$dir = '' if (!defined($dir)); + +my $idx = $in{'idx'}; +$idx =~ /^\d+$/ || &error($text{'move_norule'}); + +my $rv = &move_rule_in_chain($table, $chain, $idx, $dir); +if (!defined($rv)) { + &error($text{'move_norule'}); +} + +if ($rv) { + my $err = &save_configuration(@tables); + &error(&text('move_failed', $err)) if ($err); + &webmin_log("move", "rule", undef, + { 'table' => $table->{'name'}, + 'family' => $table->{'family'}, + 'chain' => $chain, + 'dir' => $dir }); +} + +&redirect("index.cgi?table=$in{'table'}"); diff --git a/nftables-lib.pl b/nftables-lib.pl index bc3d1a352..7b567ced7 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -191,6 +191,56 @@ if (defined($type) || defined($hook) || defined($priority) || defined($policy)) return 1; } +sub move_rule_in_chain +{ +my ($table, $chain, $idx, $dir) = @_; +return undef if (!defined($table) || ref($table) ne 'HASH'); +return undef if (!defined($idx) || $idx !~ /^\d+$/); +return undef if (!defined($chain) || $chain eq ''); +return undef if (!$table->{'rules'} || ref($table->{'rules'}) ne 'ARRAY'); +return undef if ($idx > $#{$table->{'rules'}}); +my $rule = $table->{'rules'}->[$idx]; +return undef if (!$rule || $rule->{'chain'} ne $chain); + +my @chain_idxs; +for (my $i = 0; $i < @{$table->{'rules'}}; $i++) { + my $r = $table->{'rules'}->[$i]; + next if (!$r || ref($r) ne 'HASH'); + push(@chain_idxs, $i) if ($r->{'chain'} && $r->{'chain'} eq $chain); +} +my $pos; +for (my $i = 0; $i <= $#chain_idxs; $i++) { + if ($chain_idxs[$i] == $idx) { + $pos = $i; + last; + } +} +return undef if (!defined($pos)); + +my $swap; +if ($dir eq 'up') { + return 0 if ($pos == 0); + $swap = $chain_idxs[$pos-1]; +} +elsif ($dir eq 'down') { + return 0 if ($pos == $#chain_idxs); + $swap = $chain_idxs[$pos+1]; +} +else { + return undef; +} + +($table->{'rules'}->[$idx], $table->{'rules'}->[$swap]) = + ($table->{'rules'}->[$swap], $table->{'rules'}->[$idx]); + +for (my $i = 0; $i < @{$table->{'rules'}}; $i++) { + my $r = $table->{'rules'}->[$i]; + $r->{'index'} = $i if ($r && ref($r) eq 'HASH'); +} + +return 1; +} + sub format_addr_expr { my ($dir, $rule) = @_; diff --git a/t/run-tests.t b/t/run-tests.t index 2e79c8b12..813a128ab 100755 --- a/t/run-tests.t +++ b/t/run-tests.t @@ -135,4 +135,28 @@ ok(!&validate_chain_base('filter', 'input', undef, 'accept'), ok(&validate_chain_base(undef, undef, undef, undef), 'chain base none set valid'); +my $table_move = { + rules => [ + { chain => 'input', index => 0, text => 'r0' }, + { chain => 'input', index => 1, text => 'r1' }, + { chain => 'forward', index => 2, text => 'r2' }, + { chain => 'input', index => 3, text => 'r3' }, + ], +}; +ok(&move_rule_in_chain($table_move, 'input', 1, 'down'), + 'move rule down returns true'); +is($table_move->{rules}->[1]->{text}, 'r3', 'rule moved down in array'); +is($table_move->{rules}->[3]->{text}, 'r1', 'rule swapped down in array'); +is($table_move->{rules}->[1]->{index}, 1, 'moved rule index updated'); +is($table_move->{rules}->[3]->{index}, 3, 'swapped rule index updated'); + +my $table_move2 = { + rules => [ + { chain => 'input', index => 0, text => 'r0' }, + { chain => 'input', index => 1, text => 'r1' }, + ], +}; +is(&move_rule_in_chain($table_move2, 'input', 0, 'up'), 0, + 'top rule cannot move up'); + done_testing(); From 4368e00250e258f7b8ed2b3afce2f358bfc570bd Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Mon, 2 Feb 2026 19:45:04 -0600 Subject: [PATCH 10/13] Some perlcritic fixes --- apply.cgi | 2 +- create_table.cgi | 2 +- delete_chain.cgi | 2 +- delete_table.cgi | 2 +- edit_chain.cgi | 2 +- edit_rule.cgi | 6 ++--- index.cgi | 2 +- move_rule.cgi | 2 +- nftables-lib.pl | 44 +++++++++++++++++++--------------- rename_chain.cgi | 2 +- save_chain.cgi | 2 +- save_rule.cgi | 6 ++--- setup.cgi | 2 +- t/perlcritic.t | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 t/perlcritic.t diff --git a/apply.cgi b/apply.cgi index f01eccebc..d5de7f591 100755 --- a/apply.cgi +++ b/apply.cgi @@ -2,7 +2,7 @@ # apply.cgi # Apply the current configuration -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/create_table.cgi b/create_table.cgi index be0aa3a52..ca8c531cd 100755 --- a/create_table.cgi +++ b/create_table.cgi @@ -2,7 +2,7 @@ # create_table.cgi # Create a new nftables table -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/delete_chain.cgi b/delete_chain.cgi index 09793bc86..b58e02574 100644 --- a/delete_chain.cgi +++ b/delete_chain.cgi @@ -2,7 +2,7 @@ # delete_chain.cgi # Delete an existing nftables chain -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/delete_table.cgi b/delete_table.cgi index 9ebc69488..799f051a3 100755 --- a/delete_table.cgi +++ b/delete_table.cgi @@ -2,7 +2,7 @@ # delete_table.cgi # Delete an existing nftables table -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/edit_chain.cgi b/edit_chain.cgi index 5d1d5ebc7..5a7add608 100644 --- a/edit_chain.cgi +++ b/edit_chain.cgi @@ -2,7 +2,7 @@ # edit_chain.cgi # Display a form for creating or editing a chain -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/edit_rule.cgi b/edit_rule.cgi index 142ace248..c0fce9dcc 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -2,7 +2,7 @@ # edit_rule.cgi # Display a form for creating or editing a rule -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); @@ -24,12 +24,12 @@ my $advanced_open; sub split_multi_value { my ($v) = @_; - return undef if (!defined($v) || $v eq ''); + return if (!defined($v) || $v eq ''); $v =~ s/^\s*\{//; $v =~ s/\}\s*$//; $v =~ s/^\s+//; $v =~ s/\s+$//; - return undef if ($v eq ''); + return if ($v eq ''); my @vals = split(/\s*,\s*/, $v); @vals = grep { $_ ne '' } @vals; return \@vals; diff --git a/index.cgi b/index.cgi index af3797db3..096f464d4 100755 --- a/index.cgi +++ b/index.cgi @@ -2,7 +2,7 @@ # index.cgi # Display current nftables configuration -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); diff --git a/move_rule.cgi b/move_rule.cgi index fef1ededb..e47c38dc0 100755 --- a/move_rule.cgi +++ b/move_rule.cgi @@ -2,7 +2,7 @@ # move_rule.cgi # Move a rule up or down within a chain -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/nftables-lib.pl b/nftables-lib.pl index 7b567ced7..7edd207c8 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -1,7 +1,7 @@ # nftables-lib.pl # Functions for reading and writing nftables rules -BEGIN { push(@INC, ".."); }; +BEGIN { push(@INC, ".."); }; ## no critic use WebminCore; use strict; use warnings; @@ -28,7 +28,13 @@ my $table; my $chain; my $lnum = 0; my $content; -open(my $fh, $file); +my $fh; +if ($file =~ /\|\s*$/) { + (my $pipe_cmd = $file) =~ s/\|\s*$//; + open($fh, '-|', $pipe_cmd); +} else { + open($fh, '<', $file); +} $content = do { local $/; <$fh> }; close($fh); @@ -194,13 +200,13 @@ return 1; sub move_rule_in_chain { my ($table, $chain, $idx, $dir) = @_; -return undef if (!defined($table) || ref($table) ne 'HASH'); -return undef if (!defined($idx) || $idx !~ /^\d+$/); -return undef if (!defined($chain) || $chain eq ''); -return undef if (!$table->{'rules'} || ref($table->{'rules'}) ne 'ARRAY'); -return undef if ($idx > $#{$table->{'rules'}}); +return if (!defined($table) || ref($table) ne 'HASH'); +return if (!defined($idx) || $idx !~ /^\d+$/); +return if (!defined($chain) || $chain eq ''); +return if (!$table->{'rules'} || ref($table->{'rules'}) ne 'ARRAY'); +return if ($idx > $#{$table->{'rules'}}); my $rule = $table->{'rules'}->[$idx]; -return undef if (!$rule || $rule->{'chain'} ne $chain); +return if (!$rule || $rule->{'chain'} ne $chain); my @chain_idxs; for (my $i = 0; $i < @{$table->{'rules'}}; $i++) { @@ -215,7 +221,7 @@ for (my $i = 0; $i <= $#chain_idxs; $i++) { last; } } -return undef if (!defined($pos)); +return if (!defined($pos)); my $swap; if ($dir eq 'up') { @@ -227,7 +233,7 @@ elsif ($dir eq 'down') { $swap = $chain_idxs[$pos+1]; } else { - return undef; + return; } ($table->{'rules'}->[$idx], $table->{'rules'}->[$swap]) = @@ -245,7 +251,7 @@ sub format_addr_expr { my ($dir, $rule) = @_; my $val = $rule->{$dir}; -return undef if (!defined($val) || $val eq ''); +return if (!defined($val) || $val eq ''); my $fam = &guess_addr_family($val, $rule->{$dir."_family"}); return $fam." ".$dir." ".$val; } @@ -254,7 +260,7 @@ sub format_l4proto_expr { my ($rule) = @_; my $proto = $rule->{'l4proto'}; -return undef if (!defined($proto) || $proto eq ''); +return if (!defined($proto) || $proto eq ''); my $fam = $rule->{'l4proto_family'} || 'meta'; if ($fam eq 'ip' || $fam eq 'ip6') { return $fam." protocol ".$proto; @@ -266,7 +272,7 @@ sub format_port_expr { my ($dir, $rule) = @_; my $val = $rule->{$dir}; -return undef if (!defined($val) || $val eq ''); +return if (!defined($val) || $val eq ''); my $proto; if ($dir eq 'sport') { $proto = $rule->{'sport_proto'} || $rule->{'proto'} || $rule->{'l4proto'}; @@ -274,14 +280,14 @@ if ($dir eq 'sport') { else { $proto = $rule->{'proto'} || $rule->{'l4proto'}; } -return undef if (!defined($proto) || $proto eq ''); +return if (!defined($proto) || $proto eq ''); return $proto." ".$dir." ".$val; } sub format_tcp_flags_expr { my ($rule) = @_; -return undef if (!defined($rule->{'tcp_flags'}) || $rule->{'tcp_flags'} eq ''); +return if (!defined($rule->{'tcp_flags'}) || $rule->{'tcp_flags'} eq ''); my $val = $rule->{'tcp_flags'}; if (defined($rule->{'tcp_flags_mask'}) && $rule->{'tcp_flags_mask'} ne '') { return "tcp flags & ".$rule->{'tcp_flags_mask'}." == ".$val; @@ -292,7 +298,7 @@ return "tcp flags ".$val; sub format_limit_expr { my ($rule) = @_; -return undef if (!defined($rule->{'limit_rate'}) || $rule->{'limit_rate'} eq ''); +return if (!defined($rule->{'limit_rate'}) || $rule->{'limit_rate'} eq ''); my $out = "limit rate ".$rule->{'limit_rate'}; if (defined($rule->{'limit_burst'}) && $rule->{'limit_burst'} ne '') { my $burst = $rule->{'limit_burst'}; @@ -305,7 +311,7 @@ return $out; sub format_log_expr { my ($rule) = @_; -return undef if (!$rule->{'log'} && !$rule->{'log_prefix'} && !$rule->{'log_level'}); +return if (!$rule->{'log'} && !$rule->{'log_prefix'} && !$rule->{'log_level'}); my @p = ("log"); if (defined($rule->{'log_prefix'}) && $rule->{'log_prefix'} ne '') { my $pfx = &escape_nft_string($rule->{'log_prefix'}); @@ -808,7 +814,7 @@ my $file = $config{'save_file'} || "$module_config_directory/nftables.conf"; if ($config{'direct'}) { return &apply_restore($file); } -return undef; +return; } # apply_restore([file]) @@ -822,7 +828,7 @@ my $out = &backquote_logged("$cmd -f $file 2>&1"); if ($?) { return "
$out
"; } -return undef; +return; } # describe_rule(&rule) diff --git a/rename_chain.cgi b/rename_chain.cgi index 30ff58496..f0db759a1 100644 --- a/rename_chain.cgi +++ b/rename_chain.cgi @@ -2,7 +2,7 @@ # rename_chain.cgi # Rename an existing chain -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/save_chain.cgi b/save_chain.cgi index ff865eca6..7cf818379 100644 --- a/save_chain.cgi +++ b/save_chain.cgi @@ -2,7 +2,7 @@ # save_chain.cgi # Save a new or existing chain -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); diff --git a/save_rule.cgi b/save_rule.cgi index a610363e3..9fb7d49ed 100755 --- a/save_rule.cgi +++ b/save_rule.cgi @@ -2,7 +2,7 @@ # save_rule.cgi # Save a new or existing rule -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); @@ -14,10 +14,10 @@ my $table = $tables[$in{'table'}]; sub join_multi_value { my ($v) = @_; - return undef if (!defined($v) || $v eq ''); + return if (!defined($v) || $v eq ''); my @vals = split(/\0/, $v); @vals = grep { defined($_) && $_ ne '' } @vals; - return undef if (!@vals); + return if (!@vals); return join(",", @vals); } diff --git a/setup.cgi b/setup.cgi index d5e623e8a..ae69076c2 100644 --- a/setup.cgi +++ b/setup.cgi @@ -2,7 +2,7 @@ # setup.cgi # Create a default nftables ruleset -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); diff --git a/t/perlcritic.t b/t/perlcritic.t new file mode 100644 index 000000000..652ed58e2 --- /dev/null +++ b/t/perlcritic.t @@ -0,0 +1,61 @@ +#!/usr/bin/perl +use strict; +use warnings; +use Test::More; + +BEGIN { + eval { require Perl::Critic; 1 } + or plan skip_all => 'Perl::Critic not installed'; +} + +use File::Find; + +sub script_dir +{ + my $path = $0; + if ($path =~ m{^/}) { + $path =~ s{/[^/]+$}{}; + return $path; + } + my $cwd = `pwd`; + chomp($cwd); + if ($path =~ m{/}) { + $path =~ s{/[^/]+$}{}; + return $cwd.'/'.$path; + } + return $cwd; +} + +my $bindir = &script_dir(); +my $module_dir = "$bindir/.."; +chdir($module_dir) or die "chdir: $!"; + +my @files; +find( + sub { + return if -d; + return unless /\.(pl|cgi)\z/; + push(@files, $File::Find::name); + }, + '.' +); + +@files = sort @files; +if (!@files) { + plan skip_all => 'no perl files to check'; +} + +my $critic = Perl::Critic->new( + -severity => 5, + -profile => '', +); + +foreach my $file (@files) { + my @violations = $critic->critique($file); + is(scalar @violations, 0, "$file perlcritic"); + if (@violations) { + diag join("", @violations); + } +} + +done_testing(); From 9145c231268d01623cf43b2389ad342ee9cfb29d Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Mon, 2 Feb 2026 19:53:40 -0600 Subject: [PATCH 11/13] Don't use old function calling semantics in new code --- apply.cgi | 12 ++--- create_table.cgi | 42 +++++++-------- delete_chain.cgi | 38 ++++++------- delete_table.cgi | 30 +++++------ edit_chain.cgi | 46 ++++++++-------- edit_rule.cgi | 138 +++++++++++++++++++++++------------------------ index.cgi | 98 ++++++++++++++++----------------- move_rule.cgi | 24 ++++----- nftables-lib.pl | 132 ++++++++++++++++++++++----------------------- rename_chain.cgi | 34 ++++++------ save_chain.cgi | 42 +++++++-------- save_rule.cgi | 46 ++++++++-------- setup.cgi | 42 +++++++-------- t/perlcritic.t | 2 +- t/run-tests.t | 20 +++---- 15 files changed, 373 insertions(+), 373 deletions(-) diff --git a/apply.cgi b/apply.cgi index d5de7f591..be53676f3 100755 --- a/apply.cgi +++ b/apply.cgi @@ -6,11 +6,11 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); -&error_setup($text{'apply_err'}); +ReadParse(); +error_setup($text{'apply_err'}); -my @tables = &get_nftables_save(); -my $err = &apply_restore(); -&error($err) if ($err); +my @tables = get_nftables_save(); +my $err = apply_restore(); +error($err) if ($err); -&redirect("index.cgi"); +redirect("index.cgi"); diff --git a/create_table.cgi b/create_table.cgi index ca8c531cd..1bec9eaf7 100755 --- a/create_table.cgi +++ b/create_table.cgi @@ -6,8 +6,8 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); -&error_setup($text{'create_err'}); +ReadParse(); +error_setup($text{'create_err'}); my @families = qw(ip ip6 inet arp bridge netdev); my %family_ok = map { $_ => 1 } @families; @@ -16,13 +16,13 @@ if ($in{'create'}) { my $name = $in{'name'}; my $family = $in{'family'}; - $name =~ /^\w[\w-]*$/ || &error($text{'create_ename'}); - $family_ok{$family} || &error($text{'create_efamily'}); + $name =~ /^\w[\w-]*$/ || error($text{'create_ename'}); + $family_ok{$family} || error($text{'create_efamily'}); - my @tables = &get_nftables_save(); + my @tables = get_nftables_save(); foreach my $t (@tables) { if ($t->{'name'} eq $name && $t->{'family'} eq $family) { - &error($text{'create_edup'}); + error($text{'create_edup'}); } } @@ -30,26 +30,26 @@ if ($in{'create'}) { 'family' => $family, 'rules' => [], 'chains' => {} }); - my $err = &save_configuration(@tables); - &error(&text('create_failed', $err)) if ($err); - &webmin_log("create", "table", $name, { 'family' => $family }); + my $err = save_configuration(@tables); + error(text('create_failed', $err)) if ($err); + webmin_log("create", "table", $name, { 'family' => $family }); my $idx = $#tables; - &redirect("index.cgi?table=$idx"); + redirect("index.cgi?table=$idx"); } -&ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1); -print &ui_form_start("create_table.cgi"); -print &ui_hidden("create", 1); +ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1); +print ui_form_start("create_table.cgi"); +print ui_hidden("create", 1); -print &ui_table_start($text{'create_header'}, "width=100%", 2); -print &ui_table_row($text{'create_family'}, - &ui_select("family", $in{'family'} || "inet", +print ui_table_start($text{'create_header'}, "width=100%", 2); +print ui_table_row($text{'create_family'}, + ui_select("family", $in{'family'} || "inet", [ map { [ $_, $_ ] } @families ])); -print &ui_table_row($text{'create_name'}, - &ui_textbox("name", $in{'name'}, 20)); -print &ui_table_end(); +print ui_table_row($text{'create_name'}, + ui_textbox("name", $in{'name'}, 20)); +print ui_table_end(); -print &ui_form_end([ [ undef, $text{'create_ok'} ] ]); -&ui_print_footer("index.cgi", $text{'index_return'}); +print ui_form_end([ [ undef, $text{'create_ok'} ] ]); +ui_print_footer("index.cgi", $text{'index_return'}); diff --git a/delete_chain.cgi b/delete_chain.cgi index b58e02574..e609240ee 100644 --- a/delete_chain.cgi +++ b/delete_chain.cgi @@ -6,15 +6,15 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); -&error_setup($text{'delete_chain_err'}); +ReadParse(); +error_setup($text{'delete_chain_err'}); -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; -$table || &error($text{'chain_notable'}); +$table || error($text{'chain_notable'}); my $chain = $table->{'chains'}->{$in{'chain'}}; -$chain || &error($text{'chain_nochain'}); +$chain || error($text{'chain_nochain'}); my @refs = grep { ($_->{'jump'} && $_->{'jump'} eq $in{'chain'}) || @@ -22,33 +22,33 @@ my @refs = grep { } @{$table->{'rules'}}; if ($in{'confirm'}) { - @refs && &error(&text('delete_chain_inuse', $in{'chain'}, scalar(@refs))); + @refs && error(text('delete_chain_inuse', $in{'chain'}, scalar(@refs))); @{$table->{'rules'}} = grep { $_->{'chain'} ne $in{'chain'} } @{$table->{'rules'}}; delete($table->{'chains'}->{$in{'chain'}}); - my $err = &save_configuration(@tables); - &error(&text('delete_chain_failed', $err)) if ($err); - &webmin_log("delete", "chain", $in{'chain'}, + my $err = save_configuration(@tables); + error(text('delete_chain_failed', $err)) if ($err); + webmin_log("delete", "chain", $in{'chain'}, { 'table' => $table->{'name'}, 'family' => $table->{'family'} }); - &redirect("index.cgi?table=$in{'table'}"); + redirect("index.cgi?table=$in{'table'}"); } -&ui_print_header(undef, $text{'delete_chain_title'}, "", "intro", 1, 1); -print &ui_form_start("delete_chain.cgi"); -print &ui_hidden("table", $in{'table'}); -print &ui_hidden("chain", $in{'chain'}); +ui_print_header(undef, $text{'delete_chain_title'}, "", "intro", 1, 1); +print ui_form_start("delete_chain.cgi"); +print ui_hidden("table", $in{'table'}); +print ui_hidden("chain", $in{'chain'}); print "
", - &text('delete_chain_confirm', + text('delete_chain_confirm', "$in{'chain'}", "$table->{'family'} $table->{'name'}"), ""; if (@refs) { - print "

", &text('delete_chain_inuse', $in{'chain'}, scalar(@refs)); + print "

", text('delete_chain_inuse', $in{'chain'}, scalar(@refs)); } print "

\n"; -print &ui_submit($text{'delete'}, "confirm"); +print ui_submit($text{'delete'}, "confirm"); print "

\n"; -print &ui_form_end(); -&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); +print ui_form_end(); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/delete_table.cgi b/delete_table.cgi index 799f051a3..0621e0d50 100755 --- a/delete_table.cgi +++ b/delete_table.cgi @@ -6,31 +6,31 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); -&error_setup($text{'delete_err'}); +ReadParse(); +error_setup($text{'delete_err'}); -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; -$table || &error($text{'delete_notable'}); +$table || error($text{'delete_notable'}); if ($in{'confirm'}) { splice(@tables, $in{'table'}, 1); - my $err = &save_configuration(@tables); - &error(&text('delete_failed', $err)) if ($err); - &webmin_log("delete", "table", $table->{'name'}, + my $err = save_configuration(@tables); + error(text('delete_failed', $err)) if ($err); + webmin_log("delete", "table", $table->{'name'}, { 'family' => $table->{'family'} }); - &redirect("index.cgi"); + redirect("index.cgi"); } -&ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1); -print &ui_form_start("delete_table.cgi"); -print &ui_hidden("table", $in{'table'}); +ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1); +print ui_form_start("delete_table.cgi"); +print ui_hidden("table", $in{'table'}); print "
", - &text('delete_confirm', + text('delete_confirm', "$table->{'family'} $table->{'name'}"), "

\n"; -print &ui_submit($text{'delete'}, "confirm"); +print ui_submit($text{'delete'}, "confirm"); print "

\n"; -print &ui_form_end(); -&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); +print ui_form_end(); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/edit_chain.cgi b/edit_chain.cgi index 5a7add608..f7580b69e 100644 --- a/edit_chain.cgi +++ b/edit_chain.cgi @@ -6,23 +6,23 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); +ReadParse(); -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; -$table || &error($text{'chain_notable'}); +$table || error($text{'chain_notable'}); my $chain = { }; 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); } 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); + $chain || error($text{'chain_nochain'}); + ui_print_header(undef, $text{'chain_title_edit'}, "", "intro", 1, 1); } my @type_opts = ( @@ -38,29 +38,29 @@ my @policy_opts = ( map { [ $_, $_ ] } qw(accept drop reject return queue continue) ); -print &ui_form_start("save_chain.cgi"); -print &ui_hidden("table", $in{'table'}); -print &ui_hidden("new", $is_new); +print ui_form_start("save_chain.cgi"); +print ui_hidden("table", $in{'table'}); +print ui_hidden("new", $is_new); -print &ui_table_start($text{'chain_header'}, "width=100%", 2); +print ui_table_start($text{'chain_header'}, "width=100%", 2); my $name_tags = $is_new ? undef : "readonly"; -print &ui_table_row(hlink($text{'chain_name'}, "chain_name"), - &ui_textbox("chain_name", $chain_name, 20, 0, undef, $name_tags)); +print ui_table_row(hlink($text{'chain_name'}, "chain_name"), + ui_textbox("chain_name", $chain_name, 20, 0, undef, $name_tags)); -print &ui_table_row(hlink($text{'chain_type'}, "chain_type"), - &ui_select("chain_type", $chain->{'type'}, \@type_opts, 1, 0, 1, 0, +print ui_table_row(hlink($text{'chain_type'}, "chain_type"), + ui_select("chain_type", $chain->{'type'}, \@type_opts, 1, 0, 1, 0, "onchange='toggle_chain_base()'")); -print &ui_table_row(hlink($text{'chain_hook'}, "chain_hook"), - &ui_select("chain_hook", $chain->{'hook'}, \@hook_opts, 1, 0, 1)); -print &ui_table_row(hlink($text{'chain_priority'}, "chain_priority"), - &ui_textbox("chain_priority", $chain->{'priority'}, 10)); -print &ui_table_row(hlink($text{'chain_policy'}, "chain_policy"), - &ui_select("chain_policy", $chain->{'policy'}, \@policy_opts, 1, 0, 1)); +print ui_table_row(hlink($text{'chain_hook'}, "chain_hook"), + ui_select("chain_hook", $chain->{'hook'}, \@hook_opts, 1, 0, 1)); +print ui_table_row(hlink($text{'chain_priority'}, "chain_priority"), + ui_textbox("chain_priority", $chain->{'priority'}, 10)); +print ui_table_row(hlink($text{'chain_policy'}, "chain_policy"), + ui_select("chain_policy", $chain->{'policy'}, \@policy_opts, 1, 0, 1)); -print &ui_table_end(); +print ui_table_end(); -print &ui_form_end([ [ undef, $text{$is_new ? 'create' : 'save'} ] ]); +print ui_form_end([ [ undef, $text{$is_new ? 'create' : 'save'} ] ]); print <<'EOF'; EOF -&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/edit_rule.cgi b/edit_rule.cgi index c0fce9dcc..7b43c8b59 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -6,8 +6,8 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); -&ReadParse(); -my @tables = &get_nftables_save(); +ReadParse(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; my $rule; my $chain_def; @@ -36,10 +36,10 @@ 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); $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); $rule = $table->{'rules'}->[$in{'idx'}]; } if ($table && $rule->{'chain'}) { @@ -70,8 +70,8 @@ if ($rule) { } $proto_sel ||= 'tcp' if ($in{'new'}); $icmp_type = $rule->{'icmp_type'} || $rule->{'icmpv6_type'}; - $ct_state_sel = &split_multi_value($rule->{'ct_state'}); - $tcp_flags_sel = &split_multi_value($rule->{'tcp_flags'}); + $ct_state_sel = split_multi_value($rule->{'ct_state'}); + $tcp_flags_sel = split_multi_value($rule->{'tcp_flags'}); $log_enabled = $rule->{'log'} || $rule->{'log_prefix'} || $rule->{'log_level'}; } $advanced_open = 1 if ($action_sel && ($action_sel eq 'jump' || $action_sel eq 'goto')); @@ -115,22 +115,22 @@ my @tcp_flags_opts = ( map { [ $_, $_ ] } qw(fin syn rst psh ack urg ecn cwr), ); -print &ui_form_start("save_rule.cgi"); -print &ui_hidden("table", $in{'table'}); -print &ui_hidden("idx", $in{'idx'}); -print &ui_hidden("chain", $rule->{'chain'}); -print &ui_hidden("new", $in{'new'}); -print &ui_hidden("raw_extra", $raw_extra); +print ui_form_start("save_rule.cgi"); +print ui_hidden("table", $in{'table'}); +print ui_hidden("idx", $in{'idx'}); +print ui_hidden("chain", $rule->{'chain'}); +print ui_hidden("new", $in{'new'}); +print ui_hidden("raw_extra", $raw_extra); -print &ui_table_start($text{'edit_header'}, "width=100%", 2); +print ui_table_start($text{'edit_header'}, "width=100%", 2); # Rule comment -print &ui_table_row(hlink($text{'edit_comment'}, "comment"), - &ui_textbox("comment", $rule->{'comment'}, 50)); +print ui_table_row(hlink($text{'edit_comment'}, "comment"), + ui_textbox("comment", $rule->{'comment'}, 50)); # Action -print &ui_table_row(hlink($text{'edit_action'}, "action"), - &ui_select("action", $action_sel, +print ui_table_row(hlink($text{'edit_action'}, "action"), + ui_select("action", $action_sel, [ [ "accept", $text{'index_accept'} ], [ "drop", $text{'index_drop'} ], @@ -141,14 +141,14 @@ print &ui_table_row(hlink($text{'edit_action'}, "action"), ])); # Addresses -print &ui_table_row(hlink($text{'edit_saddr'}, "saddr"), - &ui_textbox("saddr", $rule->{'saddr'}, 30)); -print &ui_table_row(hlink($text{'edit_daddr'}, "daddr"), - &ui_textbox("daddr", $rule->{'daddr'}, 30)); +print ui_table_row(hlink($text{'edit_saddr'}, "saddr"), + ui_textbox("saddr", $rule->{'saddr'}, 30)); +print ui_table_row(hlink($text{'edit_daddr'}, "daddr"), + ui_textbox("daddr", $rule->{'daddr'}, 30)); # Protocol -print &ui_table_row(hlink($text{'edit_proto'}, "proto"), - &ui_select("proto", $proto_sel, +print ui_table_row(hlink($text{'edit_proto'}, "proto"), + ui_select("proto", $proto_sel, [ [ "", $text{'edit_proto_any'} ], [ "tcp", "TCP" ], @@ -158,83 +158,83 @@ print &ui_table_row(hlink($text{'edit_proto'}, "proto"), ])); # Ports -print &ui_table_row(hlink($text{'edit_sport'}, "sport"), - &ui_textbox("sport", $rule->{'sport'}, 10)); -print &ui_table_row(hlink($text{'edit_dport'}, "dport"), - &ui_textbox("dport", $rule->{'dport'}, 10)); +print ui_table_row(hlink($text{'edit_sport'}, "sport"), + ui_textbox("sport", $rule->{'sport'}, 10)); +print ui_table_row(hlink($text{'edit_dport'}, "dport"), + ui_textbox("dport", $rule->{'dport'}, 10)); -print &ui_table_end(); +print ui_table_end(); -print &ui_hidden_table_start($text{'edit_advanced'}, "width=100%", 2, +print ui_hidden_table_start($text{'edit_advanced'}, "width=100%", 2, "advanced", $advanced_open ? 1 : 0); # Jump/Goto target chain -print &ui_table_row(hlink($text{'edit_jump'}, "jump"), - &ui_textbox("jump", $rule->{'jump'}, 20)); -print &ui_table_row(hlink($text{'edit_goto'}, "goto"), - &ui_textbox("goto", $rule->{'goto'}, 20)); +print ui_table_row(hlink($text{'edit_jump'}, "jump"), + ui_textbox("jump", $rule->{'jump'}, 20)); +print ui_table_row(hlink($text{'edit_goto'}, "goto"), + ui_textbox("goto", $rule->{'goto'}, 20)); # Interfaces if ($chain_hook && $chain_hook eq 'input') { # Incoming interface - print &ui_table_row(hlink($text{'edit_iif'}, "iif"), - &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); + print ui_table_row(hlink($text{'edit_iif'}, "iif"), + interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); } elsif ($chain_hook && $chain_hook eq 'output') { # Outgoing interface - print &ui_table_row(hlink($text{'edit_oif'}, "oif"), - &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); + print ui_table_row(hlink($text{'edit_oif'}, "oif"), + interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); } else { # Forward or unknown chain - allow both - print &ui_table_row(hlink($text{'edit_iif'}, "iif"), - &interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); - print &ui_table_row(hlink($text{'edit_oif'}, "oif"), - &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); + print ui_table_row(hlink($text{'edit_iif'}, "iif"), + interface_choice("iif", $rule->{'iif'}, $text{'edit_if_any'})); + print ui_table_row(hlink($text{'edit_oif'}, "oif"), + interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); } # ICMP type -print &ui_table_row(hlink($text{'edit_icmp_type'}, "icmp_type"), - &ui_select("icmp_type", $icmp_type, \@icmp_type_opts, 1, 0, 1)); +print ui_table_row(hlink($text{'edit_icmp_type'}, "icmp_type"), + ui_select("icmp_type", $icmp_type, \@icmp_type_opts, 1, 0, 1)); # Conntrack state -print &ui_table_row(hlink($text{'edit_ct_state'}, "ct_state"), - &ui_select("ct_state", $ct_state_sel, \@ct_state_opts, 5, 1, 1)); +print ui_table_row(hlink($text{'edit_ct_state'}, "ct_state"), + ui_select("ct_state", $ct_state_sel, \@ct_state_opts, 5, 1, 1)); # TCP flags -print &ui_table_row(hlink($text{'edit_tcp_flags'}, "tcp_flags"), - &ui_select("tcp_flags", $tcp_flags_sel, \@tcp_flags_opts, 8, 1, 1)); -print &ui_table_row(hlink($text{'edit_tcp_flags_mask'}, "tcp_flags_mask"), - &ui_textbox("tcp_flags_mask", $rule->{'tcp_flags_mask'}, 20)); +print ui_table_row(hlink($text{'edit_tcp_flags'}, "tcp_flags"), + ui_select("tcp_flags", $tcp_flags_sel, \@tcp_flags_opts, 8, 1, 1)); +print ui_table_row(hlink($text{'edit_tcp_flags_mask'}, "tcp_flags_mask"), + ui_textbox("tcp_flags_mask", $rule->{'tcp_flags_mask'}, 20)); # Limit -print &ui_table_row(hlink($text{'edit_limit_rate'}, "limit_rate"), - &ui_textbox("limit_rate", $rule->{'limit_rate'}, 20)); -print &ui_table_row(hlink($text{'edit_limit_burst'}, "limit_burst"), - &ui_textbox("limit_burst", $rule->{'limit_burst'}, 10)); +print ui_table_row(hlink($text{'edit_limit_rate'}, "limit_rate"), + ui_textbox("limit_rate", $rule->{'limit_rate'}, 20)); +print ui_table_row(hlink($text{'edit_limit_burst'}, "limit_burst"), + ui_textbox("limit_burst", $rule->{'limit_burst'}, 10)); # Log -my $log_row = &ui_checkbox("log", 1, hlink($text{'edit_log_enable'}, "log_enable"), $log_enabled); -$log_row .= "
".&text('edit_log_prefix', &ui_textbox("log_prefix", $rule->{'log_prefix'}, 20)); -$log_row .= " ".&text('edit_log_level', &ui_textbox("log_level", $rule->{'log_level'}, 10)); -print &ui_table_row($text{'edit_log'}, $log_row); +my $log_row = ui_checkbox("log", 1, hlink($text{'edit_log_enable'}, "log_enable"), $log_enabled); +$log_row .= "
".text('edit_log_prefix', ui_textbox("log_prefix", $rule->{'log_prefix'}, 20)); +$log_row .= " ".text('edit_log_level', ui_textbox("log_level", $rule->{'log_level'}, 10)); +print ui_table_row($text{'edit_log'}, $log_row); # Counter -print &ui_table_row(hlink($text{'edit_counter'}, "counter"), - &ui_checkbox("counter", 1, $text{'edit_counter_enable'}, $rule->{'counter'})); +print ui_table_row(hlink($text{'edit_counter'}, "counter"), + ui_checkbox("counter", 1, $text{'edit_counter_enable'}, $rule->{'counter'})); -print &ui_hidden_table_end("advanced"); +print ui_hidden_table_end("advanced"); -print &ui_table_start($text{'edit_rule'}, "width=100%", 2); +print ui_table_start($text{'edit_rule'}, "width=100%", 2); # Raw rule (read-only unless edit direct is checked) -my $raw_controls = &ui_checkbox("edit_direct", 1, $text{'edit_raw_rule_direct'}, 0); -my $raw_area = &ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, +my $raw_controls = ui_checkbox("edit_direct", 1, $text{'edit_raw_rule_direct'}, 0); +my $raw_area = ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, "readonly='true'"); -print &ui_table_row(hlink($text{'edit_raw_rule'}, "raw_rule"), $raw_controls."
".$raw_area, +print ui_table_row(hlink($text{'edit_raw_rule'}, "raw_rule"), $raw_controls."
".$raw_area, undef, undef, ["data-column-span='all' data-column-locked='1'"]); -print &ui_table_end(); +print ui_table_end(); my @buttons; if ($in{'new'}) { push(@buttons, [ undef, $text{'create'} ]); @@ -242,7 +242,7 @@ if ($in{'new'}) { push(@buttons, [ undef, $text{'save'} ]); push(@buttons, [ 'delete', $text{'delete'} ]); } -print &ui_form_end(\@buttons); +print ui_form_end(\@buttons); sub js_array { @@ -255,8 +255,8 @@ sub js_array } @vals)."]"; } -my $icmp_js = &js_array(@icmp_types); -my $icmpv6_js = &js_array(@icmpv6_types); +my $icmp_js = js_array(@icmp_types); +my $icmpv6_js = js_array(@icmpv6_types); my $icmp_any = $text{'edit_proto_any'}; $icmp_any =~ s/\\/\\\\/g; $icmp_any =~ s/"/\\"/g; @@ -530,4 +530,4 @@ print <<'EOF'; EOF -&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/index.cgi b/index.cgi index 096f464d4..4436ea22c 100755 --- a/index.cgi +++ b/index.cgi @@ -6,44 +6,44 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); -&ReadParse(); +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); } # Check for nft command -my $cmd = $config{'nft_cmd'} || &has_command("nft"); +my $cmd = $config{'nft_cmd'} || has_command("nft"); if (!$cmd) { - print &text('index_ecommand', "nft"); + print text('index_ecommand', "nft"); if (!$partial) { - &ui_print_footer("/", $text{'index'}); + ui_print_footer("/", $text{'index'}); } exit; } # Check if kernel supports it (basic check) -my $out = &backquote_command("$cmd list ruleset 2>&1"); +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
"); + print text('index_ekernel', "
$out
"); if (!$partial) { - &ui_print_footer("/", $text{'index'}); + ui_print_footer("/", $text{'index'}); } exit; } # Load tables -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $rules_html = ""; if (!@tables) { $rules_html .= "$text{'index_none'}

\n"; - $rules_html .= &ui_buttons_start(); - $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'}, + $rules_html .= ui_buttons_start(); + $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_end(); + $rules_html .= ui_buttons_end(); } else { # Select table if (!defined($in{'table'}) || $in{'table'} !~ /^\d+$/ || @@ -57,14 +57,14 @@ if (!@tables) { } if (!$partial) { - print &ui_form_start("index.cgi"); + print ui_form_start("index.cgi"); print "

\n"; - print &text('index_change')," "; - print &ui_select("table", $in{'table'}, \@table_opts, 1, 0, 1, 0, + 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'"); + print ui_submit("", "nft_submit", 0, "style='display:none'"); print "
\n"; - print &ui_form_end(); + print ui_form_end(); } # Identify current table @@ -72,8 +72,8 @@ if (!@tables) { if ($curr) { # Show chains and rules - $rules_html .= &ui_hr(); - $rules_html .= &ui_columns_start( + $rules_html .= ui_hr(); + $rules_html .= ui_columns_start( [ $text{'index_chain_col'}, $text{'index_type'}, $text{'index_hook'}, $text{'index_priority'}, $text{'index_policy_col'}, $text{'index_rules'}, @@ -90,16 +90,16 @@ if (!@tables) { my $ri = 0; $rules_html_row = "\n"; foreach my $r (@rules) { - my $desc = &describe_rule($r); - my $rule_link = &ui_link( + my $desc = describe_rule($r); + my $rule_link = ui_link( "edit_rule.cgi?table=$in{'table'}&chain=". - &urlize($c)."&idx=$r->{'index'}", + urlize($c)."&idx=$r->{'index'}", $desc); - my $move = &ui_up_down_arrows( + my $move = ui_up_down_arrows( "move_rule.cgi?table=$in{'table'}&chain=". - &urlize($c)."&idx=$r->{'index'}&dir=up", + urlize($c)."&idx=$r->{'index'}&dir=up", "move_rule.cgi?table=$in{'table'}&chain=". - &urlize($c)."&idx=$r->{'index'}&dir=down", + urlize($c)."&idx=$r->{'index'}&dir=down", $ri > 0, $ri < $#rules); $rules_html_row .= "". @@ -107,25 +107,25 @@ if (!@tables) { $ri++; } $rules_html_row .= "\n"; $rules_html_row .= "
$rule_link
". - &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". - &urlize($c)."&new=1", $text{'index_radd'}). + ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + urlize($c)."&new=1", $text{'index_radd'}). "
"; } else { $rules_html_row = "$text{'index_rules_none'}"; $rules_html_row .= "
". - &ui_link("edit_rule.cgi?table=$in{'table'}&chain=". - &urlize($c)."&new=1", $text{'index_radd'}); + ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + urlize($c)."&new=1", $text{'index_radd'}); } my $actions_html = - &ui_link("edit_chain.cgi?table=$in{'table'}&chain=". - &urlize($c), $text{'index_cedit'})."
". - &ui_link("rename_chain.cgi?table=$in{'table'}&chain=". - &urlize($c), $text{'index_crename'})."
". - &ui_link("delete_chain.cgi?table=$in{'table'}&chain=". - &urlize($c), $text{'index_cdelete'}); - $rules_html .= &ui_columns_row([ + ui_link("edit_chain.cgi?table=$in{'table'}&chain=". + urlize($c), $text{'index_cedit'})."
". + ui_link("rename_chain.cgi?table=$in{'table'}&chain=". + urlize($c), $text{'index_crename'})."
". + ui_link("delete_chain.cgi?table=$in{'table'}&chain=". + urlize($c), $text{'index_cdelete'}); + $rules_html .= ui_columns_row([ $c, $chain_def->{'type'} || "-", $chain_def->{'hook'} || "-", @@ -135,17 +135,17 @@ if (!@tables) { $actions_html ]); } - $rules_html .= &ui_columns_end(); - $rules_html .= &ui_hr(); - $rules_html .= &ui_buttons_start(); - $rules_html .= &ui_buttons_row( + $rules_html .= ui_columns_end(); + $rules_html .= ui_hr(); + $rules_html .= ui_buttons_start(); + $rules_html .= ui_buttons_row( "edit_chain.cgi?table=$in{'table'}&new=1", $text{'index_chain_create'}, $text{'index_chain_createdesc'}); - $rules_html .= &ui_buttons_row("delete_table.cgi?table=$in{'table'}", + $rules_html .= ui_buttons_row("delete_table.cgi?table=$in{'table'}", $text{'index_table_delete'}, $text{'index_table_deletedesc'}); - $rules_html .= &ui_buttons_end(); + $rules_html .= ui_buttons_end(); } } @@ -159,12 +159,12 @@ print $rules_html; print "\n"; if (@tables) { - print &ui_hr(); - print &ui_buttons_start(); - print &ui_buttons_row("create_table.cgi", $text{'index_table_create'}, + print ui_hr(); + print ui_buttons_start(); + print ui_buttons_row("create_table.cgi", $text{'index_table_create'}, $text{'index_table_createdesc'}); - print &ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'}); - print &ui_buttons_end(); + print ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'}); + print ui_buttons_end(); } -&ui_print_footer("/", $text{'index'}); +ui_print_footer("/", $text{'index'}); diff --git a/move_rule.cgi b/move_rule.cgi index e47c38dc0..456bc26de 100755 --- a/move_rule.cgi +++ b/move_rule.cgi @@ -6,35 +6,35 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); -&error_setup($text{'move_err'}); +ReadParse(); +error_setup($text{'move_err'}); -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; -$table || &error($text{'move_notable'}); +$table || error($text{'move_notable'}); my $chain = $in{'chain'}; -$chain || &error($text{'move_nochain'}); +$chain || error($text{'move_nochain'}); my $dir = $in{'dir'}; $dir = '' if (!defined($dir)); my $idx = $in{'idx'}; -$idx =~ /^\d+$/ || &error($text{'move_norule'}); +$idx =~ /^\d+$/ || error($text{'move_norule'}); -my $rv = &move_rule_in_chain($table, $chain, $idx, $dir); +my $rv = move_rule_in_chain($table, $chain, $idx, $dir); if (!defined($rv)) { - &error($text{'move_norule'}); + error($text{'move_norule'}); } if ($rv) { - my $err = &save_configuration(@tables); - &error(&text('move_failed', $err)) if ($err); - &webmin_log("move", "rule", undef, + my $err = save_configuration(@tables); + error(text('move_failed', $err)) if ($err); + webmin_log("move", "rule", undef, { 'table' => $table->{'name'}, 'family' => $table->{'family'}, 'chain' => $chain, 'dir' => $dir }); } -&redirect("index.cgi?table=$in{'table'}"); +redirect("index.cgi?table=$in{'table'}"); diff --git a/nftables-lib.pl b/nftables-lib.pl index 7edd207c8..8df33f198 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -6,14 +6,14 @@ use WebminCore; use strict; use warnings; our (%config, $module_config_directory); -&init_config(); +init_config(); # get_nftables_save([file]) # Returns a list of tables and their chains/rules sub get_nftables_save { my ($file) = @_; -my $cmd = $config{'nft_cmd'} || &has_command("nft"); +my $cmd = $config{'nft_cmd'} || has_command("nft"); if (!$file) { if ($config{'direct'}) { $file = "$cmd list ruleset |"; @@ -80,7 +80,7 @@ for(my $i=0; $i<@lines; $i++) { 'index' => scalar(@{$table->{'rules'}}), 'line' => $lnum }; - my $parsed = &parse_rule_text($rule_str); + my $parsed = parse_rule_text($rule_str); if ($parsed) { foreach my $k (keys %$parsed) { $rule->{$k} = $parsed->{$k}; @@ -252,7 +252,7 @@ sub format_addr_expr my ($dir, $rule) = @_; my $val = $rule->{$dir}; return if (!defined($val) || $val eq ''); -my $fam = &guess_addr_family($val, $rule->{$dir."_family"}); +my $fam = guess_addr_family($val, $rule->{$dir."_family"}); return $fam." ".$dir." ".$val; } @@ -314,7 +314,7 @@ my ($rule) = @_; return if (!$rule->{'log'} && !$rule->{'log_prefix'} && !$rule->{'log_level'}); my @p = ("log"); if (defined($rule->{'log_prefix'}) && $rule->{'log_prefix'} ne '') { - my $pfx = &escape_nft_string($rule->{'log_prefix'}); + my $pfx = escape_nft_string($rule->{'log_prefix'}); push(@p, "prefix", "\"".$pfx."\""); } if (defined($rule->{'log_level'}) && $rule->{'log_level'} ne '') { @@ -328,7 +328,7 @@ sub parse_rule_text my ($line) = @_; return { } if (!defined($line)); my %rule; -my @tokens = &tokenize_nft_rule($line); +my @tokens = tokenize_nft_rule($line); my @exprs; my $i = 0; while ($i < @tokens) { @@ -336,14 +336,14 @@ while ($i < @tokens) { if ($tok eq 'comment' && $i+1 < @tokens) { my $raw = $tokens[$i]." ".$tokens[$i+1]; - $rule{'comment'} = &unquote_nft_string($tokens[$i+1]); + $rule{'comment'} = unquote_nft_string($tokens[$i+1]); push(@exprs, { 'type' => 'comment', 'text' => $raw }); $i += 2; next; } if (($tok eq 'iif' || $tok eq 'iifname') && $i+1 < @tokens) { my $raw = $tok." ".$tokens[$i+1]; - $rule{'iif'} = &unquote_nft_string($tokens[$i+1]); + $rule{'iif'} = unquote_nft_string($tokens[$i+1]); $rule{'iif_type'} = $tok; push(@exprs, { 'type' => 'iif', 'text' => $raw }); $i += 2; @@ -351,7 +351,7 @@ while ($i < @tokens) { } if (($tok eq 'oif' || $tok eq 'oifname') && $i+1 < @tokens) { my $raw = $tok." ".$tokens[$i+1]; - $rule{'oif'} = &unquote_nft_string($tokens[$i+1]); + $rule{'oif'} = unquote_nft_string($tokens[$i+1]); $rule{'oif_type'} = $tok; push(@exprs, { 'type' => 'oif', 'text' => $raw }); $i += 2; @@ -477,7 +477,7 @@ while ($i < @tokens) { my @lt = ($tok); while ($j < @tokens) { if ($tokens[$j] eq 'prefix' && $j+1 < @tokens) { - $rule{'log_prefix'} = &unquote_nft_string($tokens[$j+1]); + $rule{'log_prefix'} = unquote_nft_string($tokens[$j+1]); push(@lt, $tokens[$j], $tokens[$j+1]); $j += 2; next; @@ -539,7 +539,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { if ($type eq 'iif') { if (!$used{'iif'} && defined($rule->{'iif'}) && $rule->{'iif'} ne '') { my $iftype = $rule->{'iif_type'} || 'iif'; - my $ival = &escape_nft_string($rule->{'iif'}); + my $ival = escape_nft_string($rule->{'iif'}); push(@parts, $iftype." \"".$ival."\""); $used{'iif'} = 1; } @@ -548,7 +548,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { if ($type eq 'oif') { if (!$used{'oif'} && defined($rule->{'oif'}) && $rule->{'oif'} ne '') { my $oftype = $rule->{'oif_type'} || 'oif'; - my $oval = &escape_nft_string($rule->{'oif'}); + my $oval = escape_nft_string($rule->{'oif'}); push(@parts, $oftype." \"".$oval."\""); $used{'oif'} = 1; } @@ -556,7 +556,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'saddr') { if (!$used{'saddr'}) { - my $addr = &format_addr_expr('saddr', $rule); + my $addr = format_addr_expr('saddr', $rule); if ($addr) { push(@parts, $addr); $used{'saddr'} = 1; @@ -566,7 +566,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'daddr') { if (!$used{'daddr'}) { - my $addr = &format_addr_expr('daddr', $rule); + my $addr = format_addr_expr('daddr', $rule); if ($addr) { push(@parts, $addr); $used{'daddr'} = 1; @@ -576,7 +576,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'l4proto') { if (!$used{'l4proto'}) { - my $lp = &format_l4proto_expr($rule); + my $lp = format_l4proto_expr($rule); if ($lp) { push(@parts, $lp); $used{'l4proto'} = 1; @@ -586,7 +586,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'sport') { if (!$used{'sport'}) { - my $sp = &format_port_expr('sport', $rule); + my $sp = format_port_expr('sport', $rule); if ($sp) { push(@parts, $sp); $used{'sport'} = 1; @@ -596,7 +596,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'dport') { if (!$used{'dport'} && $rule->{'proto'} && $rule->{'dport'}) { - my $dp = &format_port_expr('dport', $rule); + my $dp = format_port_expr('dport', $rule); if ($dp) { push(@parts, $dp); $used{'dport'} = 1; @@ -627,7 +627,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'tcp_flags') { if (!$used{'tcp_flags'}) { - my $tf = &format_tcp_flags_expr($rule); + my $tf = format_tcp_flags_expr($rule); if ($tf) { push(@parts, $tf); $used{'tcp_flags'} = 1; @@ -637,7 +637,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'limit') { if (!$used{'limit'}) { - my $lim = &format_limit_expr($rule); + my $lim = format_limit_expr($rule); if ($lim) { push(@parts, $lim); $used{'limit'} = 1; @@ -647,7 +647,7 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if ($type eq 'log') { if (!$used{'log'}) { - my $lg = &format_log_expr($rule); + my $lg = format_log_expr($rule); if ($lg) { push(@parts, $lg); $used{'log'} = 1; @@ -681,32 +681,32 @@ if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { } if (!$used{'iif'} && defined($rule->{'iif'}) && $rule->{'iif'} ne '') { my $iftype = $rule->{'iif_type'} || 'iif'; - my $ival = &escape_nft_string($rule->{'iif'}); + my $ival = escape_nft_string($rule->{'iif'}); push(@parts, $iftype." \"".$ival."\""); } if (!$used{'oif'} && defined($rule->{'oif'}) && $rule->{'oif'} ne '') { my $oftype = $rule->{'oif_type'} || 'oif'; - my $oval = &escape_nft_string($rule->{'oif'}); + my $oval = escape_nft_string($rule->{'oif'}); push(@parts, $oftype." \"".$oval."\""); } if (!$used{'saddr'}) { - my $addr = &format_addr_expr('saddr', $rule); + my $addr = format_addr_expr('saddr', $rule); push(@parts, $addr) if ($addr); } if (!$used{'daddr'}) { - my $addr = &format_addr_expr('daddr', $rule); + my $addr = format_addr_expr('daddr', $rule); push(@parts, $addr) if ($addr); } if (!$used{'l4proto'}) { - my $lp = &format_l4proto_expr($rule); + my $lp = format_l4proto_expr($rule); push(@parts, $lp) if ($lp); } if (!$used{'sport'}) { - my $sp = &format_port_expr('sport', $rule); + my $sp = format_port_expr('sport', $rule); push(@parts, $sp) if ($sp); } if (!$used{'dport'}) { - my $dp = &format_port_expr('dport', $rule); + my $dp = format_port_expr('dport', $rule); push(@parts, $dp) if ($dp); } if (!$used{'icmp'} && $rule->{'icmp_type'}) { @@ -716,18 +716,18 @@ if (!$used{'icmpv6'} && $rule->{'icmpv6_type'}) { push(@parts, "icmpv6 type ".$rule->{'icmpv6_type'}); } if (!$used{'tcp_flags'}) { - my $tf = &format_tcp_flags_expr($rule); + my $tf = format_tcp_flags_expr($rule); push(@parts, $tf) if ($tf); } if (!$used{'ct_state'} && $rule->{'ct_state'}) { push(@parts, "ct state ".$rule->{'ct_state'}); } if (!$used{'limit'}) { - my $lim = &format_limit_expr($rule); + my $lim = format_limit_expr($rule); push(@parts, $lim) if ($lim); } if (!$used{'log'}) { - my $lg = &format_log_expr($rule); + my $lg = format_log_expr($rule); push(@parts, $lg) if ($lg); } if (!$used{'counter'} && $rule->{'counter'}) { @@ -743,7 +743,7 @@ if ($rule->{'action'} && !$rule->{'jump'} && !$rule->{'goto'}) { push(@parts, $rule->{'action'}); } if (defined($rule->{'comment'}) && $rule->{'comment'} ne '') { - my $c = &escape_nft_string($rule->{'comment'}); + my $c = escape_nft_string($rule->{'comment'}); push(@parts, "comment \"".$c."\""); } my $text = join(" ", grep { defined($_) && $_ ne '' } @parts); @@ -803,16 +803,16 @@ my ($table) = @_; sub save_configuration { my (@tables) = @_; -my $out = &dump_nftables_save(@tables); +my $out = dump_nftables_save(@tables); my $file = $config{'save_file'} || "$module_config_directory/nftables.conf"; # Write to file -&open_tempfile(my $fh, ">$file"); -&print_tempfile($fh, $out); -&close_tempfile($fh); +open_tempfile(my $fh, ">$file"); +print_tempfile($fh, $out); +close_tempfile($fh); if ($config{'direct'}) { - return &apply_restore($file); + return apply_restore($file); } return; } @@ -823,8 +823,8 @@ sub apply_restore { my ($file) = @_; $file ||= $config{'save_file'} || "$module_config_directory/nftables.conf"; -my $cmd = $config{'nft_cmd'} || &has_command("nft"); -my $out = &backquote_logged("$cmd -f $file 2>&1"); +my $cmd = $config{'nft_cmd'} || has_command("nft"); +my $out = backquote_logged("$cmd -f $file 2>&1"); if ($?) { return "
$out
"; } @@ -837,85 +837,85 @@ sub describe_rule my ($r) = @_; my @conds; if ($r->{'iif'}) { - push(@conds, &text('index_rule_iif', &html_escape($r->{'iif'}))); + push(@conds, text('index_rule_iif', html_escape($r->{'iif'}))); } if ($r->{'oif'}) { - push(@conds, &text('index_rule_oif', &html_escape($r->{'oif'}))); + push(@conds, text('index_rule_oif', html_escape($r->{'oif'}))); } if ($r->{'saddr'}) { - push(@conds, &text('index_rule_saddr', &html_escape($r->{'saddr'}))); + push(@conds, text('index_rule_saddr', html_escape($r->{'saddr'}))); } if ($r->{'daddr'}) { - push(@conds, &text('index_rule_daddr', &html_escape($r->{'daddr'}))); + push(@conds, text('index_rule_daddr', html_escape($r->{'daddr'}))); } if ($r->{'l4proto'} || ($r->{'proto'} && !$r->{'dport'} && !$r->{'sport'})) { my $p = $r->{'l4proto'} || $r->{'proto'}; - push(@conds, &text('index_rule_proto', &html_escape($p))); + push(@conds, text('index_rule_proto', html_escape($p))); } if ($r->{'sport'}) { - push(@conds, &text('index_rule_sport', &html_escape($r->{'sport'}))); + push(@conds, text('index_rule_sport', html_escape($r->{'sport'}))); } if ($r->{'dport'}) { - push(@conds, &text('index_rule_dport', &html_escape($r->{'dport'}))); + push(@conds, text('index_rule_dport', html_escape($r->{'dport'}))); } if ($r->{'icmp_type'}) { - push(@conds, &text('index_rule_icmp', &html_escape($r->{'icmp_type'}))); + push(@conds, text('index_rule_icmp', html_escape($r->{'icmp_type'}))); } if ($r->{'icmpv6_type'}) { - push(@conds, &text('index_rule_icmpv6', &html_escape($r->{'icmpv6_type'}))); + push(@conds, text('index_rule_icmpv6', html_escape($r->{'icmpv6_type'}))); } if ($r->{'ct_state'}) { - push(@conds, &text('index_rule_ct', &html_escape($r->{'ct_state'}))); + push(@conds, text('index_rule_ct', html_escape($r->{'ct_state'}))); } if ($r->{'tcp_flags'}) { my $tf = $r->{'tcp_flags'}; if ($r->{'tcp_flags_mask'}) { $tf = $r->{'tcp_flags_mask'}."==".$r->{'tcp_flags'}; } - push(@conds, &text('index_rule_tcpflags', &html_escape($tf))); + push(@conds, text('index_rule_tcpflags', html_escape($tf))); } if ($r->{'limit_rate'}) { my $lim = $r->{'limit_rate'}; if ($r->{'limit_burst'}) { $lim .= " burst ".$r->{'limit_burst'}; } - push(@conds, &text('index_rule_limit', &html_escape($lim))); + push(@conds, text('index_rule_limit', html_escape($lim))); } if ($r->{'log_prefix'}) { - push(@conds, &text('index_rule_log_prefix', &html_escape($r->{'log_prefix'}))); + push(@conds, text('index_rule_log_prefix', html_escape($r->{'log_prefix'}))); } if ($r->{'log_level'}) { - push(@conds, &text('index_rule_log_level', &html_escape($r->{'log_level'}))); + push(@conds, text('index_rule_log_level', html_escape($r->{'log_level'}))); } if ($r->{'log'} && !$r->{'log_prefix'} && !$r->{'log_level'}) { - push(@conds, &text('index_rule_log')); + push(@conds, text('index_rule_log')); } if ($r->{'counter'}) { - push(@conds, &text('index_rule_counter')); + push(@conds, text('index_rule_counter')); } my $action_label; if ($r->{'jump'}) { - $action_label = &text('index_rule_jump', &html_escape($r->{'jump'})); + $action_label = text('index_rule_jump', html_escape($r->{'jump'})); } elsif ($r->{'goto'}) { - $action_label = &text('index_rule_goto', &html_escape($r->{'goto'})); + $action_label = text('index_rule_goto', html_escape($r->{'goto'})); } elsif ($r->{'action'}) { if ($r->{'action'} eq 'return') { - $action_label = &text('index_return_action'); + $action_label = text('index_return_action'); } else { - $action_label = &text('index_'.lc($r->{'action'})); + $action_label = text('index_'.lc($r->{'action'})); } } if ($action_label) { if (@conds) { - return &text('index_rule_desc_generic', $action_label, join(", ", @conds)); + return text('index_rule_desc_generic', $action_label, join(", ", @conds)); } - return &text('index_rule_desc_action', $action_label); + return text('index_rule_desc_action', $action_label); } -return &html_escape($r->{'text'}); +return html_escape($r->{'text'}); } # interface_choice(name, value, blanktext) @@ -923,12 +923,12 @@ return &html_escape($r->{'text'}); sub interface_choice { my ($name, $value, $blanktext) = @_; -if (&foreign_check("net")) { - &foreign_require("net", "net-lib.pl"); - return &net::interface_choice($name, $value, $blanktext, 0, 1); +if (foreign_check("net")) { + foreign_require("net", "net-lib.pl"); + return net::interface_choice($name, $value, $blanktext, 0, 1); } else { - return &ui_textbox($name, $value, 20); + return ui_textbox($name, $value, 20); } } @@ -937,7 +937,7 @@ else { sub get_webmin_port { my %miniserv; -if (&get_miniserv_config(\%miniserv) && $miniserv{'port'} =~ /^\d+$/) { +if (get_miniserv_config(\%miniserv) && $miniserv{'port'} =~ /^\d+$/) { return $miniserv{'port'}; } return 10000; diff --git a/rename_chain.cgi b/rename_chain.cgi index f0db759a1..4c82e21c3 100644 --- a/rename_chain.cgi +++ b/rename_chain.cgi @@ -6,28 +6,28 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); +ReadParse(); -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; -$table || &error($text{'chain_notable'}); +$table || error($text{'chain_notable'}); my $chain = $table->{'chains'}->{$in{'chain'}}; -$chain || &error($text{'chain_nochain'}); +$chain || error($text{'chain_nochain'}); -&ui_print_header(undef, $text{'rename_chain_title'}, "", "intro", 1, 1); -print &ui_form_start("save_chain.cgi"); -print &ui_hidden("table", $in{'table'}); -print &ui_hidden("rename", 1); -print &ui_hidden("chain_old", $in{'chain'}); +ui_print_header(undef, $text{'rename_chain_title'}, "", "intro", 1, 1); +print ui_form_start("save_chain.cgi"); +print ui_hidden("table", $in{'table'}); +print ui_hidden("rename", 1); +print ui_hidden("chain_old", $in{'chain'}); -print &ui_table_start($text{'rename_chain_header'}, "width=100%", 2); -print &ui_table_row($text{'rename_chain_old'}, - "".&html_escape($in{'chain'}).""); -print &ui_table_row(hlink($text{'rename_chain_new'}, "chain_name"), - &ui_textbox("chain_name", $in{'chain'}, 20)); -print &ui_table_end(); +print ui_table_start($text{'rename_chain_header'}, "width=100%", 2); +print ui_table_row($text{'rename_chain_old'}, + "".html_escape($in{'chain'}).""); +print ui_table_row(hlink($text{'rename_chain_new'}, "chain_name"), + ui_textbox("chain_name", $in{'chain'}, 20)); +print ui_table_end(); -print &ui_form_end([ [ undef, $text{'rename_chain_ok'} ] ]); -&ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); +print ui_form_end([ [ undef, $text{'rename_chain_ok'} ] ]); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/save_chain.cgi b/save_chain.cgi index 7cf818379..d5808f074 100644 --- a/save_chain.cgi +++ b/save_chain.cgi @@ -6,38 +6,38 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); -&ReadParse(); -&error_setup($text{'chain_err'}); +ReadParse(); +error_setup($text{'chain_err'}); -my @tables = &get_nftables_save(); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; -$table || &error($text{'chain_notable'}); +$table || error($text{'chain_notable'}); my $is_new = $in{'new'} ? 1 : 0; my $is_rename = $in{'rename'} ? 1 : 0; my $name = $in{'chain_name'}; $name =~ s/^\s+// if (defined($name)); $name =~ s/\s+$// if (defined($name)); -$name =~ /^\w[\w-]*$/ || &error($text{'chain_ename'}); +$name =~ /^\w[\w-]*$/ || error($text{'chain_ename'}); my $old = $is_rename ? $in{'chain_old'} : $name; $old =~ s/^\s+// if (defined($old)); $old =~ s/\s+$// if (defined($old)); if ($is_new) { - $table->{'chains'}->{$name} && &error($text{'chain_edup'}); + $table->{'chains'}->{$name} && error($text{'chain_edup'}); } elsif ($is_rename) { - $table->{'chains'}->{$old} || &error($text{'chain_nochain'}); + $table->{'chains'}->{$old} || error($text{'chain_nochain'}); if ($name ne $old && $table->{'chains'}->{$name}) { - &error($text{'chain_edup'}); + error($text{'chain_edup'}); } } else { - $table->{'chains'}->{$name} || &error($text{'chain_nochain'}); + $table->{'chains'}->{$name} || error($text{'chain_nochain'}); } if ($is_rename) { if ($name eq $old) { - &redirect("index.cgi?table=$in{'table'}"); + redirect("index.cgi?table=$in{'table'}"); } if ($name ne $old) { $table->{'chains'}->{$name} = $table->{'chains'}->{$old}; @@ -54,17 +54,17 @@ if ($is_rename) { $r->{'goto'} = $name; $changed = 1; } - $r->{'text'} = &format_rule_text($r) if ($changed); + $r->{'text'} = format_rule_text($r) if ($changed); } } - my $err = &save_configuration(@tables); - &error(&text('rename_chain_failed', $err)) if ($err); - &webmin_log("rename", "chain", $old, + my $err = save_configuration(@tables); + error(text('rename_chain_failed', $err)) if ($err); + webmin_log("rename", "chain", $old, { 'new' => $name, 'table' => $table->{'name'}, 'family' => $table->{'family'} }); - &redirect("index.cgi?table=$in{'table'}"); + redirect("index.cgi?table=$in{'table'}"); } my $type = $in{'chain_type'}; @@ -81,8 +81,8 @@ $hook = undef if (!defined($hook) || $hook eq ''); $priority = undef if (!defined($priority) || $priority eq ''); $policy = undef if (!defined($policy) || $policy eq ''); -&validate_chain_base($type, $hook, $priority, $policy) || - &error($text{'chain_ebase'}); +validate_chain_base($type, $hook, $priority, $policy) || + error($text{'chain_ebase'}); my $chain = $table->{'chains'}->{$name} || { }; $chain->{'type'} = $type; @@ -91,9 +91,9 @@ $chain->{'priority'} = $priority; $chain->{'policy'} = $policy; $table->{'chains'}->{$name} = $chain; -my $err = &save_configuration(@tables); -&error(&text('chain_failed', $err)) if ($err); +my $err = save_configuration(@tables); +error(text('chain_failed', $err)) if ($err); -&webmin_log($is_new ? "create" : "modify", "chain", $name, +webmin_log($is_new ? "create" : "modify", "chain", $name, { 'table' => $table->{'name'}, 'family' => $table->{'family'} }); -&redirect("index.cgi?table=$in{'table'}"); +redirect("index.cgi?table=$in{'table'}"); diff --git a/save_rule.cgi b/save_rule.cgi index 9fb7d49ed..c2f7a0ec8 100755 --- a/save_rule.cgi +++ b/save_rule.cgi @@ -6,9 +6,9 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); -&ReadParse(); -&error_setup($text{'save_err'}); -my @tables = &get_nftables_save(); +ReadParse(); +error_setup($text{'save_err'}); +my @tables = get_nftables_save(); my $table = $tables[$in{'table'}]; sub join_multi_value @@ -25,7 +25,7 @@ if ($in{'delete'}) { # Delete the rule my $rule = $table->{'rules'}->[$in{'idx'}]; splice(@{$table->{'rules'}}, $in{'idx'}, 1); - &webmin_log("delete", "rule", $rule ? $rule->{'text'} : undef); + webmin_log("delete", "rule", $rule ? $rule->{'text'} : undef); } else { my $rule = {}; if ($in{'new'}) { @@ -40,8 +40,8 @@ if ($in{'delete'}) { $raw =~ s/\r//g if (defined($raw)); $raw =~ s/^\s+// if (defined($raw)); $raw =~ s/\s+$// if (defined($raw)); - &error($text{'save_raw_empty'}) if (!defined($raw) || $raw eq ''); - &error($text{'save_raw_multiline'}) if ($raw =~ /[\r\n]/); + error($text{'save_raw_empty'}) if (!defined($raw) || $raw eq ''); + error($text{'save_raw_multiline'}) if ($raw =~ /[\r\n]/); $rule->{'text'} = $raw; } else { @@ -62,8 +62,8 @@ if ($in{'delete'}) { $rule->{'saddr'} = (defined($in{'saddr'}) && $in{'saddr'} ne '') ? $in{'saddr'} : undef; $rule->{'daddr'} = (defined($in{'daddr'}) && $in{'daddr'} ne '') ? $in{'daddr'} : undef; - $rule->{'saddr_family'} = $rule->{'saddr'} ? &guess_addr_family($rule->{'saddr'}) : undef; - $rule->{'daddr_family'} = $rule->{'daddr'} ? &guess_addr_family($rule->{'daddr'}) : undef; + $rule->{'saddr_family'} = $rule->{'saddr'} ? guess_addr_family($rule->{'saddr'}) : undef; + $rule->{'daddr_family'} = $rule->{'daddr'} ? guess_addr_family($rule->{'daddr'}) : undef; my $proto = $in{'proto'}; $proto = undef if (defined($proto) && $proto eq ''); @@ -109,8 +109,8 @@ if ($in{'delete'}) { $rule->{'l4proto_family'} = 'meta'; } - my $ct_state = &join_multi_value($in{'ct_state'}); - my $tcp_flags = &join_multi_value($in{'tcp_flags'}); + my $ct_state = join_multi_value($in{'ct_state'}); + my $tcp_flags = join_multi_value($in{'tcp_flags'}); $rule->{'ct_state'} = defined($ct_state) ? $ct_state : undef; $rule->{'tcp_flags'} = defined($tcp_flags) ? $tcp_flags : undef; $rule->{'tcp_flags_mask'} = (defined($in{'tcp_flags_mask'}) && $in{'tcp_flags_mask'} ne '') ? $in{'tcp_flags_mask'} : undef; @@ -130,7 +130,7 @@ if ($in{'delete'}) { $rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef; $rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef; - $rule->{'text'} = &format_rule_text($rule); + $rule->{'text'} = format_rule_text($rule); } if ($in{'new'}) { @@ -138,20 +138,20 @@ if ($in{'delete'}) { } if ($in{'edit_direct'}) { - my $cmd = $config{'nft_cmd'} || &has_command("nft"); + my $cmd = $config{'nft_cmd'} || has_command("nft"); if ($cmd) { - my $tmp = &tempname(); - &open_tempfile(my $fh, ">$tmp"); - &print_tempfile($fh, &dump_nftables_save(@tables)); - &close_tempfile($fh); - my $out = &backquote_logged("$cmd -c -f $tmp 2>&1"); - &unlink_file($tmp); - &error(&text('save_invalid_rule', "
$out
")) if ($?); + my $tmp = tempname(); + open_tempfile(my $fh, ">$tmp"); + print_tempfile($fh, dump_nftables_save(@tables)); + close_tempfile($fh); + my $out = backquote_logged("$cmd -c -f $tmp 2>&1"); + unlink_file($tmp); + error(text('save_invalid_rule', "
$out
")) if ($?); } } - &webmin_log("save", $in{'new'} ? "create" : "modify", $rule->{'text'}); + webmin_log("save", $in{'new'} ? "create" : "modify", $rule->{'text'}); } -my $err = &save_configuration(@tables); -&error(&text('save_failed', $err)) if ($err); -&redirect("index.cgi?table=$in{'table'}"); +my $err = save_configuration(@tables); +error(text('save_failed', $err)) if ($err); +redirect("index.cgi?table=$in{'table'}"); diff --git a/setup.cgi b/setup.cgi index ae69076c2..89182bd8a 100644 --- a/setup.cgi +++ b/setup.cgi @@ -6,53 +6,53 @@ require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text, %config); -&ReadParse(); +ReadParse(); if ($in{'action'} eq 'create') { my $type = $in{'type'}; my @tables; if ($type eq 'allow_all') { - @tables = &create_allow_all_ruleset(); + @tables = create_allow_all_ruleset(); } elsif ($type eq 'deny_incoming') { - @tables = &create_deny_incoming_ruleset(); + @tables = create_deny_incoming_ruleset(); } elsif ($type eq 'deny_all') { - @tables = &create_deny_all_ruleset(); + @tables = create_deny_all_ruleset(); } else { - &error($text{'setup_invalid_type'}); + error($text{'setup_invalid_type'}); } - my $error = &save_configuration(@tables); + my $error = save_configuration(@tables); if ($error) { - &error(&text('setup_failed', $error)); + error(text('setup_failed', $error)); } - $error = &apply_restore(); + $error = apply_restore(); if ($error) { - &error(&text('setup_failed', $error)); + error(text('setup_failed', $error)); } - &webmin_log("setup", "create", $type); - &redirect("index.cgi"); + webmin_log("setup", "create", $type); + redirect("index.cgi"); } -&ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1); +ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1); print "

$text{'setup_header'}

"; -my $webmin_port = &get_webmin_port(); +my $webmin_port = get_webmin_port(); print "

$text{'setup_desc'}

"; -print "

",&text('setup_deny_note', $webmin_port),"

"; +print "

",text('setup_deny_note', $webmin_port),"

"; -print &ui_form_start("setup.cgi"); -print &ui_hidden("action", "create"); +print ui_form_start("setup.cgi"); +print ui_hidden("action", "create"); my @type_opts = ( [ 'allow_all', $text{'setup_allow_all'} . "
" ], [ 'deny_incoming', $text{'setup_deny_incoming'} . "
" ], [ 'deny_all', $text{'setup_deny_all'} ], ); -print &ui_radio("type", "allow_all", \@type_opts); +print ui_radio("type", "allow_all", \@type_opts); -print &ui_form_end([ [ undef, $text{'setup_create'} ] ]); +print ui_form_end([ [ undef, $text{'setup_create'} ] ]); sub create_allow_all_ruleset { @@ -89,7 +89,7 @@ sub create_allow_all_ruleset sub create_deny_incoming_ruleset { my @tables; - my $webmin_port = &get_webmin_port(); + my $webmin_port = get_webmin_port(); my $table = { 'name' => 'inet_filter', 'family' => 'inet', @@ -139,7 +139,7 @@ sub create_deny_incoming_ruleset sub create_deny_all_ruleset { my @tables; - my $webmin_port = &get_webmin_port(); + my $webmin_port = get_webmin_port(); my $table = { 'name' => 'inet_filter', 'family' => 'inet', @@ -191,4 +191,4 @@ sub create_deny_all_ruleset return @tables; } -&ui_print_footer("/", $text{'index'}); +ui_print_footer("/", $text{'index'}); diff --git a/t/perlcritic.t b/t/perlcritic.t index 652ed58e2..b1b91c3af 100644 --- a/t/perlcritic.t +++ b/t/perlcritic.t @@ -26,7 +26,7 @@ sub script_dir return $cwd; } -my $bindir = &script_dir(); +my $bindir = script_dir(); my $module_dir = "$bindir/.."; chdir($module_dir) or die "chdir: $!"; diff --git a/t/run-tests.t b/t/run-tests.t index 813a128ab..f068093ce 100755 --- a/t/run-tests.t +++ b/t/run-tests.t @@ -20,7 +20,7 @@ sub script_dir return $cwd; } -my $bindir = &script_dir(); +my $bindir = script_dir(); my $confdir = tempdir(CLEANUP => 1); my $vardir = tempdir(CLEANUP => 1); @@ -96,22 +96,22 @@ my @cases = ( ); foreach my $c (@cases) { - my $r = &parse_rule_text($c->{line}); + my $r = parse_rule_text($c->{line}); ok($r && ref($r) eq 'HASH', "$c->{name} parse hash"); check_fields($c->{name}, $r, $c->{expect}); - my $out = &format_rule_text($r); + my $out = format_rule_text($r); ok($out =~ /\S/, "$c->{name} formatted non-empty"); if ($c->{preserve}) { like($out, qr/\Q$c->{preserve}\E/, "$c->{name} preserves unknowns"); } - my $r2 = &parse_rule_text($out); + my $r2 = parse_rule_text($out); check_fields($c->{name}.' roundtrip', $r2, $c->{expect}); } my $ruleset = "$bindir/rulesets/basic.nft"; -my @tables = &get_nftables_save($ruleset); +my @tables = get_nftables_save($ruleset); ok(@tables == 1, 'ruleset table count'); my $t = $tables[0]; is($t->{family}, 'inet', 'ruleset family'); @@ -128,11 +128,11 @@ check_fields('ruleset r1', $rules[0], { iif => 'lo', action => 'accept' }); check_fields('ruleset r2', $rules[1], { saddr => '192.168.1.0/24', proto => 'tcp', dport => '22', action => 'accept', comment => 'ssh' }); check_fields('ruleset r3', $rules[2], { ct_state => 'established,related', action => 'accept' }); -ok(&validate_chain_base('filter', 'input', '0', 'accept'), +ok(validate_chain_base('filter', 'input', '0', 'accept'), 'chain base allows zero priority'); -ok(!&validate_chain_base('filter', 'input', undef, 'accept'), +ok(!validate_chain_base('filter', 'input', undef, 'accept'), 'chain base missing priority invalid'); -ok(&validate_chain_base(undef, undef, undef, undef), +ok(validate_chain_base(undef, undef, undef, undef), 'chain base none set valid'); my $table_move = { @@ -143,7 +143,7 @@ my $table_move = { { chain => 'input', index => 3, text => 'r3' }, ], }; -ok(&move_rule_in_chain($table_move, 'input', 1, 'down'), +ok(move_rule_in_chain($table_move, 'input', 1, 'down'), 'move rule down returns true'); is($table_move->{rules}->[1]->{text}, 'r3', 'rule moved down in array'); is($table_move->{rules}->[3]->{text}, 'r1', 'rule swapped down in array'); @@ -156,7 +156,7 @@ my $table_move2 = { { chain => 'input', index => 1, text => 'r1' }, ], }; -is(&move_rule_in_chain($table_move2, 'input', 0, 'up'), 0, +is(move_rule_in_chain($table_move2, 'input', 0, 'up'), 0, 'top rule cannot move up'); done_testing(); From 48269733b16c02b89dd39b1118efad08ce522610 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Mon, 2 Feb 2026 20:52:30 -0600 Subject: [PATCH 12/13] Add support for sets --- create_table.cgi | 4 +- delete_set.cgi | 48 ++++++++++ edit_rule.cgi | 137 +++++++++++++++++++++++++--- edit_set.cgi | 86 ++++++++++++++++++ help/set_elements.html | 3 + help/set_flags.html | 3 + help/set_name.html | 3 + help/set_type.html | 3 + index.cgi | 36 ++++++++ lang/en | 38 ++++++++ nftables-lib.pl | 196 ++++++++++++++++++++++++++++++++++++++++- save_rule.cgi | 31 ++++++- save_set.cgi | 62 +++++++++++++ setup.cgi | 3 + t/rulesets/sets.nft | 18 ++++ t/run-tests.t | 23 +++++ 16 files changed, 675 insertions(+), 19 deletions(-) create mode 100755 delete_set.cgi create mode 100755 edit_set.cgi create mode 100644 help/set_elements.html create mode 100644 help/set_flags.html create mode 100644 help/set_name.html create mode 100644 help/set_type.html create mode 100755 save_set.cgi create mode 100644 t/rulesets/sets.nft diff --git a/create_table.cgi b/create_table.cgi index 1bec9eaf7..8b90fca18 100755 --- a/create_table.cgi +++ b/create_table.cgi @@ -29,7 +29,8 @@ if ($in{'create'}) { push(@tables, { 'name' => $name, 'family' => $family, 'rules' => [], - 'chains' => {} }); + 'chains' => {}, + 'sets' => {} }); my $err = save_configuration(@tables); error(text('create_failed', $err)) if ($err); webmin_log("create", "table", $name, { 'family' => $family }); @@ -52,4 +53,3 @@ print ui_table_end(); print ui_form_end([ [ undef, $text{'create_ok'} ] ]); ui_print_footer("index.cgi", $text{'index_return'}); - diff --git a/delete_set.cgi b/delete_set.cgi new file mode 100755 index 000000000..82747b457 --- /dev/null +++ b/delete_set.cgi @@ -0,0 +1,48 @@ +#!/usr/bin/perl +# delete_set.cgi +# Delete an existing nftables set + +require './nftables-lib.pl'; ## no critic +use strict; +use warnings; +our (%in, %text); +ReadParse(); +error_setup($text{'delete_set_err'}); + +my @tables = get_nftables_save(); +my $table = $tables[$in{'table'}]; +$table || error($text{'set_notable'}); + +my $set = $table->{'sets'}->{$in{'set'}}; +$set || error($text{'set_noset'}); + +my $refs = count_set_references($table, $in{'set'}); + +if ($in{'confirm'}) { + $refs && error(text('delete_set_inuse', $in{'set'}, $refs)); + + delete($table->{'sets'}->{$in{'set'}}); + my $err = save_configuration(@tables); + error(text('delete_set_failed', $err)) if ($err); + webmin_log("delete", "set", $in{'set'}, + { 'table' => $table->{'name'}, 'family' => $table->{'family'} }); + redirect("index.cgi?table=$in{'table'}"); +} + +ui_print_header(undef, $text{'delete_set_title'}, "", "intro", 1, 1); +print ui_form_start("delete_set.cgi"); +print ui_hidden("table", $in{'table'}); +print ui_hidden("set", $in{'set'}); +print "
", + text('delete_set_confirm', + "$in{'set'}", + "$table->{'family'} $table->{'name'}"), + ""; +if ($refs) { + print "

", text('delete_set_inuse', $in{'set'}, $refs); +} +print "

\n"; +print ui_submit($text{'delete'}, "confirm"); +print "

\n"; +print ui_form_end(); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/edit_rule.cgi b/edit_rule.cgi index 7b43c8b59..5fb0688a7 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -20,6 +20,17 @@ my $raw_extra = ""; my $ct_state_sel; my $tcp_flags_sel; my $advanced_open; +my $saddr_set; +my $daddr_set; +my $sport_set; +my $dport_set; +my $saddr_val; +my $daddr_val; +my $sport_val; +my $dport_val; +my @addr_set_opts; +my @port_set_opts; +my %set_families; sub split_multi_value { @@ -74,6 +85,49 @@ if ($rule) { $tcp_flags_sel = split_multi_value($rule->{'tcp_flags'}); $log_enabled = $rule->{'log'} || $rule->{'log_prefix'} || $rule->{'log_level'}; } +$saddr_set = set_name_from_value($rule->{'saddr'}); +$daddr_set = set_name_from_value($rule->{'daddr'}); +$sport_set = set_name_from_value($rule->{'sport'}); +$dport_set = set_name_from_value($rule->{'dport'}); +$saddr_val = $saddr_set ? "" : $rule->{'saddr'}; +$daddr_val = $daddr_set ? "" : $rule->{'daddr'}; +$sport_val = $sport_set ? "" : $rule->{'sport'}; +$dport_val = $dport_set ? "" : $rule->{'dport'}; + +@addr_set_opts = ( [ "", $text{'edit_set_none'} ] ); +@port_set_opts = ( [ "", $text{'edit_set_none'} ] ); +my %addr_set_seen; +my %port_set_seen; +if ($table && $table->{'sets'} && ref($table->{'sets'}) eq 'HASH') { + foreach my $s (sort keys %{$table->{'sets'}}) { + my $set = $table->{'sets'}->{$s} || { }; + my $label = $s; + $label .= " ($set->{'type'})" if ($set->{'type'}); + my $kind = set_type_kind($set->{'type'}); + if (!$kind || $kind eq 'addr') { + push(@addr_set_opts, [ $s, $label ]); + $addr_set_seen{$s} = 1; + } + if (!$kind || $kind eq 'port') { + push(@port_set_opts, [ $s, $label ]); + $port_set_seen{$s} = 1; + } + my $fam = set_type_family($set->{'type'}); + $set_families{$s} = $fam if ($fam); + } +} +if ($saddr_set && !$addr_set_seen{$saddr_set}) { + push(@addr_set_opts, [ $saddr_set, $saddr_set ]); +} +if ($daddr_set && !$addr_set_seen{$daddr_set}) { + push(@addr_set_opts, [ $daddr_set, $daddr_set ]); +} +if ($sport_set && !$port_set_seen{$sport_set}) { + push(@port_set_opts, [ $sport_set, $sport_set ]); +} +if ($dport_set && !$port_set_seen{$dport_set}) { + push(@port_set_opts, [ $dport_set, $dport_set ]); +} $advanced_open = 1 if ($action_sel && ($action_sel eq 'jump' || $action_sel eq 'goto')); $advanced_open = 1 if ($rule && ( $rule->{'jump'} || $rule->{'goto'} || @@ -141,10 +195,19 @@ print ui_table_row(hlink($text{'edit_action'}, "action"), ])); # Addresses -print ui_table_row(hlink($text{'edit_saddr'}, "saddr"), - ui_textbox("saddr", $rule->{'saddr'}, 30)); -print ui_table_row(hlink($text{'edit_daddr'}, "daddr"), - ui_textbox("daddr", $rule->{'daddr'}, 30)); +my $saddr_row = ui_textbox("saddr", $saddr_val, 30); +if (@addr_set_opts > 1) { + $saddr_row .= "
".text('edit_saddr_set', + ui_select("saddr_set", $saddr_set, \@addr_set_opts, 1, 0, 1)); +} +print ui_table_row(hlink($text{'edit_saddr'}, "saddr"), $saddr_row); + +my $daddr_row = ui_textbox("daddr", $daddr_val, 30); +if (@addr_set_opts > 1) { + $daddr_row .= "
".text('edit_daddr_set', + ui_select("daddr_set", $daddr_set, \@addr_set_opts, 1, 0, 1)); +} +print ui_table_row(hlink($text{'edit_daddr'}, "daddr"), $daddr_row); # Protocol print ui_table_row(hlink($text{'edit_proto'}, "proto"), @@ -158,10 +221,19 @@ print ui_table_row(hlink($text{'edit_proto'}, "proto"), ])); # Ports -print ui_table_row(hlink($text{'edit_sport'}, "sport"), - ui_textbox("sport", $rule->{'sport'}, 10)); -print ui_table_row(hlink($text{'edit_dport'}, "dport"), - ui_textbox("dport", $rule->{'dport'}, 10)); +my $sport_row = ui_textbox("sport", $sport_val, 10); +if (@port_set_opts > 1) { + $sport_row .= "
".text('edit_sport_set', + ui_select("sport_set", $sport_set, \@port_set_opts, 1, 0, 1)); +} +print ui_table_row(hlink($text{'edit_sport'}, "sport"), $sport_row); + +my $dport_row = ui_textbox("dport", $dport_val, 10); +if (@port_set_opts > 1) { + $dport_row .= "
".text('edit_dport_set', + ui_select("dport_set", $dport_set, \@port_set_opts, 1, 0, 1)); +} +print ui_table_row(hlink($text{'edit_dport'}, "dport"), $dport_row); print ui_table_end(); @@ -255,17 +327,33 @@ sub js_array } @vals)."]"; } +sub js_object +{ + my (%vals) = @_; + return "{".join(",", map { + my $k = $_; + my $v = $vals{$k}; + $k =~ s/\\/\\\\/g; + $k =~ s/"/\\"/g; + $v =~ s/\\/\\\\/g if (defined($v)); + $v =~ s/"/\\"/g if (defined($v)); + "\"$k\":\"$v\""; + } sort keys %vals)."}"; +} + my $icmp_js = js_array(@icmp_types); my $icmpv6_js = js_array(@icmpv6_types); my $icmp_any = $text{'edit_proto_any'}; $icmp_any =~ s/\\/\\\\/g; $icmp_any =~ s/"/\\"/g; +my $set_fam_js = js_object(%set_families); print "