Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

policy server for SPF and others

0 views
Skip to first unread message

Meng Weng Wong

unread,
Dec 10, 2003, 3:32:38 AM12/10/03
to

--tKW2IUtsqtDRztdT
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

On Thu, Aug 07, 2003 at 05:30:54PM -0400, Wietse Venema wrote:
| > how about i send you a new version of smtpd-policy.pl that
| > has a standardized interface for inline plugging, with the following
| > examples:
| >
| > - existing greylisting code,
| > - syslogging as its own plugin,
| > - SPF, and
| > - a generic your-test-here template.
|
| Fine. Working code is welcome.
|

This plugin is ready, but there is a minor problem.

In examples/smtpd-policy/smtpd-policy.pl, there is

return "defer_if_permit Service is unavailable";

I'm doing much the same thing:

return "defer_if_permit Greylisting delay; please try again after $GREYLIST_DELAY seconds.";

postfix-policyd[3743]: decided action=defer_if_permit Greylisting delay; please try again after 60 seconds.

postfix/smtpd[2527]: warning: unknown smtpd restriction: "Greylisting"

From SMTPD_POLICY_README,

The policy server replies with any action that is allowed in a
Postfix SMTPD access table.

Postfix appears to be trying to interpret the string after
defer_if_permit as a list of restrictions, not error text to be returned
to the SMTP client. What am I doing wrong?

It works correctly when I replace DEFER_IF_PERMIT with REJECT and DEFER:

for the "testing" plugin:

return "REJECT smtpd-policy blocking $attr{recipient}";

postfix-policyd[4978]: decided action=REJECT smtpd-policy blocking mengwong+p...@dumbo.pobox.com

>> MAIL FROM:<meng...@dumbo.pobox.com>
<< 250 Ok
>> RCPT TO:<mengwong+p...@dumbo.pobox.com>
<< 554 <mengwong+p...@dumbo.pobox.com>: Recipient address rejected: smtpd-policy blocking mengwong+p...@dumbo.pobox.com

for the SPF plugin:

return "REJECT " . ($smtp_comment || $header_comment);

postfix-policyd[5135]: decided action=REJECT domain of meng...@vw.mailzone.com does not designate 208.210.125.227 as permitted sender

>> MAIL FROM:<meng...@vw.mailzone.com>
<< 250 Ok
>> RCPT TO:<mengwong+p...@dumbo.pobox.com>
<< 554 <mengwong+p...@dumbo.pobox.com>: Recipient address rejected: domain of meng...@vw.mailzone.com does not designate 208.210.125.227 as permitted sender

Apart from that, the pluggable policy server is ready. If the bug can
be found in time, I propose we bundle it into the 2.1 distribution.

I will publish Colander, the pluggable per-user SMTP proxy, when I have
it patched to work with XCLIENT.


--tKW2IUtsqtDRztdT
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename=postfix-policyd

#!/usr/bin/perl

use DB_File;
use Fcntl;
use Sys::Syslog qw(:DEFAULT setlogsock);
use strict;
use Mail::SPF::Query;

# ----------------------------------------------------------
# configuration
# ----------------------------------------------------------

my @HANDLERS = (
"testing",
"sender_permitted_from",
"greylisting",
);

my $VERBOSE = 1;

my @Greylisting_Whitelisted_Senders =
(
qr(^postmaster)i,
);

#
# Syslogging options for verbose mode and for fatal errors.
# NOTE: comment out the $syslog_socktype line if syslogging does not
# work on your system.
#

my $syslog_socktype = 'unix'; # inet, unix, stream, console
my $syslog_facility = "mail";
my $syslog_options = "pid";
my $syslog_priority = "info";

#
# greylist status database and greylist time interval. DO NOT create the
# greylist status database in a world-writable directory such as /tmp
# or /var/tmp. DO NOT create the greylist 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.
#
my $database_name="/var/spool/postfix/smtpd-policy.db";
my $GREYLIST_DELAY=60;

# ----------------------------------------------------------
# minimal documentation
# ----------------------------------------------------------

#
# Usage: smtpd-policy.pl [-v]
#
# Demo delegated Postfix SMTPD policy server. This server
# implements greylisting and SPF.
#
# State for greylisting is kept in a Berkeley DB database.
#
# The SPF handler uses Mail::SPF::Query to do the heavy lifting.
#
# Logging is sent to syslogd.
#
# How it works: each time a Postfix SMTP server process is started
# it connects to the policy service socket, and Postfix runs one
# instance of this PERL script. By default, a Postfix SMTP server
# process terminates after 100 seconds of idle time, or after serving
# 100 clients. Thus, the cost of starting this PERL script is smoothed
# out over time.
#
# To run this from /etc/postfix/master.cf:
#
# policy unix - n n - - spawn
# user=nobody argv=/usr/bin/perl /usr/libexec/postfix/smtpd-policy.pl
#
# To use this from Postfix SMTPD, use in /etc/postfix/main.cf:
#
# smtpd_recipient_restrictions =
# ...
# reject_unauth_destination
# check_policy_service unix:private/policy
# ...
#
# 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 smtpd-policy.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=f...@bar.tld
# recipient=b...@foo.tld
# client_address=1.2.3.4
# client_name=another.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]
#

# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: client_address=208.210.125.227
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: client_name=newbabe.mengwong.com
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: helo_name=newbabe.mengwong.com
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: protocol_name=ESMTP
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: protocol_state=RCPT
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: queue_id=
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: recipient=meng...@dumbo.pobox.com
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: request=smtpd_access_policy
# Jul 23 18:43:29 dumbo/dumbo policyd[21171]: Attribute: sender=meng...@newbabe.mengwong.com

# ----------------------------------------------------------
# initialization
# ----------------------------------------------------------

#
# Log an error and abort.
#
sub fatal_exit {
syslog(err => "fatal_exit: @_");
syslog(warn => "fatal_exit: @_");
syslog(info => "fatal_exit: @_");
die "fatal: @_";
}

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

#
# 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 $database_name as $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;

# ----------------------------------------------------------
# main
# ----------------------------------------------------------

#
# Receive a bunch of attributes, evaluate the policy, send the result.
#
my %attr;
while (<STDIN>) {
chomp;
if (/=/) { my ($k, $v) = split (/=/, $_, 2); $attr{$k} = $v; next }
elsif (length) { syslog(warn=>sprintf("warning: ignoring garbage: %.100s", $_)); next; }

if ($VERBOSE) {
for (sort keys %attr) {
syslog(debug=> "Attribute: %s=%s", $_, $attr{$_});
}
}

fatal_exit ("unrecognized request type: '$attr{request}'") unless $attr{request} eq "smtpd_access_policy";

my $action = "ok";
my %responses;
foreach my $handler (@HANDLERS) {
no strict 'refs';
my $response = $handler->(attr=>\%attr);
syslog(debug=> "handler %s: %s", $handler, $response);
if ($response !~ /^(ok|dunno)/i) {

syslog(info=> "handler %s: %s is decisive.", $handler, $response);
$action = $response; last;
}
}

syslog(info=> "decided action=%s", $action);

print STDOUT "action=$action\n\n";
%attr = ();
}

# ----------------------------------------------------------
# plugin: SPF
# ----------------------------------------------------------
sub sender_permitted_from {
local %_ = @_;
my %attr = %{ $_{attr} };

my $query = new Mail::SPF::Query (ip =>$attr{client_address},
sender=>$attr{sender},
helo =>$attr{helo_name});
my ($result, $smtp_comment, $header_comment) = $query->result();

syslog(info=>"%s: SPF %s: smtp_comment=%s, header_comment=%s",
$attr{queue_id}, $result, $smtp_comment, $header_comment);

if ($result eq "pass") { return "DUNNO"; }
elsif ($result eq "fail") { return "REJECT " . ($smtp_comment || $header_comment); }
elsif ($result eq "error") { return "DUNNO"; }
else { return "DUNNO"; }

# TODO XXX: prepend Received-SPF header.
}

# ----------------------------------------------------------
# plugin: testing
# ----------------------------------------------------------
sub testing {
local %_ = @_;
my %attr = %{ $_{attr} };

if (lc address_stripped($attr{sender}) eq
lc address_stripped($attr{recipient})
and
$attr{recipient} =~ /policyblock/) {

syslog(info=>"%s: testing: will block as requested",
$attr{queue_id});
return "REJECT smtpd-policy blocking $attr{recipient}";
}
else {
syslog(info=>"%s: testing: stripped sender=%s, stripped rcpt=%s",
$attr{queue_id},
address_stripped($attr{sender}),
address_stripped($attr{recipient}),
);

}
return "DUNNO";
}

sub address_stripped {
# my $foo = localpart_lhs('foo...@baz.com'); # returns 'f...@baz.com'
my $string = shift;
for ($string) {
s/[+-].*\@/\@/;
}
return $string;
}

# ----------------------------------------------------------
# plugin: greylisting
# ----------------------------------------------------------

my $Database_Obj;
my %DB_Hash;

#
# Demo SMTPD access policy routine. The result is an action just like
# it would be specified on the right-hand side of a Postfix access
# table. Request attributes are available via the %attr hash.
#
sub greylisting {
local %_ = @_;
my %attr = %{ $_{attr} };

my($key, $time_stamp, $now);

return "DUNNO" if grep { $attr{sender} =~ $_ } @Greylisting_Whitelisted_Senders;

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

# Lookup the time stamp for this client/sender/recipient.
$key = lc join "/", @attr{qw( client_address sender recipient )};
$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);
}

# 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 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) {
syslog(debug=> "handler %s: %s showed up in the database more than $GREYLIST_DELAY seconds ago.",
"greylisting", $key);
return "dunno";
} else {
syslog(debug=> "handler %s: %s has not been in the database $GREYLIST_DELAY seconds. denying.",
"greylisting", $key);
return "defer_if_permit Greylisting delay; please try again after $GREYLIST_DELAY seconds.";
}
}

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

#
# 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 while running as $>: $!", $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;
}

--tKW2IUtsqtDRztdT--

0 new messages