Chain CRUD functionality

This commit is contained in:
Joe Cooper
2026-02-01 19:03:20 -06:00
parent 3f96fb8adb
commit 163dd04175
13 changed files with 353 additions and 2 deletions

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

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

3
help/chain_hook.html Normal file
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>

3
help/chain_name.html Normal file
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>

3
help/chain_policy.html Normal file
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>

3
help/chain_priority.html Normal file
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>

3
help/chain_type.html Normal file
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

@@ -76,7 +76,8 @@ if (!@tables) {
$rules_html .= &ui_columns_start(
[ $text{'index_chain_col'}, $text{'index_type'},
$text{'index_hook'}, $text{'index_priority'},
$text{'index_policy_col'}, $text{'index_rules'} ], 100);
$text{'index_policy_col'}, $text{'index_rules'},
$text{'index_actions'} ], 100);
foreach my $c (sort keys %{$curr->{'chains'}}) {
my $chain_def = $curr->{'chains'}->{$c} || { };
@@ -102,18 +103,30 @@ if (!@tables) {
&ui_link("edit_rule.cgi?table=$in{'table'}&chain=".
&urlize($c)."&new=1", $text{'index_radd'});
my $actions_html =
&ui_link("edit_chain.cgi?table=$in{'table'}&chain=".
&urlize($c), $text{'index_cedit'})."<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
$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'});

33
lang/en
View File

@@ -27,6 +27,10 @@ index_priority=Priority
index_policy_col=Policy
index_rules=Rules
index_rules_none=No rules
index_actions=Actions
index_chain_create=Create chain
index_chain_createdesc=Add a new chain to this table.
index_cedit=Edit Chain
index_cdelete=Delete Chain
index_crename=Rename Chain
index_cclear=Clear All Rules
@@ -125,6 +129,35 @@ edit_advanced=Advanced options
edit_oif=Outgoing Interface
edit_iif=Incoming Interface
edit_if_any=Any
chain_title_new=Create chain
chain_title_edit=Edit chain
chain_header=Chain Details
chain_name=Chain name
chain_type=Type
chain_hook=Hook
chain_priority=Priority
chain_policy=Policy
chain_type_none=Regular (no hook)
chain_hook_none=None
chain_policy_none=None
chain_err=Failed to save chain
chain_failed=Failed to save chain: <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

View File

@@ -181,6 +181,16 @@ return "ip6" if (defined($addr) && $addr =~ /:/);
return "ip";
}
sub validate_chain_base
{
my ($type, $hook, $priority, $policy) = @_;
if (defined($type) || defined($hook) || defined($priority) || defined($policy)) {
return 0 if (!defined($type) || !defined($hook) ||
!defined($priority) || !defined($policy));
}
return 1;
}
sub format_addr_expr
{
my ($dir, $rule) = @_;

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

View File

@@ -128,4 +128,11 @@ check_fields('ruleset r1', $rules[0], { iif => 'lo', action => 'accept' });
check_fields('ruleset r2', $rules[1], { saddr => '192.168.1.0/24', proto => 'tcp', dport => '22', action => 'accept', comment => 'ssh' });
check_fields('ruleset r3', $rules[2], { ct_state => 'established,related', action => 'accept' });
ok(&validate_chain_base('filter', 'input', '0', 'accept'),
'chain base allows zero priority');
ok(!&validate_chain_base('filter', 'input', undef, 'accept'),
'chain base missing priority invalid');
ok(&validate_chain_base(undef, undef, undef, undef),
'chain base none set valid');
done_testing();