Клиент отправляет RST полученным пакетам после завершения работы (SHUT_WR) - PullRequest
0 голосов
/ 17 апреля 2019

После закрытия направления записи соединения сокета с использованием shutdown() все последующие полученные данные вызывают отправку пакета RST, а read() возвращает размер 0, указывающий EOF для направления чтения. Почему это так, хотя направление чтения не было закрыто?

Используя wireshark, я проверил отправленные пакеты:

No. Time     Source        Destination   Protocol Length Info
1   0.000000 192.168.0.175 3.85.154.144  TCP      78     55318 → 80 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 WS=32
2   0.114608 3.85.154.144  192.168.0.175 TCP      74     80 → 55318 [SYN, ACK] Seq=0 Ack=1 Win=26847 Len=0 MSS=1460 WS=256
3   0.114706 192.168.0.175 3.85.154.144  TCP      66     55318 → 80 [ACK] Seq=1 Ack=1 Win=131744 Len=0
4   0.115371 192.168.0.175 3.85.154.144  HTTP     112    GET /bytes/512 HTTP/1.1 
5   0.115401 192.168.0.175 3.85.154.144  TCP      66     55318 → 80 [FIN, ACK] Seq=47 Ack=1 Win=131744 Len=0
6   0.222652 3.85.154.144  192.168.0.175 TCP      66     80 → 55318 [ACK] Seq=1 Ack=47 Win=26880 Len=0
7   0.224444 3.85.154.144  192.168.0.175 HTTP     801    HTTP/1.1 200 OK  (application/octet-stream)
8   0.224543 192.168.0.175 3.85.154.144  TCP      54     55318 → 80 [RST] Seq=48 Win=0 Len=0
9   0.226056 3.85.154.144  192.168.0.175 TCP      66     80 → 55318 [FIN, ACK] Seq=736 Ack=48 Win=26880 Len=0
10  0.226100 192.168.0.175 3.85.154.144  TCP      54     55318 → 80 [RST] Seq=48 Win=0 Len=0

После того, как [FIN, ACK] отправлено, на все полученные данные отвечают RST. Кажется, что локальная сторона думает, что соединение полностью закрыто, даже если был отправлен FIN (с указанием конца записанных данных) вместо RST (с указанием конца соединения). Удаленная сторона ожидает, что сможет отправлять данные, но не может. Мониторинг состояния сокета с помощью netstat показывает, что соединение сразу полностью закрывается после вызова shutdown(sockfd, SHUT_WR).

Вот MWE в C ++. Предполагается, что все не относящиеся к делу функции выполняются успешно и в противном случае прерывается. Коды возврата всех функций проверяются на наличие ошибок, чтобы убедиться, что они не ошибаются в результатах.

#include <arpa/inet.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define RUN_OR_ABORT(fun) {\
    int RETVAL = fun;\
    if (RETVAL == -1)\
    {\
        perror(#fun);\
        abort();\
    }\
}

int main() {
    // Establish connection to httpbin.org:80
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sockfd == -1) exit(EXIT_FAILURE);
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(80);
    // "httpbin.org" resolves to 3.85.154.144 or 52.71.234.219
    int retcode = inet_pton(addr.sin_family, "3.85.154.144", &addr.sin_addr);
    if (retcode != 1) exit(EXIT_FAILURE);
    RUN_OR_ABORT(connect(sockfd, (sockaddr*)&addr, sizeof(addr)));

    // Request 512 random bytes
    const char* msg = "GET /bytes/512 HTTP/1.1\r\nHost: httpbin.org\r\n\r\n";
    ssize_t write_len = write(sockfd, msg, strlen(msg));
    if (write_len != strlen(msg)) exit(EXIT_FAILURE);
    RUN_OR_ABORT(shutdown(sockfd, SHUT_WR));

    // Prepare buffer for recieving data
    size_t buf_size = 1024, read_offset = 0;
    char* buf = new char[buf_size];

    // Read as long as there is data available
    while (true) {
        ssize_t read_len = read(sockfd, buf + read_offset, buf_size - read_offset);
        if (read_len == -1 and (errno & (EAGAIN | EINTR)))
            continue;
        else if (read_len == -1) {
            perror("recv()");
            exit(EXIT_FAILURE);
        }
        read_offset += read_len;
        if (read_len == 0) {
            // EOF?
            printf("%lu bytes have been read\n", read_offset);
            break;
        }
        if (read_offset >= buf_size) {
            fprintf(stderr, "Unexpectedly large response\n");
            exit(EXIT_FAILURE);
        }
    }
}

Я бы ожидал, что вызов read() вернет ненулевой размер и сокет останется открытым для чтения после закрытия конца записи с помощью shutdown(sockfd, SHUT_WR). Ожидаемый выход MWE будет:

740 bytes have been read

(или аналогичный, но количество прочитанных байтов должно быть больше 512 байтов). Фактический результат был:

0 bytes have been read

1 Ответ

0 голосов
/ 17 апреля 2019

Проблема в (неадекватном) прозрачном HTTP-прокси. Web Shield Avast перехватывает пакеты перед их отправкой или получением и проверяет их на наличие вредоносных программ. Для этого Avast использует отдельные внутренние сокеты. Как только отправляется первый FIN, Avast (неправильно) закрывает свой внутренний сокет. Об этом сообщается клиентскому сокету, поэтому впоследствии он не отображается в netstat. Данные сервера затем отклоняются, поскольку сокет Avast, отвечающий за обработку входящих данных, больше не открыт. Стратегия Avast по закрытию соединения по первому пакету FIN (не сообщая об этом серверу; пакет RST отсутствует до получения данных) работает, потому что почти все клиенты HTTP никогда не вызывают shutdown(sockfd, SHUT_WR) и только close() соединение, когда больше нет данных для чтения.

...