diff --git a/nftables/apply.cgi b/nftables/apply.cgi new file mode 100755 index 000000000..be53676f3 --- /dev/null +++ b/nftables/apply.cgi @@ -0,0 +1,16 @@ +#!/usr/bin/perl +# apply.cgi +# Apply the current configuration + +require './nftables-lib.pl'; ## no critic +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/nftables/config.info b/nftables/config.info new file mode 100644 index 000000000..2c5b4ca98 --- /dev/null +++ b/nftables/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/nftables/create_table.cgi b/nftables/create_table.cgi new file mode 100755 index 000000000..8b90fca18 --- /dev/null +++ b/nftables/create_table.cgi @@ -0,0 +1,55 @@ +#!/usr/bin/perl +# create_table.cgi +# Create a new nftables table + +require './nftables-lib.pl'; ## no critic +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' => {}, + 'sets' => {} }); + 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/nftables/delete_chain.cgi b/nftables/delete_chain.cgi new file mode 100644 index 000000000..e609240ee --- /dev/null +++ b/nftables/delete_chain.cgi @@ -0,0 +1,54 @@ +#!/usr/bin/perl +# delete_chain.cgi +# Delete an existing nftables chain + +require './nftables-lib.pl'; ## no critic +use strict; +use warnings; +our (%in, %text); +ReadParse(); +error_setup($text{'delete_chain_err'}); + +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'}); + +my @refs = grep { + ($_->{'jump'} && $_->{'jump'} eq $in{'chain'}) || + ($_->{'goto'} && $_->{'goto'} eq $in{'chain'}) +} @{$table->{'rules'}}; + +if ($in{'confirm'}) { + @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'}, + { 'table' => $table->{'name'}, 'family' => $table->{'family'} }); + 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'}); +print "
\n"; +print ui_submit($text{'delete'}, "confirm"); +print "
\n"; +print ui_submit($text{'delete'}, "confirm"); +print "
\n"; +print ui_submit($text{'delete'}, "confirm"); +print "
Verdict or control action for matching packets.
+Base chain hook point, such as prerouting, input, forward, output, postrouting, or ingress.
+ diff --git a/nftables/help/chain_name.html b/nftables/help/chain_name.html new file mode 100644 index 000000000..bcc7e69e2 --- /dev/null +++ b/nftables/help/chain_name.html @@ -0,0 +1,3 @@ +Unique name for the chain within this table. Use letters, numbers, underscores, and dashes.
+ diff --git a/nftables/help/chain_policy.html b/nftables/help/chain_policy.html new file mode 100644 index 000000000..c0397985d --- /dev/null +++ b/nftables/help/chain_policy.html @@ -0,0 +1,3 @@ +Default action for this base chain, such as accept, drop, reject, queue, or continue.
+ diff --git a/nftables/help/chain_priority.html b/nftables/help/chain_priority.html new file mode 100644 index 000000000..1b09c3730 --- /dev/null +++ b/nftables/help/chain_priority.html @@ -0,0 +1,3 @@ +Priority for this base chain. Lower values run earlier. Common values include -300, -150, 0, or 100.
+ diff --git a/nftables/help/chain_type.html b/nftables/help/chain_type.html new file mode 100644 index 000000000..0279d9871 --- /dev/null +++ b/nftables/help/chain_type.html @@ -0,0 +1,3 @@ +Base chain type, such as filter, nat, or route. Leave blank to create a regular chain with no hook.
+ diff --git a/nftables/help/comment.html b/nftables/help/comment.html new file mode 100644 index 000000000..95e4f5b42 --- /dev/null +++ b/nftables/help/comment.html @@ -0,0 +1,6 @@ ++Saved as comment "text"; quotes and backslashes are escaped.
+
+
diff --git a/nftables/help/counter.html b/nftables/help/counter.html
new file mode 100644
index 000000000..555c93459
--- /dev/null
+++ b/nftables/help/counter.html
@@ -0,0 +1,3 @@
+
Add a counter to track packets and bytes.
+ diff --git a/nftables/help/counter_1.html b/nftables/help/counter_1.html new file mode 100644 index 000000000..555c93459 --- /dev/null +++ b/nftables/help/counter_1.html @@ -0,0 +1,3 @@ +Add a counter to track packets and bytes.
+ diff --git a/nftables/help/ct_state.html b/nftables/help/ct_state.html new file mode 100644 index 000000000..778aad89b --- /dev/null +++ b/nftables/help/ct_state.html @@ -0,0 +1,10 @@ +Select one or more states to match.
+IPv4/IPv6 address or CIDR (e.g., 2001:db8::/32).
+ diff --git a/nftables/help/dport.html b/nftables/help/dport.html new file mode 100644 index 000000000..45089e007 --- /dev/null +++ b/nftables/help/dport.html @@ -0,0 +1,3 @@ +TCP/UDP destination port number or range (e.g., 80 or 1000-2000).
+ diff --git a/nftables/help/edit_direct.html b/nftables/help/edit_direct.html new file mode 100644 index 000000000..f3e055a2d --- /dev/null +++ b/nftables/help/edit_direct.html @@ -0,0 +1,4 @@ +Disable structured fields and edit the raw rule line.
+Validation occurs on save.
+ diff --git a/nftables/help/edit_direct_1.html b/nftables/help/edit_direct_1.html new file mode 100644 index 000000000..f3e055a2d --- /dev/null +++ b/nftables/help/edit_direct_1.html @@ -0,0 +1,4 @@ +Disable structured fields and edit the raw rule line.
+Validation occurs on save.
+ diff --git a/nftables/help/goto.html b/nftables/help/goto.html new file mode 100644 index 000000000..359d15aad --- /dev/null +++ b/nftables/help/goto.html @@ -0,0 +1,4 @@ +Name of the chain to transfer control to.
+Does not return to the calling chain.
+ diff --git a/nftables/help/icmp_type.html b/nftables/help/icmp_type.html new file mode 100644 index 000000000..5075fc230 --- /dev/null +++ b/nftables/help/icmp_type.html @@ -0,0 +1,5 @@ +Select a type name (or leave blank for any). The valid names depend on the protocol.
+ICMP types: 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.
+ICMPv6 types: 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.
+ diff --git a/nftables/help/iif.html b/nftables/help/iif.html new file mode 100644 index 000000000..ec848034c --- /dev/null +++ b/nftables/help/iif.html @@ -0,0 +1,3 @@ +Match the incoming interface name (e.g., eth0).
+ diff --git a/nftables/help/intro.html b/nftables/help/intro.html new file mode 100644 index 000000000..c3f054d6f --- /dev/null +++ b/nftables/help/intro.html @@ -0,0 +1,4 @@ +nftables stores firewall rules in tables. Each table belongs to a family (such as inet, ip, or ip6) and contains one or more chains. Chains contain rules, and each rule is a sequence of tests (matches) followed by an action like accept, drop, jump, or log. Named sets can group addresses or services for reuse in multiple rules.
+To get started, use the Setup page to create a default ruleset, or create a table and chain manually. Then add rules (and sets) from the table view. When you are ready to activate your changes, click Apply Configuration to load the ruleset into the kernel.
+ diff --git a/nftables/help/jump.html b/nftables/help/jump.html new file mode 100644 index 000000000..606eb6b4c --- /dev/null +++ b/nftables/help/jump.html @@ -0,0 +1,4 @@ +Name of the chain to call.
+Control returns to the next rule after the jump.
+ diff --git a/nftables/help/limit_burst.html b/nftables/help/limit_burst.html new file mode 100644 index 000000000..ae69c8c5f --- /dev/null +++ b/nftables/help/limit_burst.html @@ -0,0 +1,3 @@ +Optional burst size in packets.
+ diff --git a/nftables/help/limit_rate.html b/nftables/help/limit_rate.html new file mode 100644 index 000000000..a97dbf28f --- /dev/null +++ b/nftables/help/limit_rate.html @@ -0,0 +1,3 @@ +Rate limit (e.g., 10/second, 5/minute, 100/hour).
+ diff --git a/nftables/help/log.html b/nftables/help/log.html new file mode 100644 index 000000000..883acd066 --- /dev/null +++ b/nftables/help/log.html @@ -0,0 +1,4 @@ +Add a log statement before the verdict.
+Use prefix and level to customize.
+ diff --git a/nftables/help/log_1.html b/nftables/help/log_1.html new file mode 100644 index 000000000..883acd066 --- /dev/null +++ b/nftables/help/log_1.html @@ -0,0 +1,4 @@ +Add a log statement before the verdict.
+Use prefix and level to customize.
+ diff --git a/nftables/help/log_level.html b/nftables/help/log_level.html new file mode 100644 index 000000000..31aedcdcc --- /dev/null +++ b/nftables/help/log_level.html @@ -0,0 +1,3 @@ +Common values: emerg, alert, crit, err, warning, notice, info, debug.
+ diff --git a/nftables/help/log_prefix.html b/nftables/help/log_prefix.html new file mode 100644 index 000000000..11a47084a --- /dev/null +++ b/nftables/help/log_prefix.html @@ -0,0 +1,3 @@ +String prepended to the log message.
+ diff --git a/nftables/help/oif.html b/nftables/help/oif.html new file mode 100644 index 000000000..866acf123 --- /dev/null +++ b/nftables/help/oif.html @@ -0,0 +1,3 @@ +Match the outgoing interface name (e.g., eth1).
+ diff --git a/nftables/help/proto.html b/nftables/help/proto.html new file mode 100644 index 000000000..1334a17b1 --- /dev/null +++ b/nftables/help/proto.html @@ -0,0 +1,7 @@ +Select a protocol to match.
+Ports apply to TCP/UDP; ICMP uses the type field.
+ diff --git a/nftables/help/raw_rule.html b/nftables/help/raw_rule.html new file mode 100644 index 000000000..f2355b172 --- /dev/null +++ b/nftables/help/raw_rule.html @@ -0,0 +1,4 @@ +Generated rule line.
+In Edit directly mode, this is the rule that will be saved.
+ diff --git a/nftables/help/saddr.html b/nftables/help/saddr.html new file mode 100644 index 000000000..0fe37c0fd --- /dev/null +++ b/nftables/help/saddr.html @@ -0,0 +1,3 @@ +IPv4/IPv6 address or CIDR (e.g., 192.168.1.0/24).
+ diff --git a/nftables/help/set_elements.html b/nftables/help/set_elements.html new file mode 100644 index 000000000..b6525d8d3 --- /dev/null +++ b/nftables/help/set_elements.html @@ -0,0 +1,3 @@ +Elements to store in the set. Separate entries with commas or newlines. Use nftables syntax, such as IPs/CIDR for address sets or ports for service sets.
+ diff --git a/nftables/help/set_flags.html b/nftables/help/set_flags.html new file mode 100644 index 000000000..a42ef9e25 --- /dev/null +++ b/nftables/help/set_flags.html @@ -0,0 +1,3 @@ +Optional flags for the set, like interval or timeout. Use the same syntax as nftables, separated by spaces if multiple.
+ diff --git a/nftables/help/set_name.html b/nftables/help/set_name.html new file mode 100644 index 000000000..7dd30a64f --- /dev/null +++ b/nftables/help/set_name.html @@ -0,0 +1,3 @@ +Unique name for the set within the table. Use letters, numbers, underscores, or dashes. Rules reference a set as @name.
+ diff --git a/nftables/help/set_type.html b/nftables/help/set_type.html new file mode 100644 index 000000000..3e952c2ce --- /dev/null +++ b/nftables/help/set_type.html @@ -0,0 +1,3 @@ +Type of elements stored in the set, such as ipv4_addr, ipv6_addr, or inet_service. This must match the elements you enter.
+ diff --git a/nftables/help/sport.html b/nftables/help/sport.html new file mode 100644 index 000000000..ee9c8730e --- /dev/null +++ b/nftables/help/sport.html @@ -0,0 +1,3 @@ +TCP/UDP source port number or range (e.g., 22 or 1000-2000).
+ diff --git a/nftables/help/tcp_flags.html b/nftables/help/tcp_flags.html new file mode 100644 index 000000000..4c591b2e4 --- /dev/null +++ b/nftables/help/tcp_flags.html @@ -0,0 +1,14 @@ +Select one or more TCP flags to match.
+Use the mask field for a bitmask match.
+ diff --git a/nftables/help/tcp_flags_mask.html b/nftables/help/tcp_flags_mask.html new file mode 100644 index 000000000..8857d6b18 --- /dev/null +++ b/nftables/help/tcp_flags_mask.html @@ -0,0 +1,4 @@ +Optional mask used with tcp flags & mask == value.
+Example mask: syn|rst.
+ diff --git a/nftables/images/icon.gif b/nftables/images/icon.gif new file mode 100644 index 000000000..650a15379 Binary files /dev/null and b/nftables/images/icon.gif differ diff --git a/nftables/index.cgi b/nftables/index.cgi new file mode 100755 index 000000000..545eb585f --- /dev/null +++ b/nftables/index.cgi @@ -0,0 +1,206 @@ +#!/usr/bin/perl +# index.cgi +# Display current nftables configuration + +require './nftables-lib.pl'; ## no critic +use strict; +use warnings; +our (%in, %text, %config); +ReadParse(); +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"); + if (!$partial) { + 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"); + if (!$partial) { + ui_print_footer("/", $text{'index'}); + } + exit; +} + +# Load tables +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'}, + $text{'index_table_createdesc'}); + $rules_html .= 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'} ]); + } + + if (!$partial) { + print ui_form_start("index.cgi"); + print "
| $rule_link | ". + "$move |
| ". + ui_link("edit_rule.cgi?table=$in{'table'}&chain=". + urlize($c)."&new=1", $text{'index_radd'}). + " | |
$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 +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_set_none=None +edit_saddr_set=Use set $1 +edit_daddr_set=Use set $1 +edit_sport_set=Use set $1 +edit_dport_set=Use set $1 +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_raw_rule_direct=Edit directly +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 +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+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 +save_set_missing=Selected set $1 does not exist. +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 +set_title_new=Create set +set_title_edit=Edit set +set_header=Set Details +set_name=Set name +set_type=Type +set_type_select=Select type +set_flags=Flags +set_elements=Elements +set_elements_desc=Separate elements with commas or newlines. +set_err=Failed to save set +set_failed=Failed to save set:
$1+set_ename=Set name is invalid +set_edup=A set with that name already exists +set_etype=Set type is required +set_notable=No such table selected +set_noset=No such set selected +delete_set_title=Delete set +delete_set_err=Failed to delete set +delete_set_failed=Failed to delete set:
$1+delete_set_confirm=Are you sure you want to delete set $1 from table $2? +delete_set_inuse=Set $1 is referenced by $2 rule(s). Remove those rules first. diff --git a/nftables/module.info b/nftables/module.info new file mode 100644 index 000000000..3aeb690fe --- /dev/null +++ b/nftables/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/move_rule.cgi b/nftables/move_rule.cgi new file mode 100755 index 000000000..456bc26de --- /dev/null +++ b/nftables/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'; ## no critic +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/nftables-lib.pl b/nftables/nftables-lib.pl new file mode 100644 index 000000000..7ac1ac977 --- /dev/null +++ b/nftables/nftables-lib.pl @@ -0,0 +1,1140 @@ +# nftables-lib.pl +# Functions for reading and writing nftables rules + +BEGIN { push(@INC, ".."); }; ## no critic +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 $set; +my $set_depth = 0; +my $set_elem_open = 0; +my $set_elem_buf = ''; +my $lnum = 0; +my $content; +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); + +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 ($set) { + my $sline = $line; + $sline =~ s/^\s+//; + $sline =~ s/\s+$//; + if ($set_elem_open) { + if ($sline =~ /(.*)\}/) { + $set_elem_buf .= " ".$1; + $set_elem_open = 0; + $set_elem_buf =~ s/;\s*$//; + $set->{'elements'} = parse_set_elements_string($set_elem_buf); + $set_elem_buf = ''; + } + else { + $set_elem_buf .= " ".$sline if ($sline ne ''); + } + } + else { + if ($sline =~ /^type\s+(\S+)\s*;?$/) { + $set->{'type'} = $1; + $set->{'type'} =~ s/;\s*$//; + } + elsif ($sline =~ /^flags\s+(.+?)\s*;?$/) { + $set->{'flags'} = $1; + } + elsif ($sline =~ /^elements\s*=\s*\{(.*)$/) { + my $rest = $1; + if ($rest =~ /(.*)\}/) { + my $content = $1; + $content =~ s/;\s*$//; + $set->{'elements'} = parse_set_elements_string($content); + } + else { + $set_elem_open = 1; + $set_elem_buf = $rest; + } + } + elsif ($sline ne '' && $sline ne '}') { + push(@{$set->{'raw_lines'}}, $sline); + } + } + + my $opens = () = $line =~ /\{/g; + my $closes = () = $line =~ /\}/g; + $set_depth += $opens - $closes; + if ($set_depth <= 0) { + $set = undef; + $set_depth = 0; + $set_elem_open = 0; + $set_elem_buf = ''; + } + next; + } + + if ($line =~ /^table\s+(\S+)\s+(\S+)\s+\{/) { + # Start of a table + $table = { 'name' => $2, + 'family' => $1, + 'line' => $lnum, + 'rules' => [ ], + 'chains' => { }, + 'sets' => { } }; + push(@rv, $table); + $chain = undef; + } + elsif ($line =~ /^\s*set\s+(\S+)\s+\{/) { + # Start of a set + if ($table) { + my $setname = $1; + $set = { + 'name' => $setname, + 'line' => $lnum, + 'elements' => [ ], + 'raw_lines' => [ ], + }; + $table->{'sets'}->{$setname} = $set; + $set_depth = () = $line =~ /\{/g; + $set_depth -= () = $line =~ /\}/g; + $set_elem_open = 0; + $set_elem_buf = ''; + } + } + 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 + }; + my $parsed = parse_rule_text($rule_str); + if ($parsed) { + foreach my $k (keys %$parsed) { + $rule->{$k} = $parsed->{$k}; + } + } + push(@{$table->{'rules'}}, $rule); + } + } +} + +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 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 move_rule_in_chain +{ +my ($table, $chain, $idx, $dir) = @_; +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 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 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; +} + +($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) = @_; +my $val = $rule->{$dir}; +return 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 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 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 if (!defined($proto) || $proto eq ''); +return $proto." ".$dir." ".$val; +} + +sub format_tcp_flags_expr +{ +my ($rule) = @_; +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; +} +return "tcp flags ".$val; +} + +sub format_limit_expr +{ +my ($rule) = @_; +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'}; + $out .= " burst ".$burst; + $out .= " packets" if ($burst =~ /^\d+$/); +} +return $out; +} + +sub format_log_expr +{ +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'}); + 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; +} + +sub parse_set_elements_string +{ +my ($text) = @_; +return [ ] if (!defined($text)); +$text =~ s/^\s+//; +$text =~ s/\s+$//; +return [ ] if ($text eq ''); +my @vals = split(/\s*,\s*/, $text); +@vals = grep { defined($_) && $_ ne '' } @vals; +return \@vals; +} + +sub parse_set_elements_input +{ +my ($text) = @_; +return [ ] if (!defined($text)); +$text =~ s/\r//g; +$text =~ s/^\s+//; +$text =~ s/\s+$//; +return [ ] if ($text eq ''); +$text =~ s/\n/,/g; +return parse_set_elements_string($text); +} + +sub set_elements_text +{ +my ($set) = @_; +return "" if (!$set || ref($set) ne 'HASH'); +return "" if (!$set->{'elements'} || ref($set->{'elements'}) ne 'ARRAY'); +return join("\n", @{$set->{'elements'}}); +} + +sub set_elements_summary +{ +my ($set) = @_; +return "-" if (!$set || ref($set) ne 'HASH'); +return "-" if (!$set->{'elements'} || ref($set->{'elements'}) ne 'ARRAY'); +my @elems = @{$set->{'elements'}}; +return "-" if (!@elems); +my $max = 3; +my $preview = join(", ", @elems[0 .. ($#elems < $max-1 ? $#elems : $max-1)]); +if (@elems > $max) { + $preview .= ", ..."; +} +return $preview; +} + +sub set_type_kind +{ +my ($type) = @_; +return if (!defined($type)); +return 'addr' if ($type =~ /addr$/); +return 'port' if ($type =~ /(service|port)$/); +return; +} + +sub set_type_family +{ +my ($type) = @_; +return if (!defined($type)); +return 'ip6' if ($type eq 'ipv6_addr'); +return 'ip' if ($type eq 'ipv4_addr'); +return; +} + +sub set_name_from_value +{ +my ($val) = @_; +return if (!defined($val)); +return $1 if ($val =~ /^\@(\S+)$/); +return; +} + +sub rule_uses_set +{ +my ($rule, $setname) = @_; +return 0 if (!$rule || !$setname); +foreach my $k (qw(saddr daddr sport dport)) { + return 1 if (defined($rule->{$k}) && $rule->{$k} eq '@'.$setname); +} +return 1 if ($rule->{'text'} && $rule->{'text'} =~ /\@\Q$setname\E\b/); +return 0; +} + +sub count_set_references +{ +my ($table, $setname) = @_; +return 0 if (!$table || ref($table) ne 'HASH' || !$setname); +return 0 if (!$table->{'rules'} || ref($table->{'rules'}) ne 'ARRAY'); +my $count = 0; +foreach my $r (@{$table->{'rules'}}) { + next if (!$r || ref($r) ne 'HASH'); + $count++ if (rule_uses_set($r, $setname)); +} +return $count; +} + + +# 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"; + } + + if ($t->{'sets'} && ref($t->{'sets'}) eq 'HASH') { + foreach my $s (sort keys %{$t->{'sets'}}) { + my $set = $t->{'sets'}->{$s}; + next if (!$set || ref($set) ne 'HASH'); + $rv .= "\tset $s {\n"; + $rv .= "\t\ttype $set->{'type'};\n" if ($set->{'type'}); + $rv .= "\t\tflags $set->{'flags'};\n" if ($set->{'flags'}); + if ($set->{'raw_lines'} && ref($set->{'raw_lines'}) eq 'ARRAY') { + foreach my $l (@{$set->{'raw_lines'}}) { + next if (!defined($l) || $l eq ''); + $rv .= "\t\t$l\n"; + } + } + if ($set->{'elements'} && ref($set->{'elements'}) eq 'ARRAY' && + @{$set->{'elements'}}) { + my $el = join(", ", @{$set->{'elements'}}); + $rv .= "\t\telements = { $el }\n"; + } + $rv .= "\t}\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; +} + +# 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; +} + +# describe_rule(&rule) +sub describe_rule +{ +my ($r) = @_; +my @conds; +if ($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'}))); +} +if ($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'}))); +} +if ($r->{'l4proto'} || ($r->{'proto'} && !$r->{'dport'} && !$r->{'sport'})) { + my $p = $r->{'l4proto'} || $r->{'proto'}; + push(@conds, text('index_rule_proto', html_escape($p))); +} +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) +# 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); +} +} + +# 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/nftables/rename_chain.cgi b/nftables/rename_chain.cgi new file mode 100644 index 000000000..4c82e21c3 --- /dev/null +++ b/nftables/rename_chain.cgi @@ -0,0 +1,33 @@ +#!/usr/bin/perl +# rename_chain.cgi +# Rename an existing chain + +require './nftables-lib.pl'; ## no critic +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/nftables/save_chain.cgi b/nftables/save_chain.cgi new file mode 100644 index 000000000..d5808f074 --- /dev/null +++ b/nftables/save_chain.cgi @@ -0,0 +1,99 @@ +#!/usr/bin/perl +# save_chain.cgi +# Save a new or existing chain + +require './nftables-lib.pl'; ## no critic +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/nftables/save_rule.cgi b/nftables/save_rule.cgi new file mode 100755 index 000000000..04d083233 --- /dev/null +++ b/nftables/save_rule.cgi @@ -0,0 +1,180 @@ +#!/usr/bin/perl +# save_rule.cgi +# Save a new or existing rule + +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(); +my $table = $tables[$in{'table'}]; + +foreach my $sfield (qw(saddr_set daddr_set sport_set dport_set)) { + if ($in{$sfield}) { + $table->{'sets'}->{$in{$sfield}} || + error(text('save_set_missing', $in{$sfield})); + } +} + +sub join_multi_value +{ + my ($v) = @_; + return if (!defined($v) || $v eq ''); + my @vals = split(/\0/, $v); + @vals = grep { defined($_) && $_ ne '' } @vals; + return if (!@vals); + return join(",", @vals); +} + +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'}]; + } + + 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->{'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->{'action'} = $action; + } + + my $saddr = $in{'saddr'}; + my $daddr = $in{'daddr'}; + $saddr = '@'.$in{'saddr_set'} if ($in{'saddr_set'}); + $daddr = '@'.$in{'daddr_set'} if ($in{'daddr_set'}); + $rule->{'saddr'} = (defined($saddr) && $saddr ne '') ? $saddr : undef; + $rule->{'daddr'} = (defined($daddr) && $daddr ne '') ? $daddr : undef; + $rule->{'saddr_family'} = $rule->{'saddr'} ? guess_addr_family($rule->{'saddr'}) : undef; + $rule->{'daddr_family'} = $rule->{'daddr'} ? guess_addr_family($rule->{'daddr'}) : undef; + if ($rule->{'saddr'} && $rule->{'saddr'} =~ /^\@(\S+)/) { + my $fam = set_type_family($table->{'sets'}->{$1}->{'type'}); + $rule->{'saddr_family'} = $fam if ($fam); + } + if ($rule->{'daddr'} && $rule->{'daddr'} =~ /^\@(\S+)/) { + my $fam = set_type_family($table->{'sets'}->{$1}->{'type'}); + $rule->{'daddr_family'} = $fam if ($fam); + } + + my $proto = $in{'proto'}; + $proto = undef if (defined($proto) && $proto eq ''); + my $sport = $in{'sport'}; + my $dport = $in{'dport'}; + $sport = '@'.$in{'sport_set'} if ($in{'sport_set'}); + $dport = '@'.$in{'dport_set'} if ($in{'dport_set'}); + $rule->{'sport'} = (defined($sport) && $sport ne '') ? $sport : undef; + $rule->{'dport'} = (defined($dport) && $dport ne '') ? $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'; + } + + 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; + $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); +error(text('save_failed', $err)) if ($err); +redirect("index.cgi?table=$in{'table'}"); diff --git a/nftables/save_set.cgi b/nftables/save_set.cgi new file mode 100755 index 000000000..f12556229 --- /dev/null +++ b/nftables/save_set.cgi @@ -0,0 +1,62 @@ +#!/usr/bin/perl +# save_set.cgi +# Save a new or existing set + +require './nftables-lib.pl'; ## no critic +use strict; +use warnings; +our (%in, %text); +ReadParse(); +error_setup($text{'set_err'}); + +my @tables = get_nftables_save(); +my $table = $tables[$in{'table'}]; +$table || error($text{'set_notable'}); + +my $is_new = $in{'new'} ? 1 : 0; +my $name = $in{'set_name'}; +$name =~ /^\w[\w-]*$/ || error($text{'set_ename'}); + +if ($is_new && $table->{'sets'}->{$name}) { + error($text{'set_edup'}); +} + +my $type = $in{'set_type'}; +$type = undef if (defined($type) && $type =~ /^\s*$/); +error($text{'set_etype'}) if (!$type); + +my $flags = $in{'set_flags'}; +if (defined($flags) && $flags ne '') { + my @vals = split(/\0/, $flags); + @vals = grep { defined($_) && $_ ne '' } @vals; + $flags = @vals ? join(" ", @vals) : undef; +} +$flags = undef if (defined($flags) && $flags =~ /^\s*$/); + +my $elements = parse_set_elements_input($in{'set_elements'}); + +my $set; +if ($is_new) { + $set = { 'name' => $name, 'raw_lines' => [ ] }; +} +else { + my $orig = $in{'set'}; + $set = $table->{'sets'}->{$orig}; + $set || error($text{'set_noset'}); + $name = $orig; +} + +$set->{'name'} = $name; +$set->{'type'} = $type; +$set->{'flags'} = $flags; +$set->{'elements'} = $elements; +$set->{'raw_lines'} ||= [ ]; + +$table->{'sets'}->{$name} = $set; + +my $err = save_configuration(@tables); +error(text('set_failed', $err)) if ($err); + +webmin_log($is_new ? "create" : "save", "set", $name, + { 'table' => $table->{'name'}, 'family' => $table->{'family'} }); +redirect("index.cgi?table=$in{'table'}"); diff --git a/nftables/setup.cgi b/nftables/setup.cgi new file mode 100644 index 000000000..206a826b1 --- /dev/null +++ b/nftables/setup.cgi @@ -0,0 +1,197 @@ +#!/usr/bin/perl +# setup.cgi +# Create a default nftables ruleset + +require './nftables-lib.pl'; ## no critic +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_desc'}
"; +print "",text('setup_deny_note', $webmin_port),"
"; + +print ui_form_start("setup.cgi"); +print ui_hidden("action", "create"); + +my @type_opts = ( + [ 'allow_all', $text{'setup_allow_all'} . "