Reality dest IP leak: теория и подтверждение

Xray-core с транспортом Reality слушает порт 443 и имитирует TLS-поведение целевого сайта (dest). Когда подключается легитимный клиент с правильным publicKey и shortId - происходит штатный туннель. Когда подключается кто угодно другой - xray форвардит соединение напрямую на dest, чтобы со стороны всё выглядело как обычный HTTPS-сайт.
Это форвардирование - внутренний механизм ядра. Оно не проходит через routing rules и outbounds. Xray открывает TCP-соединение до dest напрямую через системный сетевой стек.

Теория атаки

Инфраструктура построена по схеме двойной цепочки: entry-нода в России принимает трафик, exit-нода на Западе выходит в интернет. Смысл - скрыть IP entry-ноды от конечных сайтов.
Но Reality на entry-ноде замаскирована под российский сайт (российский dest). РКН может попросить этот сайт логировать входящие TLS-подключения. Дальше - тривиально: бот стучится curl’ом на 443 entry-ноды, xray форвардит это на dest, российский сайт видит в логах IP entry-ноды.

Двойная цепочка ничего не даёт, потому что уязвимость срабатывает до того как трафик вообще попадает в туннель.

Для проверки теории поднят nginx с детальным access-логом на отдельном домене (limiter.[…]). Этот домен прописан как dest в realitySettings на entry-ноде ru-inbound-1.
Конфиг логирования nginx:

log_format detailed '$time_iso8601 | $remote_addr | $ssl_protocol $ssl_cipher | '
                    '$request | status=$status | bytes=$body_bytes_sent | '
                    'ua="$http_user_agent"';

После перезапуска xray с новым dest - в логах nginx появились записи:

2026-04-30T03:39:06+00:00 | 138.124.xxx.xx | TLSv1.3 TLS_AES_128_GCM_SHA256 | GET / HTTP/1.1 | status=200 | bytes=2 | ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0"
2026-04-30T03:39:06+00:00 | 138.124.xxx.xx | TLSv1.3 TLS_AES_128_GCM_SHA256 | GET /favicon.ico HTTP/1.1 | status=200 | bytes=2 | ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0"

DNS-резолв IP из лога:

Name:    ru-inbound-1.[...]
Address: 138.124.xxx.xx

Совпадение подтверждено. IP в логах - это именно entry-нода, а не exit-нода и не IP клиента.

Первое что мне пришло в голову:

Поднять отдельное ядро - “warp-router” контейнер с kernel TUN (noKernelTun: false(потому что без него, варп не запускается ни в какую). Перевести remnanode с network_mode: host на bridge-сеть, где default gateway - warp-router контейнер(443 и 2222 порт от ноды ввыести наружу). Тогда весь исходящий трафик remnanode, включая dest-форвард, автоматически идёт через WARP без каких-либо изменений в xray конфиге.

Но я не проверял, как появится время, проверю, и дам подтверждение.

А в общем хочу сказать, что это реально проблема, ибо мы делаем цепочки, стараемся скрыть входной адрес(особенно это критично если айпи из бс, а хостер при этом охотится на владельца такого айпи), но рандомный человек/бот/сканер может сдеанонить ваш входной сервак, и все, we are fucked.

(Текст в основном сгенерирован нейронкой)

То что сайт к SNI которого мы делаем реверс-прокси (Reality) видит наши проксируемые запросы это было очевидно всегда. Мой IP VPS так уже заблокировал тот сайт, пришлось менять SNI.

Сайты с шпионскими модулями куда мы проксируем запросы могут расценить это как спам / боттинг / скраппинг или парсинг. Коррелировать запросы – могут, и с WARP как прослойкой к SNI-сайту тоже – могут. Не обязан IP совпадать, обязано совпадать моменты логирования. Корреляция.

Как избежать этого – вопрос, лишь уменьшить шанс, избирательно выбирая сайт, наверное..

Да, время, и прочие совпадения могут помочь, но можно по максимуму затруднить им этот процесс.

  • спрятять айпишник
  • добавить искусственную задержку

Это уже может улучшить ситуацию

Именно поэтому и не рекомендует использовать Reality с внутренними сайтами. Покупайте свой домен и используйте свой TLS. Давно не проверял, но вроде Wireguard и другие “обычные впн“ не трогают внутри страны, поэтому их тоже можно рассмотреть.

Думайте не “РКН может попросить этот сайт логировать входящие TLS-подключения“, а “ТСПУ уже видит все, что происходит в сети внутри страны”. Нет необходимости просить кого-то. В теории XHTTP должен решить эту проблему.

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

Например, можно поднять свой Matrix homeserver (Synapse + Element Web) как идея для “что развернуть”. Домен купить у Cloudflare за 10$.

Вы правы, я проверял – работало, но долго я этим не пользовался всё равно (да и не для проксирования, а для другого вообще – истинной цели VPN, без прокси), для проксирования веб-трафик как-то спокойнее. Лучше таким VPN не злоупотреблять.

Внутри страны – да, внутри подсети того же хостера – вряд ли. А SNI мы, как правило, берем из той же подсети.

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

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

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

Ну я не из БС IP уж брал. Я вообще что-либо из БС сам брать не стал бы. Для постоянной инфраструктуры (не для БС) лучше обычный российский хостинг. А WG я всё равно пробовал на выходном IP, т.к логично экспериментировать всё на нём.

Опять же, злоупотреблять этим я крайне не советую. Использовать как VPN (без forward) – можно, для forward прокси – категорически нет, не для этого нам его оставляют.

Для БС использовать временные любые доступные мосты к своему центральному хабу. А все яйца ложить в эту карзину – гиблое дело.


Сначала нужно понять в чём модель угроз вообще. Не ясно, видит ли ТСПУ подсетевой трафик в том же хостинге или только на выходе. То есть стоит ли он до NAT или только на выходе.

И потом, шпионские модули могут ставить по идее лишь в аккредитованных компаниях. Если вы защищаете свой БС SNI IP – то почти наверняка трафик будет идти к крупной аккредитованной компании. А в общем случае – не обязательно.

Тогда сфокусируемся на частном случае, как предотвратить корреляцию запросов к своему IP и логов в ВК? Сервер, если он честный реверс-прокси обязан иметь почти такой же пинг, отличаться на 1-2 ms максимум, если он в той же подсети.

Спрятать айпишник это косметика. Анализировать могут долго и периодически. Но если мы все через WARP будем это делать, то в ВК будет литься огромный легитимный трафик с WARP как от пользователей, так и от Reality серверов. Тогда это усложнит корреляцию, если это крупный сервер. Но анализировать могут уникальными паттернами трафика, генерировать различные объемы в разное время. Будут ли это делать? Это спецоперация, как-будто вряд ли так будут заморачиваться.

Исскуственная задержка – тоже может выдать, ведь это получается целый треугольник. Слишком большая дельта различия пинга между основным сервером и реверс-прокси – тоже признак, если использовать WARP оно и будет тоже создавать задержку.

Тогда что?

Раз задержка между реверс-прокси в той же подсети – это, вероятно, аномалия, то можно хотя бы использовать выходной IP к Reality SNI, чтобы уж банальную корреляцию сломать. Так и IP напрямую для уж очень тупого шпионского модуля не светится, и аномалии с задержкой не будет.

Тогда да, в этом случае только Reality.

“Это форвардирование - внутренний механизм ядра. Оно не проходит через routing rules и outbounds.“. Это не так, на это можно повлиять. Вот пример конфига:

Summary
{
    "log": {
        "loglevel": "info",
        "dnsLog": true
    },
    "inbounds": [
        {
            "protocol": "tunnel",
            "port": 80,
            "settings": {
                "address": "www.vk.com",
                "port": 80
            }
        },
        {
            "protocol": "tunnel",
            "port": 4430,
            "settings": {
                "address": "www.vk.com",
                "port": 443
            }
        },
        {
            "protocol": "vless",
            "port": 443,
            "settings": {
                "clients": [],
                "decryption": "none"
            },
            "streamSettings": {
                "network": "xhttp",
                "security": "reality",
                "xhttpSettings": {
                    "host": "www.vk.com",
                    "path": "/xhttp",
                    "mode": "auto"
                },
                "realitySettings": {
                    "target": "localhost:4430",
                    "serverNames": [
                        "www.vk.com"
                    ],
                    "privateKey": "",
                    "shortIds": []
                }
            }
        }
    ],
    "outbounds": [
        {
            "tag": "direct",
            "protocol": "freedom"
        },
        {
            "tag": "block",
            "protocol": "blackhole"
        },
        {
            "tag": "warp",
            "protocol": "socks",
            "settings": {
                "servers": [
                    {
                        "address": "warp",
                        "port": 1080
                    }
                ]
            }
        }
    ],
    "routing": {
        "domainStrategy": "IPIfNonMatch",
        "rules": [
            {
                "domain": [
                    "geosite:private"
                ],
                "outboundTag": "block"
            },
            {
                "ip": [
                    "geoip:private"
                ],
                "outboundTag": "block"
            },
            {
                "domain": [
                    "geosite:category-ru"
                ],
                "outboundTag": "warp"
            },
            {
                "ip": [
                    "geoip:ru"
                ],
                "outboundTag": "warp"
            }
        ]
    },
    "dns": {
        "hosts": {
            "dns.cloudflare.com": "1.1.1.1",
            "dns.google": "8.8.8.8"
        },
        "servers": [
            "https://dns.cloudflare.com/dns-query",
            "https://dns.google/dns-query"
        ]
    }
}

dest не обязательно должен быть конечным сайтом, xray просто делает к нему запрос и ожидает получить в ответ TLS-сессию одного из serverNames. В данном примере все запросы к www.vk.com пойдут через warp outbound. Этот outbound может быть любым. Например, второй нодой с xhttp, а она уже в свою очередь шлет запрос через warp. xhttp нужен, чтобы сделать паттерны трафика ассиметричными и избежать ситуации, когда кто-то стучится на первую ноду и она сразу делает запрос куда-то.

Да, верно. Спасибо за уточнение

Да, это решение гораздо оптимальнее, спасибо.

Зачем? Ставим форвардирование на российском сервере на уровне ядра (iptables/nftables, например), а Reality под российский сайт - на иностранном. Тогда российский сайт увидит только иностранный IP адрес, входной ему вообще не виден будет.

Разве утечка не происходит уже на этом этапе? Сервер, на котором стоит Reality, должен обратиться к dest для получения временного сертификата.

Это всё равно что использовать Warp как прослойку к SNI сайту. Даёт аномальное различие RTT между Reality сайтом и основным (тот что резолвится по домену).

Тогда уж проще просто иметь два разных IP.

Почему так?

У вас входной IP из БС диапазона, очевидно он должен давать тот же RTT как и другой SNI-сайт в той же подсети.

Нет, он на лету генерирует сертификат, на то там и стоит пункт privatekey. Reality используется исключительно для защиты от зондирования.

Во-первых, есть документация. А во-вторых, это довольно просто исследуется. Отключаем все обходы, берём запрещённый SNI (этот же ntc.party, к примеру), курлим до другого IP в этом же subnet. Если запрос проходит без блокировки, то, значит, ТСПУ не стоит прямо у выхода сервера. Если нет - значит, стоит. В моём случае от белого резидентского IP до другого IP в этом же subnet блокировки нет, значит ТСПУ подальше находится.

Можно и так, тут придётся выбрать между полным скрытием входного AS или же скрытием различия RTT.

privateKey используется как раз для аутентификации вместе с shortId (чтобы сервер мог отличать настоящих клиентов от обычных “браузеров”). При этом сервер обращается к dest на каждое входящее соединение в процессе хендшейка - ещё до того, как определил, легитимный ли это клиент. Он использует структуру и параметры реального handshake dest, но подставляет клиенту временный сертификат вместо настоящего. Этот временный сертификат подписан с помощью privateKey таким образом, что проверить подпись может только легитимный клиент, знающий соответствующий publicKey.

источник

Хотя да, ты прав, если бы он не пытался стилить сертификат, то какой-нибудь фаерфокс не уведомлял бы, что этот сертификат выдает себя за нный сайт.

Тезис о том, что форвардинг в Reality — это «зашитый» механизм, который нельзя завернуть в прокси, не совсем верен. И в sing-box, и в Xray есть способы перехватить этот трафик и пустить его по нужному маршруту.

1. Решение в sing-box (Native way)

В sing-box это реализовано через параметры handshake и detour. Это позволяет явно указать, куда отправлять некорректные запросы (скрыть сервер за прокси или другим выходом).

Пример конфигурации:

JSON

{
  "inbounds": [
    {
      "type": "vless",
      "tag": "vless-in",
      "listen_port": 443,
      "tls": {
        "enabled": true,
        "reality": {
          "enabled": true,
          "handshake": {
            "server": "microsoft.com",
            "server_port": 443,
            "detour": "warp-out" // Весь "чужой" трафик уходит в нужный аутбаунд
          }
        }
      }
    }
  ]
}

2. Решение в Xray-core (Loopback way)

В Xray можно использовать «петлю» через протокол dokodemo-door. Мы направляем dest не во внешнюю сеть, а на локальный inbound, тем самым вводя трафик в таблицу маршрутизации.

Пример конфигурации:

  • В inbound Reality ставим: "dest": "127.0.0.1:5555"

  • Создаем дополнительный inbound:

JSON

{
  "tag": "reality-divert",
  "port": 5555,
  "listen": "127.0.0.1",
  "protocol": "dokodemo-door",
  "settings": {
    "address": "microsoft.com",
    "port": 443,
    "network": "tcp"
  }
}

  • Теперь в routing этот трафик можно перехватить по тегу и отправить куда угодно:

JSON

{
  "type": "field",
  "inboundTag": ["reality-divert"],
  "outboundTag": "chained-proxy-out" 
}

Итог: Как только трафик попадает на любой inbound (даже через локальную петлю), он перестает быть «невидимым» для ядра и подчиняется общим правилам routing и outbounds. Это позволяет гибко управлять поведением Reality при подключении нелегитимных клиентов.

Если задача — любой ценой защитить RU-IP и сделать так, чтобы провайдер даже не нашел на нем следов VPN-софта (Xray/sing-box), схема выглядит так:

  1. RU-VDS (РФ): На нем НЕТ VPN-сервера. Вообще. Там запущен только SSH-сервер. Для провайдера это просто пустая «виртуалка» для тестов или хостинга.

  2. EU-VDS (Европа): Весь VPN-стек (Reality и прочее) развернут здесь.

  3. Магия: EU-VDS инициирует соединение к RU-VDS и командой ssh -R 443:localhost:443 открывает (публикует) порт 443 на российском сервере.

В чем профит такой «инверсии»:

  • Чистый софт в РФ: На российском сервере нет конфигов Xray, нет приватных ключей Reality, нет логов подключений. Весь трафик, приходящий на RU:443, SSH-демон просто «выплевывает» в сторону Европы. Даже при физическом анализе сервера найти следы прокси-деятельности крайне сложно.

  • Инициация извне: Соединение (туннель) поднимает европейский сервер. Для ТСПУ это выглядит как входящий SSH из-за границы. Это самая легитимная активность, которую можно придумать для VDS.

  • Защита от сканирования: Порт 443 на RU-VDS открыт, но за ним стоит SSH-туннель до Европы. Если цензор попробует «простучать» этот порт своими методами, он будет биться о Reality-движок, который физически находится в EU, что ломает многие тайминги и логику локальных проверок.

  • IPv6: Если SSH-сессия между серверами идет по v6, то для провайдера «выхлоп» на 443 порту (v4) и управляющий канал (v6) вообще никак не связаны.

Краткий итог:

Клиент стучится на RU-IP:443 → трафик засасывается в SSH-туннель → обрабатывается на EU-VDS. Провайдер видит входящий SSH из Европы и какую-то возню на 443 порту, которая не коррелирует с запущенными на сервере процессами (кроме самого sshd).