Updated 2011-07-22 13:02:49 by RLE

rdt 2006.08.31 I have been using this tcl script for some time now to monitor the ssh daemon and automatically drop packets from IP address based upon several criteria. It is freely given here for your use. Please feel free to comment upon the code and how it may be improved:

External programs used are tail, iptables, and dig.

rdt What does it do? Well, it scans a log file (/var/log/secure, or /var/log/auth.log in my case) for attempts to login (using 'tail -f') and (based upon configuration of rates and such) adds a rule to the iptables filter to completely DROP all packets from the IP address. And (after a configurable time - default of 1 hour) then deletes that rule to allow traffic.

I have polished this package up a little and posted a description here: ip-drop - JBR
 #!/bin/sh
 # This line continues for Tcl, but is a single line for 'sh' \
 exec tclsh "$0" ${1+"$@"}
 ########################################################################
 # tcl/tk code follows:
 #=======================================================================
 # Configuration:
 #-----------------------------------------------------------------------
 # set the maximum number of attempts for various types:
 #   iuf = "Illegal User From"
 #   fpi = "Failed Password from Illegal user"
 #   fpu = "Failed Password from User"
 #   fpr = "Failed Password from Root"
 array set cfg { iuf 4  fpi 3  fpu 9  fpr 2 }
 #-----------------------------------------------------------------------
 # set how long an IP should remain DROPed:
 array set cfg { delay 1h }
 #-----------------------------------------------------------------------
 # establish hosts / IP's that should NEVER be dropped (see ckIgnore):
 #   read a file for values:
 #set cfg(ignore) /root/ad/ad.ign
 #   provide a list of hostnames/IP's:
 #set cfg(ignore) [list host.example.com 192.168.144.120 ]
 #-----------------------------------------------------------------------
 # define the log file to scan for input:
 set ipt(file)        "/var/log/secure"
 # define the log file to write:
 set ipt(log)        "/var/log/autoDrop"
 #=======================================================================
 # Colors (used for debugging):
 #        0 is black,        1 is red,        2 is green,        3 is yellow,
 #        4 is blue,        5 is magenta,        6 is cyan,        7 is white.
 set ipt(norm)        "\033\[0m"                ;# normal
 set ipt(dbg)        "\033\[34m"                ;# debug in blue
 set ipt(dat)        "\033\[36m"                ;# data in cyan
 set ipt(err)        "\033\[0;31m"                ;# error in red
 #=======================================================================
 if 0 {        set DEBUG 1
         set ipt(file) "./secure"
         set ipt(log)  "./log"
 }
 ########################################################################
 proc Debug {beg str} {
     global ipt
     if {[info exists ::DEBUG]} {
         puts stderr "$ipt($beg)$str$ipt(norm)"
     }
 } ;# end of Debug
 #-----------------------------------------------------------------------
 proc error {str} {Debug err $str}
 proc debug {str} {Debug dbg $str}
 proc data  {str} {Debug dat $str}
 #-----------------------------------------------------------------------
 proc log {str} {
     global ipt
     set dt [clock format [clock seconds] -format "%h %d %T" -gmt 0]
     set fh [open $ipt(log) a]
     puts $fh "$dt $str"
     catch {close $fh}
 } ;# end of log
 #-----------------------------------------------------------------------
 # Allow the ms parameter to end with s for seconds, m for minutes,
 #        h for hours, & d for days.
 proc After {ms args} {
     if {[regexp {^(\d+)([smhd])} $ms -> ms suffix]} {
         switch -exact -- $suffix {
             s {set ms [expr $ms * 1000]}
             m {set ms [expr $ms * 1000*60]}
             h {set ms [expr $ms * 1000*60*60]}
             d {set ms [expr $ms * 1000*60*60*24]}
         }
     }
     after $ms $args
 } ;# end of After
 #=======================================================================
 #        Typical pop lines:
 # Jul 19 19:24:35 sb xinetd[1957]: START: pop3s pid=10806 from=70.181.95.115
 # Jul 19 19:24:36 sb xinetd[1957]: EXIT: pop3s pid=10806 duration=1(sec)
 proc got.pop3s {d h dp rest} {
     global ipt
     if {[lindex $rest 0] == "START:"} {
         set exp1 {from=(\S+)}
         if {[regexp $exp1 $rest -> ip]} {
             if {[info exists ipt(ignore)]} {
                 foreach item $ipt(ignore) {
                     if {$item == $ip} {return}
                 }
             }
             log " pop $ip"
         }
     }
 } ;# end of got.pop3s
 #-----------------------------------------------------------------------
 #        Typical bad user line:
 # Jul 19 19:22:09 sb sshd[10740]: Illegal user win from 213.193.225.13
 proc got.Illegal {d h dp rest} {
     global ipt
     set exp1 {Illegal user (\S+) from (\S+)}
     if 0 {
     } elseif {[regexp $exp1 $rest -> user ip]} {
         set ipt(iuf) "Illegal user from"
         on.ip iuf $ip $user
     } else {
         error "got.Illegal($d : $rest)"
     }
 } ;# end of got.Illegal
 #-----------------------------------------------------------------------
 #        Typical bad root attempt:
 # Jul 19 19:17:18 sb sshd[10304]: Failed password for root from 60.48.155.19 port 60406 ssh2
 # Jul 19 19:17:18 sb sshd[10304]: Failed password for illegal user junk from 60.48.155.19 port 60406 ssh2
 proc got.Failed {d h dp rest} {
     global ipt
     set exp1 {Failed password for illegal user (\S+) from (\S+)}
     set exp2 {Failed password for (\S+) from (\S+)}
     set exp3 {^\D+([0-9\.]+)\s+.*}
     if 0 {
     } elseif {[regexp $exp1 $rest -> user ip]} {
         set ipt(fpi) "Failed password from illegal user"
         on.ip fpi $ip $user
     } elseif {[regexp $exp2 $rest -> user ip]} {
         set ipt(fpu) "Failed password from user"
         on.ip [expr {$user == "root" ? "fpr" : "fpu"}] $ip $user
     } elseif {[regexp $exp3 $rest -> ip]} {
         error "1 got.Failed($d : $rest)"
         set ipt(fpz) "Failed password regexp"
         on.ip fpz $ip
     } else {
         error "2 got.Failed($d : $rest)"
     }
 } ;# end of got.Failed
 #-----------------------------------------------------------------------
 #        Typical happening after the host has been droped:
 # Jul 19 19:24:18 sb sshd[10750] fatal: Timeout before authentication for 213.193.225.13
 proc got.fatal {d h dp rest} {
     global ipt
     set ip  [lindex $rest end]
     set exp1 {fatal: Timeout before authentication for (\S+)}
     if 0 {
     } elseif {[regexp $exp1 $rest -> user ip]} {
     } else {
         error "$got.fatal($d $rest)"
     }
 } ;# end of got.fatal
 #-----------------------------------------------------------------------
 #        Typical 'Did not' message:
 # Jul 19 18:28:56 sb sshd[8840]: Did not receive identification string from 213.193.225.13
 proc got.Did {d h dp rest} {
     global ipt
     set ip  [lindex $rest end]
     set exp "Did not receive identification string "
     if {![regexp $exp $rest]} {
         error $rest
     }
 } ;# end of got.Did
 #=======================================================================
 proc iptables {act ip} {
     global ipt
     set table "INPUT"
     switch -exact -- $act {
         del        { set opt -D }
         default        { set opt -I }
     }
     log " $act $ip"
     if {[catch {exec iptables $opt $table -s $ip -j DROP} res]} {
         log "  exec of 'iptables $opt $table -s $ip' NOT started"
     }
 } ;# end of iptables
 #-----------------------------------------------------------------------
 proc accept {ip} {
     global drop
     if {[info exists drop($ip,st)] && [info exists drop($ip,id)]} {
         after cancel $drop($ip,id)
         # call iptables to issue an accept command for $ip.
         iptables del $ip
     }
 } ;# end of accept
 #-----------------------------------------------------------------------
 proc drop {ip tp} {
     global drop db cfg 
     if {![info exists drop($ip,st)]} {
         data [format "drop: %15s  %s:%s"  $ip $tp $cfg($tp)]
         set drop($ip,st) 1
         # call iptables to issue a drop command for $ip, then:
         iptables $tp $ip
         if {![info exists cfg(delay)]} {set cfg(delay) 1h}
         set drop($ip,id) [After $cfg(delay) accept $ip]
     }
 } ;# end of drop
 #-----------------------------------------------------------------------
 proc expire {index} {
     global db
     if {$db($index) > 1} { ;# yes leave it at one, not zero.
         incr db($index) -1
     }
 } ;# end of expire
 #-----------------------------------------------------------------------
 proc on.ip {type ip {user {}}} {
     global db cfg ipt
 
     regsub {::ffff:} $ip "" ip
     if {[info exists ipt(ignore)]} {
         foreach item $ipt(ignore) {
             if {$ip == $item} {return}
         }
     }
     if {![info exists db($ip,$type)]} {
         set db($ip,$type) 0
     }
     incr db($ip,$type)
     if {[info exists cfg($type)] && $db($ip,$type) >= $cfg($type)} {
         drop $ip $type
     }
     After 60s expire $ip,$type
 } ;# end of on.ip
 #=======================================================================
 proc rdHandler {fh} {
     global ipt
     if {[eof $fh]} {
         set ipt(end) "eof"
     } elseif {[gets $fh line] != -1} {
         # split the line into: date, host, daemon/port, theRest:
         set exp1 {^\s*(\S+\s+\d+\s+\S+)\s+(\S+)\s+(\S+):\s+(.*)}
         if {![regexp $exp1 $line -> d h dp rest]} {
             error $line
         } else {
             switch -exact -- [lindex $rest 0] {
                 START: -
                 EXIT:        { got.pop3s        $d $h $dp $rest }
                 Illegal        { got.Illegal        $d $h $dp $rest }
                 Failed        { got.Failed        $d $h $dp $rest }
                 fatal:        { got.fatal        $d $h $dp $rest }
                 Did        { got.Did        $d $h $dp $rest }
                 default        { ;# not yet handled:
                     error "$d | $h | $dp | $rest"
                 }
             }
         }
     } else {
         set ipt(end) -1
     }
 } ;# end of rdHandler
 #-----------------------------------------------------------------------
 proc show {} {
     global db
     set types [list iuf fpi fpu fpr]
     for {set i 0} {$i < [llength $types]} {incr i} {
         set n [lindex $types $i]
         set $n $i
     }
     foreach i [array names db] {
         set ip [lindex [split $i ,] 0]
         set tmp($ip) [list 0 0 0 0]
     }
     foreach t $types {
         foreach i [array names db *,$t] {
             set it [split $i ,]
             set ip [lindex $it 0]
             set tp [lindex $it 1]
             set tmp($ip) [lreplace $tmp($ip) [set $tp] [set $tp] $db($i)]
         }
     }
     debug "\n     IP Address   iuf  fpi  fpu  fpr"
     foreach name [array names tmp] {
         foreach {a b c d} $tmp($name) {break}
         debug [format "%15s  %4d %4d %4d %4d" $name $a $b $c $d]
     }
     debug ""
 } ;# end of show
 #-----------------------------------------------------------------------
 proc ckFile {{how 0}} {
     global ipt
     file stat $ipt(file) stat
     set inode $stat(ino)
     if {![info exists ipt(inode)]} {set ipt(inode) ""}
     if {$ipt(inode) != $inode} {set ipt(end) "new inode"}
     set ipt(inode) $inode
     After 60s ckFile 1
 } ;# end of ckFile
 #=======================================================================
 proc tailFile {} {
     global ipt
     ckFile
     while 1 {
         if {[catch {set fh [open "|tail -f $ipt(file)" r]} out]} {
             error "can NOT start '$out'"
         }
         set ipt(end) 0
         fconfigure $fh -blocking 0 -buffering line
         fileevent  $fh readable [list rdHandler $fh]
         vwait ipt(end)
         log "Reopen $ipt(file) was $out"
         catch {close $fh}
         catch {exec kill -HUP $out}
         After 1s catch {exec kill -HUP $out}
     }
 } ;# end of tailFile
 #-----------------------------------------------------------------------
 proc ckIgnore {} {
     global cfg ipt
 
     set ignore [set todo [list]]
     if {[info exists cfg(ignore)]} {set todo $cfg(ignore)}
     while {$todo != "" || [llength $todo] > 0} {
         set doing [lindex $todo 0]
         set todo  [lrange $todo 1 end]
         if {[string index $doing 0] == {\{}} {        
             ;# if it starts with an open brace, assume regexp,
         } elseif {[regexp {^\d+} $doing]} {        
             lappend ignore $doing        ;# if its an ip, ignore that,
         } elseif {[regexp {/} $doing]} {        
             catch {                        ;# filename if it contains a /
                 set fh [open $doing r]
                 set d1 [read $fh]
                 close $fh
                 foreach item [split $d1 \n] {
                     if {$item != ""} {lappend todo $item}
                 }
             }
         } else {                        ;# otherwise assume host name.
             catch {
                 set fh [open "|dig +sh $doing" r]
                 set d1 [read $fh]
                 close $fh
             }
             foreach item [split $d1 \n] {
                 if {$item != ""} {lappend todo $item}
             }
             catch {exec ""}
         }
     }
     set ipt(ignore) $ignore
     set out [join $ignore ", "]
     log " ignore $out"
     After 1d ckIgnore
 } ;# end of ckIgnore
 #=======================================================================
 ckIgnore
 tailFile
 #show
 ########################################################################
 # End