Merge swelljoe/nftables as new nftables module

Import https://github.com/swelljoe/nftables into the Webmin tree under nftables/ while preserving upstream history.
This commit is contained in:
Ilia Ross
2026-04-17 15:19:20 +02:00
61 changed files with 3662 additions and 0 deletions

16
nftables/apply.cgi Executable file
View File

@@ -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");

12
nftables/config.info Normal file
View File

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

55
nftables/create_table.cgi Executable file
View File

@@ -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'});

54
nftables/delete_chain.cgi Normal file
View File

@@ -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 "<center><b>",
text('delete_chain_confirm',
"<tt>$in{'chain'}</tt>",
"<tt>$table->{'family'} $table->{'name'}</tt>"),
"</b>";
if (@refs) {
print "<br><br>", text('delete_chain_inuse', $in{'chain'}, scalar(@refs));
}
print "<p>\n";
print ui_submit($text{'delete'}, "confirm");
print "</center>\n";
print ui_form_end();
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

48
nftables/delete_set.cgi Executable file
View File

@@ -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 "<center><b>",
text('delete_set_confirm',
"<tt>$in{'set'}</tt>",
"<tt>$table->{'family'} $table->{'name'}</tt>"),
"</b>";
if ($refs) {
print "<br><br>", text('delete_set_inuse', $in{'set'}, $refs);
}
print "<p>\n";
print ui_submit($text{'delete'}, "confirm");
print "</center>\n";
print ui_form_end();
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

36
nftables/delete_table.cgi Executable file
View File

@@ -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 "<center><b>",
text('delete_confirm',
"<tt>$table->{'family'} $table->{'name'}</tt>"),
"</b><p>\n";
print ui_submit($text{'delete'}, "confirm");
print "</center>\n";
print ui_form_end();
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

87
nftables/edit_chain.cgi Normal file
View File

@@ -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';
<script>
function toggle_chain_base() {
var type = document.getElementById('chain_type');
var disabled = !type || !type.value;
var ids = ['chain_hook', 'chain_priority', 'chain_policy'];
for (var i = 0; i < ids.length; i++) {
var el = document.getElementById(ids[i]);
if (el) {
el.disabled = disabled;
}
}
}
if (window.addEventListener) {
window.addEventListener('load', toggle_chain_base);
} else if (window.attachEvent) {
window.attachEvent('onload', toggle_chain_base);
}
</script>
EOF
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

646
nftables/edit_rule.cgi Executable file
View File

@@ -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 .= "<br>".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 .= "<br>".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 .= "<br>".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 .= "<br>".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 .= "<br>".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."<br>".$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 "<script>\n";
print "(function() {\n";
print " var icmpTypes = $icmp_js;\n";
print " var icmpv6Types = $icmpv6_js;\n";
print " var icmpAnyLabel = \"$icmp_any\";\n";
print " var setFamilies = $set_fam_js;\n";
print <<'EOF';
function byName(name) {
var els = document.getElementsByName(name);
return els && els.length ? els[0] : null;
}
function val(name) {
var el = byName(name);
if (!el) return "";
if (el.tagName === "SELECT" && el.multiple) {
var vals = [];
var sawEmpty = false;
for (var i = 0; i < el.options.length; i++) {
var opt = el.options[i];
if (opt.selected) {
if (opt.value === "") sawEmpty = true;
else vals.push(opt.value);
}
}
if (vals.length && sawEmpty) {
for (var j = 0; j < el.options.length; j++) {
if (el.options[j].value === "") el.options[j].selected = false;
}
}
return vals.join(",");
}
if (el.type === "checkbox") {
return el.checked ? (el.value || "1") : "";
}
return el.value || "";
}
function ifaceVal(name) {
var v = val(name);
if (v === "other") {
return val(name + "_other");
}
return v;
}
function escapeNft(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}
function isNumeric(s) {
return /^[0-9]+$/.test(s);
}
function guessFamily(addr) {
return addr.indexOf(":") >= 0 ? "ip6" : "ip";
}
function familyForSet(name) {
return setFamilies && setFamilies[name] ? setFamilies[name] : "";
}
function familyForValue(val) {
if (!val) return "";
if (val.charAt(0) === "@") {
var fam = familyForSet(val.substr(1));
if (fam) return fam;
}
return guessFamily(val);
}
function buildRule() {
var direct = byName("edit_direct");
if (direct && direct.checked) return;
var parts = [];
var iif = ifaceVal("iif");
if (iif) parts.push("iif \"" + escapeNft(iif) + "\"");
var oif = ifaceVal("oif");
if (oif) parts.push("oif \"" + escapeNft(oif) + "\"");
var saddrSet = val("saddr_set");
var saddr = val("saddr");
if (saddrSet) {
var sf = familyForSet(saddrSet) || guessFamily("@" + saddrSet);
parts.push(sf + " saddr @" + saddrSet);
} else if (saddr) {
parts.push(familyForValue(saddr) + " saddr " + saddr);
}
var daddrSet = val("daddr_set");
var daddr = val("daddr");
if (daddrSet) {
var df = familyForSet(daddrSet) || guessFamily("@" + daddrSet);
parts.push(df + " daddr @" + daddrSet);
} else if (daddr) {
parts.push(familyForValue(daddr) + " daddr " + daddr);
}
var proto = val("proto");
var sportSet = val("sport_set");
var dportSet = val("dport_set");
var sport = sportSet ? ("@" + sportSet) : val("sport");
var dport = dportSet ? ("@" + dportSet) : val("dport");
var icmpType = val("icmp_type");
if (!proto && (sport || dport)) {
proto = "tcp";
}
var l4proto = "";
var portProto = "";
if (proto && (proto === "tcp" || proto === "udp")) {
portProto = proto;
if (!sport && !dport) {
l4proto = proto;
}
}
else if (proto) {
l4proto = proto;
}
if (l4proto) {
parts.push("meta l4proto " + l4proto);
}
if (sport && portProto) parts.push(portProto + " sport " + sport);
if (dport && portProto) parts.push(portProto + " dport " + dport);
if (proto === "icmp" && icmpType) parts.push("icmp type " + icmpType);
if (proto === "icmpv6" && icmpType) parts.push("icmpv6 type " + icmpType);
if (!proto && icmpType) {
var inIcmp = icmpTypes.indexOf(icmpType) >= 0;
var inIcmpv6 = icmpv6Types.indexOf(icmpType) >= 0;
if (inIcmpv6 && !inIcmp) {
parts.push("meta l4proto icmpv6");
parts.push("icmpv6 type " + icmpType);
} else {
parts.push("meta l4proto icmp");
parts.push("icmp type " + icmpType);
}
}
var tcpFlags = val("tcp_flags");
var tcpMask = val("tcp_flags_mask");
if (tcpFlags) {
if (tcpMask) parts.push("tcp flags & " + tcpMask + " == " + tcpFlags);
else parts.push("tcp flags " + tcpFlags);
}
var ctState = val("ct_state");
if (ctState) parts.push("ct state " + ctState);
var limitRate = val("limit_rate");
var limitBurst = val("limit_burst");
if (limitRate) {
var lim = "limit rate " + limitRate;
if (limitBurst) {
lim += " burst " + limitBurst;
if (isNumeric(limitBurst)) lim += " packets";
}
parts.push(lim);
}
var logBox = byName("log");
var logEnabled = logBox && logBox.checked;
var logPrefix = val("log_prefix");
var logLevel = val("log_level");
if (logEnabled || logPrefix || logLevel) {
var lp = ["log"];
if (logPrefix) lp.push("prefix \"" + escapeNft(logPrefix) + "\"");
if (logLevel) lp.push("level " + logLevel);
parts.push(lp.join(" "));
}
var counter = byName("counter");
if (counter && counter.checked) parts.push("counter");
var action = val("action");
var jump = val("jump");
var go = val("goto");
if (action === "jump" && jump) parts.push("jump " + jump);
else if (action === "goto" && go) parts.push("goto " + go);
else if (action && action !== "jump" && action !== "goto") parts.push(action);
var comment = val("comment");
if (comment) parts.push("comment \"" + escapeNft(comment) + "\"");
var extra = val("raw_extra");
if (extra) parts.push(extra);
var raw = parts.join(" ").replace(/^\s+|\s+$/g, "");
var rawEl = byName("raw_rule");
if (rawEl) rawEl.value = raw;
}
function toggleDirect() {
var direct = byName("edit_direct");
var on = direct && direct.checked;
var form = direct ? direct.form : document.forms[0];
if (!form) return;
var els = form.querySelectorAll("input, select, textarea");
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (el.name === "edit_direct" || el.name === "raw_rule") continue;
if (el.type === "hidden" || el.type === "submit" || el.type === "button") continue;
el.disabled = on;
}
var rawEl = byName("raw_rule");
if (rawEl) rawEl.readOnly = !on;
if (!on) buildRule();
}
function uniqList(list) {
var seen = {};
var out = [];
for (var i = 0; i < list.length; i++) {
var v = list[i];
if (seen[v]) continue;
seen[v] = true;
out.push(v);
}
return out;
}
function updateIcmpTypes() {
var el = byName("icmp_type");
if (!el) return;
var proto = val("proto");
var current = el.value || "";
var list;
if (proto === "icmp") list = icmpTypes;
else if (proto === "icmpv6") list = icmpv6Types;
else list = uniqList(icmpTypes.concat(icmpv6Types));
while (el.options.length) {
el.remove(0);
}
var optAny = document.createElement("option");
optAny.value = "";
optAny.text = icmpAnyLabel;
el.add(optAny);
for (var i = 0; i < list.length; i++) {
var opt = document.createElement("option");
opt.value = list[i];
opt.text = list[i];
el.add(opt);
}
if (current && list.indexOf(current) >= 0) el.value = current;
else el.value = "";
}
function maybeSetProtoFromIcmp() {
var protoEl = byName("proto");
if (!protoEl || protoEl.value) return;
var t = val("icmp_type");
if (!t) return;
var inIcmp = icmpTypes.indexOf(t) >= 0;
var inIcmpv6 = icmpv6Types.indexOf(t) >= 0;
if (inIcmp && !inIcmpv6) protoEl.value = "icmp";
else if (inIcmpv6 && !inIcmp) protoEl.value = "icmpv6";
if (protoEl.value) updateIcmpTypes();
}
function bind() {
var direct = byName("edit_direct");
var form = direct ? direct.form : document.forms[0];
if (!form) return;
var els = form.querySelectorAll("input, select, textarea");
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (el.name === "raw_rule") continue;
if (el.name === "edit_direct") {
el.addEventListener("change", toggleDirect);
continue;
}
if (el.name === "proto") {
el.addEventListener("change", function() {
updateIcmpTypes();
buildRule();
});
continue;
}
if (el.name === "icmp_type") {
el.addEventListener("change", function() {
maybeSetProtoFromIcmp();
buildRule();
});
continue;
}
el.addEventListener("input", buildRule);
el.addEventListener("change", buildRule);
}
updateIcmpTypes();
toggleDirect();
buildRule();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bind);
} else {
bind();
}
})();
</script>
EOF
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

86
nftables/edit_set.cgi Executable file
View File

@@ -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 .= "<br><small>$text{'set_elements_desc'}</small>";
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'});

View File

@@ -0,0 +1,7 @@
<header>Action</header>
<p>Verdict or control action for matching packets.</p>
<ul>
<li>Accept, Drop, Reject, Return</li>
<li>Jump or Goto to another chain</li>
</ul>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Hook</header>
<p>Base chain hook point, such as <tt>prerouting</tt>, <tt>input</tt>, <tt>forward</tt>, <tt>output</tt>, <tt>postrouting</tt>, or <tt>ingress</tt>.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Chain name</header>
<p>Unique name for the chain within this table. Use letters, numbers, underscores, and dashes.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Policy</header>
<p>Default action for this base chain, such as <tt>accept</tt>, <tt>drop</tt>, <tt>reject</tt>, <tt>queue</tt>, or <tt>continue</tt>.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Priority</header>
<p>Priority for this base chain. Lower values run earlier. Common values include <tt>-300</tt>, <tt>-150</tt>, <tt>0</tt>, or <tt>100</tt>.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Chain type</header>
<p>Base chain type, such as <tt>filter</tt>, <tt>nat</tt>, or <tt>route</tt>. Leave blank to create a regular chain with no hook.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,6 @@
<header>Comment</header>
Optional note stored with the rule.<p>
Saved as comment "text"; quotes and backslashes are escaped.<p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Counter</header>
<p>Add a counter to track packets and bytes.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Counter</header>
<p>Add a counter to track packets and bytes.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,10 @@
<header>Conntrack state</header>
<p>Select one or more states to match.</p>
<ul>
<li>invalid</li>
<li>new</li>
<li>established</li>
<li>related</li>
<li>untracked</li>
</ul>
<footer>nft(8)</footer>

3
nftables/help/daddr.html Normal file
View File

@@ -0,0 +1,3 @@
<header>Destination address</header>
<p>IPv4/IPv6 address or CIDR (e.g., 2001:db8::/32).</p>
<footer>nft(8)</footer>

3
nftables/help/dport.html Normal file
View File

@@ -0,0 +1,3 @@
<header>Destination port</header>
<p>TCP/UDP destination port number or range (e.g., 80 or 1000-2000).</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,4 @@
<header>Edit directly</header>
<p>Disable structured fields and edit the raw rule line.</p>
<p>Validation occurs on save.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,4 @@
<header>Edit directly</header>
<p>Disable structured fields and edit the raw rule line.</p>
<p>Validation occurs on save.</p>
<footer>nft(8)</footer>

4
nftables/help/goto.html Normal file
View File

@@ -0,0 +1,4 @@
<header>Goto target chain</header>
<p>Name of the chain to transfer control to.</p>
<p>Does not return to the calling chain.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,5 @@
<header>ICMP/ICMPv6 type</header>
<p>Select a type name (or leave blank for any). The valid names depend on the protocol.</p>
<p>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.</p>
<p>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.</p>
<footer>nft(8)</footer>

3
nftables/help/iif.html Normal file
View File

@@ -0,0 +1,3 @@
<header>Incoming interface</header>
<p>Match the incoming interface name (e.g., eth0).</p>
<footer>nft(8)</footer>

4
nftables/help/intro.html Normal file
View File

@@ -0,0 +1,4 @@
<header>Introduction</header>
<p>nftables stores firewall rules in tables. Each table belongs to a family (such as <tt>inet</tt>, <tt>ip</tt>, or <tt>ip6</tt>) and contains one or more chains. Chains contain rules, and each rule is a sequence of tests (matches) followed by an action like <tt>accept</tt>, <tt>drop</tt>, <tt>jump</tt>, or <tt>log</tt>. Named sets can group addresses or services for reuse in multiple rules.</p>
<p>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.</p>
<footer>nft(8)</footer>

4
nftables/help/jump.html Normal file
View File

@@ -0,0 +1,4 @@
<header>Jump target chain</header>
<p>Name of the chain to call.</p>
<p>Control returns to the next rule after the jump.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Limit burst</header>
<p>Optional burst size in packets.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Limit rate</header>
<p>Rate limit (e.g., 10/second, 5/minute, 100/hour).</p>
<footer>nft(8)</footer>

4
nftables/help/log.html Normal file
View File

@@ -0,0 +1,4 @@
<header>Logging</header>
<p>Add a log statement before the verdict.</p>
<p>Use prefix and level to customize.</p>
<footer>nft(8)</footer>

4
nftables/help/log_1.html Normal file
View File

@@ -0,0 +1,4 @@
<header>Logging</header>
<p>Add a log statement before the verdict.</p>
<p>Use prefix and level to customize.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Log level</header>
<p>Common values: emerg, alert, crit, err, warning, notice, info, debug.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Log prefix</header>
<p>String prepended to the log message.</p>
<footer>nft(8)</footer>

3
nftables/help/oif.html Normal file
View File

@@ -0,0 +1,3 @@
<header>Outgoing interface</header>
<p>Match the outgoing interface name (e.g., eth1).</p>
<footer>nft(8)</footer>

7
nftables/help/proto.html Normal file
View File

@@ -0,0 +1,7 @@
<header>Protocol</header>
<p>Select a protocol to match.</p>
<ul>
<li>Any, TCP, UDP, ICMP, ICMPv6</li>
</ul>
<p>Ports apply to TCP/UDP; ICMP uses the type field.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,4 @@
<header>Raw rule</header>
<p>Generated rule line.</p>
<p>In Edit directly mode, this is the rule that will be saved.</p>
<footer>nft(8)</footer>

3
nftables/help/saddr.html Normal file
View File

@@ -0,0 +1,3 @@
<header>Source address</header>
<p>IPv4/IPv6 address or CIDR (e.g., 192.168.1.0/24).</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Set elements</header>
<p>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.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Set flags</header>
<p>Optional flags for the set, like <tt>interval</tt> or <tt>timeout</tt>. Use the same syntax as nftables, separated by spaces if multiple.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Set name</header>
<p>Unique name for the set within the table. Use letters, numbers, underscores, or dashes. Rules reference a set as <tt>@name</tt>.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,3 @@
<header>Set type</header>
<p>Type of elements stored in the set, such as <tt>ipv4_addr</tt>, <tt>ipv6_addr</tt>, or <tt>inet_service</tt>. This must match the elements you enter.</p>
<footer>nft(8)</footer>

3
nftables/help/sport.html Normal file
View File

@@ -0,0 +1,3 @@
<header>Source port</header>
<p>TCP/UDP source port number or range (e.g., 22 or 1000-2000).</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,14 @@
<header>TCP flags</header>
<p>Select one or more TCP flags to match.</p>
<ul>
<li>fin</li>
<li>syn</li>
<li>rst</li>
<li>psh</li>
<li>ack</li>
<li>urg</li>
<li>ecn</li>
<li>cwr</li>
</ul>
<p>Use the mask field for a bitmask match.</p>
<footer>nft(8)</footer>

View File

@@ -0,0 +1,4 @@
<header>TCP flags mask</header>
<p>Optional mask used with tcp flags &amp; mask == value.</p>
<p>Example mask: syn|rst.</p>
<footer>nft(8)</footer>

BIN
nftables/images/icon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

206
nftables/index.cgi Executable file
View File

@@ -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', "<tt>nft</tt>");
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', "<pre>$out</pre>");
if (!$partial) {
ui_print_footer("/", $text{'index'});
}
exit;
}
# Load tables
my @tables = get_nftables_save();
my $rules_html = "";
if (!@tables) {
$rules_html .= "<b>$text{'index_none'}</b><p>\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 "<div class='nftables_table_select'>\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 "</div>\n";
print ui_form_end();
}
# Identify current table
my $curr = $tables[$in{'table'}];
if ($curr) {
# Show sets
$rules_html .= ui_hr();
$rules_html .= "<b>$text{'index_sets'}</b><br>\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'})."<br>".
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 .= "<i>$text{'index_sets_none'}</i><br>\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 = "<table class='nftables_rules_table' width='100%'>\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 .= "<tr><td>$rule_link</td>".
"<td align='right' style='white-space:nowrap'>$move</td></tr>\n";
$ri++;
}
$rules_html_row .= "<tr><td colspan='2'>".
ui_link("edit_rule.cgi?table=$in{'table'}&chain=".
urlize($c)."&new=1", $text{'index_radd'}).
"</td></tr>\n";
$rules_html_row .= "</table>";
} else {
$rules_html_row = "<i>$text{'index_rules_none'}</i>";
$rules_html_row .= "<br>".
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'})."<br>".
ui_link("rename_chain.cgi?table=$in{'table'}&chain=".
urlize($c), $text{'index_crename'})."<br>".
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 "<div id='nftables_ruleset'>\n";
print $rules_html;
print "</div>\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'});

18
nftables/install_check.pl Normal file
View File

@@ -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;
}

222
nftables/lang/en Normal file
View File

@@ -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: <pre>$1</pre>
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: <pre>$1</pre>
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: <pre>$1</pre>
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: <pre>$1</pre>
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: <pre>$1</pre>
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: <pre>$1</pre>
delete_confirm=Are you sure you want to delete table $1?
delete_notable=No such table selected
save_failed=Failed to save rule: <pre>$1</pre>
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: <pre>$1</pre>
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: <pre>$1</pre>
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: <pre>$1</pre>
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.

5
nftables/module.info Normal file
View File

@@ -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.

40
nftables/move_rule.cgi Executable file
View File

@@ -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'}");

1140
nftables/nftables-lib.pl Normal file

File diff suppressed because it is too large Load Diff

33
nftables/rename_chain.cgi Normal file
View File

@@ -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'},
"<tt>".html_escape($in{'chain'})."</tt>");
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'});

99
nftables/save_chain.cgi Normal file
View File

@@ -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'}");

180
nftables/save_rule.cgi Executable file
View File

@@ -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', "<pre>$out</pre>")) 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'}");

62
nftables/save_set.cgi Executable file
View File

@@ -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'}");

197
nftables/setup.cgi Normal file
View File

@@ -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 "<h3>$text{'setup_header'}</h3>";
my $webmin_port = get_webmin_port();
print "<p>$text{'setup_desc'}</p>";
print "<p>",text('setup_deny_note', $webmin_port),"</p>";
print ui_form_start("setup.cgi");
print ui_hidden("action", "create");
my @type_opts = (
[ 'allow_all', $text{'setup_allow_all'} . "<br>" ],
[ 'deny_incoming', $text{'setup_deny_incoming'} . "<br>" ],
[ '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'});

61
nftables/t/perlcritic.t Normal file
View File

@@ -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();

View File

@@ -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
}
}

View File

@@ -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
}
}

185
nftables/t/run-tests.t Executable file
View File

@@ -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();