#!/usr/bin/python
'''\
%prog [options] <pipe>

  We watch the input from <pipe> for ssh login errors. If we encounter 
  SCAN_COUNT errors in SCAN_TIME, we block the IP address making the 
  connection attempts for RELEASE_TIME seconds.

  To create <pipe>, run:
    # mknod p /path/to/pipe

  Then edit either syslog.conf or syslog-ng.conf as follows:
    syslog.conf:
      auth.info;authpriv.info			|/path/to/pipe

    syslog-ng.conf:
      destination fwssh { pipe("/path/to/pipe" group (tty) perm(0600)); };'''

import os, re, select, sys, time
from optparse import OptionParser

def ipfw(cmd):
  '''Interface to os.system(iptables_cmd) that logs to syslog'''
  # print time.ctime(), "iptables", cmd
  r = os.system("iptables %s | logger -p auth.info" % cmd)
  if r != 0:
    sys.stderr.write("  error %d\n" % r)

def main():
  '''Grunt workers apply here!'''
  addrs = {}

  parser = OptionParser(usage=__doc__, version="%prog v0.1")
  parser.add_option("-c", "--scan-count", type="int", dest="SCAN_COUNT", 
		default=3, metavar="<secs>", 
		help="Override the value of SCAN_COUNT.")
  parser.add_option("-t", "--scan-time", type="int", dest="SCAN_TIME", 
		default=60, metavar="<count>", 
		help="Override the value of SCAN_TIME.")
  parser.add_option("-r", "--release-time", type="int", dest="RELEASE_TIME", 
		default=900, metavar="<secs>", 
		help="Override the value of RELEASE_TIME")
  parser.add_option("-d", "--debug", action="store_true", dest="DEBUG",
default=False, help="Enable debug mode")

  (options, FILE) = parser.parse_args()

  if (len(FILE) < 1):
    parser.print_help()
    sys.exit(1)

  try:
    pipe = open(FILE[0])
  except IOError, (errno, strerror):
    print "Unable to open '"+ FILE[0] + "':", strerror
    sys.exit(1)

  while True:
    ready = select.select([pipe.fileno()], [], [], 60)
    now = time.time()
    if len(ready[0]) > 0:
      m = None
      s = pipe.readline()
      m = re.search("(Invalid user|Illegal user|authentication error) .* from ([.0-9]+$)", s)
      if m:
	if options.DEBUG:
	  sys.stderr.write("Match found for address: %s\n" % m.group(2))
        addr = m.group(2)
        if addr not in addrs:
          addrs[addr] = {'times': []}
          addrs[addr]['time'] = 0

        addrs[addr]['times'] += [now]
        if addrs[addr]['time'] == 0 and len(addrs[addr]['times']) >= options.SCAN_COUNT:
          addrs[addr]['time'] = now
          if options.DEBUG:
            sys.stderr.write("DROPping %s\n" % addr)
          ipfw("-I INPUT -s %s -j DROP" % addr)
    else:
      for a in addrs.keys():
        while len(addrs[a]['times']) > 0 and addrs[a]['times'][0] < now - options.SCAN_TIME:
          addrs[a]['times'].pop()

        if addrs[a]['time'] != 0 and now >= addrs[a]['time'] + options.RELEASE_TIME:
          if options.DEBUG:
            sys.stderr.write("CLEARing %s\n" % a)
          ipfw("-D INPUT -s %s -j DROP" % a)
          del addrs[a]

if __name__ == '__main__':
  main()

