SMTP after POP - kontrolliertes eMail Relaying

Bitte nach Ihnen!

von Georg Horn


Das Problem dürfte allgemein bekannt sein. Lässt man seinen Mailserver als offenes Relay laufen, d.h. jeder darf den Server zum Versenden von eMail benutzen, wird der Server ruckzuck von Spammern zum Versenden Ihres unliebsamen Werbemülls mißbraucht. Lässt man kein Relaying zu, beklagen sich die User, daß sie von zuhause, unterwegs, oder wo auch immer sie sich gerade aufhalten und sich über irgendeinen Provider eingewählt haben, keine eMail über den heimischen Server versenden können; während dieser dämliche Internet-by-Call Anbieter, bei dem sie sich gerade in Mittel-Uganda eingewählt haben, gar keinen SMTP-Server zur Verfügung stellt...

Dieser Artikel stellt eine Lösung für das Problem vor: die Benutzer müssen sich zunächst per POP3 beim Server authorisieren, und dürfen anschließend per SMTP ihre ausgehende eMail versenden. Dabei wird im Gegensatz zu schon bekannten Lösungen, die auf Perl-Scripts basieren, ein effizienterer Ansatz mit einem modifizierten POP3 daemon vorgestellt.


Das Problem

Eigentlich besteht unser Problem aus mehreren Teilproblemen bzw. einer Verkettung von Problemen:

Die Lösung

Da das Abholen von eingehender eMail und das Versenden von ausgehender eMail oft in einem Arbeitsgang erledigt wird, und da zum Abholen von eMail gerne POP3 verwendet wird, welches eine Authorisierung via Username und Passwort erfordert (siehe da!), liegt es nahe, die beim Abholen der eMail erfolgte Authorisierung auch für das anschliessende Versenden von eMail per SMTP zu verwenden. Dazu wird die IP-Adresse, die dem Benutzer bei der Einwahl dynamisch zugewiesen wird, nach der erfolgreichen Authorisierung via POP3 in eine Tabelle freigeschalteter Adressen eingetragen. Anschliessend darf dann von dieser Adresse aus der Server als Mail-Relay benutzt werden.

Um zu vermeiden, daß sich diese Tabelle mit der Zeit mit den gesamten Adressräumen diverser Provider füllt, bleiben die Adressen nur eine gewisse, kurze Zeit erhalten und werden dann wieder gelöscht.

Diese Idee ist nun keineswegs eine Erfindung des Autors (leider...), sondern es gibt schon diverse Patches und Scripte zur Realisierung dieses "SMTP after POP" genannten Verfahrens (Siehe Literaturverzeichnis). Allerdings basieren diese Lösungen alle auf Perl-Scripts, die das Systemlog des Servers scannen, nach Meldungen des POP3 daemons suchen, und die darin enthaltenen Adressen freigeben. Dies erschien dem Autor erstens unelegant, zweitens ist er kein Perl-Fan ;-)), und drittens generiert dies nach den Aussagen des Postmasters eines lokalen Providers bei hohem Mailaufkommen und vielen Usern zu viel Systemlast und funktioniert nicht mehr zuverlässig.

Deshalb wurde hier ein anderer Weg beschritten, und zwar ist sowieso ein Patchen des POP3 daemons notwendig, damit dieser die IP-Adressen ins Systemlog schreibt, welches dann wiederum von besagtem Perl-Script gelesen wird. Was lag also näher, als den POP3 daemon selbst direkt den Eintrag vornehmen zu lassen? Soweit dem Autor bekannt ist, nichts! Es ist dann lediglich noch ein kleines Programm zu schreiben, welches regelmässig die IP-Adressen wieder löscht.

Die Realisierung

Im vorliegenden Fall wurde SMTP after POP auf einem unter SuSE-5.2 laufenden Server implementiert. Dazu wurde das System zunächst auf Sendmail Version 8.9.3 aufgerüstet, denn ab der Version 8.9 bietet Sendmail ein acces_db genanntes Feature, mit dem sich das Relaying sehr genau steuern lässt.

Weiterhin wurde der POP3 daemon der Firma Qualcomm verwendet, der in der Version qpopper2.53 vorlag.

Da Sendmail die in der access_db vorgehaltenen Adressen im Berkley db Format speichert, wurde auch die entsprechende Bibliothek benötigt. SuSE liefert zwar das Paket mit aus, aber ohne Manualpages... Deshalb wurden die manpages aus dem Paket db.1.85.tar.gz manuell nachinstalliert.

Konfiguration von Sendmail

Sendmail muß so konfiguriert werden, daß das access_db Feature verwendet wird. Sinnvollerweise konfiguriert man seinen Sendmail über die mitgelieferten m4 Macrodateien (Verzeichnis cf in der Sendmail Distribution, cf/README beachten!), wobei folgende Zeile das access_db Feature aktiviert.
FEATURE(`access_db', `hash -o /etc/mail/access.db')

Angenommen, unser m4 Eingabefile liege in /etc/mail/linux.mc, so erzeugen wir daraus nun ein Sendmail Konfigurationsfile mit dem folgenden Kommando:

/usr/bin/m4 /etc/mail/linux.mc > /etc/sendmail.cf

Nun müssen wir noch eine initiale Datei access.db anlegen. Das geschieht mit dem folgenden Kommando:

/usr/sbin/makemap hash /etc/mail/access.db < /etc/mail/access

Die Datei /etc/mail/access ist dabei ein Textfile, welches Kommentare und Key-Value Paare folgender Form enthält:

# Allow relaying from 192.168.0 192.168.0 RELAY # No Spammers please spamford.com REJECT # This machine at spamford is OK ok.spamford.com OK

Dies bedeutet, daß Rechner mit IP-Adressen von 192.168.0.1 bis 192.168.0.254 den Server als Relay benutzen dürfen, alle eMail aus der Domain "spamford.com" abgeweisen wird, während eMail von "ok.spamford.com" akzeptiert wird. Wir können natürlich auch mit einem leeren access File starten, denn die Adressen werden ja später vom POP3 daemon dynamisch eingetragen.

Unser Sendmail sollte nun soweit konfiguriert und startbereit sein. Für weitergehende Fragen zur Sendmail Konfiguration sei hier auf das im Verzeichnis cf der Distribution enthaltene README verwiesen...

Der POP3 Daemon

Nachdem Sendmail nun zufriedenstellend läuft, muß noch dafür gesorgt werden, daß der POP3 daemon die IP-Adressen der Clients, die sich erfolgreich per POP3 authorisiert haben, in oben erwähnte access.db einträgt. Hierzu wurde nach einigem Studium der Manualpage zu dbopen() und der Quellen des qpoppers folgender Patch geschrieben:

Listing 1. Patch für den Qualcomm popper

*** pop_pass.c.orig Thu Jun 24 11:10:17 1999 --- pop_pass.c Thu Jun 24 12:07:18 1999 *************** *** 28,33 **** --- 28,94 ---- char *pwerrmsg = "Password supplied for \"%s\" is incorrect."; + #define SMTP_AFTER_POP + + #ifdef SMTP_AFTER_POP + + #include <unistd.h> + #include <fcntl.h> + #include <sys/file.h> + #include <db.h> + + void smtp_after_pop(POP *p) + { + char stamp[64] = "/etc/mail/popips/"; + char *dbname = "/etc/mail/access.db"; + DB *db; + DBT key, data; + DBT *pk = &key, *pd = &data; + int fd, rc; + + db = dbopen(dbname, O_RDWR, 0, DB_HASH, NULL); + if (db == NULL) { + pop_log(p, LOG_WARNING, "error opening %s\n", dbname); + return; + } + fd = (db->fd)(db); + if (flock(fd, LOCK_EX) == -1) { + pop_log(p, LOG_WARNING, "can't get lock on %s\n", dbname); + (db->close)(db); + return; + } + key.data = p->ipaddr; + key.size = strlen(key.data); + data.data = "RELAY"; + data.size = strlen(data.data); + rc = (db->put)(db, pk, pd, 0); + switch (rc) { + case 0: + strcat(stamp, p->ipaddr); + rc = open(stamp, O_CREAT, 0600); + if (rc == -1) { + pop_log(p, LOG_WARNING, "error creating %s\n", stamp); + } else { + close(rc); + } + pop_log(p, LOG_INFO, "inserted %s into %s for user %s\n", + p->ipaddr, dbname, p->user); + break; + case 1: + pop_log(p, LOG_INFO, "%s alrady in %s for user %s\n", + p->ipaddr, dbname, p->user); + break; + default: + pop_log(p, LOG_WARNING, "error inserting %s into %s for user %s\n", + p->ipaddr, dbname, p->user); + break; + } + flock(fd, LOCK_UN); + (db->close)(db); + } + #endif + + #ifdef NONAUTHFILE checknonauthfile(user) char *user; *************** *** 472,477 **** --- 533,542 ---- sleep(SLEEP_SECONDS); return (pop_msg(p,POP_FAILURE, pwerrmsg, p->user)); } + + #ifdef SMTP_AFTER_POP + smtp_after_pop(p); + #endif return(POP_SUCCESS); }

Wie man leicht sieht, wird hier eine Funktion smtp_after_pop definiert, die als Aufrufparameter einen Zeiger auf einen struct POP bekommt. Diese Funktion wird nun einfach in der Funktion auth_user nach der erfolgreichen Authorisierung des Benutzers mit dem Argument p aufgerufen. p ist hier ein Zeiger auf einen struct POP, der zu der Zeit schon mit allen benötigten Werten gefüllt ist. Uns interessieren hier die IP-Adresse des Clients (p->ipaddr) und der Benutzername (p->user). Der Patch kann mit dem Kommando patch < pop_pass.c.diff angewendet werden, oder man nimmt den fertig gepatchten aus [1].

Unser so gepatchter POP3 daemon öffnet also nach der Authorisierung des Benutzers die access Datenbank von Sendmail und schreibt einen Datensatz mit der IP-Adresse als Schlüssel und dem Wort "RELAY" als Datum hinein. Dies geschieht effizient durch direktes Verwenden der Funktionen aus der Berkley db library, also ohne weitere Prozesse die ständig laufen oder gar bei jeder neuen Adresse neu gestartet werden! Man beachte auch, daß das Textfile /etc/mail/access, aus dem wir unsere Ausgangsdatenbank erzeugten, hiervon unberührt bleibt.

Weiterhin legt der gepatchte POP3 daemon im Verzeichnis /etc/mail/popips eine leere Datei an, die als Namen die IP-Adresse hat. Die Dateien in diesem Verzeichnis werden später benötigt, wenn es darum geht, "alte" Adressen wieder zu löschen.

Um den Qualcomm Popper zu übersetzen, muß nun zunächst mittels des configure-Scriptes ein Makefile erzeugt werden, und zwar mit:

./configure --enable-specialauth

Der Schalter --enable-specialauth muß angegeben werden, damit die Shadow-Passwort-Datei anstelle der normalen Passwort-Datei gelesen wird. Ohne dies funktioniert die Benutzerauthorisierung auf einem System mit Shadow-Passworten nicht. Anschliessend muß noch im Makefile die Berkley db library angegeben werden, damit diese beim Binden des Programmes hinzugebunden wird. Dazu werden die beiden Zeilen die mit DBM_LIBS bzw. mit LIBS beginnen, ersetzt durch:

DBM_LIBS = -ldb LIBS = -ldb
Anschliessend kann unser popper mit dem Kommando make übersetzt, und mit cp popper /usr/sbin/popper dorthin kopiert werden, von wo er vom inet daemon aufgerufen wird (Siehe /etc/inetd.conf).

Eine erster Test

Mach soviel trockenem Geschreibsel und Quellcode wollen wir nun Taten folgen lassen und die Früchte unserer Arbeit testen: Wir konfigurieren unseren Mailclient so, daß er unseren Server sowohl für ein- als auch für ausgehende eMail verwendet:

Mail Konfiguration

Abb. 1: Ausschnitt aus dem Konfigurationsdialog des Mailclients.

Anschliessend wählen wir uns bei unserem Lieblingsprovider ein, und versuchen eine eMail an horn@uni-koblenz.de zu versenden: Leider ohne rechten Erfolg...

Ungesendete Mail

Abb. 2a: Die Mail liegt im "Outbox" Ordner.

Fehlermeldung

Abb. 2b: Relaying? Nein Danke!

Nun gut, sehen wir mal nach, ob wir wenigstens eine eMail bekommen haben:

Passworteingabe

Abb. 3a: Das POP3 Passwort wird eingegeben...

No mail today...

Abb. 3b: Leider keine eMail bekommen.

Auch nicht, aber authorisiert haben wir uns ja dennoch beim POP3 daemon. Also sollte jetzt das Versenden funktionieren!

Mail versendet

Abb. 4: Na also, geht doch! Die Mail ist im "Sent" Ordner gelandet.

Diese unsere Beobachtungen decken sich auch mit den Einträgen im Logfile, die der Server gemacht hat:

Jun 26 12:00:52 horn sendmail[28541]: MAA28541: ruleset=check_rcpt, arg1=<horn@uni-koblenz.de> relay=horn@pppin21.altenkirchen.rhein-zeitung.DE [195.189.136.149], reject=550 <horn@uni-koblenz.de> Relaying denied

Diese Meldung entspricht dem ersten Versuch, eine eMail zu versenden.

Jun 26 12:01:47 horn popper[28542]: inserted 195.189.136.149 into /etc/mail/access.db for user horn

Das war der Eintrag des POP3 daemons: Er hat wie gewüscht unsere dynamisch zugewiesene Adresse in die access.db eingetragen. Netterweise verrät er uns auch noch, welcher User diesen Eintrag bewirkt hat.

Jun 26 12:02:21 horn sendmail[28544]: MAA28544: from=<horn@koblenz-net.de> size=632, class=0, pri=30632, nrcpts=1, msgid=<3774A411.2993F29D@koblenz-net.de> proto=SMTP, relay=horn@pppin21.altenkirchen.rhein-zeitung.DE [195.189.136.149] Jun 26 12:02:51 horn sendmail[28546]: MAA28544: to=<horn@uni-koblenz.de> ctladdr=<horn@koblenz-net.de> (210/0), delay=00:00:32, xdelay=00:00:30, mailer=esmtp, relay=mailhost.uni-koblenz.de. [141.26.64.1], stat=Sent (MAA18697 Message accepted for delivery)

Und hier zu guter Letzt die Sendmail Meldungen von der Annahme und dem erfolgreichen Weiterleiten der eMail an mailhost.uni-koblenz.de.

Aufräumarbeiten...

Zu guter Letzt wollen wir unseren Arbeitsplatz natürlich wieder fein säuberlich aufgeräumt verlassen, so wie wir Ihn hoffentlich auch angetroffen haben. Was also noch fehlt, ist oben schon angesprochenes Programm, daß die IP-Adressen nach einer gewissen Zeit wieder aus der access.db löscht.

Wir haben uns dazu zunächst ein kleines C-Programm geschrieben, das die Berkley db library benutzt. Dieses Programm kann Datensätze aus einem db file auflisten, einfügen und löschen. Es kann als kleiner Einstieg in die Programmierung mit der db libraray gesehen werden.

Listing 2. C-Programm zum Zugriff auf Berkley db files

/* db.c: manage sendmail database files (/etc/mail/access.db for example) Written as a quick hack by Georg Horn , so don't use it for educational purposes. Although, it may be used to demonstrate what "spaghetti code" is... ;-) It is used to remove IP-Adresses from sendmails access.db database, that have been inserted therein by a modified POP3 daemon to allow SMTP after POP, but it can also be used to list the contents of the database (or other databases) or to insert or delete records. */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/file.h> #include <db.h> void usage(void) { printf("usage: db dbfile [action] [args]\n"); printf("where [action] is one of:\n"); printf(" ins - insert [args] into dbfile\n"); printf(" del - delete [args] from dbfile\n"); printf(" lst - list contents of dbfile\n"); printf("and [args] are key-value pairs that are to be inserted,\n"); printf("or keys that are to be deleted or listed.\n"); printf("Examples:\n"); printf("db /etc/mail/access.db lst\n"); printf(" will list all records from /etc/mail/access.db\n"); printf("db /etc/mail/access.db del 192.168.0.1\n"); printf(" will delete the record with key 192.168.0.1\n"); printf("db /etc/mail/access.db add 192.168.0.1 RELAY\n"); printf(" will add a record with key 192.168.0.1 and value RELAY\n"); exit(1); } int main(int argc, char *argv[]) { DB *db; DBT key, data; DBT *pk = &key, *pd = &data; enum { ins, del, lst } action; int arg, rc, fd; char k[128], d[128]; if (argc < 3) usage(); db = dbopen(argv[1], O_RDWR, 0, DB_HASH, NULL); if (db == NULL) { perror(argv[1]); usage(); } fd = (db->fd)(db); if (flock(fd, LOCK_EX) == -1) { perror(argv[1]); (db->close)(db); usage(); } if (!strcmp(argv[2], "ins")) action = ins; else if (!strcmp(argv[2], "del")) action = del; else if (!strcmp(argv[2], "lst")) action = lst; else usage(); arg = 3; switch (action) { case lst: if (argc >= 4) { while (arg < argc) { key.data = argv[arg]; key.size = strlen(key.data); rc = (db->get)(db, pk, pd, 0); switch (rc) { case 0: strncpy(d, data.data, data.size); d[data.size] = 0; printf("%s\t%s\n", argv[arg], d); break; case 1: fprintf(stderr, "key %s not found!\n", argv[arg]); break; default: perror(argv[arg]); break; } arg++; } } else { while ((db->seq)(db, pk, pd, R_NEXT) == 0) { strncpy(k, key.data, key.size); k[key.size] = 0; strncpy(d, data.data, data.size); d[data.size] = 0; printf("%s\t%s\n", k, d); } } break; case ins: while (arg < argc) { key.data = argv[arg]; key.size = strlen(key.data); arg++; data.data = argv[arg]; data.size = strlen(data.data); rc = (db->put)(db, pk, pd, 0); switch (rc) { case 0: printf("%s\t%s inserted\n", argv[arg - 1], argv[arg]); break; case 1: printf("key %s already exists\n", argv[arg - 1]); break; default: perror(key.data); break; } arg++; } break; case del: while (arg < argc) { key.data = argv[arg]; key.size = strlen(key.data); rc = (db->del)(db, pk, 0); switch (rc) { case 0: printf("%s deleted\n", argv[arg]); break; case 1: printf("key %s does not exist\n", argv[arg]); break; default: perror(key.data); break; } arg++; } break; } flock(fd, LOCK_UN); (db->close)(db); return 0; }

Das Programm wird mit folgender Kommandozeile compiliert und gebunden:

gcc -O -s db.c -ldb -odb

Man beachte die Option -ldb, die das Hinzubinden der db library bewirkt. Mit ./db /etc/mail/access.db lst können wir uns nun z.B. den aktuellen Inhalt unserer access.db ansehen:

195.189.138.94 RELAY 195.189.132.130 RELAY 195.189.132.198 RELAY

Das stimmt auch mit dem Inhalt des Verzeichnisses /etc/mail/popips überein. ls -la /etc/mail/popips sagt uns:

total 2 drwxr-xr-x 2 root root 1024 Jun 28 10:21 ./ drwxr-xr-x 3 root root 1024 Jun 24 17:06 ../ -rw------- 1 root root 0 Jun 28 10:00 195.189.132.130 -rw------- 1 root root 0 Jun 28 10:10 195.189.132.198 -rw------- 1 root root 0 Jun 28 10:21 195.189.138.94

Die Dateien in diesem Verzeichnis brauchen wir nun, um "alte" IP-Adressen wieder aus der access.db löschen zu können, denn am Dateidatum können wir sehen, wann eine Adresse eingetragen wurde.

Ein Shellscript, welches einmal pro Stunde durch den Eintrag

59 * * * * root $HOME/bin/cron.hourly

in der /etc/crontab per cron gestartet wird, erledigt das, indem es mittels find alle Dateien in /etc/mail/popips sucht die älter als 15 Minuten sind, diese löscht und auch durch einen Aufruf unseres oben vorgestellten db Programmes aus der access.db entfernt.

Listing 3. Shellscript zum Aufräumen der IP-Adressen

#!/bin/sh # # cron.hourly. This script is executed as a cron-job every hour # # expire IP addresses from /etc/mail/access.db DIR="/etc/mail/popips" if [ -d "$DIR" ]; then cd $DIR FILES=`/usr/bin/find . -type f -mmin +15 | /usr/bin/sed 's/^.*\///'` if [ "$FILES" != "" ]; then /bin/rm $FILES /root/bin/db /etc/mail/access.db del $FILES > /dev/null /usr/bin/logger -p mail.info -t cron.hourly "removed $FILES from access.db" fi fi

Kernstück dieses Scripts ist die Zeile

FILES=`/usr/bin/find . -type f -mmin +15 | /usr/bin/sed 's/^.*\///'`

die mittels find alle "normalen" Dateien findet (-type f), die älter als 15 Minuten sind (-mmin +15). Da find die Dateinamen inclusive Pfad ausgibt, hier also in der Form ./192.168.0.1, wird durch den nachgeschalteten sed-Aufruf alles bis zum '/' abgeschnitten.

Wenn auf diese Art nun Dateien gefunden wurden, d.h. $FILES ist ungleich dem leeren String, so werden diese Dateien gelöscht und durch einen Aufruf unseres db-Programmes aus der access.db entfernt. Zur Unterhaltung des Systemadmins wird noch eine Meldung in das Systemlog geschrieben.

Imap

Kai-Olaf von Wolff hat den patch für den Qualcomm qpopper für den Imap/Pop-Server der Universität Washington angepasst [1.1]. Ein Problem bei der Einrichtung, auf das er hinweist, ergibt sich daraus, dass SuSE im Sendmail die alte LibDB (1.x) und nicht wie empfohlen die 2. oder 3. LibDB einsetzt. Sein Patch und dieser hier für den Qpopper passe jedoch.

Fazit

Es wurde hier mit relativ wenig Aufwand eine kompakte und effiziente Lösung des "SMTP after POP"-Problems gefunden und implementiert. Der reine Programmieraufwand war ca. ein Tag, und das auch nur, weil der Autor noch keinerlei Erfahrung mit der Berkley db lib hatte, und auf seinem System die manpages dazu fehlten...

Eine weitere Performance-Steigerung auf stark belasteten Servern könnte man sicher noch erreichen, indem man das Suchen der Dateien, die älter als z.B. 15 Minuten sind, direkt in C innerhalb des Programmes db.c implementieren würde. Dies würde den Overhead beim stündlichen Aufräumen der Adressen weiter reduzieren, da das Shellscript und das find-Kommando wegfallen würden. Auch den Qpopper-Patch könnte man noch so anpassen, daß er den Eintrag in die Datenbank nur dann vornimmt, wenn er nicht sowieso schon vorhanden ist.

Zu guter Letzt, und das ist nach Meinug des Autors eigentlich der wichtigste Aspekt dieses Artikels, zeigt dieser Fall wie leicht man sich bei Benutzung von Open-Source-Software individuelle Anpassungen selbst "stricken" kann, und diese dann auch wieder der Open-Source-Gemeinde zur Verfügung stellen sollte. Mit proprietärer Mailserver-Software wäre solch eine Anpassung mangels Quellcode garnicht möglich gewesen, bzw. hätte vom Hersteller gekauft und teuer bezahlt werden müssen...

Literatur und Bezugsquellen

[1] http://koblenz-net.de/~horn/smtp_after_pop Hier finden sich die vom Autor verwendeten Programme und Patches.
[1.1] http://koblenz-net.de/~horn/smtp_after_pop/imap.diff Patch von Kai-Olaf von Wolff für den Imap/Pop-Server der Universität Washington.
[2] Sendmail Homepage Hier gibt's sendmail.
[3] README zu den Sendmail Konfigurationsfiles: "cf/README"
[4] Sendmail-Buch von O'Reilly.
[5] Qualcomm Homepage.
[6] Qualcomm Free Servers Page Hier gibt's qpopper.
[7] Sleepycat Software Homepage Hier gibt's die Berkley db lib.

Der Autor
Georg Horn ist Diplom-Informatiker und arbeitet als freier Programmierer und Systemadministrator im Linux- und Internet-Bereich. Unter anderem betreut er den Webserver einer Koblenzer Werbeagentur, auf dem auch dieses Verfahren hier implementiert wurde. Zu erreichen ist er unter http://koblenz-net.de oder per eMail unter horn@koblenz-net.de.