Лимиты: все четыре правила ADR-021 — регистрация 5/час на IP, вход 10/10 мин на IP и ник, сообщения 30/мин, прочие изменяющие 60/мин; 429 с Retry-After; X-Real-IP читается только с loopback, иначе адрес соединения — иначе заголовок отменял бы лимит на IP; карты вёдер ограничены поколениями. Аудит нашёл то, что пропустили пять раундов ревью: ADR-056: nginx вёл access_log с IP и полными путями вопреки обещанию deploy.md. Ники и социальный граф ложились в /var/log/nginx рядом с чистым журналом bare. ADR-058: «выйти на других устройствах» не обрывал уже открытый SSE — отозванная сессия продолжала получать сообщения. ADR-059: промежуточный ключ комнаты был невосстановим. Участник, пропустивший офлайн два rekey подряд, навсегда не расшифровал бы сообщения среднего ключа — вопреки обещанию storage.md о повторной попытке после получения keyId. ADR-063: ACK уходил по одному на конверт, а не пачкой. Получатель в оживлённой комнате выедал общее ведро подтверждениями и упирался в 429 на всех изменяющих запросах, включая выход из комнаты: 116 отказов за прогон стало нулём. ADR-055, 057, 060, 061, 062: ключи вёдер и границы, +dirty у bare version, 403 unknown_device не хоронит сообщение, усечение имени в подсказке ввода, kdf как оракул после повышения цели KDF. Модель угроз пополнена тем, что действительно видит оператор: push-подписки лежат в базе открытым текстом, и вместе с VAPID-ключом с той же машины это произвольное уведомление на экране блокировки. README приведён к v1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
7.6 KiB
ADR-055: Ключи лимитов, границы карт и доверие к X-Real-IP
Уточняет ADR-021: четыре правила названы там, всё остальное про них — здесь.
Уточнён ADR-063: POST /api/ack приходит пачками не только при подключении — клиент копит подтверждения и шлёт их не чаще раза в две секунды.
Контекст
ADR-021 задаёт лимиты одним списком: регистрация — 5 в час на IP, вход — 10 за 10 минут на пару IP+ник, сообщения — 30 в минуту на пользователя пакетом 10, остальные изменяющие запросы — 60 в минуту на пользователя. Этап 6 доводит список до кода, и пять вещей списком не решены.
Пакет. Он назван только у сообщений. У остальных правил его нет, а token bucket без него не собрать.
«Остальные изменяющие». Какие именно и одним ли ведром — не сказано. POST /api/ack изменяет очередь, но приходит пачками после каждого подключения; GET не изменяет ничего.
Место в порядке проверок. У сообщений оно записано (docs/protocol.md, «Сообщения»): лимит последний, после формы и прав. Для общего лимита такого места нет: чтобы спросить ведро, нужен только ник сессии, а разбор тела — уже та работа, ради отказа от которой лимит и заводится.
Размер карт. Ведро заводится на каждый новый ключ, а ключ — чужой адрес: их бывает сколько угодно. Выбрасывать полные вёдра, как делал этап 2, под потоком новых ключей бесполезно — полных не бывает, каждое только что потратило токен. Миллион адресов давал миллион вёдер и рост памяти без предела.
X-Real-IP. ADR-021 говорит «только если соединение с 127.0.0.1». Соединение с ::1 приходит с той же машины и заслуживает того же доверия, а по букве оно его не получает: тогда все клиенты за таким nginx складываются в одно ведро, и лимит на IP превращается в лимит на сервер.
Рядом — расхождение в docs/deploy.md: «Логи» разрешают писать ник «для ошибок аутентификации по лимитам». Такой строки в коде нет и не заводится: ник — данные пользователя, а отказ и так видно по статусу.
Решение
- Пакет равен лимиту, где ADR-021 его не назвал: 5 в час — пакет 5, 10 за 10 минут — 10, 60 в минуту — 60. За окно набегает ровно лимит, и потратить его можно разом. Отдельный пакет остаётся у сообщений: 30 в минуту, пакет 10.
- «Остальные изменяющие» — все непубличные маршруты, кроме
GET, одним ведром на пользователя, включаяPOST /api/ack. Чтения не ограничиваются: ADR-021 ограничивает изменяющие, и большего v1 не вводит.POST /api/messagesв это ведро не входит — у него своё правило. - Общий лимит стоит на маршруте, сразу за проверкой сессии, и отвечает раньше разбора тела. ADR-043 это не нарушает:
429говорит не о правах и не о существовании сущностей, а о частоте;401 unauthenticatedстоит там же и раньше. - У регистрации, входа и сообщений лимит стоит в обработчике: после проверки формы и до работы. У сообщений это записанное место в порядке проверок. У входа — раньше обращения к хранилищу и argon2: перебор не должен заказывать серверу работу. У регистрации — раньше проверки инвайт-кода, иначе код подбирается запросами без счёта.
- Форма регистрации проверяется раньше инвайт-кода (ADR-043). Занятость ника по-прежнему за ним:
409 nick_takenживёт после проверки кода, и без кода ники не перебрать. - Карты вёдер ограничены сменой поколения. Карт две: нынешняя и прежняя. Как только нынешняя дорастает до 4096 ключей, она становится прежней, а прежняя выбрасывается целиком. Ключ, по которому продолжают ходить, переезжает в нынешнюю и смену переживает. Обе карты вместе — не больше 8192 вёдер на правило.
- X-Real-IP читается с любого loopback-адреса —
127.0.0.0/8и::1. Соединение не с loopback — заголовок не читается вовсе, ключом становится адрес соединения. - Ник в журнал не пишется никогда, включая отказы по лимитам; строка про это убрана из
docs/deploy.md.
Следствия
- Лимит на IP держится ровно до тех пор, пока nginx — единственный, кто ходит на порт. Прямой доступ к
8411снаружи снял бы его целиком, поэтому порт слушается на127.0.0.1(ADR-022). - Миллион разных адресов стоит около мегабайта на правило, а не гигабайта. Цена — поток чужих ключей протирает ведро того, кого лимит держал: забытое ведро равно новому. ADR-021 уже принял, что рестарт обнуляет лимиты; это то же самое, только чаще.
- Промахнувшийся инвайт-кодом пять раз ждёт час. Числа ADR-021 не меняются: барьер от ботов дороже удобства опечатки.
- Клиент отличает
429от прочих отказов по кодуrate_limitedи показывает «слишком часто, попробуйте позже» (ADR-028). Новых текстов интерфейса решение не заводит.