Guckloch ins Netzwerk (Linux-Magazin, Februar 2008)

Das Kommando netstat zeigt Aktivität des lokalen Rechners im Netzwerk an und hat schon manchem Admin den Tag gerettet. Mit ein paar Perl-Modulen vom CPAN lässt sich daraus ein dynamisches Terminal-Tool bauen, das netstat-Daten ähnlich komfortabel wie die Unix-Utility top anzeigt.

Wer wissen will, welche Ports auf einer Maschine gerade mit Netzwerkaufgaben beschäftigt sind, ruft netstat auf. Diese praktische Linux-Utility operiert in mehreren Modi, die der Benutzer durch Kommandozeilenoptionen einstellt. Mit -s aufgerufen gibt das Tool eine Statistik des abgewickelten Netzwerkverkehrs aus (Abbildung 2). Mit -put hingegen druckt es die Ports aller Applikationen, die über das TCP-Protokoll mit dem Netzwerk kommunizieren (Abbildung 1). Beide Ausgaben sind nützlich, und eigentlich interessiert auch die zeitliche Abfolge der Geschehnisse und nicht nur ein Sekundenschnappschuss.

Top für's Netzwerk

Um Prozesse zu beobachten, liegt Linux die Utilty top bei, die die Belastung des Rechners, sowie den Speicherverbrauch, die CPU-Auslastung und viele weitere Eckdaten der laufenden Prozesse anzeigt und dynamisch auffrischt. Aus der statischen Ausgabe von netstat eine dynamisch laufende Terminalapplikation a la top zu erzeugen, ist dank CPAN auch nicht weiter schwer.

Abbildung 1: Die Ausgabe des Kommandos netstat -put zeigt eine Liste aktiver TCP-Ports.

Das Modul Curses::UI, das im Perl-Snapshot in [2] schon einmal für einen Video-Selektor zum Einsatz kam, liefert auch heute das notwendige Framework, um Daten dynamisch im Terminal aufzufrischen und auf Tastendrücke des Users zu reagieren. Seine Eventschleife lässt sich leicht in den Kernel des Perl-Objekt-Environments (POE) einbinden, das den Ablauf vieler verschiedener Tasks in einem Prozess und einem Thread möglich macht.

Nicht einfrieren!

Allerdings stellt sich wie bei allen GUI-Applikationen, die externe Programme aufrufen, die Frage nach dem Einfrieren der Applikation. Ist ein Prozess nämlich mit anderen Dingen beschäftigt, bekommt er Benutzereingaben und Mausklicks nicht mehr mit und hängt unbeweglich im Window-Manager herum. Das Gefühl der 'eingefrorenen' Applikation macht sich beim Anwender breit.

Das Kommando netstat läuft zwar im allgemeinen recht zügig durch, mit der Option -put löst es allerdings die einem Socket zugehörigen IP-Adressen mittels eines Reverse-DNS-Aufrufs in Hostnamen auf. Bei einem langsamen DNS-Server oder recht vielen dargestellten Sockets kann dies jedoch zu beachtlichen Verzögerungen führen, manchmal dauert es einige Sekunden, bis netstat endlich fertig ist. Dies könnte man durch die Option -n verhindern und sich mit den IP-Adressen statt der aufgelösten Hostnamen begnügen, aber wer den vollen Luxus möchte, bindet den Aufruf von netstat einfach in den POE-Reigen des GUI-Moduls ein.

Abbildung 2: Das Kommando netstat -s liefert statistische Daten über den von Linux bewältigten Netzwerkverkehr.

Renner-Komponente

Die GUI-Applikation in Curses::UI tickt im Zusammenspiel mit dem POE-Kernel. Tastendrücke und Mausklicks verarbeitet dieser als Events, gleichberechtigt mit Events von anderen POE-Sessions.

Folglich liegt es nahe, auch den Aufruf von netstat darin einzubinden. POE soll den Prozess starten, aber nicht auf das Ergebnis warten, sondern die Kontrolle sofort wieder an den Kernel zurückgeben, damit dieser weiterhin die Anzeige auffrischen und auf Tastendrücke reagieren kann. Trudelt die Ausgabe von netstat dann endlich ein, bekommt der Kernel dies wiederum als Event mit und ruft die Funktion auf, die die Daten filtert und in Variablen zur Weiterverarbeitung auf der Displayebene ablegt.

Das Modul POE::Wheel::Run ist Bestandteil der POE-Distribution vom CPAN. Es betreut einen externen Prozess und springt Zustände eines Automaten an, falls sich etwas auf der Standardausgabe des Prozesses tut. Andere Events treten auf, falls sich der Prozess erfolgreich oder auch wegen eines Fehlers beendet.

Räder im Kernel

Das den Prozess betreuende ``Rädchen'' wiederum findet in einer POE::Session Platz, einem Zustandsautomaten, der sich in den POE-Kernel einklinkt. Der Kernel sorgt dann dafür, dass der Automat hin und wieder eine Zeitscheibe zugeteilt bekommt, muss aber sicherstellen, dass alle eingetragenen Sessions irgendwann einmal drankommen. Anders als im preemptiven Linux-Kernel, der schon mal einem Prozess den Teppich unter den Füßen wegreißt, verlässt sich das POE-Framework darauf, dass alle Sessions sich rücksichtsvoll verhalten.

Jede Session muss die Kontrolle sofort wieder an den Kernel zurückgeben, sobald sie nicht mehr mit voller Geschwindigkeit laufen kann. Bei diesem kooperativen Multitasking ist es wichtig, dass alle Tasks, die auf etwas warten (Festplatte, ein Netzwerkereignis oder die Ausgabe eines externen Prozesses) mitspielen, ein rücksichtsloser Teil legt das gesamte System lahm. Jede Session verfügt über einen privaten Datenbereich, den ``Heap''. Er ist als Hash implementiert und speichert Key/Value-Paare angefallener Session-Daten.

Abbildung 3: Der in Perl geschriebene Monitor zeigt laufend die aktuellen TCP-Verbindungen an.

Autonome Automaten

Autonome Zustandsautomaten kapselt man in der POE-Welt gerne in sogenannten ``Components''. Diese Klassen bindet eine Applikation dann nur ein, erzeugt ein neues Objekt und hinter den Kulissen klinkt sich eine eigenständige Applikation in den POE-Kernel ein. Listing PoCoRunner.pm (PoCo ist die gängige Abkürzung für POE-Component) zeigt eine Komponente, die den Namen eines Programms (z.B. netstat) mit Parametern entgegennimmt und ein ``Wheel'' erzeugt, das einen externen Prozess mit dem angegebenen Programm abfeuert. Danach gibt das Rad die Kontrolle sofort wieder an den Kernel zurück, ohne auf das Ergebnis zu warten.

Bei jeder Zeile, die in der Standardausgabe des Prozesses auftaucht, springt POE den Zustand stdout und damit die Methode stdout() von PoCoRunner.pm an. Dort sammelt die Session-eigene Heap-Variable data (ein Skalar) die Daten als Text ein. Ist netstat beendet, springt der Automat im Erfolgs- wie im Fehlerfall die Methode finish an. Dieser kopiert die gesammelten STDOUT-Daten in einem Rutsch an die per Referenz an den Konstruktor new hereingereichte Variable data.

Anschließend ruft finish() in Zeile 71 die Methode delay() des Kernels auf und veranlasst ihn, nach einer eingestellten Verzögerung die Methode run aufzurufen, die die Heapvariable data zurücksetzt und das Rädchen POE::Wheel::Run erneut mit dem eingestellten externen Program netstat aufruft.

Bindet also jemand diese Komponente in ein POE-Programm ein und ruft den Konstruktor mit einem Kommando, einer Option, einer Intervalldauer und einer Referenz auf einen Skalar auf, ruft die Komponente nicht nur das externe Kommando wieder und wieder auf, sondern sorgt auch dafür, dass in dem Skalar stets dessen neueste und komplette Ausgabe liegt.

Den Zustandsautomaten von PoCoRunner.pm definiert der Aufruf der Methode create() in Zeile 18. Die Zustände sind _start (Ausgangszustand), run (feuert den Prozess ab), stdout (Prozess sendet eine Ladung Daten nach STDOUT) und finish (Prozess beendet). Diese Zustände bildet POE wegen des Parameters package_states in Zeile 19 auf die gleichnamigen Funktionen innerhalb des Moduls PoCoRunner.pm ab.

Parameter POE-Style

Es fällt auf, dass POE Session-Parameter auf eigenwillige Art und Weise übergibt. Steht in einem Event-Handler zum Beispiel

   my($kernel, $heap) = 
     @_[KERNEL, HEAP];

überreicht die Session dem Handler eine Reihe von Argumenten im Perl-typischen Array @_. Der Handler angelt sich nur zwei davon über die Macros KERNEL und HEAP. Diese von POE in den Namensraum importierten konstanten Funktionen liefern Integerwerte zurück, sodass das Konstrukt oben ein sogenanntes Array-Slice darstellt, das eine Untermenge der im Array liegenden Parameter als Liste zurückliefert.

Listing 1: PoCoRunner.pm

    01 ###########################################
    02 package PoCoRunner;
    03 # Mike Schilli, 2007 (m@perlmeister.com)
    04 ###########################################
    05 use strict;
    06 use warnings;
    07 use POE::Wheel::Run;
    08 use POE;
    09 
    10 our $PKG = __PACKAGE__;
    11 
    12 ###########################################
    13 sub new {
    14 ###########################################
    15   my($class, %options) = @_;
    16 
    17   my $self = { %options };
    18 
    19   POE::Session->create(
    20     package_states => [ 
    21       $PKG => [ qw(_start stdout 
    22                    finish run) ]
    23     ],
    24     heap => { self => $self },
    25   );
    26 
    27   bless $self, $class;
    28 }
    29 
    30 ###########################################
    31 sub _start {
    32 ##########################################
    33   my ($kernel, $session) = 
    34                        @_[KERNEL, SESSION];
    35   $kernel->post($session, "run");
    36 }
    37 
    38 ###########################################
    39 sub run {
    40 ###########################################
    41   my ($kernel, $heap, $session) = 
    42                  @_[KERNEL, HEAP, SESSION];
    43 
    44   my $wheel = POE::Wheel::Run->new(
    45     Program    => $heap->{self}->{command},
    46     ProgramArgs => [$heap->{self}->{args}],
    47     StdoutEvent => "stdout",
    48     ErrorEvent  => "finish",
    49     CloseEvent  => "finish",
    50   );
    51 
    52   $heap->{"wheel"}  = $wheel;
    53   $heap->{"stdout"} = "";
    54 }
    55 
    56 ###########################################
    57 sub stdout {
    58 ###########################################
    59   my ($input, $heap) = @_[ARG0, HEAP];
    60 
    61   $heap->{stdout} .= "$input\n";
    62 }
    63 
    64 ###########################################
    65 sub finish {
    66 ###########################################
    67   my ($kernel, $heap) = @_[KERNEL, HEAP];
    68 
    69   ${ $heap->{self}->{data} } = 
    70                            $heap->{stdout};
    71 
    72   $kernel->delay("run", 
    73                 $heap->{self}->{interval});
    74 }
    75 
    76 1;

Daten filtern

Wer nutzt nun diese Komponente? Listing nettop bindet in den Zeilen 12 und 18 gleich zwei Instanzen von PoCoRunner ein, einen für netstat -s und einen weiteren für netstat -put. Die Ausgaben landen in den Skalaren $stats_data und $conns_data.

Die Funktion conns_parse in Zeile 160 arbeitet sich durch die netstat-Ausgabe nach Abbildung 1, extrahiert die wichtigen Kolumnen (Lokale IP, Netzwerk-IP, Status, Programm), macht aus dem Tabellenformat einen Array von Arrays, und gibt eine Referenz darauf zurück. stats_parse in Zeile 111 hingegen analysiert die Ausgabe von netstat -s nach Abbildung 2 und legt die Ausgabe in einem Hash von Hashes ab. Aus Zwischenüberschriften (z.B. ``Ip:'') werden so Einträge im übergeordneten Hash und die Beschriftungen der Einzelwerte (z.B. ``incoming packets deliverd'') wandern als Schlüssel in den untergeordneten Hash. Die darunter gespeicherten Werte entsprechen den in netstat-Ausgabe angezeigten Zahlenkolonnen. Insgesamt nutzt stats_parse() drei verschiedene reguläre Ausdrücke, um Zwischenüberschriften sowie zwei verschiedene Ausgabeformate von netstat zu erfassen.

Etabliert nach oben

Von allen Verbindungen sind diejenigen mit einem Status von ``ESTABLISHED'' oft die interessantesten, deswegen sortiert sie die Sort-Routine conn_sort() in Zeile 136 ganz nach oben. Wie in Perl üblich, erhält die in Zeile 79 aufgerufene Sortierfunktion sort die Sort-Routine als Parameter überreicht. Bei jedem Vergleich im Sortiervorgang ruft sort dann conn_sort() auf und besetzt die Spezialvariablen $a und $b mit den Werten der zu sortierenden Einträge. Liefert conn_sort dann -1 zurück, ist $a ``kleiner'' als $b, wandert also nach oben in die Anzeige. Kommt +1 zurück, soll hingegen $b nach oben. Ist keiner der beiden Kandidaten im Status ``ESTABLISHED'', liefert conn_sort den Wert 0 zurück. Somit kommen in diesem Fall beide Kandidaten in der Darstellung irgendwo unterhalb der ESTABLISHED-Sektion zu liegen.

Formatierer als Rückgabe

Die in der GUI tabellenartig angezeigten Werte muss das Skript manchmal zurechtstutzen, damit ein schönes Spaltenlayout entsteht. Die Funktion col_fmt nimmt zwei Parameter entgegen: Eine Referenz auf einen Array aller Zeilen einer Tabellenspalte und eine maximal verfügbare Breite $max_space für diese Spalte. Mit der Funktion max aus dem CPAN-Modul List::Util bestimmt es anschließend die längste Zeile. Ist diese kürzer als $max_space, ist dies die festgesetzte Breite der Spalte. Andernfalls ist $max_space maßgebend. Der als Code-Referenz zurückgegebene Formatierer nimmt anschließend wiederum die Zeilen einer Spalte entgegen und stutzt stutzt sie mit substr() auf die Maximalbreite zurecht. Sind sie zu kurz, füllt er sie mit sprintf() notfalls mit Leerzeichen auf. Jede Spalte erhält so ihren eigenen Formatierer, insgesamt tun vier von ihnen ihren Dienst. Die Werte für die maximale Breite ergeben aufaddiert den Wert 80, da dies oft die Breite des Textfensters ist. Ist eine Spalte deutlich kleiner als der maximal für sie reservierte Platz, gibt sie diesen für andere Spalten auf. So kann es zwar sein, dass die Anzeige bei variierendem Netzwerkverkehr manchmal etwas hin- und herschnackelt, aber die Platzaufteilung ist immer optimal.

Auf den Schirm!

Die graphische Ausgabe übernimmt wie schon in [2] das Modul Curses::UI::POE. Die Anzeige besteht aus drei Teilen, einer blauen Kopfzeile $TOP mit statistischen Daten aus netstat -s, einer Listbox $LBOX mit bestehenden Netzwerkverbindungen (notfalls versteckt sie überschüssige Einträge) und einer ebenfalls blauen Fußzeile $BOTTOM, die nur die Version des Programmes anzeigt.

Der Parameter paddingspaces füllt die blauen Kopf- und Fußzeilen rechts auf, damit die blauen Balken sich über die gesamten Bildschirmbreite erstrecken und nicht mit der tatsächlichen Länge des enthaltenen Textes variieren. Die Methode set_binding() definiert in Zeile 51, dass die Taste 'q' einen Programmabbruch auslöst, denn sie ruft im Ereignisfall einfach eine Funktion auf, die exit 0 ausführt.

Automat kriegt Zustände

Der finite Automat der Darstellungsschicht kennt zwei Zustände: Den Startzustand _start und den Aufwachzustand wake_up, in dem der Automat den Bildschirm mit den neuesten Daten auffrischt. Statt package_states kommt in nettop der Parameter inline_states zum Zuge, denn der Konstruktor der POE-Session weist den Zustandsnamen direkt anonyme Subroutinen zu und verweist nicht implizt auf gleichlautende Funktionsnamen im gleichen Modul. Während wake_up noch läuft, setzt es mit delay() schon einen Event an den Kernel ab, den dieser nach der in $REFRESH_RATE gespeicherten Anzahl von Sekunden ausführt. So entsteht eine Endlosschleife, die unermüdlich im Sekundentakt das Terminal auf den neuesten Stand bringt.

In wake_up holt zunächst der Aufruf von data_refresh() die neuesten Daten der über PoCoRunner.pm laufend aufgerufenen netstat-Prozesse ab, presst sie in übersichtliche Datenstrukturen und legt diese in globalen Variablen ab.

Den dynamisch aufgefrischte Text im Kopfbalken formatiert die Funktion top_text() und liefert ihn an die Methode text() des Kopfbalkenobjekts. Damit das Ganze auch auf dem Bildschirm erscheint, ist anschließend der Aufruf der Methode draw() erforderlich. Ähnliches gilt für die Listbox, deren Einträge haben Werte (-values), die jedoch im vorliegenden Fall nicht interessieren, da der Benutzer keine Listenelemente auswählt. Der Eintrag -labels hingegen definiert, was zu jedem Listenelement im Curses-Fenster angezeigt wird und setzt diese 'Labels' einfach ebenfalls auf die bereits definierten Werte der Listboxeinträge.

Ruckelfreier Reigen

Zeile 52 startet die mainloop der graphischen Oberfläche und damit den POE-Kernel mit allen bis dato eingebunden Komponenten. Der ruckelfreie Reigen beginnt, und jede Sekunde (eingestellt in $REFRESH_RATE) wird die Anzeige mit den neuesten verfügbaren Daten aufgefrischt. Dies muss nicht unbedingt heißen, dass schon neue netstat-Daten vorliegen. Aber da in einer Umgebung, in der immer nur ein Thread aktiv ist, keine Race-Conditions auftreten, ist sichergestellt, dass in den beiden von den POE-Komponenten gefüllten Skalaren $stats_data und $conns_data immer der vollständige Datensatz des letzten erfolgreichen netstat-Aufrufs liegt.

Erweitern könnte man das Skript mit zusätzlichen Tastatureingaben, die den Bildschirm unterteilen und weitere Details zu einem in der Listbox ausgewählten Prozess anzeigen, als dies in der beschränkten Platz, den ein einzeiliger Eintrag bietet, möglich wäre. Und statt netstat lassen sich natürlich auch die Ausgaben völlig anderer Utilities in einem top-ähnlichen dynamisch aufgefrischten Fenster darstellen.

Listing 2: nettop

    001 #!/usr/bin/perl -w
    002 use strict;
    003 use Curses::UI::POE;
    004 use List::Util qw(max);
    005 
    006 my ($STATS, $CONNS);
    007 my $netstat = "netstat";
    008 my $REFRESH_RATE = 1;
    009 
    010 use PoCoRunner;
    011 
    012 PoCoRunner->new(
    013     command  => $netstat,
    014     args     => "-s",
    015     data     => \my $stats_data,
    016     interval => 1,
    017 );
    018 PoCoRunner->new(
    019     command  => $netstat,
    020     args     => "-put",
    021     data     => \my $conns_data,
    022     interval => 1,
    023 );
    024 
    025 my $CUI = Curses::UI::POE->new(
    026   -color_support => 1, 
    027   inline_states  => {
    028     _start => sub {
    029       $poe_kernel->delay('wake_up', 
    030                          $REFRESH_RATE)},
    031     wake_up => \&wake_up_handler,
    032     chld    => sub { waitpid $_[ARG1], 0; },
    033 });
    034 
    035 my $WIN = $CUI->add(qw( win_id Window ));
    036 
    037 my $TOP = $WIN->add(qw( top Label 
    038   -y 0 -width -1 -paddingspaces 1 
    039   -fg white -bg blue
    040   ), -text => top_text());
    041 
    042 my $LBOX = $WIN->add(qw( lb Listbox
    043     -padtop 1 -padbottom 1 -border 1 ),
    044 );
    045 
    046 my $BOTTOM = $WIN->add(qw( bottom Label
    047   -y -1 -width -1 -paddingspaces 1 
    048   -fg white -bg blue
    049   ), -text => "TCP Watcher v1.0"
    050 );
    051 
    052 $CUI->set_binding(sub { exit 0; }, "q");
    053 $poe_kernel->sig("CHLD", "chld");
    054 $CUI->mainloop;
    055 
    056 ###########################################
    057 sub wake_up_handler {
    058 ###########################################
    059         # Re-enable timer
    060   $poe_kernel->delay('wake_up', 
    061                      $REFRESH_RATE);
    062   data_refresh();
    063   $TOP->text(top_text());
    064   $TOP->draw();
    065 
    066   my $state_fmt = col_fmt([map $_->{state}, 
    067                            @$CONNS], 8);
    068   my $prog_fmt  = col_fmt([map $_->{prog}, 
    069                            @$CONNS], 20);
    070   my $rem_fmt  = col_fmt([map $_->{remote}, 
    071                            @$CONNS], 32);
    072   my $loc_fmt  = col_fmt([map $_->{local}, 
    073                            @$CONNS], 20);
    074 
    075   my @lines = map { 
    076          $state_fmt->($_->{state}) . " " .
    077          $prog_fmt->($_->{prog}) . " " .
    078          $rem_fmt->($_->{remote}) . " " .
    079          $loc_fmt->($_->{local}) . " " .
    080          "";
    081       } sort conn_sort @$CONNS;
    082 
    083   $LBOX->{-values} = [@lines];
    084   $LBOX->{-labels} = { map { $_ => $_ } 
    085                            @lines };
    086   
    087   $LBOX->draw(1);
    088 }
    089 
    090 ###########################################
    091 sub top_text {
    092 ###########################################
    093   my $ip  = $STATS->{Ip};
    094   my $tcp = $STATS->{Tcp};
    095 
    096   return sprintf 
    097     "Packets rcvd:%s sent:%s TCPopen " .
    098     "active:%s passive:%s",
    099     $ip->{'total packets received'},
    100     $ip->{'requests sent out'},
    101     $tcp->{'active connections openings'},
    102     $tcp->{'passive connection openings'};
    103 }
    104 
    105 ###########################################
    106 sub data_refresh {
    107 ###########################################
    108     $STATS = stats_parse($stats_data);
    109     $CONNS = conns_parse($conns_data);
    110 }
    111 
    112 ###########################################
    113 sub stats_parse {
    114 ###########################################
    115   my($output) = @_;
    116 
    117   my $section;
    118   my $data = {};
    119   my $key = qr/\w[\w\s]+/;
    120 
    121   for (split /\n/, $output) {
    122     if( /($key):$/ ) {
    123         $section = $1;
    124         next;
    125     } elsif( /($key): (\d+)/ ) {
    126         $data->{$section}->{$1} = $2;
    127     } elsif( /(\d+)\s+($key)/ ) {
    128         $data->{$section}->{$2} = $1;
    129     } else {
    130         die "Cannot parse stats line '$_'";
    131     }
    132   }
    133 
    134   return $data;
    135 }
    136 
    137 ###########################################
    138 sub conn_sort {
    139 ###########################################
    140  return -1 if $a->{state} eq "ESTABLISHED";
    141  return  1 if $b->{state} eq "ESTABLISHED";
    142  return  0;
    143 }
    144 
    145 ###########################################
    146 sub col_fmt {
    147 ###########################################
    148   my($cols, $max_space) = @_;
    149 
    150   my $max_len = max map { 
    151                         length $_ } @$cols;
    152   $max_len = $max_space if 
    153                      $max_len > $max_space;
    154 
    155   return sub {
    156       return sprintf("%${max_len}s", 
    157                substr(shift, 0, $max_len));
    158   };
    159 }
    160 
    161 ###########################################
    162 sub conns_parse {
    163 ###########################################
    164   my($output) = @_;
    165 
    166   my $data = [];
    167 
    168   for (split /\n/, $output) {
    169     my($proto, $rec, $snd, $local, $remote, 
    170        $state, $prog) = split ' ', $_;
    171 
    172     next if $proto ne "tcp";
    173     push @$data,
    174          { local  => $local,
    175            remote => $remote,
    176            state  => $state,
    177            prog   => $prog };
    178   }
    179 
    180   return $data;
    181 }

Infos

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

[2]
``Ich glotz' TV'', Michael Schilli, http://www.linux-magazin.de/heft_abo/ausgaben/2006/08/ich_glotz_tv

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.