From: Frank Brehm Date: Tue, 1 Apr 2008 09:14:48 +0000 (+0000) Subject: Neu gemacht X-Git-Url: https://git.uhu-banane.de/?a=commitdiff_plain;h=9f604955d650298982aaf827ee5955198f1d65d9;p=my-stuff%2Fgreylist.git Neu gemacht git-svn-id: http://svn.brehm-online.com/svn/my-stuff/greylist@9 ec8d2aa5-1599-4edb-8739-2b3a1bc399aa --- diff --git a/greylist-test.0.txt b/greylist-test.0.txt new file mode 100644 index 0000000..67c339b --- /dev/null +++ b/greylist-test.0.txt @@ -0,0 +1,15 @@ +request=smtpd_access_policy +protocol_state=RCPT +protocol_name=SMTP +helo_name=ich +queue_id=8045F2AB23 +sender=saeger@acwain.com +recipient=frank@brehm-online.com +client_address=77.6.103.140 +client_name=acwain.net +instance=123.456.7 +sasl_method=plain +sasl_username=you +sasl_sender= +size=12345 + diff --git a/greylist-test.1.txt b/greylist-test.1.txt new file mode 100644 index 0000000..46acb7f --- /dev/null +++ b/greylist-test.1.txt @@ -0,0 +1,24 @@ +cert_fingerprint= +sasl_method= +sasl_sender= +size=0 +helo_name=[190.65.153.50] +reverse_client_name=unknown +queue_id= +encryption_cipher= +encryption_protocol= +etrn_domain= +ccert_subject= +request=smtpd_access_policy +protocol_state=RCPT +recipient=frank@brehm-online.com +sasl_username= +instance=1b88.47f145ed.dc854.0 +protocol_name=ESMTP +encryption_keysize=0 +recipient_count=0 +ccert_issuer= +sender=sebirock@web.de +client_name=unknown +client_address=190.65.153.50 + diff --git a/greylist.pl b/greylist.pl index 931e147..2a0343d 100755 --- a/greylist.pl +++ b/greylist.pl @@ -8,101 +8,196 @@ use warnings; use DBI; use Config::General; use Sys::Syslog qw(:DEFAULT setlogsock); +use File::Basename; +use Getopt::Long; +use Pod::Usage; +use Carp qw(cluck); -# -# Usage: greylist.pl -# -# Delegated Postfix SMTPD policy server. This server implements -# greylisting. State is kept in a Mysql database. 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/greylist.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 greylist.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=foo@bar.tld -# recipient=bar@foo.tld -# client_address=1.2.3.4 -# client_name=another.domain.tld -# instance=123.456.7 -# sasl_method=plain -# sasl_username=you -# sasl_sender= -# size=12345 -# [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] -# -# database format -# create table grey ( address varchar( 15 ) not null, -# sender varchar( 200) not null, -# recipient varchar(200) not null, -# count int unsigned default 1 not null, -# request_time timestamp not null, -# expire_time timestamp not null, -# primary key ( address, sender, recipient ) ); -# -# create table white ( address varchar( 15 ) not null, -# mask varchar( 15 ) not null, -# count int unsgined default 0 not null, -# descr text ); -# create table black ( address varchar( 15 ) not null, -# mask varchar( 15 ) not null, -# count int unsgined default 0 not null, -# descr text ); +=head1 NAME + +greylist.pl + +=head1 DESCRIPTION + +Delegated Postfix SMTPD policy server. This server implements greylisting. +State is kept in a Mysql database. Logging is sent to syslogd. + +=head1 SYNOPSIS + + greylist.pl [-c ] [-v[ -v...]|-D ] + greylist.pl -h + greylist.pl -V + +=head1 OPTIONS + +=over 4 + +=item B<-c > - Konfigurationsdatei + +Wenn nicht angegeben, wird C verwendet. + +=item B<-v> Verbose-Level (Debug-Level) + +Wird durch Mehrfach-Aufzaehlung erhoeht. + +=item B<-D level> - Debug-Level +Numerische Angabe des Debug-Levels. + +I: + +Die Parameter C<-v> und C<-D> wirken sich gleich aus. +Wenn beide angegeben werden, wird der hoehere von beiden verwendet. + +=item B<-h|-?> + +Gibt diesen Hilfebildschirm aus und beendet sich. + +=item B<-V> + +Gibt die Versionsnummer dieses Programms aus und beendet sich. + +=back + +=head1 DESCRIPTION + +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/greylist.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 greylist.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=foo@bar.tld + recipient=bar@foo.tld + client_address=1.2.3.4 + client_name=another.domain.tld + instance=123.456.7 + sasl_method=plain + sasl_username=you + sasl_sender= + size=12345 + [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] + + database format + create table grey ( address varchar( 15 ) not null, + sender varchar( 200) not null, + recipient varchar(200) not null, + count int unsigned default 1 not null, + request_time timestamp not null, + expire_time timestamp not null, + primary key ( address, sender, recipient ) ); + + create table white ( address varchar( 15 ) not null, + mask varchar( 15 ) not null, + count int unsgined default 0 not null, + descr text ); + create table black ( address varchar( 15 ) not null, + mask varchar( 15 ) not null, + count int unsgined default 0 not null, + descr text ); + +=cut + +my $Revis = <<'ENDE'; + $Revision$ +ENDE +$Revis =~ s/^.*:\s*(\S+)\s*\$.*/$1/s; +our $VERSION = "1.0." . $Revis; ###################### ## Global Variables ###################### -my ( $dbh, $config ); +my ( $dbh, $configfile ); ###################### # read configuration -($config) = $0 =~ m#^(.+\.)pl$#; -$config .= "cf"; -# +$configfile = dirname($0) . "/greylist.cf"; + +$| = 1; + +Getopt::Long::Configure('bundling'); + +my ( $test_mode, $help, $show_version, $cmdline_verbose, $DebugLevel, ); + +unless ( + GetOptions( + "configfile|conf|c=s" => \$configfile, + "test|t" => \$test_mode, + "help|h|?" => \$help, + "verbose|v+" => \$cmdline_verbose, + "version|V" => \$show_version, + "DebugLevel|Debug|D=i" => \$DebugLevel, + ) + ) { - my $conf; - $conf = new Config::General($config); - $config = { $conf->getall() }; + pod2usage( { -exitval => 1, -verbose => 1 } ); +} ## end unless ( GetOptions( "configfile|conf|c=s" => ... + +$cmdline_verbose ||= 0; +$cmdline_verbose = $DebugLevel if $DebugLevel and $DebugLevel > $cmdline_verbose; + +my $verbose = $cmdline_verbose; + +if ($help) { + $verbose ? pod2usage( -exitstatus => 1, -verbose => 2 ) : pod2usage(1); } +if ($show_version) { + print "Version $0: " . $VERSION . "\n"; + print "Version BsDaemon::Common: " . $BsDaemon::Common::VERSION . "\n"; + print "\n"; + exit 0; +} + +die "Konfiguration '" . $configfile . "' nicht gefunden.\n" unless -f $configfile; + +# +my $config; +debug( "reading config file '%s'", $configfile ); +eval { + my $conf; + $conf = new Config::General($configfile); + $config = { $conf->getall() }; +}; +die "Konnte Konfiguration '" . $configfile . "' nicht lesen: " . $@ . "\n" if $@; + +$verbose = 1 if $config->{'verbose'}; + $0 = 'postfix/greylist'; #### main #### @@ -116,34 +211,36 @@ openlog $0, $config->{'syslog'}{'options'}, $config->{'syslog'}{'facility'}; # # Unbuffer standard output. # -select((select(STDOUT), $| = 1)[0]); +select( ( select(STDOUT), $| = 1 )[0] ); # # Receive a bunch of attributes, evaluate the policy, send the result. # { - my ( $attr, $action ); - while () { - if (/([^=]+)=(.*)\n/) { - $attr->{substr($1, 0, 512)} = substr($2, 0, 512); - } elsif ($_ eq "\n") { - if ($config->{'verbose'}) { - foreach (keys %$attr) { - syslog $config->{'syslog'}{'priority'}, 'Attribute: %s=%s', $_, $attr->{$_}; + my ( $attr, $action ); + while () { + if (/([^=]+)=(.*)\n/) { + $attr->{ substr( $1, 0, 512 ) } = substr( $2, 0, 512 ); } - } - fatal_exit( "unrecognized request type: '%s'", $attr->{'request'} ) - unless $attr->{'request'} eq 'smtpd_access_policy'; - $action = smtpd_access_policy($attr); - syslog $config->{'syslog'}{'priority'}, 'Action: %s', $action if $config->{'verbose'}; - print STDOUT "action=$action\n\n"; - $attr = {}; - } else { - chop; - syslog $config->{'syslog'}{'priority'}, 'warning: ignoring garbage: %.100s', $_; - } - } - $dbh->disconnect() if $dbh; + elsif ( $_ eq "\n" ) { + if ($verbose) { + foreach ( sort { lc($a) cmp lc($b) } keys %$attr ) { + debug( "Attribute: %s=%s", $_, $attr->{$_} ); + } + } + fatal_exit( "unrecognized request type: '%s'", $attr->{'request'} ) + unless $attr->{'request'} eq 'smtpd_access_policy'; + $action = smtpd_access_policy($attr); + debug( "Action: %s", $action ); + print STDOUT "action=$action\n\n"; + $attr = {}; + } ## end elsif ( $_ eq "\n" ) + else { + chop; + do_log( "warning: ignoring garbage: %.100s", $_ ); + } + } ## end while () + $dbh->disconnect() if $dbh; } ######################################### @@ -155,31 +252,33 @@ select((select(STDOUT), $| = 1)[0]); # table. Request attributes are available via the %attr hash. # sub smtpd_access_policy { - my ( $param ) = @_; + my ($param) = @_; my $age; + #my ( $time_stamp, $now, $key ); # Open the database on the fly. open_database() unless $dbh; # first look up the whitelist - if ( $config->{'database'}{'whitelist_table'} && db_read_whitelist( $param ) ) { - syslog $config->{'syslog'}{'priority'}, 'request address in whitelist'; + if ( $config->{'database'}{'whitelist_table'} && db_read_whitelist($param) ) { + do_log('request address in whitelist'); if ( $config->{'greylist'}{'whitelist_header_text'} ) { return 'PREPEND X-GreyList: ' . $config->{'greylist'}{'whitelist_header_text'}; - } else { + } + else { return 'dunno'; } } - if ( $config->{'database'}{'blacklist_table'} && db_read_blacklist( $param ) ) { - syslog $config->{'syslog'}{'priority'}, 'request address in blacklist'; - return 'defer_if_permit ' . ( $config->{'greylist'}{'blacklist_reason'} || 'Service is unavailable'); + if ( $config->{'database'}{'blacklist_table'} && db_read_blacklist($param) ) { + do_log('request address in blacklist'); + return 'defer_if_permit ' . ( $config->{'greylist'}{'blacklist_reason'} || 'Service is unavailable' ); } # timestamps will be generated within the database # Lookup the time stamp for this client/sender/recipient. - $age = db_read_greylist( $param ); - unless ( $age ) { # expired or unseen + $age = db_read_greylist($param); + unless ($age) { # expired or unseen db_add_greylist($param); } @@ -193,22 +292,50 @@ sub smtpd_access_policy { # In case of failure, specify ``DEFER_IF_PERMIT optional text...'' # so that mail can still be blocked by other access restrictions. # - syslog $config->{'syslog'}{'priority'}, 'request age %d', $age; + do_log( "request age %d", $age ); if ( $age > $config->{'greylist'}{'delay'} ) { return "dunno"; - } else { + } + else { return 'defer_if_permit ' . ( $config->{'greylist'}{'delay_message'} || 'Service is unavailable' ); } -} + +} ## end sub smtpd_access_policy # # Log an error and abort. # sub fatal_exit { my $first = shift; - syslog 'err', 'fatal: '.$first, @_; + if ($test_mode) { + cluck 'fatal: ' . sprintf( $first, @_ ) . "\n"; + } + else { + syslog 'err', 'fatal: ' . $first, @_; + } $dbh->disconnect() if $dbh; exit 1; +} ## end sub fatal_exit + +# +# Log a debug message +# +sub debug { + return unless $verbose; + do_log(@_); +} + +# +# Log a message +# +sub do_log { + my $first = shift; + if ($test_mode) { + warn sprintf( $first, @_ ) . "\n"; + } + else { + syslog $config->{'syslog'}{'priority'}, $first, @_; + } } # @@ -216,105 +343,111 @@ sub fatal_exit { # sub open_database { $dbh = DBI->connect( - "DBI:".$config->{'database'}{'db_type'}.':'.$config->{'database'}{'db_name'}, + "DBI:" . $config->{'database'}{'db_type'} . ':' . $config->{'database'}{'db_name'}, $config->{'database'}{'db_user'}, $config->{'database'}{'db_pass'}, { AutoCommit => 0, RaiseError => 0, PrintError => 0 } ); fatal_exit( "Cannot open database %s: %s", $config->{'database'}{'db_name'}, $DBI::errstr ) unless $dbh; - syslog $config->{'syslog'}{'priority'}, "open db %s", $config->{'database'}{'db_name'} if $config->{'verbose'}; -} + debug( "open db %s", $config->{'database'}{'db_name'} ); +} ## end sub open_database # # Read database. Use a lock to avoid reading the database -# while it is being changed. +# while it is being changed. # sub db_read_greylist { my $param = shift; my ( $sqlst, $sth, $val, $tofast ); - $dbh->do( 'lock table '.$config->{'database'}{'greylist_table'}. ' write' ) or fatal_exit( "Cannot lock table: %s", $dbh->errstr ); + $dbh->do( 'lock table ' . $config->{'database'}{'greylist_table'} . ' write' ) or fatal_exit( "Cannot lock table: %s", $dbh->errstr ); + # lc $attr{"client_address"}."/".$attr{"sender"}."/".$attr{"recipient"}; - $sqlst = q( select unix_timestamp(now())-unix_timestamp(request_time) age from ).$config->{'database'}{'greylist_table'} . - q( where address = ? and sender = ? and recipient = ? and expire_time > now() ); - $sth = $dbh->prepare( $sqlst ) or fatal_exit( 'Cannot prepare statement: %s with error: %s', $sqlst, $dbh->errstr ); + $sqlst + = q( select unix_timestamp(now())-unix_timestamp(request_time) age from ) + . $config->{'database'}{'greylist_table'} + . q( where address = ? and sender = ? and recipient = ? and expire_time > now() ); + $sth = $dbh->prepare($sqlst) or fatal_exit( 'Cannot prepare statement: %s with error: %s', $sqlst, $dbh->errstr ); $sth->execute( @$param{qw( client_address sender recipient )} ) or fatal_exit( 'Cannot execute: %s', $dbh->errstr ); - $val = ($sth->fetchrow_hashref()||{})->{'age'}; + $val = ( $sth->fetchrow_hashref() || {} )->{'age'}; $sth->finish(); - - if ( $val ) { - $tofast = $val < $config->{'greylist'}{'delay'}; - # generate new expire time - $sqlst = 'update '.$config->{'database'}{'greylist_table'} . ' set ' . - ($tofast?'':'request_time = request_time, ') . # keep old time if old enough - 'expire_time = date_add( now(), interval ? second ), count = count + 1 where address = ? and sender = ? and recipient = ?'; - $dbh->do( $sqlst, {}, $config->{'greylist'}{$tofast?'expire_short':'expire_long'}, - @$param{qw( client_address sender recipient )} ) or - fatal_exit( "Cannot update record: %s", $dbh->errstr ); - } else { - $val = 0; # prevent undef + + if ($val) { + $tofast = $val < $config->{'greylist'}{'delay'}; + + # generate new expire time + $sqlst + = 'update ' + . $config->{'database'}{'greylist_table'} . ' set ' + . ( $tofast ? '' : 'request_time = request_time, ' ) + . # keep old time if old enough + 'expire_time = date_add( now(), interval ? second ), count = count + 1 where address = ? and sender = ? and recipient = ?'; + unless ($test_mode) { + $dbh->do( + $sqlst, {}, + $config->{'greylist'}{ $tofast ? 'expire_short' : 'expire_long' }, + @$param{qw( client_address sender recipient )} + ) or fatal_exit( "Cannot update record: %s", $dbh->errstr ); + } + } ## end if ($val) + else { + $val = 0; # prevent undef } - syslog $config->{'syslog'}{'priority'}, - 'lookup %s: %s', join('/', @$param{qw( client_address sender recipient )} ), $val if $config->{'verbose'}; + do_log( 'lookup %s: %s', join( '/', @$param{qw( client_address sender recipient )} ), $val ); $dbh->do("unlock tables"); return $val; -} +} ## end sub db_read_greylist # add a new requester to db sub db_add_greylist { my $param = shift; - my ( $sqlst ); + my ($sqlst); # replace already existing record because its expired anyway - $sqlst = 'replace into '.$config->{'database'}{'greylist_table'} . - ' (address, sender, recipient, expire_time ) values ( ?, ?, ?, date_add( now(), interval ? second ) ) '; - $dbh->do( $sqlst, {}, @$param{qw( client_address sender recipient )}, $config->{'greylist'}{'expire_short'} ) or - fatal_exit( 'Cannot insert record: %s', $dbh->errstr ); -} + $sqlst + = 'replace into ' + . $config->{'database'}{'greylist_table'} + . ' (address, sender, recipient, expire_time ) values ( ?, ?, ?, date_add( now(), interval ? second ) ) '; + unless ($test_mode) { + $dbh->do( $sqlst, {}, @$param{qw( client_address sender recipient )}, $config->{'greylist'}{'expire_short'} ) + or fatal_exit( 'Cannot insert record: %s', $dbh->errstr ); + } +} ## end sub db_add_greylist # read whitelist table to avoid greylisting sub db_read_whitelist { - my $param = shift; - open_database() unless $dbh; + my $param = shift; + open_database() unless $dbh; - my ( $sqlst, $res ); + my ( $sqlst, $res ); - $sqlst = 'update ' . $config->{'database'}{'whitelist_table'} . ' set count = count + 1' . - ' where INET_ATON( address ) & INET_ATON( mask ) = INET_ATON( ? ) & INET_ATON( mask )'; - $res = $dbh->do( $sqlst, {}, $param->{'client_address'} ) or - fatal_exit( 'Cannot update whitelist, statement is: %s with error %s', $sqlst, $dbh->errstr ); - return $res + 0; # transform number of rows to bool -} + $sqlst + = 'update ' + . $config->{'database'}{'whitelist_table'} + . ' set count = count + 1' + . ' where INET_ATON( address ) & INET_ATON( mask ) = INET_ATON( ? ) & INET_ATON( mask )'; + $res = $dbh->do( $sqlst, {}, $param->{'client_address'} ) + or fatal_exit( 'Cannot update whitelist, statement is: %s with error %s', $sqlst, $dbh->errstr ); + return $res + 0; # transform number of rows to bool +} ## end sub db_read_whitelist # read blacklist table to never let mail through sub db_read_blacklist { - my $param = shift; - open_database() unless $dbh; + my $param = shift; + open_database() unless $dbh; - my ( $sqlst, $res ); + my ( $sqlst, $res ); - $sqlst = 'update ' . $config->{'database'}{'blacklist_table'} . ' set count = count + 1' . - ' where INET_ATON( address ) & INET_ATON( mask ) = INET_ATON( ? ) & INET_ATON( mask ) limit 1'; - $res = $dbh->do( $sqlst, {}, $param->{'client_address'} ) or - fatal_exit( 'Cannot prepare statement: %s with error %s', $sqlst, $dbh->errstr ); - return $res + 0; # false if no records where updated -} + $sqlst + = 'update ' + . $config->{'database'}{'blacklist_table'} + . ' set count = count + 1' + . ' where INET_ATON( address ) & INET_ATON( mask ) = INET_ATON( ? ) & INET_ATON( mask ) limit 1'; + $res = $dbh->do( $sqlst, {}, $param->{'client_address'} ) + or fatal_exit( 'Cannot prepare statement: %s with error %s', $sqlst, $dbh->errstr ); + return $res + 0; # false if no records where updated +} ## end sub db_read_blacklist __END__ -# -# We don't need getopt() for now. -# -# maybe later add a switch for config file -# -while ($option = shift(@ARGV)) { - if ($option eq "-v") { - $verbose = 1; - } else { - syslog $syslog_priority, "Invalid option: %s. Usage: %s [-v]", - $option, $0; - exit 1; - } -} -