Мы делаем мессенджер. Весной 2026 наш бэкенд начал отваливаться у части пользователей из России: HTTPS-запросы к API таймаутятся, WebSocket не поднимается. Картина знакомая всем, кто держит сервис с одним доменом и одним IP.
Для мессенджера это приговор. Не «неудобно», а именно приговор: приложение, которое не может даже подключиться, бесполезно. И вариант «попросите пользователя сначала включить VPN» нас не устраивал совсем. Ниже разберу, почему мы в итоге встроили обход прямо в приложение, на чём он работает и на какие грабли мы наступили. Без маркетинга, по делу.
Почему не «просто VPN»
Первая мысль у всех одинаковая: пусть юзер поставит VPN. Но если посмотреть на это глазами продукта, всё разваливается.
Во-первых, это убивает воронку. Каждый дополнительный шаг до «приложение заработало» стоит вам части аудитории, а «установите и настройте отдельное приложение» это не шаг, это пропасть. Особенно болезненно, что отваливается ровно та часть пользователей, которой продукт нужнее всего.
Во-вторых, сам VPN это движущаяся мишень. Популярные протоколы (OpenVPN, WireGuard, IKEv2) давно и неплохо детектируются по сигнатурам, диапазоны известных VPN-провайдеров режут пачками. То есть вы делегируете критичную для вас функцию стороннему приложению, которое завтра само перестанет работать.
Нам хотелось другого поведения: пользователь открывает мессенджер, и он работает. Без отдельного приложения, без подписки на чужой сервис, без VPN-профиля в системных настройках iOS. Обход должен быть деталью реализации, а не задачей пользователя.
Что именно блокируется
Чтобы понимать, что чинить, надо понимать, что ломается. У нас довольно типичная схема: HTTPS API и WebSocket на одном домене. Заблокировать такое соединение можно несколькими способами, и обычно их комбинируют:
по IP-адресу сервера;
по имени домена в ClientHello (поле SNI отправляется открытым текстом даже в TLS);
по DPI, который смотрит на саму структуру трафика и опознаёт протокол.
Обычное TLS-соединение на ваш домен срезается по SNI на раз. Значит, задача формулируется так: соединение должно выглядеть как обращение к какому-то другому, ничем не примечательному и заведомо разрешённому хосту. Не «зашифровать сильнее», а именно «выглядеть иначе».
Почему VLESS и Reality
Немного истории, она тут важна. Shadowsocks и VMess в своё время решали ровно эту задачу, и какое-то время решали хорошо. Проблема в активном пробинге: цензор не просто смотрит на ваш трафик пассивно, он сам отправляет на подозрительный сервер пробный запрос. Если сервер отвечает как прокси (а ранние реализации отвечали узнаваемо), адрес уезжает в блок. Плюс накопились пассивные сигнатуры, по которым «прокси-трафик» отличался от обычного HTTPS.
Reality решает это аккуратным трюком. Прокси-сервер не предъявляет собственный TLS-сертификат. Вместо этого во время рукопожатия он проксирует TLS-handshake на реальный, посторонний, популярный сайт. Для пассивного наблюдателя и для активного пробера ваш сервер неотличим от этого сайта: валидный сертификат, валидное рукопожатие, валидная цепочка. И только клиент, у которого есть правильный публичный ключ, после рукопожатия «переключается» на настоящий туннель. Постороннему пробингу переключиться не на что, он видит обычный крупный сайт.
Что это даёт на практике:
нет своего домена, который можно занести в блок по SNI;
нет своего сертификата, по которому можно опознать «вот это прокси»;
активный пробинг упирается в чужой легитимный сайт.
VLESS тут это лёгкий транспорт без собственного слоя шифрования (шифрование берёт на себя TLS), с маленьким и невыразительным отпечатком. А поток xtls-rprx-vision дополнительно сглаживает паттерн «TLS внутри TLS», который иначе виден по характерным размерам пакетов.
Ничего из этого мы не изобретали. Reality и sing-box это чужая, и очень хорошая, работа. Интересная часть начиналась дальше: как затащить это внутрь обычного приложения так, чтобы пользователь вообще ничего не заметил.
sing-box внутри приложения
sing-box это универсальная прокси-платформа на Go, и среди прочего она умеет быть клиентом VLESS + Reality. Обычно её запускают как отдельный процесс с конфигом. Нам отдельный процесс не нужен, нам нужно, чтобы это жило внутри iOS-приложения.
Здесь помогает gomobile. Через gomobile bind Go-код собирается в нативный .xcframework, который линкуется в приложение как обычная зависимость. То есть sing-box у нас не CLI рядом, а фреймворк внутри бинарника. Запускаем его прямо в процессе приложения.
Поднятый таким образом sing-box открывает локальный inbound (тип mixed, то есть SOCKS и HTTP сразу) на 127.0.0.1 на случайном порту. Дальше есть развилка, и она принципиальная.
Можно сделать системный VPN через NEPacketTunnelProvider: тогда через туннель пойдёт весь трафик устройства. Можно сделать прокси уровня приложения: тогда через туннель пойдёт только трафик вашего приложения. Мы выбрали второе, и вот почему. Нам не нужно гнать через relay весь телефон, нам нужно довести до сервера только трафик мессенджера. А раз так, то Network Extension с его архитектурой, отдельным процессом-расширением, лимитами по памяти и дополнительными вопросами на ревью в App Store нам просто не нужен. Меньше движущихся частей, меньше точек отказа.
На практике это выглядит так: sing-box крутит локальный прокси, а сетевой стек приложения (URLSession) мы заворачиваем на этот локальный адрес.
let config = URLSessionConfiguration.default config.connectionProxyDictionary = [ "SOCKSEnable": 1, "SOCKSProxy": "127.0.0.1", "SOCKSPort": localPort, ]
Весь остальной телефон при этом не затронут, никакого VPN-профиля в настройках не появляется. Минус подхода честный: это не универсальный VPN, через него не пойдут другие приложения. Но нам это и не требовалось.
Конфиг для sing-box генерируем в рантайме. Если убрать лишнее, ядро выглядит так:
{ "inbounds": [{ "type": "mixed", "listen": "127.0.0.1", "listen_port": 0 }], "outbounds": [{ "type": "vless", "server": "RELAY_ADDR", "server_port": 443, "uuid": "...", "flow": "xtls-rprx-vision", "tls": { "enabled": true, "server_name": "www.посторонний-крупный-сайт.com", "utls": { "enabled": true, "fingerprint": "chrome" }, "reality": { "enabled": true, "public_key": "...", "short_id": "..." } } }] }
Отдельно отмечу utls с отпечатком chrome. Стандартная Go-реализация TLS имеет свой узнаваемый ClientHello, и это само по себе сигнатура. uTLS подменяет ClientHello так, чтобы он совпадал с настоящим Chrome. Деталь мелкая, но из таких мелочей и складывается «выглядит как обычный браузер».
Грабли с relay
Теперь то, ради чего стоило писать эту статью. С протоколом всё оказалось ровно. По граблям мы прошлись на инфраструктуре.
Первый relay мы подняли на дешёвом VPS у типичного хостера. Адрес сгорел быстро. Диапазоны датацентров и хостингов, на которых исторически много прокси, отслеживаются и режутся либо превентивно, либо очень оперативно. Reality прекрасно прячет протокол, но он никак не прячет сам факт «трафик идёт на IP из подозрительного диапазона».
Мы перенесли relay на адрес в диапазоне крупного облачного провайдера. И вот он живёт. Логика простая: вырезать оптом адресное пространство большого облака дорого по сопутствующему ущербу, на тех же адресах висит масса легитимных сервисов, которые ломать никто не хочет.
Вывод, который мы для себя записали: с Reality слабое место это не протокол, а IP. Горит именно адрес. Значит, relay по определению расходник, и относиться к нему надо как к расходнику.
Адрес relay не должен быть зашит в бинарник
Прямое следствие предыдущего пункта. Если адрес relay захардкожен в приложении, то сгоревший IP означает новый релиз в App Store и ожидание ревью. Для расходника, который может сгореть в любой день, это неприемлемо.
Поэтому параметры relay (адрес, ключи, SNI) приложение должно получать отдельно от своей сборки. Тогда смена relay это смена конфига, а не релизный цикл. Конкретный механизм доставки конфига можно выбрать разный, важен сам принцип: то, что горит часто, не живёт в бинарнике.
Честно про границы
Туннель это не магия, и продавать его как магию нечестно.
Оператор relay (в нашем случае мы сами) видит ваш трафик к нашим серверам ровно так же, как его видел бы ваш провайдер при прямом подключении. Туннель меняет то, как соединение выглядит для цензора по дороге, и не меняет того, кто стоит на концах. Содержимое переписки у нас в любом случае защищено сквозным шифрованием на уровне приложения (libsignal), и туннель к этому ничего не добавляет и ничего не отнимает. Это важно разделять: обход блокировок и приватность переписки это две разные задачи, которые решаются разными слоями.
Reality хорошо держит активный пробинг, но «навсегда» в этой области не существует. IP горят, сигнатуры накапливаются, методы детекта развиваются. Это гонка, и относиться к ней надо как к гонке: иметь запас адресов, уметь их менять быстро, мониторить, что именно отвалилось.
У нас обход по умолчанию выключен. Приложение сначала пробует подключиться напрямую, и только если прямое соединение не проходит, поднимает туннель. Никакого смысла гонять трафик через relay там, где сеть и так открыта, нет.
Что стоит забрать из этого текста
Если вы решаете похожую задачу, три вещи, которые сэкономят вам время:
Прокси уровня приложения часто лучше системного VPN. Если вам надо довести до сервера только свой трафик, NEPacketTunnelProvider это лишняя сложность. Локальный inbound плюс connectionProxyDictionary закрывают задачу меньшими силами.
Протокол это лёгкая часть. VLESS + Reality + sing-box это решённая задача, всё уже написано до вас. Тяжёлая часть это инфраструктура: какие адреса вы используете, как быстро вы их меняете, как вы это всё мониторите.
Планируйте ротацию с первого дня. Не «когда-нибудь вынесем relay в конфиг», а сразу. Потому что первый же сгоревший IP покажет вам, есть у вас ротация или нет, и узнавать это в проде неприятно.
Всё описанное живёт в нашем мессенджере RCQ, сейчас он в открытой бете на iOS. Клиент под iOS у нас с открытым исходным кодом, так что при желании можно посмотреть, как именно устроена работа с транспортом, а не верить на слово:
Если занимаетесь чем-то похожим и набили свои шишки на инфраструктуре, расскажите в комментариях, особенно интересно про практику ротации адресов.
Комментарии (16)

art3012
23.05.2026 10:09Такой подход давно напрашивается в виде еще одного proxy в Telegram

rcq Автор
23.05.2026 10:09В Telegram уже есть свои MTProto-прокси для похожей задачи, но они маскируют именно Telegram-протокол. Reality универсальнее: его можно завернуть вокруг любого outbound-трафика. Поэтому в нашем случае это и сработало проще, чем городить свою обфускацию.

rcq Автор
23.05.2026 10:09UPD: задумывалось как ответ на коммент @art3012, промахнулся кнопкой. Дублирую в правильном треде.

pol_pot
23.05.2026 10:09Вы не ту проблему решаете. Мессенджер ценен только своей аудиторией. Ради людей которые там есть приходится терпеть и регистрацию по паспорту(телефону) и стикеры, и голосовухи, и (кремле)ботов.
У вас людей нет и никогда не будет. Сейчас не время для нового мессенджера, все силы надо направить на создание нового свободного интернета, делать такой впн который всем впнам впн, скайнет. Вот бы кто-нибудь взял и довел yggdrasil до ума.

rcq Автор
23.05.2026 10:09Network effect это правда, у любого нового мессенджера эта проблема. В том числе была у Telegram, Signal, Discord (и др.) когда они стартовали. Это не повод не пытаться, а повод делать аккуратно под конкретную аудиторию.
Yggdrasil это mesh IP-routing, замена транспортного слоя между узлами. Над ним всё равно нужен мессенджер: identity, ключи, доставка офлайн-сообщений, push, медиа. Это разные слои задачи, одно другое не заменяет.

K0Jlya9
23.05.2026 10:09Мессенджеры уже есть все какие нужны, и большие с миллиардами мух, и маленькие для групп селфхостед, и открытые без регистрации по паспорту и трекинга (jami итп), и какие угодно. Как вам вообще в голову пришло что надо сделать еще 1, для кого?

rcq Автор
23.05.2026 10:09Логика «все мессенджеры уже есть» одинаково применима к 2010-му (зачем Signal, есть WhatsApp), 2013-му (зачем Telegram), 2014-му (зачем Threema). Каждый раз ниша находилась.
Конкретно наша: люди в регионах с сетевой цензурой (РФ, СНГ, Иран), где WhatsApp/Signal/Telegram режут на сетевом уровне по TLS-фингерпринту, а Jami и подобные DHT-решения не доставляют офлайн-сообщения и не имеют встроенного обхода блокировок. RCQ закрывает именно это: libsignal + sealed sender + VLESS/Reality внутри приложения, без отдельного VPN.
Регистрация по UIN, без номера телефона и паспорта. Плюс Bluetooth-mesh работает в Иране во время shutdown-ов, когда интернета нет вообще.

sundmoon
23.05.2026 10:09Кажется, как раз логично было бы переиспользовать identity и прочую крипту от Yggdrasil. Если это вам почему-то не подходит, интересно было бы прочитать, почему именно.

rcq Автор
23.05.2026 10:09Хороший вопрос. Если правильно помню, у Yggdrasil identity это Ed25519-ключ узла, а IPv6-адрес узла однозначно из него выводится. То есть identity у них per-node (на каждую инсталляцию своя), и решает задачу «как одному узлу маршрутизировать пакеты другому, зная только его публичный ключ». Полезная штука, но довольно специфическая.
Для мессенджера нужно несколько вещей сверху, которых у Yggdrasil просто нет:
X3DH для асинхронного key agreement: signed prekey и пачка one-time prekeys лежат на сервере, чтобы Федор мог написать Яне, пока Яна оффлайн.
Double Ratchet: forward secrecy и post-compromise security на каждом сообщении. Yggdrasil-уровень это пакеты, понятия «сессия мессенджера» там в принципе не существует.
Sender Keys для эффективной групповой криптографии (без них каждое групповое сообщение приходится шифровать по числу участников).
Sealed sender для метаданных «кто кому».
И ещё момент: пользователь у нас один, а устройств может быть несколько, у каждого свой session-ключ. Yggdrasil-identity жёстко прибита к узлу, я не уверен, можно ли это туда нормально натянуть.
Поэтому я бы не сказал, что libsignal и Yggdrasil это альтернативы. Они скорее на разных слоях: libsignal закрывает крипту мессенджера, Yggdrasil (или Reality, или что-то ещё) сидит ниже и таскает байты. Кстати, Tox over Yggdrasil уже существует, и там Tox-крипта тоже осталась своя, Yggdrasil её не заменял.
Ну и честно говоря, переписывать X3DH + Double Ratchet + Sealed Sender с нуля втроём в свободное время мы бы не вытянули (даже продвинутый ИИ -не всесилен). libsignal проверен в Signal и WhatsApp, аудитов на нём прилично, так что взяли его не от любви, а из расчёта.

mitrillov
23.05.2026 10:09Из-за использования ИИ везде (от репозитория до ответов в комментариях здесь) и шаблонного описания преимуществ по модели «мы решаем проблему Х с помощью решения Y, которое лучше существующего решения Z» складывается ощущение, что мне пытаются этот мессенджер продать. А он бесплатный. Весело :)

rcq Автор
23.05.2026 10:09Да, ИИ используем активно, и в коде, и в текстах, а кто его сейчас не использует, интересно? Команда из трёх человек, базируемся в Израиле, у каждого основная работа в IT, RCQ это сайд-проект в свободное время. Шипить мессенджер втроём по вечерам без буста от LLM было бы очень медленно, поэтому и в репозитории следы видны, и в комментариях тон такой. Скрывать это смысла нет.
По «шаблонному описанию преимуществ»: техническая статья про обход блокировок неизбежно строится вокруг сравнения с альтернативами (MTProto-proxy, Shadowsocks, классический VPN). Без сравнения остаётся голый список технологий без объяснения, зачем они. По «продают, а он бесплатный»: исходники iOS-клиента открыты под AGPL-3.0 на github.com/rcq-messenger/rcq-ios, бэкенд крутится на нашем железе за наш счёт. Если бы продавали, был бы прайс-лист :)

vikarti
23.05.2026 10:09Андроида нет :(

rcq Автор
23.05.2026 10:09Пока только iOS, да. Тянуть две платформы параллельно втроём в сайд-проекте не получалось, поэтому Android осознанно отложен (но в процессе все равно). Протокол + бэкенд открыты, спецификация в репозитории, так что если кто-то из комьюнити начнёт Android-клиент раньше нас, поможем чем сможем. Сами вернёмся к Android, когда iOS-версия стабилизируется.
konkere
У меня только один вопрос: почему регулятор целенаправленно режет никому неизвестный мессенджер?
rcq Автор
Никак специально не режет. И статья этого не утверждает.
Реальность проще и хуже. РКН и операторы по его предписаниям фильтруют не «по продуктам», а по диапазонам и фингерпринтам. Когда блочат сеть провайдера, типовой SNI-паттерн или конкретный TLS-fingerprint, в этот фильтр попадают сразу десятки мелких сервисов которых никто индивидуально не «замечал». Это collateral, а не таргетинг.
Reality в этом контексте нужен ровно потому, что «правильная репутация» от такого collateral'а не спасает. Не важно знаменит ли ты, важно как твой ClientHello выглядит на DPI. Если как обычный мессенджер с cloud-IP, попадёшь в общий фильтр. Если как трафик к microsoft.com, пройдёшь.
То есть статья не про «РКН охотится за нами», а про «как сделать так, чтобы DPI вообще не различал что это мессенджер».
konkere
А, т.е. вы изначально поселились в таком диапазоне? Сорян, да