Обратите внимание: я знаю о потоковой природе TCP-соединения, мой вопрос не связан с такими вещами. Речь идет скорее о подозрении на ошибку в реализации сокетов Linux.
Обновление: принимая во внимание комментарии, я немного обновил свой код, чтобы проверять возвращаемое значение Recv() не только на -1, но и на любое отрицательное значение. Это было на всякий случай. Результаты те же.
У меня есть очень простое TCP-клиент/серверное приложение, написанное на C. Полный код этого проекта доступен на github.
Клиентская сторона запускает несколько параллельных потоков, каждый из которых выполняет следующие действия:
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;
}
На стороне сервера запускается поток для каждого входящего соединения, который выполняет следующие действия:
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
):
Такое поведение повторяется как на локальном хосте, так и при реальном сетевом подключении.
Изучение TCP-потока поврежденных данных с помощью Wireshark показывает, что:
Я не могу поверить, что эта проблема связана с реализацией TCP/IP в Linux. Может ли кто-нибудь объяснить, что не так с моим кодом?
Вы не проверяете наличие ошибки в recv()
. Он вернет отрицательный результат chunk_len
. Правильный тип — ssize_t
, а не size_t
, поэтому он может содержать отрицательные числа.
Сокет на сервере находится в неблокирующем режиме? Тогда recv()
может вернуть EWOULDBLOCK
ошибку.
@Barmar Recv() может возвращать только -1 в случае ошибки, и я это проверяю. EWOULDBLOCK возможно значение переменной errno, а не возвращаемое значение функции
Я знаю это, я использовал неофициальный язык. Но я пропустил ту часть вашего кода, которая проверяет наличие ошибок.
У меня такое же поведение (если клиент запускается с ключом -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
позволяет «принять» клиента так, чтобы он ничего не заметил, но это может создать проблемы для серверного приложения. Однако у меня нет однозначного ответа на вопрос, почему такое поведение включено в политику по умолчанию. Ваш клиент похож на сетевого злоумышленника =).
Возможно полезные ссылки:
tcp_syncookies
)Мне показалось довольно странным, что исчезает только начало последовательности... Далее я проверил dmesg
и увидел это:
TCP: request_sock_TCP: Possible SYN flooding on port 127.0.0.1:5050. Sending cookies.
Далее отключаю его (насколько я знаю, в продакшене это не рекомендуется):
sudo sysctl net.ipv4.tcp_syncookies=0
После этого «повреждение данных» исчезло.
--- 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 должны быть прозрачными для приложения. Есть ли в реализации ошибка, из-за которой такие сегменты теряются?
Насколько я понимаю, они должны быть "прозрачными" для client
. В этом случае логика сервера выходит из строя, поскольку он не может принять заголовок первого клиентского сообщения через системный вызов recv
.
Другим обходным решением было бы добавить задержку в клиенте, чтобы это не выглядело как атака Syn Flood?
да, он порождает 1 тыс. потоков и выполняет «подключение, 16B сообщений, отключение» 100 раз для каждого с нулевой задержкой: ./client -t 1000 -c 100 -d 0 -f
.
Да, после отключения tcp_syncookies проблема исчезла. На данный момент в операционной системе он выглядит таким же большим.
на первый взгляд здесь аналогичная проблема: 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, обычно считающаяся пуленепробиваемой, имеет такого рода ошибку.
Воспроизводится с небольшим количеством 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
оно исчезло.
На данный момент это все больше похоже на ошибку ядра.
Добро пожаловать в Stack Overflow! Пожалуйста, публикуйте код, данные и результаты в виде текста, а не скриншотов (как форматировать код в сообщениях ). Почему мне не следует загружать изображения кода/данных/ошибок? idownvotedbecau.se/imageofcode