]> git.wincent.com - bansshee.git/blob - bansshee
Switch to BSD license
[bansshee.git] / bansshee
1 #!/usr/bin/perl
2 # bansshee
3 #
4 # Copyright 2006-2009 Wincent Colaiuta. All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are met:
8 #
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.
14 #
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.
26
27 use strict;
28 use warnings;
29 use threads;
30 use threads::shared;
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)
33 use Proc::Daemon;
34 use File::Tail;
35
36 #
37 # Default settings: optional overrides may be placed in /etc/bansshee.conf
38 #
39
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)
46
47 # Platform specific settings, based on Red Hat Enterprise Linux ES release 3 (Taroon Update 7)
48
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
60
61 #
62 # Storage
63 #
64
65 my %last_attempt            = ();
66 my %illegal_user_attempts   = ();
67 my %incorrect_pass_attempts = ();
68 my %blocked_ips             = ();
69 my $iptables_configured     = 0;
70
71 #
72 # Functions
73 #
74
75 sub log_message($$;@) : locked
76 {
77   my $log_level     = shift;
78   my $format_string = shift;
79   my @arguments     = @_;
80   setlogsock('unix');
81   openlog('bansshee', 'pid', $log_facility);
82   if (@arguments)
83   {
84     syslog($log_level, $format_string, @arguments);
85   }
86   else
87   {
88     syslog($log_level, $format_string);
89   }
90   closelog();
91 }
92
93 sub prepare_iptables()
94 {
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;
100 }
101
102 sub cleanup_iptables()
103 {
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;
111 }
112
113 sub watch_blocklist_in_detached_thread()
114 {
115   while (1)
116   {
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)
119     {
120       lock %blocked_ips;
121       my $cutoff_time = time() - $unban_wait; # blocked ips added before the cutoff get unblocked
122       foreach my $ip (keys %blocked_ips)
123       {
124         if ($blocked_ips{$ip} < $cutoff_time)
125         {
126           push @unblock, $ip;
127         }
128       }
129       # unlock %blocked_ips;
130     }
131     foreach my $ip (@unblock)
132     {
133       unblock_ip($ip);
134     }
135     sleep $unblocking_interval; 
136   }
137   return;
138 }
139
140 sub block_ip($)
141 {
142   my $ip = shift;
143   lock %blocked_ips;
144   if (!defined($blocked_ips{$ip}))
145   {
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
149   }
150 }
151
152 sub unblock_ip($)
153 {
154   my $ip = shift;
155   lock %blocked_ips;
156   if (defined($blocked_ips{$ip}))
157   {
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};
161   }
162 }
163
164 sub update_blocked_ip_timestamp($)
165 {
166   my $ip = shift;
167   lock %blocked_ips;
168   if (defined($blocked_ips{$ip}))
169   {
170     $blocked_ips{$ip} = time();
171   }
172 }
173
174 sub illegal_user($$)
175 {
176   my $ip   = shift;
177   my $user = shift;
178   if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
179   {
180     $illegal_user_attempts{$ip} = 1;  # reset counter
181   }
182   else
183   {
184     $illegal_user_attempts{$ip} += 1;
185   }
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)
189   {
190     block_ip($ip);
191   }
192   elsif ($illegal_user_attempts{$ip} > $permitted_illegal_user)
193   {
194     update_blocked_ip_timestamp($ip);
195   }
196 }
197
198 sub incorrect_pass($$)
199 {
200   my $ip   = shift;
201   my $user = shift;
202   if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
203   {
204     $incorrect_pass_attempts{$ip} = 1;  # reset counter
205   }
206   else
207   { 
208     $incorrect_pass_attempts{$ip} += 1;
209   }
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)
213   {
214     block_ip($ip);
215   }
216   elsif ($incorrect_pass_attempts{$ip} > $permitted_incorrect_pass)
217   {
218     update_blocked_ip_timestamp($ip);
219   }
220 }
221
222 #
223 # Main
224 #
225
226 log_message('notice', 'Bansshee startup.');
227 if (`$id` != "0")
228 {
229   log_message('err', 'Requires root privileges [exiting].');
230   die;
231 }
232 if (-f $config_file && -T $config_file)
233 {
234   log_message('info', "Reading config file $config_file.");
235   do $config_file || log_message('err', "Error reading config file.");
236 }
237 log_message('notice', 'Daemonizing.');
238 Proc::Daemon::Init;
239 prepare_iptables();
240 share(%blocked_ips);
241 my $background_thread = async { watch_blocklist_in_detached_thread(); };
242
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))
246 {
247   if ($line =~ /$illegal_user_regex/)
248   {
249     illegal_user($2, $1);
250   }
251   elsif ($line =~ /$incorrect_pass_regex/)
252   {
253     incorrect_pass($2, $1);
254   }
255   else
256   { 
257     # no match: skip this line
258   }
259 }
260
261 log_message('notice', 'Banshee shutdown');
262
263 exit 0;
264
265 END
266 {
267   if ($iptables_configured != 0)
268   {
269     cleanup_iptables();
270   }
271 }
272