Search code examples
c++linuxsocketstcpnetwork-programming

Client sends RST to recieved packets after shutdown(SHUT_WR)


After closing the writing direction of a connection of a socket using shutdown() all subsequently received data causes an RST packet to be sent and read() returns a size of 0 indicating EOF for the reading direction. Why is this the case, even though the reading direction wasn't closed?

Using wireshark I checked the sent packets:

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

After the [FIN, ACK] is sent, all received data is responded to with RSTs. It seems like the local side is thinking the connection is fully closed even though a FIN was sent (indicating the end of written data) instead of an RST (indicating the end of the connection). The remote side expects to be able to send data, but can't. Monitoring the socket status with netstat reveals that the connection is instantly fully closed after shutdown(sockfd, SHUT_WR) is called.

Here's a MWE in C++. It assumes all non-relevant functions succeed and aborts otherwise. The return codes of all functions are checked for errors to make sure they aren't at fault for the results.

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

I would have expected the read() call to return a non-zero size and the socket to remain open for reading after closing the writing end with shutdown(sockfd, SHUT_WR). The expected output of the MWE would be:

740 bytes have been read

(or similar, but the number of read bytes should be bigger than 512 bytes). The actual output was:

0 bytes have been read

Solution

  • The issue is a (misbehaving) transparent HTTP proxy. Avast's Web Shield intercepts packets before they are sent or received and inspects them for Malware. Avast uses seperate internal sockets for this. As soon as the first FIN is sent, Avast (incorrectly) closes its internal socket. This is communicated to the client's socket, which is why it doesn't show up in netstat afterwards. The server's data is then rejected, because Avast's socket, which is responsible for handling incoming data is no longer open. Avast's strategy of closing the connection on the first FIN packet (without telling the server about it; There is no RST packet until data is received) works, because almost all HTTP clients never call shutdown(sockfd, SHUT_WR) and only close() the connection, when there is no more data to be read.