weidner/archives/2012/09/

Iptables aktualisieren

Wenn ich auf einem Rechner die Paketfilterregeln ändern will, verwende ich dafür gern das Programm ferm.

Zum Einen, weil ich die Regeln damit strukturiert schreiben kann, mit einer Syntax, die mir unnötige Schreibarbeit abnimmt und die es erlaubt, die Regeln (für mich) leichter lesbar zu schreiben. Zum Anderen, weil ich bei der Installation der Regeln im Netfilter des Kernels mit ferm -i ... einen Rettungsschirm habe, der mir automatisch den vorletzten Regelsatz wieder herstellt, wenn ich mich mit der letzten Änderung selbst ausgesperrt habe, zum Beispiel, weil ich die SSH-Verbindung unterbrochen habe.

Auf den ALIX-Rechnern habe ich ferm aus Platzmangel oft nicht installiert, da dieses über die Abhängigkeit von einigen Perl-Modulen zumindest bei Debian und Voyage Linux recht viel zusätzliche Softwarepakete benötigt. Den ersten Grund, warum ich ferm gern einsetze tangiert das kaum, denn ich kann die Regeln auf einem anderen Rechner mit ferm --remote ... ausgeben lassen, als Textdatei auf den Zielrechner kopieren und dort mit iptables-restore installieren. Der zweite Punkt ist da schon schwerwiegender, denn iptables-restore kann nur Regeln installieren, merkt sich aber nicht die alten und stellt sie auch nicht von sich aus wieder her.

Also kam ich auf die Idee, selbst ein Skript zu schreiben, dass die neuen Regeln installiert, danach vom Benutzer eine Bestätigung erwartet und beim Ausbleiben der Bestätigung nach einer gewissen Zeit die vor der Änderung gesicherten Regeln wiederherstellt. Das Skript muss also einen Timer starten, während dieser läuft auf eine Eingabe warten und schließlich, je nachdem, ob die Eingabe oder der Timer zuerst kam, die neuen Regeln bestehen lassen oder durch die alten ersetzen. Der Knackpunkt ist, dass Timer und Abfrage der Eingabe gleichzeitig laufen müssen.

Mit der Bourne Shell habe ich dafür keine Lösung gefunden. Da auf Debian Minimalsystemen und auf Voyage Linux ein minimales Perl installiert ist, kann ich dieses verwenden. Mit der Einschränkung, mit sowenig Perl-Modulen wie möglich auszukommen. Das ist keine wirkliche Einschränkung, da Perl alles, was ich brauche bereits in der Sprache mitbringt. Insbesondere die Funktionen alarm, eval, open, close, print sowie für den Abbruch mit Fehlermeldung die. Außerdem noch die Spezialvariable %SIG, mit der ich einen Signalhandler in meinem Skript aktiviere.

Den ersten Hinweis, wie es gemacht wird, liefert die Dokumentation zur Perlfunktion alarm (perldoc -f alarm):

eval {
    local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required
    alarm $timeout;
    $nread = sysread SOCKET, $buffer, $size;
    alarm 0;
};
if ($@) {
    die unless $@ eq "alarm\n";   # propagate unexpected errors
    # timed out
}
else {
    # didn't
}

In dem eval Block setze ich lokal einen Signalhandler für SIGALRM, der die Ausführung mit die abbricht. Dabei sorgt eval dafür, dass nur der folgende Block abgebrochen wird und nicht das ganze Programm. In dem nachfolgenden Test überprüft das Skript, ob überhaupt ein Fehler im eval Block auftrat und ob es der von mir gesetzte Signalhandler verursacht hat. Das kann ich fast unverändert übernehmen. Ich muss lediglich die Zeile mit sysread ersetzen durch die Abfrage, zum Beispiel so:

print "Please type 'yes' to commit\n";
while (<>) { last if /yes/i; }

Ausserdem muss ich die alten Regeln sichern (am besten vor dem eval Block), die neuen Regeln setzen (unmittelbar vor alarm) und bei Ansprechen des Timers die alten Regeln wieder aktivieren (bei dem Kommentar # timed out).

Um die Regeln zu sichern beziehungsweise zu setzen, verwende ich zwei Funktionen, die jeweils einen Dateinamen als Parameter annehmen. Diese schreiben die Ausgabe von iptables-save in die angegebene Datei beziehungsweise den Inhalt der Datei in die Standardeingabe von iptables-restore:

sub backup {
    my $path = shift;
    my $lines = 0;
    if (open my $out, '>', $path) {
        if (open my $in, '-|', $it_save) {
            while (<$in>) {
                print $out $_;
                $lines++;
            }
            close $in;
        }
        else {
            die "backup: can't run '$it_save' to read iptables: $!";
        }
        close $out;
    }
    else {
        die "backup: can't open '$path' for writing: $!";
    }
    die "backup: iptables-save delivered no output" unless $lines;
} # backup()

sub restore {
    my $path = shift;
    if (open my $in, '<', $path) {
        if (open my $out, '|-', $it_restore) {
            while (<$in>) {
                print $out $_;
            }
            close $out;
        }
        else {
            die "restore: can't run '$it_restore' to write iptables: $!";
        }
        close $in;
    }
    else {
        die "restore: can't open '$path' for reading: $!";
    }
} # restore()

Damit habe ich die wesentlichen Bausteine meines Skripts zusammen. Es fehlen noch ein paar Variablen, zum Beispiel die Dauer des Timeouts oder die Namen der Dateien, aus welcher ich die Regeln lesen beziehungsweise in welche ich die alten Regeln sichern will. Damit sieht der Anfang des Skripts nun so aus:

#!/usr/bin/perl
#
use strict;
use warnings;

my $timeout    = 15;
my $it_save    = '/sbin/iptables-save';
my $it_restore = '/sbin/iptables-restore';

my $new_rules = shift || '/tmp/iptables.rules';
my $old_rules = shift || $new_rules . '.bak';

backup($old_rules);

eval {
    local $SIG{ALRM} = sub { die "alarm\n" }; # see 'perldoc -f alarm'!

    restore($new_rules);
    print "Please type 'yes' to commit\n";
    alarm $timeout;
    while (<>) { last if /yes/i; }
    alarm 0;
};
if ($@) {
    die unless $@ eq "alarm\n";

    # timed out
    restore($old_rules);
    print "Timed out. Old rules restored from '$old_rules'\n";
}
else {
    # didn't time out
    print "New iptables rules from '$new_rules' commited\n";
}

Rufe ich das Skript ohne Parameter auf, sucht es die neuen Regeln in /tmp/iptables.rules und sichert die alten in /tmp/iptables.rules.bak. Mit einem Parameter interpretiert es diesen als den Namen der neuen Regeldatei und fügt für die Sicherungsdatei .bak hinten an. Mit zwei Parametern nimmt es den ersten für die Datei mit den neuen Regeln und den zweiten für die Sicherungsdatei.

Das komplette Skript nenne ich iptables-update.pl und installiere es unter /root/bin auf dem ALIX-Rechner.

Posted 2012-09-29
Tags: