#!/usr/bin/perl # # $Header: /home/anoncvs/auto-spam-ip-blocker/auto-spam-ip-blocker.pl,v 1.40 2024/05/29 19:19:56 bigby Exp $ # # # (C) 2018, Joseph Robinson # use Getopt::Std; use Sys::Syslog; # models: # Sep 2 00:05:28 ns1 sm-mta[23987]: t8275RkA023987: from=, size=8014, class=0, nrcpts=1, msgid=, proto=SMTP, daemon=MTA, relay=mademoiselledenoirs.com [207.244.75.223] (may be forged) # # or # Sep 3 03:12:35 ns1 sm-mta[40046]: t83ACWr0040046: from=, size=13361, class=0, nrcpts=1, msgid=<37992625252093799115821739796776@9m68gz9hh.smellingg.info>, proto=ESMTP, daemon=MTA, relay=[146.0.72.71] # # and # # Sep 2 00:00:22 localhost spamd[18249]: spamd: result: . -2 - BAYES_00,RCVD_IN_DNSWL_LOW scantime=0.2,size=2782,user=jdaniel,uid=3100,required_score=5.0,rhost=localhost,raddr=127.0.0.1,rport=36960,mid=<236681.363402994-sendEmail@cg3>,bayes=0.000000,autolearn=disabled # or # Sep 2 00:04:18 localhost spamd[18249]: spamd: result: Y 30 - BAYES_99,BAYES_999,EMPTY_MESSAGE,INVALID_MSGID,MISSING_SUBJECT,MSGID_SHORT,RCVD_DOUBLE_IP_SPAM,RCVD_HELO_IP_MISMATCH,RCVD_IN_BL_SPAMCOP_NET,RCVD_IN_BRBL_LASTEXT,RCVD_IN_PSBL,RCVD_IN_RP_RNBL,RCVD_IN_SBL_CSS,RCVD_IN_SORBS_WEB,RCVD_IN_XBL,RCVD_NUMERIC_HELO,RDNS_NONE,TT_MSGID_TRUNC,T_FSL_HELO_BARE_IP_2,UNCLOSED_BRACKET scantime=0.1,size=678,user=atolsma,uid=3007,required_score=5.0,rhost=localhost,raddr=127.0.0.1,rport=37195,mid=,bayes=1.000000,autolearn=disabled ############# # VARIABLES # ############# $DEBUG=0; $LIMIT=2; $KEYFILE="/etc/named/keys/Kephemeron+157+00000.key"; $LOGFILE="/var/log/all.log"; $DOMAIN="block.ephemeron.org"; $WHITELISTFILE="/etc/mail/whitelist"; $BASENAME=$0; $BASENAME=~s/.*\///; $SYSLOG_TAG=$BASENAME; $SYSLOG_FACILITY="LOG_MAIL"; $PIDFILE="/var/run/$BASENAME.pid"; ############### # SUBROUTINES # ############### sub debug { if ($DEBUG) { print "debug:: "; print pop(); print "\n"; } } sub logger { my $MSG; $MSG=pop(); if ($DEBUG) { debug($MSG); } else { syslog("info",$MSG); } } sub cleanup { closelog(); close(LOG); unlink($PIDFILE); } sub daemondie { my $MSG; $MSG="ERROR: " . pop(); logger($MSG); logger("exiting"); cleanup; exit(1); } sub usage { print stderr "Usage:\nauto-spam-ip-blocker.pl [-h] [-d] [-s] [-l logfile] [-k keyfile] [-p pidfile] [-c num] [-w whitelistfile]\n"; print stderr "\t-h\thelp\n"; print stderr "\t-d\tenable debugging\n"; print stderr "\t-D DOMAIN\tdomain to add blacklist entries to\n"; print stderr "\t-l logfile\tspecify log file to read messages from\n"; print stderr "\t-k keyfile\tspecify nsupdate key file\n"; print stderr "\t-p pidfile\tspecify alternate PID file\n"; print stderr "\t-c num\tspecify number of spam messages at which hosts are blocked (default=2)\n"; print stderr "\t-w whitelistfile\tspecify alternate whitelist file\n"; print stderr "\t-s\tread from stdin instead of from logfile\n"; exit(pop); } sub process_options { if ( ! getopts('D:shdl:k:p:c:w:') ) { # print "unknown option!\n"; usage(1); } if ($opt_D) { $DOMAIN=$opt_D; debug("DOMAIN set to $DOMAIN"); } if ($opt_d) { $DEBUG=1; closelog(); debug("debugging on."); } if ($opt_h) { usage(0); } if ($opt_k) { $KEYFILE=$opt_k; debug("KEYFILE set to $KEYFILE"); } if ($opt_w) { $WHITELISTFILE=$opt_w; debug("WHITELISTFILE set to $WHITELISTFILE"); } if ($opt_l) { if ($opt_s) { logger("-s specified, ignoring -l"); } else { $LOGFILE=$opt_l; debug("LOGFILE set to $LOGFILE"); } } if ($opt_s) { $LOGFILE="STDIN"; debug("LOGFILE set to STDIN"); } if ($opt_p) { $PIDFILE=$opt_p; debug("PIDFILE set to $PIDFILE"); } if ($opt_c) { $LIMIT=$opt_c; debug("LIMIT set to $LIMIT"); } } sub sanity_check_keyfile { -e $KEYFILE || daemondie "KEYFILE \"$KEYFILE\" DOESN'T EXIST"; -f $KEYFILE || daemondie "KEYFILE \"$KEYFILE\" ISN'T REGULAR FILE"; -r $KEYFILE || daemondie "CAN'T READ KEYFILE \"$KEYFILE\""; } sub sanity_check_domain { if (! (defined($DOMAIN)) ) { daemondie "NEED DOMAIN SET"; } } sub revip { my $garbage; $garbage=pop(); if ($garbage=~/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/) { return("$4.$3.$2.$1"); } } sub read_whitelist { open(WLFFH,"$WHITELISTFILE") || daemondie "CAN'T OPEN WHITELISTFILE \"$WHITELISTFILE\" FOR READING: $!"; undef(%WHITELIST); while() { chomp; ~s/\#.*//; if (/\W/) { $WHITELIST{$_}=1; } } close(WLFFH); } sub ip_is_in_whitelist { my $host; $host=pop; # our IP spaces if ($host=~/^(127|10)\./) { logger("IP $host looks like one of ours"); return(1); } # google/gmail if ($host=~/^(209\.85|74\.125\.)/) { logger("IP $host looks like a gmail addess"); return(1); } if ($WHITELIST{$host}) { logger("IP $host is explicitly in whitelist"); return(1); } foreach $key (keys %WHITELIST) { if ($key=~/\.$/ and $host=~/^"$key"/) { logger("IP $host matches whitelist entry \"$key\""); return(1); } } return(0); } sub block { debug("block:: starting."); my ($reverse,$ipaddress); $ipaddress=pop; $reverse=revip($ipaddress); debug("block:: called with argument $ipaddress."); if (ip_is_in_whitelist($ipaddress)) { logger("ip $ipaddress is in whitelist, not blocking"); } else { if ($DEBUG) { debug("debugging mode on, not blocking $ipaddress"); } else { logger("adding blacklist entry for $ipaddress"); open(NSUPDATE,"|nsupdate -k $KEYFILE") || daemondie "CAN'T OPEN NSUPDATE PROGRAM: $!"; print NSUPDATE "zone $DOMAIN.\n"; print NSUPDATE "update add $reverse.$DOMAIN. 3600 IN A 127.0.0.1\n"; print NSUPDATE "send\n"; close(NSUPDATE); } } debug("block:: finished."); } sub open_logfile { if ($LOGFILE eq "STDIN") { open(LOG,">&STDIN") || daemondie "CAN'T OPEN STDIN: $!"; } else { $OLDINODE=(stat($LOGFILE))[1]; open(LOG,$LOGFILE) || daemondie "CAN'T OPEN LOGFILE \"$LOGFILE\": $!"; } } sub daemonize { my $VAL; if ($DEBUG || $LOGFILE eq "STDIN") { debug("debug mode on, not forking into background."); return; } if (! (defined($VAL=fork()))) { die "COULDN'T FORK: $!"; } if ($VAL>0) { # parent exit(0); } if ($VAL==0) { # child close(stdout); close(stdin); close(stderr); logger("forked into background"); } } sub check_pidfile { if ( -e $PIDFILE ) { $MSG="PIDFILE \"$PIDFILE\" EXISTS, EXITING"; logger($MSG); die $MSG; } } sub write_pidfile { open(PFH,">$PIDFILE") || die "CAN'T OPEN PIDFILE \"$PIDFILE\" FOR WRITING: $!"; print PFH "$$\n"; close(PFH); } sub handle_hup { logger("HUP received, re-reading whitelist"); read_whitelist; logger("whitelist reloaded"); } sub handle_term { logger("SIGTERM received, exiting"); cleanup; exit(0); } sub handle_int { logger("SIGINT received, exiting"); cleanup; exit(0); } sub install_handlers { $SIG{TERM}="handle_term"; $SIG{HUP}="handle_hup"; $SIG{INT}="handle_int"; } sub process_line { my $LINE=pop; # Mar 14 00:06:38 ephemeron.org sm-mta[24088]: x2E76awU024088: from=, size=3319, class=0, nrcpts=1, msgid=<0F68CA60.562B306D@vsi.ru>, proto=ESMTP, daemon=MTA, relay=ws10.tixon.vsi.ru [80.82.33.170] # Mar 14 00:06:38 ephemeron.org sm-mta[24089]: x2E76awU024088: to="|/usr/local/bin/procmail UMASK=033 DEFAULT=/home/spam/Maildir/ /dev/null", ctladdr= (26/0), delay=00:00:01, xdelay=00:00:00, mailer=prog, pri=33704, dsn=2.0.0, stat=Sent if ($LINE=~/sm-mta\[[0-9]*\]: ([^:]*): from=[^,]*, size=[0-9]*,.*relay=[^[]*\[([^]]*)\]/) { my $MID; $MID=$1; $IP{$MID}=$2; debug("set IP for MID $MID to " . $IP{$MID}); } if ($LINE=~/sm-mta\[[0-9]*\]: ([^:]*):.*DEFAULT=\/home\/spam\/Maildir\//) { # found spam, # block the IP my $MID; $MID=$1; logger("blocking IP ".$IP{$MID}." for sending mail to honeypot."); debug("IP for MID $MID is " . $IP{$MID} . "."); debug("calling block for " . $IP{$MID} . "."); block($IP{$MID}); } if ($LINE=~/sm-mta.*, msgid=([^, ]*) *, .*relay=[^[]*\[([^]]*)]/) { my $MID; $MID=$1; debug("setting IP for MID $MID to $2."); $IP{$MID}=$2; debug("IP{$MID} = $IP{$MID}."); } if ($LINE=~/spamd: result: Y.*mid=([^,]*),/) { my $MID; $MID=$1; debug("found spamd positive for MID $MID."); if ($SEEN{$MID}) { debug("but we've already counted $MID, skipping."); next; } if ($IP{$MID}) { $COUNT{$IP{$MID}}++; $SEEN{$MID}=1; debug("IP for MID is $IP{$MID}."); debug("COUNT{".$IP{$MID}."} incremented to $COUNT{$IP{$MID}}."); } else { debug("No IP for MID $MID, not counting."); } if ($COUNT{$IP{$MID}}==$LIMIT) { debug("IP for MID $MID is " . $IP{$MID} . "."); debug("calling block for " . $IP{$MID} . "."); logger("calling block for IP ".$IP{$MID}." for sending $COUNT{$IP{$MID}} spam messages"); block($IP{$MID}); } } } sub check_for_rotated_logfile { my $NEWINODE=(stat($LOGFILE))[1]; if ($NEWINODE != $OLDINODE) { logger("detected logfile $LOGFILE rotated, re-opening"); close(LOG); open_logfile; } } sub init_syslog { openlog($SYSLOG_TAG,"pid",$SYSLOG_FACILITY) || die "COULDN'T OPENLOG: $!"; logger("starting"); } ######## # MAIN # ######## init_syslog; process_options; sanity_check_domain; check_pidfile; daemonize; install_handlers; sanity_check_keyfile; write_pidfile; read_whitelist; open_logfile; if ($LOGFILE eq "STDIN") { while() { process_line($_); } cleanup; logger("exiting"); } else { # seek to end of file seek(LOG,0,2) || daemondie "couldn't seek to eof on LOG FH: $!" ; while(1) { if (eof(LOG)) { sleep(1); seek(LOG,0,1); check_for_rotated_logfile; } else { my $LINE; $LINE=; process_line($LINE); } } }