qpsmtpd - ein Tutorial

Diesen Vortrag habe ich bei den Linuxwochen 2004 in Eisenstadt gehalten. Es gibt auch eine Englische Übersetzung.

Was ist qpsmtpd?

qpsmtpd ist ein in Perl geschriebener SMTP-Server mit einem einfachen aber mächtigen Plugin-System. Ursprünglich als Replacement für den SMTP-Server von qmail geschrieben, kann es auch mit Postfix oder Sendmail als "Backend" betrieben werden.

Das Plugin-System erlaubt es, sehr schnell und einfach Filter zu schreiben, dementsprechend gibt es auch bereits etliche, hauptsächlich zur Spam- und Virenabwehr (Blackhole-Lists, SpamAssassin, Greylisting, ClamAV, ...)

Was ist qpsmtpd nicht?

Eine out-of-the-box Solution. Wer es verwendet, muss damit rechnen, selbst Filter schreiben oder vorhandene anpassen zu müssen. Oder umgekehrt: Wer selber Filter schreiben will oder muss, sollte sich qpsmtpd ansehen (Perl ist doch etwas lesbarer als sendmail-Rules).

Zielkonfiguration

Wie bereits erwähnt, kann qpsmtpd mit verschiedenen Backends betrieben werden. qmail ist sicherlich das am besten getestete, ist aber auf Grund der Lizenzbestimmungen in vielen Linux-Distributionen nicht enthalten. Das Postfix-Plugin stammt vom Autor dieses Tutorials und verarbeitet täglich einige tausend Mails. Das SMTP-Plugin (das man für sendmail verwenden kann) ist schlampig geschrieben und sollte nur unter bestimmten Umständen im Produktiv-Einsatz eingesetzt werden.

Daher wird sich dieses Tutorial auf die Kombination xinetd, qpsmtpd und postfix beschränken.

Vorbereitungen

Postfix

Zunächst muss Postfix installiert werden. Das dürfte in den meisten Linux-Installationen enthalten sein und kann daher mit dem Paketmanager Deines Vertrauens installiert werden.

Da wir aber den smtpd von Postfix durch qpsmtpd ersetzen wollen, müssen wir dafür sorgen, dass postfix nicht auf Port 25 lauscht. Dazu kann man entweder die Zeile

smtp      inet  n       -       n       -       -       smtpd

im /etc/postfix/master.cf auskommentieren oder inet_interfaces in /etc/postfix/main.cf geeignet einschränken (manchmal ist es sinnvoll, qpsmtpd am externen Interface lauschen zu lassen und postfix auf localhost oder am internen).

Perl-Module

Welche Perl-Module man braucht, hängt natürlich von den verwendeten Modulen ab, aber auf jeden Fall braucht man Mail::Address und Net::DNS. Wenn sie bereits in der Distribution enthalten sind, ist wieder der Packagemanager zu bemühen, sonst kann man sie einfach über CPAN beziehen:

localhost:/ 0:13 127# perl -MCPAN -e shell
Terminal does not support AddHistory.

cpan shell -- CPAN exploration and modules installation (v1.7601)
ReadLine support available (try 'install Bundle::CPAN')

cpan> install Mail::Address
CPAN: Storable loaded ok
Going to read /var/cache/cpan/Metadata
  Database was generated on Mon, 17 May 2004 15:32:57 GMT
CPAN: LWP::UserAgent loaded ok
Fetching with LWP:
  ftp://cpan.inode.at/authors/01mailrc.txt.gz
Going to read /var/cache/cpan/sources/authors/01mailrc.txt.gz
Fetching with LWP:
  ftp://cpan.inode.at/modules/02packages.details.txt.gz
Going to read /var/cache/cpan/sources/modules/02packages.details.txt.gz
  Database was generated on Tue, 18 May 2004 20:34:45 GMT
Fetching with LWP:
  ftp://cpan.inode.at/modules/03modlist.data.gz
Going to read /var/cache/cpan/sources/modules/03modlist.data.gz
Going to write /var/cache/cpan/Metadata
Mail::Address is up to date.

cpan> install Net::DNS
Net::DNS is up to date.

cpan> quit
Terminal does not support GetHistory.
Lockfile removed.
perl -MCPAN -e shell  10.23s user 0.87s system 24% cpu 44.812 total

(über die Probleme beim Mischen verschiedener Packagemanagementsysteme kann ein andermal diskutiert werden)

Qpsmtpd Installation

Download von http://smtpd.develooper.com/get.html.

Man lege einen eigenen User an:

useradd -G postdrop smtpd

Die Gruppe postdrop ist wichtig, sonst kann der qpsmtpd keine Mails an Postfix weiterleiten!

Dann wird der qpsmtpd entpackt:

cd ~smtpd
tar xvfz ~/tmp/qpsmtpd-0.27.1.tar.gz
chown -R root:root qpsmtpd-0.27.1 
ln -s qpsmtpd-0.27.1 qpsmtpd 

Und damit das ganze noch ein bisschen FHS-konformer wird, verlagern wir Konfiguration und Logfiles nach /etc bzw. /var.

mkdir /var/log/qpsmtpd 
chown smtpd:wheel /var/log/qpsmtpd
chmod 750 /var/log/qpsmtpd
rm -rf qpsmtpd/log     
ln -s /var/log/qpsmtpd qpsmtpd/log 

mv qpsmtpd/config /etc/qpsmtpd
ln -s /etc/qpsmtpd qpsmtpd/config

Außerdem braucht der User smtpd ein eigenes tmp-Directory:

mkdir ~smtpd/tmp
chown smtpd ~smtpd/tmp
chmod 700 ~smtpd/tmp

Patches

Für den Betrieb mit xinetd ist ein kleiner Patch notwendig, der leider noch nicht in der Version 0.27.1 enthalten ist. Der Tarball enthält auch eine Beispielkonfiguration für xinetd, sowie ein Wrapperscript für das Anlegen der Logfiles.

Verschiedene andere Plugins benötigen ebenfalls Patches.

xinetd

Falls notwendig, installieren. Die Konfiguration für das Service smtp muss man eventuell auch anpassen (z.B. nur an bestimmte Interfaces binden).

Konfiguration

Alle Konfiguratiosfiles liegen in ~smtpd/qpsmtpd/config bzw. in /etc/qpsmtpd. Das wichtigste heißt plugins und listet alle Plugins auf, die aufgerufen werden sollen.

Hier ist natürlich queue/qmail-queue durch queue/postfix-queue zu ersetzen.

Testen

% telnet myhost smtp
Trying 192.168.1.2
Connected to localhost.
Escape character is '^]'.
220 localhost.localdomain ESMTP qpsmtpd 0.27.1 ready; send us your mail, but not your spam.
ehlo x
250-localhost.localdomain Hi localhost.localdomain [127.0.0.1]
250-PIPELINING
250 8BITMIME
mail from: <>
250 <>, sender OK - how exciting to get mail from you!
rcpt to: 
250 hjp@myhost, recipient ok
data
354 go ahead

.
250 Queued!  (Queue-Id: 5C3A4170031)
quit
221 localhost.localdomain closing connection. Have a wonderful day.
Connection closed by foreign host.

Anschließend sollte eine Mail in der Inbox des Users hjp liegen.

Weitere Plugins

Unbedingt notwendig ist (meiner ganz und gar nicht bescheidenen Meinung nach) ein Plugin, das bereits während des SMTP-Dialogs feststellt, ob der Empfänger existiert - Mails zuerst annehmen und dann einen Bounce generieren ist in Zeiten wie diesen, in denen es von Spam und Würmern wimmelt, böse™. Dafür gibt es mehrere, die die Liste der lokalen User lokalen Quellen entnehmen, z.B. der qmail-Konfiguration oder der vpopmail-Datenbank. Interessanterweise ist keines davon in der Distribution enthalten, man muss sich die also aus anderen Quellen herunterladen. Ich verwende mein eigenes aliases-Plugin, das die lokalen User einem einfachen Textfile /etc/qpsmtpd/aliases entnimmt. Das Format war vom Sendmail-Aliases-File inspiriert (daher der Name) hat sich aber deutlich geändert. Ein typischer Ausschnitt aus einem aliases-File sieht so aus:

hjp,peter.holzer@wsr.ac.at,wifo.at: hjp@asherah.wsr.ac.at (denysoft_greylist, spamassassin_reject_threshold=10)
postmaster@,wsr.ac.at,wifo.at: sysadm@wsr.ac.at
sysadm@wsr.ac.at: hjp@wsr.ac.at,gina@wsr.ac.at

Hier sind alle Kombinationen von hjp und peter.holzer mit den Domains wsr.ac.at und wifo.at äquivalent und werden an hjp@asherah.wsr.ac.at zugestellt, wobei die Optionen denysoft_greylist und spamassassin_reject_threshold gesetzt werden (die dann von anderen Plugins ausgewertet werden könnten). Ähnlich für postmaster, das aber auch ohne Angabe einer Domain gültig ist (erkennbar an der leeren Domain zwischen dem @ und dem ersten Komma). Aliases werden rekursiv aufgelöst, bis sich eine Adresse nicht mehr weiter auflösen lässt. Ein Local Part kann auch * sein, in diesem Fall gilt das Alias für alle nicht explizit aufgeführten Usernamen.

Sehr effektiv gegen aktuelle Spambots ist auch das Greylisting-Plugin.

Da dieses Plugin eine History mitführen muss, muss man entweder das Config-Directory für smtpd schreibbar machen (schlechte Idee) oder ein für smtpd schreibbares Directory ~smtpd/qpsmtpd/var/db anlegen.

Außerdem gibt es einige MTAs, die mit Greylisting überhaupt nicht zurechtkommen, z.B. die von Chello oder Groupwise-Server. Man wird also nicht umhinkommen, eine Whitelist zu führen. Dazu dient das Plugin client_options. (Oder Gavin Carrs whitelist-Plugin).

% telnet localhost smtp
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 localhost.localdomain ESMTP qpsmtpd 0.27.1 ready; send us your mail, but not your spam.
helo foo
250 localhost.localdomain Hi localhost.localdomain [127.0.0.1]; I am so happy to meet you.
mail from: <>
250 <>, sender OK - how exciting to get mail from you!
rcpt to: 
450 This mail is temporarily denied
quit
221 localhost.localdomain closing connection. Have a wonderful day.

Nach Ablauf der blacklist-Periode:

% telnet localhost smtp
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 localhost.localdomain ESMTP qpsmtpd 0.27.1 ready; send us your mail, but not your spam.
helo foo
250 localhost.localdomain Hi localhost.localdomain [127.0.0.1]; I am so happy to meet you.
mail from: <>
250 <>, sender OK - how exciting to get mail from you!
rcpt to: 
250 hjp@foo, recipient ok
rcpt to: 
250 postmaster@foo, recipient ok
data
354 go ahead
Subject: bliub

.
250 Queued!  (Queue-Id: A3BC1170031)
quit

Plugins schreiben

Die Grundidee ist einfach: Für jedes Kommando (und einige andere Events) werden Methoden aufgerufen, die das Plugin vorher registriert hat. Diese Methoden können den Zustand der Transaction ändern (insbesondere Notes abspeichern und abfragen), und schlussendlich einen der folgenden Werte zurückliefern.

DECLINED
Geht micht nichts an, mach weiter
OK
Kommando erfolgreich
DENYSOFT
Kommando fehlgeschlagen, Client kann es aber später wieder probieren.
DENY
Kommando fehlgeschlagen, Client soll es nicht mehr probieren.
DENYHARD
Kommando fehlgeschlagen und Verbindung zum Client soll abgebrochen werden.

Am einfachsten einfach die Plugins im Plugin-Directory ansehen und modifizieren.

addsig

Dieses einfache Plugin hängt einfach an alle Mails eine zur Absenderdomain passende Signatur an.

sub register {
  my ($self, $qp) = @_;
  $self->register_hook("data_post", "addsig");
}

sub addsig {
  my ($self, $transaction) = @_;

  my $domain = $transaction->sender->host;

  $domain =~ tr/-a-zA-Z0-9.//cd;

  if (open(F, "</etc/qpsmtpd/sigs/$domain")) {
    while (<F>) {
      $transaction->body_write($_);
    }
    close(F);
  }

  return (DECLINED);
}

count_denies

Dieses Plugin ist etwas komplizierter: Es wird jedesmal aufgerufen, wenn ein anderes Plugin DENY oder DENYSOFT zurückliefert, und erhöht dabei einen Zähler, der als Note abgespeichert wird. Bei jedem Empfänger wird dieser Zähler überprüft und nach einer konfigurierbaren Anzahl an Fehlern wird die Connection abgebrochen.


=head1 NAME

count_denies - Count denies and disconnect when we have too many

=head1 DESCRIPTION

Disconnect the client if it receives too many denies.
Good for rejecting thwarting dictionary attacks.

=head1 CONFIGURATION

Takes one parameter, the number of allowed denies
before we disconnect the client.  Defaults to 4.

=cut

sub register {
  my ($self, $qp, @args) = @_;
  $self->register_hook("deny", "check_deny");
  $self->register_hook("rcpt", "check_rcpt");

  if (@args > 0) {
    $self->{_deny_max} = $args[0];
    $self->log(1, "WARNING: Ignoring additional arguments.") if (@args > 1);
  } else {
    $self->{_deny_max} = 4;
  }

  $qp->connection->notes('deny_count', 0);

}

sub check_deny {
  my ($self, $cmd) = @_[0,2];
  
  my $deny_count = $self->qp->connection->notes('deny_count');
  $self->log(6, "check_deny: Deny count $deny_count");
  $self->qp->connection->notes('deny_count', $deny_count+1);

  return DECLINED;
}

sub check_rcpt {
    my ($self, $transaction, $rcpt) = @_;

  my $deny_count = 
    $self->qp->connection->notes('deny_count');
  $self->log(6, "check_rcpt: Deny count $deny_count");
  if ($deny_count >= $self->{_deny_max}) {
    $self->log(5, "Closing connection. Too many unrecognized commands.");
    return (DENYHARD, "Closing connection. $deny_count denied commands.");
  }
  return DECLINED;
}