Patch against current CVS (as all the others have been, even if I forgot
to mention it). This is a further development on the last patch I posted a
couple of months ago. This does rfc checking, but also proper background
DNS checking of the helo argument if so configured. The docs should be up
to date.
  Again this uses the tld config file.
  Cheers.

-- 
Mark Powell - UNIX System Administrator - The University of Salford
Information Services Division, Clifford Whitworth Building,
Salford University, Manchester, M5 4WT, UK.
Tel: +44 161 295 4837  Fax: +44 161 295 5888  www.pgp.com for PGP key
#
# A lot of spammers use the following which are not used by the real domain owners
#
aol\.com
yahoo\.com
bbc\.com
addr\.com
netscape\.com
microsoft\.com
compuserve\.com
cpan\.org
msn\.com
excite\.com
mail\.com
cnn\.com
barclays\.co\.uk
lloydstsb\.com
woolwich\.co\.uk
#
# This is a well known spammers domain
163\.com
.*\.163\.com
auto\.ru
#
# Block helos pretending to be in our domain
.*salford\.ac\.uk
#
# Some IPv4 address literal checks.
#
# NB Passing argument 'rfc' would already prevent all bare IP addresses so
# there'd be no need to check for them. Argument 'checkip' would also prevent
# private addresses and other IP forgery, obviating the need for any of
# the following IP based checks.
#
# Block helos pretending to be on our net.
#\[146\.87\.\d+\.\d+\]
# Block private address space IPv4 literals
#\[10\.\d+\.\d+\.\d+\]
#\[172\.(1[6-9]|2[0-9]|3[01])\.\d+\.\d+\]
#\[192\.168\.\d+\.\d+\]
# Block local address space
#\[127\.\d+\.\d+\.\d+\]
#\[0\.0\.0\.0\]
#
# Some more regexps to catch out other baddies
.*\.prod-infinitum\.com\.mx
[A-Z\d]{8}\.ipt\.aol\.com
netscape\d+\.com
krondor\.insightasia\.com
aar\.alcatel-alsthom\.fr
# Regexp specifying all the valid Top Level Domains for RFC2821 enforcement
#
# NB you may like to remove some of these e.g. biz which is questionable.
# Removing them from here would mean that they would not be accepted in the
# arguments to the HELO and MAIL commands.

([a-z][a-z]|com|edu|gov|int|mil|net|org|biz|info|name|pro|aero|coop|museum|arpa)
--- check_spamhelo      Fri Mar  5 12:46:24 2004
+++ /var/qmail/qpsmtpd/qpsmtpd/plugins/check_spamhelo   Sat Jul 31 18:22:18 2004
@@ -1,37 +1,272 @@
-=head1 NAME
+use Time::HiRes qw(gettimeofday tv_interval);
 
-check_spamhelo - Check a HELO message delivered from a connecting host.
+use constant SPAMHELO_SUCCESS   => 0;
+use constant SPAMHELO_RFCFAIL   => 1;
+use constant SPAMHELO_MATCHFAIL => 2;
+use constant SPAMHELO_IPFAIL    => 3;
 
-=head1 DESCRIPTION
+sub register {
+  my ($self, $qp, @args) = @_;
 
-Check a HELO message delivered from a connecting host.  Reject any
-that appear in the badhelo config -- e.g. yahoo.com and aol.com, which
-neither the real Yahoo or the real AOL use, but which spammers use
-rather a lot.
+  return if exists $ENV{RELAYCLIENT};
 
-=head1 CONFIGURATION
+  $self->register_hook('helo', 'check_helo');
+  $self->register_hook('ehlo', 'check_helo');
+  $self->register_hook('rcpt', 'rcpt_handler');
+  $self->register_hook('disconnect', 'disconnect_handler');
 
-Add domains or hostnames to the F<badhelo> configuration file; one
-per line.
+  %{$self->{_args}} = map { $_ => 1 } @args;
+}
 
-=cut
+sub check_helo {
+  my ($self, $transaction, $hello) = @_;
+  my $remote_ip = $self->qp->connection->remote_ip;
+  my $remote_host = $self->qp->connection->remote_host;
+  $hello = '' unless $hello;
+  my $address_literal = 0;
+  my $conn = $self->qp->connection;
 
-sub register {
-  my ($self, $qp) = @_;
-  $self->register_hook("helo", "check_helo");
-  $self->register_hook("ehlo", "check_helo");
+  #return if exists $ENV{RELAYCLIENT};
+   
+  #$hello !~ 
/^(((?!-)[a-z\d\-]+(?<!-)\.)+[a-z]{2,}|\[(((?(?<!\[)\.)(25[0-5]|2[0-4]\d|[01]?\d?\d)){4}|[a-z\d\-]*[a-z\d]:([^\\\[\]]|\\.)+)\])$/i)
 {
+
+  my $tldre = $self->qp->config('tld');
+  $tldre = '[a-z]{2,}' unless $tldre;
+
+  # are we are going to enforce RFC2821 and check for a correctly formatted FQDN or 
address literal
+  if ($self->{_args}->{rfc}) {
+    my $rfc_err = SPAMHELO_RFCFAIL;
+
+    # Check for a valid FQDN with a valid TLD
+    if ($hello =~ /^((?!-)[a-z\d\-]+(?<!-)\.)+$tldre$/i) {
+      # Valid RFC2821 FQDN
+      $rfc_err = SPAMHELO_SUCCESS;
+    } elsif ($hello =~ /^\[[a-z\d\-]*[a-z\d]:([^\\\[\]]|\\.)+\]$/i) {
+      # Valid IPv6 address literal
+      $rfc_err = SPAMHELO_SUCCESS;
+      $address_literal = 1;
+    } elsif ($hello =~ /^\[(((?(?<!\[)\.)(25[0-5]|2[0-4]\d|[01]?\d?\d)){4})\]$/) {
+      # Valid IPv4 address literal
+      $address_literal = 1;
+      $self->log(LOGDEBUG, 'hello = IPv4');
+      my $ip = $1;
+      if ($self->{_args}->{checkip}) {
+        $ip =~ s/(?<!\d)0+(?!\.)//g;   # remove leading zeroes from each octet
+        #$self->log(LOGDEBUG, "ip = $ip remote_ip = $remote_ip");
+        $rfc_err = ($ip eq $remote_ip) ? SPAMHELO_SUCCESS : SPAMHELO_IPFAIL;
+      } else { 
+        $rfc_err = SPAMHELO_SUCCESS;
+      }
+    }
+    $conn->notes('_spamhelo_address_literal', $address_literal);
+
+    $conn->notes('_spamhelo_badhelo', $rfc_err);
+    return DECLINED if ($rfc_err != SPAMHELO_SUCCESS);
+  }
+
+  # perform match
+  for my $re ($self->qp->config('badhelo')) {
+    if ($hello =~ /^$re$/i) {
+      $conn->notes('_spamhelo_badhelo', SPAMHELO_MATCHFAIL);
+      $conn->notes('_spamhelo_pattern', $re);
+      return DECLINED;
+    }
+  }
+
+  # Only perform DNS lookups if we've not already found an error and the HELO is not 
an address literal
+  if (!exists $ENV{NOIPHC} && defined($self->{_args}->{checkamx}) && 
!$address_literal) {
+    # Start the DNS lookup for an A or MX record for $hello
+    # We'll pick up the results in the RCPT TO handler
+    my $res = new Net::DNS::Resolver;
+    my $sel = IO::Select->new();
+    $self->log(LOGDEBUG, "Checking $hello for A or MX record in the background");
+    $sel->add($res->bgsend("$hello", 'MX'));
+    $sel->add($res->bgsend("$hello", 'A'));
+    $conn->notes('_start_time', [gettimeofday]);
+    $conn->notes('spamhelo_sockets', $sel);
+  }
+
+  return DECLINED;
 }
 
-sub check_helo {
-  my ($self, $transaction, $host) = @_;
-  ($host = lc $host) or return DECLINED;
+sub rcpt_handler {
+  my ($self, $transaction, $rcpt) = @_;
+  my $conn = $self->qp->connection;
+  my $hello = defined($conn->hello_host) ? $conn->hello_host : '';
+  my $pattern = $conn->notes('_spamhelo_pattern');
+  my $remote_ip = $conn->remote_ip;
+  my $remote_host = $conn->remote_host;
+  my $from = $transaction->sender->format;
+  my $to = $rcpt->format;
+  my $err = $conn->notes('_spamhelo_badhelo');
+
+  if ($err == SPAMHELO_RFCFAIL) {
+    $self->log(LOGINFO, "Denied HELO for $hello from $remote_host $remote_ip $from 
$to not a valid FQDN or address literal");
+    return (DENY, "HELO $hello is not a valid FQDN or your valid address literal: see 
RFC821/1123/2821.");
+  } elsif ($err == SPAMHELO_MATCHFAIL) {
+    $self->log(LOGINFO, "Denied HELO for $hello from $remote_host $remote_ip $from 
$to by $pattern");
+    return (DENY, "HELO $hello not accepted here.");
+  } elsif ($err == SPAMHELO_IPFAIL) {
+    $self->log(LOGINFO, "Denied HELO for $hello from $remote_host $remote_ip $from 
$to address literal ne connection");
+    return (DENY, "HELO $hello is not a valid FQDN or your valid address literal: see 
RFC821/1123/2821.");
+  }
   
-  for my $bad ($self->qp->config('badhelo')) {
-    if ($host eq lc $bad) {
-      $self->log(LOGDEBUG, "Denying HELO from host claiming to be $bad");
-      return (DENY, "Uh-huh.  You're $host, and I'm a boil on the bottom of the 
Marquess of Queensbury's great-aunt.");
+  my ( $return_code, $msg ) = ( 0, '' );
+  # Check the results of any DNS lookups
+  if (defined($self->{_args}->{checkamx}) && 
!$conn->notes('_spamhelo_address_literal')) {
+    ( $return_code, $msg ) = $self->process_sockets;
+    if ($return_code != DECLINED) { 
+      $self->log(LOGINFO, "Denied HELO for $hello from $remote_host $remote_ip $from 
$to $msg");
+      return ($return_code, $msg);
     }
   }
+
+  $self->log(LOGINFO, "Allowed HELO for $hello from $remote_host $remote_ip $from $to 
$msg");
   return DECLINED;
 }
+
+sub process_sockets {
+  my ($self) = @_;
+  my $conn = $self->qp->connection;
+  my $hello = defined($conn->hello_host) ? $conn->hello_host : '';
+
+  return ($conn->notes('spamhelo_rcode'), $conn->notes('spamhelo_rmsg')) if 
$conn->notes('spamhelo_rcode');
+
+  my ( $result, $msg ) = ();
+  my $res = new Net::DNS::Resolver;
+  my $sel = $conn->notes('spamhelo_sockets') or return (DECLINED, '');
+
+  my $start_time = $conn->notes('_start_time');
+  my $timeout = 30;
+
+PS:
+  while ($sel->count) {
+    $self->log(LOGDEBUG, 'waiting for answers on ', $sel->count, ' sockets ...');
+    # bear in mind how long we've already waited
+    my @ready = $sel->can_read($timeout - tv_interval($start_time, [gettimeofday]));
+
+    my $wait = tv_interval($start_time, [gettimeofday]);
+    $self->log(LOGDEBUG, "waited ${wait}s for answers on " , scalar @ready, ' sockets 
...') ;
+
+    for my $socket (@ready) {
+      my $query = $res->bgread($socket);
+      $self->log(LOGDEBUG, "$hello errorstring after bgread:  ", $res->errorstring);
+      ( $result, $msg ) = ( $res->errorstring eq 'NXDOMAIN' ? DENY : DENYSOFT, "could 
not resolve $hello (" . $res->errorstring . ')') if ($res->errorstring ne 'NOERROR');
+      $sel->remove($socket);
+      undef $socket;
+  
+      if ($query) {
+        $self->log(LOGDEBUG, 'checking socket answers');
+        foreach my $rr ($query->answer) {
+          #$rr->print;
+          if ($rr->type eq 'MX') {
+            $self->log(LOGINFO, 'found MX record ' . $rr->rdatastr . " for $hello");
+            ( $result, $msg ) = (DECLINED, 'found MX record ' . $rr->rdatastr . " for 
$hello");
+            last PS;
+          } elsif ($rr->type eq 'A') {
+            #if ($rr->address =~ 
/^(0\.0\.0\.0$|127\.|10\.|224\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/) {
+            #  $self->log(LOGINFO, "A record for $hello resolved to 
private/unreachable address " . $rr->address);
+            #  ( $result, $msg ) = (DENY, "A record for $hello resolved to 
private/unreachable address.");
+            #} else {
+              $self->log(LOGINFO, 'found A record ' . $rr->address . " for $hello");
+              ( $result, $msg ) = (DECLINED, 'found A record ' . $rr->address . " for 
$hello");
+              last PS;
+            #}
+          }
+        }
+      } else {
+        $self->log(LOGDEBUG, "$hello query failed: ", $res->errorstring);
+      }
+    }
+
+    # Net::DNS 0.47 doesn't support retries or timeouts for background queries,
+    # so we'll handle the timeouts ourselves.
+    if ($wait >= $timeout) {
+      ( $result, $msg ) = (DENYSOFT, "timeout resolving $hello") unless $result;
+      $self->log(LOGDEBUG, $msg);
+      last;
+    }
+  }
+
+  # clear out any sockets
+  $self->log(LOGDEBUG, "destroying sockets");
+  $conn->notes('spamhelo_sockets', undef);
+
+  ( $result, $msg ) = (DENYSOFT, "Could not find MX or A record for $hello") unless 
$result;
+  $self->log(LOGDEBUG, "saving return values $result $msg");
+
+  return ($conn->notes('spamhelo_rcode', $result), $conn->notes('spamhelo_rmsg', 
$msg));
+}
+
+sub disconnect_handler {
+  my ($self, $transaction) = @_;
+
+  $self->qp->connection->notes('spamhelo_sockets', undef);
+
+  return DECLINED;
+}
+
+1;
+
+=head1 NAME
+
+check_spamhelo - Check a HELO argument delivered from a connecting host.
+
+=head1 DESCRIPTION
+
+Check a HELO argument delivered from a connecting host.  Reject any
+that are matched by a regexp in the badhelo config -- e.g. yahoo\.com and aol\.com, 
which
+neither the real Yahoo or the real AOL use, but which spammers use
+rather a lot. Can also provide some limited RFC2821 enforcement.
+
+=head1 CONFIGURATION
+
+=head2 Regexp HELO argument matching
+
+Add regexp patterns to the F<badhelo> configuration file; one
+per line.
+All regexp patterns will have an implied prefix of '^' and a post-fix of '$'.
+Any pattern that matches a HELO argument will cause the SMTP conversation to be 
rejected.
+
+=head2 RELAYCLIENT support
+
+If RELAYCLIENT is set then this plugin will perform no checking at all. This 
+allows pre-approved sites to use any or no argument to HELO.
+
+=head2 RFC2821 enforcement
+
+If the argument 'rfc' is given to the plugin then some limited
+enforcement of RFC2821 requirements are performed.
+In that case the argument to the HELO command must either be a FQDN or a
+properly formatted address literal. The following are valid:
+
+HELO a.b.uk
+HELO 163.com
+HELO a-b.be
+HELO [1.2.3.4]
+HELO 1.2.3.4.us
+
+The following are invalid:
+
+HELO localhost
+HELO [1.2.3.256]
+HELO a-.com
+HELO a.uk.
+HELO 1.2.3.4
+
+This is a limited implementation of RFC2821; paragraphs 4.1.1.1 & 4.1.3. Both IPv4 and
+IPv6 address literals are supported, but IPv6 are not fully validated.
+
+Passing the argument 'checkip' in addition to 'rfc' will check that the IPv4 address 
given 
+in an address literal matches the IP address of the connection. This will obviously 
also
+prevent any private/unreachable addresses used in an ipv4 literal.
+
+The config file tld is used to specify valid top level domains used in the FQDN 
validation.
+
+=head2 NOIPHC
+
+The presence of this env variable allows you to turn off host A or MX testing for 
certain
+sources.
+
+=cut
 

Reply via email to