Nachdem aus dem OTRS in der Nacht 25.000 E-Mails versand wurden, hatte uns unser Provider vorübergehend auf seinen Relay Servern blacklistet.
Um solche nächtliche Überraschungen künftig zu vermeiden, habe ich einen Policy Server für Postfix geschrieben. Dieser wird auf dem ausgehenden Mail Relay eingebunden und gibt nach 750 E-Mails pro Stunde dem Exchange DEFER zurück gibt, worauf dieser die Mails in seiner Queue behält.
Der Policy Server liest aus der Datei /tmp/postfix-counter den aktuellen Count und den Timestamp des letzten Counterreset. Ist der Timestamp älter als eine Stunde, wird der Counter auf 1 gesetzt, sonst um 1 erhöht und wieder in die Datei zurückgeschrieben.
Zur Überwachung habe ich einen einfachen Nagios Check geschrieben, welcher den Counter ausliest, die Anzahl Mails prüft und als Performancedaten ausgibt. Die enstandenen PNP Graphen sehen dann wie Sägezähne aus.
Im Postfix wird das Ganze in der main.cf eingebunden.
smtpd_recipient_restrictions = permit_mynetworks check_policy_service inet:127.0.0.1:8895 reject_unauth_destination
#! /usr/bin/perl -W # based on sample from Postfix Page use strict; use Errno; use IO::Select; use IO::File; my $fds; my $flog; my $conffile; my %times = (); my $maxcount = 750; # max mail per hour sub init_sockets; # initialize anything (database etc) sub init() { $fds = IO::Select->new() or die "unable to create IO::Select object\n"; init_sockets('inet:127.0.0.1:8895'); use IO::File; $flog = IO::File->new("/tmp/smtpd-policy.log", 'a'); } init(); sub request($;$) { my ($attr,$sock) = @_; my $act = 'DUNNO'; my $now = time; open(FILE, "/tmp/postfix-counter"); while(<FILE>){ chomp($_); my ($key, $val) = split(/:/, $_); $times{$key} = $val; } close(FILE); if ($times{'1htime'} + 3600 > $now){ if ($times{'1hcount'} < $maxcount){ my $tmp = $times{'1hcount'}; $tmp = $tmp + 1; $times{'1hcount'} = $tmp; $act = 'OK'; $times{'now'} = $now; }else{ $act = 'DEFER to many mail'; $times{'now'} = $now; } }else{ $times{'1htime'} = $now; $times{'1hcount'} = 1; $act = 'OK'; $times{'now'} = $now; } open(FILE, ">/tmp/postfix-counter"); for my $key ( keys %times ) { my $value = $times{$key}; print FILE "$key:$value\n"; } close(FILE); if (defined $flog) { $flog->print("request:"); $flog->print(" $_=$attr->{$_}") foreach keys %$attr; $flog->print(" action=$act\n"); $flog->flush; } $act; } sub request_cb($) { my $s = shift; my $r = ${*$s}{attrs} || ( ${*$s}{attrs} = {} ); for(;;) { $_ = $s->getline; unless (defined $_) { $fds->remove($s) unless $!{EAGAIN}; return; } if (/^([a-zA-Z_]+)=([^\r\n]*)\r?\n$/) { $r->{$1} = $2; } elsif (/^\r?\n$/) { last; } else { $r->{error} = 1; } } ${*$s}{attrs} = undef; my $act; if ($r->{error}) { $act = 'ERROR unknown request line'; } elsif (!exists($r->{request}) || $r->{request} ne 'smtpd_access_policy') { $act = 'ERROR required request attribute is not present'; } else { $act = request($r); unless (defined $act) { $fds->remove($s); return; } } $s->print("action=$act\n\n") or $fds->remove($s); } sub accept_cb($) { my $ls = shift; my $s = $ls->accept; if ($s) { $s->blocking(0); ${*$s}{cb} = \&request_cb; $fds->add($s); } } sub init_sockets { foreach my $sock ( @_ ) { my $s; if ($sock =~ /^inet:([^:]+:.+)$/i) { use IO::Socket::INET; $s = IO::Socket::INET->new( LocalAddr => $1, Proto => 'tcp', Type => SOCK_STREAM, Listen => 5, ReuseAddr => 1, ); } elsif ($sock =~ /^unix:(.+)$/) { use IO::Socket::UNIX; unlink $1; $s = IO::Socket::UNIX->new( Type => SOCK_STREAM, Local => $1, Listen => 5, ); } else { die "invalid listening point specification: $sock\n"; } die "unable to create listening socket for $sock: $!\n" unless $s; $s->blocking(0); ${*$s}{cb} = \&accept_cb; $fds->add($s); } } for(;;) { foreach my $s ( $fds->can_read() ) { &{${*$s}{cb}}($s); } }