Síla myšlenky je neviditelná jako semeno, ze kterého vyroste obrovský strom. Je však příčinou viditelných změn v životě člověka. (L.N.Tolstoj)

Testování síťových aplikací

Zveřejněno: 21. 11. 2016
Kategorie: Bezpečnost, ICT technologie

– chování klientů a serverů na síti

Socketové programování a TCP/IP je tu s námi už desítky let. V poslední se často setkávám s chybami v aplikacích přímo na úrovni volání síťových systémových volání a souběžně se věnuju projektu, jehož cílem je podobné chyby aktivně vyhledávat a analyzovat.

Socketové API a síťová komunikace

Komunikace po síti se z pohledu aplikací na unixových systémech řeší pomocí souborových deskriptorů, které slouží jako rozhraní k síťovým socketům. Rozhraní je univerzální, funguje pro klienty, servery i rovnocennou komunikaci. Zvládá IPv4, IPv6, TCP, UDP a další základní protokoly.

Obecné API

Aplikace si nechá vytvořit socket pomocí volání socket() a uloží si jeho souborový deskriptor, na kterém pak probíhají všechny ostatní operace až po close(), kterým aplikace souborový deskriptor uvolní. Takových socketů je často potřeba více. Typickým příkladem je dvojice socketů pro IPv4 a IPv6.

Při vytváření socketu je potřeba určit jeho typ, který se pro účely běžné síťové komunikace skládá z výběru mezi IPv4 a IPv6 a výběru mezi TCP a UDP. Způsob užívání socketu se určuje dalšími systémovými voláními. Pokud je socket připravený ke komunikaci, data se vyměňují především pomocí systémových volání send(), recv(), sendto(), recvfrom(), sendmsg() a recvmsg().

Klientské aplikace

Klient pracující nad protokolem TCP vytvoří socket, pomocí volání connect() ho nechá připojit k serverové službě na dané adrese a portu a může komunikovat pomocí send() a recv(). Klient ovšem od uživatele typicky dostane jméno, které se může přeložit na více než jednu adresu. A klient podporující jak IPv4 tak IPv6 bude typicky pracovat alespoň s jednou adresou od každého protokolu.

Obrovské množství aplikací k připojování na více adres přistupuje sekvenčně. Adresy jsou seřazené podle pravidel zohledňujících požadovanou prioritu. Typicky tedy klient nejdříve zkouší adresu protokolu IPv6 a teprve při neúspěchu se dostane k adrese protokolu IPv4.

Problém je, že se zcela běžně stává, že daná síť tiše zahazuje například všechny pakety protokolu IPv6. V takovém případě může systémové volání connect() selat třeba až po minutě. Sekvenční pokus o připojení tak může trvat minutu nebo v případě několika IPv6 adres i několik minut.

Řešením je paralelní připojování. Slouží k tomu neblokující režim socketu společně se smyčkou událostí například na bázi volání select(), poll() nebo epoll_wait(). Teoreticky je možné připojovat se ke všem adresám v seznamu, v praxi by mělo stačit paralelizovat jen verze protokolu IP. Celá akce tedy probíhá tak, že se vytvoří po jednom neblokujícím socketu pro IPv4 a IPv6, nad oběma se spustí connect() a například pomocí poll() se čeká na příchozí události nad oběma sockety.

Paralelní připojování lze ješťě opatřit krátkým timeoutem (například v řádu stovek milisekund), kdy se čeká na preferovaný protokol, kterým je obvykle IPv6. Tak se zajistí jak zvolená preference protokolů v případě, že vše funguje, jak má, tak velmi rychlý fallback v opačném případě.

Klient pracující nad protokolem UDP může použít kombinaci volání connect() a send() jako TCP klient nebo může každý jednotlivý paket směrovat pomocí sendto(). Kterýkoli z těchto postupů vede ke svázání UDP socketu s náhodným klientským portem a zajištění serverové komunikace s předem známým severovým portem. Mechanismus sekvenčního i paralelního zahájení UDP komunikace vůči serveru je v zásadě stejný jako u TCP spojení až na to, že je potřeba veškerou logiku spojení implementovat nad UDP v userspace.

Serverové aplikace

Server pracující nad protokolem TCP volá nad nově vytvořeným socketem volání bind(), které zajistí svázání socketu s konkrétní existující poslouchací adresou nebo obecnou adresou (0.0.0.0 nebo ::) a volání listen(), které přepne socket do pasivního režimu. Takových socketů může server potřebovat více, například jeden pro IPv4 a jeden pro IPv6 nebo jeden pro každou explicitně nakonfigurovanou poslouchací adresu.

Pasivní socket lze rovněž sledovat pomocí smyčky událostí. Pomocí volání accept() server přijme spojení v podobě nového souborového deskriptoru, nad kterým už lze volat send() a recv() stejně jako na klientském socketu.

Server pracující nad protokolem UDP nepodporuje listen() ani accept() a vyzvedává tak každý paket přímo na původním serverovém soketu pomocí recvfrom() a zpracovává ho s ohledem na zdrojovou adresu.

Testování

Předmětem tohoto článku je především testování a analýza software, kdy se popisy očekávaného chování srovnávají s výsledky běhu klientských a serverových aplikací. Budeme se tedy bavit o nástrojích, které nám toto umožní.

Sledování systémových volání pomocí ptrace()

Systémové volání ptrace() umožňuje ladění a trasování procesů a využívají ho nástroje jako gdb nebo strace. Pomocí strace nebo přímo pomocí kernelového API lze sledovat jednotlivá systémová volání týkající se sítě, která lze následně analyzovat a pozuzovat podle nich vlastnosti a chyby síťových aplikací.

Ke smysluplné analýze je dobré rozdělit záznamy o volání síťového API podle socketů. Pro každý socket tedy bude člověk znát sekvenci volání od socket() až po close(), který je třeba pro účely analýzy považovat za součást síťového API, pokud je volaný nad síťovým socketem.

Osobně jsem začal zkoumáním záznamů příkazu strace. Nakonec jsem ale došel k závěru, že budu potřebovat větší flexibilitu a tak jsem vedle strace začal používat i projekt python-ptrace který rovněž poskytuje rozhraní nad systémovým voláním ptrace(), ale nabízí daleko větší flexibilitu. Díky němu jsem schopný velmi jednoduše v pythoním test driveru na syscally reagovat a řídit podle nich běh testu.

Konfigurace sítě pomocí jmenných prostorů

Pro posouzení chování jednotlivých klientských a serverových aplikací pomocí strace nebo python-ptrace je potřeba tyto spouštět ve více různých síťových konfiguracích, přičemž procesy aplikace nesmějí vidět žádnou jinou konfiguraci než která je pro ně určena.

Vedle možnosti spouštět klienta a server na různých fyzických či virtuálních strojích je tu možnost využít právě network namespaces. Konfigurace je pak jednoduchá a rychlá a dobře se integruje se zmíněnými nástroji na sledování volání síťového API.

Testovací framework LNST

S vývojáři projektu LNST, jenž nabízí automatizaci síťového testování na Linuxu, jsem vícekrát konzultoval možnost použití tohoto projektu pro účely testování klientských a síťových aplikací. Ukázalo se, že LNST nabízí spoustu prostředků, které pro tento účel nejsou potřeba a naopak nijak neulehčuje vytvoření těch jmenných prostorů a spouštění procesů kontrolovaných test driverem.

Projekt network-testing

Na základě všech těchto požadavků a zkušeností jsem vytvořil na githubu projekt pavlix/network-testing, který využívá výše zmíněných technik k analýze chování různých klientských a serverových aplikací. Vedle testů pro tyto aplikace projekt obsahuje samotný test driver, který celou analýzu chování řídí. Můžete v něm najít inspiraci pro vlastní testování nebo se k němu přidat a posunout ho zase o krok dál.

Projekt netresolve

V rámci výše zmíněného projektu se využívá již existující projekt github.com/pavlix/netresolve, jehož hlavním účelem je nabídnout ukázkové API a implementaci překladu síťových jmen, které netrpí omezeními getaddrinfo() v glibc. Zároveň ale nabízí různé testovací nástroje včetně knihoven pro použití s proměnnou prostředí LD_PRELOAD, která zajistí nahrazení vybraných funkcí alternativní implementací.

Pro tento konkrétní účel slouží netresolve víceméně jako alternativa k projektu cwrap, který nabízí větší množství knihoven pro použití s LD_PRELOAD, které umí falšovat výsledky různých knihovních API.

Projekt netresolve má ovšem poněkud širší záběr než jen vytvořit knihovnu a nástroje pro resolving. Součástí je i knihovna s ukázkovou implementací TCP/UDP klienta a serveru, která se používá v projektu network-testing pro otestování testovacích scénářů a rovněž slouží k porovnání s chováním existujících aplikací.

Výsledky

V současné době se nesoustředím na nějaké konkrétní verdikty ohledně open source aplikací, to považuju za předčasné. Samotný projekt network-testing je teď ve fázi sběru testovacích skriptů pro různé aplikace, na základě kterých plánuju v další fázi rozšiřovat a upravovat test driver, který jsem doposud rozvíjel bez potřebného datasetu, který vyžaduje rozličné druhy aplikací z hlediska přístupu k síťovému API.

Naopak si myslím, že je nejlepší čas začít sbírat požadavky na výstupy a testovací metody, které nejsou zahrnuty v aktuální verzi projektu a stejnětak požadavky pro organizaci knihovny pro použití v dalších projektech.