Search code examples
clinuxsocketsdatagram

What's wrong with this datagram socket?


I have some code here which queries the Steam master servers to get a list of IPs of game servers:

#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;
}

(Note the one second timeout.)

It is fine other than some odd behaviour with the communication. It seems to either work first time or continually timeout again, and again, and never succeed.

The way to fix is to simply run it again and it may work, but I don't understand the reason for it to arbitrarily not work.

Any input would be appreciated!


Solution

  • The host hl2master.steampowered.com resolves to three IP addresses:

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

    Two of the three IP address are responding to queries, the third is not:

    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$
    

    Small note, I changed fprintf(stderr, "TIMEOUT\n"); to perror("recvfrom: TIMEOUT"); in the course of trying your code.

    Maybe try a using a different server after the 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 */
    

    The above edits will try each address returned by gethostbyame() twice before moving to the next returned IP address.