]> git.wincent.com - bansshee.git/blob - bansshee
Initial import
[bansshee.git] / bansshee
1 #!/usr/bin/perl
2 #
3 # bansshee.pl
4 # Bansshee
5 #
6 # Created by Wincent Colaiuta on Monday 10 April 2006.
7 # Copyright 2006 Wincent Colaiuta
8 # $Id$
9
10 use strict;
11 use warnings;
12 use threads;
13 use threads::shared;
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)
16 use Proc::Daemon;
17 use File::Tail;
18
19 #
20 # Default settings: optional overrides may be placed in /etc/bansshee.conf
21 #
22
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)
29
30 # Platform specific settings, based on Red Hat Enterprise Linux ES release 3 (Taroon Update 7)
31
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
43
44 #
45 # Storage
46 #
47
48 my %last_attempt            = ();
49 my %illegal_user_attempts   = ();
50 my %incorrect_pass_attempts = ();
51 my %blocked_ips             = ();
52 my $iptables_configured     = 0;
53
54 #
55 # Functions
56 #
57
58 sub log_message($$;@) : locked
59 {
60   my $log_level     = shift;
61   my $format_string = shift;
62   my @arguments     = @_;
63   setlogsock('unix');
64   openlog('bansshee', 'pid', $log_facility);
65   if (@arguments)
66   {
67     syslog($log_level, $format_string, @arguments);
68   }
69   else
70   {
71     syslog($log_level, $format_string);
72   }
73   closelog();
74 }
75
76 sub prepare_iptables()
77 {
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;
83 }
84
85 sub cleanup_iptables()
86 {
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;
94 }
95
96 sub watch_blocklist_in_detached_thread()
97 {
98   while (1)
99   {
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)
102     {
103       lock %blocked_ips;
104       my $cutoff_time = time() - $unban_wait; # blocked ips added before the cutoff get unblocked
105       foreach my $ip (keys %blocked_ips)
106       {
107         if ($blocked_ips{$ip} < $cutoff_time)
108         {
109           push @unblock, $ip;
110         }
111       }
112       # unlock %blocked_ips;
113     }
114     foreach my $ip (@unblock)
115     {
116       unblock_ip($ip);
117     }
118     sleep $unblocking_interval; 
119   }
120   return;
121 }
122
123 sub block_ip($)
124 {
125   my $ip = shift;
126   lock %blocked_ips;
127   if (!defined($blocked_ips{$ip}))
128   {
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
132   }
133 }
134
135 sub unblock_ip($)
136 {
137   my $ip = shift;
138   lock %blocked_ips;
139   if (defined($blocked_ips{$ip}))
140   {
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};
144   }
145 }
146
147 sub update_blocked_ip_timestamp($)
148 {
149   my $ip = shift;
150   lock %blocked_ips;
151   if (defined($blocked_ips{$ip}))
152   {
153     $blocked_ips{$ip} = time();
154   }
155 }
156
157 sub illegal_user($$)
158 {
159   my $ip   = shift;
160   my $user = shift;
161   if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
162   {
163     $illegal_user_attempts{$ip} = 1;  # reset counter
164   }
165   else
166   {
167     $illegal_user_attempts{$ip} += 1;
168   }
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)
172   {
173     block_ip($ip);
174   }
175   elsif ($illegal_user_attempts{$ip} > $permitted_illegal_user)
176   {
177     update_blocked_ip_timestamp($ip);
178   }
179 }
180
181 sub incorrect_pass($$)
182 {
183   my $ip   = shift;
184   my $user = shift;
185   if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
186   {
187     $incorrect_pass_attempts{$ip} = 1;  # reset counter
188   }
189   else
190   { 
191     $incorrect_pass_attempts{$ip} += 1;
192   }
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)
196   {
197     block_ip($ip);
198   }
199   elsif ($incorrect_pass_attempts{$ip} > $permitted_incorrect_pass)
200   {
201     update_blocked_ip_timestamp($ip);
202   }
203 }
204
205 #
206 # Main
207 #
208
209 log_message('notice', 'Bansshee startup.');
210 if (`$id` != "0")
211 {
212   log_message('err', 'Requires root privileges [exiting].');
213   die;
214 }
215 if (-f $config_file && -T $config_file)
216 {
217   log_message('info', "Reading config file $config_file.");
218   do $config_file || log_message('err', "Error reading config file.");
219 }
220 log_message('notice', 'Daemonizing.');
221 Proc::Daemon::Init;
222 prepare_iptables();
223 share(%blocked_ips);
224 my $background_thread = async { watch_blocklist_in_detached_thread(); };
225
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))
229 {
230   if ($line =~ /$illegal_user_regex/)
231   {
232     illegal_user($2, $1);
233   }
234   elsif ($line =~ /$incorrect_pass_regex/)
235   {
236     incorrect_pass($2, $1);
237   }
238   else
239   { 
240     # no match: skip this line
241   }
242 }
243
244 log_message('notice', 'Banshee shutdown');
245
246 exit 0;
247
248 END
249 {
250   if ($iptables_configured != 0)
251   {
252     cleanup_iptables();
253   }
254 }
255