Traffic-Shaping mit Linux

03.03.2006 von STEFAN RUBNER 
Mit Linux lässt sich nicht nur ein sicherer Firewall-Router realisieren. Über die integrierten Funktionen zum Traffic-Shaping beseitigt Linux auch ruckelnde Internet-Verbindungen und verhilft wichtigem Datenverkehr zu mehr Durchsatz.

Gerade Besitzer eines DSL-Anschlusses können oft ein Lied davon singen: Beim Download großer Dateien oder auch beim Versand einer E-Mail mit umfangreichem Attachment geht auf der Leitung zum Internet gar nichts mehr. Schuld daran ist, dass die Daten normalerweise sequentiell – also hintereinander – auf die Reise geschickt werden.

Bevor also beispielsweise die nächste Instant Message über das Kabel rutscht, muss der Rechner zunächst den zuvor angestoßenen E-Mail-Versand abwickeln oder den Download beenden, dessen Antwortpakete die Leitung verstopfen.

Ab in die Warteschlange

Zum Glück lässt sich dieses Problem unter Linux recht leicht in den Griff bekommen. Anstatt eines einzigen Puffers für alle ausgehenden Daten erlaubt dieses nämlich das Aufteilen der Datenströme auf mehrere Warteschlangen, in diesem Fall etwas salopp Bit Buckets genannt.

Diese frei übersetzt als Dateneimer bezeichneten Queues geben die in ihnen zwischengespeicherten Daten mit unterschiedlicher Priorität an die Netzwerkschnittstelle und damit an das Internet weiter. Da jeder Eimer einmal an die Reihe kommt, erfolgen alle Übertragungen flüssiger, eine von einem umfangreichen Datentransfer blockierte Leitung gehört damit der Vergangenheit an.

So weit zumindest die Theorie. In der Praxis ist oft noch eine kleine Hürde zu nehmen. Vielfach ist der Flaschenhals zwischen lokalem Netz und Internet nicht der als Router arbeitende Rechner, sondern das DSL-Modem an der Leitung zum Provider. Selbst wenn dieses nur mit 10 MBit/s im Halbduplex-Verfahren mit dem Router kommuniziert, kann dieser seine Daten schneller an das DSL-Modem senden als dieses sie über die Internet-Anbindung wieder los wird. Der Effekt der Warteschlangen auf dem Router wäre also dahin, da sich die Daten nach wie vor beim DSL-Modem stauen.

Das gilt genauso für im Router selbst verbaute DSL-Modems. Zusätzlich ist also dafür zu sorgen, dass die Datenrate zum DSL-Modem schon im Router begrenzt wird, damit nun dieser den Falschenhals bildet. Auch dafür bietet Linux geeignete Tools an. Schreiten wir also zur Implementierung.

Voraussetzungen zum Traffic-Shaping

Zuvor sind allerdings ein paar Kleinigkeiten zu klären. So muss der verwendete Kernel die so genannten Hierarchical Token Buckets (HTB) sowie das Markieren der Datenpakete per DSMARK unterstützen. Ob das der Fall ist, lässt sich leicht überprüfen. Öffnen Sie eine Kommandozeile mit Root-Rechten auf Ihrem Linux-Router und führen dort die nachfolgenden Befehle aus:

cd /boot
grep HTB config*
grep DSMARK config*

Dies sollte für die zweite Zeile die Ausgabe CONFIG_NET_SCH_HTB=m ergeben, für die dritte Zeile analog dazu CONFIG_NET_SCH_DSMARK=m. Anstelle des kleinen „m“ kann auch ein „y“ stehen. Das bedeutet lediglich, dass die entsprechenden Komponenten nicht als Module vorliegen, sondern direkt in den Kernel integriert sind. Meldet die Prüfung jedoch „n“ als Ergebnis, so müssen Sie selbst einen neuen Kernel mit den Modulen HTB und DSMARK erstellen.

Zusätzlich benötigen Sie das Hilfsprogramm tc zum Erstellen der Warteschlangen. In den meisten aktuellen Linux-Distributionen ist dieses Tool bereits enthalten. Ob es auch schon auf dem Rechner installiert ist, lässt sich mit dem Befehl which tc schnell ermitteln. Ebenfalls erforderlich ist das Paket iptables, das auf einem als Router arbeitenden Linux-Rechner jedoch quasi zwingend installiert ist, da mit seiner Hilfe auch die Firewall konfiguriert wird.

Arbeitserleichterung mit tcng

Über die beiden Werkzeuge hinaus benötigen Sie noch tcng. Zum Erstellen der Warteschlangen ist es zwar nicht zwingend erforderlich, es erleichtert die Arbeit aber erheblich. Es ist nämlich in der Lage, Konfigurationsdateien für tc automatisch zu erstellen. Für seine Installation müssen Sie sich das Archiv mit den Quelldateien herunterladen und manuell übersetzen. Die Kommandos dazu lauten:

wget http://tcng.sourceforge.net/dist/tcng-10b.tar.gz
tar xvzf tcng-10b.tar.gz
cd tcng
./configure --no-tcsim --no-manual
make
make install

Mittels der beiden Parameter --no-tcsim und --no-manual nach der Anweisung configure verhindern Sie, dass die Handbücher sowie das Simulations-Werkzeug erstellt werden. Wollen Sie diese auch erzeugen, so müssen Sie sowohl die Sourcen des aktuellen Kernels als auch das Paket transfig auf dem Linux-Rechner einrichten.

Die erste Warteschlange

Ist die Installation von tcng erfolgreich abgeschlossen, können Sie für einen ersten Test gleich eine einfache Warteschlange einrichten. Legen Sie dazu mit einem Texteditor die Datei example.tc mit folgendem Inhalt an:

#include "fields.tc"
#include "ports.tc"
dev eth0 {
egress {
drop if tcp_sport != PORT_HTTP;
}
}

Sie sehen schon, wie einfach es ist, mit tcng Konfigurationen zur Manipulation des Datenverkehrs zu schreiben. In von geschweiften Klammern eingefassten Blöcken legen Sie die einzelnen Regeln fest. Unser kleines Beispiel sorgt etwa dafür, dass nur auf dem HTTP-Port mit der Nummer 80 ausgehende Pakete – gekennzeichnet durch das Schlüsselwort egress – über das Interface eth0 versendet werden. Die dabei verwendete Variable PORT_HTTP bezieht das Script aus der per Include-Anweisung eingebundenen Datei ports.tc. Die ebenfalls integrierte Datei fields.tc wird für das Beispiel eigentlich nicht benötigt. Sie sollten sich aber angewöhnen, beide Hilfsdateien in alle ihre tcng-Skripte aufzunehmen, weshalb auch wir beide Dateien aufführen. Füttern Sie nun den Übersetzer tcc, der Bestandteil des tcng-Pakets ist, mit dieser Konfigurationsdatei:

tcc -r example.tc > example.sh

Generierte Warteschlange

Als Ergebnis erhalten Sie die neue Datei example.sh, die alle zur Konfiguration der Warteschlange notwendigen Befehle enthält. Öffnen Sie example.sh mit einem Texteditor und Sie sehen, wie viel Arbeit Ihnen tcng mit dem Übersetzer tcc abnimmt.

Um diese Datei auszuführen und die Änderungen an der Regel für ausgehenden Datenverkehr wirksam werden zu lassen, führen Sie das Skript in einer Shell aus:

tc -s qdisc ls dev eth0
sh example.sh
tc -s qdisc ls dev eth0

Der wichtige Befehl ist dabei der in der Mitte. Über die beiden anderen Anweisungen lassen Sie sich lediglich die Informationen über die gerade aktive Queue Discipline – abgekürzt eben qdisc – anzeigen. So sehen Sie, dass statt des zuvor verwendeten pfifo_fast jetzt dsmark zum Einsatz kommt. Da diese neue Methode jedoch nicht sonderlich sinnvoll ist, löschen Sie sie am besten wieder:

tc qdisc del dev eth0 root

Arten von Queue Discipilines

Sicher fragen Sie sich jetzt, was für Queue Discipilines Ihnen zur Verfügung stehen und was diese leisten. Das Standardverfahren zum Abarbeiten der Warteschlange haben Sie bereits kennen gelernt. Mit ihrer Hilfe sorgt Linux bereits automatisch dafür, dass der Datenverkehr ein wenig in Form gebracht wird.

Dazu wertet Linux das Type of Service Flag (TOS) der Datenpakete aus und sortiert den Traffic anhand dieser Informationen in drei so genannte Bänder. Ein echtes Shaping erfolgt dabei allerdings nicht. Linux leert zuerst das Band Nummer Null und bedient Band 1 erst, wenn keine Daten aus Band 0 mehr zu senden sind. Gleiches gilt für Band 2, das seine Daten erst los wird, wenn keine Pakete auf Band 1 mehr warten. Außerdem erlaubt es pfifo_fast nicht, die Datenrate zu limitieren und ist daher für das Verlagern des Flaschenhalses auf den Router-PC ungeeignet.

Ein anderes Verfahren, das die Limitierung der Datenrate erlaubt, ist der „Token Bucket Filter“ (TBF). So lässt sich beispielsweise der Uplink-Datenstrom zu einem an der Schnittstelle eth0 angeschlossenen Modem mit einem einfachen Befehl auf das gewünschte Maß drosseln:

tc qdisk add dev eth0 root tbf rate 125kbit latency 50ms burst 1540

Das ist zwar schon recht praktisch, um den Flaschenhals zu verlagern. Das Verfahren gestattet es aber nicht, wichtigen Datenströmen eine höhere Priorität einzuräumen.

Etwas näher an unseren Priorisierungs-Wünschen ist das „Stochastic Fair Queueing“ (SFQ). Bei diesem Verfahren wird für jede erkannte Kommunikation eine eigene Warteschlange eingerichtet. Jede Warteschlange ist gegenüber den anderen gleichberechtigt, die Leerung erfolgt im Round-Robin-Verfahren. Oder kurz gesagt: alle Datenpakete erhalten die gleiche Priorität. Da das gelegentlich schon ausreicht um eine spürbare Erleichterung bei der Arbeit zu erzielen, hier das Kommando zum Einrichten von SFQ:

tc qdisc add dev eth0 root sfq perturb 10

Wie zu sehen ist, lässt sich für SFQ aber wieder keine Transferraten-Beschränkung einrichten.

Das Ende der Klassenlosigkeit

Den bislang beschriebenen Verfahren ist gemeinsam, dass es sich um klassenlose Methoden zum Erzeugen von Warteschlangen handelt. Ihnen gegenüber stehen die Verfahren, bei denen die durchlaufenden Datenpakete klassifiziert und entsprechend ihrer Einordnung verarbeitet werden.

Dazu richten diese Verfahren zunächst wie die klassenlosen Methoden auch eine Wurzel-Qdisc ein. Diese lässt sich mithilfe von Klassen weiter unterteilen, wobei jede Klasse wieder eine eigene Qdisc verwenden kann, die – je nach Art der Qdisc – wiederum eigene Klassen enthalten darf. Es entsteht also eine Baumstruktur, in der die Datenpakete jeweils in den Blattknoten abgelegt werden.

Der Kernel kennt nur die Root-Qdisc

Dabei ist eine Besonderheit wichtig: aus Sicht des Linux-Kernels existiert nur die Root-Qdisc. Sind Pakete zu versenden fordert der Kernel diese bei der Root-Qdisc an, die diese Anforderung ihrerseits an die Blattknoten weiterreicht. Daraus ergibt sich, dass die Daten nie schneller aus den Warteschlangen abgerufen werden können als es die Root-Qdisc zulässt. Zusätzlich sind wir in der Lage, über die Kombination verschiedener Verfahren ein einfaches Traffic-Shaping auf die Beine zu stellen.

Das einfachste Verfahren aus der mit Klassen arbeitenden Gruppe ist prio. Ähnlich wie pfifo_fast richtet diese Qdisc drei verschiedene Klassen ein, deren Qdisc-Typ sich vom Anwender spezifizieren lässt:

tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:1 handle 10: sfq
tc qdisc add dev eth0 parent 1:2 handle 20: tbf rate 125kbit buffer 1600 limit 3000
tc qdisc add dev eth0 parent 1:3 handle 30: sfq

Geben Sie wie in diesem Beispiel keine zusätzlichen Filter an, erfolgt die Zuordnung der Pakete zu den einzelnen Klassen analog dem Verfahren bei pfifo_fast. Ein großer Nachteil von prio fällt sofort ins Auge: es ist keine Möglichkeit der Bandbreitenbegrenzung vorhanden.

Class Based Queueing

Genau aus diesem Grund findet sich in der Literatur das Verfahren „Class Based Queueing“ (CBQ). Über seine vielfältigen Parameter ist es flexibel auf die eigenen Bedürfnisse abstimmbar, dafür aber allerdings auch sehr umständlich zu konfigurieren. Die Hauptschuld daran trägt das von CBQ verwendete Verfahren zur Begrenzung der Bandbreite. Dazu berechnet CBQ aus einer vom Anwender anzugebenden durchschnittlichen Paketgröße die Sendepause, die zwischen zwei Übertragungen eingehalten werden muss, um auf die gewünschte Transferrate zu kommen. Je nach Art des realen Datenaufkommens ist dieses Verfahren mehr oder weniger genau und es bedarf einer genauen Beobachtung und ständigen Fein-Tunings.

Um beispielsweise eine CBQ-Qdisc mit zwei Klassen einzurichten, die jeweils eine begrenzte Transferrate zur Verfügung haben und voneinander Bandbreite leihen können, sind folgende Befehle notwendig:

tc qdisk add dev eth0 root handle 1:0 cbq bandwidth 100Mbit avgpkt 1000 cell 8
tc class add dev eth0 parent 1:0 classid 1:1 cbq bandwidth 100 Mbit rate 125kbit weight 12kbit prio 8 allot 1514 cell 8 maxburst 20 avgpkt 1000 bounded
tc class add dev eth0 parent 1:1 classid 1:3 cbq bandwidth 100Mbit rate 100kbit weight 10kbit prio 5 allot 1514 cell 8 maxburst 20 avpkt 1000
tc class add dev eth0 parent 1:1 classid 1:4 cbq bandwidth 100Mbit rate 60kbit weight 6kbit prio 5 allot 1514 cell 8 maxburst 20 avpkt 1000
tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dport 80 0xffff flowid 1:3
tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dport 25 0xffff flowid 1:4

CBQ - im Detail

Was haben wir nun damit erreicht? Zusätzlich zur Root-Qdisc richten die Kommandos eine Klasse ein, die die verfügbare Bandbreite auf 125 kbps begrenzt. Dieser werden wiederum zwei weitere Klassen untergeordnet, wobei die eine maximal 100 kbps, die andere maximal 60 kbps der Bandbreite verbrauchen darf.

Über die Filter-Anweisungen legen wir fest, dass die erste Unterklasse für Pakete dienen soll, die an einen entfernten HTTP-Server gerichtet sind. Die zweite Unterklasse nimmt Pakete auf, die an einen SMTP-Server adressiert sind. Verbrauchen beide Unterklassen mehr als die zur Verfügung stehenden 125 kbps, wird der Traffic im Verhältnis der hinter dem Schlüsselwort weight angegebenen Geschwindigkeiten aufgeteilt. So erhalten HTTP-Transfers stets zehn Sechzehntel der verfügbaren Bandbreite – also etwas über 78 kbps – garantiert zugeteilt während für SMTP-Übertragungen stets eine Bandbreite von rund 47 kbps zur Verfügung steht.

Wollte man dieses Spiel noch auf die Spitze treiben, könnte man auf den Unterklassen noch SFQ-Qdiscs realisieren, um so die Abarbeitung anderer Protokolle sicher zu gewährleisten. Wie deutlich zu sehen ist, lässt vor allem der Komfort beim Konfigurieren der einzelnen Queues stark zu wünschen übrig. Zudem ist der die durchschnittliche Paketgröße angebende Parameter avgpkt eher beispielhaft gewählt – in der Realität werden Sie hier sicher drehen müssen um befriedigende Ergebnisse zu erzielen.

Zurück zu TCNG

Doch zum Glück gibt es noch eine weitere Qdisc, die sich noch dazu komfortabel über tcng konfigurieren lässt: der „Hierarchical Token Bucket“ (HTB). Ähnlich wie CBQ erlaubt es dieser, eine Obergrenze der verfügbaren Bandbreite sowohl für die Root-Qdisc wie auch für einzelne Klassen zu setzen. Das Ausleihen gerade nicht benötigter Bandbreite an Nachbarklassen ist ebenso realisiert wie die garantierten Mindestbandbreiten für wichtige Anwendungen. Damit ist HTB das ideale Werkzeug, um die diversen ausgehenden Datenströme in die gewünschte Form zu bekommen. Ein Beispiel verdeutlicht das eindrucksvoll. Legen Sie dazu die Datei htb1.tc mit folgendem Inhalt an:

#include "fields.tc"
#include "ports.tc"
$meter = __trTCM( cir 96kbps, cbs 10kB, pir 125kbps, pbs 10kB );
dev eth0 {
egress {
class ( <$full> ) if ip_src == 192.168.1.111;
class ( <$fast> ) if __trTCM_green( $meter );
class ( <$slow> ) if __trTCM_yellow( $meter );
drop if __trTCM_red( $meter );
htb() {
class ( rate 125kbps, ceil 125kbps ) {
$fast = class ( rate 256kbps, ceil 256kbps ) { sfq; };
$slow = class ( rate 128kbps, ceil 128kbps ) { sfq; };
$full = class ( rate 125kbps, ceil 125kbps ) { sfq; };
}
}
}
}

Erläuterung

Das sieht auf den ersten Blick etwas verwirrend aus, ist aber ein einfaches Verfahren, den ausgehenden Traffic in den Griff zu bekommen. Dreh- und Angelpunkt des Verfahrens ist die Möglichkeit, den Datenstrom anhand der aktuell genutzten Bandbreite zu priorisieren. Dazu dienen die __trTCM-Funktionen green, yellow und red zusammen mit den in der Variablen $meter gespeicherten Verbindungsparametern.

Hierbei legt der Wert von cir (Commited Information Rate) die Datenrate fest, bis zu der man die Bandbreitennutzung zulassen möchte. Ist die aktuelle Auslastung unter diesem Wert, gibt die Green-Funktion als Ergebnis „wahr“ zurück. Liegt die Auslastung zwischen der Commited Information Rate und dem als Peak Information Rate (pir) angegebenen Maximalwert, gibt die Yellow-Funktion „wahr“ zurück. Liegt die Auslastung gar über dem Spitzenwert, liefert die Red-Funktion das Ergebnis „wahr“, die zugehörige Drop-Funktion sorgt dafür, dass dieser Traffic verworfen wird.

Die einzelnen Klassen selbst sind als SFQ ausgeführt, was eine gleichmäßige Abarbeitung der ausgehenden Daten gewährleistet. Mit diesen Anweisungen reguliert sich der Verkehr quasi selbstständig. Zusätzlich sorgt die erste Regel dafür, dass der von der Station mit der IP-Adresse 192.168.1.111 ausgehende Datenverkehr nicht von der Reglementierung betroffen ist.

Zum Übersetzen der Konfiguration in ein Shell-Skript mit den entsprechenden Befehlen für tc dienen wieder die bekannte Kommandos:

tcc -r htb1.tc > htb1.sh
sh htb1.sh

Priorisierung nach Protokoll

Das gerade beschriebene Verfahren ist sicher gut geeignet, um eine gerechte Verteilung des Traffics zu erreichen. Es genügt aber nicht, wenn es gilt, einer wichtigen Anwendung eine garantiert verfügbare Bandbreite zur Verfügung zu stellen. Aber auch dies lässt sich recht einfach realisieren:

#include "fields.tc"
#include "ports.tc"
dev eth0 {
egress {
class ( <$ssh> ) if tcp_dport == PORT_SSH;
class ( <$voip> ) if tcp_dport == PORT_SIP;
class ( <$http> ) if tcp_dport == PORT_HTTP;
class ( <$rest> ) if 1;
htb () {
class ( rate 128kbps, ceil 128kbps ) { (8)
$ssh = class ( rate 8kbps, ceil 24kbps ) { sfq; };
$voip = class ( rate 64kbps, ceil 128kbps ) { sfq; };
$http = class ( rate 48kbps, ceil 128kbps ) { sfq; };
$rest = class ( rate 32kbps, ceil 96kbps ) { sfq; };
}
}
}
}

Erläuterung

Dank tcng ist auch bei diesem Beispiel die Funktion klar ersichtlich. Es werden vier Klassen für unterschiedliche Arten von Traffic konfiguriert. In der Klasse ssh landet der zur Fernwartung von Linux-Servern dienende Datenverkehr. Da es sich bei SSH um ein recht genügsames Protokoll handelt, stellen wir mindestens 8 kbps zur Verfügung, deckeln aber auch bei maximal 32 kbps.

Anders sieht es bei der Klasse voip aus, der wir mindestens 64 kbps bereitstellen und ihr auch gleich erlauben, im Zweifel auch die gesamte Bandbreite zu nutzen. Die Klasse http enthält Zugriffe auf entfernte Web-Server und wird mit einer zugesicherten Bandbreite von 48 kbps versorgt. Die restlichen noch genutzten Protokolle landen in der Klasse rest, wo ihnen mindestens 32 kbps, maximal jedoch nur 96 kbps zur Verfügung stehen.

Schön zu sehen ist, wie die Verwendung der aus ports.tc importierten Variablen für die jeweiligen Port-Nummern die Lesbarkeit der Konfiguration erheblich verbessert. Ähnliches gilt für die verschiedenen Felder der bearbeiteten IP-Pakete, die aus fields.tc stammen und von denen im Beispiel tcp_dport zum Einsatz kommt.

Ausblick

Sicher sind sie jetzt auf den Geschmack gekommen und möchten selbst eigene Konfigurationen nach Ihren Vorstellungen erzeugen. Nur zu, viel kann Ihnen dabei nicht passieren. Auch im Internet finden sich einige gute Beispiele, wie sich die Konfiguration Ihres Traffic-Shapers noch verbessern lässt.

Ein guter Startpunkt ist das Onine-Code-Verzeichnis. Allerdings sollten Sie beim Verwenden der dort abgelegten Dateien ein wenig Vorsicht walten lassen. Zum Teil sind dort auch Fehler enthalten, die Sie zunächst manuell ausbügeln müssen. Doch keine Sorge, meist handelt es sich nur um kleine Tippfehler, auf die der Übersetzer tcc schon von sich aus aufmerksam macht. (mha)