This is an old revision of the document!
În cele ce urmează vom trece prin elementele principale ce se întâlnesc în networking pe Linux. După acest laborator vom avea o înțelegere a modului în care funcționează networking-ul pe containere și ce folosesc în spate utilitare precum ip.
# ATENȚIE: update_lab nu funcționează de pe root, folosiți student inițial student@host:~# update_lab --force student@host:~# start_lab netns
Verificați ca în home să aveți directorul sock-diag/.
Un bridge Linux se comportă ca un switch. Acesta transmite pachete între interfețele care sunt conectate la el. Este utilizat de obicei pentru transmiterea pachetelor pe routere, pe gateway-uri sau între mașini virtuale și spații de nume de rețea pe un host. De asemenea, suportă STP, filtre VLAN și multicast snooping.
În diagramă putem vedea mai multe tipuri de interfețe: TUN, TAP, VETH. Astăzi vom lucra doar cu interfețe de tip VETH, pe care le vom discuta în cele ce urmează.
TUN (tunnel) operează la nivelul 3, ceea ce înseamnă că pachete pe care le vei primi de la descriptorul de fișier vor fi bazate pe IP. Datele scrise înapoi către dispozitiv trebuie să fie și ele sub formă de pachet IP.
TAP (network tap) operează în mod asemănător cu TUN, însă în loc să poată scrie și primi doar pachete de nivel 3 către/de la descriptorul de fișier, poate utiliza pachete ethernet brute. De obicei, vei vedea dispozitive TAP folosite de virtualizarea KVM/Qemu, unde un dispozitiv TAP este atribuit unei interfețe virtuale guest în timpul creării.
API-ul de utilizare din CLI este prezentat in code snipper-ul de mai jos.
# Creaza un bridge sudo ip link add name br0 type bridge # Porneste bridge-ul sudo ip link set br0 up # Conecteaza o interfata la bridge sudo ip link set some_interface master br0 # Adauga o adresa IP pe bridge # (nu este necesara atunci cand bridge-ul este folosit doar pentru comutare) sudo ip addr add 192.168.1.1/24 dev br0 # Vizualizare bridge ip link show master br0 # Eliminare interfata de la bridge sudo ip link set some_interface nomaster # Stergere bridge sudo ip link delete br0 type bridge
De asemenea, se pot folosi apelurile de sistem din Linux pentru a interacționa cu API-ul de bridge. Sunt două metode: prin IOCTL (varianta veche) și prin socket-uri Netlink. Mai jos este un exemplu cu IOCTL.
O altă componentă importantă este VETH (virtual Ethernet), o interfață virtuală de Ethernet. VETH-urile vin mereu în perechi, cum este prezentat în diagrama de mai jos.
O pereche VETH functioneaza ca un tunel, si este deseori folosit ca un tunel intre doua namespace-uri.
Pachetele transmise pe o interfață din pereche sunt primite imediat pe cealaltă interfață. Când oricare dintre interfețe este inactivă, starea legăturii perechii devine inactivă.
API-ul de CLI este urmatorul:
# Creeaza o pereche de veth ip link add veth0 type veth peer name veth1 # Porneste interfetele ip link set veth0 up ip link set veth1 up # Adauga adrese IP pe interfete ip addr add 10.0.0.1/24 dev veth0 ip addr add 10.0.0.2/24 dev veth1
Ultima componenta pe care o vom studia astazi este network namespace-ul (NS).
Sistemul de operare Linux partajează un singur set de interfețe de rețea și intrări în tabela de rutare. Chiar daca putem intrari in tabela de rutare folosind rutarea bazată pe politici , asta nu schimbă fundamental faptul că setul de interfețe de rețea și tabelele/intrările de rutare sunt partajate pe întregul sistem de operare.
Namespace-urile de rețea (network namespace) schimbă această ipoteză fundamentală. Cu namespace-urile de rețea, putem avea instanțe diferite și separate de interfețe de rețea și tabele de rutare care operează independent una de alta.
Un exemplu de utilizare sunt containerele, care folosesc namespace-uri pentru izolarea interfetelor de networking.
In cele ce urmeaza vom implementa diagrama de mai jos:
Crearea de namespace-uri
Pentru a crea un namespace, vom folosi utilitarul ip.
ip netns add <nume namespace>
Vom crea un nou namespace cu numele ns1. Pentru a afișa lista de namespace-uri, vom rula:
ip netns list
Asignarea interfețelor la un network namespace
Vom folosi interfețe virtuale. Vom începe prin a crea o pereche de interfețe veth0 ↔ veth1:
ip link add veth0 type veth peer name veth1
Interfețele vor fi vizibile ca orice altă interfață pe Linux.
ip link list
Ar trebui sa fie listata o pereche de interfețe veth. Momentan, ambele aparțin de namespace-ul global, împreună cu interfețele fizice.
Următorul pas este de a adăuga veth1 la namespace-ul ns1.
ip link set veth1 netns ns1
Dacă rulam comanda ip link list, vom vedea că interfața veth1 a dispărut din listă. Aceasta se află acum în namespace-ul ns1, așa că o vom putea vedea doar din acel namespace:
ip netns exec ns1 ip link list
Prima parte, ip netns exec, este modul în care executam comanda intr-un namespace de rețea diferit.
Următorul este namespace-ul specific în care comanda ar trebui să fie rulată (în acest caz, namespace-ul ns1).
A treia parte reprezinta comanda propriu-zisă care este executată în namespace. În acest caz, vrem să vedem interfețele din namespace-ul ns1, așa că rulam ip link list.
Configurarea interfețelor in network namespaces
Acum că veth1 a fost mutat în namespace-ul ns1, trebuie să configurăm efectiv acea interfață. Vom folosi comanda ip netns exec, de data aceasta pentru a configura interfața veth1 în namespace-ul ns1:
ip netns exec ns1 ip addr add 10.1.1.1/24 dev veth1 ip netns exec ns1 ip link set dev veth1 up
Configurarea interfețelor în namespace-ul global
Trebuie să configurăm și interfața veth0 care a rămas în namespace-ul global:
ip addr add 10.1.1.2/24 dev veth0 ip link set dev veth0 up
Crearea unui al doilea namespace
Vom crea acum un al doilea namespace cu numele ns2:
ip netns add ns2
Crearea unui bridge pentru interconectarea namespace-urilor
Vom crea un bridge virtual care va permite comunicarea între cele două namespace-uri:
ip link add br0 type bridge ip link set dev br0 up
Crearea interfețelor pentru ns2 și conectarea la bridge
Vom crea o nouă pereche de interfețe virtuale pentru ns2:
ip link add veth2 type veth peer name veth3
Vom conecta veth0 (din namespace-ul global) la bridge:
ip link set veth0 master br0
Vom muta veth3 în namespace-ul ns2:
ip link set veth3 netns ns2
Vom conecta veth2 (care a rămas în namespace-ul global) la bridge:
ip link set veth2 master br0 ip link set dev veth2 up
Configurarea interfețelor în ns2
Vom configura interfața veth3 în namespace-ul ns2:
ip netns exec ns2 ip addr add 10.1.1.3/24 dev veth3 ip netns exec ns2 ip link set dev veth3 up ip netns exec ns2 ip link set dev lo up
De asemenea, activăm interfața loopback în ns1:
ip netns exec ns1 ip link set dev lo up
Testarea conectivității cu ping
Acum putem testa conectivitatea între cele două namespace-uri folosind ping:
ip netns exec ns1 ping -c 3 10.1.1.3
Ar trebui să vedem pachete transmise cu succes între ns1 (10.1.1.1) și ns2 (10.1.1.3).
Testarea comunicării cu netcat (nc)
Pentru a testa comunicarea bidirecțională, vom folosi netcat. În ns2, vom porni un server care ascultă pe portul 5000:
ip netns exec ns2 nc -l 5000
Într-un terminal separat, din ns1, vom conecta la serverul din ns2:
ip netns exec ns1 nc 10.1.1.3 5000
Acum putem scrie mesaje în clientul din ns1 și acestea vor apărea în serverul din ns2, demonstrând comunicarea completă între cele două namespace-uri izolate prin intermediul bridge-ului.]
Netlink este un API din Linux utilizat pentru comunicarea inter-proces (IPC) atât între kernel și procesele userspace, cât și între diferite procese userspace, într-un mod similar socket-urilor de domeniu Unix.
Introdus inițial pentru a rezolva problema ioctl-urilor (API-ul de ioctl necesita modificări în kernel pentru fiecare nou protocol de comunicare), Netlink este mecanism extensibil de comunicare între spațiul utilizator și kernel. Spre deosebire de ioctl, Netlink permite adăugarea de noi funcționalități fără a modifica codul kernel-ului, evitând astfel procesul de aprobare și integrare în kernel. Acesta a fost ulterior adaptat pentru a interacționa cu mai multe subsisteme.
La fel ca socket-urile de domeniu Unix și spre deosebire de socket-urile INET, comunicarea Netlink nu poate traversa granițele gazdei. Netlink oferă o interfață standard bazată pe socket-uri pentru procesele userspace și un API pentru nucleu destinat utilizării interne de către modulele nucleului. Inițial, Netlink a folosit familia de socket-uri AF_NETLINK. Netlink este conceput pentru a fi un succesor mai flexibil al ioctl; RFC 3549 descrie protocolul în detaliu.
Netlink este folosit, printre altele, de ip route sau iptables. Mai jos gasim un exemplu de cod ce face toggle up ↔ down la some_interface.
În configurația cu două namespace-uri descrisă în secțiunea Utilizare, vom captura pachetele trimise de nc cu tcpdump și le vom studia folosind Wireshark.
În primele versiuni ale kernelului Linux (inainte de v2.2) comunicarea între user space și kernel space se realiza predominant prin
file ops (syscall-uri). Acest set de callback-uri este implementat (macar parțial) pentru fiecare fișier accesibil în Linux. Pentru un fișier normal (e.g., din home-ul vostru) callback-urile acestea pointează către funcții din API-ul sistemului de fișiere. Fișierele speciale (i.e., /dev, /proc, /sys) se diferențiază prin faptul că generează informații despre sistem în mod dinamic sau permit ajustarea parametrilor de funcționare. De exemplu, următoarele două comenzi sunt echivalente:
root@host:~# sysctl -w net.ipv4.ip_forward=1 root@host:~# echo 1 > /proc/sys/net/ipv4/ip_forward
Similar, utilitarul netstat pe care îl folosim pentru a investiga conexiunile de rețea active în sistem citește și parsează conținutul fișierelor din /proc/net/:
root@host:~# netstat -4tan Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 332 10.9.1.55:22 141.85.150.247:51302 ESTABLISHED root@host:~# cat /proc/net/tcp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 00000000:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 8358 1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7615 2: 3701090A:0016 F796558D:C866 01 00000000:00000000 02:000A9B46 00000000 0 0 22536
Acest utilitar este compatibil cu versiunile mai vechi de Linux, însă echivalentul “modern” (mai nou de v2.4), anume ss, folosește subsistemul de Socket Diagnostics din protocolul Netlink. Netlink este un datagram protocol creat pentru a permite comunicarea proceselor din userspace cu anumite drivere din kernel space. Introdus inițial pentru ca developerii se săturaseră să tot adauge ioctl-uri noi, acesta a fost ulterior adaptat pentru a interacționa cu mai multe subsisteme. De fapt, atât ip route cât și iptables se bazează pe protocolul Netlink.
În continuare vom lucra cu scheletul de cod din ~/sock-diag/. În forma sa actuală, programul inițiază o conexiune Netlink și cere kernel-ului o listă cu toți sockeții TCP ce reprezintă o conexiune activă.
root@host:~/sock-diag# gcc main.c root@host:~/sock-diag# ./a.out [-] main.c:100 ================================= [-] main.c:101 sport : 22 [-] main.c:102 dport : 51302 [-] main.c:103 src ip : 10.9.1.55 [-] main.c:108 dst ip : 141.85.150.247 [-] main.c:113 inode : 22536
Facem totuși cateva observații:
conn_req.id sunt setate pe zero. Acestea pot fi folosite pentru a filtra rezultatele produse de kernel (e.g., arată-mi toate conexiunile către 141.85.227.65 – ocw.cs.pub.ro).conn_req.idiag_ext cere sistemului de operare sa returneze câte o structură tcp_info pentru fiecare socket identificat. Acestea se numesc atribute opționale. Consultați secțiunea relevantă din man page dacă sunteți curioși ce alte informații ar mai fi disponibile.
Pentru fiecare socket raportat, folosiți aceste macro-uri pentru a parsa atributele opționale (i.e., structurile tcp_info) conținute in următorii rta_len octeți, după informațiile de bază găsite la adresa diag_msg. Inspectați câmpurile structurii tcp_info și afișați statisticile ce vi se par importante (plus unitățile de măsură). De exemplu:
[-] main.c:101 sport : 22 [-] main.c:102 dport : 51302 [-] main.c:103 src ip : 10.9.1.55 [-] main.c:108 dst ip : 141.85.150.247 [-] main.c:113 inode : 22536 [-] main.c:126 [-] main.c:127 sent : 330929 [bytes] [-] main.c:129 ACK-ed : 330893 [bytes] [-] main.c:131 recv : 297542 [bytes] [-] main.c:133 rto : 224000 [microsec]
Porniți o instantă de netcat care să asculte pe un port local. Folosiți flag-ul -k pentru a nu omorî procesul dupa finalizarea conexiunii.
Modificați programul astfel încât să obțină lista sockeților în modul TCP LISTENING, nu ESTABLISHED. Filtrați rezultatele pe baza portului pe care ascultați; ar trebui să obțineți un singur rezultat.
Pornim netcat.
root@host:~# nc -lkp 12345
În mod normal, namespace-urile persistă câtă vreme există macar un proces ce le referențiazâ. Există totuși o excepție ce ne permite să construim namespace-uri persistente. Ce contează de fapt pentru kernel este reference count-ul pentru fisier din procfs care reprezintă namespace-ul respectiv. iproute2 (i.e., comanda ip pe care ați tot utilizat-o) folosește o particularitate a syscall-ului mount()]. Daca analizam cu [[https://man.archlinux.org/man/strace.1|strace apelurile de sistem generate, vom identifica urmatoarea secvență:
root@host:~$ strace ip netns add PERSIST
openat(AT_FDCWD, "/run/netns/TEST", O_RDONLY|O_CREAT|O_EXCL, 000) = 5
mount("/proc/self/ns/net", "/run/netns/TEST", 0x556b0d60700c, MS_BIND, NULL) = 0
setns(5, CLONE_NEWNET)
root@host:~$ mount -t nsfs
nsfs on /run/docker/netns/40545dc4df20 type nsfs (rw)
nsfs on /run/docker/netns/d81045a3216b type nsfs (rw)
nsfs on /run/docker/netns/f12eee1a9e7c type nsfs (rw)
nsfs on /run/netns/PERSIST type nsfs (rw)
Observăm că utilitarul iproute a creat un fișier in directorul /run/netns/ și a făcut un bind mount cu referința sa din procfs. Această operație a incrementat reference counter-ul namespace-ului, urmând ca acesta să se separe într-unul nou prin apelul de setns(). Deși procesul iproute2 a murit, namespace-ul va persista până facem umount() pe toate referințele active. Listând mountpoint-urile curente, vedem ca inclusiv docker (sau in cazul nostru, ContainerNet) folosește această tehnică atunci când spawneaza containere noi (i.e., red, green, blue).
înainte să trecem la următorul task, folosiți ip netns pentru a porni o instanță de netcat in namespace-ul PERSIST creat anterior. Instanța de netcat ar trebui sa fie configurată ca la excercitiul anterior (i.e., TCP, ascultând pe acelasi port).
în absența acestora, ruland un proces într-un network namespace nou este o metodă bună pentru a-l preveni să se conecteze la internet (e.g., la un license server
). Singurele alternative ar fi modulul owner din iptables sau LD_PRELOAD hooking. Prima poate afecta și alte procese decat cel intenționat iar a doua este prea muncitorească și nici nu o sa mearga pe distribuțiile hardened. În trecut se putea filtra traficul si pe bază de PID dar modulul de iptables ajungea sa facă abuz de lock pe lista de task-uri din kernel.
Țineți cont că atunci când ștergeți namespace-ul, toate interfețele pe care le-ați adăugat vor fi moștenite de namespace-ul original. Dacă v-a crăpat vreodată Mininet și v-ați trezit cu câteva zeci de interfețe noi când ați rulat ip addr show, acesta este motivul.
root@host:~# ip netns add PERSIST root@host:~# ip netns exec PERSIST nc -lkp 12345
Dacă rulăm programul nostru de la exercițiul anterior, observăm că sistemul de operare nu gasește niciun rezultat. Aceasta este o limitare din Linux. Subsistemul de Socket Diagnostics poate identifica sockeți doar în network namespace-ul din care face parte socket-ul Netlink. Soluția cea mai scalabila / performanta se bazeaza pe extended Berkley Packet Filter (eBPF). Acesta stă la baza proiectului Cilium, unul dintre primele framework-uri care a adresat (ca. 2014) problema observabilității conexiunilor de rețea in mediile bazate pe microservicii. Deoarece eBPF este un subiect mult prea complex pentru un curs introductiv de rețelistică, nu vom intra in detalii. Putem însă găsi o soluție mult mai directă pentru problema noastră.
Modificați programul astfel încât să execute o tranziție în network namespace-ul unui proces, specificat din linie de comanda (e.g., netcat-ul nostru). Pe urmâ, reîncercați listarea sockeților TCP în modul listening prin protocolul Netlink Socket Diagnostics. Ce observați?
Se rulează dând ca parametru /proc/$(pidof nc)/ns/net daca vreți sa folosiți apelul de sistem open(). Alternativ, puteți folosi pidfd_open.