Voglio confidarvi un segreto: il mio criceto preferito ha realizzato tutto il codice. Io sono solo un tramite, una facciata se si vuole, appartenente al grande piano del mio animale. Perciò, non mi si rimproveri se ci sono dei bachi. Incolpate lo svelto, l'impellicciato.
iptables provvede semplicemente un array di regole in memoria (da qui il nome `iptables') e informazioni su dove i pacchetti da ciascun hook dovrebbero cominciare la traversata. Dopo che una tabella è stata registrata, lo userspace può leggere e sostituirne il contenuto utilizzando getsockopt() e setsockopt().
iptables non si registra con nessun hook di netfilter: rilascia ad altri moduli questo compito, provvede quindi solo a passare i pacchetti in modo appropriato; un modulo deve registrare hook e ip_tables separatamente, e fornire il meccanismo per chiamare ip_tables quando viene raggiunto l'hook.
Per convenienza viene utilizzata, per rappresentare una regola sia nello userspace sia nel kernel, la stessa struttura dati, sebbene qualche campo sia utilizzato solo nel kernel.
Ogni regola consiste delle seguenti parti:
La natura variabile della regola dà un'enorme disponibilità di flessibilità per le estensioni, come si vedrà, in particolare su come ciascun match (corrispondenza) oppure target (obiettivo) può trasportare un quantitativo arbitrario di dati. Ciò comporta comunque alcune trappole: è necessario prestare attenzione all'allineamento. Ciò avviene assicurandosi che le strutture `ipt_entry', `ipt_entry_match' e `ipt_entry_target' siano convenientemente dimensionate, e che tutti i pacchetti siano confinati nell'allineamento massimo della macchina utilizzando la macro IPT_ALIGN().
La `struct ipt_entry' ha i seguenti campi:
Le strutture `struct ipt_entry_match' e `struct ipt_entry_target' sono molto simili, in quanto contengono un campo di lunghezza totale (IPT_ALIGN'alizzato) (rispettivamente `match_size' e `target_size') e una union che mantiene il nome del match o del target (per userspace) e un puntatore (per il kernel).
A causa della complicata natura della struttura dati delle regole sono state previste alcune routine di aiuto:
Questa funzione inline restituisce un puntatore al target di una regola.
Questa macro invoca la funzione specificata per ogni soddisfazione della regola data. Il primo argomento della funzione è la `struct ipt_match_entry', altri argomenti (se presenti) sono quelli forniti alla macro IPT_MATCH_ITERATE(). Questa funzione ritorna zero in caso di iterazione, o un valore diverso da zero in caso di stop.
Questa funzione richiede un puntatore ad una entry, la dimensione totale della tabella delle entry e una funzione da invocare. Il primo argomento della funzione è la `struct ipt_entry', altri argomenti (se presenti) sono quelli forniti alla macro IPT_ENTRY_ITERATE(). Questa funzione ritorna zero in caso di iterazione, o un valore diverso da zero in caso di stop.
Lo userspace ha quattro funzioni: può leggere la tabella corrente, leggere le informazioni (posizione degli hook e dimensione della tabella), sostituire la tabella (ed ottenere i vecchi contatori) e aggiungere nuovi contatori.
Ciò permette la simulazione, attraverso lo userspace, di qualsiasi operazione atomica: ciò è ottenuto attraverso la libreria libiptc, la quale provvede per i programmi una comoda semantica "add/delete/replace".
Siccome queste tabelle sono trasferite nello spazio del kernel, l'allineamento diventa un problema per quelle macchine che possiedono tipi di regole userspace e kernelspace differenti (es. Sparc64 con userland a 32-bit). Questi casi sono gestiti sovrascrivendo, per queste piattaforme, la definizione di IPT_ALIGN nel file `libiptc.h'.
Il kernel comincia ad esaminare dalla locazione indicata dal particolare hook. La regola è esaminata se gli elementi della `struct ipt_ip' sono soddisfatti, ciascuna `struct ipt_entry_match' è poi controllata a turno (la funzione associata con quella soddisfatta è invocata). Se la funzione corrispondente ritorna 0, le iterazioni sono fermate su questa regola. Se il parametro `hotdrop' è impostato a 1, il pacchetto sarà immediatamente scartato (è utilizzata per alcuni pacchetti sospetti, come nella funzione match tcp).
Se l'iterazione continua verso la fine, i contatori sono incrementati e la `struct ipt_entry_target' è esaminata: se è un target (obiettivo) standard allora viene letto il campo `verdict' (valore negativo indica verdetto del pacchetto, positivo indica un offset a cui saltare). Se la risposta è positiva e l'offset non corrisponde a quello della regola successiva, la variabile `back' è impostata, e il valore `back' precedente è collocato nel campo `comefrom' di questa regola.
Per i target non-standard viene chiamata la funzione target: essa restituisce un verdetto (target non standard non possono saltare, in quanto si potrebbe infrangere il codice statico di determinazione dei loop). Il verdetto può corrispondere anche a IPT_CONTINUE per continuare con la regola successiva.
Siccome sono pigro, iptables
è abbastanza estendibile.
Questo è sostanzialmente un tentativo di passare il lavoro ad altre persone,
e rappresenta proprio ciò che è l'Open Source dopo tutto (vedi Free Software,
ciò che RMS dichiara a riguardo della parola "freedom", e io ero presente ad
uno di questi discorsi quando ho scritto ciò).
Estendere iptables
potenzialmente coinvolge due parti:
estensione del kernel, con la scrittura di un nuovo modulo, e possibilmente
estensione del programma userspace iptables
, con la realizzazione
di una nuova libreria condivisa.
Realizzare un modulo per il kernel è di per sé abbastanza semplice, come si può notare dagli esempi. Una cosa da sapere è che il codice deve essere rientrante: ci può essere un solo pacchetto in arrivo dallo userspace mentre un altro giunge su un interrupt. Di fatto in SMP ci può essere un pacchetto su un interrupt per CPU in 2.3.4 e oltre.
Le funzioni che è necessario conoscere sono:
Questa funzione è il punto di entrata del modulo. Restituisce un numero di errore negativo, oppure 0 se riesce a registrarsi con successo con netfilter.
Questo è il punto di uscita del modulo; dovrebbe eliminare la registrazione con netfilter.
Utilizzata per registrare un nuovo tipo di match (corrispondenza). Si passerà ad essa una `struct ipt_match' di solito dichiarata come variabile statica (file-scope).
Utilizzata per registrare un nuovo tipo. Si passerà ad essa una `struct ipt_target' di solito dichiarata come variabile statica (file-scope).
Utilizzata per rimuovere la registrazione del proprio target.
Utilizzata per rimuovere la registrazione del proprio match.
Un avvertimento riguardo la realizzazione di cose complicate (come ad esempio provvedere dei contatori) nello spazio extra del proprio match o del proprio target. Sulle macchine SMP, l'intera tabella è duplicata usando memcpy per ciascuna CPU: se davvero si desidera mantenere informazioni in modo centralizzato, si dovrebbe dare un'occhiata al metodo utilizzato con il match `limit'.
Nuove funzione match sono di solito realizzate come moduli a sé stanti. E' possibile estendere questi moduli successivamente, sebbene solitamente non necessario. Un modo potrebbe essere quello di utilizzare la funzione `nf_register_sockopt' del framework netfilter, per consentire agli utenti di comunicare direttamente con i propri moduli. Un'altra soluzione potrebbe essere quella di esportare i simboli per altri moduli affinché si registrino allo stesso modo di netfilter e iptables.
Il nocciolo della propria funzione match sarà la struttura ipt_match che sarà passata a `ipt_register_match()'. Questa struttura ha i seguenti campi:
Questo campo è impostabile con qualsiasi robaccia, facciamo `{ NULL, NULL }'.
Questo campo specifica il nome della funzione match, come riferito allo userspace. Il nome, affinché l'auto-caricamento funzioni, deve corrispondere al nome del modulo (ossia, se il nome è "mac", il modulo dovrà essere "ipt_mac.o").
Questo campo è un puntatore ad una funzione match che prende skb, i puntatori ai dispositivi in ed out (uno dei quali potrebbe essere NULL, a seconda dell'hook), un puntatore ai dati match della regola che è risultata soddisfatta, la dimensione della regola, l'offset IP (non zero significa un frammento non di testa), un puntatore all'intestazione del protocollo (ossia, giusto l'intestazione IP), la lunghezza dei dati (ossia la dimensione del pacchetto meno l'intestazione IP) e infine un puntatore ad una variabile `hotdrop'. Dovrebbe restituire un valore non-zero se il pacchetto la soddisfa, e può impostare `hotdrop' a 1 se restituisce 0, per segnalare che questo pacchetto deve essere scartato immediatamente.
Questo campo è un puntatore ad una funzione la quale controlla le specificazioni di una regola; se restituisce 0, allora la regola dell'utente non sarà accettata. Ad esempio, il tipo match "tcp" accetterà solo pacchetti tcp, quindi se la `struct ipt_ip', parte della regola, non specifica che il protocollo è tcp, uno zero è restituito. L'argomento tablename consente al match di controllare con quali tabelle può essere utilizzato, mentre la `hook_mask' è una bitmask di hook da cui questa regola può essere chiamata: se il match non ha senso per qualche hook di netfilter, si può evitare ciò in questo punto.
Questo campo è un puntatore ad una funzione la quale è invocata quando una entry, che utilizza questo match, è cancellata. Ciò consente di allocare dinamicamente delle risorse nella checkentry e di rilasciarle qui.
Questo campo è da impostare a `THIS_MODULE', il quale fornisce un puntatore al modulo. Esso comporta l'aumento e la diminuzione dell'usage-count a seconda che le regole di questo tipo siano create o distrutte. Ciò previene che un utente rimuova il modulo (e che quindi cleanup_module() sia invocata) quando esiste una regola riferita ad esso.
Se il proprio target modifica il pacchetto (es. l'intestazione o il corpo), bisogna chiamare la funzione skb_unshare() per copiare il pacchetto nel caso che sia clonato: altrimenti qualsiasi raw socket che ha un clone di skbuff vedrà le modifiche (es. le persone potrebbero vedere cose arcane in tcpdump).
Nuovi target sono di solito realizzati a loro volta come moduli a sé stanti. La discussione riguardante il capitolo `Nuove funzioni match' può essere ugualmente utilizzata anche qui.
Il nocciolo del proprio nuovo target è la struct ipt_target che sarà poi passata alla ipt_register_target(). La struttura ha i seguenti campi:
Questo campo è impostabile con qualsiasi robaccia, facciamo `{ NULL, NULL }'.
Questo campo specifica il nome della funzione target, come riferito allo userspace. Il nome, affinché l'auto-caricamento funzioni, deve corrispondere al nome del modulo (ossia, se il nome è "REJECT", il modulo dovrà essere "ipt_REJECT.o").
Questo è un puntatore alla funzione target, la quale richiede skbuff, il numero di hook, i puntatori ai device input ed output (uno di essi potrebbe essere NULL), un puntatore ai dati target, la dimensione dei dati target, e la posizione della regola nella tabella. La funzione target restituisce o IPT_CONTINUE (-1) se la traversata deve continuare, oppure uno dei verdetti di netfilter verdict (NF_DROP, NF_ACCEPT, NF_STOLEN etc.).
Questo campo è un puntatore ad una funzione la quale controlla le specificazioni di una regola; se restituisce 0, allora la regola dell'utente non sarà accettata.
Questo campo è un puntatore ad una funzione che è invocata quando una entry con questo target è cancellata. Ciò consente di allocare dinamicamente le risorse nella checkentry e di rilasciarle qui.
Questo campo è da impostare a `THIS_MODULE', il quale fornisce un puntatore al modulo. Esso comporta l'aumento e la diminuzione dell'usage-count a seconda che le regole di questo tipo siano create o distrutte. Ciò previene che un utente rimuova il modulo (e che quindi cleanup_module() sia invocata) quando esiste una regola riferita ad esso.
Se desiderato si può creare una nuova tabella con scopi specifici. Per crearla, si deve chiamare `ipt_register_table()' fornendo una `struct ipt_table', la quale ha i seguenti campi:
Questo campo è impostabile con qualsiasi robaccia, facciamo `{ NULL, NULL }'.
Questo campo specifica il nome della funzione target, come riferito allo userspace. Il nome, affinché l'auto-caricamento funzioni, deve corrispondere al nome del modulo (ossia, se il nome è "nat", il modulo dovrà essere "ipt_nat.o").
Questa è una `struct ipt_replace' completamente popolata, proprio come utilizzata nello userspace per sostituire una tabella. Il puntatore `counters' può essere impostato a NULL. Questa struttura dati può essere dichiarata `__initdata', in questo modo dopo il boot sarà eliminata.
Questa è una bitmask di hook IPv4 di netfilter, con cui si accederà alla tabella: questa è usata per verificare che quelle entry point siano valide, e per calcolare i possibili hook per le funzioni `checkentry()' di ipt_match e ipt_target.
Questo campo è l'interruttore lettura-scrittura dell'intera tabella; la si inizializzi a RW_LOCK_UNLOCKED.
Questo campo è utilizzato internamente dal codice di ip_tables.
Ora dopo aver realizzato uno splendente modulo per il kernel, si potrebbe
desiderare di controllare le opzioni dallo userspace.
Piuttosto che avere una versione derivata di iptables
per ogni
estensione, io utilizzo l'ultimissima tecnologia degli anni 90: i furbies.
Scusate, intendevo le librerie condivise.
Nuove tabelle generalmente non richiedono alcuna estensione di iptables
:
l'utente può utilizzare l'opzione `-t' per far sì che sia possibile utilizzare
la nuova tabella.
La libreria condivisa dovrebbe avere una funzione `_init()', la quale sarà chiamata automaticamente appena caricata: è l'equivalente della funzione `init_module()' per i moduli del kernel. Questa dovrebbe poi chiamare `register_match()' o `register_target()', a seconda che la libreria provveda un nuovo match o un nuovo target.
E' necessario fornire una libreria condivisa: può essere usata per inizializzare parte della struttura, o fornire opzioni addizionali. Insisto nel consigliare la creazione della libreria condivisa anche se non deve fare niente, ma solo per ridurre i problemi che si verificano quando le librerie sono mancanti.
Esistono funzioni molto utili descritte nel file header `iptables.h', in particolare:
controlla se un argomento è attualmente `!', e in tal caso imposta il flag `invert' se non già impostato. Se restituisce vero, si può incrementare optind, come fatto negli esempi.
converte una stringa in un numero dell'intervallo dato, restituisce -1 se malformato o fuori intervallo. `string_to_number' si appoggia a `strtol' (vedi la pagina del manuale), questo significa che uno "0x" in testa indica che il numero è in base esadecimale, e uno "0" indica che è in base ottale.
dovrebbe essere invocata se si incontra un errore. Di solito il primo argomento è `PARAMETER_PROBLEM', il quale specifica che l'utente non ha utilizzato correttamente la linea comando.
La funzione _init() della libreria condivisa passa a `register_match()' un puntatore ad una `struct iptables_match' statica, che ha i seguenti campi:
Questo puntatore è utilizzato per realizzare una lista linkata di match (come quelle utilizzate per visualizzare le regole). Dovrebbe essere inizialmente impostata a NULL.
Nome della funzione match. Questa dovrebbe corrispondere al nome della libreria (es. "tcp" per `libipt_tcp.so').
Di solito impostata con la macro IPTABLES_VERSION: questa è
utilizzata per assicurarsi che l'eseguibile iptables
non utilizzi
per sbaglio le librerie condivise errate.
Dimensione dei dati match per questo match; si dovrebbe utilizzare la macro IPT_ALIGN() per assicurarsi che sia correttamente allineato.
Per alcuni match, il kernel modifica alcuni campi internamente (il target `limit' è uno di questi casi). Ciò significa che una semplice `memcmp()' è insufficiente per comparare due regole (richiesto per la funzionalità delete-matching-rule). Se questo è il caso, si sistemino tutti i campi che non cambiano all'inizio della struttura, e qui si metta la loro dimensione. Di solito questa ha lo stesso valore del campo `size'.
Funzione che visualizza le informazioni sull'uso delle opzioni.
Questa può essere utilizzata per inizializzare lo spazio extra (se presente) della struttura ipt_entry_match, e per impostare qualsiasi bit nfcache; se si sta esaminando qualcosa non esprimibile utilizzando il contenuto di `linux/include/netfilter_ipv4.h', allora si faccia semplicemente un OR con i bit NFC_UNKNOWN. Sarà chiamata prima di `parse()'
Questa funzione è chiamata quando un'opzione non conosciuta è presente nella linea comando: dovrebbe restituire non-zero se l'opzione è effettivamente della propria libreria. `invert' è vera se un `!' è già stato incontrato. Il puntatore `flags' è di esclusivo utilizzo per la propria libreria match, e di solito è utilizzato per memorizzare una bitmask di opzioni che sono state specificate. Ci si assicuri di aver aggiustato il campo nfcache. Riallocando si può estendere, se necessario, la dimensione della struttura `ipt_entry_match', ma poi è necessario assicurarsi che la dimensione sia passata attraverso la macro IPT_ALIGN.
Questa è chiamata dopo che la linea di comando è stata analizzata, inoltre viene passato l'intero `flags' riservato per la propria libreria. Ciò dà la possibilità di controllare che tutte le opzione obbligatorie siano state specificate, quindi si invochi `exit_error()' se è il caso.
Utilizzata dal codice di visualizzazione della catena per stampare (sullo standard output) le informazioni match extra (se presenti) di una regola. L'opzione numeric viene impostata se l'utente specifica il flag `-n'.
Questa funzione è il contrario della parse: è utilizzata da `iptables-save' per riprodurre le opzioni usate per creare la regola.
Questa è una lista di opzioni extra, terminata con un NULL, offerte dalla propria libreria. E' fusa con le opzioni correnti e passata alla getopt_long; consultare le pagine del manuale per i dettagli. Il codice di ritorno della getopt_long diventa poi il primo argomento (`c') della funzione `parse()'.
Ci sono altri elementi extra alla fine di questa struttura, utilizzati
internamente da iptables
: non è necessario impostarli.
La funzione _init() della propria libreria condivisa passa a `register_target()' un puntatore ad una `struct iptables_target' statica, la quale ha campi simili alla struttura iptables_match vista prima.
libiptc
è la libreria di controllo di iptables, progettata
per visualizzare e manipolare le regole nel modulo iptables del kernel.
Anche se il suo utilizzo corrente riguarda il programma iptables, consente
di scrivere altri tool in modo molto semplice.
E' necessario essere root per utilizzare queste funzioni.
Le tabelle del kernel sono semplici tabelle di regole e un insieme di numeri che rappresentano gli entry point. I nomi delle catene ("INPUT", ecc.) sono forniti come astrazioni dalla libreria. Le catene definite dall'utente sono etichettate inserendo un nodo di errore prima dell'inizio della catena definita dall'utente, la quale contiene nella sezione dei dati extra del target, il nome della catena (le posizioni delle catene incorporate sono definite attraverso gli entry point delle tre tabelle).
Sono forniti i seguenti target standard: ACCEPT, DROP, QUEUE (che sono tradotti rispettivamente in NF_ACCEPT, NF_DROP, e NF_QUEUE,), RETURN (che è tradotto in un valore speciale IPT_RETURN gestito da ip_tables), e JUMP (che è tradotto dal nome della catena nel reale offset all'interno della tabella).
Quando `iptc_init()' è invocata, la tabella, inclusi i contatori, è letta. Questa tabella è manipolabile attraverso le funzioni `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()', e `iptc_set_policy()'.
I cambiamenti alla tabella non saranno apportati fino a quando non sarà chiamata la funzione `iptc_commit()'. Ciò significa che è possibile che due librerie utenti, operanti sulla stessa catena, concorrano una con l'altra; per prevenire queste situazioni sarebbe necessario il locking, al momento non effettuabile.
Non esiste concorrenza per quanto riguarda i contatori; i contatori sono sommati dopo nel kernel in un modo tale che i loro incrementi, tra il tempo di lettura e scrittura della tabella, siano ancora visibili nella nuova tabella.
Ci sono diverse funzioni di aiuto:
Questa funzione restituisce il nome della prima catena della tabella.
Questa funzione restituisce il nome della catena successiva della tabella: NULL indica che non ci sono altre catene.
Restituisce true (vero) se il nome della catena fornito corrisponde al nome di una catena definita da iptables.
Questa funzione restituisce un puntatore alla prima regola della catena avente il nome dato: NULL indica catena vuota.
Questa restituisce un puntatore alla regola successiva della catena: NULL indica fine della catena.
Questa funzione permette di ottenere il target di una data regola. Se si tratta di un target estensione viene restituito il nome del target. Se corrisponde ad un salto ad un'altra catena viene restituito il nome della catena. Se è un verdetto (es. DROP) ne viene restituito il nome. Se non ha un target (regola accounting-style) viene restituita una stringa vuota.
Si noti che questa funzione dovrebbe essere utilizzata al posto della consultazione diretta del valore del campo `verdict' nella struttura ipt_entry, dato che offre le sopraindicate ulteriori interpretazioni del verdetto standard.
Questa consente di ottenere la policy (tattica) di una catena incorporata, e la sua statistica di utilizzo attraverso l'argomento `counters'.
Questa funzione restituisce una ancor più eloquente spiegazione riguardo un codice di fallimento della libreria iptc. Se una funzione fallisce, essa imposta sempre errno: questo valore può essere passato a iptc_strerror() per generare un messaggio di errore.
Benvenuti al Network Address Translation presente nel kernel. Si noti che l'infrastruttura offerta è stata progettata più con l'obiettivo della completezza piuttosto che della bruta efficienza, interventi futuri potranno incrementare sensibilmente le prestazioni. Al momento sono contento che funzioni.
NAT è suddiviso in connection tracking (il quale non manipola affatto i pacchetti), e il codice di NAT stesso. Il connection tracking è stato progettato per essere utilizzato come modulo di iptables, ed effettua sottili distinzioni riguardanti lo stato, che generalmente il NAT non considera proprio.
Il connection tracking (tracciamento delle connessioni) si aggancia agli hook di alta priorità NF_IP_LOCAL_OUT e NF_IP_PRE_ROUTING, in ordine, per vedere i pacchetti prima che entrino nel sistema.
Il campo nfct della skb è un puntatore ad uno degli array infos[], presenti all'interno della struct ip_conntrack. Quindi si può ricavare lo stato della skb in base a quale elemento di questo array esso sta puntando: questo puntatore codifica sia la struttura state sia la relazione di skb con questo stato.
Il modo migliore per estratte il campo `nfct' consiste nel chiamare `ip_conntrack_get()', la quale restituisce NULL se non è impostato, oppure il puntatore alla connessione, inoltre "compila" ctinfo che descrive la relazione del pacchetto con questa connessione. Questo tipo enumerato può assumere diversi valori:
Il pacchetto è parte di una connessione stabilita, nella direzione originale.
Il pacchetto è correlato ad una connessione, ed è passato nella direzione originale.
Il pacchetto sta cercando di creare una nuova connessione (ovviamente, è nella direzione originale).
Il pacchetto è parte di una connessione stabilita, nella direzione di risposta.
Il pacchetto è correlato ad una connessione, ed è passato nella direzione di risposta.
Quindi un pacchetto in risposta può essere identificato effettuando un test di tipo >= IP_CT_IS_REPLY.
Questi framework sono stati progettati per essere adattati a qualsiasi tipo di protocollo e tipo differente di mapping. Alcuni di questi tipi di mapping potrebbero essere piuttosto specifici, per esempio mapping load-balancing/fail-over.
Internamente, il connection tracking, prima di cercare connessioni o regole che siano soddisfatte, converte un pacchetto in una "tupla", che consiste nella parte interessante del pacchetto. Questa tupla ha una parte manipolabile e una parte non manipolabile; chiamate "src" e "dst", almeno come appaiono nel mondo del SNAT durante l'ispezione del primo pacchetto (nel caso del mondo del Destination NAT corrisponderebbero ad un pacchetto di risposta). La tupla per ogni pacchetto dello stesso stream, nella stessa direzione, è sempre uguale.
Ad esempio, una tupla di un pacchetto TCP contiene la parte manipolabile: indirizzo IP sorgente e porta sorgente, la parte non manipolabile: indirizzo IP destinazione e porta destinazione. Tuttavia non è necessario che la parte manipolabile e la parte non manipolabile siano dello stesso tipo; ad esempio, una tupla di un pacchetto ICMP contiene la parte manipolabile: indirizzo IP sorgente e l'id ICMP, e la parte non manipolabile: indirizzo IP destinazione, tipo e codice ICMP.
Ogni tupla ha un inverso, il quale corrisponde alla tupla relativa ai pacchetti dello stream che arrivano in risposta. Ad esempio, l'inverso di un pacchetto ICMP ping, icmp id 12345, da 192.168.1.1 a 1.2.3.4, è un pacchetto ping-reply, icmp id 12345, da 1.2.3.4 a 192.168.1.1.
Queste tuple, rappresentate dalla `struct ip_conntrack_tuple', sono ampiamente utilizzate. Di fatto, assieme con l'hook da cui il pacchetto arriva (il quale ha effetto sul tipo di manipolazione), e il dispositivo coinvolto, questa corrisponde all'informazione completa del pacchetto.
La maggior parte delle tuple sono contenute entro la `struct ip_conntrack_tuple_hash', che aggiunge una entry alla lista doppiamente linkata, e un puntatore alla connessione a cui la tupla appartiene.
Una connessione è rappresentata dalla `struct ip_conntrack' la quale ha due campi `struct ip_conntrack_tuple_hash': uno riguardante la direzione del pacchetto originale (tuplehash[IP_CT_DIR_ORIGINAL]), e uno riguardante la direzione dei pacchetti in risposta (tuplehash[IP_CT_DIR_REPLY]).
Comunque, la prima cosa che il NAT fa è di verificare se il codice del connection tracking è riuscito ad estrarre una tupla e a trovare una connessione esistente, controllando il campo nfct della skbuff; ciò permette di conoscere se è un tentativo di nuova connessione, o in caso contrario, quale direzione ha; nell'ultimo caso inoltre sono poi effettuate le manipolazioni stabilite precedentemente per questa connessione.
Se corrisponde invece all'inizio di una nuova connessione, si cercherà una regola per questa tupla, utilizzando il meccanismo standard di attraversamento di iptables. Se una regola viene soddisfatta, è utilizzata per inizializzare le manipolazioni, sia per quella direzione sia per la risposta; il codice del connection tracking ci farà notare che la risposta, come aspettato, è stata cambiata. Quindi sarà manipolata come sopra.
Se non c'é nessuna regola, viene creato un collegamento `null': questo di solito non mappa il pacchetto, ma esiste per assicurare che non si mappi un altro stream sopra uno esistente. Qualche volta, il collegamento null non può essere creato, in quanto si è già mappato sopra uno stream, in questo caso la manipolazione per protocollo potrebbe provare a rimapparla, anche se è nominalmente un collegamento `null'.
I target NAT sono simili ai target estensione di iptables, eccetto per il fatto che sono utilizzati solo con la tabella `nat'. Sia i target SNAT che DNAT prendono una `struct ip_nat_multi_range' come dato extra; ciò serve per specificare l'intervallo di indirizzi che è consentito utilizzare per un mapping. Un elemento di intervallo `struct ip_nat_range' consiste in un indirizzo IP minimo e massimo inclusi, e in un valore massimo e minimo inclusi specifici del protocollo (es. porte TCP). C'è inoltre spazio per i flag, i quali specificano se l'indirizzo IP può essere mappato (qualche volta si desidera mappare solo la parte specifica del protocollo di una tupla, non l'IP), e un altro per indicare che la parte specifica del protocollo dell'intervallo è valida.
Un intervallo multiplo consiste in un array di elementi `struct ip_nat_range'; ciò significa che un intervallo potrebbe essere "1.1.1.1-1.1.1.2 porte 50-55 E 1.1.1.3 porta 80". Ogni elemento dell'intervallo viene aggiunto all'intervallo (una unione, per chi ama la teoria degli insiemi).
Implementare un nuovo protocollo prima di tutto significa decidere quale parte di una tupla deve essere manipolabile e quale no. Qualsiasi cosa nella tupla deve avere la proprietà di identificare univocamente lo stream. La parte manipolabile della tupla è poi la parte su cui si può effettuare il NAT: per il caso TCP questa è la porta sorgente, per ICMP è l'ID; insomma qualcosa utilizzabile come "identificatore dello stream". La parte non manipolabile consiste invece nella parte restante del pacchetto, che identifica univocamente lo stream, ma con cui non si può "giocare" (es. porta destinazione TCP, tipo ICMP).
Una volta prese queste decisioni, si può scrivere un'estensione al codice del connection-tracking nella directory, e proseguire popolando la struttura `ip_conntrack_protocol' che è necessario poi passare alla funzione `ip_conntrack_register_protocol()'.
I campi della `struct ip_conntrack_protocol' sono:
Da impostare a '{ NULL, NULL }'; utilizzata per unirsi alla lista.
Il numero del protocollo; vedere `/etc/protocols'.
Nome del protocollo. Questo è il nome che l'utente vedrà; in genere è meglio se corrisponde ad uno dei nomi canonici presenti in `/etc/protocols'.
Funzione che, dato il pacchetto, riempie le parti specifiche della tupla riguardanti il protocollo. Il puntatore `datah' punta all'inizio dell'intestazione (giusto dopo l'intestazione IP), mentre datalen è la lunghezza del pacchetto. Se il pacchetto non è abbastanza lungo per contenere le informazioni dell'intestazione, restituisce 0; datalen sarà comunque sempre di almeno 8 byte (imposto dal framework).
Questa funzione è utilizzata semplicemente per cambiare la parte specifica del protocollo della tupla in modo tale che appaia come quella di un pacchetto di risposta.
Questa funzione è utilizzata per stampare la parte specifica del protocollo di una tupla; di solito è usata la funzione sprintf() con il buffer fornito. Restituisce il numero di caratteri utilizzati. Questa è utilizzata per stampare gli stati per la /proc.
Questa funzione è utilizzata per stampare la parte privata della struttura conntrack, se presente, usata inoltre anche per stampare gli stati in /proc.
Questa funzione è chiamata quando un pacchetto è visto quale parte di una connessione stabilita. Si ottiene un puntatore alla struttura conntrack, l'intestazione IP, la lunghezza, e la ctinfo. Ritorna un verdetto per il pacchetto (normalmente NF_ACCEPT), oppure -1 se il pacchetto non è parte valida di una connessione. Si può cancellare la connessione dall'interno di questa funzione se desiderato, ma è d'obbligo utilizzare la seguente forma per evitare concorrenze (vedere ip_conntrack_proto_icmp.c):
if (del_timer(&ct->timeout))
ct->timeout.function((unsigned long)ct);
Questa funzione è chiamata quando un pacchetto crea una connessione per la prima volta; non c'è un argomento ctinfo, dato che il primo pacchetto è ctinfo IP_CT_NEW per definizione. Restituisce 0 se fallisce nella creazione della connessione, o un immediato timeout di connessione.
Una volta scritto e testato ciò è possibile tracciare il proprio nuovo protocollo, ora è tempo di istruire NAT su come interpretarlo. Ciò significa realizzare un nuovo modulo; un'estensione al codice NAT e di andare a popolare la struttura `ip_nat_protocol' che sarà necessario passare a `ip_nat_protocol_register()'.
Da impostare a '{ NULL, NULL }'; utilizzata per unirsi alla lista.
Nome del protocollo. Questo è il nome che l'utente conoscerà; in genere, per l'auto-caricamento nello userspace, è meglio se corrisponde ad uno dei nomi canonici di `/etc/protocols', come vedremo più avanti.
Numero del protocollo; vedere `/etc/protocols'.
Questa è l'altra metà della funzione pkt_to_tuple del connection tracking: si può pensare ad essa come a "tuple_to_pkt". Ci sono comunque alcune differenze: si ottiene un puntatore all'inizio dell'intestazione IP e la lunghezza totale del pacchetto. Ciò perché alcuni protocolli (UDP, TCP) necessitano di conoscere l'intestazione IP. Si fornirà il campo ip_nat_tuple_manip della tupla (ossia, il campo "src"), piuttosto che l'intera tupla, e il tipo di manipolazione che si sta per effettuare.
Questa funzione è utilizzata per indicare se la parte manipolabile della tupla fornita appartiene all'intervallo dato. Questa funzione è un po' complicata: si sta per fornire il tipo di manipolazione che è stata applicata alla tupla, la quale ci dice come interpretare l'intervallo (ci stiamo rivolgendo all'intervallo sorgente o a quello destinazione?).
Questa funzione è utilizzata per controllare se un mapping esistente ci colloca nell'intervallo corretto, e inoltre per controllare se non è necessaria una manipolazione.
Questa funzione è il nocciolo del NAT: data una tupla e un intervallo, si sta per alterare la parte della tupla relativa al protocollo per sistemarla nell'intervallo, e renderla unica. Se non si riesce a trovare una tupla non utilizzata nell'intervallo, deve restituire 0. Si ottiene inoltre un puntatore alla struttura conntrack, richiesta dalla ip_nat_used_tuple().
L'approccio comune è di iterare semplicemente la parte della tupla relativa al protocollo attraverso l'intervallo, utilizzando `ip_nat_used_tuple()' fino a quando una non restituisce false.
Si noti che il caso mapping nullo è già stato controllato: o è esterno all'intervallo dato o è già occupato.
Se IP_NAT_RANGE_PROTO_SPECIFIED non è impostato, ciò significa che l'utente sta effettuando il NAT, non il NAPT: sta facendo qualcosa di ragionevole con l'intervallo. Se il mapping non è desiderabile (per esempio, entro TCP, un mapping sulla destinazione non dovrebbe modificare la porta TCP a meno che non sia ordinato) deve restituire 0.
Dato un buffer di caratteri, una tupla match e una maschera, mostra per esteso le parti relative al protocollo e ritorna la lunghezza del buffer utilizzato.
Dato un buffer di caratteri e un intervallo, stampa per esteso la parte relativa al protocollo dell'intervallo e restituisce la lunghezza del protocollo utilizzato. Questa non sarà chiamata se il flag IP_NAT_RANGE_PROTO_SPECIFIED non è stato impostato per l'intervallo.
Questa è la parte davvero interessante. Si possono scrivere nuovi target NAT che provvedano un nuovo tipo di mapping. Due extra target sono forniti nel pacchetto di default: MASQUERADE e REDIRECT. Questi sono abbastanza semplici per illustrare il potenziale e la capacità di realizzare un nuovo target NAT.
Queste sono realizzate come qualsiasi altro target di iptables, internamente essi estraggono la connessione e chiamano `ip_nat_setup_info()'.
I protocol helper per il connection tracking permettono al codice del connection tracking di comprendere i protocolli che usano connessioni multiple (es. FTP). Inoltre segnano le connessioni `figlie' come correlate alla connessione iniziale, solitamente leggendo il relativo indirizzo del flusso di dati.
I protocol helper per il NAT fanno due cose: innanzitutto permettono al NAT di manipolare il flusso di dati per cambiarne l'indirizzo, ed in secondo luogo di eseguire il NAT sulle connessioni correlate all'originale.
Il compito del modulo per il connection tracking è quello di stabilire quali pacchetti appartengono ad una connessione già stabilita. Per farlo il modulo esegue i seguenti passi:
Se ci sono azioni aggiuntive che devono essere fatte la prima volta che un pacchetto, appartenente ad una connessione prevista, arriva, il modulo può registrare una funzione callback che sarà chiamata in quel momento.
La funzione init del proprio modulo deve chiamare `ip_conntrack_helper_register()' con un puntatore a `struct ip_conntrack_helper'. Questa struttura ha i seguenti campi:
Questa è la testa di una lista linkata. Netfilter gestisce questa lista internamente. Da inizializzare con `{ NULL, NULL }'.
Questo è un puntatore ad una stringa costante che specifica il nome del protocollo. ("ftp", "irc", ...)
Un gruppo con una o più delle seguenti flag:
Un puntatore al modulo dell'helper. Da inizializzare con la macro `THIS_MODULE'.
Massimo numero di expectation non confermate (in sospeso).
Timeout (in secondi) per ogni expectation non confermata. Una expectation è cancellata `timeout' secondi dopo che l'expectation identificata con la funzione `ip_conntrack_expect_related()'.
Questa è una `struct ip_conntrack_tuple' che specifica a quali pacchetti il modulo per il conntrack helper è interessato.
Maschera riferita a `struct ip_conntrack_tuple'. Questa maschera
specifica quali bit di tuple
sono validi.
La funzione che netfilter dovrebbe chiamare per ogni pacchetto che verifica tuple+mask
#define FOO_PORT 111
static int foo_expectfn(struct ip_conntrack *new)
{
/* funzione chiamata quando il primo pacchetto di una connessione
prevista arriva */
return 0;
}
static int foo_help(const struct iphdr *iph, size_t len,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo)
{
/* analizza i dati passati con questa connessione e
decide come saranno i pacchetti correlati */
/* aggiorna i dati privati della connessione master
(session state, ...) */
ct->help.ct_foo_info = ...
if (there_will_be_new_packets_related_to_this_connection)
{
struct ip_conntrack_expect exp;
memset(&exp, 0, sizeof(exp));
exp.t = tuple_specifying_related_packets;
exp.mask = mask_for_above_tuple;
exp.expectfn = foo_expectfn;
exp.seq = tcp_sequence_number_of_expectation_cause;
/* dati privati della connessione slave */
exp.help.exp_foo_info = ...
ip_conntrack_expect_related(ct, &exp);
}
return NF_ACCEPT;
}
static struct ip_conntrack_helper foo;
static int __init init(void)
{
memset(&foo, 0, sizeof(struct ip_conntrack_helper);
foo.name = "foo";
foo.flags = IP_CT_HELPER_F_REUSE_EXPECT;
foo.me = THIS_MODULE;
foo.max_expected = 1; /* una expectation alla volta */
foo.timeout = 0; /* le expectation non terminano */
/* siamo interessati a tutti i pacchetti TCP con porta di destinazione 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);
}
I moduli helper per il NAT, gestiscono il NAT per alcune specifiche applicazioni. Di solito questo include la manipolazione al volo dei dati: si pensi al comando PORT dell'FTP, dove il client comunica al server a quale IP/porta connettersi. Per questo un modulo helper per l'FTP deve sostituire l'IP/porta dopo che il comando PORT è stato eseguito in una connessione FTP.
Se stiamo trattando con il protocollo TCP, la faccenda si complica leggermente. La ragione sta nella possibile variazione delle dimensioni del pacchetto (esempio FTP: la lunghezza della stringa che rappresenta la tupla IP/porta dopo che il comando PORT l'ha modificata). Se cambiamo le dimensioni del pacchetto, abbiamo un syn/ack di differenza tra la parte sinistra e destra del NAT. (ad esempio se estendiamo un pacchetto di 4 ottetti, bisogna aggiungere questo offset al numero di sequenza TCP di ogni pacchetto che seguirà).
E' anche richiesta la gestione del NAT di tutti i pacchetti correlati. Prendiamo come esempio ancora FTP, dove tutti i pacchetti in ingresso di una connessione DATA devono essere NATtati verso l'IP/porta specificati dal client con il comando PORT nella connessione di controllo, piuttosto che passare attraverso la normale tabella lookup.
La funzione `init()' del modulo helper chiama `ip_nat_helper_register()' con un puntatore ad una `struct ip_nat_helper'. Questa struttura ha i seguenti elementi:
Ancora un altro header di una lista usata internamente da netfilter. Da inizializzare con { NULL, NULL }.
Un puntatore ad una stringa costante con il nome del protocollo
Un gruppo di zero, una o più delle seguenti flag:
Un puntatore al modulo dell'helper. Da inizializzare usando la macro `THIS_MODULE'.
una `struct ip_conntrack_tuple' che descrive a quali pacchetti in NAT helper è interessato.
maschera una `struct ip_conntrack_tuple', che dice a netfilter
quali bit di tuple
sono validi.
La funzione che è chiamata per ogni pacchetto che verifica tuple+mask.
La funzione che è chiamata per ogni primo pacchetto di una connessione prevista.
E' molto simile alla scrittura di un connection tracking helper.
#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)
/* chiamata ogni volta che arriva il primo pacchetto di una connessione correlata.
parametri: pksb packet buffer
hooknum HOOK di provenienza (POST_ROUTING, PRE_ROUTING)
ct informazioni su questa connessione (correlata)
info &ct->nat.info
valore di ritorno: verdetto (NF_ACCEPT, ...) */
{
/* Cambio dei valori ip/porta del pacchetto con i valori
letti da master->tuplehash, per mapparlo allo stesso modo,
viene chiamata ip_nat_setup_info e restituito NF_ACCEPT. */
}
static int foo_help(struct ip_conntrack *ct,
struct ip_conntrack_expect *exp,
struct ip_nat_info *info,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff **pksb)
/* chiamata per ogni pacchetto riconosciuto dal conntrack come expectation-cause
parametri: ct struct ip_conntrack della connessione principale
exp struct ip_conntrack_expect dell'expectation
determinata dal conntrack helper per questo protocollo
info (STATO: related, new, established, ... )
hooknum HOOK di provenienza (POST_ROUTING, PRE_ROUTING)
pksb packet buffer
*/
{
/* estrazione di informazioni sui futuri pacchetti correlati
(è possibile condividere informazioni con la funzione foo_help
del connection tracking).
Scambio di indirizzo/porta con i valori per il masquerading,
inserimento della tupla dei pacchetti correlati */
}
static struct ip_nat_helper hlpr;
static int __init(void)
{
int ret;
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;
hlpr.expect = foo_nat_expect;
ret = ip_nat_helper_register(hlpr);
return ret;
}
static void __exit(void)
{
ip_nat_helper_unregister(&hlpr);
}
Netfilter è piacevolmente semplice, ed è stato descritto in modo abbastanza esauriente nei capitoli precedenti. Tuttavia, qualche volta è necessario andare oltre a ciò che l'infrastruttura NAT o ip_tables offrono, oppure si potrebbe desiderare di sostituirle interamente.
Un importante problema per netfilter (beh, in futuro) è il caching. Ogni skb ha un campo `nfcache': una bitmask che indica quali campi dell'intestazione sono stati esaminati e se i pacchetti sono stati alterati o no. L'idea è che ciascun hook fuori di netfilter effettui un OR con i bit rilevanti, in questo modo si potrà successivamente realizzare un sistema cache sufficientemente intelligente da comprendere quando i pacchetti non necessitano di essere passati attraverso netfilter.
I bit più importanti sono NFC_ALTERED, che specifica che il pacchetto è stato alterato (questo è già utilizzato per l'hook IPv4 NF_IP_LOCAL_OUT, per re-instradare i pacchetti alterati), e NFC_UNKNOWN, che indica che il caching non dovrebbe essere effettuato in quanto sono state esaminate alcune proprietà non esprimibili. Se incerti, semplicemente si imposti il flag NFC_UNKNOWN nel campo nfcache della skb all'interno del proprio hook.
Per ricevere/manipolare i pacchetti nel kernel, si può semplicemente scrivere un modulo che registri un "hook netfilter". Questa è sostanzialmente un'espressione di interesse per alcuni determinati punti; gli attuali punti sono specifici per protocollo, e sono definiti nelle intestazioni di netfilter specifiche per i protocolli, ad esempio "netfilter_ipv4.h".
Per registrare e rimuovere le registrazioni di hook di netfilter, si utilizzeranno le funzioni `nf_register_hook' e `nf_unregister_hook'. Ciascuna di queste richiede un puntatore ad una `struct nf_hook_ops' che si dovrà popolare come segue:
Utilizzata per unirsi alla lista linkata: impostare a '{ NULL, NULL }'
Funzione invocata quando un pacchetto "colpisce" questo hook. La funzione deve restituire NF_ACCEPT, NF_DROP oppure NF_QUEUE. Nel caso NF_ACCEPT, sarà chiamato il successivo hook agganciato a questo punto. Nel caso NF_DROP, il pacchetto sarà scartato. Nel caso NF_QUEUE, sarà accodato. Si riceverà inoltre un puntatore ad un puntatore skb, perciò si può sostituire completamente la skb, se desiderato.
Al momento non utilizzata: progettata per far passare i pacchetti giunti quando la cache viene svuotata. Forse non sarà mai implementata: impostare a NULL.
La famiglia del protocollo, es. nel caso IPv4, `PF_INET'.
Numero dell'hook a cui si è interessati, es. `NF_IP_LOCAL_OUT'.
Questa interfaccia è al momento utilizzata da ip_queue; ci si può registrare per gestire, per un dato protocollo, i pacchetti accodati. Ha una semantica simile a quella delle registrazioni di un hook, eccetto il fatto che è possibile bloccare il trattamento del pacchetto, inoltre si vedranno solo i pacchetti per i quali un hook ha risposto con un `NF_QUEUE'.
Le due funzioni utilizzate per registrare l'interesse ai pacchetti accodati sono `nf_register_queue_handler()' e `nf_unregister_queue_handler()'. La funzione che si registrerà sarà chiamata con il puntatore `void *' che poi si passerà alla `nf_register_queue_handler()'.
Se nessun altro è registrato per gestire un protocollo, restituire NF_QUEUE è equivalente a restituire NF_DROP.
Una volta registrato l'interesse ai pacchetti accodati, essi cominciano ad essere accodati. Si può fare qualsiasi cosa con essi, ma è obbligatorio chiamare `nf_reinject()' una volta terminato (non si effettui semplicemente un kfree_skb()). Quando si effettua il "reinject" di skb, si passi la skb, la `struct nf_info' gestore della queue fornita e un verdetto: con NF_DROP i pacchetti vengono scartati, NF_ACCEPT fa sì che continuino ad iterare attraverso gli hook, NF_QUEUE che siano nuovamente accodati, e NF_REPEAT che l'hook che ha accodato i pacchetti sia nuovamente consultato (si evitino i loop infiniti).
Si può guardare all'interno della `struct nf_info' per ottenere informazioni ausiliarie sul pacchetto, quali ad esempio interfacce e hook.
E' cosa comune che componenti di netfilter vogliano interagire con lo userspace. Il metodo affinché ciò avvenga richiede il meccanismo setsockopt. Nota che ogni protocollo deve essere modificato per poter chiamare nf_setsockopt() per i numeri setsockopt che non comprende (e nf_getsockopt() per i numeri getsockopt), finora solo IPv4, IPv6 e DECnet sono stati modificati.
Utilizzando una tecnica ora familiare, si registrerà una `struct nf_sockopt_ops' utilizzando nf_register_sockopt(). I campi di questa struttura sono i seguenti:
Utilizzata per unirsi alla lista. Impostare a '{ NULL, NULL }'.
La famiglia del protocollo che si gestisce, es. PF_INET.
e
Questi specificano l'intervallo (esclusivo) di numeri setsockopt gestiti. Quindi utilizzare 0 e 0 significa non avere numeri setsockopt.
Questa è la funzione chiamata quando l'utente richiama una delle setsockopts. Si dovrebbe controllare che esse abbiano capacità NET_ADMIN entro questa funzione.
e
Questi specificano l'intervallo (esclusivo) dei numeri setsockopt gestiti. Quindi utilizzare 0 e 0 significa non avere numeri setsockopt.
Questa è la funzione chiamata quando l'utente richiama una delle getsockopts. Si dovrebbe controllare che esse abbiano capacità NET_ADMIN entro questa funzione.
Gli ultimi due campi sono utilizzati internamente.
Utilizzando la libreria libipq e il modulo `ip_queue', quasi tutto ciò che può essere fatto nel kernel può ora essere effettuato nello userspace. Ciò significa che, con qualche penalità nella velocità, si può sviluppare il proprio codice interamente nello userspace. A meno che non si stia provando a filtrare bande larghe, si dovrebbe trovare questo approccio superiore al manipolamento del pacchetto nel kernel.
Nei primi giorni di vita di netfilter ho constatato ciò portando una versione embrionale di iptables nello userspace. Netfilter apre le porte a tutte le persone che vogliono scrivere per conto proprio moduli efficienti di manipolazione della rete, e in qualsiasi linguaggio desiderato.