Merge branch 'master' of github.com:webmin/webmin

This commit is contained in:
Jamie Cameron
2026-05-16 09:52:09 -07:00
15 changed files with 1846 additions and 107 deletions

View File

@@ -36,10 +36,13 @@ print ui_table_row(
foreach my $a (
qw(view active create setup chains sets rules raw delete
apply bootup import clear quick manual)
apply bootup import clear quick quick_ip quick_port
quick_service quick_forward manual)
)
{
print ui_table_row($text{'acl_'.$a}, ui_yesno_radio($a, $o->{$a}));
my $enabled = $o->{$a};
$enabled = $o->{'quick'} if ($a =~ /^quick_/ && !defined($enabled));
print ui_table_row($text{'acl_'.$a}, ui_yesno_radio($a, $enabled));
}
}
@@ -58,7 +61,8 @@ else {
}
foreach my $a (
qw(view active create setup chains sets rules raw delete
apply bootup import clear quick manual)
apply bootup import clear quick quick_ip quick_port
quick_service quick_forward manual)
)
{
$_[0]->{$a} = $in{$a} || 0;

View File

@@ -13,4 +13,8 @@ bootup=1
import=1
clear=1
quick=1
quick_ip=1
quick_port=1
quick_service=1
quick_forward=1
manual=1

View File

@@ -32,6 +32,8 @@ my $saddr_val;
my $daddr_val;
my $sport_val;
my $dport_val;
my $nat_addr_val;
my $nat_port_val;
my @addr_set_opts;
my @port_set_opts;
my %set_families;
@@ -99,6 +101,8 @@ $saddr_val = $saddr_set ? "" : $rule->{'saddr'};
$daddr_val = $daddr_set ? "" : $rule->{'daddr'};
$sport_val = $sport_set ? "" : $rule->{'sport'};
$dport_val = $dport_set ? "" : $rule->{'dport'};
$nat_addr_val = $rule->{'nat_addr'};
$nat_port_val = $rule->{'nat_port'};
@addr_set_opts = (["", $text{'edit_set_none'}]);
@port_set_opts = (["", $text{'edit_set_none'}]);
@@ -198,20 +202,39 @@ print ui_table_row(
);
# Action
my $show_nat_actions =
($chain_def && (($chain_def->{'type'} || '') eq 'nat')) ||
($action_sel && $action_sel =~ /^(redirect|dnat)$/);
my @action_opts = (
["accept", $text{'index_accept'}],
["drop", $text{'index_drop'}],
["reject", $text{'index_reject'}],
["return", $text{'edit_return'}],
);
push(@action_opts,
["redirect", $text{'edit_redirect_action'}],
["dnat", $text{'edit_dnat_action'}])
if ($show_nat_actions);
push(@action_opts,
["jump", $text{'edit_jump_action'}],
["goto", $text{'edit_goto_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'}],
]
)
ui_select("action", $action_sel, \@action_opts)
);
my $nat_show = $action_sel && $action_sel =~ /^(redirect|dnat)$/;
my $nat_addr_style = $action_sel && $action_sel eq 'dnat' ? "" : " style='display:none'";
my $nat_port_style = $nat_show ? "" : " style='display:none'";
print ui_table_row(
hlink($text{'edit_nat_addr'}, "nat_addr"),
ui_textbox("nat_addr", $nat_addr_val, 30),
undef, undef, ["id='nftables_nat_addr_row'".$nat_addr_style]
);
print ui_table_row(
hlink($text{'edit_nat_port'}, "nat_port"),
ui_textbox("nat_port", $nat_port_val, 10),
undef, undef, ["id='nftables_nat_port_row'".$nat_port_style]
);
# Addresses
@@ -400,6 +423,9 @@ my $icmpv6_js = js_array(@icmpv6_types);
my $icmp_any = $text{'edit_proto_any'};
$icmp_any =~ s/\\/\\\\/g;
$icmp_any =~ s/"/\\"/g;
my $table_family = $table->{'family'} || '';
$table_family =~ s/\\/\\\\/g;
$table_family =~ s/"/\\"/g;
my $set_fam_js = js_object(%set_families);
print "<script>\n";
@@ -407,6 +433,7 @@ print "(function() {\n";
print " var icmpTypes = $icmp_js;\n";
print " var icmpv6Types = $icmpv6_js;\n";
print " var icmpAnyLabel = \"$icmp_any\";\n";
print " var tableFamily = \"$table_family\";\n";
print " var setFamilies = $set_fam_js;\n";
print <<'EOF';
function byName(name) {
@@ -454,6 +481,13 @@ print <<'EOF';
function guessFamily(addr) {
return addr.indexOf(":") >= 0 ? "ip6" : "ip";
}
function natTarget(addr, port) {
if (addr) {
if (port) return addr.indexOf(":") >= 0 ? "[" + addr + "]:" + port : addr + ":" + port;
return addr;
}
return port ? ":" + port : "";
}
function familyForSet(name) {
return setFamilies && setFamilies[name] ? setFamilies[name] : "";
}
@@ -468,6 +502,7 @@ print <<'EOF';
function buildRule() {
var direct = byName("edit_direct");
if (direct && direct.checked) return;
toggleNatFields();
var parts = [];
var iif = ifaceVal("iif");
@@ -573,6 +608,19 @@ print <<'EOF';
var go = val("goto");
if (action === "jump" && jump) parts.push("jump " + jump);
else if (action === "goto" && go) parts.push("goto " + go);
else if (action === "redirect") {
var redirectTarget = natTarget("", val("nat_port"));
parts.push(redirectTarget ? "redirect to " + redirectTarget : "redirect");
}
else if (action === "dnat") {
var natAddr = val("nat_addr");
var natPort = val("nat_port");
var dnatTarget = natTarget(natAddr, natPort);
var dnatExpr = "dnat";
if (natAddr && tableFamily === "inet") dnatExpr += " " + guessFamily(natAddr);
if (dnatTarget) dnatExpr += " to " + dnatTarget;
parts.push(dnatExpr);
}
else if (action && action !== "jump" && action !== "goto") parts.push(action);
var comment = val("comment");
@@ -603,6 +651,17 @@ print <<'EOF';
if (!on) buildRule();
}
function setRowVisible(id, visible) {
var el = document.getElementById(id);
if (el) el.style.display = visible ? "" : "none";
}
function toggleNatFields() {
var action = val("action");
setRowVisible("nftables_nat_addr_row", action === "dnat");
setRowVisible("nftables_nat_port_row", action === "redirect" || action === "dnat");
}
function uniqList(list) {
var seen = {};
var out = [];
@@ -683,6 +742,7 @@ print <<'EOF';
el.addEventListener("input", buildRule);
el.addEventListener("change", buildRule);
}
toggleNatFields();
updateIcmpTypes();
toggleDirect();
buildRule();

View File

@@ -18,10 +18,301 @@ if (!$can_view_saved &&
}
my $partial = $in{'partial'};
if (!$partial) {
ui_print_header(nft_version_text(), $text{'index_title'}, "", "intro", 1, 1,
undef, restart_button());
ui_print_header(nft_version_text() || "",
$text{'index_title'}, "", "intro", 1, 1,
undef, restart_button());
}
# quick_hidden_fields(table-index, &table, selected-view)
# Returns hidden table selectors for quick action forms
sub quick_hidden_fields
{
my ($idx, $table, $view) = @_;
return ui_hidden("table", $idx).
ui_hidden("table_family", $table->{'family'}).
ui_hidden("table_name", $table->{'name'}).
ui_hidden("view", $view);
}
# quick_service_autocomplete()
# Returns the quick service textbox and JavaScript-backed matcher
sub quick_service_autocomplete
{
my $placeholder = quote_escape($text{'quick_service_placeholder'});
my $results_style = "display: none; position: absolute; z-index: 1000; ".
"left: 0; right: auto; width: 100%; min-width: 0; ".
"max-height: 18em; overflow: auto; ".
"border: 1px solid var(--border-color-input-results, ".
"var(--border-color-input, #3f4855)); ".
"border-radius: var(--border-radius-input, 3px); ".
"background-color: var(--bg-color-input, #fff); ".
"color: var(--text-color, inherit);";
my $results = ui_tag('div', undef, {
'id' => 'nftables_quick_service_results',
'role' => 'listbox',
'style' => $results_style,
});
my $input = ui_textbox(
"service_text",
"",
32,
undef,
undef,
"autocomplete='off' placeholder='".$placeholder."'"
);
my $wrap = ui_tag('span', $input.$results, {
'id' => 'nftables_quick_service_wrap',
'style' => 'position: relative; display: inline-block; max-width: 100%;',
});
return ui_hidden("service", "").
$wrap.
quick_service_autocomplete_javascript();
}
# quick_service_autocomplete_javascript()
# Returns JavaScript for the quick service autocomplete widget
sub quick_service_autocomplete_javascript
{
my $labels = convert_to_json({
'no_matches' => $text{'quick_service_nomatch'},
'failed' => $text{'quick_service_searchfail'},
});
my $js = <<EOF;
(function() {
var labels = $labels;
if (!window.fetch) {
return;
}
var mode = document.querySelector('form[action="manage_port.cgi"] input[name="mode"][value="service"]');
if (!mode || !mode.form) {
return;
}
var form = mode.form;
var input = form.querySelector('input[name="service_text"]');
var hidden = form.querySelector('input[name="service"]');
var box = document.getElementById('nftables_quick_service_results');
if (!input || !hidden || !box) {
return;
}
var timer = null;
var serial = 0;
var currentQuery = "";
var results = [];
var active = -1;
function trim(value) {
return (value || "").replace(/^\\s+|\\s+\$/g, "");
}
function showBox() {
placeBox();
box.style.display = "block";
}
function hideBox() {
box.style.display = "none";
box.textContent = "";
results = [];
active = -1;
}
function placeBox() {
var rect = input.getBoundingClientRect();
var below = window.innerHeight - rect.bottom;
var above = rect.top;
box.style.width = input.offsetWidth + "px";
var preferred = Math.min(box.scrollHeight || 288, 288);
if (below < preferred && above >= preferred) {
box.style.top = "auto";
box.style.bottom = (input.offsetHeight + 2) + "px";
}
else {
box.style.top = (input.offsetHeight + 2) + "px";
box.style.bottom = "auto";
}
}
function styleRow(row, selected) {
row.style.padding = "0.25em 0.45em";
row.style.cursor = "pointer";
row.style.whiteSpace = "nowrap";
row.style.overflow = "hidden";
row.style.textOverflow = "ellipsis";
row.style.borderTop = "1px solid var(--border-color-input-results, #3f4855)";
row.style.backgroundColor = selected ?
"var(--bg-color-input-results-hover, rgba(127,127,127,0.16))" :
"";
}
function setActive(index) {
active = index;
var rows = box.querySelectorAll('[data-service-id]');
for (var i = 0; i < rows.length; i++) {
styleRow(rows[i], i === active);
}
var row = rows[active];
if (row) {
var top = row.offsetTop;
var bottom = top + row.offsetHeight;
if (top < box.scrollTop) {
box.scrollTop = top;
}
else if (bottom > box.scrollTop + box.clientHeight) {
box.scrollTop = bottom - box.clientHeight;
}
}
}
function choose(item) {
if (!item) {
return;
}
hidden.value = item.id || "";
input.value = item.label || item.id || "";
hideBox();
}
function message(text) {
box.textContent = "";
var row = document.createElement("div");
row.textContent = text;
row.style.padding = "0.25em 0.45em";
row.style.fontStyle = "italic";
box.appendChild(row);
results = [];
active = -1;
showBox();
}
function draw(items, query) {
box.textContent = "";
results = items || [];
if (!results.length) {
if (query) {
message(labels.no_matches);
}
else {
hideBox();
}
return;
}
results.forEach(function(item, index) {
var row = document.createElement("div");
row.setAttribute("role", "option");
row.setAttribute("data-service-id", item.id || "");
row.textContent = item.label || item.id || "";
styleRow(row, false);
row.addEventListener("mousedown", function(event) {
event.preventDefault();
choose(item);
});
row.addEventListener("mousemove", function() {
setActive(index);
});
box.appendChild(row);
});
showBox();
setActive(0);
}
function search() {
var query = trim(input.value);
currentQuery = query;
if (!query) {
hideBox();
return;
}
var mySerial = ++serial;
fetch("search_services.cgi?q=" + encodeURIComponent(query) + "&limit=20", {
credentials: "same-origin"
}).then(function(response) {
if (!response.ok) {
throw new Error("service search failed");
}
return response.json();
}).then(function(items) {
if (mySerial !== serial || query !== currentQuery) {
return;
}
draw(items, query);
}).catch(function() {
if (mySerial === serial) {
message(labels.failed);
}
});
}
input.addEventListener("input", function() {
hidden.value = "";
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(search, 200);
});
input.addEventListener("focus", function() {
if (trim(input.value) && !hidden.value) {
search();
}
});
input.addEventListener("keydown", function(event) {
var open = box.style.display !== "none";
if (!open) {
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
if (results.length) {
setActive((active + 1) % results.length);
}
}
else if (event.key === "ArrowUp") {
event.preventDefault();
if (results.length) {
setActive((active + results.length - 1) % results.length);
}
}
else if (event.key === "Enter" && active >= 0 && results[active]) {
event.preventDefault();
choose(results[active]);
}
else if (event.key === "Escape") {
hideBox();
}
});
form.addEventListener("submit", function() {
if (!hidden.value) {
hidden.value = trim(input.value);
}
});
document.addEventListener("mousedown", function(event) {
var wrap = document.getElementById("nftables_quick_service_wrap");
if (wrap && !wrap.contains(event.target)) {
hideBox();
}
});
window.addEventListener("resize", function() {
if (box.style.display !== "none") {
placeBox();
}
});
window.addEventListener("scroll", function() {
if (box.style.display !== "none") {
placeBox();
}
}, true);
})();
EOF
return ui_tag('script', $js, {
'type' => 'text/javascript',
});
}
# Check for nft command
my $cmd = get_nft_command();
if (!$cmd) {
@@ -443,29 +734,103 @@ else {
}
$rules_html .= ui_tabs_end(1);
if (check_acl('quick') && find_input_chain($curr)) {
my $ip_placeholder =
text('quick_ip_placeholder', '1.2.3.4', '2001:db8::1/64');
foreach my $action (
['allow', $text{'index_allowip_go'}],
['block', $text{'index_blockip_go'}],
)
{
if (check_quick_acl() && !table_supports_quick_l4($curr)) {
my @proto_opts = (
['tcp', 'TCP'],
['udp', 'UDP'],
);
my $has_input_chain = find_input_chain($curr) ? 1 : 0;
if ($has_input_chain) {
my $ip_placeholder =
text('quick_ip_placeholder', '1.2.3.4', '2001:db8::1/64');
if (check_quick_acl('ip')) {
foreach my $action (
['allow', $text{'index_allowip_go'}],
['block', $text{'index_blockip_go'}],
)
{
$rules_html .=
"<br>".ui_form_start("manage_ip.cgi", "post");
$rules_html .= quick_hidden_fields($in{'table'}, $curr, $tab);
$rules_html .= ui_submit($action->[1], $action->[0]).
ui_textbox(
"ip",
undef,
22,
undef,
undef,
"placeholder='".
quote_escape($ip_placeholder)."'"
);
$rules_html .= ui_form_end();
}
}
if (check_quick_acl('port')) {
$rules_html .=
"<br>".ui_form_start("manage_port.cgi", "post");
$rules_html .= quick_hidden_fields($in{'table'}, $curr, $tab);
$rules_html .= ui_hidden("mode", "port");
$rules_html .= ui_submit($text{'index_allowport_go'}, "allow_port").
ui_textbox(
"port",
undef,
14,
undef,
undef,
"placeholder='".
quote_escape($text{'quick_port_placeholder'})."'"
).
" ".
ui_select("proto", "tcp", \@proto_opts, 1, 0, 1);
$rules_html .= ui_form_end();
}
if (check_quick_acl('service')) {
$rules_html .=
"<br>".ui_form_start("manage_port.cgi", "post");
$rules_html .= quick_hidden_fields($in{'table'}, $curr, $tab);
$rules_html .= ui_hidden("mode", "service");
$rules_html .=
ui_submit($text{'index_allowservice_go'},
"allow_service").
quick_service_autocomplete();
$rules_html .= ui_form_end();
}
}
if (check_quick_acl('forward')) {
$rules_html .=
"<br>".ui_form_start("manage_ip.cgi", "post");
$rules_html .= ui_hidden("table", $in{'table'});
$rules_html .=
ui_hidden("table_family", $curr->{'family'});
$rules_html .= ui_hidden("table_name", $curr->{'name'});
$rules_html .= ui_submit($action->[1], $action->[0]).
"<br>".ui_form_start("manage_forward.cgi", "post");
$rules_html .= quick_hidden_fields($in{'table'}, $curr, $tab);
$rules_html .= ui_submit($text{'index_forward_go'}, "forward").
ui_textbox(
"ip",
"src_port",
undef,
22,
10,
undef,
undef,
"placeholder='".
quote_escape($ip_placeholder)."'"
"placeholder='".quote_escape($text{'quick_forward_src'})."'"
).
" ".
ui_select("proto", "tcp", \@proto_opts, 1, 0, 1).
" ".
$text{'quick_forward_to'}.
" ".
ui_textbox(
"dst_port",
undef,
10,
undef,
undef,
"placeholder='".quote_escape($text{'quick_forward_dst'})."'"
).
" ".
ui_textbox(
"dst_addr",
undef,
32,
undef,
undef,
"placeholder='".quote_escape($text{'quick_forward_addr'})."'"
);
$rules_html .= ui_form_end();
}

View File

@@ -58,14 +58,37 @@ index_edit_manual=Edit Config Files
index_edit_manualdesc=Edit saved nftables configuration files manually.
index_allowip_go=Allow IP/CIDR
index_blockip_go=Block IP/CIDR
index_allowport_go=Add allowed port
index_allowservice_go=Add allowed service
index_forward_go=Add port forward
quick_ip_placeholder=$1 or $2
quick_port_placeholder=80 or 8000-8080
quick_service_placeholder=ssh, http or dns
quick_service_nomatch=No matching services
quick_service_searchfail=Service lookup failed
quick_forward_src=from port
quick_forward_to=to
quick_forward_dst=port
quick_forward_addr=destination IP, blank for this system
quick_source_ports=source
quick_allow_err=Failed to allow IP/CIDR
quick_block_err=Failed to block IP/CIDR
quick_port_err=Failed to add allowed port
quick_service_err=Failed to add allowed service
quick_forward_err=Failed to add port forward
quick_eaction=No quick action selected.
quick_etable=No such table selected.
quick_echain=Table $1 has no input chain to add this rule to.
quick_efamily=Table $1 cannot contain IP source address rules.
quick_efamily=Table $1 cannot contain these quick firewall rules.
quick_eip=Enter a valid IPv4 or IPv6 address, with an optional CIDR prefix.
quick_eport=Enter a valid TCP or UDP port number, or a range like 8000-8080.
quick_eportrange=Port ranges must start with a lower port number.
quick_eproto=Select TCP or UDP as the network protocol.
quick_eservice=Enter or select a valid service to allow.
quick_eservice_empty=Service $1 has no usable TCP, UDP or protocol definitions for nftables.
quick_eforward_addr=Enter a valid IPv4 or IPv6 destination address, without a CIDR prefix.
quick_eforward_family=Table $1 cannot forward to that destination address family.
quick_eforward_target=Enter a destination port and/or destination address for the forward.
quick_edup=An equivalent quick rule for $1 already exists.
quick_failed=Failed to save and apply quick rule: $1
index_unapply=Revert Configuration
@@ -172,15 +195,23 @@ index_accept=Accept
index_drop=Drop
index_reject=Reject
index_return_action=Return
index_redirect=Redirect
index_dnat=DNAT
index_redirect_to=Redirect to $1
index_dnat_to=DNAT to $1
edit_title_new=Create Rule
edit_title_edit=Edit Rule
edit_comment=Comment
edit_action=Action
edit_return=Return
edit_redirect_action=Redirect
edit_dnat_action=DNAT
edit_jump_action=Jump
edit_goto_action=Goto
edit_jump=Jump target chain
edit_goto=Goto target chain
edit_nat_addr=Forward to address
edit_nat_port=Forward to port
edit_proto=Protocol
edit_proto_any=Any
edit_saddr=Source address
@@ -361,5 +392,9 @@ acl_apply=Apply saved configuration
acl_bootup=Enable firewall at boot
acl_import=Import active tables
acl_clear=Clear active tables
acl_quick=Use quick allow/block controls
acl_quick=Use quick controls
acl_quick_ip=Use quick IP allow/block controls
acl_quick_port=Use quick allowed port controls
acl_quick_service=Use quick allowed service controls
acl_quick_forward=Use quick port forward controls
acl_manual=Edit config files manually

61
nftables/manage_forward.cgi Executable file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/perl
# manage_forward.cgi
# Quickly add a simple port forward in the selected table
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
assert_quick_acl('forward');
error_setup($text{'quick_forward_err'});
my @tables = get_nftables_save();
my $table_idx = $in{'table'};
my $table;
if (defined($in{'table_family'}) && defined($in{'table_name'})) {
for (my $i = 0 ; $i <= $#tables ; $i++) {
if ($tables[$i]->{'family'} eq $in{'table_family'} &&
$tables[$i]->{'name'} eq $in{'table_name'})
{
$table_idx = $i;
$table = $tables[$i];
last;
}
}
}
else {
$table = $tables[$table_idx];
}
$table || error($text{'quick_etable'});
assert_table_acl($table);
my $err = add_quick_forward_rule(
$table,
$in{'src_port'},
$in{'proto'},
$in{'dst_port'},
$in{'dst_addr'}
);
error($err) if ($err);
$err = save_table_configuration($table, @tables);
error(text('quick_failed', $err)) if ($err);
# Quick forwarding is expected to affect the live firewall immediately.
$err = apply_restore();
error(text('quick_failed', $err)) if ($err);
webmin_log(
"create",
"forward",
$in{'src_port'},
{'table' => $table->{'name'}, 'family' => $table->{'family'}}
);
my $redir = "index.cgi?table_family=".
urlize($table->{'family'}).
"&table_name=".
urlize($table->{'name'});
$redir .= "&view=".urlize($in{'view'})
if (($in{'view'} || '') =~ /^(chains|sets)$/);
redirect($redir);

View File

@@ -7,7 +7,7 @@ use strict;
use warnings;
our (%in, %text);
ReadParse();
assert_acl('quick');
assert_quick_acl('ip');
my $action = $in{'allow'} ? 'allow' : $in{'block'} ? 'block' : '';
error_setup(
@@ -48,7 +48,10 @@ error(text('quick_failed', $err)) if ($err);
webmin_log($action, "ip", $in{'ip'},
{'table' => $table->{'name'}, 'family' => $table->{'family'}});
redirect("index.cgi?table_family=".
my $redir = "index.cgi?table_family=".
urlize($table->{'family'}).
"&table_name=".
urlize($table->{'name'}));
urlize($table->{'name'});
$redir .= "&view=".urlize($in{'view'})
if (($in{'view'} || '') =~ /^(chains|sets)$/);
redirect($redir);

69
nftables/manage_port.cgi Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/perl
# manage_port.cgi
# Quickly allow a port or service in the selected table
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in, %text);
ReadParse();
my $mode = $in{'mode'} || '';
assert_quick_acl($mode eq 'service' ? 'service' : 'port');
error_setup(
$mode eq 'service' ? $text{'quick_service_err'} : $text{'quick_port_err'}
);
my @tables = get_nftables_save();
my $table_idx = $in{'table'};
my $table;
if (defined($in{'table_family'}) && defined($in{'table_name'})) {
for (my $i = 0 ; $i <= $#tables ; $i++) {
if ($tables[$i]->{'family'} eq $in{'table_family'} &&
$tables[$i]->{'name'} eq $in{'table_name'})
{
$table_idx = $i;
$table = $tables[$i];
last;
}
}
}
else {
$table = $tables[$table_idx];
}
$table || error($text{'quick_etable'});
assert_table_acl($table);
my $err;
my $service = $in{'service'};
if (!defined($service) || $service eq '') {
$service = $in{'service_text'};
}
if ($mode eq 'service') {
$err = add_quick_service_rule($table, $service);
}
else {
$err = add_quick_port_rule($table, $in{'port'}, $in{'proto'});
}
error($err) if ($err);
$err = save_table_configuration($table, @tables);
error(text('quick_failed', $err)) if ($err);
# Quick allow actions are expected to affect the live firewall immediately.
$err = apply_restore();
error(text('quick_failed', $err)) if ($err);
webmin_log(
"allow",
$mode eq 'service' ? "service" : "port",
$mode eq 'service' ? $service : $in{'port'},
{'table' => $table->{'name'}, 'family' => $table->{'family'}}
);
my $redir = "index.cgi?table_family=".
urlize($table->{'family'}).
"&table_name=".
urlize($table->{'name'});
$redir .= "&view=".urlize($in{'view'})
if (($in{'view'} || '') =~ /^(chains|sets)$/);
redirect($redir);

File diff suppressed because it is too large Load Diff

View File

@@ -81,6 +81,9 @@ else {
$rule->{'action'} = undef;
$rule->{'jump'} = undef;
$rule->{'goto'} = undef;
$rule->{'nat_addr'} = undef;
$rule->{'nat_port'} = undef;
$rule->{'nat_family'} = undef;
if ($action eq 'jump') {
$rule->{'jump'} = $in{'jump'};
}
@@ -90,6 +93,29 @@ else {
else {
$rule->{'action'} = $action;
}
if ($action eq 'redirect') {
my $nat_port = $in{'nat_port'};
$nat_port =~ s/^\s+// if (defined($nat_port));
$nat_port =~ s/\s+$// if (defined($nat_port));
$rule->{'nat_port'} =
(defined($nat_port) && $nat_port ne '') ? $nat_port : undef;
}
elsif ($action eq 'dnat') {
my $nat_addr = $in{'nat_addr'};
my $nat_port = $in{'nat_port'};
$nat_addr =~ s/^\s+// if (defined($nat_addr));
$nat_addr =~ s/\s+$// if (defined($nat_addr));
$nat_port =~ s/^\s+// if (defined($nat_port));
$nat_port =~ s/\s+$// if (defined($nat_port));
$rule->{'nat_addr'} =
(defined($nat_addr) && $nat_addr ne '') ? $nat_addr : undef;
$rule->{'nat_port'} =
(defined($nat_port) && $nat_port ne '') ? $nat_port : undef;
$rule->{'nat_family'} =
$rule->{'nat_addr'} &&
(($table->{'family'} || '') eq 'inet') ?
guess_addr_family($rule->{'nat_addr'}) : undef;
}
my $saddr = $in{'saddr'};
my $daddr = $in{'daddr'};

22
nftables/search_services.cgi Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/perl
# search_services.cgi
# Return matching quick service definitions for the autocomplete widget
require './nftables-lib.pl'; ## no critic
use strict;
use warnings;
our (%in);
ReadParse();
assert_quick_acl('service');
my $limit = $in{'limit'} || 20;
$limit = 20 if ($limit !~ /^\d+$/);
my @services = search_quick_services($in{'q'}, $limit);
my @results = map {
{
'id' => $_->{'id'},
'label' => $_->{'label'} || $_->{'id'},
}
} @services;
print_json(\@results);

View File

@@ -1,8 +0,0 @@
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

@@ -1,8 +0,0 @@
table inet firewalld {
flags owner,persist
chain filter_INPUT {
type filter hook input priority filter + 10; policy accept;
ct state { established, related } accept
}
}

View File

@@ -1,18 +0,0 @@
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
}
}

View File

@@ -40,6 +40,18 @@ $ENV{'FOREIGN_ROOT_DIRECTORY'} = $rootdir;
chdir("$bindir/..") or die "chdir: $!";
require "$bindir/../nftables-lib.pl";
our %access;
{
local %access = (quick => 1);
ok(check_quick_acl('forward'), 'quick sub-acl defaults to allowed');
$access{quick_forward} = 0;
ok(!check_quick_acl('forward'), 'quick sub-acl can deny one action');
$access{quick_forward} = 1;
ok(check_quick_acl('forward'), 'quick sub-acl can allow one action');
$access{quick} = 0;
ok(!check_quick_acl('forward'), 'quick master acl denies sub-actions');
}
my $services_file = "$confdir/services";
open(my $sfh, ">", $services_file) or die "services: $!";
@@ -71,6 +83,16 @@ sub check_fields
}
}
sub write_ruleset
{
my ($dir, $name, $content) = @_;
my $file = "$dir/$name";
open(my $fh, ">", $file) or die "$name: $!";
print $fh $content;
close($fh);
return $file;
}
my @cases = (
{
name => 'tcp dport accept',
@@ -117,6 +139,30 @@ my @cases = (
expect => { proto => 'tcp', dport => '22', action => 'accept' },
preserve => 'meta skgid 1000',
},
{
name => 'redirect target',
line => 'tcp dport 2023 redirect to :20022 comment "Webmin quick forward"',
expect => {
proto => 'tcp',
dport => '2023',
action => 'redirect',
nat_port => '20022',
comment => 'Webmin quick forward',
},
},
{
name => 'dnat target',
line => 'tcp dport 8080 dnat ip to 192.0.2.10:80 comment "Webmin quick forward"',
expect => {
proto => 'tcp',
dport => '8080',
action => 'dnat',
nat_family => 'ip',
nat_addr => '192.0.2.10',
nat_port => '80',
comment => 'Webmin quick forward',
},
},
);
foreach my $c (@cases) {
@@ -134,7 +180,28 @@ foreach my $c (@cases) {
check_fields($c->{name}.' roundtrip', $r2, $c->{expect});
}
my $ruleset = "$bindir/rulesets/basic.nft";
my $redirect_desc = describe_rule(parse_rule_text(
'tcp dport 2026 redirect to :20026 comment "Webmin quick forward"'));
like($redirect_desc, qr/Redirect.*:20026.*Destination port 2026/,
'redirect rule summary includes target port');
my $dnat_desc = describe_rule(parse_rule_text(
'tcp dport 2024 dnat ip to 10.211.55.21:20024 comment "Webmin quick forward"'));
like($dnat_desc, qr/DNAT.*10\.211\.55\.21:20024.*Destination port 2024/,
'dnat rule summary includes target address and port');
is(format_forward_target({ family => 'ip' }, '10.211.55.21', 'ip', '20024'),
'dnat to 10.211.55.21:20024',
'ip quick forward omits inet-only dnat family');
my $ruleset = write_ruleset($confdir, "basic.nft", <<'EOF');
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
}
}
EOF
my @tables = get_nftables_save($ruleset);
ok(@tables == 1, 'ruleset table count');
my $t = $tables[0];
@@ -147,32 +214,63 @@ is($chain->{hook}, 'input', 'chain hook');
is($chain->{priority}, '0', 'chain priority');
is($chain->{policy}, 'drop', 'chain policy');
my $ruleset_prio = "$bindir/rulesets/firewalld-priority.nft";
my $ruleset_prio = write_ruleset($confdir, "externally-managed-priority.nft", <<'EOF');
table inet externally_managed {
flags owner,persist
chain managed_INPUT {
type filter hook input priority filter + 10; policy accept;
ct state { established, related } accept
}
}
EOF
my @tables_prio = get_nftables_save($ruleset_prio);
ok(@tables_prio == 1, 'firewalld priority table count');
is($tables_prio[0]->{flags}, 'owner,persist', 'firewalld table flags');
ok(table_is_externally_managed($tables_prio[0]), 'firewalld table is externally managed');
ok(@tables_prio == 1, 'externally managed priority table count');
is($tables_prio[0]->{flags}, 'owner,persist', 'externally managed table flags');
ok(table_is_externally_managed($tables_prio[0]),
'table with owner,persist flags is externally managed');
is(active_table_status($tables_prio[0], []), 'external',
'external active table status');
is(active_table_status({ family => 'inet', name => 'filter' }, [ $t ]), 'webmin',
'saved active table status');
is(active_table_status({ family => 'inet', name => 'loose' }, []), 'unclaimed',
'unclaimed active table status');
my $fw_chain = $tables_prio[0]->{chains}->{filter_INPUT};
ok($fw_chain, 'firewalld priority chain present');
is($fw_chain->{type}, 'filter', 'firewalld priority chain type');
is($fw_chain->{hook}, 'input', 'firewalld priority chain hook');
is($fw_chain->{priority}, 'filter + 10', 'firewalld priority chain priority');
is($fw_chain->{policy}, 'accept', 'firewalld priority chain policy');
my $managed_chain = $tables_prio[0]->{chains}->{managed_INPUT};
ok($managed_chain, 'externally managed priority chain present');
is($managed_chain->{type}, 'filter', 'externally managed priority chain type');
is($managed_chain->{hook}, 'input', 'externally managed priority chain hook');
is($managed_chain->{priority}, 'filter + 10',
'externally managed symbolic priority preserved');
is($managed_chain->{policy}, 'accept',
'externally managed priority chain policy');
is(scalar @{$tables_prio[0]->{rules}}, 1,
'firewalld priority chain definition is not parsed as a rule');
'externally managed chain definition is not parsed as a rule');
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 $ruleset_sets = write_ruleset($confdir, "sets.nft", <<'EOF');
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
}
}
EOF
my @tables_sets = get_nftables_save($ruleset_sets);
ok(@tables_sets == 1, 'sets ruleset table count');
my $ts = $tables_sets[0];
@@ -257,6 +355,112 @@ my $quick_ip_table = {
like(add_quick_ip_rule($quick_ip_table, '2001:db8::1/64', 'allow'),
qr/cannot contain/, 'wrong address family rejected');
my $quick_port_table = {
family => 'inet',
name => 'quickports',
chains => {
input => { hook => 'input' },
},
sets => {
allowed_tcp => {
name => 'allowed_tcp',
type => 'inet_service',
elements => [ '22' ],
raw_lines => [ ],
},
},
rules => [
{
chain => 'input',
index => 0,
proto => 'tcp',
dport => '@allowed_tcp',
action => 'accept',
text => 'tcp dport @allowed_tcp accept',
},
],
};
is(add_quick_port_rule($quick_port_table, '8443', 'tcp'), undef,
'quick port added to accepted set');
is_deeply($quick_port_table->{sets}->{allowed_tcp}->{elements},
[ '22', '8443' ], 'quick port set extended');
like(add_quick_port_rule($quick_port_table, '8443', 'tcp'), qr/exists/,
'duplicate quick port rejected');
my ($customsvc) = grep { $_->{id} eq 'customsvc' }
read_etc_service_defs($services_file);
ok($customsvc, '/etc/services service parsed');
is($customsvc->{id}, 'customsvc', '/etc/services service id');
like($customsvc->{label}, qr/customsvc \(4242 TCP; 4243 UDP\)/,
'/etc/services service label includes ports and protocol');
is_deeply([ sort { $a cmp $b } quick_service_rules($customsvc) ],
[ 'tcp dport 4242 accept',
'udp dport 4243 accept' ],
'/etc/services service rules generated');
my @service_matches = search_quick_services('customsvc', 5, $services_file);
ok(@service_matches, 'quick service search returns matches');
is($service_matches[0]->{id}, 'customsvc',
'quick service search ranks exact service IDs first');
my @alias_service_matches = search_quick_services('custom-alias', 5, $services_file);
is($alias_service_matches[0]->{id}, 'customsvc',
'quick service search matches /etc/services aliases');
is(quick_service_by_id('custom-alias', $services_file)->{id}, 'customsvc',
'quick service lookup accepts /etc/services aliases');
my @empty_service_matches = search_quick_services('', 5, $services_file);
is(scalar(@empty_service_matches), 0,
'empty quick service search returns no matches');
my $forward_table = {
family => 'inet',
name => 'forward',
chains => {
input => { type => 'filter', hook => 'input', priority => 0, policy => 'drop' },
forward => { type => 'filter', hook => 'forward', priority => 0, policy => 'drop' },
},
sets => {},
rules => [],
};
is(add_quick_forward_rule($forward_table, '8080', 'tcp', '80', '192.0.2.10'), undef,
'quick forward added');
ok($forward_table->{chains}->{prerouting}, 'quick forward created prerouting chain');
is($forward_table->{chains}->{prerouting}->{type}, 'nat',
'quick forward prerouting chain is nat');
ok(scalar(grep {
$_->{chain} eq 'prerouting' &&
$_->{text} eq 'tcp dport 8080 dnat ip to 192.0.2.10:80 comment "Webmin quick forward"'
} @{$forward_table->{rules}}),
'quick forward DNAT rule added');
ok(scalar(grep {
$_->{chain} eq 'forward' &&
$_->{text} eq 'ct state established,related accept comment "Webmin quick forward"'
} @{$forward_table->{rules}}),
'quick forward established rule added');
ok(scalar(grep {
$_->{chain} eq 'forward' &&
$_->{text} eq 'ip daddr 192.0.2.10 tcp dport 80 accept comment "Webmin quick forward"'
} @{$forward_table->{rules}}),
'quick forward destination accept added');
my $redirect_table = {
family => 'inet',
name => 'redirect',
chains => {
input => { type => 'filter', hook => 'input', priority => 0, policy => 'drop' },
forward => { type => 'filter', hook => 'forward', priority => 0, policy => 'drop' },
},
sets => {},
rules => [],
};
is(add_quick_forward_rule($redirect_table, '2023', 'tcp', '20022', ''), undef,
'quick local redirect added');
my ($redirect_rule) = grep {
$_->{chain} eq 'prerouting' &&
$_->{text} eq 'tcp dport 2023 redirect to :20022 comment "Webmin quick forward"'
} @{$redirect_table->{rules}};
ok($redirect_rule, 'quick local redirect rule added');
check_fields('quick local redirect rule', $redirect_rule,
{ proto => 'tcp', dport => '2023', action => 'redirect', nat_port => '20022' });
my %setup_services = map { $_->{id} => $_ } setup_services();
is($setup_services{ssh}->{port}, '2022, 2200, 2223',
'ssh service uses configured sshd ports');