I'm phasing in usage of reject_unknown_client_hostname.

Since I'm in the U.S., I'm giving ripe, apnic, lacnic and afrinic /8's the harsh treatment of reject_unknown_helo_hostname,reject_unknown_client_hostname. But I can't get away with using reject_unknown_client_hostname on all of the US or CA IP addresses.

So I've adapted the example Perl greylist policy server (see attached), to provide something between reject_unknown_reverse_client_hostname and reject_unknown_client_hostname.

It only tests if the rdns hostname exists as an A record, not if it matches the 
client ip.
It allows retries over a specific time interval (4xx response). Then switches to a 5xx response, to short circuit long queue lifetimes on the sending server.

I'm using this script to test against specific /8's. I was passing all traffic from these IPs to the script, but now I'm only passing "unknown's" into the policy server.

Questions:
Should I be checking for other records, besides A records?
Is it safe to use the client supplied "reverse_client_name" as a variable in my 
response?
Any glaring errors?


Also. When a unknown does pass this script, I notice that the client address is very often in the same /24. Sometimes only off by one IP.
This has me wondering if there is a patch out there that does something like 
the following.

reject_???_client_hostname (dunno, on any A record)
reject_???_client_hostname=255.255.255.0 (dunno, on match /24)
reject_???_client_hostname=255.255.0.0 (dunno, on match /16)
reject_???_client_hostname=255.0.0.0 (dunno, on match /8)
#!/usr/bin/perl

use DB_File;
use Fcntl;
use Sys::Syslog qw(:DEFAULT setlogsock);
use Net::DNS;
# use Net::IP;

#
# Usage: postfix-grey-fdns.pl [-v]
#
# Adapted from the Demo (greylisting) delegated Postfix SMTPD policy server.
#
# This provides a lighter test than reject_unknown_client_hostname
# It only tests that  the rdns name exists on a forward lookup
# rather then testing that the fnds and rdns match exactly
#
# State is kept in a Berkeley DB database.  Logging is
# sent to syslogd.
#
# To run this from /etc/postfix/master.cf:
#
#    grey_fdns  unix  -       n       n       -       -       spawn
#      user=nobody argv=/usr/bin/perl /usr/sbin/postfix-grey-fdns.pl
#
# To use this from Postfix SMTPD, use in /etc/postfix/main.cf:
#
#    smtpd_recipient_restrictions =
#       ...
#       reject_unauth_destination
#       check_policy_service unix:private/grey_fdns
#       ...
#
# NOTE: specify check_policy_service AFTER reject_unauth_destination
# or else your system can become an open relay.
#
# To test this script by hand, execute:
#
#    % perl postfix-grey-fdns.pl
#
# Each query is a bunch of attributes. Order does not matter, and
# the demo script uses only a few of all the attributes shown below:
#
#    request=smtpd_access_policy
#    protocol_state=RCPT
#    protocol_name=SMTP
#    helo_name=some.domain.tld
#    queue_id=8045F2AB23
#    sender=...@bar.tld
#    recipient=...@foo.tld
#    client_address=1.2.3.4
#    client_name=unknown
#    instance=123.456.7
#    sasl_method=plain
#    sasl_username=you
#    sasl_sender=
#    size=12345
#        reverse_client_name=some.domain.tld
#    [empty line]
#
# The policy server script will answer in the same style, with an
# attribute list followed by a empty line:
#
#    action=dunno
#    [empty line]
#
#
# grey_fdns status database and grey_fdns time interval. DO NOT create the
# grey_fdns status database in a world-writable directory such as /tmp
# or /var/tmp. DO NOT create the grey_fdns database in a file system
# that can run out of space.
#
# In case of database corruption, this script saves the database as
# $database_name.time(), so that the mail system does not get stuck.
#
# Return 4xx response during $greylist_delay, then 5xx
#
$database_name="/var/mta/grey-fdns.db";
$greylist_delay=3600;


#
# Syslogging options for verbose mode and for fatal errors.
# NOTE: comment out the $syslog_socktype line if syslogging does not
# work on your system.
#
$syslog_socktype = 'unix'; # inet, unix, stream, console
$syslog_facility="mail";
$syslog_options="pid";
$syslog_priority="info";

#
# This provides a lighter test than reject_unknown_client_hostname
# It only tests that  the rdns name exists on a forward lookup
# rather then testing that the fnds and rdns match exactly
#
sub smtpd_access_policy {

# skip all checks if client_name IS NOT unknown
        if (lc $attr{"client_name"} ne "unknown") {
       return "DUNNO";
   }
# END skip all checks

# dns lookup of reverse_client_name A record
        my $res = Net::DNS::Resolver->new(
        udp_timeout  => 2,
        retry        => 2,
);
# if any A record exists, return dunno
        my $a_query = $res->search($attr{"reverse_client_name"});
if ($a_query) { return "DUNNO";
     }
# END dns lookup


# as no dns A record exists, do everything else.
   my($key, $time_stamp, $now, $count);

   # Open the database on the fly.
   open_database() unless $database_obj;

   # Lookup the time stamp for this client/sender/recipient.
   $key =
        lc $attr{"reverse_client_name"};
   $time_stamp = read_database($key);
   $now = time();

   # If this is a new request add this client/sender/recipient to the database.
   if ($time_stamp == 0) {
        $time_stamp = $now;
        update_database($key, $time_stamp);
   }

   # The result can be any action that is allowed in a Postfix access(5) map.
   #
   # To label mail, return ``PREPEND'' headername: headertext
   #
   # In case of success, return ``DUNNO'' instead of ``OK'' so that the
   # check_policy_service restriction can be followed by other restrictions.
   #
   # In case of failure, specify ``DEFER_IF_PERMIT optional text...''
   # so that mail can still be blocked by other access restrictions.
   #
   syslog $syslog_priority, "request age %d", $now - $time_stamp if $verbose;
   if ($now - $time_stamp > $greylist_delay) {
        return "554 ".$attr{"reverse_client_name"}." does not resolve. See 
http://domain.tld/smtp/rdnsresolve.htm?rdns=".$attr{"reverse_client_name"};
   } else {
        return "450 ".$attr{"reverse_client_name"}." does not resolve See 
http://domain.tld/smtp/rdnsresolve.htm?rdns=".$attr{"reverse_client_name"};
   }
        
        return "DUNNO";
}

#
# You should not have to make changes below this point.
#
sub LOCK_SH { 1 };      # Shared lock (used for reading).
sub LOCK_EX { 2 };      # Exclusive lock (used for writing).
sub LOCK_NB { 4 };      # Don't block (for testing).
sub LOCK_UN { 8 };      # Release lock.

#
# Log an error and abort.
#
sub fatal_exit {
   my($first) = shift(@_);
   syslog "err", "fatal: $first", @_;
   exit 1;
}

#
# Open hash database.
#
sub open_database {
   my($database_fd);

   # Use tied database to make complex manipulations easier to express.
   $database_obj = tie(%db_hash, 'DB_File', $database_name,
                            O_CREAT|O_RDWR, 0644, $DB_BTREE) ||
        fatal_exit "Cannot open database %s: $!", $database_name;
   $database_fd = $database_obj->fd;
   open DATABASE_HANDLE, "+<&=$database_fd" ||
        fatal_exit "Cannot fdopen database %s: $!", $database_name;
   syslog $syslog_priority, "open %s", $database_name if $verbose;
}

#
# Read database. Use a shared lock to avoid reading the database
# while it is being changed. XXX There should be a way to synchronize
# our cache from the on-file database before looking up the key.
#
sub read_database {
   my($key) = @_;
   my($value);

   flock DATABASE_HANDLE, LOCK_SH ||
        fatal_exit "Can't get shared lock on %s: $!", $database_name;
   # XXX Synchronize our cache from the on-disk copy before lookup.
   $value = $db_hash{$key};
   syslog $syslog_priority, "lookup %s: %s", $key, $value if $verbose;
   flock DATABASE_HANDLE, LOCK_UN ||
        fatal_exit "Can't unlock %s: $!", $database_name;
   return $value;
}

#
# Update database. Use an exclusive lock to avoid collisions with
# other updaters, and to avoid surprises in database readers. XXX
# There should be a way to synchronize our cache from the on-file
# database before updating the database.
#
sub update_database {
   my($key, $value) = @_;

   syslog $syslog_priority, "store %s: %s", $key, $value if $verbose;
   flock DATABASE_HANDLE, LOCK_EX ||
        fatal_exit "Can't exclusively lock %s: $!", $database_name;
   # XXX Synchronize our cache from the on-disk copy before update.
   $db_hash{$key} = $value;
   $database_obj->sync() &&
        fatal_exit "Can't update %s: $!", $database_name;
   flock DATABASE_HANDLE, LOCK_UN ||
        fatal_exit "Can't unlock %s: $!", $database_name;
}

#
# Signal 11 means that we have some kind of database corruption (yes
# Berkeley DB should handle this better).  Move the corrupted database
# out of the way, and start with a new database.
#
sub sigsegv_handler {
   my $backup = $database_name . "." . time();

rename $database_name, $backup || fatal_exit "Can't save %s as %s: $!", $database_name, $backup;
   fatal_exit "Caught signal 11; the corrupted database is saved as $backup";
}

$SIG{'SEGV'} = 'sigsegv_handler';

#
# This process runs as a daemon, so it can't log to a terminal. Use
# syslog so that people can actually see our messages.
#
setlogsock $syslog_socktype;
openlog $0, $syslog_options, $syslog_facility;

#
# We don't need getopt() for now.
#
while ($option = shift(@ARGV)) {
   if ($option eq "-v") {
        $verbose = 1;
   } else {
        syslog $syslog_priority, "Invalid option: %s. Usage: %s [-v]",
                $option, $0;
        exit 1;
   }
}

#
# Unbuffer standard output.
#
select((select(STDOUT), $| = 1)[0]);

#
# Receive a bunch of attributes, evaluate the policy, send the result.
#
while (<STDIN>) {
   if (/([^=]+)=(.*)\n/) {
        $attr{substr($1, 0, 512)} = substr($2, 0, 512);
   } elsif ($_ eq "\n") {
        if ($verbose) {
            for (keys %attr) {
                syslog $syslog_priority, "Attribute: %s=%s", $_, $attr{$_};
            }
        }
        fatal_exit "unrecognized request type: '%s'", $attr{request}
            unless $attr{"request"} eq "smtpd_access_policy";
        $action = smtpd_access_policy();
        syslog $syslog_priority, "Action: %s", $action if $verbose;
        print STDOUT "action=$action\n\n";
        %attr = ();
   } else {
        chop;
        syslog $syslog_priority, "warning: ignoring garbage: %.100s", $_;
   }
}


Reply via email to