This is an old revision of the document!


Laborator 10: Exerciții

Pentru desfășurarea laboratorului pornim de la arhiva de sarcini a laboratorului. Descărcăm și decomprimăm arhiva în directorul so2/ din directorul home al utilizatorului student de pe sistemul de bază (stația mjolnir):

student@mjolnir:~$ cd so2/
student@mjolnir:~/so2$ wget http://elf.cs.pub.ro/so2/res/laboratoare/lab10-tasks.zip
student@mjolnir:~/so2$ unzip lab10-tasks.zip
student@mjolnir:~/so2$ tree lab10-tasks

În cadrul directorului lab10-tasks/ se găsesc resursele necesare pentru dezvoltarea exercițiilor de mai jos: fișiere schelet de cod sursă, fișiere Makefile și Kbuild, scripturi și programe de test.

Vom dezvolta exercițiile pe sistemul de bază (stația mjolnir) și apoi le vom testa pe mașina virtuală QEMU. După editarea și compilarea unui modul de kernel îl vom copia în directorul dedicat pentru mașina virtuală QEMU folosind o comandă de forma

student@mjolnir:~/so2$ cp /path/to/module.ko ~/so2/qemu-vm/fsimg/root/modules/

unde /path/to/module.ko este calea către fișierul obiect aferent modulului de kernel. Apoi vom porni, din directorul ~/so2/qemu-vm/ mașina virtuală QEMU folosind comanda

student@mjolnir:~/so2/qemu-vm$ make

După pornirea mașinii virtuale QEMU vom putea folosi comenzi în fereastra QEMU pentru a încărca și descărca modulul de kernel:

# insmod modules/module-name.ko
# rmmod module/module-name

unde module-name este numele modulului de kernel.

Pentru dezvoltarea laboratorului, este recomandat să folosim trei terminale sau, mai bine, trei tab-uri de terminal. Pentru a deschide un nou tab de terminal folosim combinația de taste Ctrl+Alt+t. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:

  1. În primul tab de terminal dezvoltăm modulul de kernel: editare, compilare, copiere în directorul dedicat pentru mașina virtaulă QEMU. Lucrăm în directorul aferent rezultat în urma decomprimării arhivei de sarcini a laboratorului.
  2. În al doilea tab de terminal pornim mașina virtuală QEMU și apoi testăm modulul de kernel: încărcare/descărcare modul, rulare teste. Lucrăm în directorul aferent mașinii virtuale: ~/so2/qemu-vm/.
  3. În al treilea tab de terminal pornim minicom sau un server UDP care să primească mesajele de netconsole. Nu contează în ce director ne aflăm. Folosim comanda
    student@mjolnir:~$ netcat -lup 6666

1. [2p] Afișare pachete în kernel space

Creați un modul de kernel care afișează adresa și portul sursă pentru pachetele TCP care inițiază o conexiune spre exterior. Porniți de la scheletul din 1-2-netfilter/ și completați zonele marcate cu TODO 1, având în vedere observațiile de mai jos.

Va trebui să înregistrați un hook netfilter de tipul NF_INET_LOCAL_OUT așa cum este explicat în secțiunea netfilter.

Structura sk_buff dă acces la antetele unui pachet. Antetul IP se obține într-o structură iphdr folosind funcția ip_hdr, iar antetul TCP se obține într-o structură tcphdr folosind funcția tcp_hdr.

Diagrama explică cum se realizează o conexiune TCP. Pachetul de inițiere a unei conexiuni are flagul SYN din atentul TCP activ, iar flagul ACK inactiv.

Pentru afișarea adresei IP sursă folosiți formatul %pI4 al funcției printk. Detalii găsiți în documentația nucleului (secțiunea IPv4 addresses). Un exemplu de snippet de cod care să folosească %pI4 este:

printk("IP address is %pI4\n", &iph->saddr);

Atunci când folosim formatul %pI4 argumentul către printk este un pointer. De unde și construcția &iph->saddr (cu operatorul & - ampersand) în loc de iph->saddr.

Portul TCP sursă este, în antentul TCP, în formatul network byte-order. Parcurgeți secțiunea Conversii. Folosiți ntohs pentru conversie.

Pentru testare folosiți fișierul 1-2-netfilter/test-1.sh. Testul generează o conexiune către stația locală (pe localhost), conexiune ce va fi interceptată și afișată de modulul de kernel. Îl copiați în cadrul mașinii virtuale QEMU și apoi îl rulați obținând un output similar celui de mai jos:

# ./test-1.sh
[  229.783512] TCP connection initiated from 127.0.0.1:44716
Should show up in filter.
Check dmesg output.

2. [2p] Precizare adresă destinație

Extindeți modulul de la exercițiul 1 astfel încât să permită precizarea unei adrese destinație dorite prin intermediul unei rutine ioctl MY_IOCTL_FILTER_ADDRESS. O să afișați doar pachetele care conțin adresa destinație precizată. Pentru rezolvarea acestui task completați zonele marcate cu TODO 2 și urmăriți precizările de mai jos.

Pentru implementarea rutinei ioctl trebuie să completați funcția my_ioctl. Revedeți secțiunea din laboratorul 4. Adresa transmisă din user space este în network byte-order, deci nu va fi nevoie de conversie.

Adresa IP trimisă prin ioctl este trimisă prin adresă, nu prin valoare. Adresa trebuie stocată în variabila ioctl_set_addr. Pentru stocare folosiți copy_from_user.

Pentru compararea adreselor, completați funcția test_daddr. Se vor folosi adresele în network byte-order; fără a fi nevoie conversia adreselor (dacă sunt egale de la stânga la dreapta vor fi egale și invers).

Funcția test_daddr trebuie apelată în hook-ul netfilter pentru a afișa pachetele de inițiere de conexiune pentru care adresa destinație este cea transmisă prin rutina de tip ioctl. Pachetele de inițiere de conexiune au flag-ul syn activat și flag-ul ack dezactivat în antetul TCP. Adică trebuie să verificați două lucruri:

  • flag-urile TCP
  • adresa destinație a pchetului (folosind funcția test_daddr)

Pentru testare folosiți scriptul 1-2-netfilter/test-2.sh. Acest script are nevoie de compilarea fișierului 1-2-netfilter/test.c în executabilul test. Compilarea o faceți pe sistemul fizic folosind comanda

make -f Makefile.test

Copiați modulul de kernel, împreună cu scriptul test-2.sh și executabilul test pe mașina virtuală QEMU pentru testare. În urma rulării testului veți obține un output similar celui de mai jos:

# ./test-2.sh
[  797.673535] TCP connection initiated from 127.0.0.1:44721
Should show up in filter.
Should NOT show up in filter.
Check dmesg output.

Testul comandă filtrarea pachetelor întâi către adresa IP 127.0.0.1 și apoi către adresa IP 127.0.0.2. Primul pachet de inițiere de conexiune (către 127.0.0.1) este interceptat și afișat de filtru, în vreme ce al doilea (către 127.0.0.2) nu este interceptat.

3. [2p] Socket TCP în starea listening

Creați un modul de kernel care creează un socket TCP ce ascultă conexiuni pe portul 60000 pe interfața de loopback (în init_module). Folosiți scheletul din 3-4-tcp-sock/ din arhiva de sarcini a laboratorului și completați zonele marcate cu TODO 1 ținând cont de observațiile de mai jos.

Parcurgeți secțiunile Operații asupra structurii socket și Structura proto_ops din laborator.

Pentru a crea un socket folosiți funcția sock_create_kern.

Socketul sock este un server socket și trebuie pus în starea listening. Adică pe socket trebuie aplicate operațiile de tip bind și listen. Pentru echivalentul bind, listen în kernel space va trebui să apelați funcții de tipul sock->ops->...; exemple de astfel de funcții pe care să le apelați sunt sock->ops->bind, sock->ops-listen etc.

Pentru exemplu de apel a funcțiilor sock->ops->bind, respectiv sock->ops->listen, urmăriți cum sunt acestea apelate în handler-ele de apel de sistem sys_bind și sys_listen.

Pentru al doilea argument al apelului de tip listen (backlog) folosiți macro-ul LISTEN_BACKLOG.

Să aveți în vedere eliberarea socket-ul în funcția de tip exit a modulului și în zona cu label-uri de eroare.

Pentru testare, rulați scriptul 3-4-tcp_sock/test-3.sh. În urma rulării testului, se va afișa un socket TCP ascultând conexiuni pe portul 60000.

4. [2p] Acceptarea unei conexiuni în kernel space

Extindeți modulul de la exercițiul anterior pentru a permite acceptarea unei conexiuni din exterior (nu trebuie transmis mesaj, doar acceptată o nouă conexiune). Trebuie completate zonele marcate cu TODO 2.

Parcurgeți secțiunile Operații asupra structurii socket și Structura proto_ops din laborator. Pentru echivalentul accept în kernel-space, consultați handler-ul de apel de sistem sys_accept4. Urmăriți în implementarea sys_accept4 apelul sock->ops->accept. Folosiți 0 ca valoarea pentru ultimul argument (flags).

Afișați adresa și portul socket-ului destinație. Pentru a afla numele peer-ului unui socket (adresa sa), consultați handler-ul de apel de sistem sys_getpeername.

Primul argument al funcției de tip getname va fi socket-ul de tip conexiune, adică new_sock, cel inițializat cu ajutorul apelului accept.

Ultimul argument al funcției de tip getname va fi 1.

Afișați adresa peer-ului (indicată de variabila raddr) folosind macro-ul print_sock_address.

Eliberați socket-ul nou creat (în urma acceptării conexiunii) în funcția de exit a modulului și în zona cu label-uri de eroare. După adăugarea codului care face accept în funcția de inițializare a modulului, operația de insmod se va bloca până se va realiza o conexiune. Puteți debloca folosind netcat pe portul respectiv. În consecință, scriptul de testare de la exercițiul precedent nu va ma funcționa.

Pentru testarea, adică inițierea unei conexiuni din exterior, folosiți scriptul 3-4-tcp_sock/test-4.sh. Nu se va afișa ceva special (în buffer-ul kernel-ului). Reușita testului va fi dată de realizarea conexiunii. Apoi folosiți Ctrl+c pentru a încheia rularea scriptului de testare și apoi puteți elimina modulul din kernel.

5. [3p] Socket UDP sender

Creați un modul de kernel care creează un socket UDP și transmite mesajul din macro-ul MY_TEST_MESSAGE pe socket către adresa loopback pe portul 60001.

Porniți de la scheletul 5-udp-sock/ din arhiva de sarcini a laboratorului.

Citiți secțiunile Operații asupra structurii socket și Structura proto_ops din laborator.

Pentru a vedea cum se face transmiterea de mesaje în kernel-space, consultați handler-ul de apel de sistem sys_send sau secțiunea Transmiterea/recepția unui mesaj.

Câmpul msg_name al structurii struct msghdr trebuie inițializat la adresa destinație (de tipul struct sockaddr *), iar câmpul msg_namelen la dimensiunea adresei.

Inițializați câmpului msg_flags a structurii struct msghdr la valoarea 0.

Inițializați respectiv câmpurile control și controllen ale structurii struct msghdr la NULL și 0.

Pentru transmiterea efectiva folosiți kernel_sendmsg.

Parametrii de transmitere a mesajului sunt preluați din kernel-space. Faceți, pentru structura de tip struct iovec cast la struct kvec * în apelul kernel_sendmsg.

Ultimii doi parametri ai funcției kernel_sendmsg sunt 1 (numărul de vectori I/O) și len (dimensiunea mesajului).

Pentru testare folosiți scriptul test-5.sh și integrați executabilul static nc-static așa cum este descris în continuare.

Utilitarul nc din busybox din mașina virtuală QEMU nu are suport pentru UDP. De aceea vom folosi un alt executabil.

Pentru deschiderea unui socket UDP pe portul 60001 folosiți utilitarul netcat compilat static de la adresa http://www.stearns.org/nc/nc-static. Descărcați binarul pe sistemul fizic în directorul fsimg/root/ al mașini virtuale QEMU. Binarul poate fi descărcat cu ajutorul comenzii

wget http://www.stearns.org/nc/nc-static

Verificați că fișierul a fost descărcat corect verificând suma de control MD5 cu ajutorul comenzii

$ md5sum nc-static 
ec6aa710d3112808cb31a1d6ded775a1  nc-static

După pornirea mașinii virtuale QEMU, acordați executabilului nc-static permisiuni de execuție folosind

chmod a+x nc-static

și apoi copiați executabilul nc-static în /bin/nc:

cp nc-static /bin/nc

Mesajul Punt! afișat în urma rulării testului test-5.sh este un mesaj afișat de utilitarul netcat în momentul în care procesul este încheiat cu ajutorul comenzii ''kill'.

Soluții

so2/laboratoare/lab10/exercitii.1492937636.txt.gz · Last modified: 2017/04/23 11:53 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0