Doppelt hält besser (Linux-Magazin, Januar 2008)

Fällt ein Internet-Service-Provider aus, ist tote Hose auf den Computern im Haushalt. Es sei denn, man schaltet auf die Konkurrenz um. Ein Perlskript modifiziert Konfigurationsdateien, erweckt neue Router zum Leben und nordet beteiligte Rechner auf die neuen Routen ein.

In Kalifornien gibt es derzeit zwei große Konkurrenten für Internetanschlüsse in Privathaushalten: Die Telefongesellschaft AT&T mit ihrem DSL und die Kabelfernsehfirma Comcast, die durch das Fernsehkabel ebenfalls Breitband-Internet anbietet. Ein DSL- oder Kabelmodem zapft die Daten aus der Leitung, ein davorgeschalteter Router verbindet ruckzuck sämtliche Rechner des Haushalts mit dem Internet.

Beide Verfahren haben Vor- und Nachteile. So leidet die Kabelverbindung, wenn zuviele Kinder in der Nachbarschaft ``World of Warcraft'' spielen, da die Bandbreite mit der Zahl der Nutzer abnimmt. Der legendäre Werbespot ``Web Hog!'' [2] des DSL-Anbieters Southern Bell illustriert auf humorvolle Weise, wie sich friedfertige Nachbarn einer Kleinstadt über Nacht in Berserker verwandeln, weil sie sich angeblich gegenseitig das Internet lahmlegen. Das DSL der Telefongesellschaft ist zumindest in der langsamen Billigversion preisgünstiger und jeder bekommt die gleiche Bandbreite zugeteilt. Allerdings hängt die Verfügbarkeit und der tatsächliche Durchsatz von der Entfernung zum nächsten Knotenpunkt ab.

Abbildung 1: Haushaltsnetzwerk der Perlmeister-Studios

Keines der beiden Verfahren ist jedoch hundertprozentig zuverlässig, hie und da treten Probleme auf. Hier ein Stromausfall, da ein schlafender Sysadmin, dort bohrt ein Bauarbeiter ein Kabel an, und schon fällt das Internet aus. Unschön, wenn man es gerade dringend braucht. Nachdem aber sowohl DSL als auch Kabel-Internet im Sonderangebot nur etwa 20 Dollar im Monat kosten, habe ich mir einfach beide bestellt. Und wenn ein Anbieter nicht funktioniert, schalte ich kurzerhand auf den anderen um.

Das Skript isp-switch nimmt als Parameter entweder ``cable'' oder ``dsl'' auf der Kommandozeile entgegen und führt die zum Umschalten notwendigen Einzelschritte durch.

Im Skript isp-watch bestimmt Zeile 21 die möglichen Werte dieser Parameter. Bei unbekannten Providernamen bricht das Skript mit pod2usage() ab, das eine Fehlermeldung und die verkürzte Bedienungsanleitung aus dem hinten angehängten POD-Bereich ausgibt. Die 'weiche' Referenz $switch_to->() in Zeile 31 ruft dann dementsprechend die weiter unten definierten Funktionen cable() oder dsl() auf. Damit der Perlinterpreter im strict-Modus bei diesem schmutzigen Trick nicht ausflippt, muss das Skript die Strenge mit no strict 'prefs' etwas abmildern. Das umschließende eval-Konstrukt fängt etwaige Fehler ab und lässt das Skript durchlaufen, auch wenn nicht alle Schritte erfolgreich ablaufen.

Das Skript nutzt Log::Log4perl für Status- und Fehlermeldungen, das in Zeile 48 verwendete Makro ALWAYS kam erst mit Version 1.13 hinzu, deshalb fordert Zeile 9 mindestens diese Version an.

Patch mich!

Mein Linux-Rechner hat eine statische 192.er-IP im lokalen Netzwerk, nutzt also kein DHCP. Unter Fedora konfiguriert er in der Datei /etc/sysconfig/network sein ``Default-Gateway'', also den Router, der Anfragen an alle Rechner des Internets weiterleitet. Abbildung 2 zeigt die Datei im Originalzustand.

Das Gateway ist ein Gerät der Firma Buffalo vom Typ G54, das einmal 30 Dollar gekostet hat und seit Jahren den internen Netzwerkverkehr der Perlmeister-Studios zuverlässig mit dem DSL-Modem und damit dem Internet verbindet.

Kommt wegen eines DSL-Ausfalls jedoch statt des DSL-Anschlusses die Kabelverbindung zum Einsatz, ist ein anderer Kabel-Router das ``Gateway''. Er stammt ebenfalls von der Firma Buffalo stammt und arbeitet unter der IP 192.168.10.98. Wie verändert man nun Konfigurationsdateien und speichert gleichzeitig ihren Originalzustand, so dass sie sich jederzeit wieder in den Ursprungszustand zurückversetzen lassen?

Das CPAN-Modul Config::Patch fügt Patches in Systemkonfigurationsdateien ein und merkt sich den Originaltext als Base-64-kodierte Kommentare. Möchte der User den Patch wieder entfernen, liest es die Base-64-Kodierung, extrahiert den Originaltext und restauriert ihn.

Abbildung 3 zeigt die Datei network, nachdem der Patch eingespielt wurde. Die ab Zeile 60 definierte Funktion gateway_patch nimmt als Parameter die IP-Addresse des Gateways entgegen. Der Konstruktor des Config::Patch-Objektes erhält den Namen der zu patchenden Datei und einen Schlüssel. Diese Zeichenkette ist typischerweise der Projekt- oder Applikationsname und dient dazu, dass das Modul Patches ein und derselben Datei durch verschiedene Schlüssel auseinanderhalten und getrennt bearbeiten kann. Bereits gepatchte Bereiche markiert das Modul als verbotene Zonen und verhindert so wirksam überlappende Patches.

Abbildung 2: Ursprüngliche Version von /etc/sysconfig/network

Abbildung 3: Mit Config::Patch modifizierte Konfigurationsdatei

Listing 1: isp-switch

    001 #!/usr/bin/perl -w
    002 use strict;
    003 use Getopt::Std;
    004 use Pod::Usage;
    005 use Sysadm::Install qw(:all);
    006 use Config::Patch;
    007 use Buffalo::G54;
    008 use X10::Home;
    009 use Log::Log4perl 1.13 qw(:easy);
    010 
    011 my @isps = qw(cable dsl);
    012 my($switch_to) = @ARGV;
    013 
    014 Log::Log4perl->easy_init($INFO);
    015 
    016 if(! defined $switch_to) {
    017     pod2usage(
    018           "Specify what ISP to switch to");
    019 }
    020 
    021 if(! grep { $_ eq $switch_to }  @isps) {
    022     pod2usage("Unknown isp, use ", 
    023               join(", ", @isps));
    024 }
    025 
    026 my $x10 = X10::Home->new();
    027 
    028 my $P = password_read("Router password: ");
    029 
    030 no strict 'refs';
    031 eval { $switch_to->(); };
    032 network_restart();
    033 
    034 ###########################################
    035 sub dsl {
    036 ###########################################
    037   gateway_patch("192.168.10.1");
    038   $x10->send("bridge", "off");
    039   dhcp("on");
    040 }
    041 
    042 ###########################################
    043 sub cable {
    044 ###########################################
    045   gateway_patch("192.168.10.98");
    046   $x10->send("bridge", "on");
    047   dhcp("off");
    048   ALWAYS "Waiting for bridge to start up";
    049   sleep 20;
    050 }
    051 
    052 ###########################################
    053 sub network_restart {
    054 ###########################################
    055   tap "sudo", "/etc/rc.d/init.d/network", 
    056       "restart";
    057 }
    058 
    059 ###########################################
    060 sub gateway_patch {
    061 ###########################################
    062     my($ip) = @_;
    063 
    064     my $patcher = Config::Patch->new(
    065         file => "/etc/sysconfig/network",
    066         key  => "isp-switch",
    067     );
    068 
    069     if($patcher->patched()) {
    070         # patched already? Remove old patch
    071         $patcher->remove();
    072     }
    073 
    074     $patcher->replace(qr(^GATEWAY=.*)m, 
    075                       "GATEWAY=$ip");
    076 }
    077 
    078 ###########################################
    079 sub dhcp {
    080 ###########################################
    081     my($onoff) = @_;
    082 
    083     DEBUG "Setting dhcp to $onoff";
    084 
    085     my $b = Buffalo::G54->new();
    086     DEBUG "Connecting";
    087     $b->connect(password => $P);
    088 
    089     if(defined $onoff) {
    090         INFO "Setting DHCP to $onoff";
    091         $b->dhcp($onoff);
    092     }
    093 
    094     INFO "DHCP is now ", $b->dhcp() ? 
    095                          "on" : "off";
    096 }
    097 
    098 __END__
    099 
    100 =head1 NAME
    101 
    102     isp-switch - Cable or DSL?
    103 
    104 =head1 SYNOPSIS
    105 
    106     isp-switch [dsl|cable]
    107 
    108 =head1 DESCRIPTION
    109 
    110 isp-switch switches between Comcast cable 
    111 and Pacbell DSL.
    112 
    113 =head1 AUTHOR
    114 
    115 2007, Mike Schilli <cpan@perlmeister.com>

Config::Patch kann Patches am Anfang einer Datei oder zwischen zwei Zeilen einfügen, oder sie am Ende anhängen. Die Methode replace ersetzt gar eine mittels eines regulären Ausdrucks spezifizierte Zeile oder einen Zeilenbereich mit der angegebenen Ersatzzeile. Der Aufruf

    $patcher->replace(
      qr(^GATEWAY=.*)m, 
      "GATEWAY=$ip");

sucht nach einer Zeile, die mit GATEWAY= beginnt. Da der Regex über mehrere Zeilen greift, ist der Modifizierer /m relevant, damit das Metazeichen ^ tatsächlich sämtliche Zeilenanfänge erkennt und nicht nur den der ersten Zeile. Die gefundene Gateway-Zeile kodiert Config::Patch zu einem Merker, kommentiert diesen anschließend aus und ersetzt die Zeile mit dem Gateway mit der neuen IP-Adresse. Um die Datei wieder in den Ursprungszustand zurück zu versetzen, genügt der Aufruf $patcher->remove(), denn der Patcher benötigt nur den Dateinamen und den Key, die bereits im Konstruktor des Objekts vorliegen. Die Methode patched() stellt fest, ob eine Datei bereits mit dem angegebenen Schlüssel gepatcht wurde und liefert in diesem Fall einen wahren Wert zurück.

Strom auf Kommando

Der Kabel-Router ist im DSL-Betrieb abgeschaltet. Mittels der in [3] besprochenen X10-Schnittstelle schaltet ihn das CPAN-Modul X10::Home über das Stromnetz ein. Abbildung 4 zeigt den Eintrag in /etc/x10.conf, der die Ansteuerung des Kabelrouter über den Namen cable_router erlaubt. Beide Router sind baugleich und agieren auch als DHCP-Server. Hängt sich ein Computer ins Hausnetz, bekommt er so eine dynamische IP zugewiesen und weiß, an welchen DNS-Server er sich wenden muss, um Hostnamen aufzulösen. Zwei DHCP-Server, die zeitgleich den gleichen Service anbieten, lösen allerdings Chaos aus und schließlich sollen die Rechner im Kabelbetrieb nicht mehr den alten, sondern den neuen DHCP-Server mit dessen Kabel-Gateway nutzen.

Abbildung 4: Der Eintrag C in /etc/x10.conf erlaubt

X10::Home, den Kabelrouter mit Namen anzusprechen.

Deswegen kontaktiert das CPAN-Modul Buffalo::G54 die Konfigurationsoberfläche des Routers und fummelt mittels Screen-Scraping über das Modul WWW::Mechanize intern solange daran herum, bis der DHCP-Server des DSL-Routers abgestellt ist. Allerdings braucht es dazu das Administrationspasswort des Routers welches das Skript isp-switch mit der Funktion password_read() aus dem CPAN-Modul Sysadm::Install beim User abfragt. Im umgekehrten Fall, wenn durch das Kommando dsl wieder zurück ans DSL geht, wird der DHCP-Server des DSL-Routers wieder aktiviert und dem Kabelrouter per X10 rigoros der Strom abgedreht.

Da es eine zeitlang dauert, bis der neue Router vollständig hochgefahren ist, wartet isp-switch mit sleep() zwanzig Sekunden, bevor es weitere Aktionen einleitet.

Restart

Rechner mit dynamischen Adressen holen sich nach einem Reboot oder einem Restart des Netzwerksystems eine neue IP-Addresse vom neuen DHCP-Server. Die Linux-Box mit der statischen IP muss ebenfalls ihr Netzwerk-Initialisierungsskript neu starten, damit Requests an das neue und nicht mehr an das alte Gateway geschickt werden.

Abbildung 5: Ein Eintrag in /etc/sudoers erlaubt dem User mschilli, das Netzwerk rauf- und wieder runter zu fahren.

Hierzu lässt isp-switch das Linux-Startup-Skript /etc/rc.d/init.d/network mit dem Parameter ``restart'' laufen. Hierzu sind Root-Rechte erforderlich, aber ein Eintrag mit NOPASSWD in /etc/sudoers (Abbildung 5) für das Skript erlaubt es auch dem nichtpriviligierten User mschilli, das Netzwerk neu zu initialisieren. Die Funktion tap aus dem unerschöpflichen Fundus des CPAN-Moduls Sysadm::Install führt das Kommando aus und schluckt dessen Ausgabe.

Mit diesen drei Maßnahmen schaltet isp-switch zwischen den beiden Internetanbietern hin und her. Der Internetanschluss wird zwar so auf einmal doppelt so teuer, ist aber auch doppelt so zuverlässig. Als Alternative könnte man einen dritten Router einsetzen, der als Gateway arbeitet und Requests wahlweise an den Kabel- oder den DSL-Router weiterleitet. Er könnte auf einem Linux-PC oder einem ebenfalls unter FreeWRT laufenden Linksys-Router des Typs WRT54GL.

Wer möchte, kann noch einen Nagios-Plugin schreiben, der regelmäßig die Internetverbindung prüft und bei Problemen automatisch auf die Konkurrenz umschaltet. Hoffentlich ohne dass der Enduser das überhaupt mitbekommt.

Infos

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

[2]
Der Werbeclip ``Web Hog'' von Southwestern Bell, der Kabelkunden auf DSL umzulenken versucht: http://www.youtube.com/watch?v=ubc7zFSyEbg

[3]
Michael Schilli, ``Heimschaltwarte'', Linux-Magazin, April 2007, http://www.linux-magazin.de/heft_abo/ausgaben/2007/04/heimschaltwarte

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.