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", $_;
}
}