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

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
LINUX DO - 热门话题
S
Secure Thoughts
TaoSecurity Blog
TaoSecurity Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
AI
AI
B
Blog RSS Feed
S
Schneier on Security
雷峰网
雷峰网
Schneier on Security
Schneier on Security
Help Net Security
Help Net Security
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
罗磊的独立博客
有赞技术团队
有赞技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
博客园 - Franky
Attack and Defense Labs
Attack and Defense Labs
The Cloudflare Blog
Webroot Blog
Webroot Blog
Last Week in AI
Last Week in AI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 叶小钗
美团技术团队
L
Lohrmann on Cybersecurity
T
The Blog of Author Tim Ferriss
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Vercel News
Vercel News
Know Your Adversary
Know Your Adversary
O
OpenAI News
博客园 - 【当耐特】
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cybersecurity and Infrastructure Security Agency CISA
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
PCI Perspectives
PCI Perspectives
H
Heimdal Security Blog
I
InfoQ
GbyAI
GbyAI
T
Threatpost
C
Cisco Blogs

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Прячем метаданные в мессенджере: 2-hop onion-lite поверх обычных VLESS + Reality relay, и почему это почти бесплатно
RCQ · 2026-06-16 · via Все публикации подряд на Хабре

В прошлые разы я разбирал, как мы встроили обход блокировок прямо в iOS-приложение: sing-box внутри бинарника, VLESS + Reality, relay как расходник, конфиг отдельно от сборки. Та статья закрывала ровно одну задачу: довести трафик мессенджера до сервера там, где прямое соединение режется.

Но в самом конце был честный абзац, который мне не давал покоя. Я написал тогда: туннель меняет то, как соединение выглядит для цензора по дороге, и не меняет того, кто стоит на концах. Оператор relay (в нашем случае мы сами) видит ваш трафик ровно так же, как его видел бы провайдер при прямом подключении.

Вот про эту дыру и будет текст. Она называется красиво: метаданные. На практике это означает, что один relay видит больше, чем должен. И мы её закрыли, не трогая боевой парк relay вообще. Без маркетинга, по делу.

В чём, собственно, дыра

Разложим, что видит один relay в нашей текущей схеме (single-hop, та самая, из прошлой статьи).

Клиент поднимает туннель VLESS + Reality (или Hysteria2) до одного relay. Внутри туннеля идёт обращение к острову (так мы называем сервер-бэкенд; островов может быть несколько). Содержимое переписки защищено сквозным шифрованием на уровне приложения (libsignal), это не меняется. Sealed sender прячет, кто именно отправитель. То есть что написано и кто написал, relay не видит.

Но relay видит другое, и это другое никуда не девается:

  • IP клиента (он же подключается напрямую к relay);

  • адрес острова назначения (его клиент запрашивает внутри туннеля, и relay этот запрос терминирует, чтобы переслать дальше);

  • тайминг (когда соединение поднялось, сколько висит, когда пошёл трафик).

Сложите это вместе. Один relay знает факт: «вот этот IP разговаривает вот с этим островом, вот в это время». Этого достаточно, чтобы привязать человека к сервису. Не к содержанию переписки, а к самому факту использования. Для пользователя под реальной цензурой факт использования бывает важнее содержания.

И вот ключевой момент. Relay это самая уязвимая точка периметра. Это коробка, стоящая на виду, с известным IP. Её можно изъять. Её оператора можно принудить. И как только это произошло, тот, кто получил relay, получает связку IP - остров - время по всем, кто через него ходил. Single-hop оставляет ровно эту дыру: одного скомпрометированного relay достаточно, чтобы слить метаданные.

Это становится особенно острым на следующем шаге нашего роадмапа: мы хотим открыть relay, которые держат волонтёры (мы зовём этот трек «гидрой»). Домашний relay волонтёра до onion видит IP, остров и тайминг, и при этом его можно изъять, а оператора принудить, всё тем же противником внутри страны. То есть он сольёт метаданные ровно по тем людям, которых должен защищать. Поэтому, забегая вперёд: сначала onion, потом гидра. К этому вернёмся в конце.

Модель: 2-hop onion-lite через вложенный VLESS + Reality

Решение структурное. Не «зашифровать ещё сильнее», а сделать так, чтобы ни один relay не видел оба конца сразу.

Берём не один relay, а два, и строим из них цепочку:

клиент ──VLESS/Reality──▶ ENTRY relay ──(непрозрачный туннель)──▶ EXIT relay ──▶ остров

Механика держится на одной возможности sing-box, которая называется detour. Клиент строит два исходящих (outbound), а не один. Outbound на EXIT (VLESS/Reality до relay B) помечен detour: onion-entry. Это значит, что соединение этого outbound до relay B не идёт в сеть напрямую, оно само заворачивается внутрь туннеля до relay A.

Адрес острова запрашивается во внутреннем, exit-слое, который умеет расшифровать только relay B.

Что в итоге видит каждый relay:

  • ENTRY (relay A) видит IP клиента и факт «надо доставить байты до relay B на :443». Он НЕ может прочитать адрес острова: тот лежит внутри Reality-туннеля relay B, а у A нет приватного Reality-ключа relay B. Для A это просто непрозрачный поток на соседний relay.

  • EXIT (relay B) видит IP relay A и адрес острова. Он НЕ видит IP клиента вообще, для него соединение пришло от entry.

Подчеркну важное: эта слепота криптографическая, а не на честном слове и не на правилах роутинга. ENTRY физически не может прочитать назначение, даже если очень захочет, у него нет ключа. Это не политика «мы обещаем не смотреть», это математика. Ни один relay не сводит источник и назначение вместе.

Почему это вышло почти бесплатно

Тут самое интересное, ради чего, как и в прошлый раз, стоило писать статью. Я ожидал, что онион потребует переделки парка relay. Оказалось, что нет. Совсем нет.

Relay не потребовали ни одной строчки переконфигурации. Наши relay уже используют direct outbound без route-правил (я перепроверил это, читая боевой конфиг). То есть relay просто пересылает байты туда, куда просит VLESS-слой клиента, и ему всё равно куда. Из-за этого:

  • ENTRY видит обычное VLESS-соединение, которое просит дойти до :443 другого relay. Для него это рядовой клиент, идущий на рядовой адрес.

  • EXIT видит обычное VLESS-соединение (с IP entry), которое просит дойти до острова. Тоже рядовой клиент.

Цепочка невидима самим relay. Они не знают, что стоят в контуре. Никто из них не в курсе, что является звеном онион-маршрута, для каждого это просто ещё одно соединение. Это два очень приятных следствия. Во-первых, ноль переконфигурации парка. Во-вторых, боевой парк вообще не подвергается риску во время раскатки: мы ничего на relay не меняем, вся новая логика живёт в клиенте.

flow: xtls-rprx-vision работает поверх detour. Вот этого я честно боялся. Vision сглаживает паттерн «TLS внутри TLS», и был резонный вопрос: а сохранится ли vision на exit-хопе, когда его сокет это туннель entry, а не настоящая сеть. Если бы не сохранялся, нам пришлось бы держать для relay, выступающих средним или выходным звеном, отдельный inbound-вариант без vision. Это была бы серьёзная морока. Проверили на sing-box 1.13: vision поверх detour работает. Exit-хоп держит vision, хотя его сокет это туннель entry. Никаких vision-less вариантов не нужно. Большое упрощение, и большое облегчение.

Доказательство: локальный прототип

Теория хороша, но мы её сначала собрали руками.

sing-box 1.13 на рабочем Mac, конфиг, который цепляет relay-do-fra (entry) → relay-oracle-il (exit, detour) → остров. Никаких изменений на боевом парке, обычный CLI:

curl --socks5-hostname 127.0.0.1:1099 https://api.rcq.app/health

→ HTTP 200 за 0.98s против 0.27s при прямом подключении.

Да, лишний хоп стоит латентности. Округлённо это 0.7 секунды накладных на хорошей сети. Для мессенджера это нормально: переписка не страдает от +0.7s на установление, а ради разрыва связки IP - остров это приемлемая цена. Главный вывод прототипа был даже не в числах, а в том, что цепочка собирается из обычных relay через detour и доезжает до острова, ничего не ломая по дороге.

Как это выглядит в конфиге

Вот ядро того конфига, что клиент собирает в рантайме, когда онион включён. Я убрал лишнее и оставил суть: entry, один exit, заворот exit через entry, и urltest сверху. Ключи и теги реальные.

{
"outbounds": [
{
"type": "vless",
"tag": "onion-entry",
"server": "ENTRY_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": "..." }
}
},
{
"type": "vless",
"tag": "onion-relay-oracle-il",
"detour": "onion-entry",
"server": "EXIT_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": "..." }
}
},
{
"type": "urltest",
"tag": "out",
"outbounds": ["onion-relay-oracle-il"],
"url": "
https://api.rcq.app/health",
"interval": "5m",
"tolerance": 50
}
]
}

Несколько деталей, которые тут несут смысл.

detour: onion-entry на exit-outbound это весь онион в одну строчку. Именно она заворачивает соединение exit внутрь туннеля entry.

utls с отпечатком chrome остаётся на обоих хопах: ClientHello каждого слоя должен выглядеть как Chrome, иначе vision и Reality на одном из хопов мимо.

Локальный inbound (его тут нет в обрезке) поднимается, как и в single-hop, на 127.0.0.1, порт 1089, тип mixed. Сетевой стек приложения заворачивается на него через тот же connectionProxyDictionary, который мы разбирали в прошлой статье. Со стороны приложения вообще ничего не меняется: оно как ходило в локальный прокси, так и ходит. Двухслойность живёт целиком в конфиге sing-box.

urltest сверху это не косметика, это центральная часть устройства цепочки. Про неё дальше.

Sticky entry guard: урок Tor

Если бы мы просто на каждом запуске выбирали entry случайно или гоняли urltest по всем relay как по entry, мы бы воспроизвели старую ошибку, которую анонимные сети прошли давно. Постоянная ротация входного узла это плохо. Пассивный наблюдатель, который умеет ронять соединения, может вынудить клиента перебирать входные узлы и тем самым увеличить шанс, что когда-нибудь клиент войдёт через узел, который наблюдатель контролирует.

Поэтому входной узел у нас залипающий. Это прямой заём идеи entry guard из Tor.

Логика sticky-входа одинаковая на iOS и Android (отличаются только имена ключей хранилища):

  • Входной узел выбирается ОДИН раз: самый приоритетный VLESS-relay из пула. Пул отсортирован по приоритету, так что это просто первый элемент.

  • Выбор персистится на устройстве (iOS: UserDefaults ключ rcq.singbox.onionEntryTag; Android: SharedPreferences ключ onion_entry) и переживает перезапуски приложения.

  • Между запусками entry НЕ перетасовывается. Если сохранённый тег ещё есть в пуле, он переиспользуется. Пассивный противник не может заставить клиента крутить входной узел.

А вот выходной узел крутится свободно. Тот самый urltest гоняет гонку между exit-цепочками (все VLESS-relay, кроме залипшего entry, каждый со своим detour), пробит https://api.rcq.app/health каждые 5m с допуском tolerance: 50. Победивший exit идёт в дело. Итог: exit ротируется сам по здоровью маршрута, entry стоит как вкопанный.

Залипший вход это не «навсегда». Он ротируется ровно в одном случае: вход подтверждённо заблокирован. Под капотом крутится health-loop. На Android это цикл в Session с задержкой 60_000ms: каждую минуту он пробит текущий маршрут, и если проба провалилась, инкрементит счётчик deadStreak. Когда deadStreak >= 2 (две провалившиеся подряд пробы, а не первый же чих сети) И rotateEntry() вернул true, транспорт останавливается, пул relay перечитывается, транспорт стартует заново уже с новым входом, а API- и socket-клиенты пересобираются, чтобы подхватить новый SOCKS-прокси. rotateEntry() round-robin переходит к следующему VLESS-relay по модулю и персистит новый выбор. Для онион-режима нужно минимум два VLESS-relay, иначе ротировать некуда и функция возвращает false.

На iOS петля само-лечения устроена иначе по реализации, но смысл тот же: ретраи раз в ~10 секунд, порог в шесть подряд провалов (те же примерно минуту), после чего вход ротируется и транспорт пересобирается. Одинаков на платформах именно сам guard (выбор и персист входа), а не петля.

Важная подробность: этот health-loop спит, пока онион выключен. На single-hop он не делает ничего. Сторожевой механизм просыпается только в онион-режиме.

Скажу честно про границу проверки. Сам путь rotate-on-block можно прогнать только вживую, на реально заблокированном входе: на эмуляторе мы проверили, что guard персистится и переживает перезапуск, но «вход умер, ротируемся» это уже когортное тестирование под реальной блокировкой, не лабораторный кейс.

Доставка конфига: флип без релиза в App Store

Здесь работает ровно тот же принцип, что я выводил в прошлой статье: то, что горит или меняется часто, не живёт в бинарнике. Только теперь он применяется не к адресу relay, а к самому факту «онион включён».

Конфиг relay у нас подписан Ed25519 и раздаётся по двум каналам (GitHub-raw как основной плюс Cloudflare KV как вторичный). Клиент его тянет и проверяет подпись против вшитого публичного ключа. Мы добавили в payload необязательный блок:

{ “onion”: { “enabled”: true } }

Поведение клиентов:

  • Блок необязателен. Если его нет, onionEnabled = false. Дефолт строго ВЫКЛ. Не «неопределённое состояние», а именно выкл.

  • Подпись покрывает весь payload, кроме самого поля sig. Канонический JSON у питоновского подписывальщика и у обоих верификаторов (iOS, Android) должен совпадать байт в байт: сортировка ключей, компактные сепараторы, неэкранированные слеши и не-ASCII. Любое расхождение в пробелах или порядке ключей ломает подпись. Плохая подпись не доверяется никогда: клиент откатывается на дисковый кэш, а потом на вшитый статический список relay.

Включить онион для когорты это:

# добавить в relays.yaml: onion: { enabled: true }
python tools/sign-relay-config.py tools/relays.yaml > /tmp/relay-config.json
wrangler kv key put --binding=RELAY_CONFIG config --path /tmp/relay-config.json

И всё. Клиенты подхватят новый подписанный конфиг при следующем обновлении и включат онион. Ноль релизов в App Store. Откат такой же мгновенный: пушим конфиг без онион-блока, клиенты на следующем рефреше возвращаются на single-hop. Тот же ключ Ed25519, что и для ротации relay, вшит в оба клиента: подпись неподдельна, флип неподделен.

У этого подхода есть одна тупость: ключ подписи один, поэтому флип бьёт сразу по всем, у кого этот ключ. Для первой когорты это слишком грубо. Поэтому есть второй, более тонкий рычаг: клиентский opt-in. Онион включается, если RelayConfigStore.onionEnabled ИЛИ персональный тумблер устройства (iOS: rcq.singbox.onionOptIn; Android: onion_optin). В настройках это переключатель «Onion routing (experimental)» в разделе Privacy & Network, по умолчанию выключен, доступен только когда обфусцированное соединение уже поднято. Волонтёр-тестер сам включает себя в 2-hop цепочку, не трогая подписанный конфиг вообще. Когорта из одного человека (моё собственное устройство) это теперь просто тумблер.

Честно про границы

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

Это ещё НЕ «лёгкий клиент». Пограничный хоп, тот, что пересекает границу, это по-прежнему клиент → ENTRY, а ENTRY это иностранный relay. Значит, этот хоп обязан оставаться тяжёлым (Reality / Hy2), это не лёгкая безобидная HTTPS-струйка. Видение «лёгкий клиент, тяжёлый relay → остров» требует ДОМАШНИХ входных relay (клиент → домашний это внутрибордерный лёгкий хоп; домашний → иностранный это тяжёлый хоп). Это трек гидры, и онион его как раз открывает, но сам онион лёгким клиентом нас ещё не делает.

Оба relay сегодня наши. Это значит, что МЫ как оператор по-прежнему видим оба конца, просто разнесённые по двум коробкам. Ни один отдельный relay не видит оба конца, но RCQ как оператор видит. Скажу прямо: онион рвёт связку для сетевых наблюдателей, не для нас самих. Реальный противник, от которого онион защищает СЕЙЧАС, это ОДИН изъятый или принуждённый relay, в том числе будущий волонтёрский relay гидры. От оператора, который контролирует оба relay и остров, онион внутри одной инфраструктуры не спасает. Это структурная, а не криптографическая граница, и её снимет только разнесение relay по разным независимым операторам.

Глобальный пассивный противник вне зоны действия. Тот, кто наблюдает большую часть сети одновременно и коррелирует тайминги, деанонимизирует и через нашу цепочку. Это не Tor, и я не буду называть это Tor-ом. Корректная формулировка: это «Tor-lite поверх нашего собственного пула». Полную несвязываемость против глобального пассивного наблюдателя онион не даёт, и от Tor её тоже не получить. Мы этого и не обещаем.

IP горят так же быстро. Лишний хоп не отменяет того, что оба relay так же обнаружимы блок-листами и DPI. Онион это не волшебное решение проблемы ротации адресов, это структурное изменение, которое разрывает связку метаданных, но добавляет латентность и удваивает число relay на пути. Связка между relay и островом сегодня это обычный TLS/HTTP, не зашифрованный под сквозным ключом приложения. Exit-relay видит plaintext маршрутные метаданные (получатель, тип, размер, время), хотя содержимое прочитать не может. Онион прячет связку IP - использование от сетевых наблюдателей, но не сам факт использования от relay.

Дефолт ВЫКЛ, раскатка поэтапная. Single-hop это безопасный пол. Каждый онион-клиент откатывается на single-hop, если онион выключен в конфиге ИЛИ если в пуле меньше двух VLESS-relay. Связность от онион никогда не становится хуже: в худшем случае это ровно тот single-hop, что был в прошлой статье. Сам флип когортный и под наблюдением: люди, которых это защищает, под реальной цензурой, и сломать им обход неаккуратной флит-вайд раскаткой недопустимо.

Что это открывает: почему сначала онион, потом гидра

Теперь к стратегии, ради которой всё затевалось.

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

Но открыть волонтёрские relay ДО онион было бы вредительством. Домашний relay волонтёра в single-hop видит IP клиента, остров и тайминг вместе. И этот же relay можно изъять, а его оператора принудить, всё тем же противником внутри страны. То есть мы бы своими руками расставили по стране коробки, каждая из которых сдаёт связку IP - остров по тем самым людям, которых должна защищать, плюс подставили бы волонтёра-оператора под юридический риск.

Онион это снимает. После онион даже изъятый домашний relay не сводит источник и назначение: как entry он не видит остров (нет ключа exit), как exit он не видит IP клиента. Связка криптографически разорвана. Только теперь можно открывать пул для чужих рук.

Поэтому порядок жёсткий: онион → broker (Lox-подобная раздача relay) → гидра. Никакой гидры до онион.

Что стоит забрать из этого текста

Если вы строите похожий транспорт, три вещи, которые стоит держать в голове.

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

Иногда нужная фича уже лежит в вашем стеке. 2-hop онион у нас это detour в sing-box плюс залипший вход плюс urltest по выходам. Мы не писали свою анонимную сеть. Самое дорогое открытие было приятным: relay не потребовали переконфигурации, а vision пережил detour, так что боевой парк во время раскатки вообще не трогался.

Залипайте на входе, ротируйте на выходе. Это урок Tor, и он применим к любой цепочке: постоянная ротация входного узла играет на руку наблюдателю, который умеет ронять соединения. Вход держите как guard, персистите его, ротируйте только на подтверждённой блокировке. Выход крутите свободно по здоровью.

И честно про границы, всегда. Онион внутри одной инфраструктуры защищает от ОДНОГО скомпрометированного relay, а не от оператора этой инфраструктуры и не от глобального пассивного наблюдателя. Если вы скажете пользователю больше, чем механизм даёт, вы соврёте человеку, который полагается на вас под реальной цензурой.


Всё описанное живёт в нашем мессенджере RCQ. Онион-стек собран на обеих платформах и лежит DEFAULT-OFF: single-hop это пол, на который всё откатывается. Клиент под iOS у нас с открытым исходным кодом, так что вместо «поверьте на слово» можно посмотреть, как именно собирается 2-outbound цепочка, как устроен sticky guard и как читается онион-флаг из подписанного конфига: github.com/rcq-messenger/rcq-ios.

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