LINUX.ORG.RU

Но ведь это дыра для DDoS!

 , , ,


0

3

Посмотрел тут недавно лайв кодинг одного прикольного чувака из Сибири, который стримит на Твиче и позже выкладывает на Ютубе.

Ниже ссылки на этот стрим на обоих ресурсах:

Этот стрим посвящён проблеме закрытия сокета до того, как все посланные данные на самом деле ушли к получателю. Суть проблемы в том, что функция write (как и неупомянутая в стриме send) пишет данные в ядро, а дошли ли эти данные до получателя уже полностью она не знает, хотя сокеты в Linux блокирующие по-умолчанию. Если сразу после write() или send() вызвать close(sock), у принимающей стороны случится ECONNRESET (Connection reset by peer). Случиться это может в случае, если и принимающая сторона решила что-то нам послать в самом начале, а мы это не прочитали. Это важное условие, потому что в противном случае всё работает верно и без ухищрений.

Первую половину ролика сибиряк разбирает одну старую статью на эту тему, пробует повторить и исправить по статье данную проблему в своём тестовом коде. Вторая половина ролика посвящена исправлению кода в его старых проектах и в контексте этой темы она не так интересна.

В статье предлагаются следующие шаги для решения данной проблемы:

Во-первых предлагается вызывать shutdown(sock, SHUT_WR) на отправляющей стороне сразу после последнего write. Но этого недостаточно, поэтому так же предлагается выставить опцию сокета SO_LINGER которая уже сама по себе должна решить проблему, но почему-то её не решает и это выглядит так, как будто она просто не работает. Есть и Linux-only вещи, такие как SIOCOUTQ ioctl(). Но универсальным решением предлагается просто бесконечный цикл, в котором вычитываются все данные и лишь затем можно закрывать сокет.

Я проверил на своём компьютере большинство из выше перечисленного и этот бесконечный цикл действительно работает, а SO_LINGER с и без shutdown(sock, SHUT_WR) и даже с shutdown(sock, SHUT_RDWR) действительно не работает. Но ведь решение с бесконечным чтением в цикле - это дыра уровня DDoS, ведь если ко мне будет идти бесконечный поток данных, сокет я никогда не закрою.

Ещё одно наблюдение. Если перед закрытием сокета вызвать shutdown(sock, SHUT_WR) и короткий sleep, это так же решает проблему. Неужели в UNIX/Linux/POSIX нет нормального решения данной проблемы?



Последнее исправление: zg (всего исправлений: 2)
Ответ на: комментарий от zg

Именно так. Осталось лишь понять как или почему непрочитанные данные мешают ядру отправить другие данные, уже находящиеся в буфере, в обратную сторону.

Сокет закрыт, программа закрыта - кто отвественнен за буфер? Решил перед закрытием сокета дождаться пока все данные из буфера ядра не будут отправлены и не будут получены все ACK:

#include <linux/tcp.h>

    struct tcp_info tcpInfo;
    socklen_t tcp_info_length = sizeof(struct tcp_info);
    do {
        int res = getsockopt(sock, IPPROTO_TCP, TCP_INFO, &tcpInfo, &tcp_info_length);
        if (res != 0) {
            perror("getsockopt");
            break;
        }
    } while (tcpInfo.tcpi_notsent_bytes != 0 || tcpInfo.tcpi_unacked != 0);

    printf("notsent %d. unacked %d.\n", tcpInfo.tcpi_notsent_bytes, tcpInfo.tcpi_unacked);

И тут есть две ситуации:

  • с shutdown(sock, SHUT_WR) все отлично доходит.
  • без shutdown почему-то не доходит последний байт. Даже если я пару секунд не закрываю сокет, логично что принимающая сторона уже должна была прочитать данные но выходит что теряется один байт. Вот этот момент я что-то не могу понять.
V1KT0P ★★
()
Ответ на: комментарий от V1KT0P

Сокет закрыт, программа закрыта - кто отвественнен за буфер?

SO_LINGER должен блокировать закрытие сокета, пока весь буфер не будет отправлен. Но почему-то этого не делает.

zg
() автор топика
Ответ на: комментарий от zg

Откуда ты это взял?

Из wireshark я увидел что shutdown добавляет обмен FIN, а RST в обоих случаях будет. Без FIN похоже нет гарантии доставки данных даже если был получен ACK. Другое дело почему закрытие сокета автоматически не вызывает shutdown для TCP?

V1KT0P ★★
()
Ответ на: комментарий от zg

shutdown(sock, SHUT_WR) посылает FIN, а не RST. RST посылает close(sock), если буфер не пустой.

Я именно это и написал =). Просто для TCP посылка FIN это правильный вариант, поэтому удивительно что по умолчанию этого не делается.

V1KT0P ★★
()

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

Я проверил, действительно если вычитать все данные из входящего буфера то SO_LINGER отрабатывает правильно, и возможную причину я нашел в исходниках ядра (net/ipv4/tcp.c):

	} else if (data_was_unread) {
		/* Unread data was tossed, zap the connection. */
		NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
		tcp_set_state(sk, TCP_CLOSE);
		tcp_send_active_reset(sk, sk->sk_allocation);
	} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
		/* Check zero linger _after_ checking for unread data. */
		sk->sk_prot->disconnect(sk, 0);
		NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);

Проверка ветки SO_LINGER происходит только если нет непрочитанных данных иначе срабатывает другая ветка.

Итак для того чтоб SO_LINGER сработал правильно я так понял надо выполнить все условия:

  • Установить саму опцию SO_LINGER.
  • Выполнить shutdown с SHUT_RD, чтоб все принимаемые данные отбрасывались.
  • Прочитать данные из входящего буфера чтоб он был пуст. После этого по идеи закрытие сокета будет только после передачи всех данных.
V1KT0P ★★
()
Ответ на: комментарий от zg

“A host MAY implement a ‘half-duplex’ TCP close sequence, so that an application that has called CLOSE cannot continue to read data from the connection. If such a host issues a CLOSE call while received data is still pending in TCP, or if new data is received after CLOSE is called, its TCP SHOULD send a RST to show that data was lost.”

https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable

В твоей же статейке все написано. Почему, почему.

anonymous
()
Ответ на: комментарий от V1KT0P

Выполнить shutdown с SHUT_RD, чтоб все принимаемые данные отбрасывались.

Прочитать данные из входящего буфера чтоб он был пуст. После этого по идеи закрытие сокета будет только после передачи всех данных.

именно это я ему ужо писал.

alysnix ★★★
()
Ответ на: комментарий от V1KT0P

Собственно Linux клал просто на SHUT_RD:

void tcp_shutdown(struct sock *sk, int how)
{
	/*	We need to grab some memory, and put together a FIN,
	 *	and then put it into the queue to be sent.
	 *		Tim MacKenzie(tym@dibbler.cs.monash.edu.au) 4 Dec '92.
	 */
	if (!(how & SEND_SHUTDOWN))
		return;

	/* If we've already sent a FIN, or it's a closed state, skip this. */
	if ((1 << sk->sk_state) &
	    (TCPF_ESTABLISHED | TCPF_SYN_SENT |
	     TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
		/* Clear out any half completed packets.  FIN if needed. */
		if (tcp_close_state(sk))
			tcp_send_fin(sk);
	}
}
V1KT0P ★★
()
Ответ на: комментарий от zg

TCP SHOULD send a RST to show that data was lost

Как этот «‘half-duplex’ TCP close sequence» отменить

На все вопросы «Как» тебе уже не раз ответили - правильно использовать свой протокол поверх TCP. Linger этот ваш - бесполезная вещь. Хотя бы потому, что не понятно сколько секунд ставить таймаут - это какое-то гадание на кофейной гуще - сколько данные реально будут передаваться. К тому же явно любая ОС будет игнорировать слишком высокие значения int l_linger. Максимально в int 68 лет влезает.

anonymous
()
Ответ на: комментарий от anonymous

это какое-то гадание на кофейной гуще - сколько данные реально будут передаваться

А сколько данные из TCP буфера ядра могут реально передаваться? По-твоему этот буфер большой?

zg
() автор топика
Ответ на: комментарий от zg

А сколько данные из TCP буфера ядра могут реально передаваться? По-твоему этот буфер большой?

Раз это до сих пор никто не исправил, значит оно не особо и нужно. Если очень сильно хочется чего-то странного то есть tcpInfo.tcpi_notsent_bytes и tcpInfo.tcpi_unacked, можно в цикле с sleep ждать пока действительно не отправятся. В остальных ситуациях протокол поверх TCP предусматривает либо подтверждение получения, либо сам получатель закрывает сокет после получения данных, либо же сервер читает всё что посылает получатель а получатель не спамит мусорными данными в сторону отправителя(а если спамит то ССЗБ). Вот тогда все работает без проблем.

V1KT0P ★★
()
Ответ на: комментарий от zg

Этот буфер настраиваемый, и неизвестный процессу. И скорость передачи через сеть тоже неизвестна, и drop rate. И как эти параметры изменятся в следующие 5 мс тоже неизвестно. По сути единственный use case применения linger - c таймаутом 0. Когда дальнейшая передача данных резко потеряла смысл - сказать ОС побыстрее освободить ресурсы после close.

anonymous
()
Ответ на: комментарий от V1KT0P

без shutdown почему-то не доходит последний байт

там ошибка при приеме: при ошибке единица вычитается, д.б. как-то так:

    for (;;) {
        res = read(client, buffer, sizeof buffer);
        if (res == 0)  // EOF
            break;

        if (res < 0)  {
            perror("read");
            printf("error number: %d\n", errno);
            break;
        }

        bytesRead += res;
        printf("%ld bytes received. %ld total.\n", res, bytesRead);
    }

    close(client);
zudwa
()
Последнее исправление: zudwa (всего исправлений: 1)