Pentru rezolvarea laboratorului, vom lucra în același director din care pornim mașina virtuală (~/so2/linux/tools/labs).
Pașii de rezolvare sunt următorii:
Pentru rezolvarea laboratorului trebuie sa activam suportul de netfilter din kernel. In meniul deschis cu make menuconfig
activati optiunea Networking support/Networking options/Network packet filtering framework (Netfilter).
Scheletul de laborator este generat din sursele din directorul tools/labs/templates. Putem genera scheletele pentru toate laboratoarele folosind următoarea comanda:
tools/labs $ make skels
Pentru a genera scheletul pentru un singur laborator, vom folosi variabila de mediu LABS:
tools/labs $ make clean tools/labs $ LABS=<lab name> make skels
networking.
Similar, putem genera și scheletul pentru un singur exercițiu, atribuind valoarea <lab_name>/<task_name> variabilei LABS.
tools/labs/skels.
Comanda make build compilează toate modulele din directorul skels.
student@eg106:~/so2/linux/tools/labs$ make build echo "# autogenerated, do not edit " > skels/Kbuild echo "ccflags-y += -Wno-unused-function -Wno-unused-label -Wno-unused-variable " >> skels/Kbuild for i in ./networking/1-2-netfilter/kernel ./networking/3-4-tcp-sock ./networking/5-udp-sock; do echo "obj-m += $i/" >> skels/Kbuild; done ...
Putem copia modulele generate pe mașina virtuală folosind target-ul copy al comenzii make, atunci când mașina virtuală este oprită.
student@eg106:~/so2/linux/tools/labs$ make copy student@eg106:~/so2/linux/tools/labs$ make boot
Alternativ, putem copia fișierele prin scp, pentru e evita repornirea mașinii virtuale. Pentru detalii despre folosirea interacțiunea prin rețea cu mașina virtuală citiți Interacțiunea cu mașina virtuală.
Modulele generate sunt copiate pe mașina virtuală în directorul /home/root/skels/<lab_name>.
root@qemux86:~/skels/networking# ls 1-2-netfilter 3-4-tcp-sock 5-udp-sock root@qemux86:~/skels/networking# ls 1-2-netfilter/kernel/ filter.ko
După pornirea mașinii virtuale QEMU vom putea folosi comenzi în fereastra QEMU (sau în minicom) pentru a încărca și descărca modulul de kernel:
root@qemux86:~# insmod skels/<lab_name>/<task_name>/<module_name>.ko root@qemux86:~# rmmod skels/<lab_name>/<task_name>/<module_name>.ko
Ctrl+Shift+t. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:
~/so2/linux/tools/labs.~/so2/linux/ cu sursele nucleului unde putem folosi Vim și cscope pentru parcurgerea codului sursă.student@eg106-pc:~$ netcat -lup 6666
git pull --rebase in directorul ~/so2/linux, pentru a obține ultima versiune a scheletului de laborator.
De asemenea, trebuie să vă asigurați că este activat suportul pentru netfilter în kernel, prin opțiunea CONFIG_NETFILTER. Pentru aceasta, rulați make menuconfig în directorul linux și verificați opțiunea Network packet filtering framework (Netfilter) din Networking support → Networking options.
În cazul în care nu era activată, activați-o (ca builtin, nu modul extern – trebuie să fie marcată cu *).
În cazul în care ați activat-o, trebuie să forțati recompilarea kernel-ului înainte de pornirea mașinii virtuale. Pentru aceasta, ștergeți fisierul linux/arch/x86/boot/bzImage. După acest pas, la pornirea mașinii virtuale se va declanșa automat recompilarea.
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ă struct iphdr folosind funcția ip_hdr, iar antetul TCP se obține într-o structură struct 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.
%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/user/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. Scriptul este copiat pe mașina virtuală la make copy doar dacă este marcat ca executabil. Scriptul folosește utilitarul compilat static netcat din skels/networking/netcat; acest executabil trebuie să aibă permisiuni de execuție.
După rulare ar trebui să obținem 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.
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.
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:
test_daddr)
Pentru testare folosiți scriptul 1-2-netfilter/user/test-2.sh.
Acest script are nevoie de compilarea fișierului 1-2-netfilter/user/test.c în executabilul test. Compilarea se face automat pe sistemul fizic la rularea comenzii make build. Scriptul de testare este copiat pe mașina virtuală doar dacă este marcat ca executabil. Scriptul folosește utilitarul compilat static netcat din skels/networking/netcat; acest executabil trebuie să aibă permisiuni de execuție.
Î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.
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 scheletul 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.
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.
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; folosiți funcția sock_release.
Pentru testare, rulați scriptul 3-4-tcp_sock/test-3.sh. Scriptul este copiat pe mașina virtuală la make copy doar dacă este marcat ca executabil.
În urma rulării testului, se va afișa un socket TCP ascultând conexiuni pe portul 60000.
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 lnet_sock_accept cum este folosit apelul sock->ops->accept. Folosiți 0 ca valoarea pentru penultimul argument (flags) și false pentru ultimul argument (kern).
new_sock) trebuie creat cu apelul sock_create_lite și apoi îi trebuie configurate operațiile folosind
newsock->ops = sock->ops;
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.
sock->ops->getname va fi socket-ul de tip conexiune, adică new_sock, cel inițializat cu ajutorul apelului accept.
Ultimul argument al funcției sock->ops->getname va fi 1, însemnând că dorim aflarea de infomații despre capătul destinație (remote end sau peer).
Afișați adresa peer-ului (indicată de variabila raddr) folosind macro-ul print_sock_address() definit mai sus în fișier.
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. Scriptul este copiat pe mașina virtuală la make copy doar dacă este marcat ca executabil.
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.
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 scheletul 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.
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 msg_control și msg_controllen ale structurii struct msghdr la NULL și 0.
Pentru transmiterea efectivă folosiți kernel_sendmsg.
Parametrii de transmitere a mesajului sunt preluați din kernel-space. Faceți, pentru pointer-ul la structura de tip struct iovec cast la un pointer la structura struct kvec în apelul kernel_sendmsg.
1 (numărul de vectori I/O) și len (dimensiunea mesajului).
Pentru testare folosiți scriptul test-5.sh. Scriptul este copiat pe mașina virtuală la make copy doar dacă este marcat ca executabil. Scriptul folosește utilitarul compilat static netcat din skels/networking/netcat; acest executabil trebuie să aibă permisiuni de execuție.
test-5.sh va conduce la afișarea mesajului kernelsocket, într-un output precum cel de mai jos:
/root # ./test-5.sh + pid=1059 + sleep 1 + nc -l -u -p 60001 + insmod udp_sock.ko kernelsocket + rmmod udp_sock + kill 1059