Tunnelblick (Linux-Magazin, Juli 2010)

Ein selbstgebauter Mailserver bohrt einen SSH-Tunnel zu einem SMTP-Server, falls lokale Clients versuchen, Mails zu senden.

Das Hin- und Herschaufeln von rohen Datenpaketen erledigt mein Internetprovider AT&T normalerweise zufriedenstellend. Fällt allerdings etwas aus, sieht man sich an der Hotline mit textablesenden Nichtswissern konfrontiert, die unter Umgegung elementarster logischer Prinzipien die Schuld auf den Anwender zu schieben versuchen, anstatt in dem Höllenladen ebenfalls arbeitenden geschulten Systemadministratoren Bescheid zu stoßen, die offensichtlich auf AT&T-Seite verursachten Serverprobleme zu beheben. Als ich einmal anrief, um die Trägheit des DNS-Servers zu bemängeln, fragte mich der freundliche Herr am anderen Ende der Leitung doch tatsächlich, ob mein DSL-Modem auf dem Boden oder im Regal stünde.

Kein Glück im Spam-Zeitalter

Nun scheint der mir von AT&T zugewiesene SMTP-Server fast alle an ihn gesendeten Emails wegzuwerfen, und einen erneuten Anruf wollte ich mir ersparen. Viel Email sendet mein Desktop nicht, aber wenn, dann sollte sie schon rausgehen: Fällt zum Beispiel der Strom aus, springt das UPS an, was Nagios bemerkt und schnell noch eine Email schickt, bevor dem System endgültig der Saft abgedreht wird.

Statt dem AT&T-eigenen SMTP-Server könnte ich natürlich den SMTP-Server meines relativ zuverlässigen Hosting-Providers nutzen, doch nimmt der im Spam-Zeitalter natürlich keine Mails von wildfremden IPs an. Aber da der Hoster einen SSH-Zugang anbietet, könnte man mit

  ssh -L 1025:localhost:25 mschilli@host.provider.com

zum Beispiel einen Tunnel vom lokalen Port 1025 zum SMTP-Port 25 des Hosters bohren. Ein lokaler SMTP-Client ist auch schnell auf 1025 eingestellt, und für den Hoster sähe es dann so aus, als käme der Request vom gemieteten Shared-Host-Webserver und nicht vom Desktop daheim.

Dynamisch bohren

Da ein Billig-Hoster eventuell nicht möchte, dass irgendwelche Geizhälse Tag und Nacht ssh-Tunnel offenhalten, ohne auf ihren gemieteten Webseiten herumzutippen, bietet sich eine dynamische Lösung an: Ein in Perl selbstgebauter Dämon minimail lauscht auf dem lokalen SMTP-Port 25 auf Anfragen lokaler Mail-Clients, die nichts von der dahinter steckenden Komplexität ahnen.

Der Dämon nimmt den Request entgegen, baut den Tunnel zum Hoster auf und trödelt anschließend solange herum, bis die Verbindung steht. Für den lokalen Mailclient sieht dass so aus, als hätte er nur einen etwas langsameren Mailserver vor sich. Der Dämon schaufelt dann die Request-Zeilen des Clients (lokaler Port 25) auf den lokalen Port 1025 weiter, also den Eingang des Tunnels, auf dessen anderem Ende Port 25 des Hosters liegt. Aus dem Tunnel zurückkommende Protokollzeilen gibt der Dämon weiter an den lokalen Client, für den es aussieht, als spräche er mit lokalen einem SMTP-Server.

Kommen mehrere Requests zum Email-Senden schnell hintereinander, wäre ein Ab- und wieder Aufbauen des Tunnels wenig effektiv, und so lässt der Dämon den Tunnel nach dem Abkoppeln des letzten Clients noch 10 Sekunden bestehen, in der Hoffnung, den nächsten Client schneller bedienen zu können. Nach etwa 10 Sekunden Inaktivität fährt der Dämon den Tunnel dann herunter. Damit das Ganze in den Hosterlogs menschlich aussieht, addiert das Skript eine Zufallszahl zwischen 0 und 25.

Listing 1: minimail

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Sysadm::Install qw(:all);
    04 use App::Daemon qw(daemonize);
    05 use Log::Log4perl qw(:easy);
    06 
    07 BEGIN {
    08   sudo_me();
    09   $App::Daemon::as_user = "root";
    10   $App::Daemon::logfile = 
    11                      "/var/log/minimail.log";
    12   $App::Daemon::loglevel = $INFO;
    13   daemonize();
    14 };
    15 
    16 use POE;
    17 use PoCoForwarder;
    18 use PoCoTimedProcess;
    19 
    20 my $port_from      = 25;
    21 my $port_to        = 25;
    22 my $tunnel_port    = 1025;
    23 my $real_smtp_host = 'host.provider.com';
    24 
    25 my $process = PoCoTimedProcess->new(
    26   heartbeat => 10,
    27   timeout   => int(rand(25)) + 10,
    28   command   => ["ssh", '-N', '-L', 
    29     "$tunnel_port:localhost:$port_to", 
    30     $real_smtp_host],
    31 );
    32 
    33 my $forwarder = PoCoForwarder->new(
    34     port_from => $port_from,
    35     port_to   => $tunnel_port,
    36     port_bound => sub {
    37        INFO "Dropping privileges";
    38        $< = $> = getpwnam($ENV{SUDO_USER});
    39     },
    40     client_connect => sub {
    41       $process->launch();
    42     },
    43 );
    44 
    45 $process->spawn();
    46 $poe_kernel->run();

Root oder nicht Root

Damit der Dämon den SMTP-Port 25 mit bind() an sich reißen kann, muss er unter root laufen, und um das damit verbundene Sicherheitsrisiko zu mindern, gibt er diese Privilegien später wieder ab. Ein mit sudo also root gestartetes Programm führt in der Environment-Variablen SUDO_USER den Nutzer, der den Sudo-Befehl ausführte, und auf diesen unpriviligierten User stutzt das Skript später seine Rechte zurück. Der Befehl sudo_me() in Zeile 8 aus dem Modul Sysadm::Install vom CPAN prüft, ob das Skript unter root läuft und startet sich selbst noch einmal mit sudo, falls dies nicht der Fall war.

Das Dämonen-Werkzeug App::Daemon vom CPAN exportiert die Funktion daemonize(), die dafür sorgt, dass der Dämon die Befehle minimail start|stop kennt und sang- und klanglos im Hintergrund arbeitet, sobald er die Startsequenz durchlaufen hat, und nur die Logdatei zeigt an, was der Dämon gerade treibt. Das Log4perl-Logfile setzt -l (oder die Variable App::Daemon::logfile) und wird der Dämon mit -X im Vordergrund gestartet, erscheint die Logausgabe auf STDERR.

Der BEGIN-Block ab Zeile 7 sorgt dafür, dass das POE Modul (Zeile 16) erst nach dem Dämonisieren des Prozesses (Zeile 13) geladen wird. Das ist wichtig, denn so wurde mir von einer hilfreichen Seele auf der POE-Mailingliste erklärt, denn sonst schießt POE abgefeuerte Kindprozesse nicht richtig ab.

Da App::Daemon selbst ein Feature zum Abgeben der Root-Privilegien bietet, stellt Zeile 9 ``root'' in der Variablen $as_user des Moduls ein und überlässt damit den Sicherheits-Umschwung dem Skript, das ihn direkt nach dem Binden des Dämons an Port 25 ab Zeile 37 ausführt.

POE zur Hilfe

Ein selbstgeschriebener Netzwerkdämon kostet normalerweise beträchtlich Schweiß und Tränen, doch zum Glück bietet das CPAN einige POE-Komponenten, die man nur wie Legosteine aneinanderstecken muss. So formt minimail aus den Komponenten POE::Component::Client::TCP und ::Server::TCP den Port-Forwarder PoCoForwarder. Er hängt sich an den lokalen $port_from und leitet alles dort ankommende an den Tunnelport $tunnel_port weiter und umgekehrt. Das ist keine triviale Angelegenheit, denn am lokalen Port können mehrere Mailclients gleichzeitig hängen und müssen parallel bedient werden.

Die zweite Komponente, PoCoTimedProcess, fährt einen Prozess wie den Tunnel mit der Methode launch() für eine vorbestimmte Zeit hoch oder verlängert seine Lebenszeit, falls er schon oben ist. Jedes Mal, wenn der Forwarder einen neu angedockten Client feststellt, ruft er im client_connect()-Callback hierzu die Methode launch() auf. Diese schnappt sich das ssh-Kommando

    ssh -N -L 1025:localhost:25 host.provider.com

in Zeile 28 und verbindet sich damit mit dem Host host.provider.com über das verschlüsselte ssh-Protokoll, loggt sich dort ein, spannt aber dank der Option -N keine interaktive Shell auf sondern hängt nur so da und leitet die Datenströme vor- und zurück. Der Port 1025 ist das Desktop-seitige Ende des Tunnels, der ``localhost'' im ssh-Befehl bezieht sich allerdings auf host.provider.com, da ssh sich jetzt dort befindet. Der dem Doppelpunkt folgende Port 25 ist der SMTP-Port des Hosters. Falls der Username auf dem Hosting-Rechner nicht derselbe ist wie auf dem Desktop, darf ihn der Aufruf mit mschilli@host.provider.com voranstellen, damit ssh Bescheid weiß.

Komponentenkleber

Was nun geht hinter den Kulissen in den beiden POE-Komponenten vor? Abbildung 1 zeigt das Diagramm mit den Server- und Clientkomponenten, sowie den jeweils genutzten Portnummern. Der im Port-Forwarder auf Port 25 lauschende TCP-Server spannt pro Client jeweils eine TCP-Client-Komponente auf, um diesen mit dem Tunnel zu verbinden.

Als Parameter erwartet die Klasse den Port ``port_from'' (auf den der Server lauscht), ``port_to'' (der Tunnelport), sowie zwei Callbackroutinen. Die unter port_bound abgelegte Subroutinenreferenz springt die Komponente an, sobald der Server sich mit Port 25 verbunden hat und damit keine Root-Rechte mehr benötigt. Beim Aufgeben der Root-Rechte ist darauf zu achten, dass dies in der richtigen Reihenfolge für effektive und reale User-ID geschieht, sonst kann der Dämon hinterher die Root-Rechte wieder herstellen ([2]). Wären hier mehrere Threads gleichzeitig zugange, müsste PoCoTimedProcess intern aufpassen, dass keine Race-Condition den Tunnel zweimal startet, aber in der von POE bereitgestellten Ein-Prozess-ein-Thread-Umgebung genügt ein einfacher Variablen-Check ohne jegliches Locking. Robust, einfach hinzuschreiben und später leicht zu begreifen!

Der zweite Callback des Forwarders, client_connect, wird jedesmal angesprungen, wenn ein Mailclient an Port 25 andockt. Die im Callback ausgeführte Methode launch() der Komponente PoCoTimedProcess fährt daraufhin den Tunnel hoch, falls dieser noch nicht besteht. Intern stellt PoCoForwarder jeder Client-Verbindung eine eigene POE-Komponente vom Typ PoCo::Client::TCP zur Seite, die mit dem Tunnelport Kontakt aufnimmt. Während also PoCo::Server::TCP beliebig viele Clients betreut, nutzt es für jede Client-Verbindung eine separate PoCo::Client::TCP-Komponente. Die Ursache hierfür liegt im Design der letzteren, da sie explizit auf einen Client ausgelegt ist, während der Server naturgemäß mit vielen Clients quasi-gleichzeitig kommuniziert.

Abbildung 1: Der Mail-Client kommuniziert mit Port 25 des Forwarders, dessen Client wiederum mit dem Tunnel kommuniziert.

Listing 2: PoCoForwarder.pm

    01 package PoCoForwarder;
    02 use strict;
    03 use Log::Log4perl qw(:easy);
    04 use POE::Component::Server::TCP;
    05 use POE::Component::Client::TCP;
    06 use POE;
    07 
    08 ###########################################
    09 sub new {
    10 ###########################################
    11   my($class, %options) = @_;
    12 
    13   my $self = { %options };
    14 
    15   my $server_session =
    16   POE::Component::Server::TCP->new(
    17     ClientArgs     => [$self],
    18     Port           => $self->{port_from},
    19     ClientConnected  => \&client_connect,
    20     ClientInput      => \&client_request,
    21     Started          => sub {
    22       $self->{port_bound}->(@_) if
    23         defined $self->{port_bound};
    24     },
    25   );
    26 
    27   return bless $self, $class;
    28 }
    29 
    30 ###########################################
    31 sub client_connect {
    32 ###########################################
    33   my ($kernel, $heap, $session, $self) = 
    34           @_[ KERNEL, HEAP, SESSION, ARG0];
    35 
    36   $self->{client_connect}->(@_) if
    37     defined $self->{client_connect};
    38 
    39   my $client_session = 
    40   POE::Component::Client::TCP->new(
    41     RemoteAddress => "localhost",
    42     RemotePort    => $self->{port_to},
    43     ServerInput   => sub {
    44       my $input = $_[ARG0];
    45         # $heap is the tcpserver's (!) heap
    46       $heap->{client}->put( $_[ARG0] );
    47     },
    48     Connected => sub { 
    49         $_[HEAP]->{connected} = 1; },
    50     Disconnected => sub {
    51      $kernel->post( $session, "shutdown" );
    52     },
    53     ConnectError => sub {
    54      $_[HEAP]->{connected} = 0;
    55      $kernel->delay('reconnect', 1);
    56     },
    57     ServerError => sub {
    58      ERROR $_[ARG0] if $_[ARG1];
    59      $kernel->post( $session, "shutdown" );
    60     },
    61   );
    62 
    63   $heap->{client_heap} = $kernel->
    64     ID_id_to_session( $client_session )->
    65     get_heap();
    66 }
    67 
    68 ###########################################
    69 sub client_request {
    70 ###########################################
    71   my ($kernel, $heap, $request) = 
    72      @_[ KERNEL, HEAP, ARG0 ];
    73 
    74   return if  # tunnel not up yet, discard
    75     ! $heap->{client_heap}->{connected};
    76 
    77   $heap->{client_heap}->
    78          {server}->put( $request );
    79 }
    80 
    81 1;

Closures: Verwirrend und Schön

Zeile 22 in PoCoForwarder.pm zeigt, wie die Komponente den Callback port_bound implementiert. Der in Zeile 16 erzeugte POE-TCP-Server springt nach einem erfolgreichen Start den Zustand Started an, und dort kramt PoCoForwarder die von minimail definierte Subroutinenreferenz aus dem Objekthash $self hervor und springt sie an. Der in minimail definierte Callback-Code tut den Rest. Zu beachten ist, dass $self sich gar nicht im Scope der dem Zustand Started zugewiesenen Handlers befindet. Vielmehr stammt sie aus dem Konstruktor new() der Klasse PoCoForwarder, aber die Subroutine verwandelt sich dadurch in eine sogenannte Closure, die die lexikalische Variable $self umschließt und die darum auch nach dem Verlassen des Konstruktor-Scopes (aber nur innerhalb des Callbacks) gültig bleibt.

Andererseits sorgt der Parameter ClientArgs in Zeile 17 dafür, dass die Server-Komponente den Objekthash $self auch als Argument ARG0 mitliefert, falls sie die Callback-Funktion client_connect anspringt. Dort ruft Zeile 36 den von minimail gesetzten Callback client_connect auf, der den Tunnel hochfährt. Nun ergibt sich allerdings ein Timing-Problem, denn es ist schwer vorherzusagen, wann der Tunnel steht, und deswegen versucht der Client eventuell, sich mit einem Port zu verbinden, auf dem niemand lauscht. Doch das ist kein Problem, denn in diesem Fall springt der TCP-Client den Zustand ConnectError ab Zeile 53 an, der mittels der POE-Kernelfunktion delay() einfach einen ``reconnect''-Event für eine Sekunde später in POEs Terminkalender einträgt. Dieses Spielchen setzt sich eventuell ein paar Runden fort, doch irgendwann steht der Tunnel, der TCP-Client verbindet sich erfolgreich mit dem nun betriebenen Port und springt deshalb den Zustand Connected ab Zeile 48 an.

Falls minimail ein Kommando sendet, springt der TCP-Server den Zustand client_request und damit den gleichnamigen Handler ab Zeile 69 an. Dieser prüft, ob der Tunnel bereits hochgefahren wurde, und verwirft den Client-Text, falls die Verbindung noch nicht steht. Im SMTP-Protokoll meldet sich der Server zuerst mit einer Grußmeldung, und deswegen wird ein wohlerzogener Client nicht losplappern, bevor der Tunnel steht. Bei anderen Protokollen (wie z.B. http) wäre dies anders, und in diesem Fall müsste die Forwarder-Komponenente die Client-Kommandos puffern bis die Verbindung steht, und sie dann im Client-Auftrag gebündelt an den Server weiterleiten.

Stimmen aus dem Tunnel

Ist der Tunnel hingegen betriebsbereit, wurde vorher im Zustand Connected die Heap-Variable connected auf 1 gesetzt. Um die Nachricht an den Tunnel weiterzuleiten, kramt Zeile 77 aus dem in client_heap zwischengespeicherten Heap des TCP-Clients die Tunnelreferenz hervor und schickt ihr mit put() den eingegangenen Text. Zu beachten ist hier, dass client_request ein Callback der Server-Session ist, die vom Client, der in einer anderen Session tobt, und seinem Heap, nichts weiß. Die Heap-Variable client_heap in der Server-Session löst das Problem.

Kommen Nachrichten aus dem Tunnel zurück, tritt der TCP-Client in den Zustand ServerInput ab Zeile 43, der den Text mit der im Heap gespeicherten client-Referenz und der Methode put() an minimail zurückgibt.

Falls minimail sich vom TCP-Server abkoppelt, springt dieser in den Zustand Disconnected, worauf der Handler dort einen ``shutdown''-Event an die laufende Session schickt, was die Client-Server-Verbindung trennt.

Prozesse auf Zeit

Die Komponente PoCoTimedProcess.pm hingegen kümmert sich ausschließlich um den Auf- und Abbau des Tunnels. Startet minimail in Zeile 45 mit spawn die zugehörige POE-Session, springt es zunächst den ab Zeile 33 in PoCoTimedProcess.pm definierten _start-Handler an. Dieser extrahiert (wiederum durch eine Closure) alle wichtigen Parameter wie heartbeat (Check-Frequenz für den Timeout), timeout (Anzahl der Sekunden bis zum Tunnelabbruch) und command (das ssh-Kommando zum Aufbau des Tunnels) aus dem Objekt-Hash self und legt sie im Session-eigenen Heap ab. Anschließend setzt er zwei Events zur späteren Abbarbeitung durch den POE-Kernel ab, ``keep-alive'' und ``heartbeat''. Ersterer setzt die Heap-Variable countdown auf das Maximum zurück: die in timeout liegende Maximalzahl der Sekunden, die ein Tunnel offen bleibt. Den Zustand ``heartbeat'' hingegen ruft POE wegen der delay-Methode in Zeile 65 regelmäßig auf, immer wenn die in der Heap-Variablen heartbeat liegende Sekundenzahl verstrichen ist.

Der Tunnel ist zu diesem Zeitpunkt noch geschlossen, doch sobald die launch()-Methode den Event up absetzt und POE den zugehörigen Handler up (Zeile 84) aktiviert, startet ein POE-Rädchen vom Typ POE::Wheel::Run (Zeile 97) den ssh-Prozess. Die in den Zeilen 109 und 110 definierten Handler für die Unix-Signale TERM und INT sorgen dafür, dass ein abgeschossener minimail-Prozess auch einen eventuell geöffneten Tunnel mit einreißt.

Erreicht der Tunnel seine maximale Lebensdauer, setzt Zeile 77 den Event ``down'' ab und der gleichnamige Handler ab Zeile 117 schickt dem ssh-Prozess ein kill-Signal. Damit andere Handler darüber Bescheid wissen, dass es den Tunnel nun nicht mehr gibt, setzt down() die Variable is_up auf 0. Das auslösende Signal ist nun fertig bearbeitet, und würde Zeile 129 den POE-Kernel nicht über sig_handled() darüber informieren, zöge dieser die Notbremse und beendete den Dämon.

Damit aus dem abgeschossenen Prozess kein Zombie entsteht, der sich mit Artgenossen zusammenschließt und im Lauf der Zeit den Rechner lahmlegt, definiert Zeile 108 einen sig_child-Handler, der den sterbenden Prozess abfängt und den ab Zeile 44 definierten Handler sig_child anspringt. Damit verpasst POE dem abnippelnden Tunnel mit waitpid() die letzte Ölung und bewahrt ihn damit vor dem ewig lodernden Höllenfeuer. Der Handler löscht die letzte Referenz auf das POE::Wheel, worauf POE den Kernel ordnungsgemäß zusammenfaltet.

Listing 3: PoCoTimedProcess.pm

    001 package PoCoTimedProcess;
    002 use strict;
    003 use warnings;
    004 use POE;
    005 use POE::Wheel::Run;
    006 use Log::Log4perl qw(:easy);
    007 
    008 ###########################################
    009 sub new {
    010 ###########################################
    011   my($class, %options) = @_;
    012 
    013   my $self = { %options };
    014   bless $self, $class;
    015 }
    016 
    017 ###########################################
    018 sub launch {
    019 ###########################################
    020   my($self) = @_;
    021 
    022   $poe_kernel->post($self->{session},'up');
    023 }
    024 
    025 ###########################################
    026 sub spawn {
    027 ###########################################
    028   my($self) = @_;
    029 
    030   $self->{session} =
    031   POE::Session->create(
    032     inline_states => {
    033       _start => sub {
    034         my($h,$kernel) = @_[HEAP, KERNEL];
    035 
    036         $h->{is_up} = 0;
    037         $h->{command} = $self->{command};
    038         $h->{timeout} = $self->{timeout};
    039         $h->{heartbeat} = 
    040                        $self->{heartbeat};
    041         $kernel->yield('keep_alive');
    042         $kernel->yield('heartbeat');
    043       },
    044       sig_child => sub {
    045           delete $_[HEAP]->{wheel};
    046       },
    047       heartbeat => \&heartbeat,
    048       up   => \&up,
    049       down => \&down,
    050       keep_alive => sub {
    051         $_[HEAP]->{countdown} = 
    052           $_[HEAP]->{timeout};
    053       },
    054       closing => sub {
    055         $_[HEAP]->{is_up} = 0;
    056       },
    057    })->ID();
    058 }
    059 
    060 ###########################################
    061 sub heartbeat {
    062 ###########################################
    063   my($kernel, $heap) = @_[KERNEL, HEAP];
    064 
    065   $kernel->delay( "heartbeat", 
    066                   $heap->{heartbeat});
    067 
    068   if( $heap->{is_up} ) {
    069     INFO "Process is up for another ", 
    070       $heap->{countdown}, " seconds";
    071 
    072     $heap->{countdown} -= 
    073       $heap->{heartbeat};
    074 
    075     if($heap->{countdown} <= 0) {
    076         INFO "Time's up. Shutting down";
    077         $kernel->yield("down");
    078         return;
    079     }
    080   }
    081 }
    082 
    083 ###########################################
    084 sub up {
    085 ###########################################
    086   my ($heap, $kernel) = @_[ HEAP, KERNEL ];
    087 
    088   if($heap->{is_up}) {
    089       INFO "Is already up";
    090       $_[KERNEL]->yield('keep_alive');
    091       return 1;
    092   }
    093 
    094   my($prog, @args) = @{ $heap->{command} };
    095 
    096   $heap->{wheel} =
    097     POE::Wheel::Run->new(
    098       Program     => $prog,
    099       ProgramArgs => [@args],
    100       CloseEvent  => "closing",
    101       ErrorEvent  => "closing",
    102       StderrEvent => "ignore",
    103   ); 
    104 
    105   my $pid = $heap->{wheel}->PID();
    106   INFO "Started process $pid";
    107 
    108   $kernel->sig_child($pid, "sig_child");
    109   $kernel->sig( "INT"  => "down" );
    110   $kernel->sig( "TERM" => "down" );
    111 
    112   $_[KERNEL]->yield('keep_alive');
    113   $heap->{is_up} = 1;
    114 }
    115 
    116 ###########################################
    117 sub down {
    118 ###########################################
    119   my ($heap, $kernel) = @_[ HEAP, KERNEL ];
    120 
    121   if(! $heap->{is_up}) {
    122       INFO "Process already down";
    123       return 1;
    124   }
    125 
    126   INFO "Killing pid ", $heap->{wheel}->PID;
    127   $heap->{wheel}->kill();
    128   $heap->{is_up} = 0;
    129   $kernel->sig_handled();
    130 }
    131 
    132 1;

Schlüssel statt Passwort

Da ein Dämon sich nicht mit einem interaktiv eingegebenen Passwort identifizieren kann, erfordert das obige ssh-Kommando, dass der User vorher mit

    ssh-keygen -t rsa

ein Schlüsselpaar erzeugt, dessen Daten typischerweise in den Dateien id_rsa (private key) und id_rsa.pub (public key) im Directory .ssh unter dem Home-Verzeichnis des ausführenden Users landen. Damit der Hoster den Dämon hereinlässt, muss der User den mit der Option ``no passphrase'' erzeugten Public Key auf den Host-Server spielen. Hierzu kopiert er einfach den lokalen Inhalt der Datei id_rsa.pub in die Datei .ssh/authorized_keys auf dem Hosting-Server. Das von Hand eingebene ssh-Kommando oben (ohne die Option -N) sollte sich nun ohne Rückfragen auf dem Hostingserver einloggen.

Testlauf mit Telnet

Abbildung 2: Bei der ersten Anfrage muß Minimail erst den Tunnel öffnen ...

Ob der mit sudo minimail start gestartete Mailserver tatsächlich funktioniert, findet der Telnet-Aufruf aus Abbildung 2 auf localhost und Port 25 heraus. Ist der Tunnel des Dämons ordnungsgemäß heruntergefahren, verzögert Minimail die Antwort etwa ein, zwei Sekunden, bis der Mailserver des Providers antwortet und stellt dann durch (Abbildung 3). Wer ein paar Sätze SMTP spricht, kann so gleich (selbstverständlich nur zu Testzwecken) allerhand Schabernack treiben (Abbildung 4). In der Logdatei /var/log/minimail.log protokolliert der Dämon mit, was gerade geschieht (Abbildung 5). Aus Datenschutzgründen unterbleibt das Mitschneiden der Mailheader oder -texte.

Abbildung 3: ... und nach ein bis zwei Sekunden antwortet der SMTP-Server am anderen Ende des Tunnels ...

Das telnet-Kommando bleibt übrigens stecken, falls der Server die Verbindung nicht irgendwann abbricht. Die Kombination CTRL-] hilft aus der Breduille, in dem sie telnet in eine Shell fallen lässt, aus der dann das Kommando q herausführt.

Abbildung 4: ... mit dem der Client dann ganz normal SMTP-Kommandos austauscht. Moment mal: Versucht hier etwa ein Schlingel, sich als Steve Jobs auszugeben?

Abbildung 5: In der Logdatei protokolliert der Dämon die wichtigsten Ergeignisse mit.

Stromausfall kommt bestimmt

Um den Minimailserver schon beim Booten hochzufahren (der nächste Stromausfall kommt bestimmt), ist die folgende Zeile

  SUDO_USER=mschilli /path/to/minimail

unter Ubuntu in eine eventuell neu anzulegende Datei /etc/init.d/minimail einzuspeisen, diese mit chmod +x ausführbar zu machen und anschließend

    sudo update-rc.d minimail defaults 80

aufzurufen, damit Ubuntu das Skript in den Bootvorgang einhängt. Kommt dann der Strom wieder, fährt der Mailserver automatisch hoch und ein vorher aufgesetzter Nagios-Plugin meldet dem erfreuten Besitzer per Email die überstandene Katastrophe.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/07/Perl

[2]

Privilegien abgeben, aber richtig: http://perlmonks.com/?node_id=833950

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.