Elmondok egy titkot: az én kicsi hörcsögöm végezte a teljes kódolást. Én csak egy csatorna, egy arcvonal vagyok a kis kedvencem nagy tervében. Nos, ne engem hibáztassatok, ha hiba van benne, hanem az aranyos, kis szõröst.
iptables egyszerûen a szabályok egy nevesített tömbjét szolgáltatja (innen a név: ip-táblák), valamint informálnak a beérkezõ csomagok útjáról. Miután egy tábla bejegyzésre került, a felhasználói programok képesek olvasni és kicserélni a tartalmát a getsockopt() és setsockopt() függvényekkel.
iptables nincs bejegyezve egyik netfilter hookhoz sem: arra számít, hogy más modulok megteszik ezt, s a csomagokat helyes sorrendben továbbítják felé; egy modult be kell jegyezni a netfilter hookokra, valamint az ip_tables-be is, valamint lehetõséget kell adni az ip_tables meghívására, ha a hook elérésre került.
A kényelmesség miatt azonos adatstruktúra társul egy szabályhoz az userspace-ben és a kernelben is, annak ellenére, hogy egyes mezõk csak a kernelben kerülnek alkalmazásra.
Minden szabály a következõ részekbõl áll:
A szabály változó természete nagy szabadságot kölcsönöz a bõvítményeknek, mint láthatjuk is akár minden match vagy target különbözõ mennyiségû adatot hordozhat. Ez azonban néhány csapdát hordoz magában: figyelni kell az igazításra, kerekítésre. Ennek során ügyelünk, hogy a 'ipt_entry', 'ipt_entry_match' és a 'ipt_entry_target' struktúrák megfelelõ méretûek legyenek, és a rendszeren elérhetõ legnagyobb igazítási méretre legyenek felkerekítve (IPT_ALIGN() macro).
'struct ipt_entry' mezõi:
A `struct ipt_entry_match' és `struct ipt_entry_target' struktúrák nagyon hasonlóak: tartalmazzák a teljes (IPT_ALIGN-olt) hossz mezejét (`match_size' és `target_size'), valamint egy unionban a match vagy target nevét (userspace) és mutatóját (kernel).
A szabályszerkezet trükkös természete miatt pár segédfunkció is elérhetõ:
Ez a beépített függvény visszaad egy pointert a szabály targetjére.
Ez a makró meghívja az adott funkciót minden egyes match-ra az adott szabályban. A függvények elsõ argumentuma egy `struct ipt_match_entry', míg a többi (ha létezik) az, amit az IPT_MATCH_ITERATE() makró kapott. A funkció nullát ad vissza az iteráció folytatásához, nem nulla értéket a megszakításához.
Ez a funkció mutatókat vár egy bejegyzésre, a tábla teljes bejegyzéseinek méretére, valamint a meghívandó funkcióra. A funkció elsõ argumentuma egy `struct ipt_entry', és a további argumentumai (ha vannak) megegyeznek az IPT_ENTRY_ITERATE()-ben megadottakkal. A funkció nullát ad vissza az iteráció folytatásához, nem nulla értéket a megszakításához.
Userspace-nek négy mûvelete van: olvasni tudja az aktuális táblát, információhoz juthat (hook helye, tábla mérete), kicserélheti a táblát (és megtarthatja a régi számlálókat), és új számlálókat adhat hozzá.
Ezzel bármilyen elemi mûvelet szimulálható userspace-bõl: a libiptc könyvtáron keresztül, ami kényelmes "add/delete/replace" szemantikát ad a programokhoz.
Amiért ezek a táblák továbbításra kerülnek a kernelbe, az igazítás komoly kérdés olyan rendszerekben, ahol eltérõ a méret (pl. Sparc64 kernel 32bites userspace-el). Ezek az esetek az IPT_ALIGN makró felüldefiniálásával vannak megoldva a `libiptc.h'-ban.
A kernel azon a helyen kezdi el az értelmezést, ahol az adott hook kívánja. Az a szabály kerül vizsgálatra, aminek a `struct ipt_ip' elemei megegyeznek, minden `struct ipt_entry_match' sorban ellenõrzésre kerül (a match-al összerendelt függvényen keresztül). Ha a match függvény 0-t ad eredményül, akkor megáll az értelmezés. Ha a `hotdrop' paramétert 1-be állítja, a csomag azonnal eldobásra kerül.
Ha az iteráció végigér a számlálók növelésre kerülnek, s a `struct ipt_entry_target' kerül megvizsgálásra: ha ez egy alap target, akkor a `verdict' mezõ kerül olvasásra (negatív jelenti azt, hogy már van döntés, a pozitív pedig egy ugrási eltolást ad meg.) Ha pozitív a válasz, s az offset nem a következõ szabályra mutat, a `back' változó beállításra kerül és az elõzõ `back' értéke a szabály `comefrom' mezõjébe kerül.
A nem standard targeteknél a target függvény hívódik meg: ez egy döntést ad vissza (nem alap targetek nem tudnak ugrani, ugyanis megsérthetnék a hurokdetektálást). A döntés lehet IPT_CONTINUE a következõ szabályon való továbbhaladáshoz.
Amiért ilyen lusta vagyok, az iptables
meglehetõsen jól bõvíthetõ.
Ez alapjában egy csalás a munka másra való áthárításával, ami kb. az,
amirõl az Open Source szól (Free Software - ahogy RMS mondaná - a szabadságról
szól, és én egy beszédén ülve írtam ezt.)
iptables
i kibõvítése potenciálisan két részbõl áll:
a kernel kibõvítése egy új modul írásával, és lehetõség szerint az
userspace rész iptables
programjának bõvítése egy új shared
könyvtár írásával.
Egy kernel modult írni magában egy egyszerû feladat, ahogy azt a példában is láthatod. Amire oda kell figyelned, hogy a kódodnak újra-belépõnek kell lennie: elõfordulhat, hogy egy csomag érkezik az userspace-bõl, míg egy másik egy megszakításon keresztül. SMP esetében minden CPU esetében lehet csomag a megszakításokon (2.3.4 és fölötte).
Azok a funkciók, amikrõl tudnod kell:
Ez a modul belépési pontja. Ez egy negatív hibaszámot ad vissza, vagy 0-t, ha sikeresen regisztrálta magát a netfilterben.
A modul kilépési pontja; itt veheti ki magát a modul a netfilterbõl.
Ez egy új match bejegyzésére használható. Egy `struct ipt_match'-al kezelhetõ, amit rendszerint static-ként deklarálnak.
Ez agy új target bejegyzésére használható. Egy `struct ipt_target'-al kezelhetõ, amit rendszerint static-ként deklarálnak.
A tergetem visszavonására használható.
A match-em visszavonására használható.
Egy figyelmeztetés a trükkös dolgokkal kapcsolatban (mint pl. számlálók nyújtása) az extra helyekben az új match-ben vagy target-ban. SMP eszközön a teljes tábla megduplázódik egy memcpy()-val minden CPU-ra: ha valóban központi információt akarsz tárolni, akkor nézd meg azt, ahogy ez a 'limit' match-ben megvalósításra került.
Új match funkciók rendszerint különálló modulokként kerülnek megírásra. Ez lehetõvé teszi ezeknek a moduloknak a felváltott bõvítését, bár ez rendszerint nem szükséges. Egyik lehetõség a netfilter váz `nf_register_sockopt' funkciója a felhasználói kapcsolatteremtésre. Másik lehetõség szimbólumok kiexportálása más modulok felé, ahol regisztrálhatják magukat, azonos módon, mint ahogy a netfilter és az ip_tables csinálja.
Az új match funkciód központi része az ipt_match struktúra, ami az `ipt_register_match()'-nak kerül átadásra. A struktúra szerkezete:
Tetszõleges junk lehet, állítsd `{ NULL, NULL }'-ra.
Ez a mezõ tartalmazza a match funkció nevét, ahogyan az userspace-bõl hivatkozunk rá. A név lehetõleg egyezzen meg a modul nevével (pl. ha a név 'mac', akkor a modul neve legyen 'ipt_mac.o'), hogy az automatikus betöltés mûködhessen.
Ez egy mutató a match funkcióra, ami megkapja az 'skb', az 'in' és 'out' device mutatókat (ami lehet NULL, a hook-tól függõen), egy mutatót a match adatra az éppen feldolgozott szabályban (az a struktúra, ami az userspace-ben készült), IP offsetet (nem nulla jelenti hogy nem-fejléc csomag), egy pointert a protokollfejlécre, az adat hosszát (pl. a csomag hossza az IP fejléc méretével csökkentve) és végül egy mutatót a `hotdrop' változóra. Ez nem-nulla értékkel jelzi, ha a csomag egyezett, és a `hotdrop' 1-be állításával ill. 0 visszaadásával dobathatja el azonnal a csomagot.
Ez egy mutató, ami egy olyan függvényre mutat, ami ellenõrzi a szabály specifikációját; ha 0-t ad vissza, akkor nem lett elfogadva a szabály. Például: a 'tcp' match típus csak TCP csomagokat fog elfogadni, s ha a `struct ipt_ip' része a szabálynak nem tartalmazza, hogy a protokollnak TCP-nek kell lennie, nullát ad vissza. A táblanév argument segít megtalálni, hogy hol van a szabály, míg a `hook_mask' bitmask megadja, hogy melyik hookokból kerülhet meghívásra a szabály. Ha a szabály nem függ a hookoktól, akkor figyelmen kívül lehet hagyni.
Ez egy mutató egy olyan függvényre, ami akkor hívódik meg, amikor a match-ot tartalmazó szabály törlésre kerül. Ez lehetõvé teszi a dinamikus területfoglalást a chechkentry-ben, s a felszabadítást.
Ez a mezõ `THIS_MODULE'-ra van beállítva, egy pointert ad erre a modulra. Egy használatszámlálóhoz van kötve, ami fel- le változik amikor szabály születik vagy törlésre kerül. Ez meggátolja a felhasználót a modul eltávolításában (cleanup_module()) ha szabály tartalmazza.
Az új target-ek rendszerint különálló modulként kerülnek megvalósításra. A tárgyalás módja megegyezik az 'Új match funkciók' fejezetben találhatókkal.
Az új targeted központi része az ipt_target struktúra, ami az ipt_register_target()-nek kerül átadásra. A struktúra a következõ mezõket tartalmazza:
A mezõ értéke tetszõleges junk lehet, legyen `{ NULL, NULL }'.
A target neve, ahogyan az userspace-bõl hivatkoznak rá. A név lehetõleg egyezzen meg a modul nevével (pl. ha a név 'REJECT', a modult hívjad 'ipt_REJECT.o'-nak), hogy az automatikus betöltés mûködjön.
Pointer a target funkcióra, ami megkapja az skbuff-ok, a hook számok, a be- és kimenõ eszközöket (bármelyik lehet NULL), egy pointert a target adataira és a szabály helyét a táblában. A visszatérési érték lehet IPT_CONTINUE(-1), ha a vizsgálat folytatódhat, vagy egy netfilter döntés (NF_DROP, NF_ACCEPT, NF_STOLEN stb.).
Egy mutató arra a funkcióra, amely a szabály szerkezetét ellenõrzi. Nullával jelzi, ha a megadott szabály nem elfogadható.
A target törlésekor meghívandó függvényre egy mutató. Lehetõség van a checkentry-ben lefoglalt területek felszabadítására.
A mezõ értéke `THIS_MODULE', ami egy pointert ad a modulra. Tartalmaz egy számlálót, aminek az értéke nõ vagy csökken amikor a targetre hivatkoznak, vagy megszüntetik a hivatkozást. Ez meggátolja a felhasználót a modul eltávolításában (cleanup_module()), ha szabály hivatkozik rá.
Tetszõleges célra létrehozhatsz egy táblát, amikor csak akarod. Ehhez a `ipt_register_table()'-t kell meghívnod egy `struct ipt_table' struktúrával, aminek a következõ mezõi vannak:
A mezõ értéke tetszõleges junk, legyen `{ NULL, NULL }'.
A táblának a nevét atrtalmazza, ahogyan az userspace-bõl hivatkozunk rá. A név lehetõleg egyezzen meg a modul nevével (pl. ha 'nat' a tábla neve, akkor a modul legyen 'iptable_nat.o'), hogy az autómatikus betöltés mûködjön.
Ez egy teljesen kitöltött `struct ipt_replace', ahogy az userspace-bõl a tábla kicserélésére használják. A `counters' mutatót NULL-ra kell állítani. Az adatterültet '__initdata'-nak lehet deklarálni, s így betöltés után eldobható.
Ez egy bitmaszk az IPv4 netfilter hook-okról, ahol a csomag belelép: a bejegyzés helyességének ellenõrzésére használható, valamint az ipt_match és ipt_target `checkentry()' funkciójának lehetséges hookjainak származtatásához.
Ez egy írható/olvasható zár(lock) az egész táblára; RW_LOCK_UNLOCKED-re kell beállítani.
Az ip_tables kód belsõ használatára fenntartott.
Nos, megírtad a szép, csillogó kernelmodulodat, s most használni
szeretnéd a funkcióit userspace-bõl. Ahelyett, hogy magát az
iptables
-t kellene módosítani minden bõvítéshez, egy
késõ 90-es évek beli technológiát használok: furbikat. Bocsánat,
shared library-kre gondoltam.
Új táblák rendszerint nem igényelnek bõvítést az iptables
-ben:
a felhasználó használhatja a '-t' opciót az új tábla használatához.
A könyvtárban jó, ha van egy '_init()' funkció, ami automatikusan meghívódik betöltéskor: egy megfelelõje a kernel modulok 'init_module()' funkciójának. Ez meghívhatja a 'register_match()' vagy a 'register_target()' függvényeket, attól függõen, hogy a könyvtár match-et vagy target-et tartalmaz.
A könyvtárat el kell készítened: ez használható a struktúrák beállítására, vagy további opciók nyújtására. Jelenleg ragaszkodunk a shared library-hoz, még akkor is, ha nem csinál semmit, az olyan problémák csökkentésére, amik a könyvtár hiányára hivatkoznak.
Van pár hasznos funkció az `iptables.h' fejlécfileban:
azt ellenõrzi, hogy egy argument '!'-e, ha az, akkor beállítja az 'invert' flaget, ha még nem volt beállítva. Ha igazat ad vissza, az optind-t növelned kell, ahogy a példában is látszik.
egy karaktersort számmá konvertál az adott tartományban, -1-et ad vissza ha hibás, vagy a határon túli a karaktersor.
lehetõleg ezt hívd meg, ha hibát találtál. Rendszerint az elsõ paramétere `PARAMETER_PROBLEM', ami azt jelenti, hogy a felhasználó hibás parancssort adott be.
A könyvtár _init() funkciója egy `register_match()' hívást tartalmaz egy statikus `struct iptables_match' struktúra-pointerrel, aminek a következõ mezõi vannak:
A match-ek láncolt listájának kezelésére használják (pl. a szabályok listája). alapértelmezésben NULL értékre kell állítani.
A match funkció neve. Meg kell egyeznie a könyvtár nevével (pl. 'tcp' - 'libipt_tcp.so').
Rendszerint a NETFILTER_VERSION makróra van állítva:
azt biztosítja, hogy az iptables
nem olvas be hibás könyvtárat.
A match adat mérete ehhez a match-hez; lehetõleg használd az IPT_ALIGN() makrót a helyes igazításhoz.
Néhány matchben a kernel módosít pár mezõt. Ez azt jelenti, hogy egy egyszerû 'memcmp()' nem elég két szabály összehasonlítására (a delete-matching-rule funkcióhoz elengedhetetlen). Ha ez a helyzet, akkor az állandó mezõket a struktúra elején kell elhelyezni, s a nem módosuló rész méretét kell itt megadni. Rendszerint ez megegyezik a 'size' mezõvel.
Az a funkció, ami az opciók használatát írja ki.
Az extra helyek beállítására használható (ha van) az ipt_entry_match struktúrában, s állíthatja az nfcache biteket. Ha valami olyant vizsgálsz, ami nem kifejezhetõ a `linux/include/netfilter_ipv4.h'-val, egyszerûen OR-old meg az NFC_UNKNOWN bitet. A 'parse()' elõtt fog meghívódni.
Ez akkor kerül meghívásra, ha egy nem ismert funkciót talál a parancssorban: nem nullát ad vissza, ha valóban a könyvtáradhoz tartozik. `invert' értéke igaz, ha már talált '!'-t. A 'flags' kizárólag a match könyvtár által használt, rendszerint bitmaszk tárolására használják, ami a beállított kapcsolókat reprezentálja. Meg kell gyõzõdnöd arról, hogy az nfcache mezõt állítod. Szükséged lehet az `ipt_entry_match' méretének növelésére áthelyezéssel, de a méretet az IPT_ALIGN makróval kell megadnod!
A parancssor értelmezése után hívódik meg, és a 'flags' értékét vizsgálja. Lehetõséget ad összeférhetetlenség-vizsgálatra, s az `exit_error()' hívással jelezheted a problémát.
A lánclistázó kód használja a (standard kimenetre) való funkciókiíráskor. A numeric flag be van állítva, ha a felhasználó megadta a '-n' kapcsolót.
A parse ellentettje: az `iptables-save' használja a szabályt létrehozó opciók visszaállításához.
Ez egy NULL-lezárt listája az extra funkcióknak, amiket a könyvtárad nyújt. Az eddigi opciókkal összedolgozásra kerül, s úgy kerül a getopt_long-hoz (nézd meg a mauálját). A getopt_long visszatérési kódja az elsõ argument lesz ('c') a 'parse()' funkcióhoz.
Van még pár extra funkció a struktúra végén, de azokat az iptables
használja: nem kell beállítanod õket!
A könyvtárak _init() funkciója kezeli a `register_target()'-t, s a statikus `struct iptables_target' struktúráját, aminek a felépítése hasonló az iptables_match struktúrájához.
libiptc
a táblakezelõ könyvtár, az iptables szabályok listázására
és módosítására tervezett könyvtár. Jelenleg csak az iptables program
használja, könnyû egyéb programok implementálása. Root jogokkal kell
rendelkezned a használatához.
A kernel táblák magukban csak egyszerû szabálytáblázatok, valamint belépési pontokat tartalmazó halmazok. A láncok elnevezése ("INPUT", stb.) csak egy, a könyvtárak által szolgáltatott leképezés. Felhasználó által definiált láncok neveit a chain fejléce elé beillesztett hiba-bejegyzés tartalmazza a target extra adat-területén (a beépített chain pozíciók a három tábla belépési pontjainál vannak definiálva.
A következõ standart target-ek támogatottak: ACCEPT, DROP, QUEUE (amik NF_ACCEPT, NF_DROP és NF_QUEUE -ra vannak fordítva), RETURN (ami a speciális IPT_RETURN-nek felel meg, s az ip_tables kezeli), valamint a JUMP (ami egy eltolási értékre (offset) fordul le).
Az `iptc_init()' meghívásakor a tábla - beleértve a számlálókat - kerül beolvasásra. A tábla az `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()' és `iptc_set_policy()' függvényekkel módosítható.
A változtatások nem kerülnek visszaírásra, csak az `iptc_commit()' meghívása után. Ez azt jelenti, hogy két felhasználó is módosíthatja ugyanazt a táblát, s így versenyhelyzetet kialakítva; szükség lenne lock-olásra, de még nem készült el.
A számlálókra nem él a versenyhelyzet: a visszaíráskor az érték korrigálódik az eltelt idõ alatti változással.
Számos segítõ funkciót implementáltak:
Visszaadja az elsõ lánc nevét a táblában.
A következõ chain nevét adja, NULL-al jelzi a lista végét.
Igazat ad vissza, ha az adott láncnév egy beépített chain neve.
Egy mutatót ad vissza az elsõ szabályra az adott láncon belül. NULL jelzi az üres láncot.
A következõ szabályt adja az adott chain-ben. NULL jelenti a lánc végét.
Az adott szabály target-jét adja vissza. Ha ez egy kiterjesztett target, akkor a nevét adja vissza. Ha egy másik chain-re ugrás, akkor az új chain nevét. Ha egy döntés (pl. DROP), akkor azt tartalmazza, s ha nincs target (accounting szabály), akkor üres sort.
Figyelem! Ezt a funkciót célszerû használni az ipt_entry struktúra `verdict' mezeje helyett, mert bõvebb információ szerezhetõ belõle.
A beépített lánc policy-ét kérdezi le, valamint a `counters' argumentumát kitölti a szabály találati paramétereivel.
Az iptc könyvtár hibajelzéseinek jelentéssel való kibõvítését adja. A hibával visszatérõ függvény beállítja az errno értékét, s ez a funkció kiírja a hibakódhoz tartozó üzenetet.
Üdvözöllek a kernel címfordítási részében! Felhívnám arra a figyelmedet, hogy az itt nyújtott infrastruktúra inkább a teljességre, mint a nyers hatásfokra helyezte a hangsúlyt, és a jövõ trükkjei jelentõsen növelhetik a teljesítõképességet. Jelenleg boldog vagyok, hogy mûködik.
A NAT fel van bontva kapcsolat-követési (connection tracking) (ez nem módosítja a csomagokat) és magára a fordítási kódra. Connection tracking az iptables modulokban való felhasználhatóságra lett tervezve, így olyan állapotok szövevényes rendszer alapján dönthet, amelyek a NAT-ot nem érdeklik.
A Connection tracking hookjai nagy prioritási szinttel az NF_IP_LOCAL_OUT és az NF_IP_PRE_ROUTING hookokban találhatók, így a rendszerbe való megérkezésük elõtt vizsgálja a csomagokat.
Az nfct mezõ az skb struktúrában egy pointer az ip_conntrack struktúrába, az infos[] tömb egy elemére. Ennélfogva meg tudjuk mondani az skb állapotát az általa mutatott elemen keresztül: ez a mutató tárolja az állapot-struktúrát és az skb - állapot közötti kapcsolatot is.
A legjobb eljárás az `nfct' mezõ kicsomagolására az `ip_conntrack_get()' használata, ami NULL-al jelzi, ha nincs beállítva, vagy visszaadja a kapcsolat-mutatót, valamint kitölti a ctinfo-t, ami leírja a csomag és a kapcsolat viszonyát. Ennek a változónak számos értéke lehet:
A csomag egy már létrejött kapcsolathoz tartozik, az eredei irányban.
A csomag kapcsolatban van egy connection-nel, és az eredi irányba halad.
A csomag egy új kapcsolatot próbál kialakítani (természetesen az eredeti irányban).
A csomag egy már létrejött kapcsolathoz tartozik, az ellenkezõ irányban. (Válasz)
A csomag kapcsolatban van egy connection-nel, és az ellenkezõ irányba halad. (Válasz)
Így a válasz csomagra egyszerûen ellenõrizhetünk egy >=IP_CT_IS_REPLY teszttel.
Ezek a programvázak tetszõleges protokollhoz és leképezési módhoz való illesztésre lettek tervezve. Néhány ilyen leképezés nagyon speciális is lehet, pl. terheléselosztás vagy tartalékolás.
Belsõleg a connection tracking párokba alakítja a csomagot, ami az érdekes részét mutatja a csomagnak, mielõtt függõségekre vagy egyezõ szabályokra keresne. Ennek a leírónak van egy módosítható és egy nem módosítható része is; "src" és "dst" névvel, ahogy ez a Source NAT-ban az elsõ csomagnál látható (lehetõleg kell lennie egy válasz csomagnak is a Destination NAt világában). Ez a leíró minden csomagra az adott folyamaton belül az adott irány mellett azonos.
Például a TCP csomag leírójában a módosítható rész tartalma: forráscím é sport, a nem módosíthatóé: célcím és port. A két résznek nem feltétlenül kell azonos szerkezetûnek lennie: pl. egy ICMP csomagnál a forráscím és az ICMP id a módosítható; míg a célcím és az ICMP típus és kód a nem módosítható rész.
Minden leírónak (tuple) van egy inverze, ami a válsz csomagnak a leírója. Például egy ICMP ping csomagnak (icmp id 12345, from 192.168.1.1 to 1.2.3.4) az ellentettje a ping-reply csomag (icmp id 12345, from 1.2.3.4 to 192.168.1.1).
Ezek a párok, amiket a `struct ip_conntrack_tuple' testesít meg, széles körben használtak. Valójában azzal a hook-kal, ahonnan a csomag jött (aminek a várható módosításra van hatása) és a beérkezési device adataival a csomaggal kapcsolatos összes információnkat tartalmazza.
A legtöbb leyrót a `struct ip_conntrack_tuple_hash' struktúrában találjuk meg, ami egy két-irányba láncolt lista kezeléséhez szükséges kiegészítést és egy segédmutatót (a leíró melyik kapcsolathoz tartozik) rendel még mellé.
A kapcsolatot a `struct ip_conntrack'-al reprezentáljuk: két `struct ip_conntrack_tuple_hash' mezõje van: egyik az eredeti irányba mutat (tuplehash[IP_CT_DIR_ORIGINAL]), a másik pedig a válasz-csomagokra (tuplehash[IP_CT_DIR_REPLY]).
Az elsõ dolog, amit a NAT kód végrehajt az, hogy megnézi, hogy a connection tracking kód kicsomagolta-e a leíróját, s egy meglevõ kapcsolathoz tartozónak találta-e az skbuff nfct mezõjének vizsgálatával; ez megmondja, hogy új kapcsolathoz tartozik-e vagy nem, melyik irányba halad. Az utóbbi esetben a szükséges módosítás már meghatározásra került a kapcsolathoz.
Ha egy új kapcsolat kezdete, akkor megpróbál szabályt keresni a leíróhoz, az alap iptables keresési rendszerrel a `nat' táblában. Ha egy szabály egyezik rá, akkor felhasználja a mindkét irányba szükséges módosítások meghatározásához; a connection-tracking kód jelezheti, hogy várhatóan a másik irányba is módosítani kell. Ezután a fentiek alapján módosításra kerül.
Ha nem talál szabályt, akkor egy `null' kötést készít: ez rendszerint nem kezeli a csomagot, de létezik, hogy biztosítsuk, hogy nem lapolunk be egy új kapcsolatot a régi fölé. Néha azonban nem sikerül elkészíteni a null-kötést, mert egy már meglevõ kapcsolattal felülírtuk. Ebben az esetben a protokollonkénti módosítás megpróbálhatja újra felvenni, annak ellenére, hogy ez egy null-kötés.
NAT targetek hasonlítanak a hagyományos iptales kiegészítésekre, kivéve, hogy a `nat' táblában kerülnek csak felhasználásra. Az SNAT és DAT targetek mindegyike kap egy `struct ip_nat_multi_range'-t az extra adataihoz; a kötések elkészítéséhez használható címterületet adja meg. Egy tartományelem, `struct ip_nat_range' tartalmaz egy minimum és egy maximum IP címet és egy protokoll-függõ maximum és minimum értéket (pl. TCP portok). Szintén található hely a flageknek, amik megmondhatják, hogy legyen az IP cím beírva (néha csak a protokoll-specifikus részre van szükségünk a leíróból), vagy azt, hogy a protokoll-specifikus része a tartománynak értelmezetõ.
Egy több elembõl álló tartomány egy tömb ezekbõl a `struct ip_nat_range' elemekbõl. Ez azt jelenti, hogy a tartomány lehet: "1.1.1.1-1.1.1.2 ports 50-55 AND 1.1.1.3 port 80". Minden tartományelem hozzáadásra kerül a tartományhoz (egy union, azoknak, akik ezt szeretik).
Új protokoll implementálása a leíró(tuple) a változtatható és a nem változtatható részeinek meghatározásával kezdõdik. A leíróban mindenre megvan a lehetõséged, hogy egy folyamot egyértelmûen azonosíthass. A változtatható része a leírónak az a rész, amivel a NAT-ot elvégezheted: a TCP-hez ez a forrásport, az ICMP-nek az icmp ID; valami, amit a folyam azonosítására használhatsz. A nem módosítható rész a csomag maradéka, ami egyértelmûen meghatározza a hálózati folyamot, de nem szeretnél játszani vele (pl. a TCP célport, ICMP típus).
Ha egyszer eldöntöttük, meg lehet írni a bõvítést a connection-tracking kódhoz a megfelelõ könyvtárban, és elkezdheted az `ip_conntrack_protocol' struktúra kitöltését, ami az `ip_conntrack_register_protocol()' híváshoz szükséges.
A `struct ip_conntrack_protocol' szerkezete:
Legyen '{ NULL, NULL }'; a listakezeléshez használjuk.
A protokoll-sorszáma (`/etc/protocols').
A protokoll neve. Ezt a nevet fogja látni a felhasználó; rendszerint az a legjobb, ha megegyezik az `/etc/protocols'-ban találhatóval.
A funkció, ami kitölti a protokoll-specifikus részét a leírónak az adott csomagra vonatkozóan. A `datah' pointer a fejléc kezdetére mutat, és a datalen a csomag méretét tartalmazza. Ha a csomag nem elég hosszú ahhoz, hogy benne legyen a teljes fejléc, 0-t ad vissza, noha a datalen mindig legalább 8 byte lesz (a keretrendszer garantálja).
Ez a funkció egyszerûen megváltoztatja a protokoll-függõ részét a leírónak a válasz irányból érkezõ csomagnak megfelelõen.
Ez a funkció jeleníti meg a protokoll-függõ részét a leírónak; rendszerint a megadott bufferbe kerül beírásra. A használt karakterek száma a visszatérési érték. A /proc-ban az állapotok megjelenítésére használjuk.
A conntrack struktúra privát részének megjelenítésére használjuk (ha van ilyen), valamint szintén megjelenik a /proc-bejegyzésben.
Akkor kerül meghívásra, ha egy csomag a kapcsolathoz tartozónak tûnik. Mutatókat kapcs a conntrack struktúrára, az IP fejlécre, a méretre és a ctinfo-ra. Egy döntést kell visszaadnod a csomagra (rendszerint NF_ACCEPT), vagy -1-et, ha a csomag nem tartozik a kapcsolathoz. Törölni is tudod a kapcsolatot, de használnod kell a következõ beszúrást a versenyhelyzet elkerülésére (ip_conntrack_proto_icmp.c):
if (del_timer(&ct->timeout))
ct->timeout.function((unsigned long)ct);
Ha a csomag egy új kapcsolat indít; nincs ctinfo paramétere, mert az elsõ csomag ctinfo-ja definíció szerint IP_CT_NEW. 0-t ad vissza, ha nem sikerült elkészítenie a kapcsolatot, vagy timeout volt.
Miután megírtuk és leteszteltük, hogy remekül tudjuk követni a protokollunk, itt az idõ, hogy megtanítsuk a NAT-ot, hogy hogyan kell átfordítania azt. Ez egy új modul írását jelenti; egy kiterjesztést a NAT kódhoz, valamint az `ip_nat_protocol' struktúra feltöltését az `ip_nat_protocol_register()' funkcióhoz.
'{ NULL, NULL }'
A protokoll neve. Ezt a nevet fogja látni a felhasználó; rendszerint az a legjobb, ha megegyezik az `/etc/protocols'-ban találhatóval. (Fõleg az userspace-beli autómatikus betöltés miatt.)
A protokoll-sorszáma (`/etc/protocols').
Ez a másik fele a connection tracking pkt_to_tuple funkciójának: gondold azt, hogy egy "tuple_to_pkt". Azért van néhány különbség: kapsz egy mutatót az IP fejléc kezdetére és a teljes csomagméretre. Ez azért van, mert néhány protokollhoz (UDP, TCP) szükség van a fejlécre. Továbbá az ip_nat_tuple_manip mezejét a leírónak (pl. az "src" mezõ) a teljes leíró helyett, és a módosítás típusát.
Megmondja, hogy a módosítható része az adott leírónak a megadott tartományon belül van-e. A funkció egy kicsit trükkös: megadjuk a leíróra alkalmazott módosítás típusát, ami megmondja nekünk, hogyan kell értelmezni a tartományt (a forrás vagy a céltartományra céloztunk...).
Ezzel a funkcióval ellenõrizhetjük, hogy egy meglevõ megfeleltetés helyes tartományba rakott-e minket, valamint azt is hogy szükséges-e a módosítás.
Ez a funkció a NAT központja(magja): egy leírót és egy tartományt kap, s itt kerül a protokoll-függõ része a leírónak a tartományon belülivé, s válik egyedivé. Ha nem találsz nemhasznált leírót a tartományban, akkor 0-t kell visszaadnod. Szintén kapunk egy mutatót a conntrack struktúrára, ami az ip_nat_used_tuple()-hoz szükséges.
A szokásos hozzáállás az, hogy egyszerûen folyamatosan léptetjük végig a protokoll-függõ részét a leírónak a tartományon, elvégezve az `ip_nat_used_tuple()' ellenõrzést rajta, amíg hamisat ad vissza.
A null-megfeleltetés már tesztelve volt: vagy a tartományon kívül van, vagy már foglalt.
Ha az IP_NAT_RANGE_PROTO_SPECIFIED nem lett beállítva, az azt jelenti, hogy a felhasználó NAT-ot csinál, s nem NAPT-ot: valami érzékeny dolgot csinál a tartománnyal. Ha nincs szükség megfeleltetésre (például a TCP-n belül a cél-megfeleltetésnek nem kell megváltoztatnia a TCP portot, kivéve, ha utasítják rá), 0-t adjon vissza.
Egy karakter-buffert, egy megegyezõ leírót és egy maszkot vár, s kiírja a protokoll-függõ részeket, s visszaadja a felhasznált bufferméretet.
Egy karakter-buffert és egy tartományt vár, s kiírja a protokoll-függõ részét a tartománynak, s visszaadja a felhasznált bufferméretet. Nem kerül meghívásra, ha az IP_NAT_RANGE_PROTO_SPECIFIED jelzõbit nem volt beállítva a tartományhoz.
Ez egy valóban érdekes rész. Lehetõséged van új NAT targetek írására, amelyek új leképezést valósítanak meg. Két extra targetet alapból biztosít a csomag: MASQUERADE és REDIRECT. Ezek egyszerû minták, s remekül bemutatják az új NAT targetek életképességét és erejét.
Ezek a többi iptables atrgethez hasonlóan vannak megírva, de belül megszakítják a kapcsolatot és meghívják az `ip_nat_setup_info()'-t.
A kapcsolatkövetés protokoll helperei lehetõvé teszik a követõ kódnak a több kapcsolatot tartalmazó protokollok megértését (pl. FTP), és megjelölik a leszármazott kapcsolatokat, a szülõ alá rendelik azokat, rendszerint az adatkapcsolatból kiolvasott cím alapján.
A NAT protokoll helperei két dolgot végeznek: lehetõvé teszik, hogy a NAT kód módosítsa az adatfolyamot a benne található címek átírásával, valamint elvégezhesse a NAT-ot a kapcsolódó folyamon amikor az beérkezik (az eredeti kapcsolat alapján).
A connection tracking module kötelessége, hogy meghatározza, melyik csomagok tartoznak egy már felépült kapcsolathoz. A modul a következõket teszi:
A kernel modulod init funkciójának meg kell hívnia a `ip_conntrack_helper_register()' függvényt egy pointerrel a `struct ip_conntrack_helper'-ra. A struktúra a következõ mezõkkel rendelkezik:
A láncolt lista feje. A netfiletr belsõleg kezeli. Alapbeállításban legyen `{ NULL, NULL }'.
Egy `struct ip_conntrack_tuple', ami megadja, hogy milyen csomagok érdeklik a helperünket.
Mégegy `struct ip_conntrack_tuple'. A mask megadja,
hogy a tuple
-nek melyik bitjei valósak.
A függvény, amit a netfilter meghívhat minden csomagra, ami illeszkedik a tuple+mask-ra.
#define FOO_PORT 111
static int foo_help(const struct iphdr *iph, size_t len,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo)
{
/* analyze the data passed on this connection and
decide how related packets will look like */
if (there_will_be_new_packets_related_to_this_connection)
{
t = new_tuple_specifying_related_packets;
ip_conntrack_expect_related(ct, &t);
/* save information important for NAT in
ct->help.ct_foo_info; */
}
return NF_ACCEPT;
}
static struct ip_conntrack_helper foo;
static int __init init(void)
{
memset(&foo, 0, sizeof(struct ip_conntrack_helper);
/* we are interested in all TCP packets with destport 111 */
foo.tuple.dst.protonum = IPPROTO_TCP;
foo.tuple.dst.u.tcp.port = htons(FOO_PORT);
foo.mask.dst.protonum = 0xFFFF;
foo.mask.dst.u.tcp.port = 0xFFFF;
foo.help = foo_help;
return ip_conntrack_helper_register(&foo);
}
static void __exit fini(void)
{
ip_conntrack_helper_unregister(&foo);
}
A NAT-helper modulok alkalmazásfüggõ NAT-kezelést tesznek lehetõvé. Rendszerint az adatok röptében történõ elemzését jelentik: gondolj csak a PORT parancsra az FTP-ben, ahol a kliens megmondja a szervernek, hogy melyik IP/port-párhoz kell kapcsolódnia. Így az FTP-helper modulnak ki kell cserélnie az IP/port-ot a PORT parancs után az FTP parancs-csatornában.
Ha elbántunk a TCP-vel, akkor a dolgok kissé összetettebbé válnak. Az ok a lehetséges csomagméret-változás (FTP példa: a PORT utáni IP/port-párt reprezentáló string hossza megváltozik). Ha megváltoztatjuk a csomagméretet, akkor egy syn/ack eltéréshez jutunk a NAT két oldala között. (Ez azt jelenti, hogy ha kiegészítettük a csomagot 4 oktettel, akkor ezután minden csomag TCP sorszámához hozzá kell adnunk).
A kapcsolódó csomagok speciális kezelésére is szükség van. Az FTP példához visszatérve: az adatkapcsolat minden egyes csomagját NAT-olni kell a kliens által a parancscsatornán belül a PORT parancsban megadott IP/port-párra, a sima táblázatos keresés helyett.
A nat helper modulod `init()' funkciójának meg kell hívnia a `ip_nat_helper_register()' függvényt egy pointerrel a `struct ip_nat_helper'-ra. A struktúra a következõ mezõkkel rendelkezik:
A láncolt lista feje. A netfiletr belsõleg kezeli. Alapbeállításban legyen `{ NULL, NULL }'.
Egy `struct ip_conntrack_tuple', ami megadja, hogy milyen csomagok érdeklik a NAT helperünket.
Mégegy `struct ip_conntrack_tuple'. A mask megadja,
hogy a tuple
-nek melyik bitjei valósak.
A függvény, amit a netfilter meghívhat minden csomagra, ami illeszkedik a tuple+mask-ra.
Egy egyedi név, ami a modulunkat azonosítja.
Ez pontosan megegyezik a connection tracking helper modul írásával.
Jelezni tudod, hogy a modulod képes minden várható kapcsolat NAT-olását elvégezni (valószínûleg egy connection tracking modullal került beillesztésre). Ezt a `ip_nat_expect_register()' függvénnyel teheted meg, ami egy `struct ip_nat_expect' struktúrát vár. A struktúra a következõ mezõkkel rendelkezik:
A láncolt lista feje. A netfiletr belsõleg kezeli. Alapbeállításban legyen `{ NULL, NULL }'.
a funkció, ami elvégzi a NAT-olást a várt csomagokra. TRUE-val jelzi, hogy lekezelte a csomagot, különben a következõ bejegyzett funkció kerül meghívásra. Ha TRUE-t ad vissza, akkor ki kell töltenie a döntés mezõt!
#define FOO_PORT 111
static int foo_nat_expected(struct sk_buff **pksb,
unsigned int hooknum,
struct ip_conntrack *ct,
struct ip_nat_info *info,
struct ip_conntrack *master,
struct ip_nat_info *masterinfo,
unsigned int *verdict)
/* called whenever a related packet (as specified in the connection tracking
module) arrives
params: pksb packet buffer
hooknum HOOK the call comes from (POST_ROUTING, PRE_ROUTING)
ct information about this (the related) connection
info &ct->nat.info
master information about the master connection
masterinfo &master->nat.info
verdict what to do with the packet if we return 1.
{
/* Check that this was from foo_expect, not ftp_expect, etc */
/* Then just change ip/port of the packet to the masqueraded
values (read from master->tuplehash), to map it the same way,
call ip_nat_setup_info, set *verdict, return 1. */
}
static int foo_help(struct ip_conntrack *ct,
struct ip_nat_info *info,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff **pksb)
/* called for the packet causing related packets
params: ct information about tracked connection
info (STATE: related, new, established, ... )
hooknum HOOK the call comes from (POST_ROUTING, PRE_ROUTING)
pksb packet buffer
*/
{
/* extract information about future related packets (you can
share information with the connection tracking's foo_help).
Exchange address/port with masqueraded values, insert tuple
about related packets */
}
static struct ip_nat_expect foo_expect = {
{ NULL, NULL },
foo_nat_expected };
static struct ip_nat_helper hlpr;
static int __init(void)
{
int ret;
if ((ret = ip_nat_expect_register(&foo_expect)) == 0) {
memset(&hlpr, 0, sizeof(struct ip_nat_helper));
hlpr.list = { NULL, NULL };
hlpr.tuple.dst.protonum = IPPROTO_TCP;
hlpr.tuple.dst.u.tcp.port = htons(FOO_PORT);
hlpr.mask.dst.protonum = 0xFFFF;
hlpr.mask.dst.u.tcp.port = 0xFFFF;
hlpr.help = foo_help;
ret = ip_nat_helper_register(hlpr);
if (ret != 0)
ip_nat_expect_unregister(&foo_expect);
}
return ret;
}
static void __exit(void)
{
ip_nat_expect_unregister(&foo_expect);
ip_nat_helper_unregister(&hlpr);
}
Netfilter nagyon egyszerû, és elég pontosan le van írva az elõzõ fejezetekben. Néha azonban szükséges a NAT és az ip_tables által nyújtott szolgáltatások alá menni, vagy esetleg teljesen ki is cserélhetõek.
Egy fontos kitétel a netfilterhez (nos, a jövõben) az elrejtés. Minden skb-nek van egy `nfcache' mezõje: egy bitmask, ami megmutatja, hogy milyen mezõket kell a fejlécbõl megvizsgálni, valamint, hogy megváltozott-e a csomag, vagy nem. A terv az, hogy minden netfilter hook VAGY-al állítja be a fontos bitjeit, így lehetõvé válik a késõbbiekben egy olyan rendszer kialakítása, ami okos annyira, hogy el tudja dönteni, hogy a csomagot el kell-e küldeni a netfilterben, vagy teljesen kihagyható.
A legfontosabb bitek az NFC_ALTERED, ami azt jelenti, hogy a csomag megváltozott-e (ez már használva van az IPv4-es NF_IP_LOCAL_OUT-ban a megváltoztatott csomagok routolására), és NFC_UNKNOWN, ami azt mutatja, hogy a caching-et nem lehet elvégezni, mert néhány olyan jellemzõ került megvizsgálásra, amit nem lehet kifejezni. Ha kétségeid vannak, akkor egyszerûen állítsd be a NFC_UNKNOWN flaget az skb nfcache mezõjében, a hook-odon belül.
Kernelen belül a csomagok fogadásához egyszerûen tudsz írni egy modult, ami bejegyez egy "netfilter hook"-ot. Ez alapjában az érdeklõdés kifejezésének a módja; az aktuális pontok protokoll-specifikusak lehetnek, és külön headerekben találhatók, mint pl. a "netfilter_ipv4.h".
Netfilter hook bejegyzéséhez és eltávolításához használd az `nf_register_hook' és `nf_unregister_hook' függvényeket. Ezek mindegyike vár egy pointert a `struct nf_hook_ops'-ra, ami a következõképpen épül fel:
'{ NULL, NULL }', láncolt listába illesztéshez használt.
A funkció akkor kerül meghívásra, ha a csomag eléri a hook-ot. A lehetséges visszatérési értékek: NF_ACCEPT, NF_DROP vagy NF_QUEUE. NF_ACCEPT: a következõ, erre a pontra csatlakozó hook kerül meghívásra. NF_DROP: a csomag eldobásra kerül. NF_QUEUE: sorbaállításra kerül. Egy mutatót kapsz egy skb pointerre, szóval teljesen le tudod cserélni az skb-t, ha akarod.
Aktuálisan nem hsznált: a cache ürítésekor a találatok kezeléséhez készült. Talán sose lesz implementálva: állítsd NULL-ra!
A protokollcsalád, pl. `PF_INET' IPv4-hez.
A hook száma, amiben érdekelt vagy. Pl: `NF_IP_LOCAL_OUT'.
Ezt a felületet az ip_queue használja; be tudod jegyezni, hogy egy adott protokollhoz tartozó csomagokat lekezelje. Hasonló a felépítése, mintha egy hook-hoz regisztrálnád magad, kivéve, hogy lehetõséged van a scomag feldolgozásának megállítására, valamint csak azokat a csomagokat látod, amelyekre a hook `NF_QUEUE'-t válaszolt.
A két, a regisztrációhoz használható függvény: `nf_register_queue_handler()' és `nf_unregister_queue_handler()'. A beregisztrált függvény `void *' pointerrel kerül lekezelésre.
Ha senki sem jelentkezett az adott protokoll lekezelésére, akkor az NF_QUEUE megegyezik az NF_DROP visszatérési értékkel.
Amennyiben bejegyezted az érdeklõdésed a sorbaállított csomagokra, elkezdõdik a sorbaállítás. Bármit megtehetsz velük, csak meg kell hívnod az `nf_reinject()'-et miután befejezted a módosítást (ne csak egyszerûen kfree_skb()-d õket). Amikor visszaküldesz egy skb-t, te kezeled az skb-t, a `struct nf_info'-t, ami a kezelõdet jelenti, valamint a döntést: NF_DROP eredményezi a csomag eldobását, NF_ACCEPT jelenti a hookokban való továbbküldést, NF_QUEUE jelenti az ismételt sorbaállítást, NF_REPEAT hatására pedig ismét bekerül a hook-ba (figyelt a végtelen ciklusokra!).
A `struct nf_info'-ban információt kapsz a csomagról, mint pl. a hozzá tartozó intarface-ek, a hook, amin fennakadt, stb.
Gyakori a netfilter részekben, hogy kommunikálni szeretnének az userspace-el. Az erre használható módszer a setsockopt mechanizmus. ehhez azonban módosítani kell minden protokollt, hogy meghívja a nf_setsockopt()-ot azokra a setsockopt számokra, amiket nem ismer (valamint a nf_getsockopt()-ot a getsockopt-hoz), s nemcsak az IPv4, IPv6 és DECnet protokollokban tegye meg ezt.
Egy nem szokványos módszerként bejegyzünk egy `struct nf_sockopt_ops'-ot az nf_register_sockopt() hívással. A struktúra mezõi:
A listába való beillesztéshez használt: '{ NULL, NULL }'.
A kezelt protokollcsalád, pl: PF_INET.
és
Ezek megadják a (kizárólagos) tartományát a kezelt setsockopt számoknak. Ezentúl 0-val jelezheted, hogy nincs setsockopt opciód.
Ez a funkció kerül meghívásra, ha a felhasználó meghívja az egyik setsockopt opciódat. Célszerû ellenõrizni, hogy megvan-e a NET_ADMIN capability-e.
és
Ezek megadják a (kizárólagos) tartományát a kezelt getsockopt számoknak. Ezentúl 0-val jelezheted, hogy nincs getsockopt opciód.
Ez a funkció kerül meghívásra, ha a felhasználó meghívja az egyik getsockopt opciódat. Célszerû ellenõrizni, hogy megvan-e a NET_ADMIN capability-e.
Az utolsó két mezõt belsõleg használjuk.
A libipq könyvtár és a `ip_queue' modul segítségével majdnem mindent megtehetsz az userspace-ben, amit a kernelen belül. Ez azt jelenti - kisebb sebességcsökkenéssel - a programodat teljes egészében fejlesztheted az userspace-ben. Amíg nem akarsz nagy sávszélességet szûrni, használd inkább ezt a lehetõséget a kernelen belüli csomagkezeléssel szemben.
A netfilter nagyon korai szakaszában kipróbáltam az iptables egy nagyon korai verziójának userspace-be való portolásával. Netfilter kinyitja a kaput, hogy bárki tetszõleges, egészen hatékony modulokat írjon azon a nyelven, amelyiken csak szeretne.