Dydaktyka:
FeedbackOmawiany 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 netcat 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?
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:
#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.
#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.
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:
#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; }
#include <iostream> #include <string> #include <unordered_map> #include <ws2tcpip.h> // ┐ getaddrinfo jest #pragma comment(lib, "Ws2_32.lib") // ┘ w innej bibliotece // 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{{0,"0"},{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) { WSADATA wsadata{}; // ┐ no i standardowo trzeba WSAStartup(MAKEWORD(2, 2),&wsadata); // ┘ zainicjalizować winsock 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; }
Zwróć uwagę, że Windows nie ustawia
.ai_socktype ani .ai_protocol jeżeli
pasuje kilka wartości – w tym nie trzyma się
specyfikacji POSIX.
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:
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:
#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); } }
#include <iostream> #include <sstream> #include <winsock2.h> // ┐ #include <ws2tcpip.h> // ├ inne pliku nagłówkowe dla winsock #pragma comment(lib, "Ws2_32.lib") // ┘ // pomocnicza funkcja dla prostszej obsługi błędów inline char *getLastErrorMessage() { static thread_local char msgBuf[1024]; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, WSAGetLastError(), 0, msgBuf, 1024, nullptr); return msgBuf; } int main(int c, char **a) { WSADATA wsadata{}; // ┐ no i standardowo trzeba WSAStartup(MAKEWORD(2, 2),&wsadata); // ┘ zainicjalizować winsock const char *port = (c < 2 ? "4444" : a[1]); int family = (c < 3 ? AF_INET6 : AF_INET), status; // zwróć uwagę że tutaj jest ustawiane też socktype addrinfo *res, hints{.ai_flags = AI_PASSIVE, .ai_family = family, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP}; if ((status = getaddrinfo(nullptr, port, &hints, &res))) { std::cerr << "Getaddrinfo failed: " << gai_strerror(status) << std::endl; return 1; } SOCKET s = socket(res->ai_family, res->ai_socktype, 0), one = 1, zero = 0; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one)); // windows domyślnie dla AF_INET6 słucha tylko na IPv6; poniższa linia zmienia to zachowanie setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&zero, sizeof(zero)); if (bind(s, res->ai_addr, res->ai_addrlen) == SOCKET_ERROR) { std::cerr << "Bind failed: " << getLastErrorMessage() << std::endl; return 1; } freeaddrinfo(res); listen(s, 1); std::cout << "Listening on port " << port << std::endl; while (1) { sockaddr *cliAddr = (sockaddr *)new sockaddr_storage; socklen_t cliAddrLen = sizeof(sockaddr_storage); SOCKET c = accept(s, cliAddr, &cliAddrLen); if (c == INVALID_SOCKET) { 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 << "\r [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; send(c, msg.str().c_str(), msg.str().length(), 0); shutdown(c, SD_BOTH); closesocket(c); } }
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.