]> git.wincent.com - bansshee.git/blob - bansshee
Rewrap README to fit within 80 columns
[bansshee.git] / bansshee
1 #!/usr/bin/perl
2 # bansshee
3 #
4 # Copyright 2006-2009 Wincent Colaiuta. All rights reserved.
5
6 use strict;
7 use warnings;
8 use threads;
9 use threads::shared;
10 use Sys::Syslog qw(:DEFAULT setlogsock);
11 use sigtrap qw(die untrapped normal-signals); # ensure that END block gets run (to clean up iptables)
12 use Proc::Daemon;
13 use File::Tail;
14
15 #
16 # Default settings: optional overrides may be placed in /etc/bansshee.conf
17 #
18
19 my $config_file               = "/etc/bansshee.conf";
20 our $permitted_illegal_user   = 5;      # number of invalid user attempts permitted from a single IP address before it gets blocked
21 our $permitted_incorrect_pass = 5;      # number of incorrect pass attempts permitted from a single IP address before it gets blocked
22 our $unban_wait               = 3600;   # minimum number of seconds an IP must wait before it gets removed from the blocklist (1 hour)
23 our $grace_period             = 3600;   # number of seconds that must pass before prior invalid/incorrect attempt counts are reset (1 hour)
24 our $unblocking_interval      = 300;    # number of seconds between checks of the blocklist for removing old IPs (5 minutes)
25
26 # Platform specific settings, based on Red Hat Enterprise Linux ES release 3 (Taroon Update 7)
27
28 our $logpath                  = '/var/log/secure';                         # logfile to watch
29 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';
30 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';
31 our $iptables                 = '/sbin/iptables';                          # for manipulating the firewall
32 our $iptables_create          = '-N BANSSHEE';                             # iptables parameters for creating the BANSSHEE chain
33 our $iptables_add             = '-A INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for adding the JUMP rule
34 our $iptables_remove          = '-D INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for removing the JUMP rule
35 our $iptables_flush           = '-F BANSSHEE';                             # iptables parameters for flushing the BANSSHEE chain
36 our $iptables_delete          = '-X BANSSHEE';                             # iptables parameters for deleting the BANSSHEE chain
37 our $id                       = '/usr/bin/id -u';                          # for determing if running as root
38 our $log_facility :shared     = 'authpriv';                                # Bansshee messages logged to /var/log/secure
39
40 #
41 # Storage
42 #
43
44 my %last_attempt            = ();
45 my %illegal_user_attempts   = ();
46 my %incorrect_pass_attempts = ();
47 my %blocked_ips             = ();
48 my $iptables_configured     = 0;
49
50 #
51 # Functions
52 #
53
54 sub log_message($$;@) : locked
55 {
56   my $log_level     = shift;
57   my $format_string = shift;
58   my @arguments     = @_;
59   setlogsock('unix');
60   openlog('bansshee', 'pid', $log_facility);
61   if (@arguments)
62   {
63     syslog($log_level, $format_string, @arguments);
64   }
65   else
66   {
67     syslog($log_level, $format_string);
68   }
69   closelog();
70 }
71
72 sub prepare_iptables()
73 {
74   log_message('notice', 'Creating new BANSSHEE iptables chain.');
75   system("$iptables $iptables_create") == 0 or die;
76   log_message('info', 'Adding JUMP rule (redirects all SSH traffic to BANSSHEE chain).');
77   system("$iptables $iptables_add") == 0 or die;
78   $iptables_configured = 1;
79 }
80
81 sub cleanup_iptables()
82 {
83   log_message('info', 'Removing JUMP rule from INPUT chain.');
84   system("$iptables $iptables_remove") == 0 or die;
85   log_message('info', 'Flushing BANSSHEE iptables chain.');
86   system("$iptables $iptables_flush") == 0 or die;
87   log_message('notice', 'Deleting BANSSHEE iptables chain.');
88   system("$iptables $iptables_delete") == 0 or die;
89   $iptables_configured = 0;
90 }
91
92 sub watch_blocklist_in_detached_thread()
93 {
94   while (1)
95   {
96     log_message('info', 'Performing periodic check of blocked IPs list.');
97     my @unblock; # build a list of IPs to unblock (so as to avoid altering the hash while enumerating over it)
98     {
99       lock %blocked_ips;
100       my $cutoff_time = time() - $unban_wait; # blocked ips added before the cutoff get unblocked
101       foreach my $ip (keys %blocked_ips)
102       {
103         if ($blocked_ips{$ip} < $cutoff_time)
104         {
105           push @unblock, $ip;
106         }
107       }
108       # unlock %blocked_ips;
109     }
110     foreach my $ip (@unblock)
111     {
112       unblock_ip($ip);
113     }
114     sleep $unblocking_interval; 
115   }
116   return;
117 }
118
119 sub block_ip($)
120 {
121   my $ip = shift;
122   lock %blocked_ips;
123   if (!defined($blocked_ips{$ip}))
124   {
125     log_message('warning', "Adding IP $ip to blocklist.");
126     system("$iptables -I BANSSHEE -s $ip -j DROP") == 0 or die;
127     $blocked_ips{$ip} = time();    # record time that IP was added to the blocklist
128   }
129 }
130
131 sub unblock_ip($)
132 {
133   my $ip = shift;
134   lock %blocked_ips;
135   if (defined($blocked_ips{$ip}))
136   {
137     log_message('notice', "Removing IP $ip from blocklist.");
138     system("$iptables -D BANSSHEE -s $ip -j DROP") == 0 or die;
139     undef $blocked_ips{$ip};
140   }
141 }
142
143 sub update_blocked_ip_timestamp($)
144 {
145   my $ip = shift;
146   lock %blocked_ips;
147   if (defined($blocked_ips{$ip}))
148   {
149     $blocked_ips{$ip} = time();
150   }
151 }
152
153 sub illegal_user($$)
154 {
155   my $ip   = shift;
156   my $user = shift;
157   if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
158   {
159     $illegal_user_attempts{$ip} = 1;  # reset counter
160   }
161   else
162   {
163     $illegal_user_attempts{$ip} += 1;
164   }
165   $last_attempt{$ip} = time();
166   log_message('warning', "Attempted connection with illegal user ($user) from IP $ip ($illegal_user_attempts{$ip} attempt(s) so far).");
167   if ($illegal_user_attempts{$ip} == $permitted_illegal_user)
168   {
169     block_ip($ip);
170   }
171   elsif ($illegal_user_attempts{$ip} > $permitted_illegal_user)
172   {
173     update_blocked_ip_timestamp($ip);
174   }
175 }
176
177 sub incorrect_pass($$)
178 {
179   my $ip   = shift;
180   my $user = shift;
181   if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time()))
182   {
183     $incorrect_pass_attempts{$ip} = 1;  # reset counter
184   }
185   else
186   { 
187     $incorrect_pass_attempts{$ip} += 1;
188   }
189   $last_attempt{$ip} = time();
190   log_message('warning', "Failed password attempt for user ($user) from IP $ip ($incorrect_pass_attempts{$ip} attempt(s) so far)");
191   if ($incorrect_pass_attempts{$ip} == $permitted_incorrect_pass)
192   {
193     block_ip($ip);
194   }
195   elsif ($incorrect_pass_attempts{$ip} > $permitted_incorrect_pass)
196   {
197     update_blocked_ip_timestamp($ip);
198   }
199 }
200
201 #
202 # Main
203 #
204
205 log_message('notice', 'Bansshee startup.');
206 if (`$id` != "0")
207 {
208   log_message('err', 'Requires root privileges [exiting].');
209   die;
210 }
211 if (-f $config_file && -T $config_file)
212 {
213   log_message('info', "Reading config file $config_file.");
214   do $config_file || log_message('err', "Error reading config file.");
215 }
216 log_message('notice', 'Daemonizing.');
217 Proc::Daemon::Init;
218 prepare_iptables();
219 share(%blocked_ips);
220 my $background_thread = async { watch_blocklist_in_detached_thread(); };
221
222 log_message('notice', "Tailing log: $logpath.");
223 my $file = File::Tail->new(name=>$logpath, ignore_nonexistant=>1);
224 while (defined(my $line=$file->read))
225 {
226   if ($line =~ /$illegal_user_regex/)
227   {
228     illegal_user($2, $1);
229   }
230   elsif ($line =~ /$incorrect_pass_regex/)
231   {
232     incorrect_pass($2, $1);
233   }
234   else
235   { 
236     # no match: skip this line
237   }
238 }
239
240 log_message('notice', 'Banshee shutdown');
241
242 exit 0;
243
244 END
245 {
246   if ($iptables_configured != 0)
247   {
248     cleanup_iptables();
249   }
250 }
251