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 "
", + text('delete_chain_confirm', + "$in{'chain'}", + "$table->{'family'} $table->{'name'}"), + ""; +if (@refs) { + print "

", text('delete_chain_inuse', $in{'chain'}, scalar(@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/nftables/delete_set.cgi b/nftables/delete_set.cgi new file mode 100755 index 000000000..82747b457 --- /dev/null +++ b/nftables/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/nftables/delete_table.cgi b/nftables/delete_table.cgi new file mode 100755 index 000000000..0621e0d50 --- /dev/null +++ b/nftables/delete_table.cgi @@ -0,0 +1,36 @@ +#!/usr/bin/perl +# delete_table.cgi +# Delete an existing nftables table + +require './nftables-lib.pl'; ## no critic +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/nftables/edit_chain.cgi b/nftables/edit_chain.cgi new file mode 100644 index 000000000..f7580b69e --- /dev/null +++ b/nftables/edit_chain.cgi @@ -0,0 +1,87 @@ +#!/usr/bin/perl +# edit_chain.cgi +# Display a form for creating or editing a 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 = { }; +my $chain_name = ""; +my $is_new = $in{'new'} ? 1 : 0; + +if ($is_new) { + 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); +} + +my @type_opts = ( + [ "", $text{'chain_type_none'} ], + map { [ $_, $_ ] } qw(filter nat route) +); +my @hook_opts = ( + [ "", $text{'chain_hook_none'} ], + map { [ $_, $_ ] } qw(prerouting input forward output postrouting ingress) +); +my @policy_opts = ( + [ "", $text{'chain_policy_none'} ], + 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_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_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_end(); + +print ui_form_end([ [ undef, $text{$is_new ? 'create' : 'save'} ] ]); + +print <<'EOF'; + +EOF + +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); + diff --git a/nftables/edit_rule.cgi b/nftables/edit_rule.cgi new file mode 100755 index 000000000..5fb0688a7 --- /dev/null +++ b/nftables/edit_rule.cgi @@ -0,0 +1,646 @@ +#!/usr/bin/perl +# edit_rule.cgi +# Display a form for creating or editing a rule + +require './nftables-lib.pl'; ## no critic +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; +my $action_sel; +my $proto_sel; +my $icmp_type; +my $log_enabled; +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 +{ + my ($v) = @_; + return if (!defined($v) || $v eq ''); + $v =~ s/^\s*\{//; + $v =~ s/\}\s*$//; + $v =~ s/^\s+//; + $v =~ s/\s+$//; + return 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); + $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; +} +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'; + } + 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'}; + $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'}; +} +$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'} || + $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'}); +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); + +# Rule comment +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, + [ + [ "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'} ], + ])); + +# Addresses +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"), + ui_select("proto", $proto_sel, + [ + [ "", $text{'edit_proto_any'} ], + [ "tcp", "TCP" ], + [ "udp", "UDP" ], + [ "icmp", "ICMP" ], + [ "icmpv6", "ICMPv6" ], + ])); + +# Ports +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(); + +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(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)); + +# 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)); + +# 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)); + +# 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); + +# 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(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; +if ($in{'new'}) { + push(@buttons, [ undef, $text{'create'} ]); +} else { + push(@buttons, [ undef, $text{'save'} ]); + push(@buttons, [ 'delete', $text{'delete'} ]); +} +print ui_form_end(\@buttons); + +sub js_array +{ + my (@vals) = @_; + return "[".join(",", map { + my $v = $_; + $v =~ s/\\/\\\\/g; + $v =~ s/"/\\"/g; + "\"$v\""; + } @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 " +EOF + +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/nftables/edit_set.cgi b/nftables/edit_set.cgi new file mode 100755 index 000000000..9158afbac --- /dev/null +++ b/nftables/edit_set.cgi @@ -0,0 +1,86 @@ +#!/usr/bin/perl +# edit_set.cgi +# Display a form for creating or editing a set + +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{'set_notable'}); + +my $set = { }; +my $set_name = ""; +my $is_new = $in{'new'} ? 1 : 0; + +if ($is_new) { + ui_print_header(undef, $text{'set_title_new'}, "", "intro", 1, 1); +} +else { + $set_name = $in{'set'}; + $set = $table->{'sets'}->{$set_name}; + $set || error($text{'set_noset'}); + ui_print_header(undef, $text{'set_title_edit'}, "", "intro", 1, 1); +} + +my $elements_text = set_elements_text($set); +my @type_opts = ( + [ "", $text{'set_type_select'} ], + [ "ipv4_addr", "ipv4_addr" ], + [ "ipv6_addr", "ipv6_addr" ], + [ "ether_addr", "ether_addr" ], + [ "inet_proto", "inet_proto" ], + [ "inet_service", "inet_service" ], + [ "mark", "mark" ], +); +my %type_seen = map { $_->[0] => 1 } @type_opts; +if ($set->{'type'} && !$type_seen{$set->{'type'}}) { + push(@type_opts, [ $set->{'type'}, $set->{'type'} ]); +} +my @flag_opts = ( + [ "constant", "constant" ], + [ "dynamic", "dynamic" ], + [ "interval", "interval" ], + [ "timeout", "timeout" ], +); +my @flags_sel; +my $flags_sel; +if ($set->{'flags'}) { + @flags_sel = split(/\s+|,\s*/, $set->{'flags'}); + @flags_sel = grep { $_ ne '' } @flags_sel; + my %flag_seen = map { $_->[0] => 1 } @flag_opts; + foreach my $f (@flags_sel) { + push(@flag_opts, [ $f, $f ]) if (!$flag_seen{$f}++); + } +} +$flags_sel = @flags_sel ? \@flags_sel : undef; + +print ui_form_start("save_set.cgi"); +print ui_hidden("table", $in{'table'}); +print ui_hidden("new", $is_new); +print ui_hidden("set", $set_name) if (!$is_new); + +print ui_table_start($text{'set_header'}, "width=100%", 2); + +my $name_tags = $is_new ? undef : "readonly"; +print ui_table_row(hlink($text{'set_name'}, "set_name"), + ui_textbox("set_name", $set_name, 20, 0, undef, $name_tags)); + +print ui_table_row(hlink($text{'set_type'}, "set_type"), + ui_select("set_type", $set->{'type'}, \@type_opts, 1, 0, 1)); + +print ui_table_row(hlink($text{'set_flags'}, "set_flags"), + ui_select("set_flags", $flags_sel, \@flag_opts, 5, 1, 1)); + +my $elem_field = ui_textarea("set_elements", $elements_text, 6, 60); +$elem_field .= "
$text{'set_elements_desc'}"; +print ui_table_row(hlink($text{'set_elements'}, "set_elements"), + $elem_field); + +print ui_table_end(); + +print ui_form_end([ [ undef, $text{$is_new ? 'create' : 'save'} ] ]); +ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'}); diff --git a/nftables/help/action.html b/nftables/help/action.html new file mode 100644 index 000000000..c5ca866f7 --- /dev/null +++ b/nftables/help/action.html @@ -0,0 +1,7 @@ +
Action
+

Verdict or control action for matching packets.

+ + diff --git a/nftables/help/chain_hook.html b/nftables/help/chain_hook.html new file mode 100644 index 000000000..4ba86aec1 --- /dev/null +++ b/nftables/help/chain_hook.html @@ -0,0 +1,3 @@ +
Hook
+

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 @@ +
Chain name
+

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 @@ +
Policy
+

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
+

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 @@ +
Chain type
+

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 @@ +
Comment
+ +Optional note stored with the rule.

+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 @@ +
Counter
+

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 @@ +
Counter
+

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 @@ +
Conntrack state
+

Select one or more states to match.

+ + diff --git a/nftables/help/daddr.html b/nftables/help/daddr.html new file mode 100644 index 000000000..fbd242cf6 --- /dev/null +++ b/nftables/help/daddr.html @@ -0,0 +1,3 @@ +
Destination address
+

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 @@ +
Destination port
+

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 @@ +
Edit directly
+

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 @@ +
Edit directly
+

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 @@ +
Goto target chain
+

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 @@ +
ICMP/ICMPv6 type
+

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 @@ +
Incoming interface
+

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 @@ +
Introduction
+

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 @@ +
Jump target chain
+

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 @@ +
Limit burst
+

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 @@ +
Limit rate
+

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 @@ +
Logging
+

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 @@ +
Logging
+

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 @@ +
Log level
+

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 @@ +
Log prefix
+

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 @@ +
Outgoing interface
+

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 @@ +
Protocol
+

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 @@ +
Raw rule
+

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 @@ +
Source address
+

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 @@ +
Set elements
+

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 @@ +
Set flags
+

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 @@ +
Set name
+

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 @@ +
Set type
+

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 @@ +
Source port
+

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 @@ +
TCP flags
+

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 @@ +
TCP flags mask
+

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 "

\n"; + 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 "
\n"; + print ui_form_end(); + } + + # Identify current table + my $curr = $tables[$in{'table'}]; + + if ($curr) { + # Show sets + $rules_html .= ui_hr(); + $rules_html .= "$text{'index_sets'}
\n"; + if ($curr->{'sets'} && ref($curr->{'sets'}) eq 'HASH' && + keys %{$curr->{'sets'}}) { + $rules_html .= ui_columns_start( + [ $text{'index_set_name'}, $text{'index_set_type'}, + $text{'index_set_flags'}, $text{'index_set_elements'}, + $text{'index_set_actions'} ], 100); + foreach my $s (sort keys %{$curr->{'sets'}}) { + my $set = $curr->{'sets'}->{$s} || { }; + my $actions_html = + ui_link("edit_set.cgi?table=$in{'table'}&set=". + urlize($s), $text{'index_set_edit'})."
". + ui_link("delete_set.cgi?table=$in{'table'}&set=". + urlize($s), $text{'index_set_delete'}); + $rules_html .= ui_columns_row([ + $s, + $set->{'type'} || "-", + $set->{'flags'} || "-", + set_elements_summary($set), + $actions_html + ]); + } + $rules_html .= ui_columns_end(); + } + else { + $rules_html .= "$text{'index_sets_none'}
\n"; + } + $rules_html .= ui_buttons_start(); + $rules_html .= ui_buttons_row( + "edit_set.cgi?table=$in{'table'}&new=1", + $text{'index_set_create'}, + $text{'index_set_createdesc'}); + $rules_html .= ui_buttons_end(); + + # Show chains and rules + $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'}, + $text{'index_actions'} ], 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_row; + if (@rules) { + my $ri = 0; + $rules_html_row = "\n"; + foreach my $r (@rules) { + my $desc = describe_rule($r); + my $rule_link = ui_link( + "edit_rule.cgi?table=$in{'table'}&chain=". + urlize($c)."&idx=$r->{'index'}", + $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 .= "\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'}); + } + + 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, + $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'}); + $rules_html .= 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/nftables/install_check.pl b/nftables/install_check.pl new file mode 100644 index 000000000..78dd5a6f9 --- /dev/null +++ b/nftables/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/nftables/lang/en b/nftables/lang/en new file mode 100644 index 000000000..de701347f --- /dev/null +++ b/nftables/lang/en @@ -0,0 +1,222 @@ +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 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 +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_actions=Actions +index_sets=Sets +index_sets_none=No sets defined. +index_set_name=Name +index_set_type=Type +index_set_flags=Flags +index_set_elements=Elements +index_set_actions=Actions +index_set_create=Create set +index_set_createdesc=Add a new set to this table. +index_set_edit=Edit set +index_set_delete=Delete set +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 +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_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 (except SSH and Webmin), allow all outgoing +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
+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_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"); + +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' => [], + 'sets' => {}, + '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 $webmin_port = get_webmin_port(); + my $table = { + 'name' => 'inet_filter', + 'family' => 'inet', + 'rules' => [ + { + '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' + } + ], + 'sets' => {}, + '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 $webmin_port = get_webmin_port(); + my $table = { + 'name' => 'inet_filter', + 'family' => 'inet', + 'rules' => [], + 'sets' => {}, + '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' + } + } + }; + $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; +} + +ui_print_footer("/", $text{'index'}); diff --git a/nftables/t/perlcritic.t b/nftables/t/perlcritic.t new file mode 100644 index 000000000..b1b91c3af --- /dev/null +++ b/nftables/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(); diff --git a/nftables/t/rulesets/basic.nft b/nftables/t/rulesets/basic.nft new file mode 100644 index 000000000..2d43483b6 --- /dev/null +++ b/nftables/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/nftables/t/rulesets/sets.nft b/nftables/t/rulesets/sets.nft new file mode 100644 index 000000000..16c6c0572 --- /dev/null +++ b/nftables/t/rulesets/sets.nft @@ -0,0 +1,18 @@ +table inet filter { + set trusted_v4 { + type ipv4_addr; + flags interval; + elements = { 192.168.1.0/24, 10.0.0.1 } + } + set web_ports { + type inet_service; + elements = { + 80, + 443 + } + } + chain input { + type filter hook input priority 0; policy drop; + ip saddr @trusted_v4 tcp dport @web_ports accept + } +} diff --git a/nftables/t/run-tests.t b/nftables/t/run-tests.t new file mode 100755 index 000000000..9b5c612dd --- /dev/null +++ b/nftables/t/run-tests.t @@ -0,0 +1,185 @@ +#!/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' }); + +my $ruleset_sets = "$bindir/rulesets/sets.nft"; +my @tables_sets = get_nftables_save($ruleset_sets); +ok(@tables_sets == 1, 'sets ruleset table count'); +my $ts = $tables_sets[0]; +ok($ts->{sets} && $ts->{sets}->{trusted_v4}, 'trusted_v4 set present'); +is($ts->{sets}->{trusted_v4}->{type}, 'ipv4_addr', 'trusted_v4 type'); +is($ts->{sets}->{trusted_v4}->{flags}, 'interval', 'trusted_v4 flags'); +is_deeply($ts->{sets}->{trusted_v4}->{elements}, + [ '192.168.1.0/24', '10.0.0.1' ], + 'trusted_v4 elements'); +ok($ts->{sets}->{web_ports}, 'web_ports set present'); +is($ts->{sets}->{web_ports}->{type}, 'inet_service', 'web_ports type'); +is_deeply($ts->{sets}->{web_ports}->{elements}, + [ '80', '443' ], + 'web_ports elements'); + +my $rset = $ts->{rules}->[0]; +check_fields('set rule', $rset, + { saddr => '@trusted_v4', proto => 'tcp', dport => '@web_ports', action => 'accept' }); +my $rset_out = format_rule_text($rset); +like($rset_out, qr/\@trusted_v4/, 'set rule format preserves address set'); +like($rset_out, qr/\@web_ports/, 'set rule format preserves port set'); + +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'); + +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();