Mihai Budiu -- mihaib+@cs.cmu.edu
http://www.cs.cmu.edu/~mihaib/
aprilie 2000
Poate cel mai mare succes al ingineriei contemporane este Internetul. Acesta este cu siguranță cel mai mare sistem creat vreodată de umanitate, care leagă sute de milioane de utilizatori de pe toate continentele. Admirabilă este proiectarea sa, care a permis sistemului să crească și să funcționeze excelent la o scară nemaiîntîlnită.
Unele dintre calitățile sale derivă cu certitudine din faptul că a fost proiectat inițial ca o rețea militară, care este capabilă să supraviețuiască chiar unui atac nuclear, continuînd să funcționeze chiar în prezența distrugerii unei porțiuni substanțiale din infrastructură. Dar deși Internetul a fost proiectat pentru a rezista unor atacuri dinafara, nimeni nu a anticipat in design-ul original că multe probleme vor apărea dinauntru.
În acest articol voi prezenta trei astfel de slăbiciuni de concepție, care permit unor calculatoare lipsite de scrupule să interfere în mod dramatic cu comportarea rețelei. Vom vedea apoi și unele soluții propuse pentru a contra-ataca astfel de acțiuni.
Acest text este bazat pe un articol al lui Stephan Savage, un excelent cercetător în domeniul rețelelor de calculatoare, care în curînd își va obține doctoratul de la prestigioasa universitate Washington din Seattle.
În PC Report au apărut o mulțime de articole despre arhitectura Internetului. Aceasta este conceptual simplă, dar destul de complicată pentru a umple o carte întreagă. De aceea, în această secțiune vom revizui doar unele dintre trăsăturile arhitecturale esențiale care ne vor permite să înțelegem de ce apar unele probleme.
Pietrele de temelie ale Internetului sunt două protocoale1 numite IP (Internet Protocol) și TCP (Transport Control Protocol). IP este un protocol foarte simplu, care rulează pe toate calculatoarele care compun Internetul; IP primește pachete de date de la un calculator cu rugămintea de a fi livrate la o anumită destinație, și încearcă să trimită pachetul acolo. Am vorbit despre IP cu mai multe ocazii (de pildă unul în PC Report din mai 1998, 'si unul in aprilie și mai 1999; puteți citi aceste articole din pagina mea de web).
În textul de față vom vorbi însă despre TCP. IP nu oferă nici un fel de garanții asupra datelor trimise; TCP se folosește de IP pentru a trimite date în mod ne-fiabil, dar repară lipsurile lui IP: TCP oferă o transmisiune complet fiabilă a tuturor datelor transmise de la un capăt la altul.
De fapt protocolul TCP rezolvă nu mai puțin de trei probleme diferite simultan:
În lipsa acestei funcțiuni a lui TCP, rețeaua ar intra rapid în ceea ce se numește colaps de congestie: calculatoarele ar trimite pachete mai repede decît rețeaua ar putea transporta, aceste pachete ar trebui să se piardă, ceea ce ar cauza re-transmiterea pachetelor pierdute, etc. Rezultatul ar fi întreruperea aproape completă a oricărei comunicații utile.
Protocoalele de funcționare a Internetului sunt publicate într-o formă extrem de bizară: sub forma unor documente numite ``Request For Comments'', sau RFC. Exista mai mult de 2000 de astfel de documente, dar nu toate sunt la fel de importante. Multe astfel de documente sunt de fapt vide (nu au fost niciodată publicate), unele sunt niște glume sau poezioare, iar unele sunt documente foarte serioase. Toate sunt disponibile în formă electronică într-o mulțime de locuri de pe Internet (normal, nu?).
Aceasta metodă de formalizare a arhitecturii stă într-un contrast dramatic față de metoda folosită de comitetele de standardizare internaționale, care publică documente extrem de groase, revizuite pentru multă vreme, si extrem de ``serioase''.
RFC-urile ilustrează evoluția extrem de rapidă și ``impulsivă'' (prin comparație) a Internetului; multe RFC-uri sunt revizuite în mod repetat.
Specificația TCP/IP se întinde pe mai multe RFC-uri. Definiția oficială a fost dată în 1981 în RFC 793. Corecții și lămuriri apar în RFC 1122. Din păcate alte detalii ale funcționării sale se întind efectiv pe zeci de RFC-uri.
TCP este un protocol simetric (full-duplex), în care ambele capete pot transmite date. E mai simplu însă să studiem problema ca și cum unul dintre participanți doar trimite date, iar celălalt doar le receptează.
Pentru a descrie comunicația vom folosi desene ca în figura 1.
TCP funcționează cam ca scrisorile cu confirmare de primire de la poșta: poșta este un mecanism ne-fiabil de transmisiune, care pierde scrisori. De aceea trimițătorul poate să ceară în mod explicit ca cealaltă parte să confirme primirea plicului. Cînd poștașul livrează un plic, receptorul trebuie să semneze de primire. Apoi confirmarea de primire este trimisă înapoi tot prin poștă.
Pentru a obține fiabilitate, emițătorul așteaptă ce așteaptă, iar dacă nu primește nici o confirmare, mai trimite încă o dată scrisoarea. În clipa cînd a primit o confirmare, știe că scrisoarea a ajuns la celălalt capăt.
Observați că lipsa recepției unei confirmări poate indica două lucruri:
Schema cu confirmare este foarte eficace pentru a combate pierderea de pachete, dar este ineficace dacă pachetele călătoresc mult timp pînă la destinație2. Pentru că pînă la întoarcerea primei confirmări emițățorul stă degeaba, o grămadă de resurse sunt irosite.
Din cauza asta, TCP încearcă sa trimită mai multe pachete la rînd, înainte de a primi confirmarea pentru fiecare din ele. Astfel, emițătorul menține o fereastră, care este lista pachetelor trimise dar încă ne-confirmate. De îndată ce primește confirmările, TCP avansează fereastra și trimite pachete noi.
Modul în care se încrucișează pachetele de date și confirmările este ilustrat de figura 4.
În fine, discutăm aici pe scurt despre modul în care TCP face față congestiei din rețea. Această secțiune este foarte importantă, pentru că toate cele trei atacuri pe care le discutăm în acest text manipulează slăbiciuni în specificarea și implementarea acestei scheme.
Asumpția de bază a lui TCP este că rețeaua de transmisiune este inerent fiabilă. TCP presupune că singura cauză de pierderi în rețea este congestia3.
Ce este congestia? Să ne imaginăm un ruter4 în rețea, care are trei interfețe de 1 megabit/secundă. Să ne imaginăm că pe două dintre interfețe se află niște trimițători care emit cu 800 kbps, iar pe a treia se află receptorul datelor. Ei bine, suma celor două fluxuri de 800 kbps depășește capacitatea legăturii spre receptor. Ca atare, pachetele care intră în ruter nu pot ieși toate în timp util. Ruterul va trebui să stocheze pachetele în exces în memoria internă. Dar dacă această situație durează destul de mult, ruterul trebuie să consume cîte 600 kbps de memorie pentru a stoca aceste pachete în exces. După 30 de secunde de trafic neîntrerupt, e nevoie de peste 2M de RAM. Atunci cînd memoria ruterului este complet utilizată, ruterul nu mai are nimic de făcut decît să renunțe la a mai trimite pachetele spre ieșire (pur și simplu sunt șterse din memorie). Aceasta este congestia, și rezultatul ei, pierderea de pachete.
Deci TCP își imaginează că atunci cînd un pachet nu este confirmat în timp util, s-a pierdut fie pachetul fie confirmarea, deci rețeaua este congestionată. Ca atare, trimițătorul imediat reduce rata de transmisiune, pentru a reduce numărul de pachete din rețea.
Dacă toți transmițătorii care detectează congestie reduc rata simultan, efectul este că ruterele din rețea primesc mai puține date la intrare, și au timp să golească pachetele stocate în memorie trimițindu-le la destinație. Pentru că marea majoritate a calculatoarelor din rețea folosesc protocolul TCP, acest comportament duce la dispariția congestiei.
Cum reduce emițătorul TCP rata de transmisiune? Într-un mod foarte simplu: reduce automat dimensiunea ferestrei, și mărește dimensiunea duratei de timeout. În felul acesta va face ca mai puține pachete să se afle simultan în rețea.
Am văzut pînă acum care sunt mecanismele prin care TCP își îndeplinește misiunile (unele dintre ele). Pentru a înțelege de ce aceste mecanisme nu sunt suficiente pentru orice scenariu, trebuie să aruncăm o privire asupra participanților la trafic din Internet și asupra intereselor lor contradictorii.
Într-o excelentă expunere despre arhitectura Internetului, Stefan Savage a prezentat gradația din figura 5.
Între cooperare și indiferență este o distincție: de exemplu eu aș putea să doresc să măsor niște parametri ai rețelei între mine și calculatorul de la Agora, ca să știu de pildă unde se pierd pachete. Serverul de web www.agora.ro nu îmi va oferi însă nici un suport; nu are nici un avantaj din faptul că eu pot să măsor parametrii rețelei.
În acest articol ne vom focaliza doar asupra uneia dintre aceste relații competitive: relația de concurență. În orice context în care mai multe entități concurează asupra unui număr redus de resurse, vom avea de a face cu competiție. Pentru că în Internet avem de a face cu un număr uriaș de clienți potențiali, este virtual imposibil să avem resurse suficiente la dispoziție pentru a satisface pe deplin pe oricine. Așa că prezența competiției este absolut naturală. Atunci care e problema?
Problema este dacă nu cumva accesul la resurse nu este ``democratic''. În mod ideal fiecare client ar trebui să obțină la fel de multe resurse cît ceilalți clienți5.
Trebuie să fim deci pregătiți pentru competiție între feluriții participanți la trafic. Întrebarea este: asigură protocoalele din rețea o competiție ``onestă'' între participanți? Altfel spus, impun aceste protocoale un comportament echitabil? În mod normal ar trebui să fie imposibil să ``păcălim'' protocoalele în vreun fel. Un nod de comunicație în Internet nu are voie să aibă încredere în nimeni, nici măcar în cel cu care comunică în mod direct; de ce ar avea un site de web încredere în clienții care îi cer date? (Serviciul preponderent ca importanță -- în termeni de trafic sau importanță comercială -- în Internet este serviciul de web, așa că asupra lui ne vom concentra.)
Într-un articol scris de Ion Stoica în colaborare cu autorul celui de față, din PC Report din mai 1999, am arătat cum un transmițător care nu respectă semnalele de congestie (în acel caz un transmițător care folosea protocolul UDP, despre care nu am vorbit în acest text), poate căpăta avantaje în detrimentul tuturor celorlalți. Să zicem că un server refuză să asculte semnalele de congestie, și transmite tot timpul la maxim. Celelalte servere, cinstite, vor reduce traficul, și toată capacitatea rețelei va fi disponibilă pentru ticălos.
Atîta vreme cît traficul ``ticăloșilor'' nu cauzează congestie (adică capacitatea rețelei este suficientă pentru a transporta acest trafic), ticăloșii vor putea transmite la capacitate maximă, iar participanții onești nu vor transmite decît pe capacitatea rămasă. Dacă însă nu e sugicientă capacitate pentru cei incorecți, rețeaua ajunge imediat în colaps de congestie, și nimeni nu mai poate transmite nimic.
Și totuși rețeaua funcționează rezonabil. De ce oare? Ce împiedică comportamente anti-sociale pe scară largă?
Aparent am putea fi tentați să spunem: deși protocoalele nu pot împiedica comportamente necinstite, totuși în practică nu le putem folosi în mod ilegal, din cauza separației între părți. Iată argumentul:
Pentru a putea comite o faptă anti-socială trebuie să avem două elemente deodată: motivul și ocazia. Să le analizăm pe fiecare în parte.
În realitate lucrurile nu stau așa: studiile arată că în cazul serverelor mari de web ``gîtuitura'' în Internet este de fapt foarte aproape de server. Resursa critică deci nu e partea din rețea comună cu alte servere, ci chiar ``ieșirea'' serverului la Internet. Mărind congestia deci serverul își dă singur cu ciocanul peste degete.
|
Iată deci că aparent cei care ar putea face rău (cei care trimit multe date, serverele) nu pot fi necinstiți, pentru că-și fac rău lor înșile, iar clienții nu pot face rău, pentru că ei nu transmit multe date, ci doar recepționează.
În realitate clienții au la dispoziție o unealtă foarte puternică cu care pot controla comportarea serverului, fără voia acestuia: confirmările.
Stefan Savage împreună cu colegii din grupul lui de cercetare au demonstrat acest lucru ``pe viu'': au luat un client linux și au făcut modificări minore în implementarea protocolului TCP (sub 100 de linii pentru toate atacurile prezentate la un loc). Apoi au demonstrat cum acest client, folosit pentru a extrage date de la mai multe site-uri de web foarte importante (deci care nu pot fi suspectate de colaborare), poate monopoliza rețeaua în detrimentul celorlalți clienți.
În cele ce urmează voi ilustra pe scurt trei modificări diferite și impactul fiecăreia dintre ele. Datele experimentale sunt din articolul citat.
În TCP datele transmise formează un flux (stream). Fiecare pachet trimite o parte din date, și indică poziționarea lor în flux. De pildă, primul pachet ar putea trimite datele de la 0 la 1000, al doilea de la 1001 la 2000, etc. Confirmările pe de altă parte sunt cumulative: o confirmare care spune ``3001'' înseamnă: ``am primit toate datele între 0 și 3000; aștept date începînd de la 3001 în continuare''. În felul acesta, dacă o confirmare pentru un pachet se pierde (de exemplu cea pentru pachetul 1001-2000), confirmările ulterioare o pot subsuma, reducînd traficul necesar.
Primul atac e surprinzător de simplu: cînd receptorul primește un pachet, să zicem cu datele 0-1000, el va trimite în loc de o confirmare, trei: una pentru 333, una pentru 667, una pentru 1000. Atacul este ilustrat de figura 6.
E clar că în felul acesta protocolul rămîne corect. Care e problema?
Problema este în modul în care emițătorul trebuie să reacționeze la astfel de mesaje: dacă citim în RFC 2581 vom vedea un paragraf care spune următoarele:
...cînd un pachet de confirmare este primit, trimițătorul mărește fereastra cu SMSS (Sender Maximum Segment Size)6.
Nu ne interesează prea tare cît e valoarea lui SMSS (această valoare este stabilită între cele două părți cînd se stabilește conexiunea); important e că, trimițînd mai multe confirmări, receptorul poate mări fereastra emițătorului aproape în mod arbitrar! Dacă alege să confirme fiecare octet, poate mări fereastra de mii de ori după un singur pachet primit! Figura 7 arată ce se întîmplă după aceea.
Asta înseamnă că emițătorul (serverul) va trimite apoi o rafală de pachete de date. Observați că pachetele sunt trimise unul după altul, înainte de a da o șansă rețelei să trimită semnale de congestie. În mod normal fereastra emițătorului crește treptat pînă atinge o valoare de echilibru, care nu produce congestie (această creștere se numește ``slow start''). Graficul din figura 8 arată că receptorul poate ``suge'' de la sursă documente uriașe aproape instantaneu.
A doua schemă este și mai simplă și se bazează pe aceeași slăbiciune din specificația TCP. În loc să trimită mai multe pachete de confirmare diferite, receptorul trimite în mod repetat un singur pachet de confirmare. Receptorul poate folosi chiar pachetul de confirmare pentru octetul 1 (ca și cum n-ar fi primit încă nimic).
Figurila 9 arată cum se comportă emițătorul. Performanța măsurată în Internet (și nu într-o rețea de laborator!) este aceeași ca pentru schema cu diviziune.
Această schemă este și mai perfidă decît cea precedentă; dacă pentru cea precedentă serverul ar putea să devină suspicios, pentru că sunt confirmate doar fragmente de pachet7. Din punct de vedere al serverului un scenariu în care vin mai multe confirmări identice este perfect plauzibil (protocolul IP nu promite că nu duplică pachete).
În fine, ultimul atac este mai riscant, pentru că confirmă date care încă nu au fost primite. Ideea este că confirmările și datele se încrucișează pe parcurs, și serverul are impresia că datele au ajuns mult mai repede. Figura 10 arată cum funcționează acest atac.
Desigur, în cazul unei pierderi reale de date, receptorul are probleme, pentru că emițătorul nu va re-trimite datele deja confirmate. Dar, așa cum arată cercetătorii de la U Washington, protocolul HTTP, prin care clientul comunică cu serverul de web8 permite clientului să re-ceară date de la server în mod selectiv. Deci pierderile la nivel TCP pot fi compensate de un client modificat prin nivele superioare de corecție a erorilor.
Performanțele schemei cu anticipare de confirmări sunt mai puțin spectaculoase, dar oricum, mult superioare celei a unui client normal (figura 11).
Partea cea mai neplăcută din toată povestea asta este că la ora actuală nu există nici un fel de soluții practice pentru a împiedica astfel de comportamente. Lucrările lui Savage și a echipei sale propun mai multe soluții, dar toate necesită schimbări în infrastructura Internetului, care schimbări sunt foarte greu de făcut și mai ales implementat pe scară largă.
Voi menționa doar pe scurt natura propusă a soluției; pentru detalii cei interesați pot vedea articolul original. Ideea ar fi ca emițătorul să poată ``verifica'' fiecare confirmare venită de la receptor, garantînd că acesta nu încalcă regulile. Pentru aceasta, emițătorul va pune în fiecare pachet un număr aleator diferit. Receptorul va trebui să includă în confirmare numărul cu pricina. Aceasta va preveni atacuri cu confirmări anticipate.
De asemenea, emițătorul va ține minte confirmările primite, și nu va crește fereastra pentru confirmări duplicate sau divizate.
O listă a tuturor RFC-urilor cu opțiuni de căutare este prezentă
la
http://www.cis.ohio-state.edu/hypertext/information/rfc.html.
RFC-ul de bază care descrie funcționarea TCP pentru prevenirea congestiei este RFC 2581 http://www.cis.ohio-state.edu/htbin/rfc/rfc2581.html.
Pagina de web a lui Stefan Savage este la http://www.cs.washington.edu/homes/savage.
Articolul lui Savage care a inspirat acest text este ``TCP Congestion
Control with a Misbehaving Receiver'', Stefan Savage, Neal
Cardwell, David Wetherall and Tom Anderson, ACM Computer
Communications Review, pp. 71-78, v 29, no 5, October, 1999,
http://www.cs.washington.edu/homes/savage/papers/CCR99.ps.
Despre comportarea necinstită în Internet, efectele ei, și soluții, vedeți articolul ``Scalabilitatea în rețele de comunicații de date'', Ion Stoica și Mihai Budiu, din PC Report mai 1999, de asemenea la http://www.cs.cmu.edu/~mihaib/articles/csfq.ps.gz.
Pentru o descriere amplă și plăcută a funcționării Internetului vedeți de pildă cartea ``Internetworking with TCP/IP, volume I, Principles, Protocols and Architecture'', Douglas Comer, Prentice Hall 1995.
Un ghid interesant despre RFC-uri este la
http://www.netbook.cs.purdue.edu/othrpags/page24.htm. Acesta
este de fapt o anexă a cărții mai sus-citate, care însă din
păcate este ușor învechită (de exemplu RFC 2581 despre congestie
nu este menționat de loc).