Botnet.api.txt0000444000000000000060000000725110542471052013530 0ustar rootmail00000000000000 If you want to write perl programs that do the same checks as Botnet, they can now be used without having to go through SpamAssassin. You will need to have SpamAssassin installed to get Botnet.pm to load, but other than that, you don't have to interact with SpamAssassin. Here are the perl statements that evaluate to the same process as the Botnet checks: Same as BOTNET_NORDNS: $hostname = Mail::SpamAssassin::Plugin::Botnet::get_rdns($ip); $nordns = ($hostname eq ""); Given the IP address (without surrounding []'s), will return the hostname contained within the _FIRST_ PTR record it finds for that IP address. Same as BOTNET_BADDNS: $baddns = Mail::SpamAssassin::Plugin::Botnet::check_dns($hostname, $ip, "A", -1); Returns 1 if $hostname resolves back to $ip. Otherwise returns 0. The third argument can be set to "MX" to resolve MX records back to an IP address. Only "A" and "MX" are currently supported. The fourth argument says how many records to look at. -1 says "all of them". If you set this to 5, it will only look at 5 records before giving up. If you set this to 5, and set the record type to "MX", then the only the first 5 MX records are checked, AND for each MX record only the first 5 A records are checked. Same as BOTNET_IPINHOSTNAME: $iphost = Mail::SpamAssassin::Plugin::Botnet::check_ipinhostname($hostname, $ip); Returns 1 if the hostname contains 2 or more octets of the IP address, in decimal or hexidecimal form. Same as BOTNET_CLIENTWORDS or BOTNET_SERVERWORDS: $cwordexp = '((\b|\d)cable(\b|\d))|((\b|\d)catv(\b|\d))|((\b|\d)ddns(\b|\d))|' . '((\b|\d)dhcp(\b|\d))|((\b|\d)dial-?up(\b|\d))|' . '((\b|\d)dip(\b|\d))|((\b|\d)(a|s|d(yn)?)?dsl(\b|\d))|' . '((\b|\d)dynamic(\b|\d))|((\b|\d)modem(\b|\d))|' . '((\b|\d)ppp(\b|\d))|((\b|\d)res(net|ident(ial)?)?(\b|\d))|' . '((\b|\d)client(\b|\d))|((\b|\d)fixed(\b|\d))|' . '((\b|\d)pool(\b|\d))|((\b|\d)static(\b|\d))|((\b|\d)user(\b|\d))'; $cwords = Mail::SpamAssassin::Plugin::Botnet::check_words($hostname, $cwordexp); $swordexp = '((\b|\d)mail(\b|\d))|((\b|\d)mta(\b|\d))|((\b|\d)mx(\b|\d))|' . '((\b|\d)relay(\b|\d))|((\b|\d)smtp(\b|\d))'; $swords = Mail::SpamAssassin::Plugin::Botnet::check_words($hostname, $swordexp); (the above $cwordexp matches the expression sent to the client word check based upon the default Botnet.cf; similarly, the above $swordexp matches the expression sent to the server word check based upon the default Botnet.cf) Returns 1 if the hostname matches the regular expression in $cwordexp, or $swordexp, not including within the two right-most domains in $hostname. You must supply the regular expression yourself, and act accordingly to whether or not it is server words or client words. Same as BOTNET_CLIENT: $client = ((! $swords) && ($cwords || $iphost)); OR $client = check_client($hostname, $ip, $cwordexp, $swordexp, \$tests) $tests (optional) will contain the names of which subchecks were triggered: serverwords, clientwords, ipinhostname Same as BOTNET_SOHO: $soho = Mail::SpamAssassin::Plugin::Botnet::check_soho($hostname, $ip, $domain, $helo); $domain should be the part after the @ in the sender's email address. $helo doesn't actualy do anything ... and probably wont ever. Same as BOTNET: $botnet = ((! $soho) && ($nordns || $baddns || $client)); OR $botnet = Mail::SpamAssassin::Plugin::Botnet::check_botnet($hostname, $ip, $cwordexp, $swordexp, $domain, $helo, \$tests); $tests (optional) will contain the names of which subchecks were triggered: nordns, badrdns, serverwords, clientwords, ipinhostname, client, soho Botnet.cf0000444000000000000060000001114210655500263012525 0ustar rootmail00000000000000# # See Botnet.txt for explanations ###################################################################### # # THE PLUGIN # ###################################################################### loadplugin Mail::SpamAssassin::Plugin::Botnet Botnet.pm ###################################################################### # # CONFIGURATION SETTINGS # ###################################################################### # I don't think SA is properly setting the 'auth=' field in the # untrusted-relay's pseudoheader anyway, so I don't think this matters botnet_pass_auth 0 # If there are trusted relays, then look to see if there's a # public IP address; if so, then pass the message through. botnet_pass_trusted public # look past Untrusted Relays with these IP's at the next Untrusted Relay # I've included ip-v4 loopback, and the RFC 1918 reserved IP blocks. botnet_skip_ip ^127\.0\.0\.1$ botnet_skip_ip ^10\..*$ botnet_skip_ip ^172\.1[6789]\..*$ botnet_skip_ip ^172\.2[0-9]\..*$ botnet_skip_ip ^172\.3[01]\..*$ botnet_skip_ip ^192\.168\..*$ # pass messages entirely (no botnet rules triggered) if you come to an # untrusted relay with these IP addresses. (example only, unless you're # using that block locally, you probably don't want to uncomment this # next line) #botnet_pass_ip ^10\.0\.0\..*$ botnet_pass_ip ^128\.223\.98\.16$ # dynamic.uoregon.edu # domain names we pass; note: if the RDNS owner tricks this, by putting # this domain in their PTR record, then: # a) it's probably a direct spammer and not a botnet anyway, and # b) there are other spam assassin rules for dealing with those # issues. botnet_pass_domains amazon\.com # they use IP in Hostname; dorks botnet_pass_domains apple\.com # special test case botnet_pass_domains ebay\.com # pool in hostname # basic substrings (regular expressions) for "client-like hostnames" botnet_clientwords .*dsl.* cable catv ddns dhcp dial(-?up)? dip docsis botnet_clientwords dyn(amic)?(ip)? modem ppp(oe)? res(net|ident(ial)?)? botnet_clientwords bredband # slightly more controversial client words botnet_clientwords client fixed ip pool static user # basic substrings (regular expressions) for "mail server-like hostnames" botnet_serverwords e?mail(out)? mta mx(pool)? relay smtp # used by many exchange servers botnet_serverwords exch(ange)? ###################################################################### # # THE RULES # ###################################################################### describe BOTNET Relay might be a spambot or virusbot header BOTNET eval:botnet() score BOTNET 5.0 describe BOTNET_SOHO Relay might be a SOHO mail server header BOTNET_SOHO eval:botnet_soho() score BOTNET_SOHO 0.0 describe BOTNET_NORDNS Relay's IP address has no PTR record header BOTNET_NORDNS eval:botnet_nordns() score BOTNET_NORDNS 0.0 describe BOTNET_BADDNS Relay doesn't have full circle DNS header BOTNET_BADDNS eval:botnet_baddns() score BOTNET_BADDNS 0.0 describe BOTNET_CLIENT Relay has a client-like hostname header BOTNET_CLIENT eval:botnet_client() score BOTNET_CLIENT 0.0 describe BOTNET_IPINHOSTNAME Hostname contains its own IP address header BOTNET_IPINHOSTNAME eval:botnet_ipinhostname() score BOTNET_IPINHOSTNAME 0.0 describe BOTNET_CLIENTWORDS Hostname contains client-like substrings header BOTNET_CLIENTWORDS eval:botnet_clientwords() score BOTNET_CLIENTWORDS 0.0 describe BOTNET_SERVERWORDS Hostname contains server-like substrings header BOTNET_SERVERWORDS eval:botnet_serverwords() score BOTNET_SERVERWORDS 0.0 ###################################################################### # # NON-MODULE RULES # ###################################################################### # Botnet rules that don't make direct or indirect use of the Botnet.pm module # shawcable.net uses customer hostnames that don't match other botnet patterns describe BOTNET_SHAWCABLE Shawcable.net customer address meta BOTNET_SHAWCABLE (__BOTNET_SHAWCABLE && __BOTNET_NOTRUST) header __BOTNET_SHAWCABLE X-Spam-Relays-Untrusted =~ /^[^\]]+ rdns=s[0-9a-f]*\...\.shawcable\.net\b/i score BOTNET_SHAWCABLE 5.0 # ocn.ne.jp uses customer hostnames that don't match other botnet patterns describe BOTNET_OCNNEJP Ocn.ne.jp customer address meta BOTNET_OCNNEJP (__BOTNET_OCNNEJP && __BOTNET_NOTRUST) header __BOTNET_OCNNEJP X-Spam-Relays-Untrusted =~ /^[^\]]+ rdns=p\d{4}-ip\S*\.ocn\.ne\.jp\b/i score BOTNET_OCNNEJP 5.0 # If the message was authenticated or hit a trusted host, then we want to # exempt these 'non-module' rules. describe __BOTNET_NOTRUST Message has no trusted relays header __BOTNET_NOTRUST X-Spam-Relays-Trusted !~ /ip=/i Botnet.credits.txt0000444000000000000060000000120310542467656014422 0ustar rootmail00000000000000 People who have given suggestions, bug reports, feedback, and other help in the development of the Botnet plugin: Mark Boolootian Terry Figel Josh Homan Paul Tatarsky Jim Warner David F. Skoll Chris (Pollock?) John D. Hardin Bret Miller Jonas Eckerman Tom Shaw Craig Morrison Patrick Snyers Jeff Mincy Rob Mangiafico Till Klampaeckel Loren Wilton Daryl C.W. O'Shea Mark Martinec Dennis Davis Bill Landry Larry M. Rosenbaum Ralf Hildebrandt Michael Schaap Chris/decoder Chris Lear Rene Berber Billy Huddleston Mark Nienberg Carlos Horowicz Federico Giannici Phil Barnett Steven Manross Dylan Bouterse Kosmaj Derek Harding Michael Alan Dorman Botnet.pl0000555000000000000060000001132310655500746012562 0ustar rootmail00000000000000#!/usr/bin/perl use Botnet; my $ip = shift(@ARGV); my $domain = shift(@ARGV); my $max = shift(@ARGV); my @clientwords = ('.*dsl.*', 'cable', 'catv', 'ddns', 'dhcp', 'dial(-?up)?', 'dip', 'docsis', 'dyn(amic)?(ip)?', 'modem', 'ppp(oe)?', 'res(net|ident(ial)?)?', 'bredband' , 'client', 'fixed', 'ip', 'pool', 'static', 'user' # controversial ones ); my $cwordre = '((\b|\d)' . join('(\b|\d))|((\b|\d)', @clientwords) . '(\b|\d))'; my @serverwords = ('e?mail(out)?', 'mta', 'mx(pool)?', 'relay', 'smtp' , 'exch(ange)?' ); my $swordre = '((\b|\d)' . join('(\b|\d))|((\b|\d)', @serverwords) . '(\b|\d))'; my ($word, $i, $temp, $tests); my ($rdns, $baddns, $client, $soho, $cwords, $swords, $iphost); $rdns = $baddns = $client = $soho = $cwords = $swords = $iphost = 0; if (defined($ip)) { $ip =~ s/^\[//; $ip =~ s/\]$//; } else { print "usage: $0 ip-address [maximum]\n"; exit(1); } unless ($ip =~ /^\d+\.\d+\.\d+\.\d+$/) { print "must be a ipv4 ip-address\n"; exit(1); } my $version = Mail::SpamAssassin::Plugin::Botnet::get_version(); print "Botnet Version = " . $version . "\n"; print "checking IP address: $ip\n"; unless (defined $domain) { $domain = ""; } if ($domain ne "") { print "checking mail domain: $domain\n"; } unless ((defined ($max)) && ($max =~ /^\d+$/)) { $max = 5; } my $hostname = Mail::SpamAssassin::Plugin::Botnet::get_rdns($ip); if ($hostname eq "") { $rdns = 1; print " BOTNET_NORDNS: hit\n"; } else { print " BOTNET_NORDNS: not hit - $hostname\n"; } if ( ($hostname ne "") && (Mail::SpamAssassin::Plugin::Botnet::check_dns($hostname, $ip, "A", "-1"))) { print " BOTNET_BADDNS: not hit - hostname resolves back to ip\n"; } elsif ($hostname ne "") { print " BOTNET_BADDNS: hit - hostname doesn't resolve back to ip\n"; $baddns = 1; } else { print " BOTNET_BADDNS: not hit\n"; } #print " BOTNET_CLIENT:\n"; if (Mail::SpamAssassin::Plugin::Botnet::check_ipinhostname($hostname, $ip)) { print " BOTNET_IPINHOSTNAME: hit\n"; $iphost = 1; } else { print " BOTNET_IPINHOSTNAME: not hit\n"; } #print " BOTNET_CLIENTWORDS:\n"; $i = 0; $tests = ""; foreach $word (@clientwords) { $temp = '((\b|\d)' . $word . '(\b|\d))'; if (Mail::SpamAssassin::Plugin::Botnet::check_words($hostname, $temp)) { #print " hostname matched $word\n"; $tests .= $word . " "; $i++; } } $tests =~ s/ $//; if ($i) { print " BOTNET_CLIENTWORDS: hit, matches=$tests\n"; $cwords = 1; } else { print " BOTNET_CLIENTWORDS: not hit\n"; } #print " BOTNET_SERVERWORDS:\n"; $i = 0; $tests = ""; foreach $word (@serverwords) { $temp = '((\b|\d)' . $word . '(\b|\d))'; if (Mail::SpamAssassin::Plugin::Botnet::check_words($hostname, $temp)) { #print " hostname matched $word\n"; $tests .= $word . " "; $i++; } } $tests =~ s/ $//; if ($i) { print " BOTNET_SERVERWORDS: hit, matches=$tests\n"; $swords = 1; } else { print " BOTNET_SERVERWORDS: not hit\n"; } if ((! $swords) && ($cwords || $iphost)) { $client = 1; print " BOTNET_CLIENT (meta) hit\n"; } elsif ($swords && ($cwords || $iphost)) { print " BOTNET_CLIENT (meta) not hit, BOTNET_SERVERWORDS exemption\n"; } else { print " BOTNET_CLIENT (meta) not hit\n"; } $tests = ""; if (Mail::SpamAssassin::Plugin::Botnet::check_client($hostname, $ip, $cwordre, $swordre, \$tests)) { $tests = "none" if ($tests eq ""); print " BOTNET_CLIENT (code) hit, tests=$tests\n"; } else { $tests = "none" if ($tests eq ""); print " BOTNET_CLIENT (code) not hit, tests=$tests\n"; } if (($domain ne "") && ($hostname ne $domain)) { if (Mail::SpamAssassin::Plugin::Botnet::check_soho($hostname, $ip, $domain, "")) { $soho = 1; print " BOTNET_SOHO: hit\n"; } else { print " BOTNET_SOHO: not hit\n"; } } elsif ($domain ne "") { print " BOTNET_SOHO: skipped (hostname eq mail domain)\n"; } else { print " BOTNET_SOHO: skipped (no mail domain given)\n"; } if ((! $soho) && ($rdns || $baddns || $client)) { print "BOTNET (meta) hit\n"; } else { print "BOTNET (meta) not hit\n"; } $tests = ""; if (Mail::SpamAssassin::Plugin::Botnet::check_botnet($hostname, $ip, $cwordre, $swordre, $domain, $helo, \$tests)) { $tests = "none" if ($tests eq ""); print "BOTNET (code) hit, tests=$tests\n"; } else { $tests = "none" if ($tests eq ""); print "BOTNET (code) not hit, tests=$tests\n"; } Botnet.pm0000444000000000000060000006771010655477463012604 0ustar rootmail00000000000000 package Mail::SpamAssassin::Plugin::Botnet; # Copyright (C) 2003 The Regents of the University of California # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # The Author, John Rudd, can be reached via email at # jrudd@ucsc.edu # # Botnet - perform DNS validations on the first untrusted relay # looking for signs of a Botnet infected host, such as no reverse # DNS, a hostname that would indicate an ISP client or domain # workstation, or other hosts that aren't intended to be acting as # a direct mail submitter outside of their own domain. use Socket; use Net::DNS; use Mail::SpamAssassin::Plugin; use strict; use warnings; use vars qw(@ISA); @ISA = qw(Mail::SpamAssassin::Plugin); my $VERSION = 0.8; sub new { my ($class, $mailsa) = @_; $class = ref($class) || $class; my $self = $class->SUPER::new($mailsa); bless ($self, $class); Mail::SpamAssassin::Plugin::dbg("Botnet: version " . $VERSION); $self->register_eval_rule("botnet_nordns"); $self->register_eval_rule("botnet_baddns"); $self->register_eval_rule("botnet_ipinhostname"); $self->register_eval_rule("botnet_clientwords"); $self->register_eval_rule("botnet_serverwords"); $self->register_eval_rule("botnet_soho"); $self->register_eval_rule("botnet_client"); $self->register_eval_rule("botnet"); $self->{main}->{conf}->{botnet_pass_auth} = 0; $self->{main}->{conf}->{botnet_pass_trusted} = "public"; $self->{main}->{conf}->{botnet_skip_ip} = ""; $self->{main}->{conf}->{botnet_pass_ip} = ""; $self->{main}->{conf}->{botnet_pass_domains} = ""; $self->{main}->{conf}->{botnet_clientwords} = ""; $self->{main}->{conf}->{botnet_serverwords} = ""; return $self; } sub parse_config { my ($self, $opts) = @_; my ($temp); my $key = $opts->{key}; my $value = $opts->{value}; if ( ($key eq "botnet_pass_auth") || ($key eq "botnet_pass_trusted") ) { Mail::SpamAssassin::Plugin::dbg("Botnet: setting $key to $value"); $self->{main}->{conf}->{$key} = $value; $self->inhibit_further_callbacks(); } elsif ( ($key eq "botnet_skip_ip") || ($key eq "botnet_pass_ip") || ($key eq "botnet_pass_domains") || ($key eq "botnet_clientwords") || ($key eq "botnet_serverwords") ) { foreach $temp (split(/\s+/, $value)) { if ($temp eq "=") { next ; } # not sure why that happens if ( ($key eq "botnet_clientwords") || ($key eq "botnet_pass_domains") || ($key eq "botnet_serverwords") ) { $temp =~ s/\^//g; # remove any carets $temp =~ s/\$//g; # remove any dollars } if ( ($key eq "botnet_clientwords") || ($key eq "botnet_serverwords") ) { $temp = '(\b|\d)' . $temp . '(\b|\d)'; } if (($key eq "botnet_pass_domains") && ($temp !~ /^\\(\.|A)/)) { $temp = '(\.|\A)' . $temp; } if ($temp eq "") { # don't add empty terms next; } if ($key eq "botnet_pass_domains") { # anchor each domain to end of string $temp .= '$'; } Mail::SpamAssassin::Plugin::dbg("Botnet: adding " . $temp . " to $key"); if ($self->{main}->{conf}->{$key} ne "") { $self->{main}->{conf}->{$key} = $self->{main}->{conf}->{$key} . "|(" . $temp . ")"; } else { $self->{main}->{conf}->{$key} = "(" . $temp . ")"; } } $self->inhibit_further_callbacks(); } else { return 0; } return 1; } sub _botnet_get_relay { my ($self, $pms) = @_; my $msg = $pms->get_message(); my @untrusted = @{$msg->{metadata}->{relays_untrusted}}; my @trusted = @{$msg->{metadata}->{relays_trusted}}; my ($relay, $rdns, $ip, $auth, $tmp, $iaddr, $hostname, $helo); my $skip_ip = $self->{main}->{conf}->{botnet_skip_ip}; my $pass_ip = $self->{main}->{conf}->{botnet_pass_ip}; my $pass_trusted = $self->{main}->{conf}->{botnet_pass_trusted}; my $pass_auth = $self->{main}->{conf}->{botnet_pass_auth}; my $pass_domains = '(?:' . $self->{main}->{conf}->{botnet_pass_domains} . ')'; my $private_ips = '(?:^127\..*$|^10\..*$|^172\.1[6789]\..*$|' . '^172\.2[0-9]\..*$|^172\.3[01]\..*$|^192\.168\..*$)'; # if there are any trusted relays, AND $pass_trusted is set to # public, private, or any, or $pass_auth is true, then check the # trusted relays for any pass conditions if ( (defined($trusted[0]->{ip})) && (($pass_auth) || ($pass_trusted eq "any") || ($pass_trusted eq "public") || ($pass_trusted eq "private")) ) { foreach $relay (@trusted) { if ($pass_trusted eq "any") { Mail::SpamAssassin::Plugin::dbg("Botnet: found any trusted"); return (0, "", "", ""); } elsif (($pass_auth) && ($relay->{auth} ne "")) { $auth = $relay->{auth}; Mail::SpamAssassin::Plugin::dbg("Botnet: Passed auth " . $auth); return (0, "", "", ""); } elsif ( ($pass_trusted eq "private") && ($relay->{ip} =~ /$private_ips/) ) { Mail::SpamAssassin::Plugin::dbg("Botnet: found private trusted"); return (0, "", "", ""); } elsif ( ($pass_trusted eq "public") && ($relay->{ip} !~ /$private_ips/) ) { Mail::SpamAssassin::Plugin::dbg("Botnet: found public trusted"); return (0, "", "", ""); } } if ( ($pass_trusted eq "any") || ($pass_trusted eq "public") || ($pass_trusted eq "private") ) { # didn't find what we were looking for above Mail::SpamAssassin::Plugin::dbg("Botnet: " . $pass_trusted . " trusted relays not found"); } if ($pass_auth) { Mail::SpamAssassin::Plugin::dbg("Botnet: authenticated and" . " trusted relay not found"); } } elsif (!defined ($trusted[0]->{ip})) { Mail::SpamAssassin::Plugin::dbg("Botnet: no trusted relays"); } while (1) { $relay = shift(@untrusted); if (! defined ($relay)) { Mail::SpamAssassin::Plugin::dbg("Botnet: All skipped/no untrusted"); return (0, "", "", ""); } $ip = $relay->{ip}; if (! defined ($ip) ) { Mail::SpamAssassin::Plugin::dbg("Botnet: All skipped/no untrusted"); return (0, "", "", ""); } elsif (($pass_ip ne "") && ($ip =~ /(?:$pass_ip)/)) { Mail::SpamAssassin::Plugin::dbg("Botnet: Passed ip $ip"); return (0, "", "", ""); } elsif (($skip_ip ne "") && ($ip =~ /(?:$skip_ip)/)) { Mail::SpamAssassin::Plugin::dbg("Botnet: Skipped ip $ip"); next; } ## I think we should only look for authenticated relays in the ## trusted relays #elsif (($pass_auth) && ($relay->{auth} ne "")) { # $auth = $relay->{auth}; # Mail::SpamAssassin::Plugin::dbg("Botnet: Passed auth " . $auth); # return (0, "", "", ""); # } else { if ((exists $relay->{rdns}) && ($relay->{rdns} ne "") && ($relay->{rdns} ne "-1")) { # we've got this relay's RDNS Mail::SpamAssassin::Plugin::dbg("Botnet: get_relay good RDNS"); $rdns = $relay->{rdns}; } elsif ((exists $relay->{rdns}) && ($relay->{rdns} eq "-1")) { # rdns = -1, which means we set it to that, because the # MTA didn't include it, and then we couldn't find it on # lookup, which means the IP addr REALLY doesn't have RDNS Mail::SpamAssassin::Plugin::dbg("Botnet: get_relay -1 RDNS"); $rdns = ""; } else { # rdns hasn't been set in the hash, which means _either_ # the IP addr really doesn't have RDNS, _OR_ the MTA # is lame, like CommuniGate Pro, and doesn't put the RDNS # data into the Received header. So, we'll try to look # it up _one_ time, and if we don't get anything we'll # set the value in the hash to -1 Mail::SpamAssassin::Plugin::dbg( "Botnet: get_relay didn't find RDNS"); $hostname = get_rdns($ip); if ((defined $hostname) && ($hostname ne "")) { $relay->{rdns} = $hostname; $rdns = $hostname; } else { $relay->{rdns} = "-1"; $rdns = ""; } } $helo = $relay->{helo}; Mail::SpamAssassin::Plugin::dbg("Botnet: IP is '$ip'"); Mail::SpamAssassin::Plugin::dbg("Botnet: RDNS is '$rdns'"); Mail::SpamAssassin::Plugin::dbg("Botnet: HELO is '$helo'"); if ($rdns =~ /(?:$pass_domains)/i) { # is this a domain we exempt/pass? Mail::SpamAssassin::Plugin::dbg("Botnet: pass_domain '$rdns'"); return(0, "", "", ""); } return (1, $ip, $rdns, $helo); } } } sub botnet_nordns { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; Mail::SpamAssassin::Plugin::dbg("Botnet: checking NORDNS"); ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: NORDNS skipped"); return 0; } if ($hostname eq "") { # the IP address doesn't have a PTR record $pms->test_log("botnet_nordns,ip=$ip"); Mail::SpamAssassin::Plugin::dbg("Botnet: NORDNS hit"); return (1); } Mail::SpamAssassin::Plugin::dbg("Botnet: NORDNS miss"); return (0); } sub botnet_baddns { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; Mail::SpamAssassin::Plugin::dbg("Botnet: checking BADDNS"); ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: BADDNS skipped"); return 0; } if ($hostname eq "") { # covered by NORDNS Mail::SpamAssassin::Plugin::dbg("Botnet: BADDNS miss"); return (0); } elsif (check_dns($hostname, $ip, "A", -1)) { # resolved the hostname Mail::SpamAssassin::Plugin::dbg("Botnet: BADDNS miss"); return (0); } else { # failed to resolve the hostname $pms->test_log("botnet_baddns,ip=$ip,rdns=$hostname"); Mail::SpamAssassin::Plugin::dbg("Botnet: BADDNS hit"); return (1); } } sub botnet_ipinhostname { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; Mail::SpamAssassin::Plugin::dbg("Botnet: checking IPINHOSTNAME"); ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: IPINHOSTNAME skipped"); return 0; } if ($hostname eq "") { # covered by NORDNS Mail::SpamAssassin::Plugin::dbg("Botnet: IPINHOSTNAME miss"); return (0); } elsif (check_ipinhostname($hostname, $ip)) { $pms->test_log("botnet_ipinhosntame,ip=$ip,rdns=$hostname"); Mail::SpamAssassin::Plugin::dbg("Botnet: IPINHOSTNAME hit"); return(1); } else { Mail::SpamAssassin::Plugin::dbg("Botnet: IPINHOSTNAME miss"); return (0); } } sub botnet_clientwords { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; my $wordre = $self->{main}->{conf}->{botnet_clientwords}; Mail::SpamAssassin::Plugin::dbg("Botnet: checking CLIENTWORDS"); if ($wordre eq "") { Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENTWORDS miss"); return (0); } else { Mail::SpamAssassin::Plugin::dbg("Botnet: client words regexp is" . $wordre); } ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENTWORDS skipped"); return 0; } if ($hostname eq "") { # covered by NORDNS Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENTWORDS miss"); return (0); } elsif (check_words($hostname, $wordre)) { # hostname contains client keywords, outside of the registered domain $pms->test_log("botnet_clientwords,ip=$ip,rdns=$hostname"); Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENTWORDS hit"); return (1); } else { Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENTWORDS miss"); return (0); } } sub botnet_serverwords { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; my $wordre = $self->{main}->{conf}->{botnet_serverwords}; Mail::SpamAssassin::Plugin::dbg("Botnet: checking SERVERWORDS"); if ($wordre eq "") { Mail::SpamAssassin::Plugin::dbg("Botnet: SERVERWORDS miss"); return (0); } else { Mail::SpamAssassin::Plugin::dbg("Botnet: server words list is" . $wordre); } ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: SERVERWORDS skipped"); return 0; } if ($hostname eq "") { # covered by NORDNS Mail::SpamAssassin::Plugin::dbg("Botnet: SERVERWORDS miss"); return (0); } elsif (check_words($hostname, $wordre)) { # hostname contains server keywords outside of the registered domain $pms->test_log("botnet_serverwords,ip=$ip,rdns=$hostname"); Mail::SpamAssassin::Plugin::dbg("Botnet: SERVERWORDS hit"); return (1); } else { Mail::SpamAssassin::Plugin::dbg("Botnet: SERVERWORDS miss"); return (0); } } sub botnet_soho { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; my ($sender, $user, $domain); Mail::SpamAssassin::Plugin::dbg("Botnet: checking for SOHO server"); ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: SOHO skipped"); return 0; } if (defined ($sender = $pms->get("EnvelopeFrom"))) { Mail::SpamAssassin::Plugin::dbg("Botnet: EnvelopeFrom is " . $sender); } elsif (defined ($sender = $pms->get("Return-Path:addr"))) { Mail::SpamAssassin::Plugin::dbg("Botnet: Return-Path is " . $sender); } elsif (defined ($sender = $pms->get("From:addr"))) { Mail::SpamAssassin::Plugin::dbg("Botnet: From is " . $sender); } else { Mail::SpamAssassin::Plugin::dbg("Botnet: no sender"); Mail::SpamAssassin::Plugin::dbg("Botnet: SOHO miss"); return 0; } ($user, $domain) = split (/\@/, $sender); if ( (defined ($domain)) && ($domain ne "") && (check_soho($hostname, $ip, $domain, $helo)) ) { # looks like a SOHO mail server $pms->test_log("botnet_soho,ip=$ip,maildomain=$domain,helo=$helo"); Mail::SpamAssassin::Plugin::dbg("Botnet: mail domain is " . $domain); Mail::SpamAssassin::Plugin::dbg("Botnet: SOHO hit"); return 1; } elsif ( (defined($domain)) && ($domain ne "")) { # does not look lik a SOHO mail server Mail::SpamAssassin::Plugin::dbg("Botnet: mail domain is " . $domain); Mail::SpamAssassin::Plugin::dbg("Botnet: SOHO miss"); return 0; } else { # no domain Mail::SpamAssassin::Plugin::dbg("Botnet: no sender domain"); Mail::SpamAssassin::Plugin::dbg("Botnet: SOHO miss"); return 0; } # shouldn't get here Mail::SpamAssassin::Plugin::dbg("Botnet: SOHO miss"); return (0); } sub botnet_client { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; my $cwordre = $self->{main}->{conf}->{botnet_clientwords}; my $swordre = $self->{main}->{conf}->{botnet_serverwords}; my $tests = 0; Mail::SpamAssassin::Plugin::dbg("Botnet: checking for CLIENT"); ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENT skipped"); return 0; } if (check_client($hostname, $ip, $cwordre, $swordre, \$tests)) { $pms->test_log("botnet_client,ip=$ip,rdns=$hostname," . $tests); Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENT hit (" . $tests . ")"); return 1; } else { $tests = "none" if ($tests eq ""); Mail::SpamAssassin::Plugin::dbg("Botnet: CLIENT miss (" . $tests . ")"); return 0; } } sub botnet { my ($self, $pms) = @_; my ($code, $ip, $helo); my $hostname = ""; my $cwordre = $self->{main}->{conf}->{botnet_clientwords}; my $swordre = $self->{main}->{conf}->{botnet_serverwords}; my ($sender, $user, $domain); my $tests = ""; Mail::SpamAssassin::Plugin::dbg("Botnet: starting"); ($code, $ip, $hostname, $helo) = $self->_botnet_get_relay($pms); unless ($code) { Mail::SpamAssassin::Plugin::dbg("Botnet: skipping"); return 0; } if ( (defined ($sender = $pms->get("EnvelopeFrom"))) || (defined ($sender = $pms->get("Return-Path:addr"))) || (defined ($sender = $pms->get("From:addr"))) ) { # if we find a sender Mail::SpamAssassin::Plugin::dbg("Botnet: sender '$sender'"); ($user, $domain) = split (/\@/, $sender); unless (defined $domain) { $domain = ""; } } else { $domain = ""; } if (check_botnet($hostname, $ip, $cwordre, $swordre, $domain, $helo, \$tests)) { if (($tests =~ /nordns/) && ($domain eq "")) { $pms->test_log("botnet" . $VERSION . ",ip=$ip," . $tests); } elsif ($tests =~ /nordns/) { # could use "eq", but used "=~" to be safe $pms->test_log("botnet" . $VERSION . ",ip=$ip,maildomain=$domain," . $tests); } elsif ($domain eq "") { $pms->test_log("botnet" . $VERSION . ",ip=$ip,rdns=$hostname," . $tests); } else { $pms->test_log("botnet" . $VERSION . ",ip=$ip,rdns=$hostname," . "maildomain=$domain," . $tests); } Mail::SpamAssassin::Plugin::dbg("Botnet: hit (" . $tests . ")"); return 1; } else { $tests = "none" if ($tests eq ""); Mail::SpamAssassin::Plugin::dbg("Botnet: miss (" . $tests . ")"); return 0; } } sub check_client { my ($hostname, $ip, $cwordre, $swordre, $tests) = @_; my $iphost = check_ipinhostname($hostname, $ip); my $cwords = check_words($hostname, $cwordre); if (defined ($tests)) { if ($iphost && $cwords) { $$tests = "ipinhostname,clientwords"; } elsif ($iphost) { $$tests = "ipinhostname"; } elsif ($cwords) { $$tests = "clientwords"; } else { $$tests = ""; } } if ( ($iphost || $cwords) && # only run swordsre check if necessary (check_words($hostname, $swordre)) ) { if (defined ($tests)) { $$tests = $$tests . ",serverwords"; } return 0; } elsif ($iphost || $cwords) { return 1; } else { return 0; } } sub check_botnet { my ($hostname, $ip, $cwordre, $swordre, $domain, $helo, $tests) = @_; my ($baddns, $client, $temp); if ($hostname eq "") { if (defined ($tests)) { $$tests = "nordns"; } return 1; } $baddns = ! (check_dns($hostname, $ip, "A", -1)); $client = check_client($hostname, $ip, $cwordre, $swordre, \$temp); if (defined ($tests)) { if ($baddns && $client) { $$tests = "baddns,client," . $temp; } elsif ($baddns) { $$tests = "baddns"; } elsif ($client) { $$tests = "client," . $temp; } else { $$tests = ""; } } # if the above things triggered, check for soho mail server if ( ($baddns || $client) && # only run soho check if necessary (check_soho($hostname, $ip, $domain, $helo)) ) { # looks like a SOHO mail server if (defined ($tests)) { $$tests = $$tests . ",soho"; } return 0; } elsif ($baddns || $client) { return 1; } else { return 0; } } sub check_soho { my ($hostname, $ip, $domain, $helo) = @_; if ((defined $domain) && ($domain ne "")) { if ( (defined ($hostname)) && (lc($hostname) eq lc($domain)) ) { # if the mail domain is the hostname, and the hostname looks # like a botnet (or we wouldn't have gotten here), then it's # probably a botnet attempting to abuse the soho exemption return 0; } elsif (check_dns($domain, $ip, "A", 5)) { # we only check 5 because we expect a SOHO to not have a huge # round-robin DNS A record return (1); } # I don't like the suggested HELO check, because the HELO string is # within the botnet coder's control, and thus cannot be relied upon, # so I have commented out the code for it. I have left it here, its # head upon a pike, as a warning, and so everyone knows it wasn't # an oversight. # 0.8 update: I give an exemption above based on the mail domain, # and the botnet coder has as much control over that as they do # over the HELO string. So, under the same circumstances (HELO != # Hostname) I'll let the HELO string act in the same capacity as # the mail domain. elsif ( (defined $helo) && (defined $hostname) && (lc($hostname) ne lc($helo)) && ($helo ne "") && (check_dns($helo, $ip, "A", 5)) ) { # we only check 5 because we expect a SOHO to not have a huge # round-robin DNS A record return (1); } elsif (check_dns($domain, $ip, "MX", 5)) { # we only check 5 because we expect a SOHO to not have a huge # number of MX hosts return (1); } return (0); } else { return (0); } # shouldn't get here return (0); } sub check_dns { my ($name, $ip, $type, $max) = @_; my ($resolver, $query, $rr, $i, @a); if ( (defined $name) && ($name ne "") && (defined $ip) && ($ip =~ /^\d+\.\d+\.\d+\.\d+$/) && (defined $type) && ($type =~ /^(?:A|MX)$/) && (defined $max) && ($max =~ /^-?\d+$/) ) { $resolver = Net::DNS::Resolver->new(); if ($query = $resolver->search($name, $type)) { # found matches $i = 0; foreach $rr ($query->answer()) { $i++; if (($max != -1) && ($i >= $max)) { # max == -1 means "check all of the records" # $ip isn't in the first $max A records for $name return(0); } elsif (($type eq "A") && ($rr->type eq "A")) { if ($rr->address eq $ip) { # $name resolves back to this ip addr return(1); } } elsif (($type eq "MX") && ($rr->type eq "MX")) { if (check_dns($rr->exchange, $ip, "A", $max)) { # found $ip in the first MX hosts for $domain return(1); } } } # $ip isn't in the A records for $name at all return(0); } else { # the sender leads to a host that doesn't have an A record return (0); } } # can't resolve an empty name nor ip that doesn't look like an address return (0); } sub check_ipinhostname { # check for 2 octets of the IP address within the hostname, in # hexidecimal or decimal format, with zero padding or not, and with # optional spacing or not. And, for decimal format, check for # combined decimal values (ex: 3rd octet * 256 + 4th octet) my ($name, $ip) = @_; my ($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n); unless ( (defined ($name)) && ($name ne "") ) { return 0; } ($a, $b, $c, $d) = split(/\./, $ip); # decimal octets # permutations of combined decimal octets into single decimal values $e = ($a * 256 * 256 * 256) + ($b * 256 * 256) + ($c * 256) + $d; # all 4 octets $f = ($a * 256 * 256) + ($b * 256) + $c; # first 3 octets $g = ($b * 256 * 256) + ($c * 256) + $d; # last 3 octets $h = ($a * 256) + $b; # first 2 octets $i = ($b * 256) + $c; # middle 2 octets $j = ($c * 256) + $d; # last 2 octets # hex versions of the ip address octets, in lower case # we don't need combined hex octets, as they'll # just look like sequential individual octets $k = sprintf("%02x", $a); # first octet $l = sprintf("%02x", $b); # second octet $m = sprintf("%02x", $c); # third octet $n = sprintf("%02x", $d); # fourth octet #$k = lc (sprintf("%02x", $a)); # first octet #$l = lc (sprintf("%02x", $b)); # second octet #$m = lc (sprintf("%02x", $c)); # third octet #$n = lc (sprintf("%02x", $d)); # fourth octet # #$name = lc ($name); # so that we're all lower case return check_words($name, "$a.*$b|$b.*$c|$c.*$d|$d.*$c|$c.*$b|$b.*$a|" . "$n.*$m|$m.*$l|$l.*$k|$k.*$l|$l.*$m|$m.*$n|" . "$e|$f|$g|$h|$i|$j"); #"(?:$a.*$b|$b.*$c|$c.*$d|$n.*$m|$m.*$l|$l.*$k|$k.*$l|$l.*$m|" . # "$m.*$n|$e|$f|$g|$h|$i|$j)" . ".*\..+\..+$'; #if ( ($name =~ /(?:$a.*$b|$b.*$c|$c.*$d).*\..+\..+$/) || # ($name =~ /(?:$d.*$c|$c.*$b|$b.*$a).*\..+\..+$/) || # ($name =~ /(?:$n.*$m|$m.*$l|$l.*$k).*\..+\..+$/) || # ($name =~ /(?:$k.*$l|$l.*$m|$m.*$n).*\..+\..+$/) || # ($name =~ /(?:$e|$f|$g|$h|$i|$j).*\..+\..+$/ ) ) { # # hostname contains two or more octets of its own IP addr # # in hex or decimal form, with or w/o leading 0's or separators # # but don't check in the tld nor registered domain # # probably a spambot since this is an untrusted relay # return(1); # } # #return(0); } sub check_words { # check for words outside of the top 2 levels of the hostname my ($name, $wordre) = @_; my $wordexp = '(' . $wordre . ')\S*\.\S+\.\S+$'; return check_hostname($name, $wordexp); #if (($name ne "") && ($wordre ne "") && ($name =~ /(?:$wordexp)/i) ) { # return (1); # } #return (0); } sub check_hostname { # check for an expression within the entire hostname my ($name, $regexp) = @_; if (($name ne "") && ($regexp ne "") && ($name =~ /(?:$regexp)/i) ) { return (1); } return (0); } sub get_rdns { my ($ip) = @_; my ($query, @answer, $rr); my $resolver = Net::DNS::Resolver->new(); my $name = ""; if ($query = $resolver->query($ip, 'PTR', 'IN')) { @answer = $query->answer(); #if ($answer[0]->type eq "PTR") { # # just return the first one, even if it returns many # $name = $answer[0]->ptrdname(); # } # just return the first PTR record, even if it returns many foreach $rr (@answer) { if ($rr->type eq "PTR") { return ($rr->ptrdname()); } } } return ""; } sub get_version { return $VERSION; } 1; Botnet.txt0000444000000000000060000001625710540561104012762 0ustar rootmail00000000000000Plugin: Botnet Botnet looks for possible botnet sources of email by checking various DNS values that indicate things such as other ISP's clients or workstations, or misconfigured DNS settings that are more likely to happen with client or workstation addresses than servers. Botnet looks in the Untrusted Relays pseudoheader. It defaults to looking at the first relay in that list. However, certain options allow it to skip past relays in that list (or not score a hit if it finds certain relays). Installing: Copy Botnet.pm and Botnet.cf into /etc/mail/spamassassin (or whatever directory you use for your plugins). If you use something like spamc/spamd, mailscanner, or a milter, you probably need to restart that. From there, it should "just work". Rule: BOTNET_NORDNS The relay has no PTR record (no reverse dns). This rule does NOT incur a DNS check, as Botnet obtains this invormation from the rdns= field in SpamAssassin's Untrusted Relays pseudo-header. Rule: BOTNET_BADDNS The relay doesn't have a full circle DNS. Full circle DNS means that, starting with the relay's IP address, going to its PTR record, and then looking at the IPs returned from that hostname's A record, is the relay's IP address in that group if addresses? If it isn't, then there's probably a DNS forgery. Note: BOTNET_BADDNS causes Botnet to do a DNS lookup. This can be time consuming for your SpamAssassin Checks. Rule: BOTNET_IPINHOSTNAME Does the relay's hostname contain 2 or more octets of its IP address within the hostname? They can be in decimal or hexadecimal format. Each octet can have leading zeroes, or a single separator character. Rule: BOTNET_CLIENTWORDS Does the relay's hostname contain certain keywords that look like a client hostname? They can be any keywords, but the included list is intended to identify ISP end clients and dynamic workstations. Rule: BOTNET_SERVERWORDS Does the relay's hostname contain certain keywords that look like a mail server hostname? They can be any keywords, but the included list is intended to identify exceptions to the BOTNET_IPINHOSTNAME and BOTNET_CLIENTWORDS checks, that might indicate they actually are legitimate mail servers. Rule: BOTNET_CLIENT This rule duplicates the checks in BOTNET_IPINHOSTNAME, BOTNET_CLIENTWORDS, and BOTNET_SERVERWORDS to decide whether or not the hostname looks like a client. It is effectively (!serverwords && (iphostname || clientwords)) See Botnet.variants.txt for a way to replace this a meta rule. Rule: BOTNET_SOHO This rule checks to see if the relay is possibly a SOHO (small office, home office) mail server. In this case, the sender's mail domain is examined, and resolved. First an A record look up is done, and if the relay's IP address is found in the first 5, then BOTNET_SOHO hits. Second, the same check is done on the MX records for the domain, again limited to 5 records. These checks are limited to 5 records because a SOHO domain is not likely to have a large round-robin A record nor a large number of MX records. In order to avoid having this check used as a back-door by botnet coders, by using a throw-away sender domain that has all of its botnet hosts in the A records or MX records, BOTNET_SOHO only looks at 5 records. Rule: BOTNET This rule duplicates the checks done by the above rules. The intent is to flag a message automatically for quarantine or storage in a spam folder if the message does have the fingerprints of a spambot or virusbot, but does NOT have the fingerprints of a server. It is effectively (!soho && (client || baddns || nordns)) See Botnet.variants.txt for a way to replace this with a meta rule, or replace this with piece-meal rules. Option: botnet_pass_auth (1|0) If the untrusted relay being considered performed SMTP-AUTH, (the auth field is not empty), then Botnet will not score a hit if this setting is non-zero. Defaults to 0 (off). Option: botnet_pass_trusted (any|public|private|ignore) If there are trusted relays (received headers that match the trusted networks, before getting to a received header that doesn't match the trusted networks), then pass the message through Botnet without matching any rules, IF it matches the critereon of this option. If the option is set to "any", then pass the message if there are any trusted relays. If the option is set to "private", then pass the message if there are any relays from localhost and/or RFC-1918 reserved IP addresses (10.*, etc.). If the option is set to "public", then pass the message if there are any relays that are neither localhost nor RFC-1918 reserved. If the option is set to "ignore" (or, really, anything other than "any", "public", or "private"), then ignore the trusted relays. Defaults to "public". Option: botnet_skip_ip (regular-expression) A regular expression that will cause Botnet to move to the NEXT untrusted relay if the current one's IP address matches the expression. Multiple entries are ORed together. Multiple entries may be space delimited or made with multiple lines. Defaults to empty (no IPs will be skipped). Option: botnet_pass_ip (regular-expression) A regular expression that will cause Botnet to not score a hit if the current relay's IP address matches the expression. All Botnet tests will return 0. Multiple entries are ORed together. Multiple entries may be space delimited or made with multiple lines. Defaults to empty (no IPs will be passed without checking). Option: botnet_pass_domains (regular-expression) A regular expression that will cause Botnet to not score a hit if the current relay's hostname matches the expression. The expression is automatically anchored with a $, so it will only match the end of the hostname, and prepended with "(\.|\A)". If the relay has RDNS, the expression will not match at the beginning nor end of the hostname (carets are removed from the expression). All Botnet tests will return 0 if the RDNS matches the expression. Multiple entries are ORed together. Multiple entries may be space delimited or made with multiple lines. Defaults to empty (no domain names will be passed without checking). Note: if the RDNS owner tricks botnet_pass_domains, by putting these domains into their PTR record, then: a) it's probably a direct spammer and not a botnet anyway, and b) there are other spam assassin rules for dealing with those issues. Option: botnet_clientwords (regular-expression) Space delimited list of regexps that are indicate an end client or dynamic host which should not directly connect to other mail servers besides its own provider's. Multiple entries are ORed together. Multiple entries may be space delimited or made with multiple lines. Defaults to empty (no client word check will be done). The example cf file comes with a basic entry, however. The expressions will not match against the top two domains (the TLD and usually the registed domain). All word expressions have (\b|\d) added to the beginning and end, to ensure they are not sub-words of larger words. Option: botnet_serverwords (regular-expression) Same as above, but for hostname words that indicate it might NOT be a client, but is, instead, an actual mail server. Such as "mail" or "smtp" being in the hostname. Botnet.variants.txt0000444000000000000060000001111410541144111014567 0ustar rootmail00000000000000 =========================== Skipping some Botnet checks =========================== If you want to skip some Botnet checks, but not all of them, such as BOTNET_BADDNS, then you'll need to use the piece-meal rules variation, and replace BOTNET and/or BOTNET_CLIENT with meta rules. ========================== BOTNET as piece-meal rules ========================== set the following scores as shown: score BOTNET 0.0 score BOTNET_CLIENT 0.0 And set the following scores as you want to weight them (be sure to keep BOTNET_SOHO and BOTNET_SERVERWORDS as negateive numbers): score BOTNET_SOHO -0.01 score BOTNET_NORDNS 0.01 score BOTNET_BADDNS 0.00 0.01 0.00 0.01 score BOTNET_IPINHOSTNAME 0.01 score BOTNET_CLIENTWORDS 0.01 score BOTNET_SERVERWORDS -0.01 ============================ BOTNET_CLIENT as a meta rule ============================ The old style for these two rules was to do them as meta rules. To set this up, replace the following line: header BOTNET_CLIENT eval:botnet_client() with: meta BOTNET_CLIENT (!BOTNET_SERVERWORDS && (BOTNET_IPINHOSTNAME || BOTNET_CLIENTWORDS) and, last, set the following scores: score BOTNET_IPINHOSTNAME 0.01 score BOTNET_CLIENTWORDS 0.01 score BOTNET_SERVERWORDS -0.01 ===================== BOTNET as a meta rule ===================== The old style for these two rules was to do them as meta rules. To set this up, replace the following line: header BOTNET eval:botnet() with: meta BOTNET (!BOTNET_SOHO && (BOTNET_CLIENT || BOTNET_BADDNS || BOTNET_NORDNS)) and, last, set the following scores: score BOTNET_SOHO -0.01 score BOTNET_NORDNS 0.01 score BOTNET_BADDNS 0.00 0.01 0.00 0.01 score BOTNET_CLIENT 0.01 ==================== DKIM, DK, and/or p0f ==================== (see above for making BOTNET a meta rule) From Mark Martinec (using all 3): > > ... coupling it with p0f (passive operating system fingerprinting) > matching on non-unix hosts seems to bring up the best of both approaches: > > meta BOTNET_W !DKIM_VERIFIED && !DK_VERIFIED && (L_P0F_WXP || > L_P0F_W || L_P0F_UNKN) && (BOTNET_CLIENT+BOTNET_BADDNS+BOTNET_NORDNS) > 0 > score BOTNET_W 3.2 > > meta BOTNET_OTHER !BOTNET_W && > (BOTNET_CLIENT+BOTNET_BADDNS+BOTNET_NORDNS) > 0 > score BOTNET_OTHER 0.5 > > About p0f see: > http://marc.theaimsgroup.com/?l=amavis-user&m=116439276912418 > http://marc.theaimsgroup.com/?l=amavis-user&m=116440910822408 From Jonas Eckerman (just using p0f): > describe BOTNET Relay might be part of botnet > meta BOTNET (!BOTNET_SOHO && (BOTNET_CLIENT || BOTNET_BADDNS || BOTNET_NORDNS)) > score BOTNET 2.0 > > describe BOTNET_WINDOWS Windows relay might be part if botnet > meta BOTNET_WINDOWS (BOTNET && __OS_WINDOWS) > score BOTNET_WINDOWS 1.0 > > header __OS_WINDOWS p0fIP2OS =~ /Windows/i I personally would suggest not using p0f (there's nothing to prevent a linux box from being root-kitted and used as a spambot), and I still stick with the idea that 5.0 is a good score for Botnet. So, my suggestion is: meta BOTNET (!DKIM_VERIFIED && !DK_VERIFIED && !BOTNET_SOHO && (BOTNET_CLIENT || BOTNET_BADDNS || BOTNET_NORDNS)) ========================== Using SPF to exempt Botnet ========================== (see above for making BOTNET a meta rule) You _could_ have the BOTNET meta rule say: meta BOTNET (!SPF_PASS && !DKIM_VERIFIED && !DK_VERIFIED && !BOTNET_SOHO && (BOTNET_CLIENT || BOTNET_BADDNS || BOTNET_NORDNS)) But, some spambot owner could then make a throw-away domain, and give it an SPF record that says "+all" or any of a few other mechanisms that evaluate to "every host may send mail from this domain". That essentially makes the Botnet checks impotent. So, I would recommend NOT using SPF as a means of exempting a message from the Botnet check. In order to try to deal with small scale mail servers (SOHO, small office/home office) that might be stuck with service from an ISP that has terrible DNS policies, I have added the BOTNET_SOHO check. It has some limitations, but should work for SOHO mail servers. Larger organizations have other means of dealing with "not looking like a Botnet", such as: a) forcing their ISP to do the right thing, b) using a different ISP, or c) using a hosted mail server that has good DNS. COPYING0000444000000000000060000004330610525736544012033 0ustar rootmail00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. ^L GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) ^L These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. ^L 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. ^L 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ^L How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. INSTALL0000444000000000000060000000040010525753144012010 0ustar rootmail00000000000000 Copy the .pm and .cf file(s) in this directory into /etc/mail/spamassassin (or whatever directory you put your plugins into). If you use spamc/spamd, mailscanner, or a milter, you may need to restart that process in order for this plugin to get loaded.