Daumen drauf (Linux-Magazin, Februar 2007)

Was passiert eigentlich so den ganzen Tag (und bei Nacht) auf einem lokalen Netzwerk? Wer den heute vorgestellten Perl-Dämon laufend Daten sammeln und in einer Datenbank ablegen lässt, kann Alarme auslösen und hinterher feststellen, was vorgefallen ist.

In [2] hat mein Kolumnistenkollege Charly Künast vor einiger Zeit arpalert ([4]) vorgestellt, dessen Dämon ARP-Anfragen überwacht, mit einer Whitelist vergleicht und bei unbekannten MAC-Adressen einen Alarm auslöst. Sehr nützlich, das Ganze, allerdings löste das Programm teilweise multiple Alarme für den gleichen Vorfall aus und die beiliegende Dokumentation war teils französich und teils grottenschlecht.

Dank der Module Net::Pcap und NetPacket::Ethernet vom CPAN ist es nicht schwer, die MAC-Adressen von im LAN herumschwimmenden Paketen herauszufischen. Und mit Rose, dem neulich hier vorgestellten objektorientierten Datenbankmapper, lassen sich die gewonnenen Daten in eine MySQL-Datenbank einlesen, in der man später skriptgestützt nach Herzenslust herumstöbern kann.

Um zum Beispiel festzustellen, welche Geräte während der letzten 24 Stunden auf dem LAN aktiv waren, genügt ein Aufruf des Skripts lastaccess wie in Abbildung 1 dargestellt.

Abbildung 1: Welche Geräte waren in den letzten 24 Stunden auf dem LAN aktiv?

Schnüffeln als Root

Ähnlich wie der schon einmal hier besprochene graphische Netzwerkschnüffler capture [3], schaltet das Skript arpcollect in Listing 1 die erste Netzwerkkarte des Rechners in den promiscuous mode. So schnappt sie nicht nur die für den angeschlossenen Rechner bestimmten Pakete aus der Ethernetleitung, sondern leitet einfach alle gefundenen Pakete an das Schnüffelskript weiter. Hierzu sind root-Rechte erforderlich, die Zeile 9 entweder bestätigt oder das Skript bricht ab.

Die in Zeile 13 aufgerufene Funktion lookupdev() gibt den Namen eines verfügbaren Netzwerk-Devices zurück. Bei nur einer angeschlossenen Netzwerkkarte ist das "eth0". Das anschließende open_live() tritt dann in eine Endlosschleife ein (der Timeout wurde mit -1 abgeschaltet), in der es jeweils die ersten 1024 bytes jedes ankommenden Paketes liest und sofort die Callback-Funktion callback aufruft. Diese erhält nicht nur die vorher ermittelte lokale Netzwerkadresse/maske als $user_data, sondern die rohen Paketdaten in $raw_packet.

Das Modul NetPacket::Ethernet dekodiert dieses Datalink-Layer-Paket und stellt unter dem Hash-Key "src_mac" die MAC-Addresse des Senders im Hexformat bereit. Dieses enthält noch nicht die typischen Doppelpunkte nach jedem zweiten Zeichen, sodass Zeile 40 sie mittels eines regulären Ausdrucks hineinpflanzen muss.

In den Zeilen 48 bis 50 prüft arpcollect dann anhand der IP-Addresse, ob das Paket von einem an das lokale Netz angeschlossenen Gerät stammt. Die IP-Addresse ermittelt es anhand der Nutzdaten des Ethernetpakets, die es mit der Funktion strip des Moduls NetPacket::Ethernet aus dem Datalink-Layer-Paket extrahiert. Das Ergebnis ist das rohe IP-Paket, das mittels der Funktion decode des Moduls NetPacket::IP entpackt wird. Unter dem Hash-Schlüssel src_ip findet sich dann die IP-Addresse des Absenders.

Falls ein bitweises 'und' der IP-Adresse mit der Netzwerkadresse wieder die Netzwerkadresse ergibt, wurde das Paket von einem Gerät auf dem lokalen Netzwerk abgesandt und es ist für die Weiterverarbeitung relevant.

Die Methode event_add() des vorher instantierten Datenbankobjekts vom Typ WatchLAN nimmt die IP-Addresse und die MAC-Adresse entgegen und pumpt sie zur späteren Analyse in die Datenbank.

Listing 1: arpcollect

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Net::Pcap;
    04 use NetPacket::IP;
    05 use NetPacket::Ethernet;
    06 use Socket;
    07 use WatchLAN;
    08 
    09 die "You need to be root to run this.\n"
    10   if $> != 0;
    11 
    12 my ( $err, $netaddr, $netmask );
    13 my $dev = Net::Pcap::lookupdev( \$err );
    14 
    15 Net::Pcap::lookupnet($dev, \$netaddr, 
    16                      \$netmask, \$err) and
    17     die "lookupnet $dev failed ($!)";
    18 
    19 my $object =
    20   Net::Pcap::open_live( $dev, 1024, 1, 
    21                         -1, \$err );
    22 
    23 my $db = WatchLAN->new();
    24 
    25 Net::Pcap::loop( $object, -1, \&callback,
    26   [ $netaddr, $netmask ] );
    27 
    28 ###########################################
    29 sub callback {
    30 ###########################################
    31   my ($user_data, $hdr, $raw_packet) = @_;
    32 
    33   my ($netaddr, $netmask) = @$user_data;
    34 
    35   my $packet = NetPacket::Ethernet->
    36                       decode($raw_packet);
    37 
    38   my $src_mac = $packet->{src_mac};
    39     # Add separating colons
    40   $src_mac =~ s/(..)(?!$)/$1:/g;
    41 
    42   my $edata =
    43    NetPacket::Ethernet::strip($raw_packet);
    44 
    45   my $ip = NetPacket::IP->decode($edata);
    46 
    47     # Package coming from local network?
    48   if ((inet_aton( $ip->{src_ip} ) &
    49        pack( 'N', $netmask )
    50       ) eq pack( 'N', $netaddr )) {
    51     $db->event_add( $src_mac, 
    52                     $ip->{src_ip} );
    53   }
    54 }

Minutenpuffer

Das Modul WatchLAN.pm implementiert die Speicherungsschicht. Jedes Paket sofort in der Datenbank abzulegen, wäre nicht effektiv, da so selbst auf einem nur leicht aktiven Netzwerk mehrere Schreibzugriffe pro Sekunde fällig würden. Außerdem kosten mehreren Millionen Tabellenzeilen nicht gerade wenig Plattenplatz und Rechner-Resourcen.

Aus diesem Grund speichert WatchLAN.pm die ankommenden Paketadressen zunächst in einem temporären Hash, dessen Inhalt jeweils zur vollen Minute an die Datenbank übertragen wird. Ein Zähler wird mit jeder IP/MAC-Kombination um eins erhöht und mit cache_flush später in der Datenbanktabelle activity in der Spalte counter abgelegt. Der Parameter flush_interval im WatchLAN-Konstruktor bestimmt, wie oft diese Spülung erfolgt. Aus der aktuellen Zeit und flush_interval wird der Zeitpunkt des nächsten Spülvorgangs berechnet und in der Instanzvariablen next_update abgelegt.

Listing 2: dbinit

    1 DBNAME=watchlan
    2 mysqladmin -f -uroot drop $DBNAME
    3 mysqladmin -uroot create $DBNAME
    4 mysql -uroot $DBNAME <sql.txt

Listing dbinit zeigt die notwendigen Shell-Befehle, um eine neue MySQL-Datenbank anzulegen. Die SQL-Befehle aus der Datei sql.txt sind in Abbildung 2 zu sehen. Das so entstehende Tabellenschema der Datenbank in Abbildung 3 verlinkt die Haupttabelle activity über Foreign Keys mit den Tabellen device und ip_address. Sie speichern MAC-Adressen mitsamt Gerätedaten sowie IP-Adressen. Stünden die Adressen in der Haupttabelle, würde nicht nur Speicherplatz verschwendet, sondern auch Datenredundanz erzeugt.

Abbildung 2: Die drei Tabellen des definierten Datenbankschemas.

Abbildung 3: SQL-Befehle, um die Datenbank anzulegen

Extrawurst für MySQL

MySQL macht es dem Rose::DB-Loader nicht gerade leicht, solche Relationen zu erkennen. Laut Rose::DB-Autor John Siracusa ist es notwendig, FOREIG KEY-Deklarationen mit korrekten REFERENCES-Klauseln anzubringen und sowohl die referenzierenden als auch die referenzierten Kolumnen mit einem Index zu versehen. Steht die SQL-Definition aber einmal wie abgebildet, reicht ein Aufruf der Methode make_classes wie in Listing WatchLAN.pm (Zeile 18) und Rose::DB kontaktiert die Datenbank und definiert selbständig den kompletten Objekt-Wrapper auf alle Tabellen und deren Kolumnen. Praktisch!

Listing 3: WatchLAN.pm

    01 package WatchLAN;
    02 ###########################################
    03 use strict;
    04 use Apache::DBI;   # share a single DB conn
    05 use Rose::DB::Object::Loader;
    06 use Log::Log4perl qw(:easy);
    07 use DateTime;
    08 
    09 my $loader = Rose::DB::Object::Loader->new(
    10   db_dsn => 'dbi:mysql:dbname=watchlan',
    11   db_username  => 'root',
    12   db_password  => undef,
    13   db_options   => {
    14     AutoCommit => 1, RaiseError => 1 },
    15   class_prefix => 'WatchLAN'
    16 );
    17 
    18 $loader->make_classes();
    19 
    20 ###########################################
    21 sub new {
    22 ###########################################
    23   my ($class) = @_;
    24 
    25   my $self = {
    26     cache          => {},
    27     flush_interval => 60,
    28     next_update    => undef,
    29   };
    30 
    31   bless $self, $class;
    32   $self->cache_flush();
    33 
    34   return $self;
    35 }
    36 
    37 ###########################################
    38 sub event_add {
    39 ###########################################
    40   my($self, $mac, $ip)= @_;
    41 
    42   $self->{cache}->{"$mac,$ip"}++;
    43   $self->cache_flush()
    44     if time() > $self->{next_update};
    45 }
    46 
    47 ###########################################
    48 sub cache_flush {
    49 ###########################################
    50   my ($self) = @_;
    51 
    52   for my $key ( keys %{ $self->{cache} } ){
    53     my ($mac, $ip) = split /,/, $key;
    54     my $counter = $self->{cache}->{$key};
    55 
    56     my $minute = DateTime->from_epoch(
    57       epoch => $self->{next_update} -
    58                $self->{flush_interval},
    59       time_zone => "local",
    60     );
    61 
    62     my $activity = WatchLAN::Activity->new(
    63               minute => $minute);
    64 
    65     $activity->device(
    66       { mac_address => $mac } );
    67     $activity->ip_address(
    68       { string => $ip } );
    69     $activity->counter($counter);
    70     $activity->save();
    71   }
    72 
    73   $self->{cache} = {};
    74   $self->{next_update} = time() -
    75     ( time() % $self->{flush_interval} ) +
    76     $self->{flush_interval};
    77 }
    78 
    79 ###########################################
    80 sub device_add {
    81 ###########################################
    82   my ( $self, $name, $mac_address ) = @_;
    83 
    84   my $device = WatchLAN::Device->new(
    85     mac_address => $mac_address );
    86   $device->load( speculative => 1 );
    87   $device->name($name);
    88   $device->save();
    89 }
    90 
    91 1;

Das Modul WatchLAN ruft den Rose-Loader auf, sobald eine Applikation es mit use WatchLAN einbindet. Die aus MySQL gelesenen Tabellen und ihre Beziehungen legt es als Klassen im Perl-Namespace unter WatchLAN:: ab. Die Methode cache_flush() schreibt den temporären Hash in die Datenbank. Dank Rose wird hierzu lediglich ein neues Objekt $activity der Klasse WatchLAN::Activity erzeugt. Es arbeitet mit der Haupttabelle activity, greift aber über Methoden auch auf die referenzierten Tabellen devices und ip_addresses zu. Das unscheinbare Konstrukt

    $activity->device({ 
        mac_address => $mac });

erledigt nach einem späteren save() des Objekts zweierlei: Falls in der Tabelle devices noch kein Eintrag eines Geräts mit der gegebenen MAC-Adresse besteht, legt es dort einen neuen Record an. Und in die Haupttabelle activity pflanzt es in das Feld device_id einen neuen Integerwert, der auf den Eintrag in devices verweist. Klever! Einträge in der Tabelle activity werden hingegen ohne Angabe eines anonymen Hashes vorgenommen, hierfür stellt Rose nach der entsprechenden Spalte benannte Methoden bereit. Der Aufruf

    $activity->counter($counter);

setzt den Spaltenwert $counter im aktuell bearbeiteten Record auf den Wert $counter. Nach dem save() löscht cache_flush() den Cache, berechnet die nächste Auffrischzeit und kehrt zurück zum Aufrufer. Ähnliches gilt für die Methode device_add(), die entweder ein neues Device mit einer MAC-Adresse einfügt oder den Eintrag eines bestehenden Gerätes umschreibt. Der Aufruf von

  $device->load(speculative => 1);

lädt einen zur vorher im Konstruktor von WatchLAN::Device angegebenen MAC-Adresse passenden Record aus der Tabelle devices. Dies ist möglich, da die Spalte mac_address beim Anlegen der Datenbank mit UNIQUE(mac_address) als eindeutiger Schlüssel definiert wurde. Rose merkt dies und erlaubt das Laden des Records aufgrund dieses Kriteriums. Wäre dies nicht der Fall, müsste der Record mit einem Query gesucht werden. Der Parameter speculative gibt an, dass es in Ordnung ist, wenn der Record noch nicht existiert. Ein nachfolgendes save() legt ihn dann an.

Verschwendung bremsen

Rose geht relativ verschwenderisch mit Datenbankverbindungen um. Jedes neue Objekt der Klasse WatchLAN::Activity ruft die Funktion connect aus dem DBI-Modul auf, und jedesmal, wenn ein solches Objekt ausgedient hat, löst Rose die Verbindung wieder. Das stellt sicher, dass keine unerwünschten Nebeneffekte auftreten, wenn man mit Transaktionen arbeitet, ist im vorliegenden Fall jedoch pure Verschwendung. Das Modul Apache::DBI sorgt rein durch sein Hinzuladen hinter den Kulissen dafür, dass genau eine persistente DB-Verbindung genutzt wird.

Das Kind braucht einen Namen

Die Tabelle devices speichert nicht nur MAC-Adressen, sondern ordnet diesen auch gleich einprägsame Namen zu. Aus 00:11:11:5b:ed:46 wird so ``Mike's Linux Box'' und gleichzeitig beweist dieser Eintrag, dass es sich um ein auf dem lokalen Netz geduldetes Gerät handelt. Hängt sich der Wohnungsnachbar hingegen unerlaubterweise mit seinem Laptop über Wireless ins LAN und schnorrt wertvolle Bandbreite, schnappt arpcollect dies auf, trägt die MAC-Adresse in die Tabelle devices ein, lässt aber das name-Feld unberührt. So wird das weiter unten vorgestellte Überwachungsskript arpemail kurze Zeit später auf diesen Missstand aufmerksam und schickt eine Email an den Admin.

Um die MAC-Adressen bereits bekannter Geräte einzutragen, liest das Skript in Listing namedev die im DATA-Bereich stehenden Einträge zeilenweise aus. Sie stehen dort im gleichen Format wie sie das Original-arpalert-Skript aus [4] in seiner Konfigurationsdatei erwartet.

Listing 4: namedev

    01 #!/usr/bin/perl
    02 use strict;
    03 use warnings;
    04 use WatchLAN;
    05 
    06 my $db = WatchLAN->new();
    07 
    08 while(<DATA>) {
    09   if(/^#\s+(.*)/) {
    10     my $name = $1;
    11     my $nextline = <DATA>;
    12     chomp $nextline;
    13     my($mac, $ip, $ip_change) = 
    14                       split ' ', $nextline;
    15     $db->device_add($name, $mac);
    16   }
    17 }
    18 
    19 __DATA__
    20 
    21 # Slimbox
    22 00:04:20:03:00:0d 192.168.0.74 ip_change
    23 
    24 # Laptop Wireless
    25 00:16:6f:8d:58:db 192.168.0.75 ip_change
    26 
    27 # Laptop Wired
    28 00:15:60:c3:44:10 192.168.0.71 ip_change
    29 
    30 # Mike's Linux Box
    31 00:11:11:5b:ed:46 192.168.0.18
    32 
    33 ...

Alarm in Sektor B

Um festzustellen, ob es in den Datenbanktabellen einen activity-Eintrag eines Gerätes gibt, dessen name-Eintrag in der device-Tabelle gleich NULL ist, ist ein JOIN von zwei Tabellen erforderlich. Muss noch die IP-Adresse des Eintrags her, sind drei Tabellen betroffen. Rose erledigt dies automatisch hinter den Kulissen.

Das Skript arpemail benachrichtigt den Sysadmin bei neu auftauchenden MAC-Adressen. Es nutzt die Klasse WatchLAN::Activity::Manager, um eine SQL-Abfrage an die Datenbank abzuschicken. Die Methode get_activity() fragt die Tabelle activity ab und der Parameter with_objects bestimmt, dass auch die in den Tabellen device und ip_address referenzierten Daten extrahiert werden. Die betroffenen Tabellen werden von Rose mit t1 (activity), t2 (device), und t3 (ip_address) durchnumeriert, sodass sich die Abfrage

    query => [ "t2.name" => undef ],

auf die Tabelle device bezieht und Einträge abruft, deren name-Spaltenwert in der Datenbank gleich NULL sind. Das Ergebnis des Queries ist eine Referenz auf einen Array mit passenden Datenbankeinträgen. Jeder Eintrag ist ein Objekt vom Typ WatchLAN::Activity, das Methoden zum Erfragen seiner Spaltenwerte (und auch der Werte der referenzierten Tabellen) bereitstellt.

arpemail merkt sich einmal beanstandete Geräte in einem dateibasierten Cache der Marke Cache::File, damit es nicht immer wieder Meldungen mit denselben Warnungen ausschickt. Falls schon ein Cache-Eintrag zur MAC-Adresse $mac existiert liefert das Konstrukt

  !$cache->get($mac) &&
    ($cache->set($mac, 0) || 1);

einen falschen Wert zurück. Falls $mac noch unbekannt ist, kommt die nachgeschaltete set-Methode zum Einsatz, die dem Cache den neuen Wert unterjubelt, und das Konstrukt liefert einen wahren Wert zurück.

Diese Rückgabewerte macht sich der ab Zeile 18 herumgewickelte grep-Befehl zunutze und filtert bereits im Cache enthaltenen MAC-Adressen aus den in $events liegenden potentiellen Bandbreitenschorrern aus. Ist der von $events referenzierte Array anschließend leer, bricht das regelmäßig per Cronjob aufgerufene Programm ab.

Die Formatierung der Warnungsmeldung erfolgt mit dem Template-Toolkit. Das im DATA-Bereich am Ende des Skripts liegende Template erhält die Arrayreferenz $events als Parameter hereingereicht und iteriert mit einer FOREACH-Schleife über die Einträge. Die eigenwillige aber praktische Syntax des Template-Toolkits erlaubt es, die Methodenkette $e->ip_address()->string() mittels e.ip_address.sting aufzurufen.

Anschließend verbindet sich arpemail mittels des CPAN-Moduls Mail::Mailer mit dem lokalen Mailsystem und schickt die Nachricht per Email an den im To-Feld in Zeile 29 eingetragenen Sysadmin.

Listing 5: arpemail

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use WatchLAN;
    04 use Mail::Mailer;
    05 use Cache::File;
    06 use Template;
    07 my $cache = Cache::File->new(
    08   cache_root => "$ENV{HOME}/.arpemail");
    09 
    10 my $events = WatchLAN::Activity::Manager->
    11   get_activity(
    12     with_objects => [ 'device',
    13                       'ip_address' ],
    14     query   => [ "t2.name" => undef ],
    15     sort_by => ['minute'],
    16 );
    17 
    18 $events = [ grep { 
    19   my $mac = $_->device()->mac_address();
    20   !$cache->get($mac) &&
    21     ($cache->set($mac, 0) || 1);
    22 } @$events ];
    23 
    24 exit 0 unless @$events;
    25 
    26 my $mailer = new Mail::Mailer;
    27 $mailer->open({
    28   'From' => 'me@_foo.com',
    29   'To'   => 'oncall@_foo.com',
    30   'Subject' => "*** New MAC detected ***",
    31 });
    32 
    33 my $t = Template->new();
    34 $t->process(
    35   \*DATA, { events => $events }, $mailer
    36 ) or die $t->error();
    37 
    38 close($mailer);
    39 
    40 __DATA__
    41 [% FOREACH e = events %]
    42   When: [% e.minute %] 
    43   IP:   [% e.ip_address.string %]
    44   MAC:  [% e.device.mac_address %]
    45 
    46 [% END %]

Was war los?

Um festzustellen, welche Geräte sich in den letzten 24 Stunden auf dem LAN getummelt haben, definiert das Skript lastaccess mit dem DateTime-Modul vom CPAN einen genau 24 Stunden zurückliegenden Zeitpunkt. Der Rose-Manager feuert dann eine SQL-Abfrage ab, die alle seit diesem Zeitpunkt aufgetretenen Ereignisse aufsteigend nach der auf Minuten gerundeten Ereigniszeit minute sortiert liefert.

Der Hash %latest speichert dann jeweils nur das letzte Ereignis für verschiedene MAC-Adressen, indem es die Hashwerte für gleiche MAC-Adressen wieder und wieder überschreibt. Eigentlich sollte man solche Kalkulationen besser von der Datenbank erledigen lassen, Aggregatsfunktionen wie MAX() liefern mit GROUP BY genau das gewünschte Ergebnis. Leider funktioniert dies jedoch noch nicht mit dem Rose Objekt-Wrapper, aber bei der rasant fortschreitenden Entwicklung ist dieses Feature wahrscheinlich schon implementiert, wenn dieser Beitrag erscheint. In der ab Zeile 31 definierten Funktion time_diff berechnet das DateTime-Modul dann noch die menschenlesbare Zeitdifferenz aus der gegebenen Sekundendifferenz. Die Textersetzung in Zeile 42 transformiert die im Plural gegebenen Zeiteinheiten in die Einzahl, falls das Ergebnis genau eine Einheit ist. Die Ausgabe von lastaccess entspricht dann der anfangs in Abbildung 1 gezeigten.

Listing 6: lastaccess

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use WatchLAN;
    04 my $reachback = DateTime
    05   ->now( time_zone => "local" )
    06   ->subtract( minutes => 60 * 24 );
    07 
    08 my $events = WatchLAN::Activity::Manager->
    09   get_activity(
    10     query   => [ minute => 
    11                  { gt => $reachback },
    12                ],
    13     sort_by => ['minute'],
    14 );
    15 
    16 my %latest = ();
    17 
    18 for my $event (@$events) {
    19   $latest{$event->device_id()} = $event;
    20 }
    21 
    22 for my $id (keys %latest) {
    23     my $event = $latest{$id};
    24     my $name = $event->device()->name();
    25     $name ||= "unknown (id=$id)";
    26     printf "%23s: %s ago\n", $name, 
    27            time_diff($event->minute());
    28 }
    29 
    30 ###########################################
    31 sub time_diff {
    32 ###########################################
    33   my ($dt) = @_;
    34 
    35   my $duration = DateTime->now(
    36     time_zone => "local"
    37   ) - $dt;
    38 
    39   for (qw(hours minutes seconds)) {
    40       if(my $n = $duration->in_units($_)) {
    41           my $unit = $_;
    42           $unit =~ s/s$// if $n == 1;
    43           return "$n $unit";
    44       }
    45   }
    46 }

Wer das Skript arpemail noch erweitern möchte, kann, wie das auch arpalert ([4]) implementiert, für bestimmte Geräte noch eine statische IP in die device-Tabelle setzen und Alarm schlagen, falls ein unter statischen IP laufendes Gerät plötzlich unter einer anderen IP daherkommt. Wie immer sind der Entwicklerfreude keine Grenzen gesetzt, wenn erst einmal ein Framework steht und man die Daten ohne großen Aufwand aus einer Datenbank abpumpen kann.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2007/02/Perl

[2]
Charly Künast, ``Die Waffe des Inquisitors'', Linux-Magazin 11/2006

[3]
Michael Schilli, ``Verkehrskontrolle'', Linux-Magazin 11/2004, http://www.linux-magazin.de/Artikel/ausgabe/2004/11/perl/perl.html

[4]
Das Original arpalert-Skript: http://arpalert.org

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.