Анализ сегмента сброса TCP и распространенных сценариев RST

TCP/IP

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

производитьRSTТри условия:

  1. место назначения для портаSYNприбыл, но сервер не прослушивает порт;
  2. TCP хочет отменить существующее соединение;
  3. TCP получил сегмент соединения, которого вообще не существует.

Следующие сценарии будут производитьRST, чтобы проиллюстрировать назначение сегмента сброса.

1. Запросы на подключение для несуществующих портов

Клиент инициирует запрос на подключение к порту на сервереSYN, но порт не существует на хосте целевого сервера и в это время отвечает клиентуRST, прервите запрос на подключение.

Дальнейший анализ осуществляется с помощью программы и захвата пакетов. Исходный код программы выглядит следующим образом:

use std::io::prelude::*;
use std::net::TcpStream;
use std::thread;

fn main() {
    let mut stream = TcpStream::connect("192.168.2.229:33333").unwrap();
    let n = stream.write(&[1,2,3,4,5,6,7,8,9]).unwrap();
    println!("send {} bytes to remote node, waiting for end.", n);

    loop{
        thread::sleep_ms(1000);
    }
}

Вышеуказанная программа отправляет хосту назначения192.168.2.229Инициировать TCP-соединение, но узел назначения не запускает порт, как33333служба мониторинга. Таким образом, когда локальный хост инициирует TCP-соединение с целевым хостом, он получит сообщение от целевого хоста.RSTи отключите. (конечно не все ответятRST, некоторые хосты могут не отвечать). Захват пакета выглядит следующим образом:

Локальный хост отправляет TCP-соединение на хост назначенияSYN:

在这里插入图片描述

Хост назначения отвечает локальному хостуACK、RST:

在这里插入图片描述

2. Разорвать соединение

Обычный способ разрыва соединения — это отправка одной сторонойFIN. Этот метод также известен как приказное освобождение. так какFINОн отправляется после того, как все ранее поставленные в очередь данные были отправлены, и потери данных обычно не происходит. Однако в любой момент мы можем отправить сегмент сброса поRSTальтернативаFINчтобы разорвать соединение. Этот метод также называется прекращенным выпуском.

Завершение соединения предоставляет приложению две основные характеристики:

  • Любые данные в очереди будут отброшены, и немедленно будет отправлен сегмент сброса;
  • Получатель сегмента сброса укажет, что другой конец связи прерван, а не корректно завершен. API ДОЛЖЕН предоставлять способ реализации поведения завершения, описанного выше, вместо обычной операции завершения работы.

API сокета доступен через параметры сокетаSO_LINGERЗначение установлено на0для достижения вышеуказанных функций.

/* Structure used to manipulate the SO_LINGER option.  */
struct linger
  {
    int l_onoff;		/* Nonzero to linger on close.  */
    int l_linger;		/* Time to linger.  */
  };

SO_LINGERЗначения различных значений следующие:

  1. l_onoffза0,l_lingerЗначение игнорируется, значение по умолчанию ядра,close()Вызов немедленно возвращается вызывающей стороне, и модуль TCP отвечает за попытку отправить оставшиеся данные буфера.
  2. l_onoffненулевое значение,l_lingerза0, соединение разрывается немедленно, TCP отбрасывает данные, оставшиеся в буфере отправки, и отправляетRSTдруг другу вместо отправкиFIN, что позволяет избежатьTIME_WAITстатус, контрагентread()получитConnection reset by peerошибка.
  3. l_onoffненулевое значение,l_lingerБольше нуля: еслиl_lingerДиапазон времени, если TCP-модуль успешно отправляет оставшиеся данные буфера, он будет закрыт в обычном режиме, если истечет время ожидания, он будет отправлен другой стороне.RST, отбросить данные, оставшиеся в буфере отправки.

Приложение Код 1 между клиентским кодом и серверным кодом выглядит следующим образом:

/* echo server with poll */
#include <poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>

#define OPEN_MAX 1024
#define LISTEN_PORT 33333
#define MAX_BUF 1024

int set_linger(int sock, int l_onoff, int l_linger);
int handle_conn(struct pollfd *nfds, char* buf);
void run();

int main(int _argc, char* _argv[]) {
    run();

    return 0;
}

void run() {
    // bind socket
    char str[INET_ADDRSTRLEN];
    struct sockaddr_in seraddr, cliaddr;
    socklen_t cliaddr_len = sizeof(cliaddr);
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_port = htons(LISTEN_PORT);

    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (-1 == bind(listen_sock, (struct sockaddr*)&seraddr, sizeof(seraddr))) {
        perror("bind server addr failure.");
        exit(EXIT_FAILURE);
    }
    listen(listen_sock, 5);

    int ret, i;
    struct pollfd nfds[OPEN_MAX];
    for (i=0;i<OPEN_MAX;++i){
        nfds[i].fd = -1;
    }

    nfds[0].fd = listen_sock;
    nfds[0].events = POLLIN;

    char* buf = (char*)malloc(MAX_BUF);   
    while (1) {
        ret = poll(nfds, OPEN_MAX, NULL);
        if (-1 == ret) {
            perror("poll failure.");
            exit(EXIT_FAILURE);
        }

        /* An event on one of the fds has occurred. */
        if (nfds[0].revents & POLLIN) {
            int conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &cliaddr_len);
            if (-1 == conn_sock) {
                perror("accept failure.");
                exit(EXIT_FAILURE);
            }
            printf("accept from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

            set_linger(conn_sock, 1, 0);    //设置SO_LINGER option值为0
            for (int k=0;k<OPEN_MAX;++k){
                if (nfds[k].fd < 0){
                    nfds[k].fd = conn_sock;
                    nfds[k].events = POLLIN;
                    break;
                }
                if (k == OPEN_MAX-1){
                    perror("too many clients, nfds size is not enough.");
                    exit(EXIT_FAILURE);
                }
            }
        }

        handle_conn(nfds, buf);
    }

    close(listen_sock);
}

int handle_conn(struct pollfd *nfds, char* buf) {
    int n = 0;
    for (int i=1;i<OPEN_MAX;++i) {
        if (nfds[i].fd<0) {
            continue;
        }

        if (nfds[i].revents & POLLIN) {
            bzero(buf, MAX_BUF);
            n = read(nfds[i].fd, buf, MAX_BUF);
            if (0 == n) {
                close(nfds[i].fd);
                nfds[i].fd = -1;
                continue;
            } 
            if (n>0){
                printf("recv from client: %s\n", buf);
                nfds[i].events = POLLIN;

                close(nfds[i].fd);  //接收数据后就主动关闭连接,用于RST测试          
            } else {
                perror("read failure.");
                exit(EXIT_FAILURE);
            }
        } else if (nfds[i].revents & POLLOUT) {
            printf("write data to client: %s\n", buf);
            write(nfds[i].fd, buf, sizeof(buf));
            bzero(buf, MAX_BUF);          

            nfds[i].events = POLLIN;
        }
    }

    return 0;
}

int set_linger(int sock, int l_onoff, int l_linger) {
    struct linger so_linger;
    so_linger.l_onoff = l_onoff;
    so_linger.l_linger = l_linger;
    int r = setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));

    return r;
}

Результаты захвата пакетов следующие:

在这里插入图片描述
Сначала три рукопожатия, затем клиент отправляет сервису 5 байт данных, а после получения 5 байт данных сервер отправляет клиенту 5 байт данных.ACKПосле этого это означает, что вы хотите разорвать соединение.SO_LINGERЗначение параметра0,close(), отправить непосредственно другой сторонеRSTвместо обычной отправкиFIN, соединение немедленно разрывается, и не будетTIME_WAITсостояние, TCP отбрасывает данные, оставшиеся в буфере отправки, другая сторонаread()получитConnection reset by peerошибка.

3. Полуоткрытое соединение

Соединение TCP считается полуоткрытым, если один конец связи закрывает или завершает соединение, не сообщая об этом другому концу.

Например, если хост-сервер выключен, а затем перезапущен (сетевой кабель может быть отключен перед отключением питания, а затем снова подключен после перезапуска), клиент в это время представляет собой полуоткрытое соединение. Когда клиент снова отправляет данные на сервер, сервер ничего не знает о соединении и ответит сегментом сброса.RSTПосле этого отключите соединение.

Или, если программа включила механизм проверки активности TCP, когда она обнаруживает, что другой хост недоступен, она отправляетRSTОтключить. Для получения дополнительной информации, пожалуйста, обратитесь к другому моему сообщению в блогеМеханизм поддержания активности TCP.

Если соединение TCP не отправляет или не получает данные в течение длительного времени, TCP отправит пакет обнаружения проверки активности, чтобы поддерживать соединение или определить, существует ли соединение.

在这里插入图片描述
Видно, что если он думает, что соединение не существует, он отправитRSTОтключить.

В-четвертых, закройте соединение заранее

Данные, полученные приложением TCP, являются данными TCP, полученными от операционной системы.Если данные достигают операционной системы, но данные приложения не хотят продолжать получать данные, в это времяRSTОтключить.

Код сервера:

/* echo server with poll */
#include <poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h>

#define OPEN_MAX 1024
#define LISTEN_PORT 33333
#define MAX_BUF 1024

#define RST_TEST 1

int handle_conn(struct pollfd *nfds, char* buf);
void run();

int main(int _argc, char* _argv[]) {
    run();

    return 0;
}

void run() {
    // bind socket
    char str[INET_ADDRSTRLEN];
    struct sockaddr_in seraddr, cliaddr;
    socklen_t cliaddr_len = sizeof(cliaddr);
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_port = htons(LISTEN_PORT);

    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (-1 == bind(listen_sock, (struct sockaddr*)&seraddr, sizeof(seraddr))) {
        perror("bind server addr failure.");
        exit(EXIT_FAILURE);
    }
    listen(listen_sock, 5);

    int ret, i;
    struct pollfd nfds[OPEN_MAX];
    for (i=0;i<OPEN_MAX;++i){
        nfds[i].fd = -1;
    }

    nfds[0].fd = listen_sock;
    nfds[0].events = POLLIN;

    char* buf = (char*)malloc(MAX_BUF);   
    while (1) {
        ret = poll(nfds, OPEN_MAX, NULL);
        if (-1 == ret) {
            perror("poll failure.");
            exit(EXIT_FAILURE);
        }

        /* An event on one of the fds has occurred. */
        if (nfds[0].revents & POLLIN) {
            int conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &cliaddr_len);
            if (-1 == conn_sock) {
                perror("accept failure.");
                exit(EXIT_FAILURE);
            }
            printf("accept from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

            for (int k=0;k<OPEN_MAX;++k){
                if (nfds[k].fd < 0){
                    nfds[k].fd = conn_sock;
                    nfds[k].events = POLLIN;
                    break;
                }
                if (k == OPEN_MAX-1){
                    perror("too many clients, nfds size is not enough.");
                    exit(EXIT_FAILURE);
                }
            }
        }

        handle_conn(nfds, buf);
    }

    close(listen_sock);
}

int handle_conn(struct pollfd *nfds, char* buf) {
    int n = 0;
    for (int i=1;i<OPEN_MAX;++i) {
        if (nfds[i].fd<0) {
            continue;
        }

        if (nfds[i].revents & POLLIN) {
            bzero(buf, MAX_BUF);
#if RST_TEST == 0
            n = read(nfds[i].fd, buf, MAX_BUF);
#else
            n = read(nfds[i].fd, buf, 5);      //只接收部分数据就主动关闭连接,用于RST测试         
#endif
            if (0 == n) {
                close(nfds[i].fd);
                nfds[i].fd = -1;
                continue;
            } 
            if (n>0){
                printf("recv from client: %s\n", buf);
                nfds[i].events = POLLOUT;
#if RST_TEST != 0  
                close(nfds[i].fd);  //只接收部分数据就主动关闭连接,用于RST测试
#endif            
            } else {
                perror("read failure.");
                exit(EXIT_FAILURE);
            }
        } else if (nfds[i].revents & POLLOUT) {
            printf("write data to client: %s\n", buf);
            write(nfds[i].fd, buf, sizeof(buf));
            bzero(buf, MAX_BUF);          

            nfds[i].events = POLLIN;
        }
    }

    return 0;
}

После того, как клиент инициирует соединение и отправляет более 5 байт данных, потому что сервер получает только 5 байт данных и больше не получает данные (в это время операционная система сервера получила 10 байт данных, но верхний уровеньreadСистемный вызов получает только 5 байт), сервер отправляет клиентуRSTОтключить. Результаты захвата пакетов следующие:

在这里插入图片描述
Первые три рукопожатия, клиент отправляет 10 байт данных после рукопожатия, а сервер отвечает клиентуACKПосле получения данных отправитьRSTОтключить.

5. Получить данные по закрытому TCP-соединению

Если закрытое TCP-соединение снова получает данные, это явно ненормально.RSTОтключить.

Другой код сервера аналогичен предыдущему коду, его заменяет следующая функция

int handle_conn(struct pollfd *nfds, char* buf) {
    int n = 0;
    for (int i=1;i<OPEN_MAX;++i) {
        if (nfds[i].fd<0) {
            continue;
        }

        if (nfds[i].revents & POLLIN) {
            bzero(buf, MAX_BUF);
            n = read(nfds[i].fd, buf, MAX_BUF);
            if (0 == n) {
                close(nfds[i].fd);
                nfds[i].fd = -1;
                continue;
            } 
            if (n>0){
                printf("recv from client: %s\n", buf);
                nfds[i].events = POLLOUT;

                close(nfds[i].fd);  //接收数据后就主动关闭连接,用于RST测试          
            } else {
                perror("read failure.");
                exit(EXIT_FAILURE);
            }
        } else if (nfds[i].revents & POLLOUT) {
            printf("write data to client: %s\n", buf);
            write(nfds[i].fd, buf, sizeof(buf));
            bzero(buf, MAX_BUF);          

            nfds[i].events = POLLIN;
        }
    }

    return 0;
}

Код клиента такой же, как и предыдущий, отличаются только следующие функции, просто замените его:

void client_handle(int sock) {
    char sendbuf[MAXLEN], recvbuf[MAXLEN];
    bzero(sendbuf, MAXLEN);
    bzero(recvbuf, MAXLEN);
    int n = 0;

    while (1) {
        if (NULL == fgets(sendbuf, MAXLEN, stdin)) {
            break;
        }
        // 按`#`号退出
        if ('#' == sendbuf[0]) {
            break;
        }
        struct timeval start, end;
        gettimeofday(&start, NULL);
        write(sock, sendbuf, strlen(sendbuf));
        sleep(2);
        write(sock, sendbuf, strlen(sendbuf));		//这里是测试RST用的代码
        sleep(60);
        n = read(sock, recvbuf, MAXLEN);
        if (0 == n) {
            break;
        }
        write(STDOUT_FILENO, recvbuf, n);
        gettimeofday(&end, NULL);
        printf("time diff=%ld microseconds\n", ((end.tv_sec * 1000000 + end.tv_usec)- (start.tv_sec * 1000000 + start.tv_usec)));
    }

    close(sock);
}

Захват пакета выглядит следующим образом:

在这里插入图片描述
Сначала 3 рукопожатия, затем клиент отправляет 5 байт данных на сервер, и сервер получает ответ из 5 байтов данныхACK; затем отправить клиентуFIN, закрыть соединение, но в это время у клиента еще есть данные для отправки, и он не инициировал их на серверFIN, только дважды замахнулся в это время, после этого клиент отправил 5 байт данных на сервер, но в это время сервер вызвал соединениеclose()Закрыто, повторное получение данных о соединении в настоящее время является ненормальным, ответьтеRSTОтключить.

6. Приложение

клиентский код для тестирования

#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include <time.h>
#include <sys/time.h>
#include<stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define SERVER_PORT 33333
#define MAXLEN 65535

void client_handle(int sock);


int main(int argc, char* argv[]) {
    for (int i = 1; i < argc; ++i) {
        printf("input args %d: %s\n", i, argv[i]);
    }
    struct sockaddr_in seraddr;
    int server_port = SERVER_PORT;
    if (2 == argc) {
        server_port = atoi(argv[1]);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr);
    seraddr.sin_port = htons(server_port);

    if (-1 == connect(sock, (struct sockaddr *)&seraddr, sizeof(seraddr))) {
        perror("connect failure");
        exit(EXIT_FAILURE);
    }
    client_handle(sock);

    return 0;
}

void client_handle(int sock) {
    char sendbuf[MAXLEN], recvbuf[MAXLEN];
    bzero(sendbuf, MAXLEN);
    bzero(recvbuf, MAXLEN);
    int n = 0;

    while (1) {
        if (NULL == fgets(sendbuf, MAXLEN, stdin)) {
            break;
        }
        // 按`#`号退出
        if ('#' == sendbuf[0]) {
            break;
        }
        struct timeval start, end;
        gettimeofday(&start, NULL);
        write(sock, sendbuf, strlen(sendbuf));
        n = read(sock, recvbuf, MAXLEN);
        if (n < 0) {
            perror("read failure.");
            exit(EXIT_FAILURE);
        }
        if (0 == n) {
            break;
        }
        write(STDOUT_FILENO, recvbuf, n);
        gettimeofday(&end, NULL);
        printf("time diff=%ld microseconds\n", ((end.tv_sec * 1000000 + end.tv_usec)- (start.tv_sec * 1000000 + start.tv_usec)));
    }

    close(sock);
}

Справочная документация:
Параметр SO_LINGER для сокета Linux
So_linger и первый из Socket

Добро пожаловать в общедоступную учетную запись WeChat и размещайте технические статьи о компьютерных сетях, серверной разработке, блокчейне, распределенных приложениях, Rust, Linux и т. д.!