strfry 1.1.2 исправляет DoS по памяти в WebSocket
Клиент мог бесконечно добавлять продолжающие кадры WebSocket, каждый из которых оставался ниже предела полезной нагрузки, и буфер релея рос без потолка. Тег 1.1.2 добавляет недостающую проверку, очищает прежнюю сессию negentropy перед открытием новой с тем же id и не даёт задержке переподключения блокировать цикл событий.
Nostr WoT Newsroom
Материал подготовлен редакцией Nostr WoT на основе указанных первоисточников и опубликован автоматически, без индивидуальной проверки человеком.
strfry поставил тег 1.1.2 27 августа 2026 года. Релей выпускает версии через теги git, а не через релизы GitHub, поэтому тег и его запись в CHANGES и есть всё объявление. От 1.1.1 его отделяют пять коммитов, затрагивающих четыре пути: CHANGES, указатель на подмодуль golpe, src/WSConnection.h и src/apps/relay/RelayNegentropy.cpp.
Перечислены три исправления. Два касаются падений или исчерпания ресурсов, достижимых со стороны подключённого клиента. Третье не даёт процессу stream или sync у оператора зависнуть после обрыва связи.
Фрагменты ниже предела
Первое исправление находится не в strfry. strfry подключает свой стек WebSocket через golpe, и тег поднимает этот подмодуль, который, в свою очередь, поднимает uWebSockets. Тот коммит добавляет пять строк в handleFragment в src/WebSocket.cpp:
if (webSocket->fragmentBuffer.length() > group->maxPayload) {
forceClose(webSocketState, (char*)"payload size exceeded", 21);
return true;
}Протокол WebSocket допускает, что одно логическое сообщение придёт в виде первого кадра и произвольного числа продолжающих кадров, а завершающий кадр несёт бит FIN. Пока сообщение не собрано, части накапливаются в fragmentBuffer. Предел полезной нагрузки применялся к кадрам, а не к буферу, в который они добавлялись, поэтому клиент, отправляющий продолжающие кадры и никогда не выставляющий FIN, мог наращивать буфер неограниченно, притом что каждый отдельный кадр оставался ниже предела. Новая проверка измеряет накопленную длину после каждого добавления и принудительно закрывает соединение с payload size exceeded, как только она превышает maxPayload.
Именно на это стоит отреагировать. Здесь не нужны ни аутентификация, ни какое-либо необычное поведение протокола, кроме отказа завершить сообщение, а издержки ложатся на память релея, а не на отправителя.
Сессия negentropy со знакомым именем
Второе исправление занимает три строки в RelayNegentropy.cpp. Negentropy это протокол сверки множеств, который strfry использует для синхронизации, и каждая сессия ведётся по id подписки, выбранному клиентом. Когда клиент открывал новую сессию, повторно используя id, чей предыдущий запрос ещё обрабатывался, релей падал.
Исправление очищает старое состояние до регистрации новой сессии:
queries.removeSub(connId, subId);
views.removeView(connId, subId);Оба вызова стоят непосредственно перед строкой журнала, отмечающей новую сессию negentropy, так что повторно использованный id теперь вытесняет то, что было под ним, вместо конфликта с ним. Подписка и представление удаляются парой, так же как и создаются.
Переподключение, которое подключалось и умирало
Третье исправление это самый большой diff из пяти коммитов, и его объяснение содержится в комментарии, который тот же коммит добавляет в WSConnection.h.
Прежний код выдерживал задержку перед повторной попыткой соединения и реализовывал её через std::this_thread::sleep_for в потоке хаба. Этот же поток выполняет цикл событий uv. Сон в нём останавливает кешированные часы цикла на длительность задержки, и следующий вызов hub.connect() заводит свой таймаут соединения в 5000 мс по уже устаревшим часам. Таймер оказывается просроченным в момент установки, срабатывает на следующем витке цикла и рвёт только что открытый сокет до завершения рукопожатия WebSocket.
Первая попытка соединения использовала нулевую задержку и потому никогда не спала, поэтому она работала. Любое переподключение после обрыва отказывало тем же образом, причём навсегда. Как отмечает комментарий, процесс продолжает попытки и никогда не завершается, поэтому супервизор видит работающую программу и не перезапускает её.
Замена откладывает повторную попытку неблокирующим uS::Timer на цикле хаба, благодаря чему часы цикла остаются точными, и выносит само подключение в connectNow(). Последующий коммит сохраняет ожидающий таймер в объекте соединения, чтобы terminate() мог остановить и закрыть его, а не оставлять взведённым на объект, который вот-вот исчезнет.
Этим путём, согласно комментарию к исправлению, пользуются команды stream и sync в strfry, так что симптомом был процесс маршрутизации или синхронизации, который переживал перезапуск релея лишь на словах.
Обновление
Ничто в этом теге не меняет конфигурацию, формат базы данных или поверхность протокола. Поднятие golpe означает, что при пересборке подмодуль нужно подтянуть заново, а не переиспользовать кешированную копию, поскольку исправление uWebSockets приходит именно оттуда, а не из какого-либо файла в дереве strfry.
Источники
Каждое утверждение в этом материале ссылается на первоисточник.
- strfry 1.1.2 — hoytech/strfry (27 августа 2026 г.)
- Сравнение 1.1.1...1.1.2 — hoytech/strfry (27 августа 2026 г.)
- Prevent a memory DoS where attacker continues appending fragments under max payload limit — hoytech/uWebSockets (19 августа 2026 г.)
- Fix WSConnection reconnect stall: don't block the event loop during the reconnect delay — hoytech/strfry (19 августа 2026 г.)