Вечный спор о двух полюсах

Если вы хоть раз обсуждали «правильную» архитектуру мессенджера, вы знаете, что разговор всегда скатывается в два полюса, и оба плохие.

Полюс первый: чистый P2P. Никаких серверов, клиенты говорят напрямую. Звучит красиво ровно до первого практического вопроса. Собеседник офлайн, а вы хотите написать ему сейчас. Куда уйдёт сообщение? В никуда, ждите, пока он включит телефон одновременно с вами. NAT, симметричные файрволы, спящий Android, который убивает фоновые сокеты. P2P горит на неудобстве.

Полюс второй: сервер. Удобно, офлайн-доставка есть, пуши есть. И ровно одна коробка, в которой лежат личности всех, граф контактов всех, очереди всех. Эту коробку можно заблокировать по сети, можно изъять физически, можно прийти к оператору с предписанием. Серверные мессенджеры горят на сервере.

Один из наших пользователей в бете сформулировал это лучше, чем мы в любой презентации: обсуждение альтернатив всегда имело два полюса. Либо ищем инфраструктуру как в Matrix, где все сидят по своим загонам и не пишут друг другу. Либо сидим без офлайн-сообщений как в P2P. Либо вообще не можем подключиться, потому что мосты для обхода блокировок съела моль.

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

Первая ошибка: путать два слоя

Главное, что мы поняли: в мессенджере есть два независимых слоя, и почти все споры о децентрализации смешивают их в кашу.

Слой первый, транспорт. Как байты физически доходят до сервера. Это про IP, домены, обфускацию, обход DPI.

Слой второй, данные. Кто такой пользователь 777, где лежат его ключи, где его очередь сообщений. Это про базу.

Когда кто-то говорит «сделаем релеи, и будет децентрализация», он почти всегда говорит про первый слой и молча оставляет второй централизованным. А уязвимость, от которой вы защищаетесь, живёт ровно во втором слое. Релей можно поднять заново за минуту. Базу с личностями всех пользователей нельзя.

Разберём оба слоя по очереди, потому что хорошее решение для одного не лечит другой.

Слой транспорта: релеи-гидры, и эту идею принесли пользователи

Скажу честно: идея конкретно этого транспортного слоя пришла не от нас, а от пользователей в нашем бета-чате. Люди с реальным опытом обхода блокировок (Asterisk, xray, нюансы работы ТСПУ) предложили вот что.

Сегодня у нас, как у многих, обфускация живёт на каждом клиенте. Каждое приложение тащит свой sing-box, свой VLESS, свои конфиги. Это работает, но это тяжело и это много точек, каждую из которых можно фингерпринтить.

Их предложение: перенести обфускацию с клиентов на релеи. Релей это тупая труба. Он гонит зашифрованные байты до ядра и всё. Он не хранит данных, и благодаря sealed sender он не видит ни отправителя, ни содержимого. Такие релеи поднимают сами пользователи, пачками, как гидру. Сегодня релей тут, завтра там, заблокировали один, поднялся другой. А клиент при этом ходит до ближайшего релея простым невинным трафиком.

Почему это сильно, и почему мы это берём:

Прецеденты ровно такие. Tor с его мостами и Snowflake, Telegram с MTProxy. Это проверенная схема: распылённая сеть одноразовых входных узлов, которую дешевле не трогать, чем блокировать.

Трафик до релея менее палевный. ТСПУ и DPI смотрят в том числе на объём и тайминг. Вялые пакетики, как будто человек бесконечно обновляет какой-то сайт, палятся хуже, чем гигабайты, которые льёт классический VPN. Релей, который обслуживает только один мессенджер, это не публичный VPN с ютубом внутри.

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

Одну оговорку мы держим прямо в дизайне, потому что она важная: гидру из домашних релеев нельзя включать раньше onion-слоя. Домашний релей видит ваш IP и то, на какой остров вы идёте, и изъять его может тот же местный противник. Поэтому порядок жёсткий: сначала наши зарубежные релеи плюс onion-маршрутизация через несколько узлов, чтобы ни один релей не видел и ваш адрес, и адресата разом, и только потом распылённая гидра внутри страны. Иначе она сольёт метаданные ровно тех, кого должна прятать.

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

Слой данных: почему релеи не отменяют единое сердце

Заманчиво сказать: ну вот, релеи распылены, поставим за ними одну хорошую базу, и готово. Релеи защитят от блокировки, а база пусть будет одна, с RAID и бэкапами.

Это и есть подмена. Релеи решают только блокировку. Сердце они не лечат, потому что релей это труба, а не копия мозга. У единого сердца остаётся ещё три способа умереть, и обфускация трафика ни один из них не закрывает.

Изъятие и принуждение. Сервер за границей это не неприкасаемый сервер. Дата-центры изымают, хостер отключает по запросу, оператора прессуют через местный суд. История знает аресты владельцев и выдачу данных. «За границей» не равно «недосягаемо».

Отказ оператора. Деньги кончились, оператор устал, ушёл, передумал. Сама аська умерла не от блокировки, а потому что владелец принял решение её выключить. Одно решение одного хозяина, и аккаунты у всех мертвы. RCQ во многом существует именно потому, что единое сердце аськи когда-то выключили росчерком пера.

Компрометация. Один взлом, и утекли метаданные всех разом. Один оператор это одна точка, которую можно обязать логировать. Весь смысл такого проекта в том, что нет единого, кого можно прижать. Единое сердце за границей это всё равно один, кого можно прижать.

И отдельно: при единой базе у того, кто поднял свой сервер, ноль суверенитета. Он не хозяин своих пользователей, он донор трафика центральному оператору.

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

Модель: ключ это личность, остров это мейлбокс

Дальше начинается наша часть. Мы разносим и слой данных тоже, но не репликацией одной базы, а по-другому.

Личность это криптографический ключ, а не номер. Номер (UIN) это просто локальная ручка на конкретном острове плюс подсказка о маршруте. Адрес выглядит как номер@остров. Голый номер по умолчанию означает наш флагманский остров, а @остров всплывает только для других островов и самохостинга. Проверяется собеседник по safety-number, то есть по ключу, а не по доверию к номеру.

Из этого сразу следует ответ на вопрос, который нам задавали чаще всего: а что с коллизией номеров. Если 777@остров-А и 777@остров-Б это два разных человека, это нормально, как bob@gmail и bob@yahoo это два разных Боба. Номер не несёт личность, личность несёт ключ. Совпадение номеров на разных островах это не коллизия личностей, это просто «эта ручка тут занята».

Остров (мы называем его мейлбоксом) это уже не релей, это отдельная роль. Он держит входящую очередь пользователя и отдаёт его бандл ключей. Очередь временная: остров подержал запечатанный конверт, пока получатель не забрал, и стёр. Своих островов меньше, чем релеев, и поднимает их тот, кто хочет настоящего суверенитета.

Доставка работает через client-multihoming, и тут ключевой принцип: острова между собой не общаются вообще. Никакого server-to-server. И это не просто проще федерации в стиле Matrix, это честнее по метаданным. При server-to-server появляется отправляющий сервер, который видит оба конца сразу: и кто пишет, и кому на какой остров. При нашей схеме такого узла нет вообще. Остров получателя узнаёт не больше, чем наш бэкенд сегодня (запечатанный конверт пришёл локальному получателю с какого-то адреса), а остров отправителя в отправке вообще не участвует. Так что ни один сервер не видит оба конца разом. Мостит их сам клиент: отправитель тянет бандл получателя с его острова, запечатывает конверт и кладёт в очередь его острова. Чтобы пережить смерть острова, пользователь прописан сразу на нескольких. Отправитель кладёт конверт во все, а клиент получателя забирает с любого живого и схлопывает дубликаты по id сообщения. Это и есть избыточность: она живёт в поведении клиента, а не в связке серверов.

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

И последнее, что делает всю эту конструкцию дешёвой: история сообщений локальная, она лежит на устройствах. Сервер это временный буфер, а не архив. Поэтому смерть острова почти ничего не стоит: то, что ценно, на сервере и не лежало, теряются максимум сообщения, которые были в полёте. И по запросу серверу нечего отдать, потому что истории у него физически нет.

Почему нельзя сделать единую базу красивых номеров

Нас регулярно спрашивают: ну сделайте всё-таки единую сквозную нумерацию, чтобы 777 был один на весь мир. Это не лень с нашей стороны, это фундаментальный размен, известный как треугольник Зуко.

Имя в сети может иметь максимум два свойства из трёх: человеко-приятное (короткий запоминаемый номер), глобально-уникальное (без коллизий) и без центрального издателя. Хотите короткие уникальные номера на весь мир, обязаны иметь центрального издателя этих номеров. А центральный издатель это ровно то единое сердце, от которого мы уходим. Блокчейн-реестр имён формально децентрализован, но это глобальный общий журнал, в который каждый остров обязан синкаться и которому обязан доверять, то есть снова глобальная точка координации. Для хранения базы он не нужен и даже вреден.

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

Честные границы: что это пока НЕ решает

Теперь обязательная часть, без которой такие тексты читать нельзя.

Это уже не только дизайн. Кросс-островная доставка (мы зовём её Layer B) работает. Мы подняли второй полностью независимый остров и прогнали полный цикл: человек на одном острове пишет человеку на другом, конверт запечатывается ключом получателя, ложится в очередь его острова, забирается и расшифровывается. Это живёт в вебе и в опубликованной сборке под Android и iOS, а не на бумаге.

Мультихоуминг и failover тоже уже не план. Аккаунт держится сразу на нескольких островах, резервный приложение подбирает само, одним переключателем. Сообщение кладётся во все ящики, клиент забирает из любого живого и схлопывает дубликаты по id. Падение острова при этом невидимо для личной переписки. У первой версии есть честные рамки, и мы их не прячем: на резервном острове ходит только наш простой формат запечатывания, а не полноценная libsignal-сессия; опрашивает резерв то устройство, которое его включило; групповые чаты пока живут на одном острове. Но базовый сценарий, ради которого всё затевалось, остров умер и ничего не потеряно, уже работает и проверен на живом втором острове.

Главная нерешённая часть это rendezvous, то есть обнаружение. Теперь, когда доставка и мультихоуминг работают, именно она выходит на первый план. Релеи и острова «везде» это не решение само по себе. Настоящая инженерия в том, как клиент находит свежий релей и новый остров переехавшего собеседника, когда старое заблокировали, так, чтобы сам этот первый шаг тоже не блокировался. Это ровно те самые «мосты, которые съела моль». Тут даже Tor не идеален, и мы не делаем вид, что у нас есть серебряная пуля. Это следующий большой кусок работы, и именно на нём модель либо взлетит, либо споткнётся.

Метаданные. Сервер всё ещё видит время и IP подключения до того, как появится onion-слой (уже проектируем, но не готов). Sealed sender прячет отправителя и содержимое, релеи прячут, на какой остров вы ходите, но против глобального пассивного наблюдателя, который видит весь трафик разом, не защищён никто, включая Tor. Мы не обещаем то, чего не бывает.

Удобство имеет цену. Локальная история означает, что новое устройство не подтянет старую переписку с сервера, её там нет. Потеряли все устройства разом, потеряли историю. Лечится мультидевайсом и опциональным локальным шифрованным бэкапом, но это честный размен privacy-мессенджера, ровно как у Signal.

Зачем мы это пишем

Не для того, чтобы сказать «у нас всё готово». Готово далеко не всё. Мы пишем это, потому что разговор о двух полюсах ведётся годами и почти всегда упирается в ложный выбор между неудобным P2P и сервером, который можно изъять. Выход есть, и он не требует ни магии, ни блокчейна: разделить транспорт и данные, сделать релеи слепыми трубами, а данные распылить по островам так, чтобы у них была та же гидро-природа, что и у релеев. Тогда сердца, которое можно изъять, просто нет.

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

Код открыт. Если у вас есть мысли по rendezvous, нам правда интересно, это сейчас самый горячий для нас вопрос.

Ссылки

Сайт и приложения: rcq.app
Карта островов (beta): fed.rcq.app
Веб-клиент, можно попробовать прямо сейчас: chat.rcq.app
Открытый код, AGPL: github.com/rcq-messenger
Каталог островов и как поднять свой: rcq.app/servers