Kontaktmann (Linux-Magazin, Dezember 2004)

Wer von außen durch die eigene Firewall ins LAN gelangen will, braucht entweder ein geheimes Loch oder einen kooperativen Agenten auf der Innenseite. Ein Jabber-Client nimmt von innen aktiv Kontakt zum öffentlichen Jabber-Server auf und wartet auf Instruktionen seiner Internet-Buddies in Form von Instant Messages.

Um vom Internet aus Aktionen auf dem lokalen Netzwerk auszulösen, könnte man freilich ein Loch in die hoffentlich vorhandene Firewall bohren und einen lokalen Webserver ins Internet stellen. Dynamisch vergebene IP-Adressen des Internetproviders verfolgen Services wie dyndns.org und erlauben beinah-statischen Zugriff.

Einfacher geht's heute mit einem Agenten oder ``bot'' (wohl von ``Robot''): Ein auf der Innenseite der Firewall laufender Messaging-Client hängt im öffentlichen Jabber-Messaging-Netzwerk und versteht Kommandos per Textnachricht. Er akzeptiert nur Befehle von Clients aus seiner Buddy-List und erlaubt nur vier Aktionen: Die Last auf dem Agenten-Rechner zu bestimmen, die öffentliche Adresse des Routers abzufragen (Kommando: ip), und das Licht im Schlafzimmer in meiner Wohnung in San Francisco ein- und auszuschalten (lamp on|off).

Abbildung 1: Der "Bot" hinter der Firewall führt Befehle aus, die ein Jabber-Client vom Internet sendet.

Das Skript agent.pl nutzt Log::Log4perl und führt in der Datei /tmp/agent.log Buch über die ausgeführten Transaktionen. Zeile 24 erzeugt ein neues Net::Jabber::Client-Objekt, das einen Instant-Message-Client implementiert.

Bevor agent.pl in die Haupt-Eventschleife in Zeile 61 eintritt, müssen ab Zeile 26 noch einige Callbacks für eintretende Ereignisse definiert werden. Der onauth-Handler ab Zeile 48 kommt an die Reihe, wenn der Client sich erfolgreich mit dem in Zeile 17 definierten Nutzernamen beim Server angemeldet hat. Der Handler fragt dann mittels RosterGet() die Buddy-List ab und speichert sie im globalen Hash %ROSTER. Die anschließend abgesetzte Methode Presence() schickt allen Clients im Roster eine presence-Nachricht, die diesen anzeigt, dass agent.pl Online ist. Ab diesem Zeitpunkt zeigt ein gaim-Client mit mikes-agent-sender als eingeloggtem Benutzer mikes-agent-receiver als aktiven Client in seiner Buddy-List an (Abbildung 2).

Abbildung 2: Der "Bot" erscheint in der Buddy-List des Senders.

Der message-Callback ab Zeile 28 kommt zum Zug, wenn der Kommandeur eine Nachricht an agent.pl schickt. Jeder beliebige Client im Jabber-Netzwerk könnte dies tun. Deshalb prüft Zeile 34, ob es sich beim Gesprächspartner auch um jemanden bekannten handelt. Im vorliegenden Fall darf es nur mikes-agent-sender sein, da die Buddy-List des Clients nur ihn enthält (siehe Abschnitt Installation). Andere Anfrager lässt die Funktion abblitzen und kehrt in Zeile 37 zur Hauptschleife zurück.

Das im Message-Text geschickte Steuerungskommando kramt in Zeile 41 die getBody()-Methode hervor und gibt sie an die ab Zeile 71 definierte run_cmd-Funktion weiter.

Listing 1: agent.pl

    001 #!/usr/bin/perl
    002 ###########################################
    003 # agent - Jabber agent behind firewall
    004 # Mike Schilli, 2004 (m@perlmeister.com)
    005 ###########################################
    006 use warnings;
    007 use strict;
    008 
    009 use Net::Jabber qw(Client);
    010 use Log::Log4perl qw(:easy);
    011 use LWP::Simple;
    012 
    013 Log::Log4perl->easy_init(
    014     { level => $DEBUG, 
    015       file => '>>/tmp/agent.log' });
    016 
    017 my $JABBER_USER   = 'mikes-agent-receiver';
    018 my $JABBER_PASSWD = "*****";
    019 my $JABBER_SERVER = "jabber.org";
    020 my $JABBER_PORT   = 5222;
    021 
    022 our %ROSTER;
    023 
    024 my $c = Net::Jabber::Client->new();
    025 
    026 $c->SetCallBacks(
    027 
    028   message => sub {
    029     my $msg = $_[1];
    030 
    031     my $sender = $msg->GetFrom();
    032 
    033     DEBUG "Message '", $msg->GetBody(), 
    034           "' from $sender";
    035 
    036         # Remove /GAIM resource
    037     $sender =~ s#/\w+$##;
    038 
    039     if(! exists 
    040        $ROSTER{$sender}) {
    041         INFO "Denied ($sender not in roster ", join('#', keys %ROSTER), ")";
    042         return;
    043     }
    044 
    045     DEBUG "Running ", $msg->GetBody();
    046     my $rep = run_cmd($msg->GetBody());
    047     chomp $rep;
    048     DEBUG "Result: ", $rep;
    049 
    050     $c->Send($msg->Reply(body => $rep));
    051   },
    052 
    053   onauth => sub {
    054     DEBUG "Auth";
    055     %ROSTER = $c->RosterGet();
    056     $c->PresenceSend();
    057   },
    058 
    059   presence => sub { 
    060     # Ignore all subscription requests
    061   },
    062 );
    063 
    064 DEBUG "Connecting ...";
    065 
    066 $c->Execute(
    067   hostname => $JABBER_SERVER,
    068   username => $JABBER_USER,
    069   password => $JABBER_PASSWD,
    070   resource => 'Script',
    071 );
    072 
    073 $c->Disconnect();
    074 
    075 ###########################################
    076 sub run_cmd {
    077 ###########################################
    078   my($cmd) = @_;
    079 
    080     # Find out external IP
    081   if($cmd eq "ip") {
    082     return LWP::Simple::get(
    083     "http://perlmeister.com/cgi/whatsmyip"
    084     );
    085   }
    086 
    087     # Print Load
    088   if($cmd eq "load") {
    089     return `/usr/bin/uptime`;
    090   }
    091 
    092     # Switch bedroom light on/off
    093   if($cmd =~ /^lamp\s+(on|off)$/) {
    094     my $rc = system("/usr/bin/lamp $1");
    095     return $rc == 0 ? "ok" : 
    096                       "not ok ($rc)";
    097   }
    098 
    099   return "Unknown Command";
    100 }

Execute() ab Zeile 61 nimmt Verbindung mit dem Jabber-Server auf jabber.org auf und meldet mikes-agent-receiver dort an. Die Haupt-Eventschleife erholt sich auch bei temporär abreißender Verbindung und sollte nie enden. Falls sie nach zuvielen Fehlern doch abbricht, räumt Zeile 68 auf und das Programm endet.

Von hinten durch die Brust ins Auge

Die IP-Adresse meines Routers findet der Agent heraus, indem er einen Web-Request auf dei öffentlich zugängliche URL http://perlmeister.com/cgi/whatsmyip abfeuert. Dort hängt nur ein einfaches Skript, das einfach die Client-Adresse des Requests zurückgibt:

    print "Content-Type: text/html\n\n";
    print $ENV{REMOTE_ADDR}, "\n";

agent.pl zieht hierzu LWP::Simple heran, dessen get-Funktion holt in Zeile 77 den Inhalt der Webseite ein, falls die empfangene Textnachricht ip war.

Ähnliches gilt für die aktuelle Rechnerlast: Zeile 84 ruft uptime auf und gibt das Ergebnis an Zeile 41 zurück, wo es mit chomp zurechtgestutzt, geloggt und schließlich in Zeile 45 in einen Message-Body verpackt und per Send an den Chat-Partner zurückgeschickt wird.

Abbildung 3:

Doch wie schaltet der auf einem Linux-Rechner laufende Agent das Schlafzimmerlicht an?

Abbildung 3 zeigt das System im Zusammenspiel. In den USA gibt es (selbstverständlich nur für das dortige elektrische System) die X10-Technologie, die Signale über die elektrischen Leitungen im Haus umher schickt und auch über eine serielle (oder auch USB)-Schnittstelle mit dem Rechner kommunizieren kann. Jede Kontrolleinheit (Abbildung 4) verfügt über einen House-Code (A-K) und einen Unit-Code (1-9), den die Steuereinheit (z. B. Abbildung 5) selektieren muss, damit auch das richtige Licht (und nicht das des Nachbarn) angeht. Das ganze ist nicht teuer: Ein vielteiliges Einsteigerkit mit allerhand Schnickschnack und Fernbedienung kostet auf [4] zwischen 50 und 100 Dollar.

Abbildung 4: Die X10-Kontrolleinheit wartet auf Signale und schaltet den Strom an oder ab.

Abbildung 5: Die X10-Steuereinheit mit seriellem Anschluss schickt Signale vom Rechner über das Stromnetz an die Kontrolleinheit.

Abbildung 6: Die Schlafzimmerlampe wurde übers Internet angeschaltet.

Listing lamp.pl zeigt das kurze Skript, das die Codes über den seriellen Port aussendet und damit die Lampe steuert. Es nutzt lediglich Device::ParallelPort und ControlX10::CM11 vom CPAN, um zunächst die richtige Einheit zu mit House/Unit-Code adressieren und dann nochmals den House-Code gefolgt von J (ein) oder K (aus) zu schicken. Als serieller Port wurde in Zeile 28 /dev/ttyS0 gewählt, da der kleine weiße Kasten aus Abbildung 6 im ersten seriellen Port des Rechners steckt. Ich gebe zu, mein Rechner ist nicht der neueste, aber Linux ist genügsam.

Listing 2: lamp.pl

    01 #!/usr/bin/perl
    02 ###########################################
    03 # lamp -- Switch lamp on and off via x10
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Device::SerialPort;
    10 use ControlX10::CM11;
    11 
    12 my $UNIT_CODE  = "K";
    13 my $HOUSE_CODE = "9";
    14 
    15 my %cmds = (
    16     "on"  => "J",
    17     "off" => "K",
    18 );
    19 
    20 die "usage: $0 [on|off]" if @ARGV != 1 
    21    or $ARGV[0] !~ /^(on|off)$/;
    22 
    23 my $onoff = $1;
    24 
    25 die "You must be root" if $> != 0;
    26 
    27 my $serial = Device::SerialPort->new(
    28     '/dev/ttyS0', undef);
    29 $serial->baudrate(4800);
    30 
    31     # Adress unit
    32 ControlX10::CM11::send($serial, 
    33     $UNIT_CODE . $HOUSE_CODE);
    34 
    35     # Send command
    36 ControlX10::CM11::send($serial, 
    37     $UNIT_CODE . $cmds{$onoff});

Root-Gefängnis

lamp.pl greift auf den seriellen Port des Computers zu und muss deswegen als root laufen. Zeile 25 prüft die effektive User-Id und bricht das Programm ab, falls sie nicht die root gehörenden Nummer 0 entspricht. Da der Jabber-Client unter einem minderpriviligierten Nutzer läuft, definiert Listing lamp.c einen C-Wrapper, der einfach mit

    gcc -o lamp lamp.c

übersetzt wird. Mit gesetztem setuid-Bit kann ein 'normaler' Nutzer dann das Perl-Skript /usr/bin/lamp.pl als root aufrufen:

    $ ls -l /usr/bin/lamp*
    -rwsr-xr-x 1 root root 11548 Oct  2 08:48 lamp
    -rwxr-xr-x 1 root root   742 Oct  2 08:45 lamp.pl

In dieser Konfiguration kann nur root das Skript lamp.pl verändern, aber ein normaler Nutzer lamp.pl unter der effektiven ID root ablaufen lassen.

Listing 3: lamp.c

    1 
    2 main(int argc, char **argv) {
    3     execv("/usr/bin/lamp.pl", argv);
    4 }

Agenten-Sicherheit

Aber zurück zum Agenten:

Damit nicht jeder dahergelaufene Jabber-Client Befehle senden kann, akzeptiert agent.pl nur Nachrichten von Leuten auf seinem Roster. Bei einer eingehenden Nachricht prüft Zeile 34, ob der Sender auf der erlaubten Liste steht und verweigert sonst den Zugriff.

Der ab Zeile 54 definierte Handler für Presence-Requests ist leer und ignoriert alle Anfragen von Clients, die den Agenten auf ihre Buddy-Liste setzen wollen. Das wäre noch nicht so schlimm, doch der von Net::Jabber bereitgestellte Default-Handler akzeptiert solche Anfragen nicht nur, sondern setzt anfragende Clients sofort auf seine eigene Buddy-Liste. Ein leerer presence-Handler verhindert dies.

Installation

Bei der Installation des Agenten muss dessen Buddy-Liste gesetzt werden, am besten mit einem gaim-Client ([5]), der unter Angabe des Passworts einen neuen Account mikes-agent-receiver anlegt, sich dort einloggt und den ebenfalls frischangelegten mikes-agent-sender auf die Liste setzt (Abbildung 7).

Sind beide Accounts online, kommt für mikes-agent-sender der Dialog in Abbildung 8 hoch, auf dem ``Authorize'' zu klicken ist, damit der Server den Vorgang erlaubt. mikes-agent-sender bekommt anschließend noch das Angebot, mikes-agent-receiver in seine Buddy-List aufzunehmen (Abbildung 9), was dieser praktischerweise tut, damit der Sender nur auf den angezeigten Namen klicken muss, um ein Kommando an den Agenten zu senden.

Nach dem anschließenden Logout sollte der Account mikes-agent-receiver nur noch vom Skript agent.pl und nicht mehr von anderen Clients benutzt werden, damit diese nicht versehentlich seine Buddy-Liste umkrempeln, die als Authorisierungs-Mechanismus dient.

Abbildung 7:

Abbildung 8:

Abbildung 9:

Nach dem Start von agent.pl sollte mikes-agent-receiver in der Buddy-Liste von mikes-agent-sender erscheinen (Abbildung 2). Die Logdatei /tmp/agent.log protokolliert die einzelnen Schritte.

Beim Probieren bitte behutsam vorgehen, jeder Implementierungsfehler könnte ein Loch in die Firewall reißen -- also Vorsicht!

Infos

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

[2]
``Jabber Developer's Handbook'', Dana Moore and William Wright, Developer's Library, Sam's Publishing, 2004.

[3]
Vertrieb von X10-Geräten: http://x10.com

[4]
Gaim, der universelle Instant-Message-Client: http://gaim.sourceforge.net

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.