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