Spis treści

DNS - przypomnienie

Omawiany wcześniej na zajęciach system nazw domenowych pozwala zmieniać nazwy domenowe na adresy IP oraz adresy IP na nazwy domenowe.

Na zajęciach o protokołach warstwy transportu pojawiła się informacja że znane numery portów mają swoje nazwy, w Linuksie źródłem informacji o nazwanych portach jest plik /etc/services.

Zadanie 1 Przypomnij sobie z zajęć z DNS ile nazw domenowych może rozwiązywać się na ten sam adres IP oraz do ilu nazw domenowych może prowadzić odwzorowanie odwrotne z adresu IP.

Zadanie 2 Używając programu netcat lub socat nawiąż połączenie TCP podając jako adres antares.put.poznan.pl, a jako numer portu podaj ciąg znaków smtp.
Następnie sprawdź jak to połączenie prezentowane jest w programie netstat z przełącznikami -ptW oraz -ptW -n, lub programie ss z przełącznikami -pt -r oraz -pt -n. (W programie ss możesz dodać też do argumentów dst antares.put.poznan.pl żeby zobaczyć tylko połączenia kierowane pod ten adres.)

Zadanie 3 Powtórz poprzednie zadanie, ale łącz się pod adres put.poznan.pl i port www.
Jaką różnicę widać w prezentowaniu informacji o połączeniu w porównaniu z poprzednim zadaniem?

Przestarzałe funkcje

Najstarsza funkcja do znalezienia IP dla podanej nazwy domenowej to gethostbyname. Ta funkcja jest przestarzała (i mimo to jeszcze używana).

Przykład użycia:

ghbn_example.cpp
#include <arpa/inet.h>
#include <cstdio>
#include <netdb.h>
int main(int c, char **v) {
    const char * addrAsText = c < 2 ? "pool.ntp.org" : v[1];
    hostent *ret = gethostbyname(addrAsText);
    if (!ret) { herror("gethostbyname failed"); return 1; }
    for (int i = 0; ret->h_addr_list[i]; ++i) {
        in_addr *addrAsInt = (in_addr *)ret->h_addr_list[i];
        puts(inet_ntoa(*addrAsInt));
    }
    return 0;
}

Zadanie 4 Używając powyższego kodu sprawdź adresy domen put.poznan.pl, dnslabs.nl i ipv6.google.com. Wejdź na te stony w przeglądarce. Których nazw domenowych funkcja gethostbyname nie rozwiązała na IP i dlaczego?

Co ważne, funkcja gethostbyname nie może być bezpiecznie używana w kodzie wielowątkowym.

Zadanie 5 Sprawdź co się może wydarzyć jeśli gethostbyname zostanie użyta współbieżnie1) uruchamiając poniższy kod.

ghbn_and_threads.cpp
#include <arpa/inet.h>
#include <cstdio>
#include <netdb.h>
#include <thread>
#include <unistd.h>
#define S usleep(100000);
 
int main() {
    std::thread([] { S;
        gethostbyname((char[]){89, 84, 46, 66, 69, 0});
    }).detach();
 
    hostent *r = gethostbyname("cat.put.poznan.pl");                   S; S;
    printf("%s: %s\n", r->h_name, inet_ntoa(*(in_addr *)r->h_addr_list[0]));
    return 0;
}

Wyjaśnij skąd biorą się wypisane na standardowe wyjście wyniki.

Poza gethostbyname istnieje funkcja gethostbyaddr zmieniająca IP na nazwę domenową.
Do zmiany nazwy portu na numer (lub na odwrót) istnieje funkcja getservbyname (i getservbyport).

Wszystkie wspominane tutaj funkcje mają podobne problemy – między innymi brak wsparcia dla IPv6 i współbieżności.

getaddrinfo

Standard POSIX wprowadza funkcję getaddrinfo, która pozwala na bezpieczne tłumaczenie nazwy domenowej (lub adresu IP) oraz nazwy portu (lub jego numeru) z tekstu na odpowiedni format dla funkcji sieciowych (czyli wskaźnik na strukturę sockaddr).
Poza adresem potrzebnym np. w bind czy connect, funkcjach getaddrinfo przygotowuje wszystkie argumenty potrzebne w funkcji socket.

Funkcja getaddrinfo sama alokuje pamięć dla wyników, stąd wyniki muszą być zwolnione ręcznie funkcją freeaddrinfo.

Uwaga: getaddrinfo w razie sukcesu zwraca 0, a w przypadku błędu zwraca jego kod. Czytelny dla człowieka komunikat powiązany z kodem błędu można uzyskać funkcją gai_strerror. Zwróć uwagę, że to odbiega od typowej konwencji POSIX.

Przykład użycia getaddrinfo wyświetlający informacje które znajdują się w zwracanych przez tą funkcję strukturach addrinfo:

gai_example.cpp
#include <iostream>
#include <netdb.h>
#include <string>
#include <unordered_map>
// poniższy blok kodu, do funkcji main, to pomocnicze struktury i funkcje;
// proszę w trakcie zajęć nie poświęcać czasu na ich analizę!
using namespace std; typedef unordered_map<int, string> map; 
map families{{AF_INET,"AF_INET "},{AF_INET6,"AF_INET6"}};
map socktypes{{SOCK_RAW,"SOCK_RAW   "},{SOCK_STREAM,"SOCK_STREAM"},{SOCK_DGRAM,"SOCK_DGRAM "}};
map protocols{{0,"0"},{IPPROTO_TCP,"IPPROTO_TCP"},{IPPROTO_UDP,"IPPROTO_UDP"}};
string getIp(sockaddr*a,socklen_t l){char b[40];getnameinfo(a,l,b,40,0,0,NI_NUMERICHOST);return b;}
string getPort(sockaddr*a,socklen_t l){char b[6];getnameinfo(a,l,0,0,b,6,NI_NUMERICSERV);return b;}
 
int main(int c, char **a) {
    const char *host = (c<2 ? "ietf.org" : a[1]);
    const char *port = (c<3 ? nullptr    : a[2]);
 
    addrinfo *resolved;
    int res = getaddrinfo(host, port, nullptr, &resolved);
 
    if (res) {
        cerr << "Getaddrinfo failed: " << gai_strerror(res) << endl;
        return 1;
    }
 
    for (addrinfo *it = resolved; it;  it = it->ai_next) {
        cout <<   "family: "   << families[ it->ai_family]
             << "  address: "  << getIp(    it->ai_addr, it->ai_addrlen)
             << "  port: "     << getPort(  it->ai_addr, it->ai_addrlen)
             << "  socktype: " << socktypes[it->ai_socktype]
             << "  protocol: " << protocols[it->ai_protocol]
             << endl;
    }
 
    freeaddrinfo(resolved);
 
    return 0;
}

Kod dla MS Windows

Zwróć uwagę że w zwracanych przez getaddrinfo strukturach addrinfo pole .ai_addr jest typu sockaddr*, więc przekazując adres do funkcji takich jak bind czy connect nie ma potrzeby rzutowania. Jednocześnie z takiego pola nie da się określić rozmiaru struktury, stąd w addrinfo jest też pole .ai_addrlen wypełniane rozmiarem adresu.

Zadanie 6 Przetestuj powyższy kod. Sprawdź go dla kilku wybranych domen i nazw portów.
Sprawdź między innymi nazwy portów bnetgame, urd i who.

Trzecim argumentem funkcji getaddrinfo jest wskaźnik na wypełnioną przez programistę strukturę addrinfo, która pozwala wybrać które z potencjalnych wyników mają zostać zwrócone. Działanie podpowiedzi jest zaprezentowane poniżej:

Protokół warstwy sieci:
Rodzaj gniazda:
Protokół warstwy transportowej:
Opcje:
                

getnameinfo

Standard POSIX wprowadza też m. inn. wygodną funkcję do tłumaczenia sockaddr* na tekst – funkcję getnameinfo.

Działanie getnameinfo użyte w serwerze do tłumaczenia adresu z którego połączy się klient prezentuje poniższy kod:

gni_example.cpp
#include <iostream>
#include <netdb.h>
#include <sstream>
#include <unistd.h>
 
int main(int c, char **a) {
    const char *port = (c < 2 ? "4444" : a[1]);
    int family = (c < 3 ? AF_INET6 : AF_INET), status, one = 1, zero = 0;
 
    addrinfo *res, hints{.ai_flags = AI_PASSIVE, .ai_family = family, .ai_protocol = IPPROTO_TCP};
    if ((status = getaddrinfo(nullptr, port, &hints, &res))) {
        std::cerr << "Getaddrinfo failed: " << gai_strerror(status) << std::endl;
        return 1;
    }
 
    int s = socket(res->ai_family, res->ai_socktype, 0);
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    // setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero));
    if (bind(s, res->ai_addr, res->ai_addrlen)) { perror("Bind failed"); return 1; }
    freeaddrinfo(res);
    listen(s, 1); std::cout << "Listening on port \e[1;33m" << port << "\e[0m" << std::endl;
 
    while (1) {
        sockaddr *cliAddr = (sockaddr *)new sockaddr_storage;
        socklen_t cliAddrLen = sizeof(sockaddr_storage);
        int c = accept(s, cliAddr, &cliAddrLen);
        if (c == -1) { perror("Bind failed"); return 1; }
        std::stringstream msg;
 
        msg << "Client connected from:" << std::endl;
 
        char host[NI_MAXHOST], port[NI_MAXSERV];
 
        status = getnameinfo(cliAddr, cliAddrLen,
                    host, NI_MAXHOST,
                    port, NI_MAXSERV,
                    0);
        if (status) std::cerr << "Getnameinfo failed: " << gai_strerror(status) << std::endl;
        msg << "  [resolved] address: " << host << "  port: " << port << std::endl;
 
        status = getnameinfo(cliAddr, cliAddrLen,
                    host, NI_MAXHOST,
                    port, NI_MAXSERV,
                    NI_NUMERICHOST | NI_NUMERICSERV);
        if (status) std::cerr << "Getnameinfo failed: " << gai_strerror(status) << std::endl;
        msg << "  [numeric ] address: " << host << "  port: " << port << std::endl;
 
        delete cliAddr;
        std::cout << msg.str() << std::flush;
        write(c, msg.str().c_str(), msg.str().length());
        shutdown(c, SHUT_RDWR); close(c);
    }
}

Kod dla MS Windows

Zadanie 7 Przetestuj powyższy kod. Poproś osobę siedzącą obok żeby połączyła się do ciebie używając zarówno twojego adresu IPv4 i IPv6.
Żeby dodatkowo łączyć się z wybranego numeru portu, możesz użyć opcji bind programu socat tak jak w tych przykładach:
socat tcp4:127.0.0.1:4444,bind=:3074 stdio       socat tcp6:[::1]:4444,bind=:5222 stdio.

1) ale nie równolegle, wtedy mogłoby być dużo gorzej