4 # Copyright 2006-2009 Wincent Colaiuta. All rights reserved.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are met:
9 # 1. Redistributions of source code must retain the above copyright notice,
10 # this list of conditions and the following disclaimer.
11 # 2. Redistributions in binary form must reproduce the above copyright notice,
12 # this list of conditions and the following disclaimer in the documentation
13 # and/or other materials provided with the distribution.
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
19 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 # POSSIBILITY OF SUCH DAMAGE.
31 use Sys::Syslog qw(:DEFAULT setlogsock);
32 use sigtrap qw(die untrapped normal-signals); # ensure that END block gets run (to clean up iptables)
37 # Default settings: optional overrides may be placed in /etc/bansshee.conf
40 my $config_file = "/etc/bansshee.conf";
41 our $permitted_illegal_user = 5; # number of invalid user attempts permitted from a single IP address before it gets blocked
42 our $permitted_incorrect_pass = 5; # number of incorrect pass attempts permitted from a single IP address before it gets blocked
43 our $unban_wait = 3600; # minimum number of seconds an IP must wait before it gets removed from the blocklist (1 hour)
44 our $grace_period = 3600; # number of seconds that must pass before prior invalid/incorrect attempt counts are reset (1 hour)
45 our $unblocking_interval = 300; # number of seconds between checks of the blocklist for removing old IPs (5 minutes)
47 # Platform specific settings, based on Red Hat Enterprise Linux ES release 3 (Taroon Update 7)
49 our $logpath = '/var/log/secure'; # logfile to watch
50 our $illegal_user_regex = 'sshd\[\d+\]: Failed password for illegal user (\S+) from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port \d+ ssh';
51 our $incorrect_pass_regex = 'sshd\[\d+\]: Failed password for (\S+) from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port \d+ ssh';
52 our $iptables = '/sbin/iptables'; # for manipulating the firewall
53 our $iptables_create = '-N BANSSHEE'; # iptables parameters for creating the BANSSHEE chain
54 our $iptables_add = '-A INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for adding the JUMP rule
55 our $iptables_remove = '-D INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for removing the JUMP rule
56 our $iptables_flush = '-F BANSSHEE'; # iptables parameters for flushing the BANSSHEE chain
57 our $iptables_delete = '-X BANSSHEE'; # iptables parameters for deleting the BANSSHEE chain
58 our $id = '/usr/bin/id -u'; # for determing if running as root
59 our $log_facility :shared = 'authpriv'; # Bansshee messages logged to /var/log/secure
65 my %last_attempt = ();
66 my %illegal_user_attempts = ();
67 my %incorrect_pass_attempts = ();
69 my $iptables_configured = 0;
75 sub log_message($$;@) : locked
77 my $log_level = shift;
78 my $format_string = shift;
81 openlog('bansshee', 'pid', $log_facility);
84 syslog($log_level, $format_string, @arguments);
88 syslog($log_level, $format_string);
93 sub prepare_iptables()
95 log_message('notice', 'Creating new BANSSHEE iptables chain.');
96 system("$iptables $iptables_create") == 0 or die;
97 log_message('info', 'Adding JUMP rule (redirects all SSH traffic to BANSSHEE chain).');
98 system("$iptables $iptables_add") == 0 or die;
99 $iptables_configured = 1;
102 sub cleanup_iptables()
104 log_message('info', 'Removing JUMP rule from INPUT chain.');
105 system("$iptables $iptables_remove") == 0 or die;
106 log_message('info', 'Flushing BANSSHEE iptables chain.');
107 system("$iptables $iptables_flush") == 0 or die;
108 log_message('notice', 'Deleting BANSSHEE iptables chain.');
109 system("$iptables $iptables_delete") == 0 or die;
110 $iptables_configured = 0;
113 sub watch_blocklist_in_detached_thread()
117 log_message('info', 'Performing periodic check of blocked IPs list.');
118 my @unblock; # build a list of IPs to unblock (so as to avoid altering the hash while enumerating over it)
121 my $cutoff_time = time() - $unban_wait; # blocked ips added before the cutoff get unblocked
122 foreach my $ip (keys %blocked_ips)
124 if ($blocked_ips{$ip} < $cutoff_time)
129 # unlock %blocked_ips;
131 foreach my $ip (@unblock)
135 sleep $unblocking_interval;
144 if (!defined($blocked_ips{$ip}))
146 log_message('warning', "Adding IP $ip to blocklist.");
147 system("$iptables -I BANSSHEE -s $ip -j DROP") == 0 or die;
148 $blocked_ips{$ip} = time(); # record time that IP was added to the blocklist
156 if (defined($blocked_ips{$ip}))
158 log_message('notice', "Removing IP $ip from blocklist.");
159 system("$iptables -D BANSSHEE -s $ip -j DROP") == 0 or die;
160 undef $blocked_ips{$ip};
164 sub update_blocked_ip_timestamp($)
168 if (defined($blocked_ips{$ip}))
170 $blocked_ips{$ip} = time();
178 if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
180 $illegal_user_attempts{$ip} = 1; # reset counter
184 $illegal_user_attempts{$ip} += 1;
186 $last_attempt{$ip} = time();
187 log_message('warning', "Attempted connection with illegal user ($user) from IP $ip ($illegal_user_attempts{$ip} attempt(s) so far).");
188 if ($illegal_user_attempts{$ip} == $permitted_illegal_user)
192 elsif ($illegal_user_attempts{$ip} > $permitted_illegal_user)
194 update_blocked_ip_timestamp($ip);
198 sub incorrect_pass($$)
202 if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
204 $incorrect_pass_attempts{$ip} = 1; # reset counter
208 $incorrect_pass_attempts{$ip} += 1;
210 $last_attempt{$ip} = time();
211 log_message('warning', "Failed password attempt for user ($user) from IP $ip ($incorrect_pass_attempts{$ip} attempt(s) so far)");
212 if ($incorrect_pass_attempts{$ip} == $permitted_incorrect_pass)
216 elsif ($incorrect_pass_attempts{$ip} > $permitted_incorrect_pass)
218 update_blocked_ip_timestamp($ip);
226 log_message('notice', 'Bansshee startup.');
229 log_message('err', 'Requires root privileges [exiting].');
232 if (-f $config_file && -T $config_file)
234 log_message('info', "Reading config file $config_file.");
235 do $config_file || log_message('err', "Error reading config file.");
237 log_message('notice', 'Daemonizing.');
241 my $background_thread = async { watch_blocklist_in_detached_thread(); };
243 log_message('notice', "Tailing log: $logpath.");
244 my $file = File::Tail->new(name=>$logpath, ignore_nonexistant=>1);
245 while (defined(my $line=$file->read))
247 if ($line =~ /$illegal_user_regex/)
249 illegal_user($2, $1);
251 elsif ($line =~ /$incorrect_pass_regex/)
253 incorrect_pass($2, $1);
257 # no match: skip this line
261 log_message('notice', 'Banshee shutdown');
267 if ($iptables_configured != 0)