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