Utilizziamo i device TunTap per realizzare una piccola VPN in userspace su GNU/Linux (parte 1).

Di recente sulla mailing list italiana di python è nata una discussione interessante sull’implementazione di stack
IP utilizzando python. Una scuola di pensiero relegava questo tipo di programmazione in kernel space, ma io faccio parte
dell’altra scuola…

Però sono un perlista (perdonatemi) quindi ora illustrerò come realizzare una semplice VPN completamente in userspace
(niente ipsec, kame e amici) in perl.

Premetto che sono disponibili molte implementazioni di VPN in user space (in cima a tutte openvpn) che svolgono il lavoro egregiamente,
io mi limiterò a svelarne la logica di funzionamento.

Il primo componente che incontreremo nello sviluppo è TunTap, si tratta di un device (lo trovate in /dev/net/tun) che fornisce due dispositivi di rete virtuale, tun e tap.

Tun (che è quello su cui lavoreremo) crea una interfaccia di rete che ridirigerà tutti i pacchetti IP allo user space, permettendone la manipolazione.
Tap invece lavora a un layer più basso in quanto passerà allo userspace anche gli header ethernet (che a noi non interessano).

Iniziamo quindi con il creare la nostra interfaccia tun (che chiameremo tun0), una volta creata potremo trovarla nell’elenco delle interfacce di rete con un semplice ifconfig -a.


root@hagrid:~ # ifconfig -a

eth0      Link encap:Ethernet  HWaddr 00:11:D8:4C:E0:F1
          inet addr:192.168.1.3  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::211:d8ff:fe4c:e0f1/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:5069906 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4817721 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:5898103300 (5.4 GB)  TX bytes:357352640 (340.7 MB)
          Interrupt:17

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:4387 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4387 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:95862 (93.6 KB)  TX bytes:95862 (93.6 KB)

root@hagrid:~ # perl stacktrace_vpn.pl
root@hagrid:~ # ifconfig -a

eth0      Link encap:Ethernet  HWaddr 00:11:D8:4C:E0:F1
          inet addr:192.168.1.3  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::211:d8ff:fe4c:e0f1/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:5075504 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4822417 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:5905339876 (5.4 GB)  TX bytes:357704869 (341.1 MB)
          Interrupt:17

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:4392 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4392 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:96130 (93.8 KB)  TX bytes:96130 (93.8 KB)

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          POINTOPOINT NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

Ok, tun0 è apparso tra le interfacce, ma cosa ha fatto lo script stacktrace_vpn.pl?
Vediamolo nel dettaglio:


use Fcntl ;

sysopen TUN,'/dev/net/tun',O_RDWR ;

my $ifr = pack('Z16 s','', 0x0001|0x1000) ;

ioctl TUN, 0x400454ca, $ifr ;

while(1) {}

Per prima cosa lo script apre in lettura+scrittura il device /dev/net/tun (fin qui nulla di strano, a parte l’inclusione del modulo Fcntl che esporta la costante O_RDWR).
La seconda linea è un po’ criptica, serve per simulare la struttura C che il device tuntap richiede per essere configurato:

struct ifreq
{
        char    ifrn_name[16];
        short   ifr_flags;
};

Guardando la struttura dal punto di vista di perl possiamo rappresentarla come una stringa di 16 caratteri (null padded) + un numero (in questo caso un signed short).
Utilizzando la funzione pack possiamo riempire la nostra struttura:

pack('Z16 s','', 0x0001|0x1000) ;

il primo argomento (consultare il man perlfunc per maggiori dettagli) specifica la tipologia di dati, (stringa di 16 + short), il secondo è il nome dell’interfaccia, lasciandolo
vuoto lo assegnerà il sistema.
L’ultimo argomento sono i flag, dando un’occhiata alla documentazione
ufficiale di tuntap
e ai suoi header C:

#define IFF_TUN     0x0001
#define IFF_NO_PI       0x1000

la prima costante indica che vogliamo creare un dispositivo tun (l’alternativa è tap), la seconda che non vogliamo che nei pacchetti passati allo userspace ci siano gli header incapsulati
da tuntap. Questa opzione ci permetterà di ricevere solamente i pacchetti IP grezzi.

La funzione ioctl è una chiamata di sistema che serve per inviare ‘comandi’ (diversi dai soliti open/read/write/close) ai device. In questo caso stiamo inviando il comando #TUNSETIFF (costante impostata su 0x400454ca).
Dopo la chiamata ioctl l’interfaccia è aggiunta a quelle di sistema ma, se il processo dovesse chiudersi, l’interfaccia sarebbe rimossa, quindi, in questa prima fase, inseriremo
un ciclo (while(1) {}) per evitarlo.

tun0 è ora configurabile come una qualsiasi scheda di rete:

root@hagrid:~ # ifconfig tun0 10.0.17.1 netmask 255.255.255.0

root@hagrid:~ # ifconfig tun0

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          inet addr:10.0.17.1  P-t-P:10.0.17.1  Mask:255.255.255.0
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:500
          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)

root@hagrid:~ # route -n

Kernel IP routeing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
10.0.17.0       0.0.0.0         255.255.255.0   U     0      0        0 tun0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 eth0
0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 eth0

Indirizzo e routing sono configurati, iniziamo la parte divertente, catturare i pacchetti.

Il bello dei device tuntap è che permettono di accedere ai dati inviati alle interfacce semplicemente leggendo dal
file descriptor:

while(sysread TUN, my $buffer, 4096) {
    print $buffer."\n" ;
}

Sostituiamo il loop del primo sorgente con questa nuova versione, che legge dal file descriptor TUN (al massimo 4096 byte, limite sufficiente in qualsiasi contesto) e lo copia nella variabile $buffer.
Rilanciamo lo script modificato, configuriamo tun0 (con ifconfig) e pinghiamo l’indirizzo 10.0.17.1, se tutto va bene riceveremo risposta normalmente.
Però il nostro script non stamperà il contenuto di $buffer a schermo, che succede?

Lo stack di rete di Linux, ad ogni richiesta, verifica che l’IP richiesto non sia configurato sul sistema, in tal caso reindirizza il pacchetto all’interfaccia di loopback. Quindi è lo a rispondere
ai nostri ping e non tun0.

Dobbiamo quindi forzare il passaggio dei pacchetti su tun0, ma come possiamo fare ?

Semplice, la nostra tabella di routing ci dice che invierà qualsiasi pacchetto per gli indirizzi 10.0.17.X a tun0 (escluso 10.0.17.1 che come abbiamo visto verrà passato su loopback).

Pinghiamo allora un indirizzo compreso tra 10.0.17.2 e 10.0.17.254 e vedremo finalmente passare i dati al nostro script!!!

Contenete l’entusiasmo, poichè come presto noterete il comando ping non otterrà risposta e nello stesso tempo il vostro terminale (dove avete lanciato lo script) sarà diventato inutilizzabile per via
dell’immane mole di dati binari che gli vengono inviati.

Chiudiamo il terminale impazzito e cerchiamo di interpretare i dati che gli vengono inviati da tun0.
Ovviamente non c’è bisogno di programmare un intero parser di pacchetti IP, possiamo utilizzare un modulo molto carino disponibile su CPAN:

NetPacket::IP

questo modulo prende in input un pacchetto IP e ne restituisce un comodo hash con tutte le informazioni di cui abbiamo bisogno.

Vediamo la nuova versione dello script:

use NetPacket::IP;

sysopen TUN,'/dev/net/tun',O_RDWR ;

my $ifr = pack('Z16 s','', 0x0001|0x1000) ;

ioctl TUN, 0x400454ca, $ifr ;

while(sysread TUN, my $buffer, 4096) {
    my $pkt = NetPacket::IP->decode($buffer) ;
    print "ricevuto pacchetto da ".$pkt->{src_ip}." per ".$pkt->{dest_ip}."\n" ;
}

Rilanciamolo, configuriamo l’interfaccia (con ifconfig) e riproviamo il ping su uno degli indirizzi non associati alla nostra macchina (il range da .2 a .254).
Ping non otterrà ancora risposta ma lo script inizierà a stampare dei chiari messaggi su mittente e destinatario del pacchetto.

Fin qui siamo in grado di prendere dati dallo stack di rete di Linux e manipolarli, ma qui si sta parlando di reti, e che rete sarebbe senza dei nodi che comunicano tra
di loro ?
Muniamoci quindi di un secondo computer, e passiamo a un po’ di teoria.

Le interfacce TunTap sono dei semplici strati software, non hanno modo di comunicare con sistemi esterni senza un qualche tipo di trasporto.
L’unico modo
quindi di far comunicare 2 interfacce tun su 2 computer diversi è di incapsulare il loro traffico in normali pacchetti da far passare attraverso le schede di rete.

In poche parole non faremo altro che prendere i dati passati allo user space dall’interfaccia tun e spedirli tramite una normale connessione UDP all’altro computer, su cui girerà
un applicativo speculare al nostro che prenderà i dati dalla rete e li riverserà su tun.

Useremo UDP perché è un protocollo più snello e semplice.
Fortunatamente perl ci mette a disposizione l’ottimo modulo IO::Socket per pasticciare, quindi potremo subito iniziare a costruire il secondo strato della nostra VPN.

Ricapitoliamo la nostra configurazione prima di passare al codice:

abbiamo 2 computer, ognuno con una scheda di rete e una interfaccia tun già configurata (tramite il nostro script perl):

Tabella 1. Riassunto interfacce e indirizzi
computer eth0 tun0
hagrid 192.168.1.3 10.0.17.1
voldemort 192.168.1.5 10.0.17.2

Per prima cosa entrambi i sistemi dovranno aprire un socket UDP in ascolto su una porta a scelta (facciamo 9999 e non ne parliamo più).

use IO::Socket;

my $socket = new IO::Socket::INET(Proto => 'udp', LocalPort => 9999) ;

per leggere i dati da un socket si usa la funzione recv:

while(my $peer = $socket->recv(my $buffer,4096)) {
    print $buffer ;
}

Notare i soliti 4096 byte e la variabile $peer, che conterrà l’indirizzo (IP e porta) del mittente del pacchetto.

Ora però ci troviamo di fronte a un problema.
Il nostro script dovrà leggere contemporaneamente dal file descriptor di tun0 e dal socket UDP.
Entrambe le due chiamate di lettura (sysread e recv)
sono bloccanti (il processo è fermo in attesa di dati), quindi come facciamo a gestire i due sistemi senza trovarci nella situazione di attendere dati all’infinito?

Bacchettata sulle mani di tutti quelli che hanno pensato ai thread ! Siamo in ambiente UNIX, e abbiamo a disposizione le tecniche di non-blocking I/O, in
particolare la syscall select().

Questa funzione di sistema può monitorare diversi file descriptor contemporaneamente e di volta in volta ci comunicherà quando uno di questi ha ‘qualcosa da segnalare’.
Nel nostro caso faremo monitorare il file descriptor di tun0 e il socket UDP.

In perl è disponibile il modulo IO::Select che semplifica di molto l’implementazione:

use IO::Select;

my $sel = IO::Select->new();

$sel->add(\*TUN) ;
$sel->add($socket) ;

while(@ready = $sel->can_read) {
    #...
}

il metodo can_read resta in attesa di una notifica da parte dei vari file descriptor monitorati e, in caso uno di questi abbia dati da leggere, esce e restituisce un array
di tutti i file descriptor con qualcosa di interessante da dirci.

while(@ready = $sel->can_read) {
    foreach my $fh (@ready) {
        if ($fh == \*TUN) {
            print "dati ricevuti sul device tuntap\n" ;
        }
        elsif ($fh == $socket) {
            print "dati ricevuti sul socket udp\n" ;
        }
    }
}

Ora sappiamo quando leggere i dati da tun o dal socket UDP, l’obiettivo è che tutti i dati ricevuti sul socket (quindi quelli inviati da altri computer) siano copiati sul device tuntap, mentre quelli ricevuti
su tun0 siano instradati via UDP ai computer esterni.

Per prima cosa dobbiamo costruirci una tabella di tutte le corrispondenze tra IP delle interfacce tun0 e quelli di eth0, un hash sarà sufficiente:

my %node ;
$node{'10.0.17.1'} = '192.168.1.3' ;
$node{'10.0.17.2'} = '192.168.1.5' ;

Possiamo quindi sapere in base all’IP di destinazione di un pacchetto ricevuto su tun0 a quale computer inviarlo.
Ovviamente l’esempio prende in considerazione 2 soli computer ma è sufficiente aggiungere altri elementi all’hash %node per gestire una rete più grande.

Il ciclo sui file descriptor diventerà così:

while(@ready = $sel->can_read) {
    foreach my $fh (@ready) {
        if ($fh == \*TUN) {
            if (sysread(TUN, my $buffer, 4096)>0) {
                print "dati ricevuti sul device tuntap\n" ;
                my $pkt = NetPacket::IP->decode($buffer) ;

                # verifico che l'indirizzo  a cui è destinato il pacchetto sia nell'hash %node
                if (exists($node{$pkt->{dest_ip}})) {

                    # apro un socket UDP sulla porta 9999 del destinatario reale del pacchetto
                    my $udp_sock = IO::Socket::INET->new(Proto => 'udp', 
                        PeerAddr => $node{$pkt->{dest_ip}}, PeerPort => 9999) ;

                    # invio il pacchetto al socket
                    $udp_sock->send($buffer) ;
                }
            }
        }
        elsif ($fh == $socket) {
            $socket->recv(my $buffer,4096) ;
            if ($buffer) {
                print "dati ricevuti sul socket UDP, inoltro a tun0\n" ;
                syswrite TUN,$buffer ;
            }
        }
    }
}

Il codice è abbastanza corposo ma i commenti dovrebbero aiutare, da notare l’utilizzo del modulo NetPacket::IP (visto prima) che ci serve per estrapolare
l’indirizzo di destinazione di un pacchetto.

Ora siamo pronti a testare il nostro ‘tunnel’ (eh si è proprio un tunnel, è ancora presto per parlare di VPN).

Questo è il codice dello script, basterà lanciarlo su ognuno dei 2 computer (o di più se volete strafare) e dopo aver assegnato gli indirizzi IP alle due interfacce tuntap
i 2 nodi comunicheranno attraverso gli IP della classe 10.0.17.X.

use IO::Socket;
use IO::Select;
use Fcntl;
use NetPacket::IP;

my %node ;
$node{'10.0.17.1'} = '192.168.1.3' ;
$node{'10.0.17.2'} = '192.168.1.5' ;

sysopen TUN,'/dev/net/tun',O_RDWR ;
my $ifr = pack('Z16 s','', 0x0001|0x1000) ;
ioctl TUN, 0x400454ca, $ifr ;

my $socket = new IO::Socket::INET(Proto => 'udp', LocalPort => 9999) ;

my $sel = IO::Select->new();

$sel->add(\*TUN) ;
$sel->add($socket) ;

while(@ready = $sel->can_read) {
    foreach my $fh (@ready) {
        if ($fh == \*TUN) {
            if (sysread(TUN, my $buffer, 4096)>0) {
                my $pkt = NetPacket::IP->decode($buffer) ;

                # verifico che l'indirizzo  a cui è destinato il pacchetto sia nell'hash %node
                if (exists($node{$pkt->{dest_ip}})) {

                    # apro un socket UDP sulla porta 9999 del destinatario reale del pacchetto
                    my $udp_sock = IO::Socket::INET->new(Proto => 'udp',
                            PeerAddr => $node{$pkt->{dest_ip}}, PeerPort => 9999) ;

                    # invio il pacchetto al socket
                    $udp_sock->send($buffer) ;
                }
            }

        }
        elsif ($fh == $socket) {
            $socket->recv(my $buffer,4096) ;
            if ($buffer) {
                print "dati ricevuti sul socket UDP, inoltro a tun0\n" ;
                syswrite TUN,$buffer ;
            }
        }
    }
}

Tutti pronti per il test, copiamo lo script su tutti i computer che parteciperanno alla rete, lanciamolo, configuriamo gli IP e proviamo il nostro solito ping:

Su hagrid:

root@hagrid:~ # perl stacktrace_vpn.pl
root@hagrid:~ # ifconfig tun0 10.0.17.1 netmask 255.255.255.0

Su voldemort:

root@voldemort:~# perl stacktrace_vpn.pl
root@voldemort:~# ifconfig tun0 10.0.17.2 netmask 255.255.255.0

Su hagrid:

root@hagrid:~ # ping 10.0.17.2
PING 10.0.17.2 (10.0.17.2): 48 data bytes
56 bytes from 10.0.17.2: icmp_seq=0 ttl=64 time=2.552 ms
56 bytes from 10.0.17.2: icmp_seq=1 ttl=64 time=2.233 ms
...

Se tutto è andato bene i 2 nodi staranno comunicando e avrete lo schermo pieno di risposte ai vostri ping.

Dopo il primo entusiasmo vi starete chiedendo che cavolo ci fate con questo applicativo, giusta domanda: sarà lo scheletro per la nostra VPN.
VPN sta per Virtual Private Network, per ora abbiamo implementato il Virtual, nelle prossime linee vedremo il Private, forse la parte più facile
e che darà al nostro script un tocco di utilità (che ora non ha).

Ora l’obiettivo è crittografare i dati che i vari nodi della nostra rete si scambiano, il modo più semplice è usare la tecnica delle preshared keys, ovvero
tutti i nodi utilizzano una chiave unica per crittare e decrittare il traffico.
Ovviamente la sicurezza dipende tutta dalla chiave che dovrete difendere con tutte le vostre forze.

Tra i vari algoritmi crittografici a disposizione ho scelto il Blowfish, non solo perché il nome è simpatico ma anche perché
è uno dei pochi algoritmi non coperto da brevetti.

Su CPAN si può trovare il modulo Crypt::CBC che permette di crittografare i dati ricevuti sul nostro dispositivo tuntap.

L’utilizzo è molto semplice:

use Crypt::CBC;
my $cipher = Crypt::CBC->new( -key => 'evviva stacktrace!!!',-cipher => 'Blowfish') ;

print $cipher->encrypt('dati da proteggere') ;

Critteremo i dati per l’invio a un server UDP:

$udp_sock->send($cypher->encrypt($buffer)) ;

E li decritteremo per passarli a tun0:

syswrite TUN,$cypher->decrypt($buffer) ;

applichiamolo al nostro script:

use IO::Socket;
use IO::Select;
use Fcntl;
use NetPacket::IP;
use Crypt::CBC;

my $preshared_key = 'evviva stacktrace!!!' ;
my $cipher = Crypt::CBC->new( -key => $preshared_key, cipher => 'Blowfish') ;

my %node ;
$node{'10.0.17.1'} = '192.168.1.3' ;
$node{'10.0.17.2'} = '192.168.1.5' ;

sysopen TUN,'/dev/net/tun',O_RDWR ;
my $ifr = pack('Z16 s','', 0x0001|0x1000) ;
ioctl TUN, 0x400454ca, $ifr ;

my $socket = new IO::Socket::INET(Proto => 'udp', LocalPort => 9999) ;

my $sel = IO::Select->new();

$sel->add(\*TUN) ;
$sel->add($socket) ;

while(@ready = $sel->can_read) {
    foreach my $fh (@ready) {
        if ($fh == \*TUN) {
            if (sysread(TUN, my $buffer, 4096)>0) {
                my $pkt = NetPacket::IP->decode($buffer) ;

                # verifico che l'indirizzo  a cui è destinato il pacchetto sia nell'hash %node
                if (exists($node{$pkt->{dest_ip}})) {

                    # apro un socket UDP sulla porta 9999 del destinatario reale del pacchetto
                    my $udp_sock = IO::Socket::INET->new(Proto => 'udp',
                            PeerAddr => $node{$pkt->{dest_ip}}, PeerPort => 9999) ;

                    # invio il pacchetto al socket
                    $udp_sock->send($cipher->encrypt($buffer)) ;
                }
            }

        }
        elsif ($fh == $socket) {
            $socket->recv(my $buffer,4096) ;
            if ($buffer) {
                print "dati ricevuti sul socket UDP, inoltro a tun0\n" ;
                syswrite TUN,$cipher->decrypt($buffer) ;
            }
        }
    }
}

Abbiamo finalmente la V e la P della nostra VPN, nel prossimo articolo implementeremo una rete più articolata (utilizzando il nostro script su due router) e vedremo dei sistemi
di protezione più robusti rispetto alle pre-shared keys.

Comments

  1. Molto interessante.
    Sono un ‘perlista’ anche io ma non avrei mai immaginato che si potessero manipolare interfacce di rete via perl…

  2. Complimenti.

  3. Roberto Franchini says:

    Grazie! Adesso comincio a capire come funziona hamachi. Mi faceva un po’ paura perchè non capivo che cosa combinava, ma è così comodo!

  4. claudio covellin e says:

    complimenti; un bell’articolo che spiega in modo preciso, semplice e chiaro come funzionano i device tun e tap e che consente di capire le modalità di interazione fra tale device ed i kernel. Vale molto più di decine di documenti che ho letto sulle vpn e dai quali non si ricava praticamente nulla, se non un’idea confusa ed approssimata.

  5. Bell’ articolo! Lettura molto gradevole e interessante. Anche il tono usato e’ accattivante. Grazie! Il codice e’ ben comprensibile e facilmente collaudabile.
    Ho scritto un articolo simile, sull’ uso di ioctl(2) con Perl che gli amici di http://www.perl.it sembra stiano adattando per la pubblicazione, ma ancora non’ lo hanno fatto:
    http://www.perl.it/documenti/articoli/2007/05/5980defef3182df2cffcf4c898861bd25bf7a6d9.html

    E’ per lo steso spirito Hacker chje il tuo articolo mi e’ piaciuto molto. Grazie ancora.
    Roberto.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.