On Mon, 14 Jun 2004, Mark Powell wrote:

> On Sun, 13 Jun 2004, Keith C. Ivey wrote:
> My finger trouble. I meant '\.?$'. The argument is supposed to be a FQDN,
> so it may end with a dot char. Thus any pattern you match, must match the
> same with and without a dot at the end.

Actually on a proper reading of RFC2821, it's clear that a '.' is not
allowed at the end of a HELO argument :(
  Here is the latest check_spamhelo that surrounds each regexp, with
simply '^' & '$'. Slashes are not needed to surround a regexp anymore. See
the attached badhelo for some examples.
  In addition this version now performs a complete check on a FQDN to make
sure it completely matches the allowed RFC2821 format e.g. a-.uk, -.com,
a.b., a_b.hello are not allowed.
  Passing the argument 'checkip', in addition to 'rfc', will also check
that the IP address given in an IPv4 address literal actually matches that
of the connection.
  It's up to you where you put the line in config/plugins. If you put it
before dnsbl, then the error reported during the conversation and in the
logs, will be due to any bad HELO, even if an RBL check was positive.
Putting it after gets you the opposite result.
  I put following line in my config/plugins:

check_spamhelo delay rfc checkip

just after dnsbl as I prefer to know whether any email was prevented due
to that rather than badhelo. This allows logging of the message details,
only allows proper FQDN or IPv4/IPv6 address literals and checks that an
IPv4 address literal corresponds to the connection. This greatly reduces
what you have to check for in badhelo, as everything illegal is already
rejected.
  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
cpan\.org
msn\.com
#
# Block helos pretending to be in our domain
.*example\.com
#
# 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.
#\[1\.2\.\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+\]
#
# Some more regexps to catch out other baddies
.*\.prod-infinitum\.com\.mx
[A-Z\d]{8}\.ipt\.aol\.com
netscape\d+\.com
=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. The RELAYCLIENT check can 
be disabled 
by passing the plugin the arg 'checkrc', in which case all HELOs will be checked. This 
provided
primarliy for testing. You should be using tcpserver to determine which IP addresses 
get RELAYCLIENT
set.

=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.

=head2 Delaying failure

If the argument 'delay' is passed to the plugin then any denial will be
delayed until the RCPT TO stage. This allows the logging of MAIL FROM and RCPT TO
on any connections that you have chosen to fail. 

=cut

use constant SPAMHELO_RFCFAIL => 1;
use constant SPAMHELO_MATCHFAIL => 2;

sub register {
  my ($self, $qp, @args) = @_;
  $self->register_hook("helo", "check_helo");
  $self->register_hook("ehlo", "check_helo");

  $self->{_spamhelo_rfc} = 0;
  $self->{_spamhelo_delayfailure} = 0;
  $self->{_spamhelo_checkrc} = 0;
  $self->{_spamhelo_badhelo} = 0;

  while (@args) {
    my $arg = shift @args;

    $self->{_spamhelo_rfc} = 1, next if ($arg eq 'rfc');
    $self->{_spamhelo_delayfailure} = 1, next if ($arg eq 'delay');
    $self->{_spamhelo_checkrc} = 1, next if ($arg eq 'checkrc');
    $self->{_spamhelo_checkip} = 1, next if ($arg eq 'checkip');
  }
      
  $self->register_hook("rcpt", "rcpt_handler") if $self->{_spamhelo_delayfailure};
}

sub check_helo {
  my ($self, $transaction, $hello) = @_;
  my $remote_ip = $self->qp->connection->remote_ip;

  return (DECLINED) if !$self->{_spamhelo_checkrc} && 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)
 {
  # are we are going to enforce RFC2821 and check for a correctly formatted FQDN or 
address literal
  if ($self->{_spamhelo_rfc}) {
    my $rfc2821 = 0;

    # check for proper FQDN or IPv6 literal
    if ($hello =~ 
/^(((?!-)[a-z\d\-]+(?<!-)\.)+[a-z]{2,}|\[[a-z\d\-]*[a-z\d]:([^\\\[\]]|\\.)+\])$/i) {
      $rfc2821 = 1;
    } else {
      # check for IPv4 literal
      if ($hello =~ /\[(((?(?<!\[)\.)(25[0-5]|2[0-4]\d|[01]?\d?\d)){4})\]/) {
        my $ip = $1;
        if ($self->{_spamhelo_checkip}) {
          $ip =~ s/(?<!\d)0+(?!\.)//g;  # remove leading zeroes from each octet
          $self->log(LOGDEBUG, "ip = $ip remote_ip = $remote_ip");
          $rfc2821 = 1 if ($ip eq $remote_ip);
        } else { 
          $rfc2821 = 1;
        }
      }
    }

    if (!$rfc2821) {
      if ($self->{_spamhelo_delayfailure}) {
        $self->{_spamhelo_badhelo} = SPAMHELO_RFCFAIL;
        $self->{_spamhelo_hello} = $hello;
        return DECLINED;
      } else {
        $self->log(LOGDEBUG, rfc_fail_log_mes($hello, $remote_ip));
        return (DENY, rfc_fail_con_mes($hello));
      }
    }
  }


  # perform match
  for my $re ($self->qp->config('badhelo')) {
    if ($hello =~ /^$re$/i) {
      if ($self->{_spamhelo_delayfailure}) {
        $self->{_spamhelo_badhelo} = SPAMHELO_MATCHFAIL;
        $self->{_spamhelo_hello} = $hello;
        $self->{_spamhelo_pattern} = $re;
        return DECLINED;
      } else {
        $self->log(LOGDEBUG, match_fail_log_mes($hello, $re));
        return (DENY, match_fail_con_mes($hello, $re));
      }
    }
  }

  return DECLINED;
}

sub rcpt_handler {
  my ($self, $transaction, $rcpt) = @_;
  my $hello = $self->{_spamhelo_hello};
  my $pattern = $self->{_spamhelo_pattern};
  my $remote_ip = $self->qp->connection->remote_ip;

  if ($self->{_spamhelo_badhelo} == SPAMHELO_RFCFAIL) {
    $self->log(LOGDEBUG, rfc_fail_log_mes($hello, $remote_ip));
    return (DENY, rfc_fail_con_mes($hello));
  } elsif ($self->{_spamhelo_badhelo} == SPAMHELO_MATCHFAIL) {
    $self->log(LOGDEBUG, match_fail_log_mes($hello, $pattern));
    return (DENY, match_fail_con_mes($hello, $pattern));
  }

  return DECLINED;
}

sub rfc_fail_log_mes {
  my ( $hello, $remote_ip ) = @_;
  return "Denied HELO for $hello from " . $remote_ip . " not a valid FQDN or address 
literal";
}

sub rfc_fail_con_mes {
  my ( $hello ) = @_;
  return "HELO $hello is not a valid FQDN or your valid address literal: see 
RFC821/1123/2821.";
}

sub match_fail_log_mes {
  my ( $hello, $pattern ) = @_;
  return "Denied HELO for $hello by $pattern";
}

sub match_fail_con_mes {
  my ( $hello, $pattern ) = @_;
  return "HELO $hello not accepted here.";
}

Reply via email to