Linux-Magazin-Logo Die Zeitschrift für Linux-Professionals

Netzwerk-Sniffer mit GTK-Oberfläche

Verkehrskontrolle

Michael Schilli

Grafische Oberflächen mit GTK müssen Entwickler nicht in aufwändigem Spaghetti-Code definieren. Eine ausgefeilte Oberfläche entsteht mit dem GUI-Builder Glade per Drag&Drop. Die Beschreibung im XML-Format lesen Programme zur Laufzeit ein. So entsteht ein Netzwerk-Sniffer mit schöner Oberfläche.

Wer wissen möchte, welche Rechner auf dem lokalen Netzwerk ihr Unwesen treiben, und die Anzeige auch gerne noch in einem GUI hätte, der benutzt das hier vorgestellte Skript »capture.pl«. Es schnüffelt die vorbeisausenden Pakete mit Hilfe des CPAN-Moduls »Net::Pcap« (siehe auch[2]) aus der Leitung oder aus dem drahtlosen Netzwerk, dekodiert sie, stellt fest, welche aus dem LAN kommen, und zeigt die gefundenen IP-Adressen in einem Text-View-Widget (siehe Abbildung 1).

Abbildung 1: Das GTK-Programm »capture.pl« zeigt alle Rechner im LAN in einer Liste an.

Neue Adressen platziert es oben und baut dynamisch die Anzeige auf. Im »File«-Menü findet sich ein »Reset«-Eintrag, mit dem Anwender alle gefundenen Adressen aus der Liste löschen. Über »Quit« beendet sich das Programm.

GUI in XML

Das Skript definiert die grafische Oberfläche dabei nicht über feste Programm-Anweisungen, sondern liest zur Laufzeit eine XML-Beschreibung ein. Die erstellt der Programmierer mit dem Tool Glade 2 von[3]. Anschließend baut »capture.pl« die Oberfläche auf und verarbeitet ankommende Events.

Dieser Ansatz unterscheidet sich von typischen GUI-Buildern: Sie lassen den Entwickler zwar auch mit Drag&Drop Widgets platzieren und Events definieren, wandeln das Ergebnis jedoch in Code um, der anschließend vom Entwickler noch den letzten Schliff erhält. Einmal von Hand nachbearbeiteter Code ist aber meist nicht mehr vom GUI-Builder einlesbar.

Drag&Drop

Glade beherrscht beide Verfahren, es generiert wahlweise C-Code oder eben eine XML-Beschreibung, die Programme mit der Bibliothek Libglade verarbeiten. »Gtk2::GladeXML« vom CPAN ist der zugehörige Perl-Wrapper. Abbildung 2 zeigt Glade im Einsatz. Links oben ist das Hauptfenster, mit dem Anwender ein neues Projekt anlegen. Der Werkzeugkasten unten links enthält die verschiedenen Widgets, in der Mitte ist die fertige Applikation zu sehen. Das Fenster rechts oben kümmert sich um die Eigenschaften des ausgewählten Widget wie Name, Maße, Editierbarkeit und verarbeitete Signale. Rechts unten befindet sich das Widget-Tree-Fenster, das eine hierarchische Ansicht aller erstellten Widgets in der Applikation bietet.

Abbildung 2: Mit Glade konstruieren Programmierer komfortabel ihre GUIs. Die Beschreibung landet in einer XML-Datei.

Attribute anpassen

Um eine neue GUI-Beschreibung zu erzeugen genügt ein Klick auf das Hauptfenster-Icon im Werkzeugkasten (links oben mit blauem Streifen). Daraufhin öffnet sich das leere Applikationsfenster von Abbildung 3. Ein VBox-Container sorgt dafür, dass der Menübalken oben schwebt und das Textfeld unten. Um weitere Widgets zu platzieren, genügt ein Klick auf das entsprechende Feld im Werkzeugkasten und dann ein weiterer in das Applikationsfenster.

Abbildung 3: Das leere Hauptfenster in Glade, das später die Widgets enthält.

Fehlen nur noch ein Menü, ein Scrolled Window und das Text-View-Widget. Abbildung 4 zeigt das fast fertige Applikationsfenster. Das vorher eingefügte Menü ist für die Zwecke von »capture.pl« aber noch zu umfangreich. Ein Klick auf »Edit Menus« im Properties-Fenster öffnet einen Dialog, in dem sich die Einträge bequem editieren lassen (siehe Abbildung 5). Weitere Anpassungen gehen ebenso leicht von der Hand. Länge und Breite des Hauptfensters im »capture.pl«-GUI sind über die Properties auf 300 und 120 gesetzt.

Abbildung 4: Die Oberfläche ist fast fertig. Nur die überflüssigen Menü-Einträge müssen noch raus.

Abbildung 5: Der Menü-Editor erlaubt einfaches Editieren von Menü-Widgets.

Ein Klick auf den »Save«-Knopf im Hauptfenster von Glade sichert in diesem Beispiel nach Angabe des Projektnamens »capture« zwei Dateien: »capture.glade« und »capture.pglade«. Die zweite ist in diesem Zusammenhang irrelevant, in der ersten steht die XML-Beschreibung des GUI.

Das Skript »capture.pl« liest diese Beschreibung in Zeile 24 beim Aufruf des Konstruktors von »Gtk2::GladeXML« ein. Im XML stehen Definitionen der einzelnen Widgets, deren Platzierung relativ zum GUI und ihre eingestellten Attribute. So sind beispielsweise in der XML-Beschreibung zum Text-View-Widget folgende Attribute definiert:

<property name="editable">False</property>
<property name="cursor_visible">False</property>

Es ist zu sehen, dass der Programmierer in Glade das Widget mit entsprechenden Knöpfen als nicht-editierbar und mit unsichtbarem Text-Cursor definiert hat. Die folgenden zwei Codezeilen führen zum gleichen Ergebnis:

$text->set_editable(0);
$text->set_cursor_visible(0);

Signale

Den dynamischen Teil des statisch definierten GUI legt die Methode »signal _autoconnect_all« in Zeile 47 fest. Sie verbindet die in der XML-Beschreibung mit den Widgets assoziierten Signale wie etwa »on_quit1_activate« (»File | Quit«-Menü-Eintrag angeklickt) und »on_reset1_activate« (»File | Reset«-Eintrag ausgewählt) mit entsprechenden Perl-Funktionen.

Die gewählten Namen entsprechen den von Glade automatisch zugeordneten (siehe Abbildung 5). Wer möchte, ändert sie während der GUI-Konstruktion. Wenn »capture.pl« in Zeile 67 dann mit »main_loop()« in die Hauptschleife springt, geht alles seinen programmierten Gang: Das GUI erscheint auf dem Bildschirm und der Benutzer klickt nach Belieben darin herum.

Ruckelfreie Anzeige

Das Schnüffeln im Netzwerk beschlagnahmt die CPU, die sich währenddessen nicht mehr um die Oberfläche kümmert. Um also gleichzeitig das GUI in Schuss zu halten und mit dem Perl-Modul »Net: :Pcap« das Ethernet abzuschnüffeln, erzeugt »capture.pl« in Zeile 32 mittels »fork()« einen Kindprozess.

Eine vorher mit »pipe()« erzeugte Pipe dient als Kommunikationskanal zwischen dem Abkömmling und dem Elternteil. Findet das Kind eine neue IP-Adresse im Netz, sendet es sie als String über das »WRITEHANDLE« der Pipe an den elterlichen GUI-Verwalter, der die Nachricht am anderen Ende der Röhre über »READHANDLE« aufschnappt.

Damit die grafische Oberfläche nicht aktiv auf Ereignisse aus der Pipe warten muss, sondern sich um Benutzereingaben kümmern kann, ist ab Zeile 62 mit

Glib::IO->add_watch(
       fileno(READHANDLE),
       'in', \&watch_callback);

ein Watch definiert, das immer dann die ab Zeile 70 definierte Callback-Funktion »watch_callback« aufruft, wenn Daten auf »READHANDLE« eintrudeln.

GTK 2 baut auf der Bibliothek Glib auf und ist daher in der Lage, deren Lowlevel-Dienste in Anspruch zu nehmen. Da »add_watch()« einen File-Deskriptor und kein File-Handle erwartet, wandelt die Perl-Funktion »fileno()« das Handle »READHANDLE« entsprechend um.

Im Büro des Schnüfflers

»Net::Pcap« vom CPAN ist eine Schnittstelle zur Libpcap-Bibliothek. Sie sammelt Pakete aus dem Netzwerk, analysiert und filtert sie performant nach voreingestellten Bedingungen. Die Sniffer-Programme wie Ethereal basieren ebenfalls auf der Libpcap.

Die Funktion »snooper()« ab Zeile 90 sucht mit »Net::Pcap::lookupdev()« das erste aktive Netzwerk-Interface des Host heraus, üblicherweise »eth0«. Das darauf folgende »Net::Pcap::lookupnet()« findet die zugehörige Netzwerkadresse und -maske. »Net::Pcap::open_live()« ab Zeile 102 öffnet ein Live-Capture und schnappt sich bis zu 1024 Bytes pro Paket zur Analyse. Weil der dritte Parameter auf »1« steht, schaltet die Funktion die Netzwerkkarte in den Promiscuous Mode, veranlasst sie also dazu, nicht nur für sie bestimmte Pakete abzugreifen, sondern alle vorbeiflitzenden zu sammeln. »-1« als vierter Parameter bestimmt, dass kein Timeout (in Millisekunden) vorgegeben ist.

Die Funktion »Net::Pcap::loop()« ab Zeile 105 springt in eine Schleife, die jedes Mal, wenn sie ein Paket findet, die eingestellte Callback-Funktion »snooper_callback()« anspringt. Der zweite Parameter bestimmt mit »-1«, dass das Schnüffeln endlos weitergeht und nicht nach einer voreingestellten Anzahl von Paketen Feierabend ist.

Der letzte Parameter im »Net::Pcap::loop()«-Aufruf bestimmt eine Referenz auf einen Array mit Daten, die »snooper_callback()« bei jedem Aufruf als ersten Parameter bekommt. Mit »[$fd, $addr, $netmask]« stehen dort drei Werte: Ein File-Deskriptor »$fd« für die Pipe sowie die vorher ermittelte Netzwerkadresse und -maske.

Pakete analysieren

»Net::Pcap::loop« sorgt auch dafür, dass »snooper_callback()« die Header- und Content-Informationen des aktuell aufgeschnappten Pakets erhält. In »snooper_callback()« extrahiert »NetPacket::Ethernet::strip« die Ethernet-Informationen aus dem Paket. »NetPacket::IP->decode()« nimmt sich den IP-Layer vor und gibt eine Referenz auf einen Hash zurück, der unter dem Schlüssel »src_ip« die IP-Adresse des Senders enthält. Diesen String wandelt »inet_aton()« aus dem »Socket«-Modul ins Binärformat mit Network Byte Order um.

Die zuvor ermittelten Werte für Netzwerkadresse (»$addr«) und -maske (»$netmask«) liegen im Binärformat des Prozessors (Big oder Little Endian) vor. Der »pack«-Aufruf ab Zeile 123 wandelt sie ins maschinenunabhängige Netzwerkformat um. Danach prüft »capture .pl«, ob es die IP-Adresse »$ip« im Netzwerk »$network_addr« gibt. Ist dies erfüllt, stammt das Paket aus dem lokalen Subnetz. »syswrite()« (Zeile 125) schickt die IP-Adresse ohne Zwischenpufferung an den Elternprozess.

Diese Nachricht wandert durch die Pipe und löst im GUI wegen des in Zeile 62 aufgesetzten Watch einen Event aus, der »watch_callback()« aufruft. Dort wird der globale Array »@IPS« aufgefrischt, der alle bislang bekannten IP-Adressen enthält. Neu gefundene IPs sind noch nicht im Hash »%IPS« gespeichert und landen mit »unshift()« am Anfang des Arrays »@IPS«. Zeile 81 stellt einen Text-String mit allen bekannten IP-Adressen zusammen, die durch New-Lines getrennt sind. Zeile 83 frischt das Text-View-Widget damit auf.

Installation

Das Skript benötigt wegen der verwendeten GTK-2-Oberfläche einen ganzen Rattenschwanz an zusätzlichen Modulen. Am einfachsten ist es daher, sie mit einer CPAN-Shell zu installieren, jedoch ist manchmal ein manueller Eingriff nicht zu vermeiden. Wichtig sind vor allem die Module »ExtUtils::Depends«, »ExtUtils::PkgConfig«, »Glib«, »Gtk2«, »Gtk2::GladeXML«, »NetPacket« und »Net::Pcap«. Ist die Libglade noch nicht auf dem Rechner installiert, finden Anwender sie auf[4]. Das Programm Glade steht auf[3] zur Verfügung. Bei der Installation von »Net::Pcap« ist darauf zu achten, dass die Testphase (»make test«) als Root laufen muss, auch wenn die eigentliche Installation gar keine Root-Rechte erfordert. Tritt trotzdem ein Fehler auf, hilft es, »make install« im Build-Verzeichnis aufzurufen.

Vor dem Starten von »capture.pl« ist sicherzustellen, dass die XML-Beschreibung in »capture.glade« verfügbar ist. Wer alles unter einem Dach haben will, ändert Zeile 24:

my $xml = join "\n", <DATA>;
   my $g = Gtk2::GladeXML->
       new_from_buffer($xml);

Jetzt lässt sich der XML-Salat aus »capture.glade« ans Ende von »capture.pl« in eine »DATA«-Sektion kopieren:

# ... Ende von capture.pl
__DATA__
<?xml version="1.0" ...
<!DOCTYPE glade-interface ...

Abbildung 6: Die XML-Beschreibung der grafischen Schnittstelle in der von Glade erstellten Datei »capture.glade«.

Wegen des Promiscuous Mode, in den das Skript die Netzwerkkarte versetzt, muss »capture.pl« als Root laufen.

Mit Glade lassen sich nicht nur einfache Oberflächen wie diese aufbauen, sondern auch weitaus kompliziertere. Dank Drag&Drop und Wysiwyg gestaltet sich die Arbeit sehr komfortabel. Die plattformunabhängige XML-Repräsentation ist elegant und hält platzraubende statische Widget-Definitionen vom Code fern - die Programmierer konzentrieren sich auf die wesentlichen dynamischen Aspekte der Applikation. (mwe)

Infos

[1] Listings zu diesem Artikel: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2004/11/Perl]

[2] Robert Casey, "Monitoring Network Traffic with Net::Pcap": The Perl Journal 7/2004, S. 6

[3] Glade: [http://glade.gnome.org]

[4] Sourcen für Libglade: [http://ftp.gnome.org/pub/GNOME/sources/libglade]

Der Autor

Michael Schilli arbeitet als Software-Engineer für AOL/Netscape in Mountain View, 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 ist: [http://perlmeister.com]

Listing 1: »capture.pl«

001 #!/usr/bin/perl
002 ###########################################
003 # capture -- Gtk2 GUI observing the network
004 # Mike Schilli, 2004 (m@perlmeister.com)
005 ###########################################
006 use warnings;
007 use strict;
008 
009 use Gtk2 -init;
010 use Gtk2::GladeXML;
011 use Glib;
012 use Net::Pcap;
013 use NetPacket::IP;
014 use NetPacket::Ethernet;
015 use Socket;
016 
017 our @IPS = ();
018 our %IPS = ();
019 
020 die "You need to be root to run this.\n" if
021     $> != 0;
022 
023     # Load GUI XML description
024 my $g = Gtk2::GladeXML->new(
025     'capture.glade');
026 
027     # Child/Parent communication pipe
028 pipe READHANDLE,WRITEHANDLE or
029     die "Cannot open pipe";
030 
031     # Fork off a child
032 our $pid = fork();
033 die "failed to fork" unless defined $pid;
034 
035 if($pid == 0) {
036         # Child, never returns
037     snooper(\*WRITEHANDLE);
038 }
039 
040     # Parent, init text window
041 my $buf = Gtk2::TextBuffer->new();
042 $buf->set_text("No activity yet.\n");
043 
044 my $text = $g->get_widget('textview1');
045 $text->set_buffer($buf);
046 
047 $g->signal_autoconnect_all(
048     on_quit1_activate => sub {
049             # Stop snooper
050         kill('KILL', $pid);
051         wait();
052         Gtk2->main_quit;
053       },
054     on_reset1_activate => sub {
055             # Reset display
056         @IPS = ();
057         %IPS = ();
058         $buf->set_text("");
059       }
060 );
061 
062 Glib::IO->add_watch(
063     fileno(READHANDLE),
064     'in', \&watch_callback);
065 
066     # Enter main loop
067 Gtk2->main();
068 
069 ###########################################
070 sub watch_callback {
071 ###########################################
072     chomp(my $ip = <READHANDLE>);
073 
074         # Register IP if unknown
075     unshift @IPS, $ip
076        unless exists $IPS{$ip};
077     $IPS{$ip}++;
078 
079     my $text = "";
080 
081     $text .= "$_\n" for @IPS;
082 
083     $buf->set_text($text);
084 
085       # Return true to keep watch
086     1;
087 }
088 
089 ###########################################
090 sub snooper {
091 ###########################################
092     my($fd) = @_;
093 
094     my($err, $addr, $netmask);
095     my $dev = Net::Pcap::lookupdev(\$err);
096 
097     if(Net::Pcap::lookupnet($dev, \$addr,
098                        \$netmask, \$err)) {
099         die "lookupnet on $dev failed";
100     }
101 
102     my $object = Net::Pcap::open_live($dev,
103                        1024, 1, -1, \$err);
104 
105     Net::Pcap::loop($object, -1,
106       \&snooper_callback,
107       [$fd, $addr, $netmask]);
108 }
109 
110 ###########################################
111 sub snooper_callback {
112 ###########################################
113     my($user_data, $header, $packet) = @_;
114 
115     my($fd, $addr, $netmask) = @$user_data;
116 
117     my $edata =
118        NetPacket::Ethernet::strip($packet);
119 
120     my $ip = NetPacket::IP->decode($edata);
121 
122     if((inet_aton($ip->{src_ip}) &
123         pack('N', $netmask)) eq
124           pack('N', $addr)) {
125         syswrite($fd, "$ip->{src_ip}\n");
126     }
127 }