Ve specifikaci funkce getaddrinfo() a v její implementaci v GLIBC se nachází zvláštní opatření jménem AI_ADDRCONFIG, které má zajistit, aby aplikace v seznamu adres nedostala adresy, ke kterým se nemůže připojit. Takovéto opatření se používá hlavně na dualstacku IPv4 a IPv6, kde je potřeba, aby se aplikace připojovala pomocí protokolu, který je na daném stroji dostupný.

Problém je v tom, že AI_ADDRCONFIG nefunguje. A nejenom to, v případech, kdy něco dělá, způsobí často více škody než užitku. Mezi škody se počítá například rozbití getaddrinfo() pro lokální síťování v rámci stroje a linkové vrstvy, když stroj nemá globální konektivitu.

Testoval jsem to na balíčku glibc-2.15-51.fc17. Od té doby se podařilo některé bugreporty zavřít jako CLOSED ERRATA nebo CLOSED UPSTREAM, aniž by se problém skutečně vyřešil.

Typické použití funkce getaddrinfo

Uživatel zadá, kam se chce připojit. To vyjádří názvem serveru nebo IP adresou. Název serveru může být název v /etc/hosts, globálním DNS (test.example.net), Multicast DNS (test.local) nebo název používaný jinou jmennou službou. Internetové adresy jsou různých typů. Globální adresy jsou například 217.31.205.50 a 2001:1488:0:3::2. Linkové adresa je například fe80::12:34ff:fe56:7890%eth0 (linkové adresy v IPv4 existují a vybírají se z rozsahu 169.254.0.0/16, ale jejich použitelnost je v Linuxu značně omezená). Adresy místní smyčky jsou 127.0.0.1 a ::1 (rozhraní lo ale může mít přidělené i další adresy, třeba i globálního rozsahu).

Klientská aplikace toto uživatelské zadání předá funkci getaddrinfo() s dalšími parametry a getaddrinfo() pro aplikaci připraví spojový seznam, jehož každý záznam obsahuje kompletní informace k připojení se po TCP nebo odesílání zpráv po UDP.

Trochu více podrobností

Funkce getaddrinfo() má čtyři parametry.

code = getaddrinfo(node, service, &hints, &result)

Prvním je node, který obsahuje výše uvedené zadání cíle, a doplňuje ho service, který určuje číslo portu (port může být uveden jménem služby a vyhledává se pak v /etc/services). Jméno služby je například http. Všechno ostatní se nastavuje argumentem hints. Argument res slouží k předání adresy ukazatele, do kterého se uloží odkaz na spojový seznam struktur addrinfo. Podle návratové hodnoty se pozná, zda byla funkce úspěšná a případně k jaké chybě došlo.

Konfigurace chování getaddrinfo parametrem hints

Bez podrobností jsem zatím nechal parametr hints, kterým se předává ukazatel na částečně vyplněnou strukturu addrinfo.

struct addrinfo hints;

hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = 0;
hints.ai_protocol = 0;
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;

Atribut ai_family určuje, že má getaddrinfo() vracet pouze adresy IPv6 (AF_INET6), pouze IPv4 (AF_INET) nebo obojí (AF_UNSPEC). Další atribut ai_socktype omezuje typy socketů a umožňuje tak vybrat transportní protokol TCP (SOCK_STREAM), UDP (SOCK_DGRAM) nebo ještě speciální SOCK_RAW. Atribut ai_protocol víceméně duplikuje ai_socktype. Ostatní atributy mají být vynulované, tedy kromě ai_flags, o který se ještě budeme hodně zajímat.

Z voleb podporovaných prostřednictvím ai_flags nás budou zajímat pouze AI_NUMERICHOST (potlačuje překládání jmen), AI_ADDRCONFIG (to je ta problematická), AI_V4MAPPED (přidává rodině AF_INET6 podporu IPv4 pomocí mapování adres) a AI_ALL (opravuje pochybné chování AI_V4MAPPED).

Volba AI_ADDRCONFIG aneb dobrý úmysl, výsledek nesmysl

Soužití protokolů IPv4 a IPv6 na globální síti znamená, že jméno může ukazovat na adresy obou protokolů. Často se stane, že některá získaná adresa nejde použít, například pokud stroj vůbec nedisponuje konektivitou na daném protokolu.

Volba AI_ADDRCONFIG se to snaží zohlednit vyřazením nepoužitelných adres ze seznamu. K jejímu vysvětlení si vypůjčím popis z informativního RFC 3493.

If the AI_ADDRCONFIG flag is specified, IPv4 addresses shall be returned only if an IPv4 address is configured on the local system, and IPv6 addresses shall be returned only if an IPv6 address is configured on the local system. The loopback address is not considered for this case as valid as a configured address.

Tedy, stručně řečeno, pro každou adresu se posoudí, zda se na některém rozhraní kromě lo nachází adresa stejné rodiny (stejné verze protokolu IP). Pokud ne, tak se adresa ze seznamu vyhodí. Můj osobní dojem je, že je to celé pitomost.

Adresy místní smyčky

getaddrinfo("127.0.0.1", ...);

Zcela legitimní dotaz na IPv4 adresu lokálního stroje. V případě, že počítač momentálně není připojen k internetu (nebo je připojen pouze po IPv6), obvykle nemá žádnou další IP adresu. Tento dotaz má tudíž podle výše uvedeného pravidla selhat.

getaddrinfo("::1", ...);

Dotaz na IPv6 adresu lokálního stroje. Podle RFC by měl selhat, pokud stroj není připojen k IPv6 internetu.

getaddrinfo("localhost4");
getaddrinfo("localhost6");
getaddrinfo("localhost");

Dotazy na adresy místní smyčky podle jména. Výsledkem jsou výše uvedené adresy 127.0.0.1 a ::1. V případě stroje nepřipojeného k internetu by mělo dojít k vyřazení každé takové adresy a localhost by měl být mimo provoz.

Linkové adresy protokolu IPv6

Kernel automaticky přiděluje připojeným rozhraním linkové adresy protokolu IPv6. Ty se vždy zadávají v kombinaci se jménem rozhraní, protože mají na každém rozhraní stejný prefix.

getaddrinfo("fe80::12:34ff:fe56:7890%eth0", ...);

Funkce getaddrinfo() takovou adresu překládá do struktury sockaddr, kde místo názvu rozhraní používá interní číslo rozhraní získané od jádra. Já osobně používám linkové adresy pro připojení k virtuálům i fyzickým strojům pomocí SSH. Usnadňuje mi to testování NetworkManageru a dalších síťových nástrojů, protože linkové adresy navzdory různým konfiguračním změnám zůstávají pořád stejné.

Přítomnost těchto adres nijak neindikuje dostupnost IPv6 konektivity a tím pádem automaticky vyřazují z činnosti AI_ADDRCONFIG pro IPv6 adresy. Problém o to více komplikuje fakt, že stroj nepřipojený k IPv6 může a nemusí disponovat linkovou adresou.

V glibc-2.15-51.fc17 je toto řešeno vyjmutím linkových adres z posouzení, zda stroj má IPv6 konektivitu. Zda je to stejně řešeno i pro výjimečně používané linkové adresy IPv4, jsem nezjišťoval.

Takovéto opatření má za následek, že kromě adres místní smyčky nebudou bez globální adresy fungovat ani linkové adresy, a to opět ani v numerické formě. Takto to po nějakou dobu fungovalo ve Fedoře, ale toto chování bylo odstraněno (ač se stručným a nepřesným popiskem).

Výsledky testů pro AF_UNSPEC

Paradoxně se u stroje nepřipojeného k žádné síti volba AI_ADDRCONFIG s rodinou AF_UNSPEC vůbec neprojeví. Žádná adresa není ze seznamu nikdy vyřazena. Aktuální kernel nenastavuje linkové adresy nepřipojeným aktivním rozhraním, takže v tomto případě jsou jediné nastavené adresy 127.0.0.1 a ::1. Toto chování je překvapivé a nezdokumentované.

Ve chvíli, kdy má stroj globální IPv4 konektivitu (a tím pádem i linkové IPv6 adresy na stejném rozhraní), začne se AI_ADDRCONFIG projevovat ve své plné kráse. Nefunguje tedy localhost6, ::1 ani linkové adresy.

Výsledky testů pro AF_INET

Pro AF_INET má tato funkcionalita jen omezený smysl, protože se nevybírá mezi různými verzemi protokolu. Nicméně, z pokusu vyplývá, že AI_ADDRCONFIG rozbije localhost, localhost4 a 127.0.0.1. Podobně mi pro AF_INET6 nefunguje localhost, localhost6, ::1 ani linkové adresy.

V kombinaci s AI_V4MAPPED a AI_ALL má navíc AF_INET6 schopnost pracovat s adresami protokolu IPv4, které jsou mapovány do prostoru IPv6. V takovém případě AI_ADDRCONFIG rozbíjí opět adresy místní smyčky a linkové adresy v obou protokolech.

hints.ai_family = AF_INET6;
hints.ai_flags = AI_V4MAPPED | AI_ALL | AI_ADDRCONFIG;

Řešení na straně getaddrinfo

Tore Anderson mě přivedl na myšlenku, že celé toto opatření se vždy mělo týkat jenom DNS. Ono je tak i popisováno v RFC 2553, které je předchůdcem RFC 3493. Původní RFC totiž specificky ošetřuje DNS. Účelem měla podle všeho být eliminace problémů s existujícími DNS servery a optimalizace. Jenže někdo v záchvatu úprav tuto formulaci zobecnil a výsledkem je filtrování adres, i kdyby pocházely třeba od samotného uživatele.

Nicméně je to další důkaz toho, že návrháři a implementátoři standardů IPv6 rozbíjejí i IPv4, a tudíž není radno nové standardy ignorovat. Řešením je odmítnout formulaci v novém RFC jako chybnou a implementovat getaddrinfo() v nsswitch. Magie s filtrováním dotazů a adres by pak celá přešla na modul zabývající se pouze DNS, kam opravdu patří.

Řešení na straně tvůrců aplikací

Momentálně nelze spoléhat na to, že AI_ADDRCONFIG nerozbije síťování bez globální konektivity, tudíž je potřeba na něj rychle zapomenout. V případě TCP spojení se případný problém s konektivitou projeví okamžitě při volání connect(). Ale connect() lze použít i pro komunikaci pomocí UDP pro určení cílové adresy pro send(). To zajistí alespoň kontrolu, zda je adresa směrovatelná. Jakékoli další kontroly je potřeba provádět přímo v aplikaci.

Samozřejmě to neřeší jiné problémy, a tak je potřeba počkat na opravu GLIBC a později používat AI_ADDRCONFIG podmínečně, v závislosti na verzi GLIBC. Důležité je ale aspoň tušit, že nějaký takový problém existuje, protože tato chyba může vyřadit z provozu některé aplikace, které spoléhají na interní komunikaci po TCP/IP.

Několik dalších poznámek k addrinfo

Když se parametrem hints předá hodnota NULL, výchozí hodnota ai_family je AF_UNSPEC a výchozí hodnota ai_flags je AI_V4MAPPED | AI_ADDRCONFIG. AI_V4MAPPED bez AI_ALL vrací IPv4 adresy, jen pokud nejsou k dispozici žádné IPv6 adresy. Nesmyslně tedy vyřazuje ze seznamu použitelné adresy. AI_V4MAPPED nemá bez AI_ALL smysl. Navíc AI_V4MAPPED funguje pouze s AF_INET6. V kombinaci s AF_UNSPEC tedy nemá smysl už vůbec.

Funkce getaddrinfo() není podporována v GLIBC Name Service Switch a kvůli tomu není možné z pluginů dodávat linkové adresy protokolu IPv6, u kterých je potřeba kromě samotné adresy vyplnit ve struktuře sockaddr_in6 i pole scope_id. To je další důvod, proč by nsswitch měl getaddrinfo() podporovat.

getaddrinfo() by mohla s nějakou volbou podporovat DNS SRV záznamy. Argument service a volba ai_socktype dohromady určují transportní protokol a název služby, které dohromady tvoří doménové jméno pro SRV dotaz. Jediné, co k tomu chybí, je volba, která SRV dotaz aktivuje. Klasické záznamy A a AAAA by sloužily jako záloha, když SRV záznam neexistuje.