Что не так с этим сокетом дейтаграммы? - PullRequest
1 голос
/ 20 октября 2011

У меня есть некоторый код, который запрашивает главные серверы Steam , чтобы получить список IP-адресов игровых серверов:

#define _BSD_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>

struct timeval timeout = {1, 0};
char master[256];
char reply[1500];
uint16_t port;
uint8_t query[] = {0x31, 0x03, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2e,
                         0x30, 0x3a, 0x30, 0x00, 0x00};
uint8_t replyHeader[] = {0xff, 0xff, 0xff, 0xff, 0x66, 0x0a};
int gotResponse = 0;
int bytesRead = 0;
int verbosity = 0;

int main(int argc, char** argv)
{
    strcpy(master, "hl2master.steampowered.com");
    port = 27011;

    int opt;

    while ((opt = getopt(argc, argv, "s:p:v")) != -1)
    {
        switch (opt)
        {
            case 's':
                strcpy(master, optarg);
                break;
            case 'p':
                port = atoi(optarg);
                break;
            case 'v':
                verbosity++;
                break;
        }
    }

    int sockFD;
    struct sockaddr_in server;
    struct hostent* hostInfo;

    sockFD = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

    if (sockFD == -1)
    {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    if ((setsockopt(sockFD, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)))
         != 0)
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    if ((setsockopt(sockFD, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)))
         != 0)
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    hostInfo = gethostbyname(master);

    if (hostInfo == NULL)
    {
        fprintf(stderr, "Unknown host %s\n", master);
        exit(EXIT_FAILURE);
    }

    server.sin_addr = *(struct in_addr*) hostInfo->h_addr_list[0];

    while (gotResponse == 0)
    {
        if ((sendto(sockFD, query, 15, 0, (struct sockaddr*) &server,
                        sizeof(server))) == -1)
        {
            perror("sendto");
            exit(EXIT_FAILURE);
        }

        socklen_t serverSize = sizeof(server);

        if ((bytesRead = recvfrom(sockFD, reply, 1500, 0,
                                          (struct sockaddr*) &server, &serverSize)) == -1)
        {
            if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
            {
                fprintf(stderr, "TIMEOUT\n");
            }
            else
            {
                perror("recvfrom");
                exit(EXIT_FAILURE);
            }
        }
        else
            gotResponse = 1;
    }

    if ((close(sockFD)) == -1)
    {
        perror("close");
        exit(EXIT_FAILURE);
    }

    if ((strncmp(reply, replyHeader, 6)) != 0)
    {
        fprintf(stderr, "Bad reply from master server\n");
        exit(EXIT_FAILURE);
    }

    uint32_t i = 6;

    while (i < bytesRead)
    {
        if (verbosity > 0)
            fprintf(stderr, "%u <= %d\n", i, bytesRead);

        uint8_t ip[4] = {reply[i], reply[i + 1], reply[i + 2], reply[i + 3]};

        printf("%hu.%hu.%hu.%hu:", ip[0], ip[1], ip[2], ip[3]);

        uint16_t thisPort = reply[i + 4] + (reply[i + 5] << 8);

        printf("%hu\n", ntohs(thisPort));

        i += 6;
    }

    return EXIT_SUCCESS;
}

(обратите внимание на одну секунду timeout.)

Это нормально, если не считать странного поведения в общении. Кажется, или работают в первый раз или постоянно снова и снова, и снова, и никогда не будет успешным.

Способ исправить это просто запустить его снова, и он может работать, но я не понимаю, почему он произвольно не работает.

Любой вклад будет оценен!

Ответы [ 5 ]

3 голосов
/ 20 октября 2011

Хост hl2master.steampowered.com разрешает три IP-адреса:

syzdek@blackenhawk$ dig +short hl2master.steampowered.com
63.234.149.83
63.234.149.90
72.165.61.153
syzdek@blackenhawk$

Два из трех IP-адресов отвечают на запросы, третий - нет:

syzdek@blackenhawk$ ./a.out -s 63.234.149.83 |head -2
66.189.187.173:27012
216.6.229.173:27015
syzdek@blackenhawk$ ./a.out -s 63.234.149.90 |head -2
66.189.187.173:27012
216.6.229.173:27015
syzdek@blackenhawk$ ./a.out -s 72.165.61.153
recvfrom: TIMEOUT: Resource temporarily unavailable
recvfrom: TIMEOUT: Resource temporarily unavailable
^C
syzdek@blackenhawk$

Небольшая заметка, я изменил fprintf(stderr, "TIMEOUT\n"); на perror("recvfrom: TIMEOUT"); во время попытки вашего кода.

Возможно, попробуйте использовать другой сервер после истечения времени ожидания:

int retryCount = 0;
while (gotResponse == 0)
{
    // verify that next address exists
    if (hostInfo->h_addr_list[retryCount/2] == NULL)
    {
        fprintf(stderr, "All servers are not responding.");
        exit(EXIT_FAILURE);
    };

    // Attempt each address twice before moving to next IP address
    server.sin_addr = *(struct in_addr*) hostInfo->h_addr_list[retryCount/2];
    retryCount++;

    if ((sendto(sockFD, query, 15, 0, (struct sockaddr*) &server,
                            sizeof(server))) == -1)
    {
        perror("sendto");
        exit(EXIT_FAILURE);
    }

    / * rest of code */

Приведенные выше правки будут пробовать каждый адрес, возвращенный gethostbyame (), дважды, прежде чем перейти к следующему возвращенному IP-адресу.

3 голосов
/ 20 октября 2011

Поскольку вы опубликовали весь исполняемый код, я попытался запустить его с помощью strace, как предложил cdleonard.Это сразу показывает, что gethostbyname вызов для hl2master.steampowered.com возвращает один из двух разных адресов при каждом запуске программы - или 63.234.149.83 или 72.165.61.153 - и когда возвращается первый адрес, он работает нормально, тогда каквторой адрес завершается неудачно с таймаутами.

Таким образом, проблема заключается в том, что для DNS-сервера есть два адреса, но один из них на самом деле не работает.h_addr_list, возвращаемый gethostbyaddr и проходящий по очереди каждый адрес в цикле, а не просто отправку по первому адресу.

3 голосов
/ 20 октября 2011

Просто дикая догадка: может быть, recvfrom искажает аргумент сервера, а следующие посылки имеют неправильный адрес?Попробуйте передать отдельную структуру sockaddr.

Вывод strace может помочь.

1 голос
/ 20 октября 2011

В случае, если вы получаете EAGAIN от вашего звонка на recvfrom(), что означает «Resouce временно недоступен», вы можете попытаться повторно позвонить gethostbyname(), на тот случай, если DNS может дать вам другой IP для этого имени хостаВы проходите (в круговом порядке).

Если это так, и хотя бы один из адресов, возвращаемых DNS, недоступен, вы будете вести себя точно так же, как и вы.

Как примечание: gethostbyname() можетвозвращать статические данные, поэтому лучше скопировать результат, а не просто сослаться на него.

0 голосов
/ 21 октября 2011

Вместо gethostbyname(), который возвращает только один IP-адрес, вы можете попробовать с getaddrinfo(), который дает вам их все, и даже со всеми поддерживаемыми семействами адресов.

Вот как это работает:

    int sockFD;
    struct hostent* hostInfo;

    struct addrinfo hints = {
        .ai_socktype = SOCK_DGRAM,
        .ai_protocol = IPPROTO_UDP /* MHO redundant*/
    };
    struct addrinfo * ai_chain, *ai;

    int gai_ret = getaddrinfo(master, NULL, &hints, &ai_chain);
    if (gai_ret != 0) {
        fprintf(stderr, "getaddrinfo: %s", gai_strerror(gai_ret));
        exit(EXIT_FAILURE);
    }

    for (ai = ai_chain; ai; ai = ai->ai_next) {
        printf("try %s\n", ai->ai_canonname ? ai->ai_canonname : "");
        sockFD = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
        if (sockFD == -1)
        {
            perror("socket");
            continue;
        }

        if ((setsockopt(sockFD, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))) != 0)
        {
            perror("setsockopt 1");
            continue;
        }

        if ((setsockopt(sockFD, SOL_SOCKET, SO_SNDTIMEO, &timeout,
    sizeof(timeout)))
            != 0)
        {
            perror("setsockopt 2");
            continue;
        }

        if ((sendto(sockFD, query, 15, 0, ai->ai_addr, ai->ai_addrlen)) == -1)
        {
            perror("sendto");
            continue;
        }

        struct sockaddr_in6 server;
        socklen_t serverSize = sizeof(server);

        if ((bytesRead = recvfrom(sockFD, reply, 1500, 0,
                                        (struct sockaddr*) &server,
&serverSize)) == -1)
        {
            if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
            {
                fprintf(stderr, "TIMEOUT\n");
                continue;
            }
            else
            {
                perror("recvfrom");
                continue;
            }
        }
        else
            break;
    }

    if ((close(sockFD)) == -1)
    {
        perror("close");
        exit(EXIT_FAILURE);
    }

    if ((strncmp(reply, replyHeader, 6)) != 0)
    {
        fprintf(stderr, "Bad reply from master server\n");
        exit(EXIT_FAILURE);
    }

    uint32_t i = 6;

    while (i < bytesRead)
    {
        if (verbosity > 0)
            fprintf(stderr, "%u <= %d\n", i, bytesRead);

        uint8_t ip[4] = {reply[i], reply[i + 1], reply[i + 2], reply[i + 3]};

        printf("%hu.%hu.%hu.%hu:", ip[0], ip[1], ip[2], ip[3]);

        uint16_t thisPort = reply[i + 4] + (reply[i + 5] << 8);

        printf("%hu\n", ntohs(thisPort));

        i += 6;
    }

    return EXIT_SUCCESS;
}

Хотя у кода все еще есть тот или иной недостаток (до сих пор он никогда не закрывает сокеты; уязвимость переполнения буфера в master из-за strcpy() вместо этогоstrncpy()), он показывает вам, как все работает.

...