Proof of concept - Analisi Funzionale

Ci proponiamo di realizzare un programma, qspnclient, che si avvale del modulo QSPN e altri moduli a sostegno (Neighborhood, Identities) per stabilire come impostare le rotte nelle tabelle di routing del kernel di una macchina. Si tratta di un programma specifico per un sistema Linux.

Ruolo del qspnclient

Questo programma permette all'utente di fare le veci del demone ntkd, simulando le sue operazioni e quelle di pertinenza di altri moduli:

Sulla base dei comandi dati sulla console dall'utente, il programma qspnclient interagisce coi moduli suddetti (QSPN, Neighborhood, Identities) e sulla base delle loro elaborazioni interviene sulle configurazioni di rete del sistema. L'utente sarà quindi in grado di verificare che il sistema riesca effettivamente a stabilire connessioni con gli altri nodi della rete, che le rotte siano quelle che ci si attende, eccetera. Inoltre il programma interattivamente consente di chiedere al modulo QSPN le informazioni che ha raccolto e mostrarle all'utente.

Interazione programma-utente

Il programma qspnclient prevede che l'utente immetta, come argomenti della riga di comando e in modo interattivo dalla console durante la sua esecuzione, tutti i requisiti del modulo.

Ai parametri che saranno individuati in modo autonomo dai moduli (ad esempio gli identificativi del nodo, gli indirizzi di scheda, ...) verranno associati degli indici progressivi che saranno visualizzati all'utente. L'utente si riferirà ad essi tramite questi indici. Questo per rendere più facilmente riproducibili gli ambienti di test.


All'avvio del nodo viene creata l'istanza di NeighborhoodManager e gli sono passati i nomi delle interfacce di rete che dovrà gestire. Ad ogni interfaccia, il modulo Neighborhood associa un indirizzo IP link-local scelto a caso. In realtà l'assegnazione dell'indirizzo è fatta proprio dal programma attraverso una classe che implementa l'interfaccia INeighborhoodIPRouteManager passata al modulo Neighborhood. Quello che fa il modulo Neighborhood è scegliere l'indirizzo. Quindi il programma si avvede dell'indirizzo scelto proprio perché viene chiamato il metodo add_address di INeighborhoodIPRouteManager. Il programma associa questo proprio indirizzo link-local all'indice autoincrementante linklocal_nextindex, che parte da 0.


L'istanza di NeighborhoodManager deve essere istruita sul numero massimo di archi che può accettare. Il programma qspnclient specifica un numero elevato. Nonostante questo, ogni arco che il modulo Neighborhood realizza passa al vaglio dell'utente che decide se utilizzarlo o meno. Questo permette di dirigere il proprio ambiente di test a piacimento anche in particolari scenari, come ad esempio un gruppo di macchine virtuali che condividono un unico dominio di broadcast ma vogliono simulare un gruppo di nodi wireless posizionati secondo una determinata disposizione.

Per ogni arco che il modulo Neighborhood realizza, le informazioni a disposizione (i due link-local e i due MAC address) sono visualizzate all'utente. Soltanto agli archi che l'utente decide di accettare e nell'ordine in cui sono accettati, il programma associa un indice autoincrementante nodearc_nextindex, che parte da 0. In seguito il programma sfrutta questi archi passandoli al modulo Identities.

Sempre per dare all'utente il maggior controllo possibile sulle dinamiche del test, anche il costo di un arco che viene rilevato dal modulo Neighborhood non è lo stesso che viene usato dal programma qspnclient. L'utente quando accetta un arco dice quale costo gli vuole associare. In seguito può variarlo a piacimento fino anche a simularne la rimozione.

Quindi gli effettivi segnali di arc_changed del modulo Neighborhood sono in realtà ignorati dal programma.


All'avvio del nodo viene creata l'istanza di IdentityManager e questo crea un NodeID casuale. Il programma recupera tale NodeID col metodo get_main_id() e lo associa all'indice autoincrementante nodeid_nextindex, che parte da 0. In seguito il programma quando crea una nuova identità col metodo add_identity associa la nuova istanza di NodeID al prossimo valore di nodeid_nextindex. Quindi l'utente può usare questo indice per dare dalla console comandi concernenti una certa identità.


Alla creazione di una nuova identità, il modulo Identities crea un nuovo network namespace. In realtà la creazione del network namespace è fatta proprio dal programma attraverso una classe che implementa l'interfaccia IIdmgmtNetnsManager passata al modulo Identities. Quello che fa il modulo Identities è scegliere il nome di tale namespace. Quindi il programma si avvede del nome scelto proprio perché viene chiamato il metodo create_namespace di IIdmgmtNetnsManager.

Il nome di tale namespace è già costruito con un indice progressivo, quindi il programma non gli associa un indice ma semplicemente lo mostra all'utente. Questi potrà usare direttamente il nome del namespace nei comandi che dà sulla console.


Sempre alla creazione di una nuova identità, il modulo Identities, di nuovo con chiamate all'interfaccia IIdmgmtNetnsManager, crea per ogni interfaccia di rete reale un pseudo-interfaccia. Anche qui il nome della pseudo-interfaccia non è casuale ma generato con lo stesso indice progressivo usato per il network namespace, quindi il programma lo mostra semplicemente all'utente.

Invece il modulo Identities genera casualmente un indirizzo IP link-local e lo associa alla pseudo-interfaccia col metodo add_address di IIdmgmtNetnsManager. Il programma vede tale indirizzo e lo associa all'indice autoincrementante linklocal_nextindex, che avevamo introdotto prima.


Quando al modulo Identities viene comunicato un arco, esso vi costruisce sopra automaticamente un arco-identità. Al modulo Identities può venire espressamente richiesto dal suo utilizzatore (cioè dal programma qspnclient) di aggiungere un arco-identità, ma di norma questo non si fa. Inoltre quando viene creata una nuova identità coi metodi prepare_add_identity e add_identity (a seguito di una migrazione o un ingresso in una rete) tutti gli archi-identità della precedente identità vengono duplicati. Infine quando una identità nuova si crea in un vicino (a seguito di una migrazione che coinvolge l'identità del vicino ma non coinvolge la nostra identità) il modulo Identities riceve direttamente dalla rete istruzioni per creare un nuovo arco-identità.

In ogni occasione in cui viene aggiunto un arco-identità, il modulo Identities emette un segnale identity_arc_added con i dati "arco" (istanza di IIdmgmtArc), "propria identità" (istanza di NodeID) e "arco-identità" (istanza di IIdmgmtIdentityArc). In questo momento il programma qspnclient può identificare il nuovo arco-identità con l'indice autoincrementante identityarc_nextindex, che parte da 0. Ad ogni indice rimane associato sia l'arco, sia la propria identità, sia l'identità nel nodo vicino. Ricordiamo che dall'associazione "arco + propria identità" si può risalire al link-local del proprio nodo, che nel tempo può cambiare. Ricordiamo che dall' "arco-identità" si può risalire sia al NodeID del vicino (che non cambia nel tempo) sia al link-local del vicino, che nel tempo può cambiare.


All'avvio del nodo viene creata la prima istanza di QspnManager con il costruttore create_net. Ogni nodo costruisce inizialmente una rete nuova che comprende solo la sua prima identità principale. Per questo devono essere passati fin dall'inizio dall'utente al programma qspnclient i dati della topologia e il primo indirizzo Netsukuku che il nodo si assegna. L'identificativo del fingerprint a livello 0 è scelto a caso dal programma e le anzianità sono a zero (primo g-nodo) a tutti i livelli.

Siccome il QSPN è un modulo di identità, ogni istanza di QspnManager viene memorizzata come membro di una identità nel modulo Identities. Quindi non è necessario un ulteriore indice perché l'utente possa referenziare una istanza di QspnManager: è sufficiente l'indice nodeid_nextindex.


Al lancio del programma qspnclient l'utente indica attraverso appositi flag come vuole che il nodo si comporti riguardo le forme di contatto anonimo. Questo concetto verrà spiegato più sotto. Il comportamento di default del programma, se l'utente non indica alcun flag a riguardo, è di:

Spazio di indirizzi Netsukuku

Gli indirizzi dei nodi nella rete Netsukuku, nella sua attuale implementazione, vanno ricavati nella classe IPv4 10.0.0.0/8.

La rete viene suddivisa in un numero arbitrario di livelli. La notazione CIDR usata per individuare classi di indirizzi nelle reti IPv4, ci obbliga ad usare come gsize ad ogni livello una potenza di 2. In teoria, ad esempio, potremmo avere un livello 0 di gsize 5. Ma quando vogliamo indicare nelle tabelle di routing tutti gli indirizzi di un g-nodo di livello 1 (ad esempio da 10.1.2.0 a 10.1.2.4) non potremmo farlo in forma compatta. Invece se usiamo un livello 0 di gsize 8, per riferirci agli indirizzi nel g-nodo da 10.1.2.0 a 10.1.2.7 possiamo usare la notazione 10.1.2.0/29; per gli indirizzi da 10.1.2.8 a 10.1.2.15 useremo 10.1.2.8/29; e così via.

Indichiamo con l il numero dei livelli. Indichiamo con gsize(i) la dimensione dei g-nodi di livello i + 1. Tali dimensioni devono essere potenze di 2. Indichiamo con g-exp(i) l'esponente della potenza di 2 che dà la dimensione dei g-nodi di livello i + 1. Il numero di bit necessari a coprire lo spazio di indirizzi è dato dalla sommatoria per i da 0 a l - 1 di g-exp(i). 𝛴 0 ≤ i < l g-exp(i).

Questo numero di bit non può essere maggiore di 22. Gli indirizzi nella classe 10.0.0.0/8 hanno 24 bit, ma due di questi li riserviamo per notazioni particolari (routing interno ai g-nodi e forma anonima).

Inoltre dobbiamo assicurarci che gsize(l-1)l. Cioè che nello spazio destinato al livello più alto sia possibile rappresentare un numero da 0 a l - 1. Questo pure ci serve per la notazione usata per il routing interno ai g-nodi.

Indirizzi del nodo

Per assegnarsi un indirizzo IPv4, un nodo parte dal suo indirizzo Netsukuku, che è una sequenza di posizioni all'interno dei vari g-nodi di appartenenza ad ogni livello. Il nodo calcola per prima cosa il suo indirizzo globale N, che è un indirizzo IPv4 che identificherà in modo univoco quel nodo in tutta la rete.

Poi il nodo calcola un indirizzo Ni, che è sempre un indirizzo IPv4, che identificherà in modo univoco quel nodo nel suo g-nodo di livello i; questo per ogni i da 1 a l - 1.

Tutti questi indirizzi il nodo se li assegna.

Inoltre, opzionalmente, il nodo può calcolare per ognuno di questi indirizzi (N, N1, N2, ...) un altro indirizzo corrispettivo per i pacchetti che gli sono destinati in "forma anonima". Si tratta sempre di ulteriori indirizzi IPv4 che identificano questo nodo in modo univoco (a livello globale o all'interno di un suo g-nodo). Questi però saranno usati, come vedremo in seguito, dai client per contattare questo nodo senza rivelare il proprio indirizzo. Il nodo quindi può assegnarsi questi indirizzi oppure no. Se lo fa significa che vuole dare la possibilità ai client di usare questo meccanismo per contattarlo e fargli richieste senza essere identificati.

L'opzione di accettare richieste anonime è distinta e indipendente dall'opzione di rendere anonimi i pacchetti che transitano per il nodo nel percorso verso un'altra destinazione. Quest'altra opzione verrà discussa in seguito.

In un altro documento viene spiegato come questi indirizzi vengono calcolati a partire dall'indirizzo Netsukuku. TODO


Vediamo come queste impostazioni si configurano in un sistema Linux e quindi quali operazioni fa il programma qspnclient.

In un sistema Linux il sistema si assegna un indirizzo associandolo ad una interfaccia di rete. In realtà ogni interfaccia può avere più indirizzi e anche lo stesso indirizzo può essere associato a più interfacce; inoltre anche se un indirizzo x viene associato alla interfaccia nicA e non all'interfaccia nicB, un pacchetto destinato a x ricevuto tramite l'interfaccia nicB giunge ugualmente al processo che sta in ascolto sull'indirizzo x.

Si noti che il fatto di associare un indirizzo IP ad una specifica interfaccia di rete ha la sua importanza in relazione al protocollo di risoluzione degli indirizzi IP in indirizzi MAC (Address Resolution Protocol). Infatti quando un nodo a vuole trasmettere un pacchetto IP ad un suo diretto vicino b, esso conosce l'indirizzo IP di b e l'interfaccia di rete di a dove trasmettere. Il nodo a trasmette in broadcast su quel segmento di rete una richiesta: "chi ha l'indirizzo IP XYZ?". Il nodo b risponde indicando al nodo a l'indirizzo MAC della sua interfaccia di rete. Quindi il nodo a può incapsulare il pacchetto IP in un frame Ethernet che riporta gli indirizzi MAC dell'interfaccia che trasmette e dell'interfaccia che deve ricevere.

Se il nodo b ha diverse interfacce di rete tutte collegate allo stesso segmento di rete, il fatto di associare diversi indirizzi IP a diverse interfacce può fornire un modo di identificare una precisa interfaccia di rete a cui un pacchetto va trasmesso. Questo è usato dal modulo Qspn per distinguere i pacchetti che vanno ricevuti da una pseudo-interfaccia, per questo gli indirizzi link-local sono diversi su ogni interfaccia e sulle pseudo-interfacce.

Fatta questa premessa, come si comporta il programma?

Il programma, all'avvio, computa tutti i suoi indirizzi (nello spazio Netsukuku) e li associa tutti a ognuna delle interfacce di rete che gestisce.

Inoltre, prima di terminare, li rimuove da tutte le interfacce che gestisce.

Rotte nelle tabelle di routing

Il programma deve istruire le policy di routing del sistema (che di norma significa impostare delle rotte nelle tabelle di routing) in modo da garantire questi comportamenti:


Vediamo come queste impostazioni si configurano in un sistema Linux e quindi quali operazioni fa il programma qspnclient. Esaminiamo prima l'aspetto del source natting e poi del routing.

Source NATting

Il source NATting in un sistema Linux può essere realizzato istruendo il kernel con il comando iptables (utilizzando una regola con l'estensione SNAT nella catena POSTROUTING della tabella nat). Quando un pacchetto da inoltrare rientra nei parametri di questa regola (un esempio di parametro che si può usare è l'indirizzo di destinazione del pacchetto che rientra in un dato range) allora il kernel modifica l'indirizzo mittente nel pacchetto sostituendolo con uno dei suoi indirizzi pubblici. Inoltre compie una serie di altre operazioni volte a mantenere inalterate il più possibile le caratteristiche della comunicazione (ad esempio la connessione stabilita dal protocollo TCP).

Ad esempio, con il comando «iptables -t nat -A POSTROUTING -d 10.128.0.0/9 -j SNAT --to 10.1.2.3» si ottiene che tutti i pacchetti da inoltrare alle destinazioni 10.128.0.0/9 vanno rimappati al mio indirizzo 10.1.2.3

Fatta questa premessa, come si comporta il programma?

Il programma, all'avvio, opzionalmente, istruisce il kernel per il source natting. Con questa configurazione il nodo si rende disponibile ad anonimizzare i pacchetti che riceve e che vanno inoltrati verso una destinazione che accetta richieste anonime.

L'opzione di rendere anonimi i pacchetti che transitano per il nodo nel percorso verso un'altra destinazione è distinta e indipendente dall'opzione di accettare richieste anonime, che è stata discussa sopra.

Se il nodo decide di implementare il source natting, calcola lo spazio di indirizzi che indicano una risorsa da raggiungere in forma anonima. Una volta calcolato il numero di bit necessari a ricoprire i vari livelli della nostra rete -si rilegga il paragrafo sopra sullo spazio di indirizzi Netsukuku- vanno considerati altri 2 bit in testa; il primo di questi va impostato e tutti gli altri sono liberi. Se ad esempio si usano tutti i 22 bit a disposizione per gli indirizzi, allora il range di indirizzi che indicano una risorsa da raggiungere in forma anonima è 10.128.0.0/9.

Poi il range viene ulteriormente scomposto in un numero di sottoclassi pari al numero dei livelli nella rete. Il primo blocco costituisce gli indirizzi globali delle risorse da raggiungere in forma anonima; si caratterizza per avere NON impostato il secondo bit. Nel nostro esempio, il range di questo blocco è 10.128.0.0/10.

Prendiamo ora ogni livello i della nostra rete, da 1 a l - 1 inclusi, dove l è il numero di livelli. Il blocco del livello i costituisce gli indirizzi interni al nostro g-nodo di livello i delle risorse (interne al suddetto g-nodo) da raggiungere in forma anonima; si caratterizza per avere impostato il secondo bit e per avere codificato il numero i nei bit destinati all'identificativo del livello più alto.

Riprendiamo il nostro esempio e supponiamo che ci siano nella rete 10 livelli e il più alto abbia dimensione 16, vale a dire gsize(9) = 16, g-exp(9) = 4. Allora il blocco del livello i ha impostato il primo e il secondo bit ed ha codificato il numero i nei 4 bit dalla posizione 3 alla 6 incluse. Questi sono quindi i blocchi:

Supponiamo ora che il nostro nodo sia nel g-nodo di livello 9 con posizione 5 e, per semplicità, supponiamo posizione 0 a tutti gli altri livelli. Quindi -si rilegga il paragrafo sopra sugli indirizzi del nodo- il nostro nodo ha assegnati a se questi indirizzi (in forma non anonima):

Allora il programma istruisce il kernel di modificare i pacchetti destinati ai seguenti range indicando come nuovo indirizzo mittente uno dei propri indirizzi, come segue:

Quando il programma termina, se aveva istruito il kernel per fare il source natting, rimuove le regole che aveva messe nella catena POSTROUTING della tabella nat.

Routing

In un sistema Linux le rotte vengono memorizzate in diverse tabelle. Queste tabelle hanno un identificativo che è un numero da 0 a 255. Hanno anche un nome descrittivo: l'associazione del nome al numero (che in realtà è il vero identificativo) è fatta nel file /etc/iproute2/rt_tables.

In ogni tabella possono esserci diverse rotte. Ogni rotta ha alcune informazioni importanti:

Quando un pacchetto va inviato ad una certa destinazione, ci sono delle regole che dicono al sistema su quali tabelle guardare. Queste regole, visibili con il comando ip rule list, di default dicono di guardare per prima la tabella "local", per penultima la tabella "main" e per ultima la tabella "default". Tra la regola che dice di guardare la "local" e quella che dice di guardare la "main" possono essere inserite altre regole.

Ogni regola può dire semplicemente di guardare una tabella, oppure di guardarla solo a determinate condizioni. Una particolare condizione che ci torna utile è questa: "guarda la tabella XXX se il pacchetto da trasmettere è marcato con il numero YYY". La marcatura del pacchetto è virtuale, nel senso che i dati del pacchetto non sono affatto modificati, ma solo il sistema locale lo vede come marcato; ed è sempre il sistema locale che lo ha precedentemente marcato. Questa marcatura viene fatta da una parte del kernel che può essere istruita usando l'azione MARK del comando iptables.

Fatta questa premessa, come si comporta il programma?

Per ogni arco verso un vicino, il programma memorizza una rotta diretta (cioè senza gateway) nella tabella "main", indicando come destinazione l'indirizzo di scheda del vicino e come mittente preferito il proprio indirizzo di scheda. Questo lo abbiamo visto poco sopra; infatti il comando "ip route add" senza specificare un nome di tabella si riferisce alla tabella "main".

Il programma crea una tabella "ntk" con identificativo YYY, dove YYY è il primo identificativo libero nel file /etc/iproute2/rt_tables. Tale tabella sarà inizialmente vuota; in essa il programma andrà a mettere le rotte di pertinenza della rete Netsukuku, cioè quelle con destinazione nello spazio 10.0.0.0/8. Inoltre aggiunge una regola che dice di guardare la tabella "ntk" prima della "main".

Il programma, per ogni suo arco, crea un'altra tabella chiamata "ntk_from_XXX" con identificativo YYY, dove XXX è il MAC address del nodo vicino, YYY è il primo identificativo libero nel file /etc/iproute2/rt_tables. Questa tabella conterrà rotte da esaminare solo per i pacchetti da inoltrare che ci sono pervenuti attraverso questo arco. Il programma quindi aggiunge una regola che dice di guardare la tabella "ntk_from_XXX" se il pacchetto da trasmettere è marcato con il numero YYY. Inoltre istruisce il kernel di marcare con il numero YYY i pacchetti che hanno XXX come MAC di provenienza.

Anche le varie tabelle "ntk_from_XXX" conterranno solo rotte di pertinenza della rete Netsukuku, cioè quelle con destinazione nello spazio 10.0.0.0/8.

Inoltre, sia la tabella "ntk" sia le varie "ntk_from_XXX" conterranno la rotta "unreachable 10.0.0.0/8". Questa rotta verrà presa in esame solo se un pacchetto ha una destinazione all'interno dello spazio di Netsukuku, ma per tale destinazione non esistono altre rotte valide con una classe più restrittiva. In altre parole, una destinazione per la quale non si conosce nessun percorso. Questa particolare rotta dice che il pacchetto non potrà giungere a destinazione e il suo mittente ne va informato.

Sulla base degli eventi segnalati dal modulo QSPN, e se necessario richiamando i suoi metodi pubblici, il programma qspnclient popola e mantiene le rotte nelle tabelle "ntk" e "ntk_from_XXX". I percorsi segnalati dal modulo QSPN contengono sempre un arco del nodo corrente come passo iniziale e da tale arco si può risalire all'indirizzo di scheda del vicino. Le rotte nelle tabelle "ntk" e "ntk_from_XXX" infatti devono avere come campo gateway (gw) l'indirizzo di scheda del vicino, non il suo indirizzo Netsukuku.

Per ogni percorso scelto dal programma qspnclient per entrare in una tabella, in realtà il programma inserisce nella tabella fino a 4 rotte. Sia i il livello del g-nodo destinazione del percorso. Queste sono le rotte:

Quando il programma ha finito di usare una tabella (ad esempio se un arco che conosceva non è più presente, oppure se il programma termina) svuota la tabella, poi rimuove la regola, poi rimuove il record relativo dal file /etc/iproute2/rt_tables.