Perl-Probleme

Florian Weimer

Unser Mailserver ist so konfiguriert, daß er die AS-Nummer des einliefernden Rechners in eine spezielle Kopfzeile schreibt, so daß sie in der Folge zum Filtern von Spam verwendet werden kann. Bis vor kurzem wurde die AS-Nummer von einem speziellen Perl-Skript ermittelt, was regelmäßig zu Lastproblemen auf dem Mailserver führte.

Der Sinn der Übung liegt darin, daß man über die AS-Nummer, die einer IP-Adresse zugeordnet ist, sehr leicht den zuständigen Internet-Provider für die Spam-Quelle ermitteln kann. Tatsächlich werden derzeit weltweit nur rund 20.000 AS-Nummern tatsächlich so genutzt, daß sie überhaupt sichtbar werden. Davon sind höchstens ein paar Tausend relevant für den Mailempfang (ob nun Spam oder nicht). Im Prinzip lassen sich AS-Nummer fälschen (ähnlich wie IP-Adressen auch), aber das ist längst nicht so einfach möglich wie z.B. beim Domainnamen des Absenders einer Nachricht, den jeder Endnutzer beliebig konfigurieren kann. Man kann also, wenn man nichts besseres zu tun hat, relativ gut eine Liste guter und böser AS-Nummern pflegen.

Die Originalquelle für die AS-Nummer zu einer IP-Adresse ist die weltweite Internet-Routingtabelle. Über ein spezielles Protokoll namens BGP tragen sich die einzelnen Internet-Provider in diese Tabelle mit ihren Adressen ein, wobei sie gleichzeitig auch angeben, wie sie erreichbar sind. Das Verfahren ist hochgradig automatisiert, und im Prinzip kann sich die AS-Nummer zu einer IP-Adresse im Minutentakt ändern. (Prüfungen auf Gültigkeit der Angaben der einzelnen Provider gibt es im allgemeinen übrigens nicht; soviel zum Thema Fälschungssicherheit. Allerdings schreien genügend Leute sehr laut, wenn man sich fremde IP-Adressen über BGP unter den Nagel reißt.)

Wenn man nun AS-Nummern auf seinem Mailserver ermitteln will, stellt sich die Frage, wie man die Internet-Routingtabelle dorthin übertragt. Eine Möglichkeit wäre, das System selbst BGP sprechen zu lassen (durch Software wie Quagga <http://www.quagga.net/>). Da die Internet-Routingtabelle allerdings recht umfangreich ist, sehe ich mittlerweile von dieser Lösung ab, denn das System hat einfach zu wenig Hauptspeicher dafür. Glücklicherweise stellt die Universität von Oregon die DNS-Zone asn.routeviews.org bereit, über die sich die AS-Nummer zu einer IP-Adresse abfragen läßt (siehe University of Oregon Route Views Project <http://www.routeviews.org/>). Die Information aus der Internet-Routingtabelle wird dabei in speziellen DNS-Einträgen vom Typ TXT abgelegt.

Nun muß man nur noch die DNS-Daten auslesen. Mit gewöhnlichen Werkzeugen wie host oder dig funktioniert das nicht so recht, da diese Programme die besondere Syntax der verwendeten TXT-Einträge nicht kennen. Also schrieb ich ein spezielles Perl-Skript, welches eine DNS-Anfrage durchführt und aus der Antwort die AS-Nummer extrahiert. (Ein wenig Nachbearbeitung ist noch erforderlich, weil die Zone asn.routeviews.org teilweise mehre Einträge für eine einzige IP-Adresse mit unterschiedlicher Präfixlänge enthält.) Die Wahl auf fiel vor allem deswegen auf Perl, weil es eine spezielle Bibliothek gibt, Net::DNS <http://www.net-dns.org/>, die die Programmierung sehr vereinfacht.

Der Einsatz von Net::DNS hat jedoch seinen Preis. Net::DNS ist eine umfangreiche Perl-Bibliothek. Ein einfacher Aufruf des kleinen Perl-Programms, das die DNS-Abfrage durchführt, dauert deswegen fast eine Sekunde:

fw@albireo:~$ time /etc/exim4/scripts/ip2asn 212.9.189.171
12374

real    0m0.875s
user    0m0.830s
sys     0m0.050s

(Der Eintrag befand sich hier bereits im Cache des lokalen Nameservers, d.h. die übliche DNS-Latenz fällt nicht ins Gewicht. Allerdings ist die Maschine zugegebenermaßen nicht die allerschnellste.) Diese Verzögerung wird zu einem Problem, wenn mal wieder ein größeres Spam-Botnet versucht, massiv Mail an Domains wie deneb.enyo.de zuzustellen. Schnell sind dann ein oder gar zwei Dutzend ip2asn-Prozesse am Laufen, und die System-Load steigt auf Werte von 40 bis 50.

Was also tun? Glücklicherweise besitze ich bereits eine ziemlich vollständige DNS-Implementiert: Enyo.Net.DNS, Teil der dnslogger-Software. Also habe ich flugs ip2asn in Ada reimplementiert. Das Ergebnis kann sich sehen lassen:

fw@albireo:~$ time ip2asn -z asn.routeviews.org 212.9.189.171
12374

real    0m0.023s
user    0m0.020s
sys     0m0.000s

Mit anderen Worten: Die real verstrichene Zeit ist von 875 Millisekunden auf 23 Millisekunden gefallen. Die vom Prozeß beanspruchte genutzte CPU-Zeit liegt nun bei 20 Millisekunden, statt wie bisher bei 880 Millisekunden: eine Verbesserung um den Faktor 44.

Für Perl spricht allerdings, daß diese Fassung viel kürzer ist: Die Ada-Reimplementierung hat ungefähr den vierfachen Umfang (gemessen mit SLOCCount <http://www.dwheeler.com/sloccount/> von David Wheeler). Wenn ich nicht dank des dnsloggers bereits eine DNS-Implementierung gehabt hätte, wäre das Ersetzen des Perl-Skriptes ziemlich aussichtslos gewesen (rund 4.000 Zeilen Ada-Code sind es nämlich). Die DNS-Bibliothek, die vom System bereitgestellt wird (in der libc, mit Routinen wie gethostbyname und res_mkquery, unterstützt leider nur die Anfrage bei Resolver, aber kaum die Analyse des Ergebnisses, so daß man Handarbeit anlegen muß, wenn man auf die zurückgelieferten Datensätze tatsächlich zugreifen will.

Rein in Perl wäre das Problem sicherlich auch zu lösen gewesen. Das Hauptproblem ist die Startzeit; das Laden von Net::DNS dauert einfach seine Zeit. Man muß also vermeiden, für jede Umsetzung von IP-Adresse zu AS-Nummer einen neuen Perl-Prozeß zu starten. Eine vorgefertigte Lösung für diese Aufgabe ist PPerl <http://search.cpan.org/dist/PPerl/>. (Achtung: Das Debian-Paket enthält zwei Sicherheitsupdates, die nicht in die offizielle Version eingeflossen sind!) Allerdings scheint PPerl unter schwierig zu reproduzierenden Voraussetzungen dazu zu führen, daß Net::DNS falsche Daten zurückliefert. Die Alternative wäre gewesen, einen richtigen Serverprozeß zu schreiben, der auch mehrere DNS-Anfragen parallel verarbeiten kann. Dieser Ansatz wäre dann aber umfangreicher geworden als die Ada-Variante von ip2asn, allerdings immer noch kleiner als die gesamte Enyo.Net.DNS-Bibliothek.

Inzwischen gab es auch zwei Spam-Wellen per Botnet. Die Systemlast stieg dabei nicht über den Wert 3. Die Mühe scheint sich folglich gelohnt zu haben.

Revisions


Florian Weimer
Home Blog (DE) Blog (EN) Impressum RSS Feeds