Next Previous Contents

4. Informationen fuer Programmierer

Ich weihe Dich in ein Geheimnis ein: mein Hamster hat alles programmiert. Ich war nur das Medium, wenn Du es so willst das 'Front-End', im grossen Plan meines Hamsters. Also mach mich nicht fuer irgendwelche Bugs verantwortlich. Mach den schlauen mit dem Fell verantwortlich.

4.1 ip_tables verstehen

iptables besteht einfach aus einem benannten Array von Regeln im Speicher (daher der Name 'iptables') und Informationen darueber, wo Pakete von jedem Hook die Reise beginnen sollen. Nachdem eine Tabelle registriert wurde, kann ihr Inhalt von Anwenderseite her gelesen oder ersetzt werden, indem getsockopt() und setsockopt() benutzt werden.

iptables registriert nicht mit irgendwelchen Netfilter Hooks: es verlaesst sich auf andere Module, die das tun und es angemessen mit Paketen fuettern sollen.

ip_tables Datenstruktur

Der Bequemlichkeit halber wird dieselbe Datenstruktur verwendet, um eine Regel von Anwenderseite her oder im Kernel selbst zu repraesentieren, obwohl einige Felder nur im Kernel selbst benutzt werden.

Jede Regel besteht aus folgenden Teilen:

  1. Einem 'struct ip_entry'.
  2. Null oder mehr 'struct ipt_entry_match' Strukturen, an jede Platz fuer Variablen (0 oder mehr Bytes) von Daten angehaengt.
  3. Einer 'struct ipt_entry_target' Struktur, an jede Platz fuer Variablen (0 oder mehr Bytes) von Daten angehaengt.

Die Variablen-Natur der Regeln gibt eine grosse Flexibilitaet fuer Erweiterungen, wie wir sehen werden, besonders, da jeder Treffer oder jedes Ziel eine willkuerliche Menge von Daten tragen koennen. Dies birgt zwar auch ein paar Fallen, aber wie auch immer: Wir muessen aufpassen. Wir tun dies, indem wir uns darueber versichern, dass die 'ipt_entry', die 'ipt_entry_match' und die 'ipt_entry_target' Strukturen eine angenehme Groesse haben, und dass alle Daten auf die maximale Auslastung des Rechners aufgerundet werden, indem wir den IPT_ALIGN() Makro verwenden.

`struct ipt_entry' hat die folgenden Felder:

  1. Einen 'struct ipt_ip' Teil, der die Bestimmungen fuer den IP-Header enthaelt, der zutreffen soll.
  2. Ein 'nf_cache' Bitfeld, das zeigt, welche Teile des Pakets diese Regel untersucht hat.
  3. Ein 'target_offset' Feld, das den Absatz vom Anfang dieser Regel angibt, wo die ipt_entry_target Struktur beginnt.
  4. Ein 'next_offset' Feld, das die absolute Groesse dieser Regel angibt, einschliesslich Treffer und Ziele.
  5. Ein 'comefrom' Feld, das vom Kernel benutzt wird, um die Reise des Pakets zurueckzuverfolgen.
  6. Ein 'struct ipt_counters' Feld, was die Paket- und Bytezaehler fuer Pakete, die auf diese Regel gepasst haben, enthaelt.

ipt_entry_match' und 'struct ipt_entry_target' sind sehr aehnlich darin, dass sie ein Feld fuer die Gesamtlaenge (jeweils 'match_size' und `target_size') enthalten, einen Union, der den Namen des Treffers oder des Ziels (auf Anwenderseite) enthaelt, und einen Pointer (fuer den Kernel).

Wegen der trickreichen Natur der Datenstruktur einer Regel werden einige Hilfsroutinen angeboten:

ipt_get_target()

Diese Funktion liefert einen Pointer auf das Target einer Regel.

IPT_MATCH_ITERATE()

Dieser Makro ruft die gegebene Funktion fuer jeden Treffer einer Regel auf. Das erste Argument der Funktion ist 'ipt_match_entry' und andere Argumente (wenn es welche gibt) sind die, die im IPT_MATCH_ITERATE() Makro angeboten werden.

IPT_ENTRY_ITERATE()

Diese Funktion macht einen Pointer zu einem Eintrag, der Gesamtgroesse der Tabelle der Eintraege und einer Funktion, die aufzurufen ist. Das erste Argument der Funktion ist 'struct ipt_entry', und andere Argumente (wenn es welche gibt) sind die, die im IPT_ENTRY_ITERATE() Makro angeboten werden.

ip_tables aus Sicht der Anwender

Userspace hat vier Operationen: Es kann die aktuelle Tabelle lesen, die Info lesen (Hook-Positionen und Groesse der Tabelle), die Tabelle ersetzen (und die alten Zaehler nehmen), und neue Zaehler einbauen.

So kann Userspace eine kleine Operation simulieren: Dies wird von der libiptc Library getan, welche die einfachen "add/delete/replace" semantics fuer Programme bietet.

Da diese Tabellen in den Kernel transferiert werden, stellt sich die Frage der Auslastung der Maschinen, die unterschiedliche Userspace- und Kernel- space Typ-Regeln haben (z.B. Sparc mit 32-Bit Userland). Diese Faelle werden mit dem Ueberschreiben der Definition von IPT_ALIGN fuer diese Platformen in 'libiptc.h' behandelt.

ip_tables verwenden und erforschen

Der Kernel beginnt die Untersuchung an der Stelle, die durch einen bestimmten Hook definiert ist. Diese Regel wird ueberprueft; wenn das 'struct ipt_ip' Element zutrifft, wird jedes 'struct ip_entry_match' der Reihe nach untersucht (es wird die match-Funktion entsprechend dem Treffer aufgerufen). Wenn die match-Funktion 0 liefert, endet die Iteration auf dieser Regel. Wenn sie den 'hotdrop' Parameter auf 1 setzt, wird das Paket ebenfalls sofort verworfen werden (Dies wird fuer einige verdaechtige Pakete benutzt, so wie in der TCP-match Funktion).

Wenn diese Iteration bis zum Ende durchlaeuft, werden die Counter erhoeht, `struct ipt_entry_target' wird untersucht: Wenn es ein Standardtarget ist, wird das 'verdict' Feld gelesen (negativ bedeutet ein Urteil ueber das Paket, positiv steht fuer ein Offset, zu dem man springen kann). Wenn die Antwort positiv ist und das Offset nicht das der naechsten Regel ist, wird die 'back' Variable gesetzt, und der vorige 'back' Wert wird in das 'comefrom' Feld der Regel geschrieben.

Fuer nicht-standard Targets wird die target Funktion aufgerufen: Sie liefert ein Urteil (nicht-standard Ziele koennen zu nichts springen, das wuerde den statischen Loop-Detection-Code brechen). Das Urteil kann IPT_CONTINUE sein, um bei der naechsten Regel weiterzumachen.

4.2 iptables erweitern

Weil ich so faul bin, ist iptables leicht erweiterbar. Dies ist hauptsaechlich der Versuch, die Arbeit auf die anderen zu schieben, worum es bei Open Source eigentlich geht (Bei Freier Software, wie RMS sagen wuerde, geht es um Freiheit, und ich sass bei einem seiner Gespraeche dabei, als ich das hier schrieb).

iptables erweitern besteht aus zwei Teilen: den Kernel erweitern, indem man ein neues Modul schreibt, und moeglicherweise das Anwendungsprogramm iptables erweitern, indem man eine neue Shared Library schreibt.

Der Kernel

Ein Kernelmodul selbst zu schreiben ist ziemlich einfach, wie Du an den Beispielen sehen kannst. Eine Sache, auf die Du achten musst, ist, dass Dein Code integriert werden muss: Es kann ein Paket von Anwenderseite her ankommen, waehrend ein anderes an einem Interrupt eintrifft. Tatsaechlich kann es ab 2.3.4 bei SMP vorkommen, dass ein Paket an einem Interrupt pro CPU eintrifft.

Folgende Funktionen musst Du kennen:

init_module()

Das ist der Eintrittspunkt des Moduls. Es liefert eine negative Zahl oder 0, wenn Netfilter es erfolgreich registriert.

cleanup_module()

Das ist der Austrittspunkt des Moduls; es soll die Registration beim Kernel wieder aufheben.

ipt_register_match()

Dies wird verwendet, um einen neuen, zutreffenden Typ zu registrieren. Du uebergibst ihm ein 'struct ip_match', was gewoehnlich als statische Variable deklariert wird (file-scope).

ipt_register_target()

Dies wird verwendet, um einen neuen Typ zu registrieren. Du uebergibst ihm ein 'struct_ip_target', was gewoehnlich als statische Variable deklariert wird (file-scope).

ipt_unregister_target()

Um die Registration des Ziel aufzuheben.

ipt_unregister_match()

Um die Registration des Treffers aufzuheben.

Eine Warnung zu trickreichen Dingen (wie das Verwenden von Countern) fuer Deine neuen Treffer oder Ziele. Auf SMP-Maschinen wird die vollstaendige Tabelle mit Hilfe von memcpy fuer jede CPU dupliziert: Wenn Du die Information wirklich zentral halten willst, solltest Du Dir die Treffermethode zu 'limit' ansehen.

Neue Match-Funktionen

Neue match-Funktionen werden gewoehnlich als alleinstehende Module geschrieben. Es ist moeglich, diese Module der Reihe nach zu erweitern, obwohl es normalerweise nicht noetig ist. Eine Moeglichkeit, um den Benutzern zu erlauben, direkt mit Deinem Modul zu sprechen, ist es, die `nf_register_sockopt' Funktion des Netfilter Rahmenwerks zu nutzen. Eine andere Moeglichkeit ist es, Symbole fuer andere Module zu exportieren, damit sie sich registrieren koennen; auf diese Weise machen es Netfilter und iptables.

Das Wesentliche Deiner neuen Match-Funktion ist 'struct ipt_match', was an 'ipt_register_match()' uebergeben wird. Diese Struktur hat die folgenden Felder:

list

Dieses Feld ist auf irgendeinen Bloedsinn gesetzt, z.B. '{NULL, NULL}'.

name

Dieses Feld ist der Name der Match-Funktion, auf die sich der Anwender bezieht. Der Name sollte dem Modul entsprechend gewaehlt werden (wenn der Name "mac" ist, muss das Modul "ipt_mac.o" heissen), damit es automatisch geladen werden kann.

match

Diese Feld ist ein Pointer auf eine Match-Funktion, welche aus einem skb, dem Pointer auf ein Eingangs- und ein Ausgangsgeraet (eins davon kann NULL sein, das haengt von dem Hook ab), einem Pointer auf die Match-Daten der zutreffenden Regel, der Groesse dieser Regel, dem IP-Offset (nicht Null steht fuer ein non-head Fragment), einem Pointer auf den Protokoll-Header (ich meine nur den IP-Header), der Gesamtlaenge der Daten (ich meine die Paketlaenge minus der IP-Header Laenge) und schliesslich einem Pointer auf die 'hotdrop' Variable besteht. Es soll nicht Null liefern, wenn die Regel auf das Paket zutrifft, und kann 'hotdrop' auf 1 setzen, wenn es Null liefert, um anzuzeigen, dass das Paket sofort verworfen werden soll.

checkentry

Dieses Feld ist ein Pointer auf eine Funktion, die die Bestimmungen fuer eine Regel untersucht; Wenn es 0 liefert, wird die Regel des Benutzers nicht akzeptiert werden. Zum Beispiel wird der "tcp" Match Typ nur TCP-Pakete akzeptieren, wenn also der 'struct ipt_ip' Teil der Regel nicht spezifiziert, dass das Protokoll TCP sein muss, wird Null geliefert. Das Tablename Argument erlaubt Deinem Match, zu kontrollieren, in welchen Tabellen es eingesetzt werden kann, und `hook_mask' ist eine Bitmaske von Hooks, von denen diese Regel moeglicherweise aufgerufen wurde: Wenn Dein Match von einem der Netfilter-Hooks keinen Sinn macht, kannst Du das hier umgehen.

destroy

Dieses Feld ist ein Pointer auf eine Funktion, die aufgerufen wird, wenn ein Eintrag, der dieses Match verwendet, geloescht wird. Dies erlaubt Dir auch, Resourcen in checkentry dynamisch zuzuordnen und sie hier wieder aufzuraeumen.

me

Dieses Feld ist auf '&__this_module' gesetzt, welches Deinem Modul einen Pointer gibt. Es bewirkt, dass der usage-count steigt und faellt, je nachdem, ob Regeln dieses Typs erstellt oder geloescht werden. Das bewahrt den Benutzer davon, das Modul zu entfernen (und somit das cleanup_module aufzurufen), wenn sich eine Regel darauf bezieht.

Neue Targets

Neue Targets werden auch gewoehnlich als alleinstehende Module geschrieben. Die Diskussionen der oberen Sektion 'Neue Match Funktionen' koennen hier aehnlich angewandt werden.

Das Wesentliche Deines neuen Targets ist 'struct ipt_target', das an `ipt_register_target()' uebergeben wird. Diese Struktur hat die folgenden Felder:

list

Dieses Feld ist auf irgendeinen Bloedsinn gesetzt, z.B. '{NULL, NULL}'.

name

Dieses Feld ist der Name der Ziel-Funktion, auf die sich der Anwender bezieht. Der Name sollte dem Modul entsprechend gewaehlt werden (wenn der Name "REJECT" ist, muss das Modul "ipt_REJECT.o" heissen), damit es automatisch geladen werden kann.

target

Diese Feld ist ein Pointer auf eine Ziel-Funktion, welche aus einem skbuff, dem Pointer auf ein Eingangs- und ein Ausgangsgeraet (eins davon kann NULL sein) einem Pointer auf die Ziel-Daten, der Groesse der Ziel-Daten, und der Position der Regel in der Tabelle besteht. Die Target-Funktion liefert eine nicht-negative, absolute Position, zu der zu springen ist, oder ein negatives Urteil (was das negierte Urteil minus 1 ist).

checkentry

Dieses Feld ist ein Pointer auf eine Funktion, die die Bestimmungen fuer eine Regel ueberprueft; wenn das 0 liefert, wird die Regel des Benutzers nicht akzeptiert werden.

destroy

Dieses Feld ist ein Pointer auf eine Funktion, die aufgerufen wird, wenn ein Eintrag, der dieses Match verwendet, geloescht wird. Dies erlaubt Dir auch, Resourcen in checkentry dynamisch zuzuordnen und sie hier wieder aufzuraeumen.

me

Dieses Feld ist auf '&__this_module' gesetzt, welches Deinem Modul einen Pointer gibt. Es bewirkt, dass der usage-count steigt und faellt, je nachdem, ob Regeln dieses Typs erstellt oder geloescht werden. Das bewahrt den Benutzer davon, das Modul zu entfernen (und somit das cleanup_module aufzurufen), wenn sich eine Regel darauf bezieht.

Neue Tabellen

Wenn Du willst, kannst Du fuer einen speziellen Zweck eine neue Tabelle erstellen. Um das zu tun, rufst Du 'ipt_register_table()' mit `struct ipt_table' auf, was die folgenden Felder hat:

list

Dieses Feld ist auf irgendeinen Bloedsinn gesetzt, z.B. '{NULL, NULL}'.

name

Dieses Feld ist der Name der Ziel-Funktion, auf die sich der Anwender bezieht. Der Name sollte dem Modul entsprechend gewaehlt werden (wenn der Name "nat" ist, muss das Modul "ipt_nat.o" heissen), damit es automatisch geladen werden kann.

table

Dies ist ein voll ausgenutztes 'stuct ipt_replace', wie es vom Anwender genutzt werden kann, um eine Tabelle zu ersetzen. Der `counters' Pointer sollte auf NULL gesetzt sein. Diese Datenstruktur kann als '__initdata' deklariert werden, damit es nach dem Booten verworfen werden kann.

valid_hooks

Dies ist eine Bitmaske der IPv4 Netfilter Hooks, mit der Du die Tabellen betreten wirst: Sie wird verwendet, um zu ueberpruefen, dass diese Eintrittspunkte gueltig sind und um die moeglichen Hooks fuer die ipt_match und die ipt_target 'checkentry() Funktionen zu berechnen.

lock

Dies ist das read-write Lock fuer die vollstaendige Tabelle, initiali- siere es auf RW_LOCK_UNLOCKED.

private

Dies wird vom ip_tables Code intern verwendet.

Anwendertools

Jetzt, wo Du Dein nettes cooles Kernelmodul geschrieben hast, moechtest Du vielleicht seine Optionen von Anwenderseite her kontrollieren. Lieber, als fuer jede Erweiterung von iptables eine abgespaltene Version zu haben, benutze ich eine Technologie der spaeten 90er: Furbies. Sorry, ich meine shared Libraries.

Gewoehnlich benoetigen neue Tabellen keine Erweiterungen von iptables: Der Benutzer verwendet einfach die '-t' Option, um auf eine neue Tabelle zugreifen zu koennen.

Die shared Libraries sollten eine '_init()' Funktion haben, die beim Laden automatisch aufgerufen wird: Das moralische Gegenstueck zu der `init_module()' Funktion der Kernelmodule. Dies sollte 'register_match()' oder 'register_target()' aufrufen, abhaengig davon, ob Deine shared Library ein neues Match oder ein neues Ziel ist.

Du brauchst nur dann eine shared Library, wenn Du einen Teil der Struktur initialisieren oder eine zusaetzliche Option bieten willst. Das Ziel `REJECT' zum Beispiel benoetigt nichts dergleichen, hat also auch keine shared Library.

Es gibt nuetzliche Funktionen, die in 'iptables.h.' beschrieben werden, besonders:

check_inverse()

Ueberprueft, ob eins der Argumente ein '!' ist, und wenn ja, setzt es das 'invert' Flag, wenn das noch nicht geschehen sein sollte. Wenn sie 'true' liefert, solltest Du optind wie im Beispiel erhoehen.

string_to_number()

Konvertiert einen String zu einer Zahl in der gegebenen Groessenordnung. Sie liefert -1, wenn der String ungueltig ist oder ausserhalb der Groessenordnung liegt.

exit_error()

Diese Funktion sollte aufgerufen werden, wenn ein Fehler gefunden wird. Das erste Argument ist gewoehnlich 'PARAMETER_PROBLEM', was bedeutet, dass der User die Kommandozeile nicht richtig benutzt hat.

Neue Match-Funktionen

Die _init() Funktion Deiner shared Library uebergibt einen Pointer auf ein statisches 'struct iptables_match' an 'register_match()', was die folgenden Felder hat:

next

Dieser Pointer wird verwendet, um eine verlinkte Liste von Matches zu erstellen (wie sie fuer die listing-Regeln verwendet wird). Am Anfang sollte er auf NULL gesetzt sein.

name

Der Name der Match-Funktion. Er sollte entsprechend dem Namen der Library gewaehlt sein (z.B. "tcp" fuer 'libipt_tcp.so').

version

Dies wird gewoehnlich auf den NETFILTER_VERION Makro gesetzt: Es stellt sicher, dass das iptables Binary nicht aus Versehen die falsche Library auswaehlt.

size

Die Groesse der Match-Daten fuer dieses Match; damit Du Dir keine Sorgen machen musst, wird sie von IPT_ALIGN() automatisch aufgerundet werden.

userspacesize

Fuer einige Matches aendert der Kernel intern einige Felder (z.B. im Fall von 'limit'). Das bedeutet, dass ein einfaches 'memcmp' nicht mehr ausreicht, um zwei Regeln zu vergleichen (benoetigt fuer die delete-matching-rule Funktionalitaet). Wenn dies der Fall ist, plaziere all die Felder, die sich nicht aendern, an den Anfang der Struktur, und lege die Groesse der sich nicht aendernden Felder hier ab.

help

Eine Funktion, die die Synopsis der Option ausdruckt.

init

Dies kann verwendet werden, um etwas mehr Platz (wenn vorhanden) in der ipt_entry_match Stuktur zu schaffen, und um jegliche nfcache Bits zu setzen; Wenn Du etwas untersuchst, das Du nicht ausdruecken kannst, indem Du den Inhalt von '/linux/include/ netfilter_ipv4.h' verwendest, wende im NFC_UNKNOWN Bit einfach OR an. Dies wird vor 'parse()' aufgerufen werden.

parse

Dies wird aufgerufen, wenn eine nicht erkannte Option auf der Kommandozeile gesehen wird: Es sollte nicht NULL liefern, wenn diese Option tatsaechlich fuer Deine Library bestimmt war. 'invert' liefert `true', wenn bereits ein '!' gesehen wurde. Der 'flags' Pointer ist fuer die exklusive Verwendung Deiner Match-Library und wird gewoehnlich dazu verwendet, eine Bitmaske von bestimmten Optionen zu speichern. Vergiss nicht, das nfcache Feld auszurichten. Wenn noetig, kannst Du die Groesse der 'ipt_entry_match' Struktur erhoehen, indem Du Sie erneut zuordnest, dann musst Du aber auch sicherstellen, dass die Groesse durch den IPT_ALIGN Makro gehen muss.

final_check

Dies wird nach ueberstandener Kommandozeileneingabe aufgerufen. Ihm werden die fuer Deine Library reservierten Integer-'flags' uebergeben. Dies gibt Dir die Gelegenheit, zu ueberpruefen, ob alle benoetigten Optionen angegeben wurden, zum Beispiel 'exit_error()' aufzurufen, wenn dies der Fall sein sollte.

print

Dies wird von Chain-Listing-Code verwendet, um jegliche zusaetzliche Match-Information einer Regel (wenn vorhanden) ausdrucken (zum Standard Output). Das numerische Flag wird gesetzt, wenn der User es mit der '-n' Option angegeben hat.

save

Dies ist das Gegenteil von parse: Es wird von 'iptables-save' verwendet, um die Optionen, die die bestimmte Regel erstellt haben, zu reproduzieren.

extra_opts

Dies ist eine NULL-terminierte Liste von Extra-Optionen, die Deine Library anbietet. Dies wird vermischt mit den jetzigen Optionen und so an getopt_long uebergeben; Fuer Details siehe Manpage.

Es gibt noch extra Elemente am Ende dieser Struktur, die iptables intern verwendet: Du brauchst sie hier nicht zu setzen.

Neue Targets

Die _init() Funktionen Deiner shared Libraries uebergeben 'register_target()' einen Pointer auf ein statisches 'struct iptables_target', welches aehnliche Felder hat wie die iptables_match Strukturen, die oben detailliert beschrieben wurden.

Manchmal braucht ein Target keine Library fuer den Userspace; Eine triviale musst Du sowieso erstellen: Es gab frueher zu viele Probleme mit schlecht plazierten Libraries.

`libiptc' verwenden

libiptc ist die Kontroll Library fuer iptables, entworfen, um Regeln im iptables Kernelmodul aufzulisten und zu manipulieren. Waehrend seine jetzige Verwendung sich auf das iptables Tool beschraenkt, macht es das Schreiben neuer Tools recht einfach. Um diese Funktionen nutzen zu koennen, musst Du root sein.

Die Kerneltabellen selbst bestehen nur aus ein paar Tabellen mit Regeln und einer Anzahl von Nummern, die die Eintrittspunkte repraesentieren. Die Namen der Ketten ("INPUT") werden als Abstraktion von der Library unterstuetzt. Benutzerdefinierte Ketten werden benannt, indem ein Fehlerverweis vor dem Beginn der benutzerdefinierten Kette eingefuegt wird, der den Kettennamen in einer speziellen Datensektion des Targets enthaelt (Die Positionen der eingebauten Ketten werden durch die drei Eintrittspunkte der Tabelle definiert).

Wenn 'iptc_init()' aufgerufen wird, wird die Tabelle einschliesslich der Counter gelesen. Diese Tabelle wird von der `iptc_insert_entry()', `iptc_replace_entry()', `iptc_append_entry()', `iptc_delete_entry()', `iptc_delete_num_entry()', `iptc_flush_entries()', `iptc_zero_entries()', `iptc_create_chain()' `iptc_delete_chain()', und `iptc_set_policy()' Funktion manipuliert.

Die Aenderungen an der Tabelle werden nicht eher zurueckgeschrieben, bis die 'iptc_commit()' Funktion aufgerufen wird. Das bedeutet, dass es fuer zwei Benutzer der Library gleichzeitig moeglich ist, auf derselben Kette zu operieren, um jeweils der Schnellere zu sein; Um das zu verhindern, ist Locking noetig, was aber zur Zeit nicht eingesetzt wird.

Es gibt kein Wettrennen mit bei Countern, wie auch immer; Counter werden so in den Kernel zurueckaddiert, dass eine Erhoehung der Zaehler zwischen dem Lesen und dem Schreiben der Tabelle immernoch in der neuen Tabelle auftauchen wird.

Es gibt verschiedene Hilfsfunktionen:

iptc_first_chain()

Diese Funktion liefert den Namen der ersten Kette in dieser Tabelle.

iptc_next_chain()

Diese Funktion liefert den Namen der naechsten Kette in dieser Tabelle: NULL bedeutet, es gibt keine weiteren Ketten.

iptc_builtin()

Liefert true, wenn der Name der gegebenen Kette der Name einer eingebauten Kette ist.

iptc_first_rule()

This returns a pointer to the first rule in the given chain name: NULL for an empty chain.

iptc_next_rule()

Dies liefert einen Pointer auf die erste Regel im Namen der gegebenen Kette: NULL fuer eine leere Kette.

iptc_get_target()

Dies gibt das Target der gegebenen Regel. Wenn es kein erweitertes Target ist, wird der Name dieses Targets geliefert. Wenn es ein Sprung auf eine andere Kette ist, wird der Name dieser Kette geliefert. Wenn es ein Urteil (z.B. DROP) ist, wird dieser Name geliefert. Wenn die Regel kein Ziel hat (und eine accountig-style Regel), wird ein leerer String geliefert.

Beachte, dass Du diese Funktion verwenden solltest, anstatt den Wert des 'verdict' Felds der ipt_entry Struktur direkt zu benutzen, da sie die oben genannten weiteren Interpretationen des Standard Urteils bietet.

iptc_get_policy()

Dies liefert die Policy einer eingebauten Kette und fuellt das Counter- argument mit den Treff-Statistiken dieser Policy.

iptc_strerror()

Diese Funktion liefert eine aussagekraeftigere Erklaerung von gescheitertem Code in der iptc Library. Wenn eine Funktion scheitert, wird es errno setzen: Dieser Wert kann an iptc_strerror() uebergeben werden, um eine Fehlermeldung zu erhalten.

4.3 NAT verstehen

Willkommen zu Network Address Translation im Kernel. Beachte, dass die angebotene Infrastruktur mehr fuer Vollstaendigkeit als fuer Effizienz entworfen wurde und dass weitere Entwicklungen die Effiziens erhoehen koennen. Im Moment bin ich froh, dass es ueberhaupt funktioniert.

NAT wird aufgeteilt in Connection Tracking (was die Pakete ueberhaupt nicht veraendert) und in den NAT Code selbst. Connection Tracking kann auch als iptables Modul verwendet werden, um subtile Unterscheidungen von Zustaenden zu machen, die NAT egal sind.

Connection Tracking

Connection Tracking benutzt als Hooks die von der Prioritaet hoehergestellten NF_IP_LOCAL_OUT und NF_IP_PRE_ROUTING, um Pakete zu sehen, bevor sie das System betreten.

Das ncft Feld in skb ist ein Pointer auf einen der infos[] Arrays im Inneren von struct ip_conntrack. So koennen wir den Zustand von skb durch das Element bestimmen, auf welches dieser Array zeigt: Dieser Pointer erkennt beides, die State Struktur und die Beziehung des skb zu diesem Zustand.

Der beste Weg, das 'nfct' Feld zu lesen, ist es, die 'ip_conntrack_get()' Funktion aufzurufen, die NULL liefert, wenn es nicht gesetzt ist, oder den Connection Pointer. Ausserdem fuellt sie ctinfo, was die Beziehung des Paket zu der Verbindung beschreibt. Dieser numerierte Typ hat folgende Werte:

IP_CT_ESTABLISHED

Das Paket ist Teil einer in Originalrichtung aufgebauten Verbindung.

IP_CT_RELATED

Das Paket steht in Zusammenhang mit der Verbindung und geht in die Originalrichtung.

IP_CT_NEW

Das Paket versucht, eine neue Verbindung aufzubauen (offensichtlich geht es in die Originalrichtung).

IP_CT_ESTABLISHED + IP_CT_IS_REPLY

Das Paket ist Teil einer aufgebauten Verbindung, und zwar in Antwort- richtung.

IP_CT_RELATED + IP_CT_IS_REPLY

Das Paket steht in Zusammenhang mit der Verbindung und geht in die Antwortrichtung.

Ein Antwortpaket kann also als solches indentifiziert werden, indem man auf >= IP_CT_IS_REPLY testet.

4.4 NAT/Connection Tracking erweitern

Diese Rahmenwerke wurden entworfen, um jegliche Art von Protokollen und verschiedenen Mapping-Arten unterzubringen. Manche dieser Mapping-Arten koennen sehr spezifisch sein, so wie load-balancing oder fail-over Mappings.

Intern konvertiert Connection Tracking ein Paket zu einem 'Tupel', der den interessanten Teil des Pakets repreasentiert, bevor nach darauf zutreffenden Regeln gesucht wird. Dieser Tupel hat einen manipulierbaren Teil und einen nicht-manipulierbaren Teil; "src" und "dst" genannt, da dies die Ansicht des ersten Pakets in der Source NAT Welt ist (in der Destination NAT Welt wuerde es ein Antwortpaket sein). Die Tupel aller Pakete in demselben Paket-Stream in dieser Richtung sind gleich.

Das Tupel eine TCP-Pakets enthaelt z.B. den manipulierbaren Teil (Quell- IP und Quellport) und den nicht-manipulierbaren Teil (Ziel-IP und Zielport) Der manipulierbare und der nicht-manipulierbare Teil muessen trotzdem nicht vom selben Typ sein; Das Tupel eines ICMP-Pakets enthaelt den manipulierbaren Teil (Quell-IP und ICMP-ID) und den nicht-manipulierbaren Teil (Ziel-IP und ICMP Typ und Code).

Jedes Tupel hat ein Inverses, naemlich das Tupel des Antwortpakets in dem Stream. Das Inverse eines ICMP Ping-Pakets (icmp id 1234, von 192.168.1.1 an 1.2.3.4) ist zum Beispiel ein Ping Antwort Paket (icmp id 1234, von 1.2.3.4 an 192.168.1.1).

Diese Tupel, die von 'struct ip_conntrack_tuple' repraesentiert werden, werden haeufig verwendet. Tatsaechlich ist das, zusammen mit dem Hook, an dem das Paket einging (was einen Effekt auf die zu erwartende Art der Manipulation hat) und dem beteiligten Device, die komplette Information ueber das Paket.

Die meisten Tupel sind Teil von 'struct ip_conntrack_tuple_hash', was einen doppelt verlinkten Listeneintrag und einen Pointer auf die Verbindung, zu der das Paket gehoert, hinzufuegt.

Eine Verbindung wird von 'struct ip_conntrack' repraesentiert: Es hat zwei 'struct ip_conntrack_tuple_hash' Felder: Eins, was sich auf die Richtung des Originalpakets bezieht (tuplehash[IP_CT_DIR_ORIGINAL]), und eins, was sich auf die Antwortrichtung des Pakets bezieht (tuplehash[IP_CT_DIR_REPLY]).

Wie auch immer, das erste, was der NAT Code tut, ist nachzusehen, ob der Connection Tracking Code es geschafft hat, ein Tupel zu extrahieren und ein bestehende Verbindung zu finden, indem er in das nfct Feld von skbuff sieht; Das sagt uns, ob es ein Versuch ist, eine neue Verbindung aufzubauen, oder, wenn nicht, in welche Richtung das ganze geht; im letzteren Fall werden die Manipulationen ausgefuehrt, die vorher fuer diese Verbindung bestimmt wurden.

Wenn es der Anfang einer neuen Verbindung war, suchen wir nach einer Regel fuer dieses Tupel, indem wir den Standard-Mechanismus von iptables verwenden. Trifft eine Regel zu, wir sie verwendet, um Manipulationen sowohl fuer die Original-, als auch fuer die Antwort- richtung auszufuehren; dem Connection Tracking Code wird mitgeteilt, dass die zu erwartenden Antwort sich geaendert hat. Dann wird das Paket wie oben beschrieben veraendert.

Wenn es keine Regel gibt, wird eine 'Null' Bindung erstellt: Das mappt das Paket gewoehnlich nicht, stellt aber sicher, dass wir keinen anderen Stream ueber den existierenden mappen. Manchmal kann die Null-Bindung nicht erstellt werden, weil wir bereits einen existierenden Stream daruebergemappt haben. In diesem Fall koennte die Pre-Protocol Manipulation versuchen, ihn zu remappen, obwohl es vom Namen her keine Null-Bindung ist.

Standard NAT-Targets

NAT Targets sind wie alle anderen iptables Target-Erweiterungen, ausser, dass sie darauf bestehen, nur in der 'nat' Tabelle verwendet zu werden. Sowohl SNAT als auch DNAT Targets verwenden 'struct ip_nat_multi_range' als Extradaten; dies wird benutzt, um einen Bereich von Adressen zu bestimmen, auf denen ein Mapping gueltig ist. Ein Element dieses Bereichs, `struct ip_nat_range', besteht aus einer inklusiven minimalen und maximalen IP-Adresse und aus einem inklusiven minimalen und maximalen Wert zur Protokollbestimmung (zum Beispiel TCP Ports). Es gibt auch Platz fuer Flags, die uns sagen, ob die IP-Adresse gemappt werden kann (manchmal wollen wir nur den protokoll-spezifischen Teil eines Tupels mappen, nicht die IP), oder ein anderes, das uns sagt, dass der protokoll-spezifische Teil des Bereichs 'valid' ist.

Ein Multi-Range ist ein Array aus diesen 'struct ip_nat_range' Elementen; das bedeutet, dass ein Bereich "1.1.1.1-1.1.1.2 Ports 50-55 AND 1.1.1.3 Port 80" sein kann. Jedes Element wird zu dem Bereich dazuaddiert (Union, fuer die, die diese Theorie moegen).

Neue Protokolle

Der Kernel von innen

Ein neues Protokoll zu implementieren bedeutet zuerst, zu entscheiden, was der manipulierbare und was der nicht-manipulierbare Teil des Tupels sein sollen. Jedes Teil des Tupels hat die Eigenschaft, dass es den Stream eindeutig identifiziert. Der manipulierbare Teil des Tupel ist der Teil, mit dem Du NAT machen kannst: Fuer TCP ist das der Quellport, fuer ICMP ist das die ICMP id; etwas, das das "Stream Identifier" benutzt werden kann. Der nicht-manipulierbare Teil ist der Rest des Pakets, der den Stream zwar auch eindeutig identifiziert, mit dem wir aber nicht spielen koennen (z.B. TCP Zielport, ICMP Typ).

Sobald Du Dich dazu entschieden hast, kannst Du eine Erweiterung fuer den Connection Tracking Code schreiben und die 'ip_conntrack_protocol' Struktur, die Du an 'ip_conntrack_register_protocol()' uebergeben musst, ausbauen.

Die Felder von 'struct ip_conntrack_protocol' sind:

list

Setz es auf '{NULL, NULL}'; verwendet, um sich der Liste zu naehern.

proto

Die Nummer Deines Protokolls, siehe '/etc/protocols'.

name

Der Name Deines Protokolls. Das ist der Name, den der Anwender sehen wird; meistens ist es das beste, den entsprechenden Namen aus `/etc/protocols' zu verwenden.

pkt_to_tuple

Die Funktion, die die protokollspezifischen Teile des Tupels ausfuellt, sobald ihr das Paket uebergeben wird. Der 'datah' Pointer zeigt auf den Anfang des Headers (direkt hinter der IP-Adresse), und datalen ist die Laenge des Pakets. Es liefert NULL, wenn das Paket nicht lang genug ist, um Header-Informationen zu enthalten; datalen wird (gezwungenermassen) immer mindestens 8 Byte lang sein.

invert_tuple

Diese Funktion wird einfach dazu verwendet, den protokollspezifischen Teil des Tupels so zu aendern, wie eine Antwort auf dieses Paket aussehen wuerde.

print_tuple

Diese Funktion wird verwendet, um den protokollspezifischen Teil eines Tupels auszudrucken; gewoehnlich wird es mit sprintf() in den mitgegebenen Buffer geschrieben. Geliefert wird die Anzahl von verwendeten Buffer-Zeichen. Dies wird verwendet, um den Status des /proc Eintrags zu drucken.

print_conntrack

Diese Funktion wird benutzt, um den privaten Teil der Conntrack Struktur (wenn vorhanden) zu drucken. Sie wird auch verwendet, um den Status unter /proc zu drucken.

packet

Diese Funktion wird aufgerufen, wenn ein Paket erkannt wird, das Teil einer aufgebauten Verbindung ist. Du bekommst einen Pointer auf die Conntrack Struktur, den IP-Header, die Laenge und ctinfo. Du gibt ein Urteil ueber das Paket zurueck (gewoehnlich NF_ACCEPT), oder -1 wenn das Paket kein gueltiger Teil der Verbindung ist. Innerhalb dieser Funktion kannst Du, wenn Du willst, diese Verbindung loeschen, Du solltest aber folgendes Idiom verwenden, um races zu vermeiden:

if (del_timer(&ct->timeout))
        ct->timeout.function((unsigned long)ct);

new

Diese Funktion wird aufgerufen, wenn ein Paket zum ersten Mal eine Verbindung aufbaut; es gibt kein ctinfo arg, da das erste Paket der Definition nach ctinfo IP_CT_NEW ist. Wenn es beim Verbindungsaufbau scheitert, liefert die Funktion 0, oder im selben Moment ein Connection Timeout.

Sobald Dein fertig bist und getestet hast, dass Du Dein neues Protokoll einsetzen kannst, ist es an der Zeit, NAT beizubringen, wie es zu uebersetzen ist. Dies bedeutet, ein neues Modul zu schreiben; eine Erweiterung zum NAT-Code und die 'ip_conntrack_protocol' Struktur, die Du an 'ip_conntrack_register_protocol()' uebergeben musst.

list

Setz es auf '{NULL, NULL}'; verwendet, um sich der Liste zu naehern.

name

Der Name Deines Protokolls. Das ist der Name, den der Anwender sehen wird; meistens ist es das beste, den entsprechenden Namen aus `/etc/protocols' zu verwenden, um auto-loading zu ermoeglichen, wie wir spaeter noch sehen werden.

protonum

Die Nummer Deines Protokolls, siehe '/etc/protocols'.

manip_pkt

Das ist die andere Haelfte der von Conntection Tracking benutzten `ptk_to_tuple' Funktion: Stell sie Dir als "tuple_to_ptk" vor. Trotzdem gibt es ein paar Unterschiede: Du bekommst einen Pointer auf den Anfang des IP-Headers, und die Gesamtlaenge des Pakets. Das liegt daran, dass manche Protokolle (UDP, TCP) den IP-Header kennen muessen. Dir wird das 'ip_nat_tuple_manip' Feld des Tupels gegeben (ich meine das "src" Feld) statt des ganzen Tupels und der Art der Manipulation, die auszufuehren ist.

in_range

Diese Funktion wird verwendet, um zu sagen, ob manipulierbarer Teil des gegebenen Tupels im gegebenen Bereich liegt. Diese Funktion ist ein bisschen trickreich: Wir erhalten die Art der Manipulation, die auf das Tupel angewandt wurde, die uns sagt, wie wir den Bereich interpretieren koennen (ist es ein Quell- oder ein Zielbereich, auf den wir abzielen?).

Diese Funktion wird benutzt, um zu ueberpruefen, ob ein existierendes Mapping uns in den richtigen Bereich bringt, ausserdem noch, um zu ueberpruefen, ob vielleicht ueberhaupt keine Manipulation noetig ist.

unique_tuple

Diese Funktion ist das Herzstueck von NAT: Wir erhalten ein Tupel und einen Bereich und sollen den Per-Protocol Teil des Tupels veraendern, um es im Bereich zu plazieren und somit einzigartig zu machen. Wenn wir kein unbenutztes Tupel in diesem Bereich finden, wird 0 geliefert. Ausserdem bekommen wir einen Pointer auf die Conntrack Struktur, welcher fuer ip_nat_user_tuple() benoetigt wird.

Gewoehnlich wird einfach der Per-Protocol Teil des Tupels durch den Bereich iteriert, und 'ip_nat_user_tupel()' wird solange ueberprueft, bis es einmal false liefert.

Beachte, dass der Null-Mapping Fall bereits ueberprueft wurde: Entweder liegt er ausserhalb des gegebeben Bereichs, oder er ist schon besetzt.

Wenn IP_NAT_RANGE_PROTO_SPECIFIED gesetzt ist, bedeutet dies, dass der Anwender NAT, und nicht NAPT macht: Etwas Vernuenftiges mit dem Bereich tun. Wenn kein Mapping erwuenscht ist (In TCP zum Beispiel sollte kein Ziel-Mapping den TCP-Port aendern, wenn es nicht ausdruecklich verlangt wird), wird 0 geliefert.

print

Given a character buffer, a match tuple and a mask, write out the per-protocol parts and return the length of the buffer used.

print_range

Mit einem gegebenen Charakter-Buffer, einem Match-Tupel und einer Maske wird dies den Per-Protocol Teil ausdrucken und die Laenge des Buffers liefern.

Neue NAT-Targets

Das ist der wirklich interessante Teil. Du kannst neue NAT-Targets schreiben, die einen neuen Mapping-Typ bieten: Zwei Extra-Targets werden im Standard Package geliefert: MASQUERADE und REDIRECT. Diese illustrieren recht einfach das Potential und die Macht des Schreibens neuer NAT-Targets.

Sie werden genauso wie alle anderen iptables Targets geschrieben, nur intern werden sie 'ip_nat_setup_info()' aufrufen und die Verbindung extrahieren.

Protokoll-Hilfen fuer TCP und UDP

Dieser Teil befindet sich noch in der Entwicklung.

4.5 Understanding Netfilter

Netfilter ist ziemlich simpel und wurde in den vorangegangenen Sektionen recht ausfuehrlich beschrieben. Wie auch immer, manchmal ist es notwendig, weiter zu gehen als die NAT oder die ip_tables Infrastruktur bietet, oder vielleicht moechtest Du sie auch vollstaendig ersetzen.

Ein wichtiger Punkt bei Netfilter (Naja, in der Zukunft) ist Caching. Jedes skb hat ein 'nfcache' Feld: Eine Bitmaske, die sagt, welche Felder im Header untersucht wurden und ob das Paket veraendert wurde oder nicht. Die Idee ist, dass auf jeden Netfilter Hook in den dazu relevanten Bits eine OR-Verknuepfung angewandt werden kann, so dass wir spaeter ein Cache-System schreiben koennen, das clever genug sein wird, um zu erkennen, wann Pakete nicht an Netfilter gereicht werden muessen.

Die wichtigsten Bits sind NFC_ALTERED, was bedeutet, dass das Paket veraendert wurde (Dies wird fuer den NF_IP_LOCAL_OUT Hook von IPv4 bereits verwendet, um geaenderte Pakete erneut zu routen), und NFC_UNKNOWN, was aussagt, dass Caching nicht angewandt werden sollte, da einige Eigenschafte untersucht wurden, die nicht ausgedrueckt werden konnten. Im Zweifelsfall solltest Du das NFC_UNKNOWN Flag in das nfcache Feld von skb in Deinem Hook setzen.

4.6 Neue Netfilter-Module schreiben

Netfilter-Schnittstellen benutzen

Um Pakete im Kernel zu empfangen/behandeln, kannst Du einfach ein Modul schreiben, welches beim "Netfilter Hook" registriert. Im Grunde ist das ein Ausdruck von Interesse an einem gegebenen Punkt; der genaue Punkt ist protokollspezifisch und wird in protokollspezifischen Netfilter-Headern, wie "netfilter_ipv4.h", definiert.

Um Netfilter Hooks zu registrieren (und das wieder aufzuheben), benutzt Du die 'nf_register_hook' und die 'nf_unregister_hook' Funktionen. Beide benoetigen einen Pointer auf 'struct nc_hook_ops', welchen Du wie folgt benutzt:

list

Setz es auf '{NULL, NULL}'; verwendet, um sich der Liste zu naehern.

hook

Die Funktion, die aufgerufen wird, wenn ein Paket auf diesen Hook- Punkt trifft. Deine Funktion muss NC_ACCEPT, NC_DROP oder NC_QUEUE liefern. Wenn NC_ACCEPT geliefert wird, wird der naechste an diesen Punkt angehaengte Hook aufgerufen werden. Wenn NC_DROP geliefert wird, wird das Paket verworfen werden. Wenn NC_QUEUE geliefert wird, wird das Paket gequeued werden. Wenn Du einen Pointer auf einen skb Pointer erhaelst, kannst Du skb, wenn Du willst, vollstaendig ersetzen.

flush

Zur Zeit unbenutzt: Es wurde entworfen, um Paket-Treffer weiterzureichen, wenn der Cache geloescht wird. Vielleicht wird es niemals implementiert werden: Setz es auf NULL.

pf

Die Protokollfamilie, zum Beispiel 'PF_INET' fuer IPv4.

hooknum

Die Nummer des Hooks, fuer den Du Dich interessierst, z.B. `NC_IP_LOCAL_OUT'.

Eingereihte Pakete behandeln

Dies ist die zur Zeit von 'ip_queue' verwendete Schnittstelle; Du kannst hier registrieren, um Pakete fuer ein gegebenes Protokoll zu behandeln. Hier herrscht eine aehnliche Semantik wie bei der Registrierung fuer einen Hook, ausser, dass Du eine Behandlung des Pakets blockieren kannst. Ausserdem siehst Du nur Pakete, fuer die ein Hook mit 'NC_QUEUE' geant- wortet hat.

Die zwei Funktionen, die verwendet werden, um Interesse an gequeueten Paketen zu registrieren, heissen 'nc_register_queue_handler()' und `nf_unregister_queue_handler()'. Die Funktion, die Du registrieren wirst, wird mit dem 'void *' Pointer aufgerufen werden, mit dem Du sie an 'nf_register_queue_handler()' uebergeben hast.

Wenn niemand registriert ist, ein Paket zu behandeln, ist das Liefern von NF_QUEUE aequivalent zum Liefern von NF_DROP.

Die Pakete werden eingereiht werden, sobald Du Dich dafuer registriert hast. Du kannst mit ihnen tun, wasimmer Du willst, wenn Du aber fertig bist mit ihnen, musst Du 'nf_reinject()' aufrufen (benutz nicht einfach nur 'kfree_skb()'). Wenn Du ein skb reinjizierst, uebergibst Du ihm das skb, das dem Queue-Handler gegebene 'struct nf_info' und ein Urteil: NF_DROP verwirft es, NF_ACCEPT laesst es weiter durch die Hooks iterieren, NF_QUEUE reiht sie erneut ein und NF_REPEAT bewirkt, dass der Hook, der das Paket eingereiht hat, erneut konsultiert wird (Pass auf unendliche Loops auf!).

Du kannst in 'struct nf_info' sehen, um hilfreiche Informationen ueber das Paket zu bekommen, z.B. ueber die Schnittstelle oder den betreffenden Hook.

Kommandos vom Anwender empfangen

Netfilter Komponenten wollen mit dem Anwender interagieren. Die Methode hierfuer ist die Verwendung des setsockopt Mechanismus. Beachte, dass jedes Protokoll modifiziert werden muss, um nf_setsockopt() fuer nicht verstandene setsockopt Nummern (und nf_getsockopt() fuer getsockopt Nummern) aufrufen zu koennen, und das bis jetzt nur IPv4, IPv6 und DECnet modifiziert wurden.

Wir verwenden eine mittlerweile bekannte Technik: Wir registrieren ein `struct nf_sockopt_ops', indem wir nf_register_sockopt() aufrufen. Die Felder dieser Struktur sind wie folgt:

list

Setz es auf '{NULL, NULL}'; verwendet, um sich der Liste zu naehern.

pf

Die Protokollfamilie, die Du behandelst, zum Beispiel PF_INET.

set_optmin

und

set_optmax

Diese bestimmen den (exklusiven) Bereich der verwendeten setsockopt Nummern. 0 und 0 bedeutet also, dass Du keine setsockopt Nummern benutzt.

set

This is the function called when the user calls one of your setsockopts. You should check that they have NET_ADMIN capability within this function.

get_optmin

and

get_optmax

These specify the (exclusive) range of getsockopt numbers handled. Hence using 0 and 0 means you have no getsockopt numbers.

get

Das ist die Funktion, die aufgerufen wird, wenn der User eine Deiner getsockopts aufruft. Du solltest ueberpruefen, ob es eine NET_ADMIN Capability in dieser Funktion gibt.

Die zwei letzten Felder werden intern verwendet.

4.7 Behandlung von Paketen aus Sicht der Anwender

Durch das Verwenden der libipq Library und des 'ip_queue' Moduls kann nun fast alles, was im Kernel getan werden kann, auch von Anwenderseite her geschehen. Das bedeutet, dass Du, mit etwas Geschwindigkeitsverlust, Deinen Code ausschliesslich im Userspace entwickeln kannst. Wenn Du keine grosse Bandbreite filtern musst, wirst Du das angenehmer finden, als die Pakete im Kernel zu behandeln.

In den fruehen Tagen von Netfilter habe ich das bewiesen, indem ich eine Embryo-Version von iptables zum Userspace portierte. Netfilter oeffnet den Leute die Tuer, um ihre eigenen recht effizienten Module zu schreiben, und das in welcher Sprache sie wollen.


Next Previous Contents