惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
小众软件
小众软件
The Cloudflare Blog
S
SegmentFault 最新的问题
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
阮一峰的网络日志
阮一峰的网络日志
博客园 - 聂微东
V
Visual Studio Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网
WordPress大学
WordPress大学
T
Tailwind CSS Blog
腾讯CDC
人人都是产品经理
人人都是产品经理
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
M
MIT News - Artificial intelligence
D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
P
Proofpoint News Feed
J
Java Code Geeks
宝玉的分享
宝玉的分享

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Обход блокировок внутри iOS-приложения: VLESS + Reality ч...
rcq · 2026-05-23 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение6 мин

Охват и читатели0

Кейс

Мы делаем мессенджер. Весной 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 у нас с открытым исходным кодом, так что при желании можно посмотреть, как именно устроена работа с транспортом, а не верить на слово:

github.com/rcq-messenger/rcq-ios

Если занимаетесь чем-то похожим и набили свои шишки на инфраструктуре, расскажите в комментариях, особенно интересно про практику ротации адресов.