Чтение поврежденных данных из сокета TCP в Ubuntu Linux

Обратите внимание: я знаю о потоковой природе TCP-соединения, мой вопрос не связан с такими вещами. Речь идет скорее о подозрении на ошибку в реализации сокетов Linux.

Обновление: принимая во внимание комментарии, я немного обновил свой код, чтобы проверять возвращаемое значение Recv() не только на -1, но и на любое отрицательное значение. Это было на всякий случай. Результаты те же.

У меня есть очень простое TCP-клиент/серверное приложение, написанное на C. Полный код этого проекта доступен на github.

Клиентская сторона запускает несколько параллельных потоков, каждый из которых выполняет следующие действия:

  1. открытая розетка
  2. подключить этот сокет к серверу
  3. записать в сокет 16 байт предопределённого шаблона данных кусками случайной длины
  4. закрыть сокет
  5. повторите шаги с 1 по 4 N раз
static size_t send_ex(int fd, const uint8_t *buff, size_t len, bool by_frags)
{
    if ( by_frags )
    {
        size_t chunk_len, pos;
        size_t res;

        for ( pos = 0; pos < len;  )
        {
            chunk_len = (size_t) random();
            chunk_len %= (len - pos);
            chunk_len++;

            res = send(fd, (const char *) &buff[pos], chunk_len, 0);
            if ( res != chunk_len) {
                return (size_t) -1;
            }

            pos += chunk_len;
        }

        return len;
    }

    return send(fd, buff, len, 0);
}

static void *connection_task(void *arg) 
{   
    connection_ctx_t *ctx = (connection_ctx_t *) arg;
    uint32_t buff[4] = {0xAA55AA55, 0x12345678, 0x12345678, 0x12345678};
    int res, fd, i;

    for ( i = 0; i < count; i++ )
    {
        fd = socket(AF_INET, SOCK_STREAM, 0);
        if ( fd < 0 ) {
            fprintf(stderr, "Can't create socket!\n");
            break;
        }

        res = connect(fd, (struct sockaddr *) ctx->serveraddr, sizeof(struct sockaddr_in));
        if ( res < 0 ) {
            fprintf(stderr, "Connect failed!\n");                    
            close(fd);
            break;
        }

        res = send_ex(fd, (const char *) buff, sizeof(buff), frags);
        if ( res != sizeof(buff) ) {
            fprintf(stderr, "Send failed!\n");
            close(fd);
            break;
        }

        ctx->sent_packs++;

        res = close(fd);
        if ( res < 0 ) {
            fprintf(stderr, "CLI: Close Failed!!\n");
        }

        msleep(delay);
    }

    return NULL;
}

На стороне сервера запускается поток для каждого входящего соединения, который выполняет следующие действия:

  1. читать данные из подключенного сокета, пока не будут прочитаны все 16 байтов
  2. после чтения хотя бы первых 4 байтов проверяется, что эти байты соответствуют заданному шаблону.
typedef struct client_ctx_s {
    struct sockaddr_in addr;
    int fd;
} client_ctx_t;

void *client_task(void *arg) 
{
    client_ctx_t *client = (client_ctx_t *) arg;
    size_t free_space, pos;
    ssize_t chunk_len;
    uint32_t buff[4] = {0};
    int res;

    pos = 0;
    while ( pos != sizeof(buff) )
    {
        free_space = sizeof(buff) - pos;
        assert(pos < sizeof(buff));

        chunk_len = recv(client->fd, &((uint8_t *) buff)[pos], free_space, 0);
        if ( chunk_len <= 0 ) {
            if ( chunk_len < 0 ) {
                fprintf(stderr, "%s:%u: ERROR: recv failed (errno = %d; pos = %zu)!\n",
                        inet_ntoa(client->addr.sin_addr), 
                        ntohs(client->addr.sin_port),
                        errno, pos);
            }
            else if ( pos && pos < sizeof(buff) ) {
                fprintf(stderr, "%s:%u: ERROR: incomplete data block (pos = %zu)!\n",
                        inet_ntoa(client->addr.sin_addr),
                        ntohs(client->addr.sin_port),
                        pos);
            }
            goto out;
        }

        assert(chunk_len <= free_space);
        pos += chunk_len;

        if ( pos >= 4 && buff[0] != 0xAA55AA55) {
            fprintf(stderr, "%s:%u: ERROR: data corrupted (%08x)!\n", 
                    inet_ntoa(client->addr.sin_addr), 
                    ntohs(client->addr.sin_port),
                    buff[0]);
        }
    }

    fprintf(stdout, "%s:%u: %08x %08x %08x %08x\n",
            inet_ntoa(client->addr.sin_addr),
            ntohs(client->addr.sin_port),
            buff[0], buff[1], buff[2], buff[3]);

out:
    debug("Connection closed\n");
    res = close(client->fd);
    assert(res == 0);
    free(client);
    return NULL;
}

Проблемы, которые возникали, когда клиент запускал тысячу потоков отправки и каждый из них повторял соединение-отправку-отключение сто раз (./client -t 1000 -c 100 -d 0 -f):

  1. Потеря первых байтов отправленного шаблона.
  2. Общий размер данных, прочитанных из сокета, соответственно меньше 16 байт.

image1

Такое поведение повторяется как на локальном хосте, так и при реальном сетевом подключении.

Изучение TCP-потока поврежденных данных с помощью Wireshark показывает, что:

  1. На стороне клиента проблем нет.
  2. Поврежденные данные соответствуют данным, которые передаются вместе с повторно переданными сегментами данных.

image2

Я не могу поверить, что эта проблема связана с реализацией TCP/IP в Linux. Может ли кто-нибудь объяснить, что не так с моим кодом?

Добро пожаловать в Stack Overflow! Пожалуйста, публикуйте код, данные и результаты в виде текста, а не скриншотов (как форматировать код в сообщениях ). Почему мне не следует загружать изображения кода/данных/ошибок? idownvotedbecau.se/imageofcode

Barmar 30.07.2024 18:20

Вы не проверяете наличие ошибки в recv(). Он вернет отрицательный результат chunk_len. Правильный тип — ssize_t, а не size_t, поэтому он может содержать отрицательные числа.

Barmar 30.07.2024 18:28

Сокет на сервере находится в неблокирующем режиме? Тогда recv() может вернуть EWOULDBLOCK ошибку.

Barmar 30.07.2024 18:29

@Barmar Recv() может возвращать только -1 в случае ошибки, и я это проверяю. EWOULDBLOCK возможно значение переменной errno, а не возвращаемое значение функции

legden 30.07.2024 18:45

Я знаю это, я использовал неофициальный язык. Но я пропустил ту часть вашего кода, которая проверяет наличие ошибок.

Barmar 01.08.2024 20:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
5
223
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

У меня такое же поведение (если клиент запускается с ключом -f [--fragments]) с реализацией сервера python3 и исходным клиентом в C. И только начало последовательности (1-й фрагмент?) всегда теряется.

#!/usr/bin/env python3

import threading
import socketserver

assert "__main__" == __name__

_mutex = threading.Lock()
_expected = "55aa55aa785634127856341278563412"

class _Handler(socketserver.BaseRequestHandler):
    def handle(self):
        _data = list()
        while True:
            _chunk = self.request.recv(1024)
            if not _chunk: break
            _data.append(_chunk)
        _data = bytes().join(_data).hex()
        if _expected == _data: return
        if _expected.endswith(_data): _case = "head case"
        else: _case = "other case"
        with _mutex: print(f"{_case}: {_data}", flush = True)

class _Server(socketserver.ThreadingMixIn, socketserver.TCPServer): pass

with _Server(("localhost", 5050), _Handler) as _server:
    _server.allow_reuse_address = True
    _server.serve_forever()

Короче говоря: я думаю, что SYN-cookies являются корнем проблемы.

Я не знаю, является ли полученное поведение («сломанный» первый вызов recv после accept) ошибкой ядра. Насколько я понимаю, функция SYN-cookies позволяет «принять» клиента так, чтобы он ничего не заметил, но это может создать проблемы для серверного приложения. Однако у меня нет однозначного ответа на вопрос, почему такое поведение включено в политику по умолчанию. Ваш клиент похож на сетевого злоумышленника =).

Возможно полезные ссылки:


Мне показалось довольно странным, что исчезает только начало последовательности... Далее я проверил dmesg и увидел это:

TCP: request_sock_TCP: Possible SYN flooding on port 127.0.0.1:5050. Sending cookies.

Далее отключаю его (насколько я знаю, в продакшене это не рекомендуется):

sudo sysctl net.ipv4.tcp_syncookies=0

После этого «повреждение данных» исчезло.


Следующее изменение в server.c:114 также исправляет это:

--- a/server.c
+++ b/server.c
-    res = listen(listenfd, 5);
+    res = listen(listenfd, 8192);

https://man7.org/linux/man-pages/man2/listen.2.html


Кроме того, обновление логики client с использованием функции MSG_MORE (только для Linux, https://man7.org/linux/man-pages/man2/sendto.2.html) решает проблему, поскольку снижает нагрузку, позволяя серверу «перехватывать» начало данных. Что наводит на странные мысли о том, что в этом механизме защиты операционной системы есть ошибка. Я не понимаю, почему в случае перегрузки сервера поведение по умолчанию не «отклонять запросы на подключение до тех пор, пока они не будут готовы».

Файлы cookie SYN должны быть прозрачными для приложения. Есть ли в реализации ошибка, из-за которой такие сегменты теряются?

Barmar 01.08.2024 20:27

Насколько я понимаю, они должны быть "прозрачными" для client. В этом случае логика сервера выходит из строя, поскольку он не может принять заголовок первого клиентского сообщения через системный вызов recv.

p5-vbnekit 01.08.2024 20:38

Другим обходным решением было бы добавить задержку в клиенте, чтобы это не выглядело как атака Syn Flood?

Barmar 01.08.2024 20:44

да, он порождает 1 тыс. потоков и выполняет «подключение, 16B сообщений, отключение» 100 раз для каждого с нулевой задержкой: ./client -t 1000 -c 100 -d 0 -f.

p5-vbnekit 01.08.2024 20:53

Да, после отключения tcp_syncookies проблема исчезла. На данный момент в операционной системе он выглядит таким же большим.

legden 02.08.2024 15:27
Ответ принят как подходящий

на первый взгляд здесь аналогичная проблема: https://wpbolt.com/syn-cookies-ate-my-dog-breaking-tcp-on-linux/

но в нашем случае в Wireshark см. подтверждение всех пакетов данных. это все еще похоже на ошибку ядра.

Чтобы воспроизвести эту ошибку, не обязательно открывать большое количество TCP-соединений. 10 достаточно.
Схематически это можно воспроизвести следующим образом:

запустить сервер

...
listenfd = socket(...  
res = bind(listenfd, ...  
res = listen(listenfd, 1); !!! backlog set 1  
wait user key press (need wait add socket to backlog queue)

запустить клиент
запустите 10 потоков с:

fd = socket(... 
z = setsockopt(fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one)); 
connect(fd ...  
for(int i=0;i<28;i++)  
    send(fd, &buff[i], 1, 0);  
recv()  

9 TCP-потоков попадают в очередь на стороне сервера и начинают повторную отправку SYN с увеличивающимися интервалами.

на стороне сервера нажмите Enter, чтобы разблокировать и

while(1)
  select([listenfd, socketN])
  listenfd: new connection
     accept(...) 
     add to socketN
  socketN: new data
     recv()

В результате первые байты данных в нескольких TCP-соединениях будут потеряны. Такое поведение наблюдается в Ubuntu 24.04 с ядром 6.10.2.

Ух ты. Спасибо за ссылку. Для меня это выглядит как точно такая же проблема. Таким образом, благодаря реализации файлов cookie SYN в Linux вы действительно можете читать поврежденные данные из TCP-соединения. Я удивляюсь, как это возможно, что эта проблема не решается уже много лет. Для меня это полная катастрофа, что реализация сетевого стека Linux, обычно считающаяся пуленепробиваемой, имеет такого рода ошибку.

legden 02.08.2024 17:49

Воспроизводится с небольшим количеством TCP-соединений на debian trixie amd64 с python3.

#!/usr/bin/env python3

import sys
import time
import socket
import threading
import traceback
import concurrent.futures

assert "__main__" == __name__

_delay = +5.0e+0  # instead press any key
_streams = 16  # or `10` like in @Drepin7 answer
_host, _port = "localhost", 5050

_expected = bytes().join((
    bytes.fromhex("55aa") * 2,
    bytes.fromhex("785634127856341278563412") * 32
))


def _request(key):
    try:
        _data = _expected
        with socket.socket(
            socket.AF_INET, socket.SOCK_STREAM
        ) as _socket:
            # `TCP_NODELAY` not required for reproduce
            # _socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
            _socket.connect((_host, _port))
            _address = _socket.getsockname()
            _address = _address[1]  # bound port
            key = f"{key}#{_address}"
            print(f"{key} connected", file = sys.stderr, flush = True)
            while _data:
                _socket.sendall(_data[:2])
                _data = _data[2:]

    except BaseException:
        print(
            traceback.format_exc().strip(),
            file = sys.stderr, flush = True
        )
        raise

    finally: return key


def _server():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as _socket:
        _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        _socket.bind((_host, _port))
        _socket.listen(1)  # backlog = 1
        time.sleep(_delay)

        print(
            f"server started after delay = {_delay}",
            file = sys.stderr, flush = True
        )

        while True:
            _data = bytes()

            _connection, _address = _socket.accept()
            _address = _address[1]  # remote port
            print(f"{_address} accepted", file = sys.stderr, flush = True)

            try:
                with _connection:
                    while len(_expected) > len(_data):
                        _chunk = _connection.recv(len(_expected) - len(_data))
                        if not _chunk: break
                        _data += _chunk
                if _expected == _data: return
                if _expected.endswith(_data): _case = "head case"
                else: _case = "other case"
                print(
                    f"{_case} [{_address}]: {_data.hex()}",
                    file = sys.stdout, flush = True
                )

            except BaseException:
                print(
                    traceback.format_exc().strip(),
                    file = sys.stderr, flush = True
                )
                raise

            finally: continue


threading.Thread(target = _server, daemon = True).start()

with concurrent.futures.ProcessPoolExecutor(max_workers = _streams) as _pool:
    for _client in _pool.map(_request, range(_streams)): print(
        f"request finished: {_client}", flush = True, file = sys.stderr
    )

Те же проблемы с потерей головы запроса на стороне сервера и

TCP: request_sock_TCP: Possible SYN flooding on port 127.0.0.1:5050. Sending cookies.

Конечно, после sudo sysctl net.ipv4.tcp_syncookies=0 оно исчезло.

На данный момент это все больше похоже на ошибку ядра.

Другие вопросы по теме

Как настроить докер и запустить докер для golang+redis+postgres и прослушать порт 8089
Как выглядит индексный файл, созданный Linux «ar», в файле .a?
Как одно общее загруженное ядро ​​процессора может повлиять на общую загрузку процессора openmp?
Как одно общее загруженное ядро ​​процессора может повлиять на общую загрузку процессора openmp?
SAP NetWeaver. Невозможно получить дескриптор iamodell.so: libnsl.so.1: невозможно открыть файл общего объекта: такого файла или каталога нет
Я хочу использовать mod_timer для таймера на 10 миллисекунд, но результат всегда 20 миллисекунд
Команда «sed» возвращает «Нет такого файла или каталога» при запуске с помощью доступного сценария?
Ок. что означает символ «@» перед регулярным выражением
Почему адреса файлов, хранящихся на диске (vbox vm), различаются каждый раз, когда я просматриваю их?
Как я могу вызвать доверенную подпись Azure из ОС Linux?

Похожие вопросы