LINUX.ORG.RU

C++ SSL чудесатые чудеса

 , ,


1

3

Здравствуйте, коллеги!

Пишу сетевой сервер с шифрованием TLS (однопоточный через poll). Большей частью получаются, но наткнулся на очень неприятную вещь: Если клиент, внезапно, некорректно рвет соединение, то сервер крашится.

Т.е. при вызове SSL_read или SSL_write все крашится.

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

Это я отлавливаю просто читая из сокета через recv с установленным флагом MSG_PEEK, т.е. считать из буфера, но не удалять данные.

recv считывает 0 байт, что является закрытием соединения, а вот SSL_read натакаясь на пакет нулевой длинны крашит программу.

Разумеется, можно сделать обертку типа:

int ssl_recv(SSL * ssl, char * data, int data_size){
    int s = SSL_get_fd(ssl);
    if(recv(s, data, data_size, MSG_PEEK)){
         return SSL_read(ssl, data, data_size);
    }
    return -111;
}

При получении -111 от ssl_recv закрывать соединение на стороне сервера.

Но способ мне кажется несколько кривоватым.

Это же каждый раз, по большому счету, делать целых 2 ресива!

Есть способ узнать количество данных в сетевом буфере без его считывания?

Или, возможно, есть более прямой способ уберечь сервер от крашей при некорректном вылете клиента?

PS

if(SSL_get_state() == TLS_ST_OK){
    retrurn SSL_read(ssl, data, data_size);
}

Не подходит. Этот SSL_get_state() радостно рапортует TLS_ST_OK даже при вылете клиента.


потому что нужно использовать gnutls который для сокетов и создан.

size_t
gnutls_record_check_pending (gnutls_session_t session);

This function checks if there are unread data in the gnutls buffers. If the return value is non-zero the next call to gnutls_record_recv() is guaranteed not to block.
anonymous2 ★★★★★
()
Последнее исправление: anonymous2 (всего исправлений: 1)
Ответ на: комментарий от anonymous2

gnutls…

Я видел эту конструкцию, но переделывать весь код - страшная морока.

Тем более, мне кажется, что gnutls_record_check_pending это скорее некая замена (select, poll, epoll…) для конкретного соединения, нежели способ борьбы с вылетом «собеседника».

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

Это я отлавливаю просто читая из сокета через recv с установленным флагом MSG_PEEK, т.е. считать из буфера, но не удалять данные.

думаю это не правильно, там не твои данные. вам должны были дать метод SSL_peek или подобное

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

думаю это не правильно, там не твои данные. вам должны были дать метод SSL_peek или подобное

Что значит не «мои данные»?

SSL/TLS надстройка над socket. socket fd можно получить из структуры SSL через SSL_get_fd.

Другое дело, что работать с «сырыми» (закодированными) данными не лучшее решение, но как быть если «нулевой пакет» крашит не только SSL/TLS соединение, но и, вообще, всю программу?

Тем более, что меня даже не интересуют «сырые» данные, а лишь их размер.

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

HighMan
() автор топика

SSL_read натакаясь на пакет нулевой длинны крашит программу

Поверх openssl написано очень много кода. Шансы, что SSL_read/SSL_write падают, когда в сокете 0 байт (внезапно, это любой EOF), скажем так, весьма призрачны. Я бы проверил для начала, что нигде по ошибке не зовется SSL_shutdown.

kawaii_neko ★★★★
()

писал такое дело, ничто там не падало, как ни отрубай клиента. правда клиент тоже свой был.

там надо ждать ивента на сокете и читать неблокирующим ssl_read. если ssl_read вернет длину 0, это ошибка и ее надо декодить.

типа вот кусок кода(поскольку я пользуюсь табами - все тут сильно сьезжает):

		int lres = SSL_read(lconn, fbuff, fsize-lbytes); //non blocking read
		if(lres > 0) {
			lbytes += lres;
			if(lbytes == fsize) return lbytes;
		}
		else {
			int lerr = SSL_get_error(lconn, lres);
			switch(lerr){
			case SSL_ERROR_WANT_READ: 
			case SSL_ERROR_WANT_WRITE:	continue;

			case SSL_ERROR_ZERO_RETURN: ///peer disconnected
				dbg("***crypto::readblock: peer disconnected");
				ssl_print_errors();
				return -1;

			case SSL_ERROR_WANT_CONNECT:
			case SSL_ERROR_WANT_ACCEPT:
				dbg("***crypto::readblock: wants CONNECT/ACCEPT!");
				return -1;

			case SSL_ERROR_SSL:
				dbg("***crypto::readblock: SSL protorcol error!");
				return -1;

			case SSL_ERROR_SYSCALL:
				dbg("***crypto::readblock: SSL error syscall!");
				return -1;

			case SSL_ERROR_NONE:
				dbg("***crypto::readblock: ERROR_NONE!");
				return -1;

			default: 
				//break;
				dbgInt("***crypto::readblock:unknown result!: ",lerr);
				ssl_print_errors();
				return -1;
			}
		}
		//sleepMs(50);

alysnix ★★★
()

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

это чтение в буфер блока размером fsize с таймаутом ftmo. возвращает число считанных байт, 0 - если вышло по таймауту, или -1 если ошибка


int CryptoConnection::readBlock(void *fbuff, int fsize, uint ftmo){
	SSL *lconn = as_SSL(_conn);

	int lbytes = 0;

	while (true) {
		//dbg("---------> try to get ssl block");
		if (fsize == lbytes) return lbytes;
		if ( SSL_pending(lconn) == 0) {
			//WaitResult lwr = waitOnHandle((upHandle)_handle,ftmo);
			WaitResult lwr = upNet::waitOnSocket(_handle,ftmo);
			switch (lwr) {
			case WaitResult::awaited: 
			//	dbg("...awaited...");
				break;
			case WaitResult::error: 
				//dbg("---------> error on read block");
				return -1;

			case WaitResult::logic_error: 
				//dbg("---------> error on read block");
				return -1;

			case WaitResult::timeouted: 
				//dbg("---------> error on read block");
				return 0;

			default: //timeout
				//dbg("--------> exit by timeout");
				//return lbytes;
				return 0;
			}
			//dbg("*****awaited");
		}

		//dbg("---------> SSL block got");
		
		
		int lres = SSL_read(lconn, fbuff, fsize-lbytes); //non blocking read
		if(lres > 0) {
			lbytes += lres;
			if(lbytes == fsize) return lbytes;
		}
		else {
			int lerr = SSL_get_error(lconn, lres);
			switch(lerr){
			case SSL_ERROR_WANT_READ: 
			case SSL_ERROR_WANT_WRITE:	continue;

			case SSL_ERROR_ZERO_RETURN: ///peer disconnected
				dbg("***crypto::readblock: peer disconnected");
				ssl_print_errors();
				return -1;

			case SSL_ERROR_WANT_CONNECT:
			case SSL_ERROR_WANT_ACCEPT:
				dbg("***crypto::readblock: wants CONNECT/ACCEPT!");
				return -1;

			case SSL_ERROR_SSL:
				dbg("***crypto::readblock: SSL protorcol error!");
				return -1;

			case SSL_ERROR_SYSCALL:
				dbg("***crypto::readblock: SSL error syscall!");
				return -1;

			case SSL_ERROR_NONE:
				dbg("***crypto::readblock: ERROR_NONE!");
				return -1;

			default: 
				//break;
				dbgInt("***crypto::readblock:unknown result!: ",lerr);
				ssl_print_errors();
				return -1;
			}
		}
	}
	return lbytes;
}

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от kawaii_neko

Поверх openssl написано очень много кода. Шансы, что SSL_read/SSL_write падают, когда в сокете 0 байт (внезапно, это любой EOF), скажем так, весьма призрачны. Я бы проверил для начала, что нигде по ошибке не зовется SSL_shutdown.

Точно, что ни кто и ни где никаких SSL_shutdown или SSL_free не вызывает.

Клиента пишу снова я, причем на Python :)

Вернее я ещё сразу пишу несколько функций, которые будут вызываться во Flask.

У меня на 2х мониторах 2 vscode. На одном сишный код, а другом питоновский. Обоих запускаю в режиме отладки и через Shift+F5 убиваю клиента.

После убийства клиента, на сервер летит пакет нулевой длинны, означающий закрытие соединения со стороны клиента, но, видимо, пакет нулевой длинны, означает закрытие на уровне сокетов, а TLS пакет, закрытия соединения, физически имеет иной размер, и когда SSL_read ловит нулевой пакет, то крашится.

Возможна и другая ситуация. В теории.

TLS/SSL надстройка над socket и его же использует. Когда крашится клиент, то на сервер летит нулевой пакет для socket! Socket ловит этот пакет и закрывает соединение, а вот SSL о прекращении соединения ни сном не духом и пытается что-то дальше мутить с соединением, которого уже нет.

Это я просто теоретезирую.

Вообще, понять причину крашей оказалась задача вовсе не тривиальная. Как понять по какой причине SSL_read крашит весь процесс при верных значениях параметров?

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

Сделать MVE и выложить код?

Что значит MVE?

Выложить весь код - это треш!

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

Сократить код, так же, не представляется возможным, т.к. начальная задумка сделать пару базовых классов и с их помощью автоматизировать всю черновую работу. Т.е. инициализация, создание TLS соединения, ещё и распедаливать вызывы чтения/записи через poll.

В результате нужно лишь унаследоваться от базового класса и обрабатывать лишь готовые данные.

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

В общем, ошибку я нашел. Получить ее можно лишь зверски убивая клиентское соединение.

Установлено точно, что прилетает пакет нулевой длинны (физической) (close connection) и если его ловит SSL_read, то просто краш.

Вот просто, вызов этой функции и программа вылетает.

Нужно будет попробовать на работе:

close(SSL_get_fd(ssl));
SSL_write(ssl, data, data_size);

Возможно и это приведет к крашу.

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

Может следует тщательнее проверить обработку ошибок, которые не ошибки собственно, а состояния движка SSL - возможно пропустил какое-то действие, и получил некорректное состояние.

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

Может следует тщательнее проверить обработку ошибок, которые не ошибки собственно, а состояния движка SSL - возможно пропустил какое-то действие, и получил некорректное состояние.

Вот это возможно.

А что проверять и как?

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

ioctl(fd, FIONREAD, & count)

Воот!

Этой конструкцией удалось заменить recv(sock_fd, data, data_size, MSG_PEEK);

Поскольку у меня recv может вызываться лишь когда на сокет что-то пришло, то:

if(!ioctl(SSL_get_fd(ssl), FIONREAD, & count))
    if(!count)
        return 0;
return SSL_read(ssl, data, data_size);

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

HighMan
() автор топика

Если хотите, чтобы все пахало понятно и без проблем, то напишите свой кастомный траспортный уровень для OpenSSL - ну т.е. чисто TCP-шную подложку. В OpenSSL есть такая возможность.

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

SSL/TLS надстройка над socket

OpenSSL не спроэктирована так чтобы ее можно было трактовать как «надстройка над socket». тоесть формально это правда, но при использовании в реале возникает много сложностей. Поэтому я люблю mBed SSL от ARM.

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

Установлено точно, что прилетает пакет нулевой длинны (физической)

пакетов нулевой длины не существует. Прилетает shutdown. recv возвращает ноль потому что соединение уже закрыто или уже не существует.

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

OpenSSL не спроэктирована так чтобы ее можно было трактовать как «надстройка над socket». тоесть формально это правда, но при использовании в реале возникает много сложностей.

В свое время потребовалось написать собственный HTTP-клиент для платежных терминалов - необходимо было более четко знать, на каком шаге отработки HTTP-протокола произошел облом (типа, ушел запрос на сервер или нет). Поэтому нижнюю чисто socket-ную логику сделал свою. Так что, соответствующий уровень абстракции в OpenSSL имеется. Правда, я писал клиент - а здесь сервер…

vinvlad ★★
()
Последнее исправление: vinvlad (всего исправлений: 1)
Ответ на: комментарий от cvv

пакетов нулевой длины не существует. Прилетает shutdown. recv возвращает ноль потому что соединение уже закрыто или уже не существует.

Я, просто, назвал так «нулевой пакет».

На данный момент я отлавливаю получение подобного пакета на уровне socket через ioctl(sock_fd, FIONREAD, &count) и если !count то не обрабатываю его через SSL_read, а просто закрываю соединение. Если же count > 0 то вызываю SSL_read.

Таким кривым способом удалось избежать крашей сервера.

HighMan
() автор топика