6 # Created by Wincent Colaiuta on Monday 10 April 2006.
7 # Copyright 2006 Wincent Colaiuta
14 use Sys::Syslog qw(:DEFAULT setlogsock);
15 use sigtrap qw(die untrapped normal-signals); # ensure that END block gets run (to clean up iptables)
20 # Default settings: optional overrides may be placed in /etc/bansshee.conf
23 my $config_file = "/etc/bansshee.conf";
24 our $permitted_illegal_user = 5; # number of illegal user attempts permitted from a single IP address before it gets blocked
25 our $permitted_incorrect_pass = 5; # number of incorrect pass attempts permitted from a single IP address before it gets blocked
26 our $unban_wait = 3600; # minimum number of seconds an IP must wait before it gets removed from the blocklist (1 hour)
27 our $grace_period = 3600; # number of seconds that must pass before prior illegal/incorrect attempt counts are reset (1 hour)
28 our $unblocking_interval = 300; # number of seconds between checks of the blocklist for removing old IPs (5 minutes)
30 # Platform specific settings, based on Red Hat Enterprise Linux ES release 3 (Taroon Update 7)
32 our $logpath = '/var/log/secure'; # logfile to watch
33 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';
34 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';
35 our $iptables = '/sbin/iptables'; # for manipulating the firewall
36 our $iptables_create = '-N BANSSHEE'; # iptables parameters for creating the BANSSHEE chain
37 our $iptables_add = '-A INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for adding the JUMP rule
38 our $iptables_remove = '-D INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for removing the JUMP rule
39 our $iptables_flush = '-F BANSSHEE'; # iptables parameters for flushing the BANSSHEE chain
40 our $iptables_delete = '-X BANSSHEE'; # iptables parameters for deleting the BANSSHEE chain
41 our $id = '/usr/bin/id -u'; # for determing if running as root
42 our $log_facility :shared = 'authpriv'; # Bansshee messages logged to /var/log/secure
48 my %last_attempt = ();
49 my %illegal_user_attempts = ();
50 my %incorrect_pass_attempts = ();
52 my $iptables_configured = 0;
58 sub log_message($$;@) : locked
60 my $log_level = shift;
61 my $format_string = shift;
64 openlog('bansshee', 'pid', $log_facility);
67 syslog($log_level, $format_string, @arguments);
71 syslog($log_level, $format_string);
76 sub prepare_iptables()
78 log_message('notice', 'Creating new BANSSHEE iptables chain.');
79 system("$iptables $iptables_create") == 0 or die;
80 log_message('info', 'Adding JUMP rule (redirects all SSH traffic to BANSSHEE chain).');
81 system("$iptables $iptables_add") == 0 or die;
82 $iptables_configured = 1;
85 sub cleanup_iptables()
87 log_message('info', 'Removing JUMP rule from INPUT chain.');
88 system("$iptables $iptables_remove") == 0 or die;
89 log_message('info', 'Flushing BANSSHEE iptables chain.');
90 system("$iptables $iptables_flush") == 0 or die;
91 log_message('notice', 'Deleting BANSSHEE iptables chain.');
92 system("$iptables $iptables_delete") == 0 or die;
93 $iptables_configured = 0;
96 sub watch_blocklist_in_detached_thread()
100 log_message('info', 'Performing periodic check of blocked IPs list.');
101 my @unblock; # build a list of IPs to unblock (so as to avoid altering the hash while enumerating over it)
104 my $cutoff_time = time() - $unban_wait; # blocked ips added before the cutoff get unblocked
105 foreach my $ip (keys %blocked_ips)
107 if ($blocked_ips{$ip} < $cutoff_time)
112 # unlock %blocked_ips;
114 foreach my $ip (@unblock)
118 sleep $unblocking_interval;
127 if (!defined($blocked_ips{$ip}))
129 log_message('warning', "Adding IP $ip to blocklist.");
130 system("$iptables -I BANSSHEE -s $ip -j DROP") == 0 or die;
131 $blocked_ips{$ip} = time(); # record time that IP was added to the blocklist
139 if (defined($blocked_ips{$ip}))
141 log_message('notice', "Removing IP $ip from blocklist.");
142 system("$iptables -D BANSSHEE -s $ip -j DROP") == 0 or die;
143 undef $blocked_ips{$ip};
147 sub update_blocked_ip_timestamp($)
151 if (defined($blocked_ips{$ip}))
153 $blocked_ips{$ip} = time();
161 if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
163 $illegal_user_attempts{$ip} = 1; # reset counter
167 $illegal_user_attempts{$ip} += 1;
169 $last_attempt{$ip} = time();
170 log_message('warning', "Attempted connection with illegal user ($user) from IP $ip ($illegal_user_attempts{$ip} attempt(s) so far).");
171 if ($illegal_user_attempts{$ip} == $permitted_illegal_user)
175 elsif ($illegal_user_attempts{$ip} > $permitted_illegal_user)
177 update_blocked_ip_timestamp($ip);
181 sub incorrect_pass($$)
185 if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
187 $incorrect_pass_attempts{$ip} = 1; # reset counter
191 $incorrect_pass_attempts{$ip} += 1;
193 $last_attempt{$ip} = time();
194 log_message('warning', "Failed password attempt for user ($user) from IP $ip ($incorrect_pass_attempts{$ip} attempt(s) so far)");
195 if ($incorrect_pass_attempts{$ip} == $permitted_incorrect_pass)
199 elsif ($incorrect_pass_attempts{$ip} > $permitted_incorrect_pass)
201 update_blocked_ip_timestamp($ip);
209 log_message('notice', 'Bansshee startup.');
212 log_message('err', 'Requires root privileges [exiting].');
215 if (-f $config_file && -T $config_file)
217 log_message('info', "Reading config file $config_file.");
218 do $config_file || log_message('err', "Error reading config file.");
220 log_message('notice', 'Daemonizing.');
224 my $background_thread = async { watch_blocklist_in_detached_thread(); };
226 log_message('notice', "Tailing log: $logpath.");
227 my $file = File::Tail->new(name=>$logpath, ignore_nonexistant=>1);
228 while (defined(my $line=$file->read))
230 if ($line =~ /$illegal_user_regex/)
232 illegal_user($2, $1);
234 elsif ($line =~ /$incorrect_pass_regex/)
236 incorrect_pass($2, $1);
240 # no match: skip this line
244 log_message('notice', 'Banshee shutdown');
250 if ($iptables_configured != 0)