You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 4 Next »

The WrongMX Plugin

Code updated May 3, 2005

WrongMX determines if an email was sent to a lower preference MX when a higher preference MX was likely available.

Requirements

Requires Net::DNS

Code

wrongmx.cf:

loadplugin	WrongMX wrongmx.pm

header		WRONGMX eval:wrongmx()
describe	WRONGMX Sent to lower pref MX when higher pref MX was up.
tflags		WRONGMX net
score		WRONGMX 1.0

wrongmx.pm:

package WrongMX;
use strict;
use Mail::SpamAssassin;
use Mail::SpamAssassin::Plugin;
use Net::DNS;
our @ISA = qw(Mail::SpamAssassin::Plugin);

sub new {
  my ($class, $mailsa) = @_;
  $class = ref($class) || $class;
  my $self = $class->SUPER::new($mailsa);
  bless ($self, $class);
  $self->register_eval_rule("wrongmx");
  return $self;
}

sub wrongmx {
  my ($self, $permsgstatus) = @_;
  my $MAXTIMEDIFF = 30;

  return 0 if $self->{main}->{local_tests_only}; # in case plugins ever get called

  # if a user set dns_available to no we shouldn't be doing MX lookups
  return 0 unless $permsgstatus->is_dns_available();

  # avoid FPs (and wasted processing) by not checking when all_trusted
  return 0 if $permsgstatus->check_all_trusted;

  # if there is only one received header we can bail
  my $times_ref = ($permsgstatus->{received_header_times});
  return 0 if (!defined($times_ref) || scalar(@$times_ref) < 2); # if it only hit one server we're done

  # next we need the recipient domain's MX records... who's the recipient
  my $recipient_domain;
  if ($self->{main}->{username} =~ /\@(\S+\.\S+)/) {
    $recipient_domain = $1;
  } else {
    foreach my $to ($permsgstatus->all_to_addrs) {
      next unless defined $to;
      $to =~ tr/././s; # bug 3366?
      if ($to =~ /\@(\S+\.\S+)/) {
        $recipient_domain = $1;
        last;
      }
    }
  }
  return 0 unless defined $recipient_domain;  # no domain means no MX records

  # Now we need to get the recipient domain's MX records.
  # We'll resolve the hosts so we can look for IP overlaps.
  my $res = Net::DNS::Resolver->new;
  my @rmx = mx($res, $recipient_domain);
  my %mx_prefs;
  if (@rmx) {
    foreach my $rr (@rmx) {
      unless (exists $mx_prefs{$rr->exchange} && $mx_prefs{$rr->exchange} < $rr->preference) {
        $mx_prefs{$rr->exchange} = $rr->preference;
      }
      my @ips = $permsgstatus->lookup_a($rr->exchange);
      next unless @ips;
      foreach my $ip (@ips) {
        unless (exists $mx_prefs{$ip} && $mx_prefs{$ip} < $rr->preference) {
          $mx_prefs{$ip} = $rr->preference;
        }
      }
    }
  } else {
    return 0; # no recipient domain MX records found, no way to check MX flow
  }

  # get relay hosts
  my @relays;
  foreach my $rcvd (@{$permsgstatus->{relays_trusted}}, @{$permsgstatus->{relays_untrusted}}) {
    push @relays, $rcvd->{by};
  }
  return 0 if (!scalar(@relays)); # this probably won't happen, but whatever

  # Bail if we don't have the same number of relays and times, or if we have
  # fewer preferences than times (or relays).
  return 0 if (scalar(@relays) != scalar(@$times_ref) || scalar(@$times_ref) > scalar(keys(%mx_prefs)));

  # Check to see if a higher preference relay passes mail to a lower
  # preference relay within $MAXTIMEDIFF seconds.  If we do decide that a message
  # has done this, wait till AFTER we lookup the sender domain's MX records
  # to return 1 since there may be MX overlaps that we'll bail on... see below.
  # We could do the sender domain MX lookups first, but we might as well save
  # the overhead if we're going to end up bailing anyway ($hits == 0).

  # We'll go through backwards so that we can detect weird local configs
  # that pass mail from the primary MX to the secondary MX for spam/virus
  # scanning, or even final delivery.  See BACKWARDS comment below.

  # We'll resolve the 'by' hosts found to see if they match any of our
  # resolved MX hosts' IPs.

  my $hits = 0;
  my $last_pref;
  my $last_time;
  foreach (my $i = $#relays; $i >= 0; $i--) {
    my $MX = 0;
    if (exists($mx_prefs{$relays[$i]})) {
      $MX = $relays[$i];
    } else {
      my @ips = $permsgstatus->lookup_a($relays[$i]);
      next unless @ips;

      foreach my $ip (@ips) {
        if ( exists $mx_prefs{$ip} ) {
         $MX = $ip;
          last;
        }
      }
    }
    if ($MX) {
      if (defined ($last_pref) && defined ($last_time)) {
        # BACKWARDS -- uncomment the next line if you need to pass mail from a
        # higher pref MX to a lower MX (for virus scanning/etc) AND back,
        # before SA sees it... this opens you up to FNs with forged headers
     #   last if ($mx_prefs{$MX} > $last_pref);

        $hits++ if ($mx_prefs{$MX} < $last_pref
          && ($last_time - $MAXTIMEDIFF <= @$times_ref[$i] && @$times_ref[$i] <= $last_time + $MAXTIMEDIFF) ); # within max time diff
      }
      $last_pref = $mx_prefs{$MX};
      $last_time = @$times_ref[$i];
    }
    last if $hits;
  }

  # Determine the sender's domain.
  # Don't bail if we can't determine the sender since it's probably spam.
  my $sender_domain;
  foreach my $from ($permsgstatus->get('EnvelopeFrom:addr')) {
    next unless defined $from;
    $from =~ tr/././s; # bug 3366?
    if ($from =~ /\@(\S+\.\S+)/) {
      $sender_domain = $1;
      last;
    }
  }
  if (defined $sender_domain) {
 
    # Until SPF is incorporated (to better define possibly shared MX servers) we
    # might as well bail here, and save the MX lookup, if the sender domain is
    # the same as the recipient domain, since the MX records will, obviously,
    # overlap.  See below.
    return 0 if (lc($sender_domain) eq lc($recipient_domain));
 
    # Bail if the recepient and sender domains share the same MX servers.
    # If the sender's primary MX is the recipient's secondary MX we don't want
    # to penalize them.  (Comparing SPF records might also be a good idea.)
    # This will FN if spam comes with a From: as your domain.  SPF should
    # really be implemented here!
    # Ignoring the sender's MX records if an SPF lookup results in failure
    # would help to avoid FNs and should do the job since anyone with an SPF
    # record that shares your MX servers probably won't be spamming you.

    # Again, MX hosts are resolved to look for IP overlaps.
    if ($sender_domain) {
      my @smx = mx($res, $sender_domain);
      if (@smx) {
        foreach my $srr (@smx) {
          foreach my $rrr (@rmx) {
            return 0 if ($rrr->exchange eq $srr->exchange);
          }
          my @sips = $permsgstatus->lookup_a($srr->exchange);
          foreach my $sip (@sips) {
            foreach my $rip (keys %mx_prefs) {
              return 0 if ($rip eq $sip);
            }
          }
        }
      }
    }
  }

  return 1 if $hits;
  return 0;
}

1;

How To Use It

Save the two files above in your local configuration directory (/etc/mail/spamassassin/) and set the score in wrongmx.cf to whatever you desire, based on your confidence in your primary MX server stability.

How NOT To Use It

Do not use this plugin on overloaded mail systems that frequently stop accepting connections on the primary MX servers due to system load since it will cause some false positives if you set the score too high.


CategorySoftware

  • No labels