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.";
}