"Fossies" - the Fresh Open Source Software Archive 
Member "sshdfilter-1.5.7/source/sshdfilter.pl" (31 May 2010, 47757 Bytes) of package /linux/privat/old/sshdfilter-1.5.7.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Perl source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "sshdfilter.pl" see the
Fossies "Dox" file reference documentation.
1 #!/usr/bin/perl
2
3 use strict 'vars';
4
5
6 # sshdfilter
7 # greg at csc.liv.ac.uk
8 # http://www.csc.liv.ac.uk/~greg/sshdfilter/
9 # http://www.sshdfilter.org/
10 my $version="1.5.6";
11
12 # Description:
13 # Picks out sshd log lines such as:
14 # Did not receive identification string from 217.147.176.135
15 # Illegal user ouoeuoeu from 192.168.9.24
16 # Failed password for {illegal,invalid} user oouoeu from 192.168.7.1 port 33562 ssh2
17 # Failed password for peter from 192.168.7.1 port 33570 ssh2
18 # Depending on the action patterns supplied in the configuration file, these messages will
19 # lead to an instant block, a block after some failures, or no block at all.
20
21 # Looking through old logs, the longest attack from a single IP was 30 mins.
22 # So the default block times could be reduced.
23
24 my $conffile="/etc/sshdfilterrc";
25 if( exists $ENV{"SSHDFILTERRC"} ) {
26 $conffile=$ENV{"SSHDFILTERRC"};
27 }
28
29 open(CONF,$conffile) || die "No config file $conffile\n";
30
31
32 # no longer used, del when it really is no longer used
33
34 my $maxblocktime=3600*24*3; # how long (seconds) after the last activity from an ip that it can be unblocked
35 my $maxchances=3; # how many password guesses of an existing user before going on the blocked list
36 # beware, sshd -e -D can report events in double, so this value needs to be twice what it shoud be
37
38 # All these options are overwritten by config file
39
40 my $chain="SSHD"; # name of sshdfilter chain, just incase you want to run multiple isolated sshdfilters
41
42 # firewall commands to execute. They have available:
43 # $ip - IP address of the offending machine.
44 # $chain - Chain to add this IP to.
45 # $idx - the next free ipfw index number (if you are using ipfw).
46 my $firewalladd=""; # add DROP rule
47 my $firewalldel=""; # delete DROP rule
48
49 my $fwcmdpath=""; # path to iptables, iptables6 or ipfw. Not normally needed.
50
51 my $iptables="iptables"; # iptables or iptables6 (ipfw mode assumes the ipfw command)
52
53 my $iptablesoptions; # obsolete. If this is set to anything, config file is for < 1.5.4
54
55 my $sshdpath="/usr/sbin/sshd"; # where sshd lives
56 my $sshdname="sshd"; # the name of the sshd process, only needed to identify the sshd processes
57 # from a none STDIN logsource.
58 my $logpid=0; # parent pid of any pids reported in syslog, so we can identify our sshd processes
59
60 my $ip6toip4=1; # should IPv6 addresses (which are really IPv4 addresses) be converted to IPv4?
61
62 my $debug=1; # !0 means debug mode, more info is logged, namely the pattern matches.
63
64 my $sanitise="[^-a-zA-Z0-9_]"; # remove everything but these chars from the username (^ means not)
65
66 my $logsource="STDIN"; # where sshd logs come from, sshd -e -D | sshdfilter style, or via a
67 # named pipe setup in syslog.conf
68
69 my $ipfwmin=-1; # if >=0, ipfw will be used instead of iptables
70 my $ipfwmax=-1;
71
72 # **** Shouldn't need to change anything after this line
73
74 use IO::Handle;
75 use Sys::Syslog qw(:DEFAULT setlogsock);
76 use POSIX 'setsid';
77 use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
78 use POSIX ":sys_wait_h";
79 use Socket; # for name lookups, some PAM messages use the FQDN instead of the IP
80
81
82 # sshd log messages
83 my @txt_pid2ip;
84 my @txt_pidexit;
85 my @txt_invalid;
86 my @txt_failval;
87 my @txt_accept;
88 my @txt_noid;
89 my @txt_quit;
90
91 my @map_pid2ip;
92 my @map_pidexit;
93 my @map_invalid;
94 my @map_failval;
95 my @map_accept;
96 my @map_noid;
97 my @map_quit;
98
99 # user policy, max counter, block time, and corresponding regular expression
100 my @maxc;
101 my @btime;
102 my @userre;
103 my %maxc_tags; # INVALID, NOID, DIRTY
104 my %btime_tags;
105
106 foreach my $i (INVALID, NOID, DIRTY, DEFAULT) {
107 $btime_tags{$i}=$maxblocktime;
108 $maxc_tags{$i}=$maxchances;
109 }
110
111 # Regexs matching IP addresses
112 my @ipre;
113 my @ipaction;
114
115 # Email block events policy
116 my @mailre;
117 my @mailaction={ 0 };
118 my $mail; # mail command line
119
120 # multiline history
121 my $maxhist=1;
122 my $curhist=0;
123 my %loghist;
124
125 # config parser counters
126 my $line=0;
127 my $polline=0;
128
129 my $confstate="";
130
131 my $parse_maxc=$maxchances;
132 my $parse_btime=$maxblocktime;
133
134 while(<CONF>)
135 {
136 $line++;
137 chomp;
138 s/ #.*//g;
139 if( /^+s*#/ || /^\s*$/ ) {
140 } elsif( /^SECTION (.*)/ ) {
141 $confstate=$1;
142 $polline=0;
143 #printf "Setting Conf $_, to $confstate\n";
144 if ( $confstate eq "EMAILPOLICY" ){ $polline=1; }
145
146 } elsif ( $confstate eq "SSHDLOG" ){
147
148 if( /^\s*(.*)\s*=\s*'(.*)'/ ) {
149 my $key=$1; my $pat=$2;
150 #print "extracted >$key<, >$pat<\n";
151 if ( $key =~ /^msg_/ ) { my $maxlfs=scalar split(/\n/,$pat)+1; if($maxlfs>$maxhist){ $maxhist=$maxlfs; } }
152 if ( $key eq "msg_pid_2_ip" ) { push @txt_pid2ip, $pat; }
153 elsif( $key eq "map_pid_2_ip" ) { push @map_pid2ip, $pat; }
154 elsif( $key eq "msg_pid_exit" ) { push @txt_pidexit, $pat; }
155 elsif( $key eq "map_pid_exit" ) { push @map_pidexit, $pat; }
156 elsif( $key eq "msg_invalid" ) { push @txt_invalid, $pat; }
157 elsif( $key eq "map_invalid" ) { push @map_invalid, $pat; }
158 elsif( $key eq "msg_failed_valid" ) { push @txt_failval,$pat; }
159 elsif( $key eq "map_failed_valid" ) { push @map_failval,$pat; }
160 elsif( $key eq "msg_accepted_user" ) { push @txt_accept,$pat; }
161 elsif( $key eq "map_accepted_user" ) { push @map_accept,$pat; }
162 elsif( $key eq "msg_no_id_string" ) { push @txt_noid,$pat; }
163 elsif( $key eq "map_no_id_string" ) { push @map_noid,$pat; }
164 elsif( $key eq "msg_quit" ) { push @txt_quit,$pat; }
165 elsif( $key eq "map_quit" ) { push @map_quit,$pat; }
166 else { print "Line $line, unknown keyword $key\n"; }
167 } else {
168 print "Ignoring line $line: %s\n",$_;
169 }
170
171 } elsif( $confstate eq "USERPOLICY" ){ # USERPOLICY section
172
173 my $ismatch=0;
174 my $mcount=""; my $t=""; my $tunits=""; my $re="";
175 #print "Considering $_\n";
176 if( /^\s*([0-9]+)\s*=\s*((?:DEFAULT|NOID|INVALID|DIRTY|'.+'))/) {
177 $ismatch=1;
178 #print "mcount=$1, userre=$2\n";
179 $mcount=$1; $re=$2;
180 } elsif( /^\s*([0-9]*)\s*,\s*([0-9]*)\s*([dhms]?)\s*=\s*((?:DEFAULT|NOID|INVALID|DIRTY|'.+'))/ ) {
181 $ismatch=1;
182 #print "mcount=$1, t=$2, tunits=$3, userre=$4\n";
183 $mcount=$1; $t=$2; $tunits=$3; $re=$4;
184 }
185 if( $ismatch ){
186 #$re =~ s/^'//; $re =~ s/'$//;
187 $re =~ s/^'(.*)'$/$1/;
188 if( $mcount =~ /[0-9]/ ) { $maxc[$polline]=$mcount; } else { $maxc[$polline]=$parse_maxc; }
189 if( $t =~ /[0-9]/ ){
190 $btime[$polline]=$t;
191 if( $tunits eq "d" ){ $btime[$polline]*=3600*24; }
192 if( $tunits eq "h" ){ $btime[$polline]*=3600; }
193 if( $tunits eq "m" ){ $btime[$polline]*=60; }
194 if( $tunits eq "s" ){ }
195 if( $btime[$polline] < 5 ){ print "Warning, short block time of $btime[$polline] seconds. "; print "Line $line: $_\n"; }
196 } else { $btime[$polline]=$parse_btime; }
197 if( $re =~ /^.+$/ ) {
198 $userre[$polline]=$re;
199 } else {
200 print "Warning, parser broke, line $line, $_\n$re\n";
201 delete $btime[$polline]; delete $maxc[$polline]; delete $userre[$polline];
202 $polline--;
203 }
204
205 # deal with NOID|INVALID|DIRTY and DEFAULTs effect on them
206 if( $re eq "DEFAULT" ) {
207 if( $mcount =~ /[0-9]/ ) { $parse_maxc=$maxc[$polline]; }
208 if( $t =~ /[0-9]/ ){ $parse_btime=$btime[$polline]; }
209 } elsif( $re eq "NOID" || $re eq "INVALID" || $re eq "DIRTY" ) {
210 if( $mcount =~ /[0-9]/ ) { $maxc_tags{$re}=$maxc[$polline]; } else { $maxc_tags{$re}=$parse_maxc; }
211 if( $t =~ /[0-9]/ ){ $btime_tags{$re}=$btime[$polline]; } else { $btime_tags{$re}=$parse_btime; }
212 if( $re eq "NOID" || $re eq "DIRTY" ) {
213 delete $btime[$polline]; delete $maxc[$polline]; delete $userre[$polline];
214 $polline--;
215 }
216 }
217 $polline++;
218
219 } else {
220 print "USERPOLICY section, bad syntax, line $line: $_\n";
221 }
222
223 } elsif( $confstate eq "IPPOLICY" ){
224
225 if( /^([\+\-])'(.*)'/ ) {
226 push @ipre,$2;
227 #print "Adding to whitelist $1, $2\n";
228 if( $1 eq "+" ) { push @ipaction,0; }
229 if( $1 eq "-" ) { push @ipaction,1; }
230 } else {
231 printf "Bad syntax, line $line: %s\n",$_; }
232
233 } elsif( $confstate eq "EMAILPOLICY" ){
234
235 if( /^([\+\-])((?:DEFAULT|DIRTY|INVALID|NOID|'.+'))/ ) {
236 my $pos=$#mailaction+1;
237 my $yy=$1; my $re=$2;
238 $re =~ s/^'(.*)'$/$1/;
239 if( $re eq "DEFAULT" ) { $pos=0; }
240 if( $yy eq "+" ){ $mailaction[$pos]=1; } else { $mailaction[$pos]=0; }
241 $mailre[$pos]=$re;
242 } else {
243 printf "Bad syntax, line $line: %s\n",$_; }
244
245 } elsif( $confstate eq "OPTIONS" ){
246
247 if( /^sanitise\s*=\s*'(.*)'/ ) { $sanitise=$1; }
248 elsif( /^maxblocktime\s*=\s*(.*)/ ) { $maxblocktime=eval $1; }
249 elsif( /^maxchances\s*=\s*(.*)/ ) { $maxchances=eval $1; }
250 elsif( /^firewalladd\s*=\s*'(.*)'/ ) { $firewalladd=$1; }
251 elsif( /^firewalldel\s*=\s*'(.*)'/ ) { $firewalldel=$1; }
252 elsif( /^fwcmdpath\s*=\s*'(.*)'/ ) { $fwcmdpath="$1/"; }
253 elsif( /^ipfwmin\s*=\s*([0-9]+)/ ) { $ipfwmin=$1; }
254 elsif( /^ipfwmax\s*=\s*([0-9]+)/ ) { $ipfwmax=$1; }
255 elsif( /^iptablesoptions\s*=\s*'(.*)'/ ) { $iptablesoptions=$1; }
256 elsif( /^chain\s*=\s*'([^ ]*)'/ ) { $chain=$1; }
257 elsif( /^mail\s*=\s*'(.*)'/ ) { $mail=$1; }
258 elsif( /^ip6toip4\s*=\s*([01])/ ) { $ip6toip4=$1; }
259 elsif( /^logsource\s*=\s*'([^ ]*)'/ ) { $logsource=$1; }
260 elsif( /^sshdpath\s*=\s*'([^ ]*)'/ ) { $sshdpath=$1; }
261 elsif( /^sshdname\s*=\s*'(.*)'/ ) { $sshdname=$1; }
262 elsif( /^logpid\s*=\s*(.*)/ ) { $logpid=$1; }
263 elsif( /^debug\s*=\s*([01234])/ ) { $debug=$1; }
264 else { print "Unknown option line $line: $_\n"; }
265
266 } else { printf "Ignoring line $line: %s\n",$_; }
267 } # For each line in the configuration file
268
269 close CONF;
270
271
272 # apply some extra option logic
273 if( $ip6toip4 == 0 ) {
274 $iptables="ip6tables";
275 }
276
277
278 # read in ARGV if needed, lets the caller pass a pid value (well, really a parent pid)
279 if( $logsource ne "STDIN" ) {
280 for(my $idx=0; $idx<=$#ARGV; $idx++) {
281 if( $ARGV[$idx] =~ /logpid\s*=\s*(.*)/ ) { $logpid=$1; }
282 }
283 }
284
285 if( $firewalladd eq "" ) { print "no firewalladd command defined\n"; exit 1; }
286 if( $firewalldel eq "" ) { print "no firewalldel command defined\n"; exit 1; }
287
288 if( $ipfwmin!=-1 && $ipfwmin>=$ipfwmax ) { print "ipfwmin greater than ipfwmax\n"; exit 1; }
289 if( $ipfwmin!=-1 && $ipfwmin>65535 ) { print "ipfwmin greater than allowed by ipfw\n"; exit 1; }
290 if( $ipfwmin!=-1 && $ipfwmax>65535 ) { print "ipfwmax greater than allowed by ipfw\n"; exit 1; }
291 if( $ipfwmin==-1 && $ipfwmax!=-1 ) { print "ipfwmax defined, but ipfwmin still undefined\n"; exit 1; }
292
293 #if( $#txt_pid2ip ==-1 ) { print "msg_pid_2_ip undefined\n"; exit 1; }
294 #if( $#map_pid2ip ==-1 ) { print "map_pid_2_ip undefined\n"; exit 1; }
295 #if( $#txt_pidexit ==-1 ) { print "msg_pid_exit undefined\n"; exit 1; }
296 #if( $#map_pidexit ==-1 ) { print "map_pid_exit undefined\n"; exit 1; }
297 if( $#txt_invalid ==-1 ) { print "msg_invalid undefined\n"; exit 1; }
298 if( $#map_invalid ==-1 ) { print "map_invalid undefined\n"; exit 1; }
299 if( $#txt_failval ==-1 ) { print "msg_failed_valid undefined\n"; exit 1; }
300 if( $#map_failval ==-1 ) { print "map_failed_valid undefined\n"; exit 1; }
301 if( $#txt_accept ==-1 ) { print "msg_accepted_user undefined\n"; exit 1; }
302 if( $#map_accept ==-1 ) { print "map_accepted_user undefined\n"; exit 1; }
303 if( $#txt_noid ==-1 ) { print "msg_no_id_string undefined\n"; exit 1; }
304 if( $#map_noid ==-1 ) { print "map_no_id_string undefined\n"; exit 1; }
305 if( $#txt_quit ==-1 ) { print "msg_quit undefined\n"; exit 1; }
306 if( $#map_quit ==-1 ) { print "map_quit undefined\n"; exit 1; }
307
308 if( $#txt_pid2ip != $#map_pid2ip ) { print "#msg_pid_2_ip != #map_pid_2_ip, $#txt_pid2ip != $#map_pid2ip\n"; exit 1; }
309 if( $#txt_pidexit != $#map_pidexit ) { print "#msg_pid_exit != #map_pid_exit, $#txt_pidexit != $#map_pidexit\n"; exit 1; }
310 if( $#txt_invalid != $#map_invalid ) { print "#msg_invalid != #map_invalid, $#txt_invalid != $#map_invalid\n"; exit 1; }
311 if( $#txt_failval != $#map_failval ) { print "#msg_failed_valid != #map_failed_valid, $#txt_failval != $#map_failval\n"; exit 1; }
312 if( $#txt_accept != $#map_accept ) { print "#msg_accepted_user != #map_accepted_user, $#txt_accept != $#map_accept\n"; exit 1; }
313 if( $#txt_noid != $#map_noid ) { print "#msg_no_id_string != #map_no_id_string, $#txt_noid != $#map_noid\n"; exit 1; }
314 if( $#txt_quit != $#map_quit ) { print "#msg_quit != #map_quit, $#txt_quit != $#map_quit\n"; exit 1; }
315
316 foreach my $i ("DEFAULT", "NOID", "DIRTY", "INVALID") {
317 if( ! defined $maxc_tags{$i} ) { $maxc_tags{$i}=$maxc_tags{"DEFAULT"}; }
318 if( ! defined $btime_tags{$i} ) { $btime_tags{$i}=$btime_tags{"DEFAULT"}; }
319 }
320
321
322
323 # Daemonise like sshd, better fits regular startup scripts. Function nicked from perlipc3 man page
324 # Have the parent wait until the other deamons have started before letting the original parent quit
325 pipe(CONSOLEPROC_RDR, CONSOLEPROC);
326 CONSOLEPROC->autoflush(1);
327
328 chdir '/' or die "Can't chdir to /: $!";
329 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
330 open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
331 #open STDOUT, '>/tmp/sshdfilter.out' or die "Can't write to /tmp/sshdfilter.out: $!";
332
333 my $daemonpid=0;
334 defined($daemonpid = fork) or die "Can't fork: $!";
335 if ($daemonpid>0) { # wait on handle for some sign until all the children have been created
336 close CONSOLEPROC;
337 my $res=1;
338 while(<CONSOLEPROC_RDR>){
339 if(/Exit ok!/) {
340 $res=0;
341 }
342 }
343 close CONSOLEPROC_RDR;
344 exit $res;
345 }
346 setsid or die "Can't start a new session: $!";
347 open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
348
349
350
351 # fork another process, just so we can have a separate instance of
352 # openlog() for sshd. Code nicked from PerlIPC man page
353 my $SSHDLOGGER = IO::Handle->new();
354 my $pid;
355 if( $logsource eq "STDIN" ) {
356 pipe(CHILD_RDR, $SSHDLOGGER);
357 $SSHDLOGGER->autoflush(1);
358
359 $pid=fork;
360 if ( $pid == 0 ) {
361 close $SSHDLOGGER;
362 close CONSOLEPROC;
363
364 chdir '/' or die "Can't chdir to /: $!";
365 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
366 open STDOUT, '>/dev/null';
367
368 # open logging specific to sshd
369 setlogsock('unix');
370 openlog 'sshd', 'pid,ndelay', 'authpriv';
371
372 while(<CHILD_RDR>) # output all received lines to this log
373 {
374 chomp;
375 syslog 'authpriv|notice',"$_";
376 }
377 closelog();
378 close CHILD_RDR;
379 exit 0;
380 }
381 die "cannot fork: $!" unless defined $pid;
382 close CHILD_RDR;
383 }
384
385
386 # open logging for sshdfilter
387 setlogsock('unix');
388 openlog 'sshdfilt', 'pid,ndelay', 'authpriv';
389
390
391 # check user has setup IP tables (or ipfw)
392
393 if( $ipfwmin==-1 ){
394 # iptables -L -n | grep "^SSHD *tcp"
395 # SSHD tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
396 open(TABCHECK,"$iptables -L -n | grep \"^$chain *tcp\"|") || die("couldn't check $iptables");
397 my $aline=<TABCHECK>;
398 close(TABCHECK);
399 if( $aline !~ /^$chain/ ) {
400 syslog 'authpriv|warning',"$iptables is missing $chain redirect, sshdfilter rendered useless.";
401 }
402
403 # iptables -L SSHD -n | grep "Chain SSHD"
404 # Chain SSHD (1 references)
405 open(TABCHECK,"$iptables -L $chain -n | grep \"^Chain $chain\"|") || die("couldn't check $iptables");
406 $aline=<TABCHECK>;
407 close(TABCHECK);
408 if( $aline !~ /^Chain $chain/ ) {
409 syslog 'authpriv|warning',"$iptables is missing $chain chain, sshdfilter rendered useless.";
410 }
411 }
412 else
413 {
414 # Don't check ipfw configuration, although I suggest a skip-these-rules rule, it isn't
415 # necessary, so the only additions could be block events as they happen.
416 }
417
418
419 # run sshd
420 if( $logsource eq "STDIN" ) {
421 syslog 'authpriv|notice',"sshdfilter $version starting up, running sshd proper.";
422 } else {
423 syslog 'authpriv|notice',"sshdfilter $version starting up.";
424 }
425
426 # maybe log the setup
427 if( $debug ) {
428 syslog 'authpriv|notice', "DB:OPTIONS";
429 syslog 'authpriv|notice',"DB: maxblocktime=$maxblocktime";
430 syslog 'authpriv|notice',"DB: maxchances=$maxchances";
431 syslog 'authpriv|notice',"DB: firewalladd=$firewalladd";
432 syslog 'authpriv|notice',"DB: firewalldel=$firewalldel";
433 syslog 'authpriv|notice',"DB: fwcmdpath=$fwcmdpath";
434 syslog 'authpriv|notice',"DB: sshdpath=$sshdpath";
435 syslog 'authpriv|notice',"DB: sshdname=$sshdname";
436 syslog 'authpriv|notice',"DB: logpid=$logpid";
437 syslog 'authpriv|notice',"DB: ip6toip4=$ip6toip4";
438 syslog 'authpriv|notice',"DB: iptables command=$iptables";
439 syslog 'authpriv|notice',"DB: iptables chain=$chain";
440 syslog 'authpriv|notice',"DB: ipfwmin=$ipfwmin";
441 syslog 'authpriv|notice',"DB: ipfwmax=$ipfwmax";
442 syslog 'authpriv|notice',"DB: debug=$debug";
443 syslog 'authpriv|notice',"DB: logsource=$logsource";
444 syslog 'authpriv|notice',"DB: sshd args=@ARGV";
445 syslog 'authpriv|notice',"DB: sanitise=$sanitise";
446 syslog 'authpriv|notice',"DB: mail=$mail";
447 syslog 'authpriv|notice',"DB: maxhist=$maxhist";
448
449 my $p=sprintf "DB:USER POLICY entries=%d",$#userre+1; syslog 'authpriv|notice',$p;
450 for(my $i=0;$i<=$#userre;$i++) {
451 syslog 'authpriv|notice',"DB: $i, $maxc[$i], $btime[$i], $userre[$i]";
452 }
453 foreach my $i ("DEFAULT", "NOID", "DIRTY", "INVALID") {
454 syslog 'authpriv|notice',"DB: $maxc_tags{$i}, $btime_tags{$i}, tag=$i";
455 }
456
457
458 $p=sprintf "DB:IP POLICY entries=%d",$#ipre+1; syslog 'authpriv|notice',$p;
459 for(my $i=0;$i<=$#ipre;$i++) {
460 syslog 'authpriv|notice',"DB: $i, action=$ipaction[$i], re=$ipre[$i]";
461 }
462
463 $p=sprintf "DB:EMAIL POLICY entries=%d",$#mailaction+1; syslog 'authpriv|notice',$p;
464 for(my $i=0;$i<=$#mailaction;$i++) {
465 syslog 'authpriv|notice',"DB: $i, action=$mailaction[$i], re=$mailre[$i]";
466 }
467
468 my $isdefined=0;
469
470 for(my $idx=0; $idx<=$#txt_pid2ip; $idx++)
471 { syslog 'authpriv|notice',"DB: msg_pid_2_ip[$idx]=$txt_pid2ip[$idx]"; $isdefined++; }
472 for(my $idx=0; $idx<=$#map_pid2ip; $idx++)
473 { syslog 'authpriv|notice',"DB: map_pid_2_ip[$idx]=$map_pid2ip[$idx]"; $isdefined++; }
474 for(my $idx=0; $idx<=$#txt_pidexit; $idx++)
475 { syslog 'authpriv|notice',"DB: msg_pid_exit[$idx]=$txt_pidexit[$idx]"; $isdefined++; }
476 for(my $idx=0; $idx<=$#map_pidexit; $idx++)
477 { syslog 'authpriv|notice',"DB: map_pid_exit[$idx]=$map_pidexit[$idx]"; $isdefined++; }
478 for(my $idx=0; $idx<=$#txt_invalid; $idx++)
479 { syslog 'authpriv|notice',"DB: msg_invalid[$idx]=$txt_invalid[$idx]"; $isdefined++; }
480 for(my $idx=0; $idx<=$#map_invalid; $idx++)
481 { syslog 'authpriv|notice',"DB: map_invalid[$idx]=$map_invalid[$idx]"; $isdefined++; }
482 for(my $idx=0; $idx<=$#txt_failval; $idx++)
483 { syslog 'authpriv|notice',"DB: msg_failed_valid[$idx]=$txt_failval[$idx]"; $isdefined++; }
484 for(my $idx=0; $idx<=$#map_failval; $idx++)
485 { syslog 'authpriv|notice',"DB: map_failed_valid[$idx]=$map_failval[$idx]"; $isdefined++; }
486 for(my $idx=0; $idx<=$#txt_accept; $idx++)
487 { syslog 'authpriv|notice',"DB: msg_accepted_user[$idx]=$txt_accept[$idx]"; $isdefined++; }
488 for(my $idx=0; $idx<=$#map_accept; $idx++)
489 { syslog 'authpriv|notice',"DB: map_accepted_user[$idx]=$map_accept[$idx]"; $isdefined++; }
490 for(my $idx=0; $idx<=$#txt_noid; $idx++)
491 { syslog 'authpriv|notice',"DB: msg_no_id_string[$idx]=$txt_noid[$idx]"; $isdefined++; }
492 for(my $idx=0; $idx<=$#map_noid; $idx++)
493 { syslog 'authpriv|notice',"DB: map_no_id_string[$idx]=$map_noid[$idx]"; $isdefined++; }
494 for(my $idx=0; $idx<=$#txt_quit; $idx++)
495 { syslog 'authpriv|notice',"DB: msg_quit[$idx]=$txt_quit[$idx]"; $isdefined++; }
496 for(my $idx=0; $idx<=$#map_quit; $idx++)
497 { syslog 'authpriv|notice',"DB: map_quit[$idx]=$map_quit[$idx]"; $isdefined++; }
498
499 if( $isdefined<10 ) {
500 syslog 'authpriv|err', "SSHDLOG section of /etc/sshdfilterrc is missing $isdefined regexs";
501 syslog 'authpriv|err', "This probably means either you are using a pre 1.5 verison of the";
502 syslog 'authpriv|err', "sshdfilterrc file, which will not work, Or, you forgot to append";
503 syslog 'authpriv|err', "the appropriate pattern file from patterns/ to your /etc/sshdfilterrc.";
504 }
505 } # end of $debug flag
506
507 my $SSHDHANDLE = IO::Handle->new();
508
509 if( $logsource ne "STDIN" )
510 {
511 open($SSHDHANDLE,$logsource) || syslog 'authpriv|err', "Tried to open a named pipe $logsource for sshd log messages, but failed";
512 }
513 else
514 {
515 # fork for sshd process. open() blocked daemonise code. fork()/exec() doesn't.
516 # Code nicked from PerlIPC man page
517 pipe($SSHDHANDLE, SSHDHAND_WTR);
518 SSHDHAND_WTR->autoflush(1);
519
520 my $sshdpid=fork;
521 if ( $sshdpid == 0 ) {
522 close $SSHDHANDLE;
523 close CONSOLEPROC;
524 close $SSHDLOGGER;
525
526 chdir '/' or die "Can't chdir to /: $!";
527 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
528 open STDOUT, ">&SSHDHAND_WTR" or die "Can't dup SSHDHAND_WTR: $!";
529 open STDERR, ">&SSHDHAND_WTR" or die "Can't dup SSHDHAND_WTR: $!";
530
531 exec "$sshdpath", @ARGV, "-e","-D";
532
533 syslog 'authpriv|err',"couldn't run $sshdpath";
534 die("couldn't run $sshdpath");
535
536 close SSHDHAND_WTR;
537 exit 0;
538 }
539
540 if( !defined $sshdpid ){ syslog 'authpriv|err',"cannot fork: $!";
541 die "cannot fork: $!";
542 }
543 close SSHDHAND_WTR;
544
545 sleep 1;
546
547 # attempt to get some better sshd status information
548 my $res=waitpid( $sshdpid, WNOHANG );
549 if( $res!=0 ) {
550 syslog 'authpriv|err',"ran sshd and waited one second, it died and said: status=$res error=$?";
551 }
552 } # If using stdin for sshd log messages, and we are running sshd ourselves
553
554
555 if( $ipfwmin==-1 ){
556 syslog 'authpriv|notice',"Flushing $chain chain";
557 loggingsystem("${fwcmdpath}$iptables -F $chain");
558 } else {
559 syslog 'authpriv|notice',"Deleting rule numbers $ipfwmin to $ipfwmax";
560 open(RULES,"${fwcmdpath}ipfw show |") || syslog 'authpriv|err',"Couldn't flush old rules, command failed.";
561 if( RULES ){
562 while(<RULES>){
563 chomp;
564 my ($idx,$therest)=split(/\s/,$_);
565 if( $idx>=$ipfwmin && $idx<=$ipfwmax ){
566 loggingsystem("${fwcmdpath}ipfw delete $idx");
567 }
568 }
569 close RULES;
570 }
571 }
572
573 # write a pid
574 open(THISPID,">/var/run/sshdfilter.pid.$chain") || die("couldn't write pid file");
575 printf THISPID "%d\n",$$;
576 close(THISPID);
577
578
579 # Run the given command, log any problems as an error
580 sub loggingsystem {
581 my $command=shift;
582 if (system($command) != 0) {
583 my $res=$?;
584 if( $res>255 ) { $res = $res >> 8; }
585 syslog 'authpriv|err',"system(\"$command\"); failed: $res";
586 syslog 'authpriv|err',"Suggest trying the same command in a shell.";
587 }
588 }
589
590 # Option to email a user(s) when a block event occurs.
591 # We also fork to send from a child process, incase the sending
592 # of mail hangs.
593 sub emailonblock {
594 my $user=shift;
595 my $ip=shift;
596 my $event=shift;
597 my $releasetime=shift;
598
599 if( defined $mail ){
600
601 my $shouldmail=$mailaction[0];
602 for(my $i=1;$i<=$#mailaction;$i++) {
603 if( $user =~ /$mailre[$i]/ ) {
604 $shouldmail=$mailaction[$i];
605 last;
606 }
607 }
608
609 if( $shouldmail == 0 ) { return; }
610
611 my $pid=fork;
612 if ( $pid == 0 ) {
613
614 chdir '/' or die "Can't chdir to /: $!";
615 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
616 open STDOUT, '>/dev/null';
617
618 my $mailcommand;
619 my $mailcomm=sprintf('$mailcommand="%s"',"$mail");
620 if($debug){ syslog 'authpriv|debug',"DB:pre mail command is $mailcomm"; }
621 eval "$mailcomm";
622 if($debug){ syslog 'authpriv|debug',"DB:post mail command is $mailcommand"; }
623 open(MAILPROC,"| $mailcommand") || syslog 'authpriv|warning',"sshdfilter couldn't email block event";
624 if( MAILPROC ){
625 my $ltime=localtime($releasetime);
626 printf MAILPROC "IP $ip was blocked. $event\n";
627 printf MAILPROC "Will remove block at %s.\n",$ltime;
628 close( MAILPROC );
629 exit 0;
630 }
631
632 die "cannot fork: $!" unless defined $pid;
633
634 }
635 }
636 }
637
638 # State tracking hashes
639 my %chances; # number of chances an ip has had. This has a purge time just
640 # like blocks, so we wont build up a list of stale login attempts.
641 my %expiretime; # indexed on ip, storing a time a block should be removed.
642 my %explain; # hash indexed on ip, storing a reason for the the block event.
643 # This also flags a block event is active.
644 my %pid2ip; # hash indexed on a pid, mapping to an ip. Supports the dropbear ssh
645 # server, which gives only the pid as a common element on each line.
646
647 my %pidtime; # cache parent pid lookups, to avoid the race condition of looking up
648 # the parent of a process that has just exited. Still doesn't catch all
649 # the scenarios, but, nothing will.
650
651 my %ipfwindex; # IP to ipfw index number
652 my %ipfwfree; # ipfw index numbers that are now free, but aren't part of the continuous set of free numbers
653 my $ipfwcur=$ipfwmin; # lowest free ipfw index of the continous set
654
655 # my $numipfwfree=keys %ipfwfree;
656 # my $numipfwindex=keys %ipfwindex;
657 # my $numexpiretime=keys %expiretime;
658 # printf("1 current=%d of %d (%d-%d), free slots=%d, blocks=%d/%d, action %s, ip=%s\n",$ipfwcur,$ipfwmax-$ipfwcur,$ipfwmin,$ipfwmax,$numipfwfree,$numipfwindex,$numexpiretime,$action,$ip);
659
660 sub deldrop {
661 my $ip=shift; # IP address to remove from drop list
662 if( defined $explain{$ip} ) {
663 my $reason=$explain{$ip};
664 delete $explain{$ip};
665 syslog 'authpriv|notice',"Cancelled $reason block from $ip";
666 my $fwcommand;
667 my $fwcomm=sprintf('$fwcommand="%s"',"$fwcmdpath$firewalldel");
668 if($debug){ syslog 'authpriv|debug',"DB: pre fw del command is $fwcomm"; }
669 if( $ipfwmin==-1 ) {
670 eval "$fwcomm";
671 if($debug){ syslog 'authpriv|debug',"DB: post fw del command is $fwcommand"; }
672 loggingsystem("$fwcommand");
673 } else {
674 my $idx=$ipfwindex{$ip};
675 eval "$fwcomm";
676 if($debug){ syslog 'authpriv|debug',"DB: post fw del command is $fwcommand"; }
677 loggingsystem("$fwcommand");
678 delete $ipfwindex{$ip};
679 delete $expiretime{$ip};
680 $ipfwfree{$idx}=1;
681 }
682 delete $expiretime{$ip};
683 delete $chances{$ip};
684 }
685
686 }
687
688 sub adddrop {
689 my $ip=shift; # IP address to block
690 my $fwcommand;
691 my $fwcomm=sprintf('$fwcommand="%s"',"$fwcmdpath$firewalladd");
692 if($debug){ syslog 'authpriv|debug',"DB: pre fw add command is $fwcomm"; }
693
694 if( $ipfwmin==-1 ) { # iptables option, simple compared to ipfw and its index numbers.
695 eval "$fwcomm";
696 if($debug){ syslog 'authpriv|debug',"DB: post fw add command is $fwcommand"; }
697 loggingsystem("$fwcommand");
698 } else {
699 my $numipfwfree=keys %ipfwfree;
700 if( $numipfwfree > 0 ) # pick an index from the free pool
701 {
702 my $lowestidx=$ipfwcur;
703 foreach my $idx (keys %ipfwfree) {
704 if( $idx<$lowestidx ) { $lowestidx=$idx; }
705 }
706 delete $ipfwfree{$lowestidx};
707
708 $ipfwindex{$ip}=$lowestidx;
709 my $idx=$lowestidx;
710 eval "$fwcomm";
711 if($debug>2){ syslog 'authpriv|notice', "DB: recycling ipfw index $lowestidx"; }
712 }
713 else
714 { # pick an index from the continous block, or forced to delete the oldest current block
715 my $idx;
716 if( $ipfwcur<=$ipfwmax )
717 {
718 $idx=$ipfwcur;
719 $ipfwindex{$ip}=$ipfwcur++;
720 if($debug>2){ syslog 'authpriv|notice', "DB: adding new ipfw index $ipfwindex{$ip}"; }
721 }
722 else
723 {
724 my $numexpiretime=keys %expiretime;
725
726 foreach my $oldip (sort { $expiretime{$a} <=> $expiretime{$b} } keys %expiretime) {
727 if( $oldip ne $ip ) { # skip small chance of trying to remove self
728 my $idx=$ipfwindex{$oldip};
729 if( $debug>2 ){ syslog 'authpriv|notice', "DB: Recycling ip $oldip with index $idx"; }
730 if(! defined $ipfwindex{$oldip} ){ syslog 'authpriv|err', "Logic error, oldip $oldip not found."; }
731 deldrop $oldip;
732 delete $ipfwfree{$idx};
733 $ipfwindex{$ip}=$idx;
734 last;
735 }
736 }
737 $idx=$ipfwindex{$ip};
738 if($debug>2){ syslog 'authpriv|notice', "DB: prematurely recycling oldest ipfw index $ipfwindex{$ip}"; }
739 }
740 eval "$fwcomm";
741 }
742 if($debug){ syslog 'authpriv|debug',"DB: post fw add command is $fwcommand"; }
743 loggingsystem("$fwcommand");
744 }
745 }
746
747 # about to enter main loop, so signal parent process to quit.
748 # We never really know if sshd started correctly, so we always exit with a success.
749 printf CONSOLEPROC "Exit ok!\n";
750 close CONSOLEPROC;
751
752 # enable non-blocking read operation
753 my $flags = fcntl($SSHDHANDLE, F_GETFL, 0) or die "Can't get flags for the socket: $!\n";
754 $flags = fcntl($SSHDHANDLE, F_SETFL, $flags | O_NONBLOCK) or die "Can't set flags for the socket: $!\n";
755
756 # buffer input from $SSHDHANDLE, read() may get more than one line, or a partial line
757 my $next;
758 my $this;
759
760 my $shouldexit=0;
761 my $purgetime=undef;
762
763 sub sig_exit { $shouldexit=1; }
764
765 local $SIG{TERM} = 'sig_exit';
766
767
768 while( !$shouldexit)
769 {
770 my $rin ='';
771 vec($rin,fileno($SSHDHANDLE),1) = 1;
772 my $ein = $rin ;
773 my $res=select($rin,undef,$ein,$purgetime);
774
775 #printf "Came back with %d\n",$res;
776
777 my $ttime=time;
778
779 # reap any zombies (which are created by sending emails)
780 my $kid;
781 do {
782 $kid = waitpid(-1, WNOHANG);
783 } until $kid <= 0;
784 $purgetime=undef;
785 foreach my $ip (sort { $expiretime{$a} <=> $expiretime{$b} } keys %expiretime) {
786 if( $expiretime{$ip} < $ttime ) {
787 if( defined $explain{$ip} ) {
788 my $reason=$explain{$ip};
789 syslog 'authpriv|notice',"Cancelled $reason block from $ip";
790 deldrop $ip;
791 delete $explain{$ip};
792 }
793 delete $expiretime{$ip};
794 delete $chances{$ip};
795 }
796 elsif( ! defined $purgetime ) {
797 $purgetime=$expiretime{$ip}-$ttime+1;
798 last
799 }
800 else {
801 last;
802 }
803 } # end of iptables purge
804
805 # trim pid cache, for most people this will always be empty
806 foreach my $p (sort { $pidtime{$a} <=> $pidtime{$b} } keys %pidtime) {
807 if( $pidtime{$p} < $ttime ) {
808 delete $pidtime{$p};
809 } elsif( !defined $purgetime || $purgetime>$pidtime{$p}-$ttime ) {
810 $purgetime=$pidtime{$p}-$ttime+1;
811 last;
812 } else {
813 last;
814 }
815 }
816
817 if( $res >0 )
818 {
819 my $firstread=1;
820 while ( 1 )
821 {
822 my $in;
823 my $num=sysread($SSHDHANDLE, $in, 1024);
824 #printf "sysread said $num and $!\n";
825 if( $num==0 && $firstread==1 && $logsource eq "STDIN" ) # detect eof
826 {
827 $shouldexit=1;
828 last;
829 }
830 $firstread=0;
831 if( $num>0 ) # we have input, add it to remains of any previous input and pull out complete lines
832 {
833 #printf "Got >$in<\n";
834 $in="$next$in";
835 while( $in =~ /\n/ )
836 {
837 ($this, $next)=split(/\n/,$in,2);
838 $in=$next;
839
840 chomp $this;
841 $this =~ tr/\n\r//d;
842 if( $logsource eq "STDIN" ) {
843 handlesshdoutput($this);
844 } elsif ( $this =~ / $sshdname\[([0-9]+)\]: (.*)/ ) {
845 my $lpid=$1;
846 my $fline=$2;
847 #syslog 'authpriv|notice',"lpid=$lpid, logpid=$logpid, fline=$fline";
848 if( $logpid<=0 || $lpid==$logpid ) {
849 handlesshdoutput($fline);
850 } elsif( $pidtime{$lpid}>$ttime ) {
851 $pidtime{$lpid}+=720;
852 #syslog 'authpriv|notice',"Using cache";
853 handlesshdoutput($fline);
854 } elsif( open(PPID,"/proc/$lpid/stat") ){ # does 8500 checks/sec on my 2.4.32 P3 660
855 my $ppid=<PPID>;
856 close PPID;
857 chomp $ppid;
858 $ppid=~ s/^[0-9]+ \($sshdname\) [A-Z] ([0-9]+) .*$/$1/;
859 #syslog 'authpriv|notice',"ppid=$ppid";
860 if( $ppid == $logpid ) {
861 $pidtime{$lpid}=$ttime+720;
862 handlesshdoutput($fline);
863 }
864 }
865 }
866
867 #printf "ALine is <$this>\n";
868 }
869 #printf "Done processing buffer\n";
870 }
871 elsif( defined $num ) # eof for named pipes
872 {
873 if( $logsource ne "STDIN" )
874 {
875 close $SSHDHANDLE;
876 open($SSHDHANDLE,$logsource) || syslog 'authpriv|err', "Tried to reopen a named pipe $logsource for sshd log messages, but failed";
877 my $flags = fcntl($SSHDHANDLE, F_GETFL, 0) or die "Can't get flags for the socket: $!\n";
878 $flags = fcntl($SSHDHANDLE, F_SETFL, $flags | O_NONBLOCK) or die "Can't set flags for the socket: $!\n";
879 $firstread=1;
880 $next="";
881 last;
882 }
883 else
884 {
885 last;
886 }
887 }
888 else # num is undefined => ran out of input, not eof
889 {
890 last;
891 }
892 } # end of while(1), sysread() input
893 }
894 } # end of while(!shouldexit)
895
896 # find the pattern that matches the given line, and map to an output array
897 # Last two parameters are references, notice the odd syntax.
898
899 sub matchre_log
900 {
901 my $aline=shift; # input line
902 my $pats=shift; # regex match array
903 my $maps=shift; # map regex matches to specific elements of output
904
905 my @res;
906 for(my $idx=0; $idx<@$pats; $idx++) {
907 if( $debug>=4){ syslog 'authpriv|err', "Comparing >$aline< to >$$pats[$idx]<"; }
908 if( $aline =~ /$$pats[$idx]/ ) {
909 #print "pattern was a hit, running $$maps[$idx]\n";
910 eval $$maps[$idx];
911 last;
912 }
913 }
914 return (@res);
915 }
916
917 sub isdirty
918 {
919 my $un=shift;
920 my $un2=$un;
921 $un =~ s/$sanitise//g;
922 if( $un ne $un2 )
923 { return 1,$un; }
924 return 0,$un;
925 }
926
927 sub matchre_ip
928 {
929 my $aline=shift; # input line, an ip address
930 my $ips=shift; # regex match array of IPs
931
932 my $idx=0;
933 for( ; $idx<@$ips; $idx++) {
934 if( $aline =~ /$$ips[$idx]/ ) {
935 return ($idx);
936 }
937 }
938 return ();
939 }
940
941
942 sub matchre_user2maxc
943 {
944 my $un=shift; # a user name, which may or may not exist
945 my $ev=shift; # INVALID, DEFAULT or DIRTY
946
947 # DIRTY never looks up names for a match other than $maxc_tags{DIRTY}
948 if( $ev eq "DIRTY" ) {
949 return $maxc_tags{$ev};
950 }
951
952 my $mc=$maxc_tags{$ev};
953
954 for(my $idx=0; $idx<=$#userre; $idx++) {
955 if($debug>2){ syslog 'authpriv|notice', "DB:u2m: un=$un, ev=$ev, idx=$idx, userre=$userre[$idx]"; }
956 if( $userre[$idx] eq $ev ) {
957 if( defined $maxc[$idx] ) { $mc=$maxc[$idx]; }
958 } elsif( $un =~ /$userre[$idx]/ ) {
959 if( defined $maxc[$idx] ) { return $maxc[$idx]; } else { return $mc; }
960 }
961 }
962
963 return $mc;
964 }
965
966 sub matchre_user2btime
967 {
968 my $un=shift; # a user name, which may or may not exist
969 my $ev=shift; # INVALID, DEFAULT or DIRTY
970
971 # DIRTY never looks up names for a match other than $btime_tags{DIRTY}
972 if( $ev eq "DIRTY" ) {
973 return $btime_tags{$ev};
974 }
975
976 my $t=$btime_tags{$ev};
977
978 for(my $idx=0; $idx<=$#userre; $idx++) {
979 if( $userre[$idx] eq $ev ) {
980 if( defined $btime[$idx] ) { $t=$btime[$idx]; }
981 } elsif( $un =~ /$userre[$idx]/ ) {
982 if( defined $btime[$idx] ) { return $btime[$idx]; } else { return $t; }
983 }
984 }
985
986 return $t;
987 }
988
989
990 # Deal with each complete line of log output from sshd.
991 # lots of code duplication, it could be reduced but would probably be even
992 # more unreadable. The variations in error messages add a lot of bulk.
993
994 sub handlesshdlines
995 {
996 my $ttime=time;
997
998 for (@_) {
999 my $aline=$_;
1000
1001 if( $debug>1 ) { syslog 'authpriv|notice',"DB:Aline=$_"; }
1002
1003 if( my ($user, $ip)=matchre_log($_, \@txt_invalid, \@map_invalid) )
1004 {
1005 my $ename="INVALID";
1006
1007 if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; }
1008
1009 my @addrs=gethostbyname($ip);
1010 @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
1011 for $ip (@addrs) {
1012
1013 if( my ($idx)=matchre_ip($ip, \@ipre) ) # white/black list
1014 {
1015 if( $ipaction[$idx]==0 ) { # white list - ignore
1016 syslog 'authpriv|notice',"Illegal username from white listed ip $ip, user $user";
1017 } else { # black list - instant block for default time
1018 if( $debug ) { syslog 'authpriv|notice',"DB:INVALID: ip black listed, $ip"; }
1019 $expiretime{$ip}=$ttime+$maxblocktime;
1020 if( ! exists $explain{$ip} ) {
1021 $explain{$ip}="Illegal user name from black listed ip, instant block of $ip";
1022 syslog 'authpriv|notice',$explain{$ip};
1023 emailonblock "INVALID", "$ip", "Illegal user name from black listed ip, instant block.", $expiretime{$ip};
1024 adddrop $ip;
1025 }
1026 }
1027 } else { # non-exceptional INVALID route
1028
1029 my ($dirty, $user) = isdirty($user);
1030
1031 if( $dirty==1 ) { $ename="DIRTY"; $user="DIRTY"; }
1032 my $maxc=matchre_user2maxc($user,$ename);
1033 my $btime=matchre_user2btime($user,$ename);
1034
1035 if( $debug ) { syslog 'authpriv|notice',"DB:INVALID: dirty=$dirty user=$user, ip=$ip"; }
1036
1037 $chances{$ip}++;
1038 $expiretime{$ip}=$ttime+$btime;
1039
1040 if( $chances{$ip} > $maxc ) { # exceeded threshold, see if we need to add a DROP rule
1041 if( ! exists $explain{$ip} ) {
1042 $explain{$ip}="Illegal user name, blocking after $maxc chances";
1043 syslog 'authpriv|notice',"Illegal user name, blocking $ip after $maxc chances";
1044 emailonblock "INVALID", "$ip", "Illegal user name, blocking after $maxc chances.", $ttime+$btime;
1045 adddrop $ip;
1046 }
1047 }
1048 else {
1049 syslog 'authpriv|notice',"Chanced illegal user name from $ip, $chances{$ip} guesses out of $maxc";
1050 }
1051 } # non-black/white list option, standand INVALID or DIRTY
1052 } # name lookup
1053 } # End of INVALID
1054
1055 elsif( my ($ip)=matchre_log($_, \@txt_noid, \@map_noid) )
1056 {
1057 if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; }
1058
1059 my @addrs=gethostbyname($ip);
1060 @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
1061 for $ip (@addrs) {
1062
1063 if( my ($idx)=matchre_ip($ip, \@ipre) ) # white/black list - ignore
1064 {
1065 if( $ipaction[$idx]== 0 ) { # white list - ignore
1066 syslog 'authpriv|notice',"No ssh id from white listed ip $ip, user $user";
1067 } else { # black list - instant block for default time
1068 if( $debug ) { syslog 'authpriv|notice',"DB:NOID: ip black listed, $ip"; }
1069 $expiretime{$ip}=$ttime+$maxblocktime;
1070 if( ! exists $explain{$ip} ) {
1071 $explain{$ip}="No ssh id from black listed ip, instant block of $ip";
1072 syslog 'authpriv|notice',$explain{$ip};
1073 emailonblock "INVALID", "$ip", "No ssh id from black listed ip, instant block.", $expiretime{$ip};
1074 adddrop $ip;
1075 }
1076 }
1077 } else { # non-exceptional NOID route
1078
1079 if( $debug ) { syslog 'authpriv|notice',"DB:NOID: ip=$ip"; }
1080
1081 my $maxc=$maxc_tags{"NOID"};
1082 my $btime=$btime_tags{"NOID"};
1083
1084 $chances{$ip}++;
1085 $expiretime{$ip}=$ttime+$btime;
1086
1087 if( $chances{$ip} > $maxc ) { # exceeded threshold, see if we need to add a DROP rule
1088 if( ! exists $explain{$ip} ) {
1089 $explain{$ip}="No ssh id string from client, blocking after $maxc chances";
1090 syslog 'authpriv|notice',"No ssh id string from client, blocking $ip after $maxc chances";
1091 emailonblock "NOID", "$ip", "No ssh id string from client, blocking after $maxc chances.", $ttime+$btime;
1092 adddrop $ip;
1093 }
1094 }
1095 else {
1096 syslog 'authpriv|notice',"Chanced missing ssh id string from $ip, $chances{$ip} guesses out of $maxc";
1097 }
1098 } # non-black/white list option, standand NOID
1099 } # name lookup
1100 } # End of NOID
1101
1102 # Failure from a genuine user
1103 elsif( my ($user, $ip)=matchre_log($_, \@txt_failval, \@map_failval) )
1104 {
1105 if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; }
1106
1107 my @addrs=gethostbyname($ip);
1108 @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
1109 for $ip (@addrs) {
1110
1111 if( my ($idx)=matchre_ip($ip, \@ipre) ) # white/black list - ignore
1112 {
1113 if( $ipaction[$idx]==0 ) { # white list - ignore
1114 syslog 'authpriv|notice',"Failure from valid user from white listed ip $ip, user $user";
1115 } else { # black list - instant block for default time
1116 if( $debug ) { syslog 'authpriv|notice',"DB:FAILVAL: ip black listed user=$user, ip=$ip"; }
1117 $expiretime{$ip}=$ttime+$maxblocktime;
1118 if( ! exists $explain{$ip} ) {
1119 $explain{$ip}="Failure from valid user on a black listed ip, instant block of $ip";
1120 syslog 'authpriv|notice',$explain{$ip};
1121 emailonblock "FAILVAL", "$ip", "Failure from a valid user on a black listed ip, instant block.", $expiretime{$ip};
1122 adddrop $ip;
1123 }
1124 }
1125 } else { # non-exceptional FAILVAL route
1126
1127 my $maxc=matchre_user2maxc($user, "DEFAULT");
1128 my $btime=matchre_user2btime($user, "DEFAULT");
1129
1130 if( $debug ) { syslog 'authpriv|notice',"DB:FAILVAL: user=$user, ip=$ip"; }
1131
1132 $chances{$ip}++;
1133 $expiretime{$ip}=$ttime+$btime;
1134
1135 if( $chances{$ip} > $maxc ) { # exceeded threshold, see if we need to add a DROP rule
1136 if( ! exists $explain{$ip} ) {
1137 $explain{$ip}="Valid user failed, blocking after $maxc chances";
1138 syslog 'authpriv|notice',"Valid user failed, blocking $ip after $maxc chances";
1139 emailonblock "$user", "$ip", "Valid user failed to login, blocking after $maxc chances.", $ttime+$btime;
1140 adddrop $ip;
1141 }
1142 } else {
1143 syslog 'authpriv|notice',"Chanced valid user name from $ip, $chances{$ip} guesses out of $maxc";
1144 }
1145 } # non-black/white list option, standand FAILVAL
1146 } # expand host names
1147 } # End of INVALID
1148
1149 # a success from an ip means removing it from the list, if it exists
1150 elsif( my ($user, $ip)=matchre_log($_, \@txt_accept, \@map_accept) )
1151 {
1152 if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; }
1153
1154 my @addrs=gethostbyname($ip);
1155 @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
1156 for $ip (@addrs) {
1157
1158 if( $debug ) { syslog 'authpriv|notice',"DB:ACCEPT: user=$user, ip=$ip"; }
1159
1160 if( defined $explain{$ip} ) { # this would happen if the SSHD chain didn't stop this connection (eg. wrong interface)
1161 my $reason=$explain{$ip};
1162 syslog 'authpriv|notice',"Valid login, cancelled $reason block from $ip";
1163 deldrop $ip;
1164 delete $explain{$ip};
1165 }
1166 delete $chances{$ip};
1167 } # expand host names
1168 }
1169
1170 # sshd quitting, 'Received signal'... more reliable than waiting for close of pipe
1171 elsif( my ($sig)=matchre_log($_, \@txt_quit, \@map_quit) )
1172 {
1173 if( $debug ) { syslog 'authpriv|notice',"DB:QUIT: signal=$sig"; }
1174
1175 syslog 'authpriv|notice',"sshd received signal $sig, closing sshdfilter";
1176
1177 $shouldexit=1;
1178 last;
1179 }
1180
1181 # pid to ip mapping, for dropbear.
1182 elsif( my ($pid,$ip)=matchre_log($_, \@txt_pid2ip, \@map_pid2ip) )
1183 {
1184 if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; }
1185
1186 if( $debug ) { syslog 'authpriv|notice',"DB:PID2IP: pid=$pid, ip=$ip"; }
1187
1188 $pid2ip{$pid}=$ip;
1189 }
1190
1191 # remove pid hash, for dropbear.
1192 elsif( my ($pid)=matchre_log($_, \@txt_pidexit, \@map_pidexit) )
1193 {
1194 if( $debug ) { syslog 'authpriv|notice',"DB:PIDEXIT: pid=$pid, stored ip=$pid2ip{$ip}"; }
1195
1196 delete $pid2ip{$pid};
1197 }
1198
1199 # Ignore 'Connection closed' type messages
1200
1201 } # end of for(), to instanciate $_
1202 } # end of handlesshdlines
1203
1204
1205 # build a line that contains $maxhist lines worth of previous input lines, or as many as currently available
1206 sub handlesshdoutput
1207 {
1208 for (@_) {
1209 my $aline=$_;
1210
1211 # first, add the new line to the history
1212 $loghist{$curhist}=$aline;
1213
1214 if( $logsource eq "STDIN" ) { # log sshd output only if we started it.
1215 print $SSHDLOGGER "$aline\n";
1216 $SSHDLOGGER->autoflush(1);
1217 }
1218
1219 if( $debug>=3){ syslog 'authpriv|notice',"DB:incoming=$loghist{$curhist}"; }
1220 my $idx=$curhist++;
1221 if( $curhist>=$maxhist )
1222 { $curhist=0; }
1223 # position history counter to oldest entry
1224 my $sidx=$curhist;
1225 for(my $i=0;$i<$maxhist && !exists $loghist{$sidx};$i++)
1226 {
1227 $sidx++;
1228 if($sidx>=$maxhist){ $sidx=0; }
1229 }
1230
1231 if( $debug>=3 ){ syslog 'authpriv|notice',"DB:curhist=$curhist, idx=$idx, sidx=$sidx"; }
1232
1233 for(my $i=0;$i<$maxhist;$i++)
1234 {
1235 if( $debug>=4 ){ syslog 'authpriv|notice',"DB:loghist[$i]=$loghist{$i}"; }
1236 $sidx++;
1237 if($sidx>=$maxhist){ $sidx=0; }
1238 }
1239
1240 # and then build a line. This could be done by trimming and extending a single string... would it be any more readable?
1241 my $bufline="";
1242 my $nl="";
1243 do
1244 {
1245 if( $sidx>=$maxhist ){ $sidx=0; }
1246 if( $debug>=4 ){ syslog 'authpriv|notice',"DB:loop[$sidx]=$loghist{$sidx}"; }
1247 $bufline.=$nl.$loghist{$sidx};
1248 $nl="\n";
1249 } while( $sidx++!=$idx );
1250
1251 if( $debug>=3 ){ syslog 'authpriv|notice',"DB:aline=$bufline"; }
1252 handlesshdlines($bufline);
1253 }
1254 }
1255
1256
1257 if( $logsource eq "STDIN" ){
1258 syslog 'authpriv|notice',"sshd quit, closing sshdfilter";
1259 } else {
1260 syslog 'authpriv|notice',"closing sshdfilter";
1261 }
1262
1263 # Reap any zombies (which could be created when sending emails)
1264 # This might also help with those rare occasions when '/etc/init.d/sshd restart' mysteriously fails.
1265 my $kid;
1266 do {
1267 $kid = waitpid(-1, WNOHANG);
1268 } until $kid <= 0;
1269
1270 close($SSHDHANDLE);
1271 close $SSHDLOGGER;
1272 if( defined $pid ){ waitpid($pid,0); }
1273
1274 closelog();
1275
1276 unlink("/var/run/sshdfilter.pid.$chain");
1277
1278 # the end
1279 exit;
1280