Add incremental ban time options to the Fail2Ban module

https://forum.virtualmin.com/t/fail2ban-bantime-increment-options-in-webmin/137675
This commit is contained in:
Ilia Ross
2026-08-01 16:55:32 +02:00
parent 4c3be62029
commit 0a92881371
6 changed files with 241 additions and 1 deletions

View File

@@ -134,6 +134,60 @@ print &ui_table_row($text{'jail_bantime'},
&ui_opt_textbox("bantime", $bantime, 6,
$text{'default'}." (".$def_bantime.")"));
# Incremental banning options, supported since Fail2Ban 0.11.1
if (&supports_bantime_increment()) {
print &ui_table_hr();
print &ui_table_span("<b>".$text{'jail_increment_header'}."</b><br>".
&ui_note($text{'jail_increment_desc'}, 0));
my $def_increment = &canonical_fail2ban_boolean(
&find_value("bantime.increment", $def));
my $increment = &canonical_fail2ban_boolean(
&find_value("bantime.increment", $jail));
my $def_increment_text = $def_increment eq "true" ? $text{'yes'} :
$text{'no'};
print &ui_table_row($text{'jail_bantime_increment'},
&ui_select("bantime_increment", $increment,
[ [ "", $text{'default'}." (".$def_increment_text.")" ],
[ "true", $text{'yes'} ],
[ "false", $text{'no'} ] ], 1, 0, 1));
my $def_factor = &find_value("bantime.factor", $def) || 1;
my $factor = &find_value("bantime.factor", $jail);
print &ui_table_row($text{'jail_bantime_factor'},
&ui_opt_textbox("bantime_factor", $factor, 8,
$text{'default'}." (".&html_escape($def_factor).")"));
my $def_maxtime = &find_value("bantime.maxtime", $def);
my $maxtime = &find_value("bantime.maxtime", $jail);
my $def_maxtime_text = $def_maxtime ne "" ?
&html_escape($def_maxtime) : $text{'jail_nolimit'};
print &ui_table_row($text{'jail_bantime_maxtime'},
&ui_opt_textbox("bantime_maxtime", $maxtime, 8,
$text{'default'}." (".$def_maxtime_text.")"));
my $def_overalljails = &canonical_fail2ban_boolean(
&find_value("bantime.overalljails", $def));
my $overalljails = &canonical_fail2ban_boolean(
&find_value("bantime.overalljails", $jail));
my $def_overalljails_text = $def_overalljails eq "true" ?
$text{'yes'} : $text{'no'};
print &ui_table_row($text{'jail_bantime_overalljails'},
&ui_select("bantime_overalljails", $overalljails,
[ [ "", $text{'default'}." (".$def_overalljails_text.")" ],
[ "true", $text{'yes'} ],
[ "false", $text{'no'} ] ], 1, 0, 1));
my $def_rndtime = &find_value("bantime.rndtime", $def);
my $rndtime = &find_value("bantime.rndtime", $jail);
my $def_rndtime_text = $def_rndtime ne "" ?
&html_escape($def_rndtime) : $text{'jail_none'};
print &ui_table_row($text{'jail_bantime_rndtime'},
&ui_opt_textbox("bantime_rndtime", $rndtime, 8,
$text{'default'}." (".$def_rndtime_text.")"));
print &ui_table_hr();
}
# IPs to ignore
my $def_ignoreip = &find_value("ignoreip", $def) || "127.0.0.1";
my $ignoreip = &find_value("ignoreip", $jail);

View File

@@ -40,6 +40,45 @@ $bantime = $def_bantime if (!defined($bantime) || $bantime eq "");
print &ui_table_row($text{'jail_bantime'},
&ui_textbox("bantime", $bantime, 5));
# Incremental banning options, supported since Fail2Ban 0.11.1
if (&supports_bantime_increment()) {
print &ui_table_hr();
print &ui_table_span("<b>".$text{'jail_increment_header'}."</b><br>".
&ui_note($text{'jail_increment_desc'}, 0));
my $increment = &canonical_fail2ban_boolean(
&find_value("bantime.increment", $jail));
print &ui_table_row($text{'jail_bantime_increment'},
&ui_select("bantime_increment", $increment,
[ [ "", $text{'default'}." (".$text{'no'}.")" ],
[ "true", $text{'yes'} ],
[ "false", $text{'no'} ] ], 1, 0, 1));
my $factor = &find_value("bantime.factor", $jail);
print &ui_table_row($text{'jail_bantime_factor'},
&ui_opt_textbox("bantime_factor", $factor, 8,
$text{'default'}." (1)"));
my $maxtime = &find_value("bantime.maxtime", $jail);
print &ui_table_row($text{'jail_bantime_maxtime'},
&ui_opt_textbox("bantime_maxtime", $maxtime, 8,
$text{'default'}." (".$text{'jail_nolimit'}.")"));
my $overalljails = &canonical_fail2ban_boolean(
&find_value("bantime.overalljails", $jail));
print &ui_table_row($text{'jail_bantime_overalljails'},
&ui_select("bantime_overalljails", $overalljails,
[ [ "", $text{'default'}." (".$text{'no'}.")" ],
[ "true", $text{'yes'} ],
[ "false", $text{'no'} ] ], 1, 0, 1));
my $rndtime = &find_value("bantime.rndtime", $jail);
print &ui_table_row($text{'jail_bantime_rndtime'},
&ui_opt_textbox("bantime_rndtime", $rndtime, 8,
$text{'default'}." (".$text{'jail_none'}.")"));
print &ui_table_hr();
}
# IPs to ignore
my $def_ignoreip = "127.0.0.1";
my $ignoreip = &find_value("ignoreip", $jail);

View File

@@ -671,6 +671,112 @@ my $out = &backquote_command("$config{'client_cmd'} -V 2>/dev/null </dev/null");
return !$? && $out =~ /v?([0-9\.]+)/ ? $1 : undef;
}
# supports_bantime_increment([version])
# Returns 1 if the installed Fail2Ban version supports incremental ban times
sub supports_bantime_increment
{
my ($version) = @_;
$version = &get_fail2ban_version() if (!defined($version));
return 0 if (!defined($version) || $version eq "");
return &compare_version_numbers($version, "0.11.1") >= 0;
}
# canonical_fail2ban_boolean(value)
# Converts the boolean spellings accepted by Fail2Ban to true or false
sub canonical_fail2ban_boolean
{
my ($value) = @_;
return "" if (!defined($value) || $value eq "");
return "true" if ($value =~ /^(1|on|true|yes)$/i);
return "false" if ($value =~ /^(0|off|false|no)$/i);
return $value;
}
# valid_fail2ban_duration(value, [allow-zero])
# Returns 1 for a safe subset of Fail2Ban duration expressions
sub valid_fail2ban_duration
{
my ($value, $allow_zero) = @_;
return 0 if (!defined($value) || $value eq "");
my $number = qr/\d+(?:\.\d+)?/;
my $unit = qr/(?:s|sec(?:ond)?s?|m|min(?:ute)?s?|h|hour(?:s)?|d|day(?:s)?|w|week(?:s)?|mo|mon|month(?:s)?|y|year(?:s)?)/i;
return 0 if ($value !~ /^\s*(?:$number\s*$unit\s*)+$/ &&
$value !~ /^\s*$number\s*$/);
return 1 if ($allow_zero);
my @numbers = $value =~ /(\d+(?:\.\d+)?)/g;
return scalar(grep { $_ > 0 } @numbers) ? 1 : 0;
}
# valid_positive_fail2ban_duration(value)
# Returns 1 for a safe, positive Fail2Ban duration expression
sub valid_positive_fail2ban_duration
{
return &valid_fail2ban_duration($_[0], 0);
}
# valid_nonnegative_fail2ban_duration(value)
# Returns 1 for a safe Fail2Ban duration expression, including zero
sub valid_nonnegative_fail2ban_duration
{
return &valid_fail2ban_duration($_[0], 1);
}
# valid_bantime_factor(value)
# Returns 1 for a positive numeric incremental-ban growth factor
sub valid_bantime_factor
{
my ($value) = @_;
return defined($value) &&
$value =~ /^(?:\d+(?:\.\d+)?|\.\d+)$/ && $value > 0;
}
# validate_bantime_increment_inputs(&input)
# Returns a language key for the first invalid incremental-ban option
sub validate_bantime_increment_inputs
{
my ($input) = @_;
foreach my $f ("bantime_increment", "bantime_overalljails") {
my $value = $input->{$f};
return "jail_e".$f if (defined($value) && $value ne "" &&
$value !~ /^(true|false)$/);
}
if (!$input->{'bantime_factor_def'} &&
!&valid_bantime_factor($input->{'bantime_factor'})) {
return "jail_ebantime_factor";
}
if (!$input->{'bantime_maxtime_def'} &&
!&valid_positive_fail2ban_duration($input->{'bantime_maxtime'})) {
return "jail_ebantime_maxtime";
}
if (!$input->{'bantime_rndtime_def'} &&
!&valid_nonnegative_fail2ban_duration($input->{'bantime_rndtime'})) {
return "jail_ebantime_rndtime";
}
return undef;
}
# save_bantime_increment_options(&input, &jail)
# Saves incremental-ban options using the standard jail directive handling
sub save_bantime_increment_options
{
my ($input, $jail) = @_;
&save_directive("bantime.increment",
$input->{'bantime_increment'} eq "" ? undef :
$input->{'bantime_increment'}, $jail);
&save_directive("bantime.factor",
$input->{'bantime_factor_def'} ? undef :
$input->{'bantime_factor'}, $jail);
&save_directive("bantime.maxtime",
$input->{'bantime_maxtime_def'} ? undef :
$input->{'bantime_maxtime'}, $jail);
&save_directive("bantime.overalljails",
$input->{'bantime_overalljails'} eq "" ? undef :
$input->{'bantime_overalljails'}, $jail);
&save_directive("bantime.rndtime",
$input->{'bantime_rndtime_def'} ? undef :
$input->{'bantime_rndtime'}, $jail);
}
# Unblock given IP in given jail
sub unblock_jailed_ip
{

View File

@@ -101,6 +101,14 @@ jail_others=Other parameters
jail_maxretry=Matches before applying action
jail_findtime=Max delay between matches
jail_bantime=Time to ban IP for
jail_increment_header=Incremental banning
jail_increment_desc=Increase ban times for IP addresses that are banned repeatedly. Ban history comes from Fail2Ban's persistent database and is limited by the database purge age in Global Configuration.
jail_bantime_increment=Increase repeat-offender ban times?
jail_bantime_factor=Ban time growth factor
jail_bantime_maxtime=Maximum increased ban time
jail_bantime_overalljails=Use ban history from all jails?
jail_bantime_rndtime=Maximum random additional ban time
jail_nolimit=No limit
jail_ignoreip=IP addresses to never ban
jail_err=Failed to save jail
jail_eclash=A jail with the same name already exists
@@ -115,6 +123,12 @@ jail_elogpath=All log files must be absolute paths or patterns
jail_emaxretry=Matches before applying action must be a number greater than zero
jail_efindtime=Delay between matches must be a number greater than zero
jail_ebantime=Time to ban an IP must be a number greater than zero
jail_ebantime_increment=Invalid incremental banning selection
jail_ebantime_factor=Ban time growth factor must be a number greater than zero
jail_ebantime_maxtime=Maximum increased ban time must be a positive duration, such as 30m, 5h, 7d or 5w
jail_ebantime_overalljails=Invalid all-jails ban history selection
jail_ebantime_rndtime=Random additional ban time must be zero or a positive duration, such as 30m, 5h, 7d or 5w
jail_eincrement_version=Incremental banning requires Fail2Ban version 0.11.1 or later
jail_backend=Check for log file updates using
jail_auto=Decide automatically
jail_systemd=systemd
@@ -235,4 +249,3 @@ status_err_unknownjail=Unknown jail
syslog_logtarget=Fail2Ban action log
__norefs=1

View File

@@ -122,6 +122,19 @@ else {
&error($text{'jail_eignoreip'});
}
# Validate incremental banning options when submitted by a supported UI
my @increment_fields = ("bantime_increment", "bantime_factor",
"bantime_maxtime", "bantime_overalljails", "bantime_rndtime");
my $increment_submitted = scalar(grep { exists($in{$_}) }
@increment_fields);
if ($increment_submitted && !&supports_bantime_increment()) {
&error($text{'jail_eincrement_version'});
}
if ($increment_submitted) {
my $err = &validate_bantime_increment_inputs(\%in);
&error($text{$err}) if ($err);
}
# Create new section or rename existing if needed
&lock_all_config_files();
if ($in{'new'}) {
@@ -144,6 +157,8 @@ else {
}
&save_directive("ignoreip",
@ignoreips ? join(" ", @ignoreips) : undef, $jail);
&save_bantime_increment_options(\%in, $jail)
if ($increment_submitted);
&unlock_all_config_files();
}

View File

@@ -30,6 +30,18 @@ foreach my $ip (@ignoreips) {
&error($text{'jail_eignoreip'});
}
# Validate incremental banning options when submitted by a supported UI
my @increment_fields = ("bantime_increment", "bantime_factor",
"bantime_maxtime", "bantime_overalljails", "bantime_rndtime");
my $increment_submitted = scalar(grep { exists($in{$_}) } @increment_fields);
if ($increment_submitted && !&supports_bantime_increment()) {
&error($text{'jail_eincrement_version'});
}
if ($increment_submitted) {
my $err = &validate_bantime_increment_inputs(\%in);
&error($text{$err}) if ($err);
}
# Update the jail
&lock_all_config_files();
@@ -47,6 +59,7 @@ foreach my $f ("maxretry", "findtime", "bantime") {
$jail);
&save_directive("banaction", $in{'banaction'} || undef, $jail);
&save_directive("protocol", $in{'protocol'} || undef, $jail);
&save_bantime_increment_options(\%in, $jail) if ($increment_submitted);
&unlock_all_config_files();
&webmin_log("jaildef");