Aus Linux-Magazin 02/2011

Xtext generiert IPtables-Regeln

© Aleksandr Frolov, 123RF.com

Manches wichtige Softwarepaket verwendet eigenwillige Syntax auf der Kommandozeile oder in Konfigurationsdateien. Dieser Artikel zeigt am Beispiel IPtables, wie sich mit Hilfe des Framework Xtext eine vereinfachte Konfigurationssprache samt Skriptgenerator bauen lässt.

Werkzeuge zur Parser-Generierung gehören seit Urzeiten zum Inventar der Unix-Betriebssysteme. Im praktischen Einsatz geben sich mächtige Tools wie Bison und Yacc aber leider oft sperrig und sind daher hauptsächlich in großen Projekten zu finden. Im Kielwasser der modellgetriebenen Entwicklung sind jedoch in den letzten Jahren alternative Werkzeuge entstanden, die sich durchaus auch für kleinere Aufgaben außerhalb der klassischen Software-Entwicklung verwenden lassen.

Firewall nach Modell

Mit Xtext [1] stellt dieser Artikel ein Werkzeug zur Erstellung Domänen-spezifischer Sprachen (DSL) vor. Als Beispiel erarbeitet er eine Sprache für Firewallregeln und produziert schließlich mit Hilfe eines Generators ein IPtables-Skript.

Naturgemäß lässt sich die Komplexität eines flexiblen Konzepts wie IPtables durch Ändern der Beschreibungsform nicht verringern. Allenfalls erhält man eine ähnlich komplexe, aber besser lesbare Konfiguration. Allein das bedeutet aber bei mancher Software schon einen nicht zu verachtenden Fortschritt.

Einen echten Mehrwert kann allerdings erzielen, wer die für den gewünschten Einsatzzweck invarianten Bestandteile identifiziert und fest in den Generator einbaut. Dadurch muss die Sprache nur noch die eigentlichen Nutzinformationen aufnehmen und lässt sich schneller erstellen und erlernen. Gleichzeitig senkt dieses Vorgehen die Wahrscheinlichkeit von Fehlbedienungen.

Um das Modellieren einer Firewall etwas einfacher zu gestalten, gelten für das Beispiel in diesem Artikel die im Kasten “Annahmen für die Firewall” formulierten Einschränkungen. Die hier beschriebene Lösung kann sicher nicht mit spezialisierten Tools wie Ferm [2] konkurrieren. Anhand dieses Beispiels lässt sich aber die Arbeit mit Xtext gut erläutern. Oft hilft es, sich zu Beginn der Arbeit einen Grobentwurf der gewünschten Sprache zu erstellen. Anhand dieser Skizze fällt es dann leichter, die Grammatik zu schreiben, die zur Grundlage aller weiteren Schritte wird.

Annahmen für die Firewall

  • Es handelt sich um eine einfache IPv4-Firewall zwischen Standorten.
  • Die Firewall hat je genau ein internes und ein externes Interface.
  • Rechner, Netze und Dienste sollen durch symbolische Namen identifiziert werden.
  • Alle beteiligten Netzbereiche sind disjunkt, NAT findet nicht statt.
  • Es werden nur TCP- und UDP-Regeln auf Port-Basis erstellt.
  • Nur explizit konfigurierte Verbindungen zwischen den angegebenen Standorten sind zugelassen.
  • Externe Zugriffe auf die Firewall sind verboten, interne Zugriffe sind erlaubt.

Abbildung 1 zeigt ein Beispiel der geplanten Firewall-Sprache. Sie enthält die Namen für Skript und Netzwerk-Schnittstellen, die symbolischen Namen für Rechner und Netze sowie die Dienste-Definitionen. Den letzten Teil bilden die Regeln für ein- und ausgehende Verbindungen. So aufgeschrieben ist die Konfiguration der Firewall auch ohne tiefe Kenntnis der IPtables-Syntax verständlich.

Abbildung 1: Ein Prototyp der gewünschten Firewall-Sprache dient als Grundlage für die Arbeit mit Xtext.

Abbildung 1: Ein Prototyp der gewünschten Firewall-Sprache dient als Grundlage für die Arbeit mit Xtext.

Grammatik

Listing 1 bildet die Grundlage der Grammatik und sieht einer um Steuerinformationen angereicherten erweiterten Backus-Naur-Form ähnlich. Die beiden Kopfzeilen hat Xtext automatisch erzeugt. Die erste Produktionsregel der Grammatik legt das Wurzelelement der künftigen Eingabedateien fest. Aus dieser Regel entsteht später ein Elementtyp namens »Model« .

Listing 1

Xtext-Grammatik

01 grammar org.myfirewall.MyFirewall with org.eclipse.xtext.common.Terminals
02 generate myFirewall "http://www.myfirewall.org/MyFirewall"
03
04 Model:
05 scripts+=Script*;
06
07 Script:
08 'script' name=STRING '{'
09 'interfaces internal' ifInternal=ID 'external' ifExternal=ID ';'
10 hosts+=HostAlias*
11 networks+=NetworkAlias*
12 services+=ServiceDefinition*
13 ('allow incoming {' incomingRules+=Rule+ '}')?
14 ('allow outgoing {' outgoingRules+=Rule+ '}')?
15 '}';
16
17 IPAddress:
18 byte1=INT '.' byte2=INT '.' byte3=INT '.' byte4=INT;
19
20 enum Protocol:
21 TCP='tcp' | UDP='udp';
22
23 HostAlias:
24 'host' name=ID '=' ip=IPAddress ';';
25
26 NetworkAlias:
27 'net' name=ID '=' ip=IPAddress '/' cidrSuffix=INT ';';
28
29 Endpoint:
30 ('host' host=[HostAlias]) | ('net' network=[NetworkAlias]);
31
32 ServiceDefinition:
33 'service' name=ID '='
34 ('proto' (protocols+=Protocol |
35 '(' protocols+=Protocol (',' protocols+=Protocol)+ ')'))?
36 ('port' (ports+=INT |
37 '(' ports+=INT (',' ports+=INT)+ ')'))? ';';
38
39 Rule:
40 source=Endpoint 'to' destination=Endpoint ':'
41 services+=[ServiceDefinition] (',' services+=[ServiceDefinition])* ';';

Zeile 5 bewirkt, dass der Typ ein Attribut »scripts« erhält, das aus beliebig vielen »Script« -Elementen besteht. Die »Script« -Produktionsregel ab Zeile 7 legt die Syntax dieses Elements fest: das Schlüsselwort »script« gefolgt vom Namen des Skripts. Auch hier bewirkt die Notation »name =STRING« , dass der Typ »Script« später ein Attribut »name« zur Aufnahme von Zeichenketten enthält.

Da hier aber kein Quantor wie »*« angegeben ist, kann dieses Attribut anders als »scripts« nur genau einen Namen aufnehmen. Die Zeilen 9 bis 12 enthalten keine wesentlichen Neuerungen bis auf den Typ »ID« , der einen (Java-)Bezeichner ohne Anführungszeichen darstellt, wogegen »STRING« -Attribute in Anführungszeichen angegeben sind. Die Zeilen 13 und 14 enthalten jeweils Gruppen: Die runden Klammern in Verbindung mit dem Quantor »?« erlauben es, die gesamte Regelgruppe für ein- oder ausgehende Verbindungen wahlweise anzugeben oder auszulassen.

Die Zeilen 17 und folgende definieren einen Typ zur Angabe von IPv4-Adressen. Die Zeilen 20 und 21 beschreiben einen Aufzählungstyp zur Angabe des Protokolls. Dabei sind »TCP« und »UDP« die sprachintern verwendeten Werte, die Literale »tcp« und »udp« werden bei der Ein- und Ausgabe verwendet. Die Zeilen 23 bis 27 definieren einfache zusammengesetzte Typen zur Angabe symbolischer Rechner- und Netzwerknamen. Zeile 30 zeigt eine Besonderheit von Xtext: »[Typname]« definiert einen Verweis auf Elemente dieses Typs.

Die Produktionsregel »Endpoint« enthält eine weitere Besonderheit: Die Literale »host« und »net« sind eine Konzession an den Parser, der ansonsten nicht zwischen einem Verweis auf einen Rechner oder auf ein Netzwerk (jeweils in Form einer ID) unterscheiden könnte und bei der Generierung eine entsprechende Warnung ausgeben würde.

Die Definition eines Dienstes ab Zeile 32 mag zunächst unübersichtlich wirken, enthält aber nichts wesentlich Neues. Protokolle und Portnummern lassen sich entweder als einfacher Wert oder als kommaseparierte Werteliste in Klammern angegeben, daher die etwas umfangreichere Regel. Bei der Definition einer Regel ist ein Unterschied zwischen den Zeilen 40 und 41 wichtig: Während die Dienste-Definitionen in eckigen Klammern stehen und daher referenziert werden, sind die Endpunkte als direkte Attribute in die Regel eingefügt. Die Grammatik ist schon so gut wie alles, was man für eine erste Version des Editors benötigt. Wer sie praktisch mit Eclipse und Xtext-Tools umsetzen möchte, findet im Kasten “Xtext installieren” Hinweise.

Xtext installieren

Voraussetzung für die Installation von Eclipse und Xtext ist lediglich eine Java-Laufzeitumgebung, wobei sich mindestens Version 5 empfiehlt. Leider gibt es mit den in den gängigen Distributionen enthaltenen Eclipse-Paketen immer wieder Probleme – vor allem sind sie in der Regel veraltet. Zu empfehlen ist dagegen das Paket “Eclipse Modeling Tools” von [3].

Nach dem ersten Start sind über »Help | Install Modeling Components« die Pakete Xpand und Xtext nachzuinstallieren. Nach der Installation stellt man in den globalen Einstellungen unter »General | Workspace« die Standardkodierung von Textdateien auf UTF-8 um, damit es keine Schwierigkeiten mit den von Xtext verwendeten Guillemets («») gibt. Um die Übersicht nicht zu verlieren, ist es außerdem ratsam, im Package Explorer die Option »Link with Editor« (das Icon mit den zwei Pfeilen) zu aktivieren.

Der Befehl «File | New Project | Xtext Project« legt neben dem Hauptprojekt auch noch ein Projekt zur Aufnahme des generierten Editors mit dem Suffix ».ui« an. Außerdem erzeugt er in der Voreinstellung ein Generator-Projekt, das später benötigt wird. Xtext kann etwas empfindlich auf ungewöhnliche Schreibweisen reagieren, deshalb sollten Einsteiger als Projektnamen »org.myfirewall« und als Sprachnamen »org.myfirewall.MyFirewall« wählen. Der Wizard öffnet nach der Anlage der Projekte die Xtext-Datei, die die Beschreibung der Grammatik aus Listing 1 aufnimmt.

Editor nach Maß

Neben der Sprachdefinition befindet sich eine Datei mit der Endung ».mwe2« . Diese Steuerdatei der Modeling Workflow Engine legt Parameter zur Generierung der Programmkomponenten fest. In dieser Datei entfernt der DSL-Entwickler die Kommentarzeichen vor dem letzten Abschnitt »project wizard« , um einen Wizard zur Anlage eines neuen Firewall-Projekts zu erhalten. Bei dieser Gelegenheit fügt er auch die Zeile

fragment = org.eclipse.xtext.generator.validation.CheckFragment {}

innerhalb des »language« -Blocks ein – zur Verwendung später mehr. Ein Rechtsklick auf die Workflow-Datei und »Run As | MWE2 Workflow« startet die Generierung der Programmkomponenten.

Bei der ersten Generierung bietet Xtext an, automatisch den Antlr-Parsergenerator herunterzuladen, was der Anwender bejaht. Aufgrund lizenzrechtlicher Bedenken darf die Eclipse Foundation diese Komponente nicht in ihren Produkten ausliefern. Details dazu finden sich in der Xtext-Dokumentation unter [4].

Nach erfolgreicher Generierung lässt sich der erstellte Editor schon starten, indem man unter »Run | Run Configurations …« eine neue Konfiguration des Typs »Eclipse Application« anlegt. In der neuen Instanz ist ein »MyFirewall« -Projekt einzurichten – danach steht der Editor zum Ausprobieren bereit: Syntax-Hervorhebung, automatische Code-Vervollständigung und viele andere Funktionen sind jetzt schon benutzbar (Abbildung 2).

Abbildung 2: Eclipse und die Xtext-Tools erzeugen aus der Sprachdefinition einen Editor, der unter anderem Syntax Highlighting und Code-Vervollständigung bietet.

Abbildung 2: Eclipse und die Xtext-Tools erzeugen aus der Sprachdefinition einen Editor, der unter anderem Syntax Highlighting und Code-Vervollständigung bietet.

Nun ist noch eine Änderung bezüglich der Datei »META-INF/MANIFEST.MF« im Hauptprojekt erforderlich. Auf der Registerkarte »Dependencies« wählt der Xtext-Anwender den Eintrag »org.eclipse.xtext.xtend« und schaltet mit der Drucktaste »Properties« die Option »Optional« aus sowie die Option »Reexport…« an. Danach markiert er nacheinander noch alle drei Projekte und wählt aus deren Kontextmenü »Configure | Add Xpand/Xtend Nature« .

Gültigkeitsprüfung

Der generierte Editor ist – insbesondere in Relation zum bisher investierten Aufwand – schon sehr ansehnlich, aber bei Weitem noch nicht perfekt: Die Produktionsregel »IPAddress« beispielsweise erlaubt es, beliebige Integerwerte in den Bytes anzugeben und damit auch Adressen wie 172345.26.5.1234 zu schreiben. Dieses Problem behebt eine Prüfregel, die in der Sprache Check formuliert wird. Check gehört zur Familie der Sprachen um Xtend und Xpand und verwendet die gleiche funktionale Notation. Dabei kommt ein automatisch aus der Grammatik abgeleitetes Meta-Modell zum Einsatz. Die Check-Referenz ist unter [5] online verfügbar.

In die Datei »MyFirewallFastChecks.chk« , die im Paket »org.myfirewall.validation« zu finden ist, fügt der Entwickler am Anfang die Anweisung »import myFirewall;« ein, um das generierte Ecore-Modell zu importieren. Anschließend trägt er die Zeilen aus Listing 2 ein. Diese Prüfung wirkt auf alle Instanzen von »IPAddress« und gibt eine Fehlermeldung aus, wenn die formulierte Bedingung nicht erfüllt ist (Abbildung 3).

Listing 2

IP-Adressen

01 context IPAddress ERROR "Not a valid IP address":
02 byte1 >= 0 && byte1 <= 255 && byte2 >= 0 &&
03 byte2 <= 255 && byte3 >= 0 && byte3 <= 255 &&
04 byte4 >= 0 && byte4 <= 255;
Abbildung 3: Erweiterte Prüfungen mit integrierter Anzeige der Ergebnisse im Editor sind mit der Sprache Check leicht zu erstellen.

Abbildung 3: Erweiterte Prüfungen mit integrierter Anzeige der Ergebnisse im Editor sind mit der Sprache Check leicht zu erstellen.

Generator

Um aus der Beschreibung der Firewallregeln ein ausführbares Shellskript zu erzeugen, sind weitere Arbeiten im Generator-Projekt zu erledigen. Vor der eigentlichen Textgenerierung sind noch ein paar Hilfsmittel nötig, die mit der Sprache Xtend realisiert werden. Diese Sprache stammt – wie viele der hier vorgestellten Projekte – aus dem Projekt Open Architecture Ware und ist mittlerweile zusammen mit Check und Xpand in ein Eclipse-Projekt umgezogen. Die Xtend-Ausdrücke finden in der Datei »templates/Extensions.ext« Platz.

Um die IP-Adressen als Text auszugeben, reicht ein einfacher Ausdruck (Listing 3). Der erste Parameter lässt sich unabhängig von seinem Namen auch als aktuell bearbeitetes Objekt »this« bezeichnen. Zum Erzeugen der Kommentare ist der Name des Rechners oder Netzwerks erforderlich, auf den ein Endpunkt verweist. Da Xtend zu den funktionalen Sprachen zählt, hat die Fallunterscheidung die Form eines ternären Operators (Listing 4). Gleichermaßen erforderlich ist die IP-Adresse des Rechners beziehungsweise die des Netzwerks, letztere in CIDR-Notation (Listing 5). Mit diesen drei Ausdrücken im Werkzeugkoffer geht es an die Generierung.

Listing 5

CIDR-Notation

01 String getAddress(Endpoint this):
02 host != null ? host.ip.format():
03 network.ip.format() + '/' + network.cidrSuffix;

Listing 4

Hostnamen

01 String getName(Endpoint this):
02 host != null ? host.name : network.name;

Listing 3

String-Ausgabe

01 String format(IPAddress this):
02 byte1.toString() + '.' + byte2.toString() + '.' +
03 byte3.toString() + '.' + byte4.toString();

Es werde Skript

Die Regeln zur Erzeugung der Ausgabedateien formuliert der Entwickler in der Sprache Xpand. Diese Sprache ist speziell auf die Vorlagen-Erstellung ausgerichtet. Bei der Ausführung werden alle Textinhalte in die Ausgabe übertragen, mit Ausnahme der Kommandos, die durch Guillemets («») gekennzeichnet sind. Ein Bindestrich vor dem schließenden Guillemet verhindert, dass der folgende Zeilenumbruch in der Ausgabedatei landet. Listing 6 zeigt den Inhalt der Datei »templates/Template.xpt« des Firewall-Generators.

Listing 6

Xpand-Template

01 «IMPORT org::myfirewall::myFirewall<C>
02 <C>EXTENSION templates::Extensions<C>
03
04 <C>DEFINE main FOR Script-<C>
05 <C>FILE name-<C>
06 #!/bin/sh
07
08 # no network address translation
09 /sbin/iptables -t nat -F
10 /sbin/iptables -t nat -P PREROUTING ACCEPT
11 /sbin/iptables -t nat -P POSTROUTING ACCEPT
12
13 # prepare filtering tables
14 /sbin/iptables -t filter -F
15 /sbin/iptables -t filter -P INPUT REJECT
16 /sbin/iptables -t filter -P OUTPUT ACCEPT
17 /sbin/iptables -t filter -P FORWARD REJECT
18
19 # always allow inbound connections from loopback and the internal network
20 /sbin/iptables -t filter -A INPUT --in-interface lo --jump ACCEPT
21 /sbin/iptables -t filter -A INPUT --in-interface <C>ifInternal<C> --jump ACCEPT
22
23 # enable connection tracking
24 /sbin/iptables -t filter -A FORWARD --match state --state INVALID --jump DROP
25 /sbin/iptables -t filter -A FORWARD --match state --state ESTABLISHED,RELATED --jump ACCEPT
26
27 # inbound connections
28 <C>FOREACH incomingRules AS rule-<C>
29 <C>EXPAND rule(ifExternal, ifInternal) FOR rule-<C>
30 <C>ENDFOREACH-<C>
31
32 # outbound connections
33 <C>FOREACH outgoingRules AS rule-<C>
34 <C>EXPAND rule(ifInternal, ifExternal) FOR rule-<C>
35 <C>ENDFOREACH-<C>
36 <C>ENDFILE-<C>
37 <C>ENDDEFINE<C>
38
39 <C>DEFINE rule(String fromInterface, String toInterface) FOR Rule-<C>
40 # from <C>source.getName()<C> to <C>destination.getName()<C>
41 <C>FOREACH services AS service-<C>
42 <C>FOREACH service.protocols AS protocol-<C>
43 <C>FOREACH service.ports AS port-<C>
44 /sbin/iptables -t filter -A FORWARD --in-interface <C>fromInterface<C> --out-interface <C>toInterface<C> \
45 --source <C>source.getAddress()<C> --destination <C>destination.getAddress()<C> \
46 --protocol <C>protocol<C> --dport <C>port<C> --jump ACCEPT
47 <C>ENDFOREACH-<C>
48 <C>ENDFOREACH-<C>
49 <C>ENDFOREACH-<C>
50 <C>ENDDEFINE»

Die Datei besteht aus zwei Vorlagen namens »main« und »rule« . Eine Vorlage ist jeweils mit Bezug zu einer Klasse des Ecore-Modells definiert und wird für eine Instanz dieser Klasse ausgewertet. Im vorliegenden Beispiel gibt es in der Modelldatei eine »script« -Anweisung mit einem festgelegten Skriptnamen, sodass die Vorlage »main« einmal für dieses Skript angewendet wird. Der »FILE« -Block legt den Namen der Ausgabedatei fest – in diesem Fall wird er aus dem Attribut der »Script« -Klasse übernommen.

Die Zeilen 6 bis 27 übernimmt das Template unverändert. Dann arbeitet der Generator die Regeln für die eingehenden und ausgehenden Verbindungen ab, indem er für jede Regel die Vorlage »rule« mit der Anweisung »EXPAND« anwendet. Dabei sind die Netzwerkschnittstellen jeweils aus Sicht der Regel angegeben.

Die Regelbearbeitung generiert zunächst einen Kommentar, wobei einer der zuvor angelegten Xtend-Ausdrücke zum Einsatz kommt. Durch die Angabe in Zeile 2 sind diese Ausdrücke so verwendbar, als wären sie Operationen der Klassen selbst. Drei geschachtelte Schleifen sorgen dann dafür, dass der Generator für jede Kombination aus Protokoll und Port eines jeden Dienstes einen »iptables« -Aufruf erzeugt. Auch hier kommen wieder Xtend-Ausdrücke zur Anwendung. Zum Auswerten dieser Regeln dient die Modeling Workflow Engine. Dazu muss der Programmierer noch die Workflow-Datei im Verzeichnis »src/workflow« des Generators anpassen, indem er die Slot-Angabe »greetings« durch »scripts« und die Typangabe »Greeting« durch »Script« ersetzt. Außerdem kommt die Vorlage »main« für jedes Skript statt für jede Begrüßung zum Einsatz:

expand = "templates::Template::mainFOREACH scripts"

Jetzt lässt sich die Generierung starten, indem man in der zweiten Eclipse-Instanz des Firewall-Projekts aus dem Kontextmenü der Workflow-Datei »Run As | MWE2 Workflow« wählt. Das Ergebnis »setup-firewall.sh« findet sich im Verzeichnis »src-gen« (Abbildung 4).

Abbildung 4: Als Ergebnis der Arbeit mit dem Modelling-Tool liegt das fertige IPtables-Skript vor.

Abbildung 4: Als Ergebnis der Arbeit mit dem Modelling-Tool liegt das fertige IPtables-Skript vor.

Ausblick

Das hier vorgestellte Konzept lässt sich flexibel auf beliebige andere Szenarien anwenden, in denen eine oder mehrere Textdateien aus einer Beschreibung entstehen sollen. Dabei können durchaus auch Konfigurationsdateien für verschiedene Anwendungszwecke (Netzwerk-Konfiguration, Routing, Firewall, MTA oder Webserver) aus einem zentralen Modell erzeugt werden. Die Entwicklung eines solchen Werkzeugs bietet sich vor allem für Admins an, die regelmäßig Systeme nach einem wiederkehrenden Schema konfigurieren.

Der Autor

Volker Wegert ist SAP-R/3-Entwickler bei einem großen deutschen Klinikkonzern und beschäftigt sich in seiner Freizeit mit Modellierungs- und Generierungstechniken.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben