Проблема атаки типа "отказ в обслуживании" в книге Unix Networking Programming

Я читаю "Сетевое программирование Unix" 3-е издание.

Я столкнулся с вопросом в разделе 6.8 «TCP Echo Server (Revisited)», здесь представлен код, как показано ниже:

#include    "unp.h"

int
main(int argc, char **argv)
{
    int                 i, maxi, maxfd, listenfd, connfd, sockfd;
    int                 nready, client[FD_SETSIZE];
    ssize_t             n;
    fd_set              rset, allset;
    char                buf[MAXLINE];
    socklen_t           clilen;
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    maxfd = listenfd;           /* initialize */
    maxi = -1;                  /* index into client[] array */
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;         /* -1 indicates available entry */
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    for ( ; ; ) {
        rset = allset;      /* structure assignment */
        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(listenfd, &rset)) {    /* new client connection */
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);

            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) {
                    client[i] = connfd; /* save descriptor */
                    break;
                }
            if (i == FD_SETSIZE)
                err_quit("too many clients");

            FD_SET(connfd, &allset);    /* add new descriptor to set */
            if (connfd > maxfd)
                maxfd = connfd;         /* for select */
            if (i > maxi)
                maxi = i;               /* max index in client[] array */

            if (--nready <= 0)
                continue;               /* no more readable descriptors */
        }

        for (i = 0; i <= maxi; i++) {   /* check all clients for data */
            if ( (sockfd = client[i]) < 0)
                continue;
            **if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                        /*4connection closed by client */
                    Close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else
                    Writen(sockfd, buf, n);**

                if (--nready <= 0)
                    break;              /* no more readable descriptors */
            }
        }
    }
}

Об этой программе автор сказал, что сервер пострадает от DDOS-атаки, как показано ниже: введите описание изображения здесь

дело в том, что когда приходит запрос клиента, сервер читает всю строку, а затем повторяет ее. Но это код, мы видим, что сервер использует функцию чтения для чтения данных от клиента, а не ReadLine или Readn, последние не вернутся, пока не обнаружат '\ n' или не извлекут данные указанного размера, но функция чтения немедленно вернется в этом случае. Функция чтения просто оболочка системного вызова «read», как показано ниже:

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
    ssize_t     n;

    if ( (n = read(fd, ptr, nbytes)) == -1)
        err_sys("read error");
    return(n);
}

Вот я и запутался, почему этот сервер пострадает от ddos ​​атаки?

Кто-нибудь может это прояснить? Большое спасибо!


person Joseph Yu    schedule 23.09.2017    source источник
comment
В примере явно подразумевается, что сервер является однопоточным, а операция чтения блокируется. Это означает, что после чтения первого байта сервер держит соединение открытым, ожидая большего, но клиент никогда не отправляет его, поэтому сервер недоступен для второго клиента.   -  person Drunken Code Monkey    schedule 23.09.2017
comment
Кроме того, это не DDoS-атака, а обычная DoS-атака.   -  person President James K. Polk    schedule 23.09.2017
comment
Спасибо за исправление Джеймса.   -  person Joseph Yu    schedule 28.09.2017


Ответы (2)


Я думаю, что путаница связана с возможной разницей между вторым и третьим изданием книги.

У меня 2-е издание и в нем "Читать" на самом деле "Читать". Тогда объяснение имеет смысл из-за того, что Readline настаивает на чтении до новой строки.

У меня нет копии 3-го издания, чтобы сравнить с ней.

Что касается объяснения от Drunken Code Monkey, правда, чтение блокируется, однако оно защищено выбором, который гарантирует, что чтение будет вызвано только в случае активности на сокете (либо отключение, либо хотя бы 1 байт для читать). Таким образом, гарантируется, что чтение не будет заблокировано. Но посмотрите мое объяснение относительно замены Read на Readline (как во 2-м издании)

См. также предыдущую публикацию о переполнении стека Разъяснение сетевого программирования Unix

person Stephane Desmarais    schedule 26.09.2017
comment
Блокировка чтения не означает блокировку навсегда. Тот простой факт, что чтение чего-то ждет, сам по себе является проблемой. Пример был про одного клиента, но допустим у вас таймаут на чтение 500мс. Теперь предположим, что злоумышленник инициирует шквал соединений, используя эту задержку. Это распределенная атака типа "отказ в обслуживании", и она поставит сервер на колени. Правильный способ обработки — создание отдельного потока для каждого входящего подключения к сокету сразу, еще до чтения. - person Drunken Code Monkey; 28.09.2017
comment
Вы правы, спасибо вам! Я подтвердил у автора Энди Рудоффа, вот его ответ: Да, вы правы. Это ошибка в книге. В предыдущем издании использовалась строка чтения, поэтому эта проблема страдала, но мы забыли изменить абзац при его обновлении. Если мы даже выпустим еще одно издание этой книги, это будет в моем списке того, что нужно исправить! - person Joseph Yu; 28.09.2017
comment
Я не знал, что 2 других автора переделали работу покойного У. Ричарда Стивенса. Поздравляем Джозефа с обнаружением этого. - person Stephane Desmarais; 28.09.2017

Согласно ответу Стефана, вот пример, иллюстрирующий правильную обработку соединения на многопоточном TCP-сервере. Обратите внимание, что я недостаточно хорошо разбираюсь в разработке под Linux, чтобы писать ее легко, так что это C#, но поток программы должен быть таким же. Считайте это псевдокодом, если необходимо.

    // We use a wait handle here to synchronize the client threads with the main thread.
    private static AutoResetEvent _waitHandle = new AutoResetEvent(false);

    static void Main(string[] args)
    {
        // Start the server on port 1337
        StartServer(1337);
    }

    private static void StartServer(int port)
    {
        // Create a connection listener
        var listener = new TcpListener(IPAddress.Any, port);

        try
        {
            // Start the listener
            listener.Start();
            while (true)
            {
                // Wait for a connection, and defer connection handling asynchronously.
                listener.BeginAcceptTcpClient(new AsyncCallback(HandleAsyncConnection), listener);
                _waitHandle.WaitOne();
                _waitHandle.Reset();
            }
        }
        catch (SocketException ex)
        {
            // Handle socket errors or any other exception you deem necessary here
        }
        finally
        {
            // Stop the server.
            listener.Stop();
        }
    }

    private static void HandleAsyncConnection(IAsyncResult state)
    {
        // Get the listener and the client references
        var listener = (TcpListener)state.AsyncState;
        using (var tcpClient = listener.EndAcceptTcpClient(state))
        {
            // Signal the main thread that we have started handling this request.
            // At this point the server is ready to handle another connection, and no amount
            // of tomfoolery on the client's side will prevent this.
            _waitHandle.Set();

            // Declare buffers
            var inBuff = new byte[tcpClient.ReceiveBufferSize];
            var outBuff = new byte[tcpClient.SendBufferSize];

            // Get the connection stream
            using (var stream = tcpClient.GetStream())
            {
                try
                {
                    // Read some data into inBuff
                    stream.Read(inBuff, 0, tcpClient.ReceiveBufferSize);

                    // Do something with the data here, put response in outBuff...

                    // Send response to client
                    stream.Write(outBuff, 0, outBuff.Length);
                }
                catch (SocketException ex)
                {
                    // Handle socket errors or any other exception you deem necessary here
                }
            }
        }
    }
person Drunken Code Monkey    schedule 28.09.2017
comment
Я предполагаю, что в разделе \\Прочитать некоторые данные в inBuff // Отправить ответ клиенту отсутствует код - что, если прочитано меньше байтов, чем tcpClient.ReceiveBufferSize? Или tcpClient.ReceiveBufferSize != outBuff.Length? - person Andrew Morton; 04.10.2017
comment
Если данных меньше, чем ReceiveBufferSize, вызов Read истечет по тайм-ауту после TcpClient.ReceiveTimeout. Если вы ожидаете, что входящие данные будут больше, чем вы можете реально буферизовать, вам придется вызывать Read несколько раз, пока данные не закончатся. Пример, так как он полностью функционален для сообщений соответствующего размера, но да, вы можете захотеть соответствующим образом раскрасить код отправки и получения. Важно то, что на данный момент это не имеет значения, потому что запрос обслуживается в отдельном потоке. Слушатель сервера снова стал доступен в тот момент, когда мы _waitHandle.Set(). - person Drunken Code Monkey; 04.10.2017