Lo strato applicativo di Internet

Cattura del traffico, compilazione, e socket

In questa attività, si tenta di acquisire una visione congiunta dei tre aspetti della programmazione, delle interfacce utente, e dell'analisi di protocollo.

Strumenti di Cattura e di analisi delle intestazioni

Sebbene si possa visualizzare il traffico in transito sulle interfacce di rete (operazione in gergo chiamata sniffing) anche mediante strumenti testuali come tcpdump o iptraf, od anche Etherape (che fornisce una interessante visualizzazione spaziale delle proprie connessioni attive), il programma più diffuso e ricco é sempre stato Ethereal, che di recente ha cambiato nome in Wireshark. Mentre tcpdump e wireshark fanno entrambi uso della libreria libpcap, iptraf accede all'interfaccia di rete tramite chiamate dirette al kernel.

L'operatività del programma è ampiamente documentata dalla esauriente documentazione presente sul suo sito, e presso il suo wiki, a cui rimandiamo per ogni dubbio.

Dopo aver lanciato wireshark in modalità privilegiata (digitando quindi il comando sudo wireshark, o rintracciandolo nel menù Applications/Internet/Wireshark (as root), ed immettendo la nostra password di utente non privilegiato), cliccare sull'icona in alto a sinistra che permette di vedere le interfacce disponibili, in modo da osservare la finestra sottostante, che elenca le interfacce di cattura, e già mostra su quali di queste si sta osservando traffico.

Configurazione delle opzioni di cattura

Nel caso in cui si desideri osservare il traffico diretto verso localhost, si scelga l'interfaccia lo; se invece si opera verso un computer remoto, si scelga eth0, o se in dubbio, any (che le cattura entrambe). Si può iniziare subito la cattura, premendo Start, oppure modificare le opzioni, giungendo ad una seconda finestra.

Menù principale e cattura

Possiamo ora eseguire un live capture, oppure caricare (File/Open..) in Wireshark questo file ottenuto pressoun computer di ingegneria, durante la visita su wikipedia della pagina dell'8 Marzo, della festa della donna, e della mimosa. D'altra parte, il wiki di wireshark contiene molti esempi di file catturati.

Nella barra principale troviamo molte utili scorciatoie, tra cui evidenziamo

Menù di Analisi

Questo menù presenta delle opzioni che possono spesso essere richiamate anche con il tasto destro del mouse, dopo aver evidenziato nella finestra superiore un particolare pacchetto, oppure una particolare intestazione di un pacchetto, od un particolare campo. Citiamo:

Possiamo ora sperimentare ad eliminare via via i diversi protocolli presenti, fino a focalizzarci solo sul traffico effettivamente pertinente alla navigazine web di cui all'esempio.

Menù statistiche

Sotto questo pop-down sono presenti molte utilissime funzioni. Mentre il sommario presenta dei dati riassuntivi, la gerarchia dei protocolli, le conversazioni e gli endpoints mostrano in dettaglio le entità presenti, e le loro relazioni.

Esercitazione

Dopo aver caricato il file di traffico catturato, eseguire le analisi indicate ai punti 9-18 della prima prova di questa esercitazione. Quindi, sempre a partire dallo stesso file di cattura, eseguire le analisi indicate ai punti 12-16 e 18-20 della seconda prova della stessa esercitazione.


A tu per tu con il codice

Per sperimentare l'uso di Wireshark, e mettere in pratica i concetti illustrati nel capitolo relativo al Network Programming, procediamo alla compilazione del codice di cui si è discusso, in modo da analizzare il traffico sviluppato utilizzando i programmi client-server, e strada facendo, aggiungiamo varianti e particolarità.

Mentre nel precedente capitolo abbiamo illustrato i comandi di base per acquisire un minimo di operatività tramite finestra terminale, descriviamo ora i concetti e le operazioni necessarie per passare dalla descrizione di un programma basata su di un linguaggio ad alto livello come il c, al formato eseguibile che può effettivamente essere fatto girare sul computer.

Compilazione

Apriamo innanzitutto una finestra terminale, dove è possibile impartire comandi con la tastiera. Creiamo quindi una directory di lavoro:

$ cd ~
$ pwd
$ mkdir codice
$ cd codice

Rechiamoci quindi sulla directory contenente archivio(o la versione compressa), e decomprimiamolo presso il nostro computer. Il file compila è uno script shell, ossia un file contenente gli stessi comandi che è possibile impartire in una finestra terminale, e nel caso non lo sia già (ls -l compila), deve essere reso eseguibile con il comando chmod 755 compila. Quindi eseguiamolo, scrivendo ./compila (il ./ è necessario, altrimenti potrebbe esistere un programma con lo stesso nome in una delle directory mostrate con echo $PATH, e verrebbe eseguito quello al suo posto). Risolviamo i possibili errori. Osserviamo il risultato dell'operazione, ed i nuovi files presenti nella directory.

Il comando che serve per compilare un generico programma C di nome pippo.c, e che viene eseguito da compila per ognuno dei files presenti, è

$ cc -o eseguibile pippo.c

in cui l'opzione -o provvede a dare un nome all'eseguibile (nell'esempio, il nome è eseguibile) che altrimenti, prenderebbe il nome di default a.out.

File di comandi

Come possiamo osservare, il file compila inizia in un modo un pò particolare, chiamato shebang (o hashbang). In assenza di questa linea, il file potrebbe essere passato come argomento all'interprete dei comandi (eseguendo bash compila), che eseguirebbe tutte le direttive che vi trova. D'altra parte, anche mandando lo script direttamente in esecuzione come stiamo invece facendo, è sempre l'interprete bash che deve decidere il da farsi. Se si tratta di un vero programma, esegue una fork e gli cede il controllo; se invece localizza in testa i caratteri #!, dopo la fork cede il controllo al programma che è scritto subito appresso a #! (nella fattispecie, un'altra istanza di se stesso), passandogli lo script come argomento, e termina. A questo punto, sembrerebbe che ci siamo cacciati in un loop senza fine, ed invece, dato che il file di comandi è stato appunto passato come parametro, e non mandato direttamente in esecuzione, questo bash-figlio inizia felicemente ed eseguire i comandi che si trovano nel file, ignorando questa volta la prima linea, anche perchè.. il simbolo # è quello che si usa per scrivere i commenti !

Makefile

Ora che abbiamo imparato le basi dei file di comandi, ci rendiamo conto che compilare ogni volta tutto quanto anche se si modifica un solo file (come suggerito nel seguito), potrebbe essere uno spreco di risorse, non tanto ora che i programmi sono piccolini, ma più in generale, qualora si abbia a che fare con progetti di dimensioni considerevoli. Ci soccorre allora il comando make, che viene in nostro aiuto usando le informazioni presenti in un file chiamato, per l'appunto, Makefile (con l'iniziale maiuscola), e che rappresenta una sorta di linguaggio di programmazione dichiarativo, in quanto specifica cosa deve fare make (e non come). In rete sono presenti alcuni buoni tutorial a riguardo.

Il vantaggio di usare il make, è che nel Makefile è definito un albero di dipendenze, descritte dichiarando quale file dipende da quale altro. Così, quando un file di un gruppo è modificato, è possibile ri-generare solo i files che dipendono da quest'ultimo, unicamente osservando se la loro data di creazione è precedente o successiva a quella dei files da cui dipendono. Un primo esempio di Makefile funzionante è

default: all

all: tcp udp

tcp:            server client selectserver
udp:            listener talker broadcaster

server:         server.o
client:         client.o
listener:       listener.o
talker:         talker.o
selectserver:   selectserver.o
broadcaster:    broadcaster.o

server.o:       server.c
client.o:       client.c
listener.o:     listener.c
talker.o:       talker.c
selectserver.o: selectserver.c
broadcaster.o:  broadcaster.c

In questo esempio, alla sinistra dei due punti sono presenti i cosidetti target, ossia obbiettivi, che possono essere citati su di una linea di comando come make target, intendendo con questo richiedere la generazione del target richiesto. A sua volta un target può dipendere da un altro target, e così si definisce come all dipenda da tcp e udp, mentre tcp dipende da server, client e selectserver. A sua volta, sever dipende da server.o, che a sua volta ancora, dipende da server.c. In questo modo, è possibile invocare

make all      # compila tutto
make tcp      # compila server, client e selectserver
make          # prende all come target di default
make listener # compila solamente listener

Una perplessità leggittima che può sorgere a questo punto, è come faccia make a capire cosa c'è da fare per soddisfare una dipendenza. La risposta è che esistono delle regole deduttive, per cui make sa che se un .o dipende da un .c, allora il .o si ottiene compilando il .c. Ma andiamo con ordine.

Macro e regole

Allo scopo di rendere un Makefile più compatto esistono alcune particolarità. Le istruzioni macro consentono di associare una serie di simboli terminali ad una unica macro chiamata (as es.) OBJ, che può essere ri-espansa successivamente, citandola con la notazione $(OBJ). Ad esempio, ponendo OBJS = server.o client.o listener.o talker.o selectserver.o broadcaster.o, la stessa sequenza di nomi di codici oggetto è ri-ottenuta successivamente mediante la notazione $(OBJ). Non solo, ma le estensioni dei singoli nomi in cui $(OBJ) si espande, possono essere ulteriormente trasformate al volo: ad esempio, se poniamo OBJ = pippo.c, citando $(OBJ:.c=.o), otteniamo l'espansione in pippo.o.

Le regole consentono di abbinare ad un target, la sequenza di comandi che permette di risolvere la dipendenza espressa dal target. La sintassi di questo costrutto è esprimibile come

target: nome(i) file di ingresso
    azioni

in cui, dopo la linea che esprime una dipendenza, è posta una seconda linea (che inizia con un TAB) che esprime una (o più) azione(i), da applicare perché sia soddisfatta. In base a queste nuove nozioni, al Makefile precedente possono essere aggiunte in testa ed in coda le istruzioni

OBJS = server.o client.o listener.o talker.o selectserver.o broadcaster.o
.
.
.
clean:
    -rm -f $(OBJS) $(OBJS:.o=) *~

rebuild: clean all

che definiscono la macro OBJS come uguale a tutti i codici oggetto, ed i target clean e rebuild, tali che

L'uso della espansione di una macro, ci evita di dover scrivere parecchie linee di codice uguali, cosicché invece di dover scrivere una regola che specifica come passare da un .c ad un .o, ossia

server.o: server.c
    cc -c server.c

per ognuno dei codici oggetto da generare, possiamo scrivere

CC = gcc # utilizziamo gcc anziché cc
OBJS = server.o client.o listener.o talker.o selectserver.o broadcaster.o

obj: $(OBJS)

%.o: %.c
    $(CC) -c $<

in modo che, invocando make obj, si ottiene la compilazione di tutti i sorgenti in codice oggetto, in base alla semantica delle seguenti macro predefinte:

la macro si espande in
%.c qualunque file con estensione .c presente nella directory
%.o un file con estensione .o, e con lo stesso nome della dipendenza della regola, e che compare come dipendenza di un'altra regola
$(CC) cc, il compilatore standard, o quale altro compilatore definito in una macro cc =
$< l'elemento che compare dal lato dipendenza della regola
$@ il nome del target

Questa logica è poi ulteriormente potenziata dalla esistenza delle regole deduttive, che permettono a make di stabilire autonomamente quale sia l'azione necessaria a soddisfare una determinata dipendenza, cosicché ad esempio, per ottenere un file eseguibile, make prova a linkare un codice oggetto dallo stesso nome. Così, anche se dovremmo scrivere

%.o: %.c
    $(CC) -c -o $@ $<

per generare i .o a partire dai .c, e

%: %.o
    $(CC) -o $@ $<

per generare gli eseguibili, il Makefile definitivo riporta queste istruzioni commentate.

Esecuzione

Ora che abbiamo gli eseguibili, possiamo verificare il funzionamento dei server parallelo e seriale studiati. Poniamoci nella directory con i files scompattati, ed impartiamo il comando make.

Errori di compilazione

Come dite? Il comando make (e/o compila) risponde con una serie di errori, il primo dei quali dice "error: stdio.h: nessun file o directory" ?.... aaahhh si, dovete installare con Synaptic, il pacchetto build-essential, che a sua volta determina l'installazione di tutto ciò che occorre per compilare!

Server parallelo TCP

Nella finestra terminale aperta, lanciamo ./server ed in una nuova finestra, ./client 127.0.0.1: constatiamo che il risultato è quello mostrato a lato - la presenza del flag PUSH è presumibilmente legata alla immediata chiusura del socket da parte del client. Ora, proviamo a svolgere questi esperimenti:

copiamo tutti i files in una nuova directory, che chiamiamo test. Modifichiamo il codice del server, in modo che

Connessioni UDP

Ora, proviamo a lanciare la coppia di programmi che fanno uso di connessioni UDP, ossia listener e talker:

Qualcuno potrà chiedersi come può una applicazione venire a conoscenza di questa notifica icmp: questo forum indica di consultare la propria man 7 ip, e usare la socket option ip_recverr. Un'altra domanda potrebbe essere: come fa una applicazione (ad es. il ping) a creare pacchetti icmp? ...deve creare un socket raw!

Diagrammi temporali, UML

Lo scambio di pacchetti può essere visualizzato con Wireshark in forma di Diagramma Temporale, selezionando Statistics/FlowGraph, e scegliendo quindi una modalità di visualizzazione. Un modo più formale di ottenere lo stesso risultato è quello di costruire un vero e proprio Diagramma di Sequenza così come definito da UML, ad esempio mediante l'uso di uno strumenti appositi (UMbreLlo, Gaphor, BoUML), con cui editare un modello, e quindi produrre diagrammi temporali come quello mostrato sopra.

Select

E' ora la volta di sperimentare il funzionamento di selectserver. Il server di chat viene lanciato su di un unico computer, di cui ti annoti l'indirizzo, e su cui è in esecuzione EtherApe, che realizza una visualizzazione spaziale degli host corrispondenti. Quindi, sia tu che tutti gli altri, iniziate a catturare il traffico, eseguite su di una finestra terminale il comando telnet indirizzo-del-server 9034, ed iniziate a chattare. Ad un certo punto, il server viene chiuso.

Telnet

Il comando telnet implementa il lato client del protocollo telnet, e consente di collegare lo standard input (la tastiera) ad un socket TCP remoto, stampando su standard output (lo schermo) ciò che la applicazione remota invia, sempre mediante la stessa connessione TCP. Per questo motivo, viene spesso usato per dialogare con applicazioni server che adottano protocolli testuali (come, ad esempio SMTP, HTTP, SIP). Dato che ogni carattere immesso da tastiera, viene trasmesso all'altro estremo della comunicazione, si pone il problema di come terminare la comunicazione, usando la tastiera. In questo caso, infatti, il normale control-c non ha effetto. La soluzione ci viene però indicata al momento della partenza della connessione:

alef@alef-laptop:~$ telnet 151.100.122.122 80
Trying 151.100.122.122...
Connected to 151.100.122.122.
Escape character is '^]'.

Sapendo che il carattere ^ è usato per indicare la pressione del tasto Control, capiamo che per terminare il telnet, e riottenere il prompt dei comandi, occorre digitare la sequenza di escape, ossia la combinazione Control+parentesi_quadra_chiusa. Quest'ultima, sulla tastiera italiana si realizza premendo assieme AltGr e ], e quindi in definitiva, occorre premere tre tasti in contemporanea. Dopodiché, premere il tasto Invio. A questo punto il nostro programma telnet ci presenta il suo prompt telnet>, ed in questa fase, possiamo impartire dei comandi locali, come ad esempio, help. Per uscire, si può invece impartire close, oppure anche solo il tasto q.

Broadcast

Ora è il momento di sperimentare il programma broadcaster

Iptables

Possiamo sperimentare ora qualche funzionalità di firewall, ad esempio mantenendo in esecuzione il nostro server tcp e, dopo averne verificato la funzionalità mediante il rispettivo client, impostare una regola del tipo

sudo iptables  -t filter -I INPUT -p tcp --dport 3490 -j REJECT

in modo da bloccare il traffico diretto verso la porta presso la quale server è in ascolto: possiamo quindi verificarne l'efficacia, invocando nuovamente client, ed osservando il nuovo tipo di risposta.


Riferimenti

x
Logo

Lo Strato Applicativo
di Internet

Dal TCP al VoIP, dal DNS all'Email alla crittografia, tutto ciò che accade dietro le quinte di Internet, completo di cattura del traffico.
Scopri come effettuare il download, ricevere gli aggiornamenti, e contribuire!


Realizzato con Document made with Kompozer da Alessandro Falaschi -
Capitolo: Netkit