vostok: сервер Gemini, версия 0.1.4

Весна в самом разгаре и Gemini сервер vostok получил серьезное исправление. Хотя всё немного сложнее: есть проблема в библиотеке libtls, от которой зависит сервер и в функцию, вызывающую tls_write из этой библиотеки, пришлось вставить "костыль".

Предыдущая запись блога разработки

Что нового в версии 0.1.4:

Возвращаемое значение функции tls_write.

Всё началось 10 апреля. Мой хостер (OpenBSD Amsterdam) написал мне, что моя виртуальная машина стала сильно потреблять CPU (чего до этого за ней не водилось) и попросил подтвердить, что это ожидаемое поведение. За что им, кстати, отдельное спасибо: уже не первый год пользуюсь их услугами, всегда адекватное взаимодействие.

OpenBSD Amsterdam

Виновника ищем командой "ps -uaxHf | less". Как несложно догадаться, читая этот материал в журнале разработки, процессор активно потреблял демон vostok. Было видно, что у процесса две нити (threads), одна из которых непрерывно кушает ЦП. Непорядок.

В силу своей лени я всё еще не реализовал нормальные логи для сервера vostok (каюсь). Но всегда есть грязные хаки: в моем случае, вместо запуска демона, я перезапустил сервер в tmux, что бы читать stderr из консоли. Плюс, временно, расширил количество логируемой информации. На время расследования я собирал IP адреса обращающихся к серверу. Накопив статистику я выяснил:

Не густо, но уже кое-что. Кстати, судя по IP, проблемным оказался сканер от некоторой компании "censys". Даже не знаю: ругать их или... Сканируют не назойливо, вскрыли проблему в сервере.

Судя по отладочному логу, сервер зацикливается в отсылке ответа клиенту: функция send_response в vostok/vostok.cc. А эта функция только вызывает transport::send, которая, фактически, повторяет пример вызова tls_write из её man-страницы:

https://man.openbsd.org/tls_write

while (len > 0) {
	ssize_t ret;

	ret = tls_write(ctx, buf, len);
	if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
		continue;
	if (ret == -1)
		errx(1, "tls_write: %s", tls_error(ctx));
	buf += ret;
	len -= ret;
}

Очевидно, что зацикливание тут может быть только в одном случае: функция tls_write бесконечно возвращает 0.

Я спросил в списке рассылки libressl что нужно делать, если tls_write возвращает 0. Мне в личном письме один из ментейнеров ответил, что поведение должно быть как в примере, приведённом на man-странице.

Хорошо, надо убедиться, что я понимаю суть проблемы. Начинаем читать исходный код tls_write. И сходу бросается в глаза один вариант работы функции, когда SSL_write возвращает ошибку, а tls_write в итоге вернёт 0. После ошибки вызова SSL_write будет вызвана tls_ssl_error, которая и должна вернуть значение -1. Для всех ошибочных случаев... кроме SSL_ERROR_ZERO_RETURN.

https://man.openbsd.org/SSL_get_error

SSL_ERROR_ZERO_RETURN
The TLS/SSL connection has been closed. If the protocol version is SSL 3.0 or TLS 1.0, this result code is returned only if a closure alert has occurred in the protocol, i.e., if the connection has been closed cleanly. Note that in this case SSL_ERROR_ZERO_RETURN does not necessarily indicate that the underlying transport has been closed.

Ага, соединение закрыто, но tls_write возвращает 0. По моему это баг. Но сообщать об ошибке разработчикам libtls надо хотя бы со стабильным воспроизведением. А воспроизвести эту ситуацию сходу не получилось. Более того, если и клиент и сервер написаны с использованием libtls, то проблема, как я понял, не воспроизводима. Поэтому клиента пришлось писать на чистом OpenSSL (точнее на LibreSSL). Изучив исходники OpеnSSL более подробно я смог написать программу, которая стабильно воспроизводит проблему. Это стало основой нового сообщения с список рассылки libressl:

Bug: tls_write infinitely returns 0 after SSL_shutdown on the other side

Ждать исправления в libtls смысла особого не вижу: есть проблемные версии библиотеки, а workaround решения проблемы добавляет незначительный оверхед. В любом случае я откровенно пожалел, что в начале разработки выбрал libtls в качестве платформы: стоило написать на чистом OpenSSL. Лишняя прослойка - лишний источник ошибок.

По хорошему мне стоило бы самому предложить патч с решением проблемы. Но libtls является частью большого проекта LibreSSL и вникать в тонкости его сборки и тестирования мне совершенно не хочется. В итоге для сервера vostok я добавил код, проверяющий, что вызов tls_write вернул 0 тысячу раз подряд. Эта ситуация обрабатывается как ошибка (как если бы tls_write вернул -1). Исправление зафиксировано в репозитории тэгом v0.1.4.

Комментарии через ActivityPub (Fediverse) можно оставить здесь:

https://honk.any-key.press/u/continue/h/4p23sHJLYqhb28sR3R

Следующая запись блога разработки