From 0a92881371bda20697483bfc0502bb0d93793815 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 1 Aug 2026 16:55:32 +0200 Subject: [PATCH] Add incremental ban time options to the Fail2Ban module https://forum.virtualmin.com/t/fail2ban-bantime-increment-options-in-webmin/137675 --- fail2ban/edit_jail.cgi | 54 +++++++++++++++++++ fail2ban/edit_jaildef.cgi | 39 ++++++++++++++ fail2ban/fail2ban-lib.pl | 106 ++++++++++++++++++++++++++++++++++++++ fail2ban/lang/en | 15 +++++- fail2ban/save_jail.cgi | 15 ++++++ fail2ban/save_jaildef.cgi | 13 +++++ 6 files changed, 241 insertions(+), 1 deletion(-) diff --git a/fail2ban/edit_jail.cgi b/fail2ban/edit_jail.cgi index 90450b3df..0c759e48a 100755 --- a/fail2ban/edit_jail.cgi +++ b/fail2ban/edit_jail.cgi @@ -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("".$text{'jail_increment_header'}."
". + &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); diff --git a/fail2ban/edit_jaildef.cgi b/fail2ban/edit_jaildef.cgi index 573a72703..63be76258 100755 --- a/fail2ban/edit_jaildef.cgi +++ b/fail2ban/edit_jaildef.cgi @@ -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("".$text{'jail_increment_header'}."
". + &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); diff --git a/fail2ban/fail2ban-lib.pl b/fail2ban/fail2ban-lib.pl index d22c822e1..a69b23cae 100644 --- a/fail2ban/fail2ban-lib.pl +++ b/fail2ban/fail2ban-lib.pl @@ -671,6 +671,112 @@ my $out = &backquote_command("$config{'client_cmd'} -V 2>/dev/null = 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 { diff --git a/fail2ban/lang/en b/fail2ban/lang/en index 6c42c2142..d944154a1 100644 --- a/fail2ban/lang/en +++ b/fail2ban/lang/en @@ -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 - diff --git a/fail2ban/save_jail.cgi b/fail2ban/save_jail.cgi index b4d644305..4762ec618 100755 --- a/fail2ban/save_jail.cgi +++ b/fail2ban/save_jail.cgi @@ -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(); } diff --git a/fail2ban/save_jaildef.cgi b/fail2ban/save_jaildef.cgi index 9a1795127..c4c682cab 100755 --- a/fail2ban/save_jaildef.cgi +++ b/fail2ban/save_jaildef.cgi @@ -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");