#!/usr/local/bin/python ########################################################## import sys,os,commands,gzip,re,string,syslog,signal,bsddb #import bsddb3 as bsddb minhits = 4 minpage = 99 whitelist = re.compile(':.hold:|Policy Rejection|Address verification in progress|connect from|Greylisted |\]: 450 4\.|stat=Deferred|timeout|timed.out|ay=bounce@localh|Domain.blocked| reject_warning: |\[127\.0\.0\.') ## leave blank if nomail/nopage: mailto = '' pageto = 'admin.pager' maxMailtoBody = 4 # lines ## NOTE: logs are not checked for duplicate lines... global runningLog runningLog = '/var/log/mail.messages' recentLogs = runningLog,'/var/log/mail.messages.0.gz' recentLogs = '' ## 0=off, 1=warnings, 2=filtered, 3=spammed, 4=all (>1 prints to stdout w/o filtering) debug = 0 ## run continuously, requires http://code.google.com/p/pytailer/, else 1 pass ## _requires_ a sighup when the log file is rotated... (see system rotatelog cron script) daemonMode = True ############################################################### ## TBD: # # daemon.py class Daemon # # add command-line flag support: # -h, --help # -v (version, from RCS) # -d[0-4] (int, override debug) # -1 (one-pass, overriding daemonMode) # # periodic cidr/24 test (SubnetTree||iptree||iplib|...) # add netblock when >N filtered in subnet/24 # re.search('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(|\/\d{1,2})$', address) # update initSpamSrcDB / egrep ipfw # split whitelistRE & whitelistIP # # check for log file rotation # else (continue to) send SIGHUP when: # 1) ipfw rules are expired, # or 2) the maillog is rotated: # if [ "$i" = "$LOGDIR/mail.messages" ]; then # IDSPID="`ps auxww|grep 'python.*cksmtprej'|egrep -v '(grep| vi )'|awk '{ print $2 }'`" # if [ "$IDSPID" != "" ]; then # kill -1 $IDSPID # logger "sent sighup to cksmtprej" # fi # fi # ############################################################### isSpam = re.compile(' reject: | discard: | Illegal address syntax from ') isRBLd = (']: 5[0-9]{2} 5.[0-9].[0-9] Service unavailable; Client host \[.*\] blocked using ') isIPaddress = re.compile('^[1-9][0-9]{0,2}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$') spamSrcDB = bsddb.btopen(None) # [ip][count] host = commands.getoutput("uname -n|awk -F. '{print $1}'") myName = os.path.basename(sys.argv[0]) try: import tailer except: print(' ERROR (' + myName + '): \"tailer\" module not found. See: http://code.google.com/p/pytailer') sys.exit() def main(): syslog.openlog(myName, syslog.LOG_PID, syslog.LOG_LOCAL7) exitIfAlreadyRunning() initSpamSrcDB() if len(recentLogs) > 0: for logfile in recentLogs: parseMailLogFile(logfile) if daemonMode: signal.signal(signal.SIGHUP, tailMLogFile) tailMLogFile(None, None) syslog.closelog() def tailMLogFile(signal, frame): ## both args ignored but required by python signal syntax if not daemonMode or not runningLog: return elif not os.path.isfile(runningLog): print ('\n ' + runningLog + ' log not found...\n') return try: runningLog.close() # if HUPed, not really required w/ 'tailer' if debug == 0: syslog.syslog('re-tailing ' + runningLog + '...') elif debug >= 1: print('\n re-tailing ' + runningLog + '...\n') initSpamSrcDB() # reinit after hup/expires for line in tailer.follow(open(runningLog), delay=30.0): checkPostfixLogEntry(line) except: if debug == 0: syslog.syslog('tailing ' + runningLog + '...') elif debug >= 1: print('\n tailing ' + runningLog + '...\n') initSpamSrcDB() # reinit after hup/expires for line in tailer.follow(open(runningLog), delay=30.0): checkPostfixLogEntry(line) runningLog.close() # should never reach here syslog.syslog('could not tail ' + runningLog) def parseMailLogFile(maillog): if not maillog: return if not os.path.isfile(maillog): print (' ' + maillog + ' log not found...\n') return elif debug >= 2: print ('\n parsing ' + maillog + '\n') elif debug == 0: syslog.syslog('parsing ' + maillog + '...') try: # if py>=2.5: logfile = hook_compressed(maillog, 'r') if maillog.endswith('.gz'): logfile = gzip.open(maillog, 'r') else: logfile = open(maillog, 'r') for line in logfile.readlines(): checkPostfixLogEntry(line) logfile.close() except IOError: syslog.syslog('could not read ' + logfile) def checkPostfixLogEntry(line): if not re.search(isSpam, line): return elif re.search(whitelist, line): return else: ## got a 5XX reject: TBD tighten rule, add multi 4XX as well (post-postgrey) ip = re.sub('^.*\[', '', re.sub('\](; from=|: 5[0-9][0-9] 5\.[0-9]).*$', '', line, 1), 1).strip() if debug >= 4: print(' checking ' + ip + ':') if not re.search(isIPaddress, ip): if debug >= 1: print(' WARNING: ' + ip + ' is not an IP address ...') if ip in filteredDB: if debug >= 3: print(' ' + ip + ' already filtered') elif not ip in spamSrcDB: spamSrcDB[ip] = '1' if debug >= 3: print(' ' + ip + ' added to db') else: spamSrcDB[ip] = str(int(spamSrcDB[ip]) + 1) if debug >= 3: print(' ' + ip + ' hits incremented to ' + spamSrcDB[ip]) if int(spamSrcDB[ip]) >= minhits: addFilter(ip, spamSrcDB[ip]) elif re.search(isRBLd, line): ## 2 hits if listed on an RBL (TBD: check whether 1 RBL hit is common) if debug >= 3: print(' ' + ip + ' isRBLd') addFilter(ip, spamSrcDB[ip]) def initSpamSrcDB(): global filteredDB filteredDB = ['127.0.0.2'] # primer, in case of empty logs if debug > 0: print(' initializing db') for ip in commands.getoutput("ipfw list|egrep 'deny (ip from.*to any|" + "tcp from.*dst-port 25)'|awk '{print $7}'|sort -u").splitlines(): if re.search(isIPaddress, ip): filteredDB.append(ip) if debug > 0: print(' adding ' + str(ip) + ' to db') def addFilter(ip, count): ip_ptr = commands.getoutput("dig -x " + ip + "|grep PTR|grep -v ^\;|head -1|awk '{ print $NF }'|sed 's/\.$//'") if not ip_ptr == '': ip_ptr = '(' + ip_ptr + ')' if debug >= 2: msg = (ip + ip_ptr + ' would be blackholed after ' + count + ' SMTP failures @ ' + host) print(' ' + msg) print(' ipfw add ' + getIpfwRuleNumber() + ' deny tcp from ' + ip + ' to any 25 2>/dev/null >/dev/null') #print(' ipfw add deny tcp from ' + ip + ' to any 25 2>/dev/null >/dev/null') print(' ' + 'mail -s "' + msg + '" ' + mailto + ' = minpage and pageto is not '': print(' ' + 'mail -s "' + msg + '" ' + pageto + ' /dev/null >/dev/null') #commands.getoutput('ipfw add deny tcp from ' + ip + ' to any 25 2>/dev/null >/dev/null') msg = (ip + ip_ptr + ' blackholed after ' + count + ' SMTP failures @ ' + host) syslog.syslog(msg) sendLogs(ip, msg) if int(count) >= minpage and pageto is not '': commands.getoutput('mail -s "' + msg + '" ' + pageto + ' 1 ): if debug > 0: print('\n running (x' + numRunning + ') in debug mode') else: syslog.syslog('already running (x' + numRunning + ')') syslog.closelog() sys.exit() def sendLogs(ip, msg): if mailto is not '': tmp = '/tmp/.' + myName + str(os.getpid()) commands.getoutput("rm -f " + tmp) for file in recentLogs: commands.getoutput("zegrep -h 'postfix.* (discard|reject): .*\[" + ip + "]' " + file + " >>" + tmp) commands.getoutput("sort -Mr " + tmp + " | head -" + str(maxMailtoBody) + " | mail -s '" + msg + "' " + mailto + " ; rm -f " + tmp) if debug >= 3: print(' ' + 'logs mailed...') def getIpfwRuleNumber(): rule=1025 while ( rule < 65534 ): if commands.getstatusoutput("/sbin/ipfw show " + str(rule))[0]: return str(rule) else: rule += 1 return '65534' if __name__ == "__main__": main()