Schnüffler im Frack (Linux-Magazin, November 2004)

Grafische Oberflächen mit Gtk2 muss der Entwickler nicht als Spaghetti-Code im Skript definieren. Mit dem Werkzeug Glade entstehen durch Drag-and-Drop Schritt für Schritt selbst ausgefeilte GUIs. Das Ergebnis liegt anschließend als XML-Beschreibung vor und wird vom Skript zur Laufzeit eingelesen.

Abbildung 1: Die Schnüffel-GUI im Einsatz

Wer wissen möchte, welche Rechner auf dem lokalen Netzwerk ihr Unwesen treiben und die Anzeige auch gerne noch in einer Gtk2-Oberfläche hätte, dem dürfte das heute vorgestellte Skript capture.pl gefallen. Es schnüffelt die vorbeisausenden Pakete mittels des CPAN-Moduls Net::Pcap von der Leitung oder aus dem drahtlosen Netzwerk, dekodiert sie, stellt fest, welche von einer Adresse auf dem lokalen Subnetz stammen und stellt die gefundenen IP-Adressen in einem TextView-Widget dar (Abbildung 1). Neu gefundene Adressen platziert es oben und baut so dynamisch die Anzeige auf. Im File-Dropdown des Menübalkens findet sich ein Reset-Eintrag zur Löschung der bisher gefundenen IPs und ein Quit-Eintrag zum Verlassen des Programms.

Dabei definiert das Skript die GUI gar nicht auf programmatischem Weg, sondern liest zur Laufzeit eine vorher mit dem Tool glade-2 ([4]) erstellte XML-Beschreibung ein. Anschließend stellt es die Oberfläche dar und verarbeitet hereinkommende Events. Dieser Ansatz unterscheidet sich vom typischen Anwendungsfall grafischer GUI-Tools: Diese lassen den Entwickler zwar auch mittels Drag-and-Drop Widgets platzieren und Events definieren, wandeln das Ergebnis aber in Code um, der dann vom Entwickler noch den letzten Schliff erhält. Nachteil: Einmal von Hand nachbearbeiteter Code ist im allgemeinen nicht mehr maschinell änderbar.

glade-2 beherrscht beide Verfahren, es generiert wahlweise C- (und auch anderen) Code oder eben eine XML-Beschreibung, die dann das Skript mit der libglade-Bibliothek verarbeitet. Gtk2::GladeXML vom CPAN ist der zugehörige Perl-Wrapper.

Abbildung 2: Mit Glade konstruieren sich GUIs ganz einfach visuell.

Abbildung 2 zeigt glade-2 im Einsatz: Links oben ist das Hauptfenster, mit dem man ein neues Projekt anlegt. Unten links ist der Werkzeugkasten mit verschiedenen Widgets. In der Mitte steht die fertige Applikation, rechts oben ein Fenster mit zusätzlichen Optionen für das gerade ausgewählte Widget. Hier lassen sich der Name, unter dem das Widget später im Code referenziert wird einstellen und auch sonstige Widget-Eigenschaften, wie Maße, Editierbarkeit, verarbeitete Signale und vieles mehr. Das Widget-Tree-Window rechts unten zeigt hingegen die hierarchische Ordnung bisher definierter Widgets.

Um eine neue GUI-Beschreibung zu erzeugen, klickt man zunächst auf das Hauptfenster-Icon im Werkzeugkasten (links oben mit blauem Streifen) und erhält sofort ein leeres Applikationsfenster, wie in Abbildung 3 gezeigt. In dieses kommt dann ein Container in Form einer V-Box, damit später der Menübalken oben schwebt und das Textfeld unten. Um neue Widgets in der Applikation zu platzieren, klickt man sie zunächst im Werkzeugkasten an und klickt anschließend noch einmal an die entsprechende Stelle in der aufzubauenden Applikation. Nicht ganz Drag-and-Drop, aber fast.

Abbildung 4 zeigt den Dialog nach dem Auswählen der V-Box (Symbol mit den drei horizontalen Streifen in der Toolbox), die mit zwei Reihen konfiguriert wird. In das obere Feld wird in Abbildung 5 ein Menü platziert, in Abbildung 6 kommt unten ein Scrolled Window hinzu und in Abbildung 7 schließlich kommt darauf noch ein TextView-Widget. Das vorher erzeugte und in Abbildung 7 probehalber ausgeklappte Standard-Menü ist für die Zwecke unserer Mini-Applikation zu komplex. Um das zu korrigieren, reicht ein Klick auf ``Edit Menus'' im ``Properties''-Window (siehe Abbildung 2), um mittels des Dialogs in Abbildung 8 die Einträge drastisch zu reduzieren.

Auch die anderen Widgets lassen sich konfigurieren, so wurde zum Beispiel Länge und Breite des Hauptfensters der capture-GUI im Property-Window manuell auf 300 und 120 gesetzt.

Abbildung 3: Zuerst das Hauptfenster ...

Abbildung 4: ... darin eine zweireihige V-Box ...

Abbildung 5: ... ein Menü oben ...

Abbildung 6: ... ein Scrolled Window unten ...

Abbildung 7: ... und ein TextView-Widget darin. Das Standard-Menü ist allerdings zu ausführlich ...

Abbildung 8: ... und deswegen wird es noch vereinfacht.

Ein Klick auf den ``Save''-Knopf im Hauptfenster von glade-2 sichert schließlich nach Angabe des Projektnamens capture zwei Dateien: capture.glade und capture.pglade. Die zweite ist für unsere Zwecke irrelevant, die erste ist die XML-Beschreibung der GUI.

Das Skript capture.pl liest diese Beschreibung in Zeile 24 beim Aufruf des Konstruktors zu Gtk2::GladeXML ein. Im XML stehen Definitionen der einzelnen Widgets, deren Platzierung relativ zur GUI, und ihre eingestellten Attribute: So stehen beispielsweise in der XML-Beschreibung zum TextView-Widget die Attribute

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

Diese Werte stammen aus der Konstruktionsphase der GUI mittels glade-2. Dort wurde das Widget mittels entsprechender Knöpfe als nicht-editierbar und mit unsichtbarem Cursor definiert.

Zum gleichen Ergebnis hätten auch folgende Codezeilen geführt:

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

Signale

Den dynamischen Teil der statisch definierten GUI definiert die signal_autoconnect_all-Methode in Zeile 47. Sie verbindet die in der XML-Beschreibung mit den Widgets assoziierten Signale wie on_quit1_activate (File/Quit-Menü gewählt) und on_reset1_activate (File/Reset-Menü angeklickt) mit entsprechenden Perl-Funktionen (Abbildung 8). Die gewählten Namen entsprechen den von glade-2 automatisch zugeordneten, wer möchte, kann sie während der GUI-Konstruktion in glade-2 verändern.

Wenn capture.pl in Zeile 67 dann mit main_loop() in die Hauptschleife springt, geht alles seinen vorprogrammierten Gang: Die GUI erscheint auf dem Bildschirm und der Benutzer darf nach Belieben darauf herumklicken.

Abbildung 9: Die XML-Beschreibung der GUI in capture.glade.

Ruckelfreies Schnüffeln

Das Schnüffeln im Netzwerk beschlagnamt allerdings die CPU, die sich währenddessen nicht mehr um die Oberfläche kümmern kann. Das stößt dem Anwender freilich sauer auf, denn niemand schätzt graue Löcher im Bildschirm. Um gleichzeitig die GUI in Schuss zu halten und mittels des Perl-Moduls Net::Pcap das Ethernet abzuschnüffeln, erzeugt capture.pl in Zeile 32 mittels fork() einen Kindprozess. Eine vorher mit pipe erzeugte Pipe stellt den 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, definiert sie ab Zeile 62 mittels

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

einen ``Watch'', der immer dann die ab Zeile 70 definierte Callback-Funktion watch_callback aufruft, falls Daten auf READHANDLE Daten eintrudeln. Gtk2 baut auf der Glib auf und kann deswegen deren Low-Level-Dienste in Anspruch 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

Mit Net::Pcap vom CPAN steht in Perl eine Schnittstelle zur libpcap-Bibliothek zur Verfügung. Letztere schnupft Pakete vom Netzwerk, analysiert sie und filtert sie performant nach voreingestellten Bedingungen. Auch Programmpakete wie Ethereal basieren darauf.

Die Funktion snooper() ab Zeile 90 sucht zunächst mit Net::Pcap::lookupdev das erste aktive Netzwerkinterface des aktuellen Hosts heraus. Das ist üblicherweise "eth0", und das darauffolgende 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 es die Netzwerkkarte in den promiscuous mode, veranlasst sie also dazu, nicht nur für sie bestimmte Pakete zu erforschen, sondern neugierig alle vorbeiflitzenden zu begaffen. -1 als vierter Parameter bestimmt, dass kein Timeout in Millisekunden vorgegeben ist.

Net::Pcap::loop ab Zeile 105 springt in eine Schleife, die jedesmal, wenn sie ein Paket findet, die eingestellte Callback-Funktion snooper_callback anspringt. Der zweite Parameter bestimmt mit -1, dass der Reigen 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 nützlichen Daten, die snooper_callback bei jedem Aufruf als ersten Parameter eingetrichtert bekommt. Mit [$fd, $addr, $netmask] stehen dort drei Werte: Ein File-Deskriptor $fd, um durch die Pipe an den Elternprozess zu schreiben, sowie die vorher ermittelte Netzwerkadresse und -maske.

Net::Pcap::loop sorgt dafür, dass nicht nur diese Daten, sondern auch die Header- und Content-Information des aktuell aufgeschnappten Pakets an snooper_callback weitergeleitet werden.


Dort extrahiert C<NetPacket::Ethernet::strip> die Ethernet-Information
aus dem Paket, C<NetPacket::IP-E<gt>decode()> nimmt sich den IP-Layer
vor und gibt eine Referenz auf einen Hash zurück, der unter dem
Schlüssel C<src_ip> die IP-Adresse des Senders enthält. Diesen String
im Format "xx.xx.xx.xx" wandelt C<inet_aton()> aus dem C<Socket>-Modul ins
Binärformat mit Network-Byteorder um. Die vorher ermittelten Werte
für Netzwerkadresse (C<$addr>) und -maske (C<$netmask>) liegen im Binärformat
des aktuell benutzten Prozessors (big/little Endian) vor und müssen deswegen 
noch mittels C<pack 'N', ...> ins maschinenunabhänige Netzwerkformat
umgewandelt werden, bevor C<capture.pl> mit
    ($ip & $mask) eq $network_addr

prüfen kann, ob die IP-Adresse $ip aus dem Netzwerk $network_addr stammt. Nur falls obige Bedingung erfüllt ist, wurde das gerade analysierte Paket irgendwo vom Subnet des aktuellen Hosts abschickt und muss deshalb berücksichtigt werden.

syswrite in Zeile 125 sorgt dafür, dass die Nachricht an den Elternprozess ohne Zwischenpufferung geschickt wird und die gefundene IP-Adresse als String im Format xx.xx.xx.xx mit einem nachfolgenden Newline enthält.

Die Nachricht wandert durch die in Zeile 28 definierte Pipe und löst in der GUI wegen des in Zeile 62 aufgesetzten Watches einen Event aus, der watch_callback() aufruft. Dort wird der globale Array @IPS aufgefrischt, der alle bislang bekannten IP-Addressen 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 Textstring mit allen bekannten IP-Adressen zusammen, die durch Newlines getrennt sind. Zeile 83 frischt das TextView-Widget damit auf.

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 }

Installation

Das Skript benötigt wegen der verwendeten Gtk2-Oberfläche einen ganzen Rattenschwanz an Modulen, hier sind die wichtigsten: ExtUtils::Depends, ExtUtils::PkgConfig, Glib, Gtk2, Gtk2::GladeXML, Net::Pcap (fails), NetPacket. Am besten installiert man sie mit einer CPAN-Shell, manchmal sind aber manuelle Korrekturen notwendig.

Ist libglade noch nicht auf dem Rechner, kann man sie von [3] beziehen. Das Tool glade-2 steht auf [4] zur Verfügung und kann einfach kompiliert werden.

Bei der Installation von Net::Pcap ist darauf zu achten, dass die Testphase (make test) unter root laufen muss, auch wenn die eigentliche Installation gar keine root-Rechte bräuchte. Tritt trotzdem ein Fehler auf, hilft ein ``Augen zu und durch'' mit einem ``make install'' im Build-Verzeichnis.

Vor dem Starten von capture.pl ist sicherzustellen, dass die XML-Beschreibung der Oberfläche in capture.glade verfügbar ist. Wer alles gerne unter einem Dach führt, der kann Zeile 24 in

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

umändern und den XML-Salat aus capture.glade einfach am Ende von capture.pl in einer DATA-Sektion anhängen:

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

Wegen des promiscuous mode, in den die Netzwerkkarte versetzt wird, muss capture.pl als root laufen.

Mit glade-2 lassen sich auch weit kompliziertere Oberflächen erstellen. Drag-and-Drop und WYSIWYG versüßen die Pfrümelarbeit. Die plattformunabhängige XML-Repräsentation ist elegant und hält platzraubene statische Widget-Definitionen vom Code fern, der sich auf die wesentlichen dynamischen Aspekte konzentrieren kann.

Infos

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

(Bitte sowohl capture.pl als auch die XML-Beschreibung capture.glade dort ablegen.)

[2]
``Monitoring Network Traffic with Net::Pcap'', Robert Casey, The Perl Journal 7/2004, Seite 6ff.

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

[4]
Glade homepage: http://glade.gnome.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.