Im Dunkeln ist gut munkeln – doch damit ist jetzt Schluss: Der hier vorgestellte Perl-Daemon holt verborgene Vorgänge im Netz ans Licht und schlägt Alarm, wenn ihm etwas verdächtig vorkommt.
Was sich unter der Decke des LAN vollzieht, ist für den Anwender normalerweise unsichtbar. Zu den Vorgängen im Verborgenen zählt beispielsweise die Adressierung der Pakete auf ihrer letzten Etappe – inklusive der Ermittlung der nötigen MAC- auf Grundlage der IP-Adressen, für die ARP (Address Resolution Protocol) zuständig ist. Dieser schwer einsehbare Bereich bietet auch Platz für mancherlei fiese Tricks, mit deren Hilfe sich unter anderem ungebetene Gäste einschmuggeln könnten [1].
Zur Abwehr rüstet der Admin mit Werkzeugen auf, die Licht ins Dunkel bringen. Eines davon, Arpalert [2], hat mein Kolumnistenkollege Charly Kühnast vor einiger Zeit vorgestellt [3]. Der Daemon überwacht ARP-Anfragen und vergleicht deren Adressen mit einer Whitelist. Unbekannte MAC-Adressen lösen einen Alarm aus. Allerdings kommt es dabei zuweilen zu Doppel-Alarmen für ein und denselben Vorfall und die beiliegende Dokumentation (teils auf Französisch) ist grottenschlecht.
Schnüffler im Eigenbau
Dank der beiden Module Net::Pcap und NetPacket::Ethernet vom CPAN ist es aber überhaupt nicht schwer, mit einem eigenen Perl-Skript die MAC-Adressen von Paketen herauszufischen, die im LAN herumschwimmen. Und mit Rose, dem kürzlich in einem Snapshot vorgestellten objektorientierten Datenbankmapper [4], lassen sich diese Werte in eine MySQL-Datenbank mit Inno-DB-Tabellen einlesen, in der das Skript später nach Herzenslust stöbern kann.
Um zum Beispiel festzustellen, welche Geräte während der letzten 24 Stunden im LAN aktiv waren, genügt dann einfach ein Aufruf des gegen Ende des Artikels vorgestellten Skripts »lastaccess« (Listing 6). Das Ergebnis zeigt der Screenshot in Abbildung 1 – doch dazu später mehr.

Abbildung 1: Das Skript »lastaccess« listet genau auf, welche Geräte in den vergangenen 24 Stunden im LAN aktiv waren.
Ähnlich wie der einmal an dieser Stelle besprochene grafische Netzwerkschnüffler »capture« [5] 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 eigenen Rechner bestimmten Pakete, sondern leitet alle, die sie findet, an das Schnüffelskript weiter. Dazu sind Root-Rechte erforderlich, auf die Zeile 9 prüft. Ohne passende Berechtigungen bricht das Skript ab.
|
Listing 1: |
|---|
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 }
|
Die in Zeile 13 aufgerufene Funktion »lookupdev()« gibt den Namen eines verfügbaren Netzwerkdevice zurück. Bei nur einer angeschlossenen Netzwerkkarte ist das »eth0«. Das anschließende »open_live()« tritt in eine Endlosschleife ein (der Timeout wurde mit »-1« abgeschaltet), in der es jeweils die ersten 1024 Bytes jedes eintreffenden Pakets liest und sofort die Callback-Funktion »callback()« aufruft. Diese erhält nicht nur die vorher ermittelte lokale Netzwerkadresse und -maske als », sondern außerdem die rohen Paketdaten in ».
Das Modul NetPacket::Ethernet dekodiert diesen Ethernet-Frame und stellt unter dem Hash-Key »src_mac« die MAC-Adresse des Senders im Hexformat bereit. Sie enthält noch nicht die typischen Doppelpunkte nach jedem zweiten Zeichen, Zeile 40 setzt diese Trenner mit einem regulären Ausdruck ein.
In den Zeilen 48 bis 50 prüft »arpcollect« anhand der IP-Adresse, ob das Paket von einem Gerät im lokalen Netz stammt. Die Adresse ermittelt es anhand der Nutzdaten des Ethernet-Pakets, die Funktion »strip()« des Moduls NetPacket::Ethernet extrahiert sie. Das Ergebnis ist das rohe IP-Paket, das die Funktion »decode()« des Moduls NetPacket::IP weiter entpackt.
Schließlich und endlich findet sich unter dem Hash-Schlüssel »src_ip« die IP-Adresse des Absenders. Falls ein Bit-weises UND der IP-Adresse mit der Netzwerkadresse wieder die Netzwerkadresse ergibt, wurde das Paket von einem Gerät im lokalen Netzwerk versandt und ist daher für die Weiterverarbeitung relevant. Die Methode »event_add()« des zuvor instantiierten Datenbankobjekts vom Typ »WatchLAN« nimmt sowohl die IP- als auch die MAC-Adresse entgegen und pumpt sie zur späteren Analyse in die Datenbank.
Minutenpuffer
Das Modul »WatchLAN.pm« (Listing 3) implementiert die Speicherungsschicht. Jedes Paket sofort in der Datenbank abzulegen wäre nämlich nicht effektiv, weil damit selbst auf einem nur wenig ausgelasteten Netzwerk mehrere Schreibzugriffe pro Sekunde fällig würden. Außerdem beanspruchen mehrere Millionen Tabellenzeilen viel Plattenplatz und Rechen-Ressourcen.
|
Listing 2: |
|---|
01 #!/bin/sh 02 DBNAME=watchlan 03 mysqladmin -f -uroot drop $DBNAME 04 mysqladmin -uroot create $DBNAME 05 mysql -uroot $DBNAME <sql.txt |
|
Listing 3: |
|---|
01 ###########################################
02 package WatchLAN;
03 ###########################################
04 use strict;
05 use Apache::DBI; # share a single DB conn
06 use Rose::DB::Object::Loader;
07 use Log::Log4perl qw(:easy);
08 use DateTime;
09
10 my $loader = Rose::DB::Object::Loader->new(
11 db_dsn => 'dbi:mysql:dbname=watchlan',
12 db_username => 'root',
13 db_password => undef,
14 db_options => {
15 AutoCommit => 1, RaiseError => 1 },
16 class_prefix => 'WatchLAN'
17 );
18
19 $loader->make_classes();
20
21 ###########################################
22 sub new {
23 ###########################################
24 my ($class) = @_;
25
26 my $self = {
27 cache => {},
28 flush_interval => 60,
29 next_update => undef,
30 };
31
32 bless $self, $class;
33 $self->cache_flush();
34
35 return $self;
36 }
37
38 ###########################################
39 sub event_add {
40 ###########################################
41 my($self, $mac, $ip)= @_;
42
43 $self->{cache}->{"$mac,$ip"}++;
44 $self->cache_flush()
45 if time() > $self->{next_update};
46 }
47
48 ###########################################
49 sub cache_flush {
50 ###########################################
51 my ($self) = @_;
52
53 for my $key ( keys %{ $self->{cache} } ){
54 my ($mac, $ip) = split /,/, $key;
55 my $counter = $self->{cache}->{$key};
56
57 my $minute = DateTime->from_epoch(
58 epoch => $self->{next_update} -
59 $self->{flush_interval},
60 time_zone => "local",
61 );
62
63 my $activity = WatchLAN::Activity->new(
64 minute => $minute);
65
66 $activity->device(
67 { mac_address => $mac } );
68 $activity->ip_address(
69 { string => $ip } );
70 $activity->counter($counter);
71 $activity->save();
72 }
73
74 $self->{cache} = {};
75 $self->{next_update} = time() -
76 ( time() % $self->{flush_interval} ) +
77 $self->{flush_interval};
78 }
79
80 ###########################################
81 sub device_add {
82 ###########################################
83 my ( $self, $name, $mac_address ) = @_;
84
85 my $device = WatchLAN::Device->new(
86 mac_address => $mac_address );
87 $device->load( speculative => 1 );
88 $device->name($name);
89 $device->save();
90 }
91
92 1;
|
Aus diesem Grund speichert »WatchLAN.pm« die ankommenden Paketadressen zunächst in einem temporären Hash, dessen Inhalt es jeweils zur vollen Minute an die Datenbank überträgt. Ein Zähler wird mit jeder IP/MAC-Kombination um 1 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 zu erfolgen hat. Aus der aktuellen Zeit und »flush_interval« berechnet sich der Zeitpunkt des nächsten Spülvorgangs. Er landet in der Instanzvariablen »next_update«.
Extrawurst für MySQL
Listing 2 zeigt die notwendigen Shell-Befehle, um eine neue MySQL-Datenbank anzulegen. Die SQL-Befehle aus der Datei »sql.txt« sind in Abbildung 3 zu sehen. Das so entstehende Tabellenschema der Datenbank in Abbildung 2 verlinkt die Haupttabelle »activity« über Foreign Keys mit den Tabellen »devices« und »ip_addresses«. Sie speichern MAC-Adressen mit den Gerätedaten sowie IP-Adressen. Stünden die Adressen in der Haupttabelle, würde nicht nur Speicherplatz verschwendet, sondern auch Datenredundanz erzeugt.

Abbildung 2: Aus diesen drei Tabellen besteht die Datenbank, in die das Skript die gefundenen IP-Mac-Adressenkombinationen einträgt.

Abbildung 3: Mit diesen SQL-Befehlen legt »dbinit« (Listing 2) die nötigen Datenbanktabellen an, die das Schnüffelskript braucht.
MySQL macht es dem Rose::DB-Loader nicht gerade leicht, solche Relationen zu erkennen. Laut Rose::DB-Autor John Siracusa ist es notwendig, Foreign-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 3 (Zeile 19) und Rose::DB kontaktiert die Datenbank und definiert selbstständig den kompletten Objekt-Wrapper auf alle Tabellen und deren Spalten.
Objektorientierte Datenablage
Das Watch-LAN-Modul ruft den Rose-Loader auf, sobald eine Applikation es 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 nur ein neues Objekt » 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 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 zu finden ist, legt es dort einen neuen Record an. In der Haupttabelle »activity« ergänzt es im Feld »device_id« einen neuen Integerwert, der auf den Eintrag in »devices« verweist.
Einträge in der Tabelle »activity« werden hingegen ohne Angabe eines anonymen Hash vorgenommen, hierfür stellt Rose nach der entsprechenden Spalte benannte Methoden bereit. Der Aufruf »->counter();« setzt den Spaltenwert » im aktuell bearbeiteten Record auf den Wert ». Nach dem »save()« löscht »cache_flush()« den Cache, berechnet die nächste Auffrischzeit und kehrt zum Aufrufer zurück. Ä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 »->load(speculative => 1);« lädt einen Record aus der Tabelle »devices«, der zur vorher im Konstruktor von »WatchLAN::Device« angegebenen MAC-Adresse passt.
Das 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 Record 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 folgendes »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 jedes Mal, wenn ein solches Objekt ausgedient hat, löst Rose die Verbindung wieder. Das stellt zwar sicher, dass keine unerwünschten Nebeneffekte auftreten, ist jedoch Verschwendung. Wenn man wie im vorliegenden Fall ohne Transaktionen arbeitet, sorgt das Modul Apache::DBI allein durch sein bloßes Hinzuladen dafür, dass Rose genau eine persistente DB-Verbindung nutzt.
Die Tabelle »devices« speichert nicht nur MAC-Adressen, sondern ordnet ihnen auch gleich einprägsame Namen zu. Aus »00:11:11:5b:ed:46« wird so »Mike\’s Linux Box«, gleichzeitig beweist dieser Eintrag, dass es sich um ein auf dem lokalen Netz geduldetes Gerät handelt.
Namenlose fallen auf
Hängt sich der Wohnungsnachbar hingegen unerlaubterweise mit seinem Laptop ins Wireless-LAN und schnorrt wertvolle Bandbreite, schnappt »arpcollect« dies auf und trägt die MAC-Adresse in die Tabelle »devices« ein, lässt aber das »name«-Feld unberührt. Damit wird das weiter unten vorgestellte Überwachungsskript »arpemail« kurze Zeit später auf den Missstand aufmerksam und schickt eine E-Mail an den Admin.
Um die MAC-Adressen bereits bekannter Geräte einzutragen, liest das Skript in Listing 4 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: |
|---|
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 ...
|
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.
Alarm in Sektor B
Das Skript »arpemail« (Listing 5) 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, der Parameter »with_objects« bestimmt, dass auch die in den Tabellen »device« und »ip_address« referenzierten Daten extrahiert werden.
|
Listing 5: |
|---|
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 %]
|
Die betroffenen Tabellen nummeriert Rose mit »t1« (»activity«), »t2« (»device«) und »t3« (»ip_address«) durch, sodass sich die Abfrage »query => [ “t2.name” => undef ]« auf die Tabelle »device« bezieht und jene Einträge abruft, deren »name«-Spaltenwert in der Datenbank gleich »NULL« ist. Das Ergebnis der Anfrage ist eine Referenz auf ein 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.
Das Alarmskript »arpemail« merkt sich einmal beanstandete Geräte in einem Datei-basierten Cache der Marke »Cache:: File«, damit es nicht immer wieder Meldungen mit denselben Warnungen ausschickt. Falls schon ein Cache-Eintrag zur MAC-Adresse » existiert, liefert das Konstrukt in den Zeilen 20 und 21
!$cache->get($mac) && ($cache->set($mac, 0) || 1);
einen falschen Wert zurück. Falls » noch unbekannt ist, kommt die »set«-Methode zum Einsatz, die dem Cache den neuen Wert unterjubelt, und das Konstrukt liefert dann einen wahren Wert zurück.
Diese Rückgabewerte macht sich der ab Zeile 18 herumgewickelte »grep«-Befehl zunutze und filtert bereits im Cache enthaltene MAC-Adressen aus den in » liegenden potenziellen Bandbreitenschnorrern aus. Ist der von » referenzierte Array anschließend leer, bricht das regelmäßig per Cronjob aufgerufene Programm ab.
Post für den Admin
Die Formatierung der Warnmeldung erfolgt mit dem Template-Toolkit. Das im »DATA«-Bereich am Ende des Skripts liegende Template erhält die Arrayreferenz » 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 »->ip_address()->string()« mit »e.ip_address.string« aufzurufen.
Anschließend verbindet sich »arpemail« mit Hilfe des CPAN-Moduls Mail::Mailer mit dem lokalen Mailsystem und schickt die Nachricht per Email an den im »To«-Feld in Zeile 29 eingetragenen Admin. E
Um festzustellen, welche Geräte sich in den letzten 24 Stunden im LAN getummelt haben, definiert das Skript »lastaccess« (Listing 6) einen genau 24 Stunden zurückliegenden Zeitpunkt.
|
Listing 6: |
|---|
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 agon", $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 }
|
Was war los?
Der Rose-Manager feuert dann eine SQL-Abfrage ab, die alle seit diesem Zeitpunkt aufgetretenen Ereignisse liefert, aufsteigend sortiert nach der auf Minuten gerundeten Ereigniszeit »minute«. Der Hash »%latest« speichert daraufhin jeweils nur das letzte Ereignis für verschiedene MAC-Adressen, indem er die Hashwerte für gleiche MAC-Adressen wieder und wieder überschreibt. Eigentlich wären solche Kalkulationen besser von der Datenbank zu erledigen, Aggregatfunktionen wie »MAX()« liefern mit »GROUP BY« das Gewünschte. Allerdings funktionierte das zur Entstehungszeit dieses Artikels noch nicht mit Rose. Doch bei der rasant fortschreitenden Entwicklung des Datenbank-Wrappers ist das Feature wahrscheinlich schon implementiert, wenn der Beitrag erscheint.
Alarm erweiterbar
In der ab Zeile 31 definierten Funktion »time_diff« berechnet das »DateTime«-Modul noch für Menschen lesbar die 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 in Abbildung 1 gezeigten.
Wer das Skript »arpemail« noch erweitern möchte, kann – wie das auch »arpalert« [4] implementiert – für bestimmte Geräte eine statische IP in die »device«-Tabelle setzen und Alarm schlagen, falls ein unter statischer IP laufendes Gerät plötzlich mit anderer Adresse 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 holen kann. (jcb)
|
Infos |
|---|
|
[1] Achim Leitner, Thomas Demuth, “Interner Zugriff”: Linux-Magazin 06/04, S. 34 [2] Arpalert-Skript: [http://arpalert.org] [3] Charly Kühnast, “Die Waffe des Inquisitors”: Linux-Magazin 11/2006 [4] Michael Schilli, “Schädlingsbekämpfung”: Linux-Magazin, 09/06, S. 114 [5] Michael Schilli, “Verkehrskontrolle”: Linux-Magazin 11/04; [https://www.linux-magazin.de/Artikel/ausgabe/2004/11/perl/perl.html] [6] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2007/02/Perl] |
|
Der Autor |
|---|
|
|







