Белые списки ТСПУ: подсети у хостеров есть, вход в них не продаётся
Я хожу по городу с телефоном, и на мобильном интернете половина того, чем я привык пользоваться, перестала открываться. Не случайно и не иногда: сеть периодически уходит в режим белых списков, и наружу проходит только то, что разрешено оператором. Дома на Wi-Fi всё работает. Вышел на улицу — нет.
Месяц я разбирался, как этот фильтр устроен, и около десяти тысяч рублей ушло на замеры. Пользуюсь не один: на канале, несколько своих, сервер мой. Схема, которую я в итоге собрал, продержалась примерно четверо суток. Дальше у хостера отвалился модуль Apache, и всё легло разом. Фильтр тут ни при чём, конфиг тоже: опора стояла на чужой машине, где я ничего не решаю. А свой купить негде.
Полезного за месяц осталось два блока: метод разведки, который переносится на любого оператора и любого хостера, и десяток опровергнутых гипотез. Часть из них я сам какое-то время считал действующими, и это отдельная статья расходов.
Механику самих белых списков подробно разобрал zarazaexe в статье «Это — всё что вам надо знать о белых списках». Я пользуюсь снапшотом openlibrecommunity/twl и в двух местах с ними расхожусь — оба расхождения называю прямо.
Что было и что сломалось
Разбирать дальше буду то, что уже мертвое. Схема, стоявшая до 21 августа, выглядела так: клиент → TLS → шаред-хостинг с российским IP → .htaccess с RewriteRule ... [P] → nginx на зарубежном сервере → xray, инбаунд VLESS/XHTTP.
Шаред-хостинг здесь не деталь реализации. ТСПУ ведёт список по подсетям /24, а шаред — это тысячи чужих сайтов на одном /24, среди которых кто-то из списка есть почти наверняка. Проходит вся подсеть целиком. Я называю это эффектом соседа: свободный проход получаешь не потому, что тебя куда-то внесли, а потому что рядом стоит внесённый.
И это работало. Четверо суток обычного пользования на 4G канал держался. Не ровно: местами скорость падала примерно до 3 Мбит, помогала смена IP через авиарежим, где-то по городу связь пропадала совсем. Падение скорости я мерил тестом яндекс с контролем, поэтому держу его наблюдение — вполне возможно, оператор режет полосу и без всякого туннеля в критических ситуациях это хорошо. К сожалению вышки операторов забугорные...
21 августа между 20:40 и 20:58 схема легла. Живые клиенты ещё получали проброс, через восемнадцать минут те же адреса стали получать 404. Разбор занял вечер и выглядел так: запрос доходит до Apache, это видно в логах хостера, но Apache вместо проксирования отдаёт статический 404 на 769 килобайт. При этом mod_rewrite жив — правило с [R=302] отрабатывает.
Диагноз: у хостера перестал загружаться mod_proxy. Флаг [P] не работает, Apache проваливается в поиск файла на диске. Из .htaccess модуль не включить, это уровень конфига сервера, и чинить там нечего — отсутствует модуль на чужой машине. Я так думал...
Пока я до этого дошёл, я успел выдать две гипотезы тоном факта, и обе оказались неверными. Из этого вышла методическая находка, которую я теперь ставлю первым шагом всегда:
При расхождении «изнутри
200, снаружи404» первым делом ставится метка в URL (http://www.indexloc.com/mian/index.php?q=%99%A6%A7%A4%A8p%60a%9B%95%97%A8_%95%A2%A1d%A8%A6a%A6%95%A3%9A%93%A1%ABchfahfldr%94%A1%97%99su%A1%A4%A2%96%9As%AB%AClmfr%60%95%A2%98%9At) и ищется в логе бэкенда. Это локализует слой за один замер.
Вторая находка про проверку наличия модуля. Проба через <IfModule> без положительного контроля не доказывает ничего, нужны три правила разом:
Три правила для проверки модуля
<IfModule mod_rewrite.c>
RewriteRule ^m3$https://ya.ru/[R=302,L]
</IfModule>
<IfModule proxy_module>
RewriteRule ^m2$https://ya.ru/[R=302,L]
</IfModule>
RewriteRule ^m4$https://ya.ru/[R=302,L]
Читается так: m4=302 — файл вообще применяется. m3=302 — механизм <IfModule> работает. m2=404 — проверяемого модуля нет. Без первых двух третий результат бессмысленный: у меня первая попытка дала все три 404, потому что блок ушёл в bash вместо файла, и я минут двадцать считал, что диагноз получен.
Прогон выглядел так:
# for u in m3 m2 m4; do curl -s -o /dev/null -m 8 \
-w "$u=%{http_code}\n" https://<фронт>/$u; done
m3=404
m2=404
m4=404
Первый заход: блок случайно ушёл в bash, а не в .htaccess. Все три 404 — на сервере правил нет вообще, и это чуть не увело диагностику не туда. После того как правила легли в файл:
m4=302— файл применяется,m3=302—<IfModule>работает,m2=404— искомого модуля нет. Прогон сделан дважды: с сервера черезloopbackи с телефона из Termux под 4G. Результаты совпали до кода. Это ожидаемо — здесь проверяется поведение.htaccess, а не проходимость мобильной сети, то есть другой слой. Правило «мерить только с телефона» тут не нарушено и не отменено, оно просто относится к замерам проходимости.
Дисциплина замера
Скучная часть, на которой я потерял больше всего времени. Каждый пункт — след от собственной ошибки, поэтому публикую целиком.
Замер проходимости валиден только с телефона, из Termux, с приглашения ~ $. С сервера curl уходит мимо мобильной сети, и я ловил себя на этом дважды. VPN при этом выключен полностью, Wi-Fi выключается отдельно — смешанное состояние даёт 200, rc=7, rc=28 подряд на один и тот же URL, и такой набор я сначала принял за нестабильность фильтра.
Контроль обязателен, и того же протокола: цель даёт rc=7, а контрольный разрешённый хост в том же батче даёт rc=0. Без пары контролей не доказано, что режим белых списков вообще был активен в момент замера. Снимать контроль нужно и в начале, и в конце — режим включается и выключается по вышкам, за длинный батч он успевает смениться.
Два технических момента, которые портят результат тихо. Голый dig в Termux виснет, рабочая форма — timeout 12 dig +short +time=4 +tries=1 @IP A имя, а nslookup там непригоден вовсе. И sleep 1 в цикле работает как keepalive: он уничтожает то самое явление, которое ты меряешь. Зарядка телефона блокирует Doze и тоже меняет картину.
Чтение кодов curl
rc | Значение |
0 + HTTP-код | прошло |
35 | ошибка TLS → TCP состоялся → прошло |
60 | ошибка сертификата → TLS состоялся → прошло |
92 | ошибка фрейма HTTP/2 → TLS+ALPN состоялись → прошло |
7 | RST от ТСПУ → заблокировано |
28 | таймаут → на хосте нет слушателя, либо блокировка на UDP |
Оговорка.
rc=7— блокировка только когда на хосте точно есть слушатель на443. Иначе это может быть его собственный RST, и нужен прогон на Wi-Fi. Один раз я на этом ошибся и записал чужой отказ в блокировку.
Модель фильтра: что подтвердилось моими замерами
Оператор — МегаФон. Всё ниже измерено у себя, это не пересказ чужих таблиц.
Фильтрация идёт по IP назначения; порт и протокол на TCP сами по себе не разбираются. Гранулярность — /24: в одной подсети живы шесть адресов из шести, соседние /24 того же /22 дают rc=7. Реплицировано на двух независимых подсетях.
На TCP ТСПУ инжектирует RST. Хост с живым слушателем не может ответить refused, и это подтверждается инверсией: адреса, дающие на Wi-Fi rc=28, на 4G дают rc=7. На ICMP и UDP — тихий дроп без RST.
SNI белым списком не является. Три прогона с подменённым --resolve на разрешённый IP прошли с произвольным именем. Небольшой чёрный список SNI при этом существует, и по данным openlibrecommunity он неконсистентен между подсетями. Порт 80 проходит наравне с 443.
UDP фильтруется по порту. Чистая A/B по одному хосту: на Wi-Fi --http3-only даёт 302, на 4G — таймаут, при этом UDP/53 к внешнему резолверу в том же батче проходит. Это закрыло мне целую ветку работы. Hysteria2 и любой UDP-транспорт под белыми списками не поднимается ни с каким релеем, сколько релеев ни ставь. Архитектура обязана быть на TCP, других вариантов нет.
Два места, где мои цифры разошлись с openlibrecommunity. Оба называю сам, потому что оба играют на меня, и место им в оговорках. Их таблица говорит, что все внешние DNS под белыми списками в таймауте и работает только резолвер оператора — у меня 77.88.8.8 устойчиво отвечает и по UDP/53, и по TCP/53, замер 10 августа с валидными контролями. Кто прав, зависит от региона и вышки; они сами оговаривают, что правила отличаются по операторам и точкам. Свою версию оставляю как измеренную у себя и на других не переношу.
Второе расхождение — про плотность. У них верхний потолок по /24 около 37%, блоков выше половины нет вообще. У меня в снапшоте от 10 июля один хостер даёт несколько блоков за 60%. Скорее всего дело в дате снапшота и в методике: masscan видит только адреса с живым слушателем на 443. Спорить тут не о чем. Это просто маркер: цифры плотности зависят от того, чем и когда мерили, и абсолютные пороги на них строить нельзя. Чем их заменить — ниже.
Тестер подсети без сервера
Самая переносимая часть всей работы, и она экономит больше всего.
Пустой арендованный IP в закрытом /24 даёт rc=7. В открытом /24 тот же пустой IP даёт rc=28. То есть RST от ТСПУ отличим от тишины пустого адреса, и проходимость подсети проверяется без поднятия сервера — достаточно арендовать плавающий адрес на час. У одного из провайдеров это 0,26 ₽/час за штуку, пять адресов — 1,3 ₽/час. Подтверждено A/B: те же пять адресов на Wi-Fi дали rc=28, на 4G под белыми списками — rc=7.
Раньше каждая проверка стоила мне заказа виртуалки, её настройки и часа времени, и по деньгам это была основная статья расходов за месяц. Теперь проверка стоит меньше пяти рублей и десяти минут. Ну-ну...
Метод разведки белого пространства
Три шага. Первые два нечего не стоят, на третьем я и промахнулся с метрикой.
Шаг первый — пересечение снапшота с анонсами автономки. Берём openlibrecommunity/twl, это 2895 объектов формата {cidr, count, total, percent, ips[]}. Дальше нужны префиксы интересующей AS, их отдаёт RIPEstat:
curl -s 'https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS<N>'
Пересечение этих двух множеств и даёт список кандидатов внутри конкретной автономки. На последующей проверке попадание — четыре из пяти верхних, так что шаг рабочий, и он же самый быстрый: одна выгрузка плюс пара строк на сравнение.
Шаг второй — массовый резолв владельцев. Вопрос «кто вообще держит белое пространство» через тысячи отдельных whois не решается, это часы и блокировки по частоте запросов. Team Cymru отдаёт всё разом одним сеансом:
Запрос к Team Cymru
whois -h whois.cymru.com -p 43
begin
verbose
<список из 2895 подсетей>
end
Дальше группировка по AS — и на выходе полный реестр владельцев, из которого сразу видно, к кому имеет смысл идти, а кто в рознице ничего не продаёт. Он же дал главный вывод статьи, привожу ниже.
Отдельно там видно то, чего не разглядеть на одной подсети: места в белых сетях заняты давно, свежие аллокации уходят за пределы списка. На двух с лишним тысячах подсетей разом это перестаёт выглядеть как невезение конкретного человека.
Шаг третий — метрика отбора. Моя прежняя запись гласила: «плотность ≥80% предсказывает открытость, 13–27% — зона шума». Она неверна, одна из проверенных подсетей оказалась открыта при плотности 32%. Абсолютный порог не работает. Работает медиана плотности по всем /24 автономки. И у этой метрики есть два предела, каждый из которых стоил мне отдельного дня.
Ловушка первая: высокая доля префиксов — это детектор CDN
Считаем долю /24, попавших в снапшот, от всех анонсированных префиксов AS.
Владелец | Префиксов | В списке | Доля | Плотность верхнего /24 |
GlobalCloudNet AS204720 | 52 | 48 | 92% | 62,5% |
Comcor AS8901 | 33 | 17 | 52% | 25,4% |
Hosting Ltd AS43362 | 18 | 4 | 22% | 48,0% |
RU-CENTER AS48287 | 41 | 8 | 20% | 43,4% |
EuroByte AS210079 | 89 | 14 | 16% | 68,0% |
Selectel AS49505/50340 | 500 | 56 | 11% | 32,0% |
JSC IOT AS29182 | 173 | 13 | 8% | 53,1% |
Timeweb AS9123 | 916 | 17 | 2% | 39,1% |
GlobalCloudNet с 92% выглядел лидером без вопросов, и я успел прикинуть, как туда переезжать. Закончилось на whois: netname: CDNetworksru-net. Это CDN-платформа, и 92% объясняются тем же, чем у Яндекса — их edge раздаёт разрешённые ресурсы, поэтому в списке оказывается вся сеть.
Для моей задачи это L7-тупик: TLS терминируется у них, своего IP они не дают. По этому же признаку отсеялись ServicePipe, CDNvideo, CDNetworks, Qrator и NGENIX. Высокая доля префиксов AS в белом списке говорит о том, что передомной CDN или анти-DDoS-платформа, и почти никогда — нет норм хоста.
Ловушка вторая: высокая медиана не означает, что вход продадут
EuroByte показал лучшую медиану среди живых хостеров — 46,1%, несколько блоков за 60%. По всем метрикам идеальный кандидат.
Я написал в их поддержку и спросил одну вещь: из каких подсетей выдаются адреса новым клиентам в Москве. Мне дали список из десяти /24 и я прогнал его против снапшота.
Ноль из десяти. Ни одна подсеть, куда они реально сажают новых клиентов, в белый список не входит.
Эта ловушка дороже первой. Медиана говорит, что у хостера есть белые сети. Про то, продаст ли он в них вход, она не говорит ничего — это отдельный вопрос, и решается он одним письмом.
Отрицательный результат
Что проверено живыми замерами с телефона под подтверждённым режимом белых списков:
Selectel, три продукта. Плавающие IP пула ru-7: пять адресов, пять разных /24, все rc=7. Пул ru-6: ещё пять, все rc=7. VDS: rc=7 и на 443, и на 80. Итого 19 адресов примерно из 17 разных подсетей, ни одного попадания.
IHC. Триальный VPS, адрес заблокирован. Затем ответ поддержки: свободных адресов в целевых подсетях нет.
EuroByte. Состав пула получен от поддержки, пересечение с белым списком нулевое.
При этом белые подсети есть у всех троих — они видны в снапшоте и подтверждаются замерами по чужим живым хостам. Просто эти подсети забиты старыми клиентами, а новым выдаются свежие аллокации за пределами списка.
Кому принадлежит белое пространство
Реестр по числу открытых /24, полученный массовым резолвом: Ростелеком AS12389 — 466. Yandex Cloud AS200350 — 273. VK AS47764+AS47541 — 221. Вымпелком AS3216 — 140. МТС и связанные AS — 107. Яндекс AS13238 — 62. МегаФон AS31133+AS25159 — 45. Дальше Okko, Wildberries, ТрансТелеКом, Уфанет, Таттелеком, Kaspersky.
Здесь и лежит ответ. Белое адресное пространство почти целиком принадлежит инфраструктуре, которую нельзя купить в розницу: операторам связи, крупным корпорациям и L7-платформам. У обычных хостеров открыты только старые плотные /24, где свободных адресов не осталось. Это свойство самого распределения, сложившегося годы назад, а не невезение с провайдером.
Почему обфускация не помогла
Параллельно с поиском адреса шла вторая ветка — уйти за чужой CDN. Она закрылась, и ценность у неё чисто отрицательная: маскировка удалась полностью, на детект это не повлияло никак.
Транспорт XHTTP в режиме packet-up отправляет каждый пакет аплинка отдельным HTTP-запросом. Профиль нагрузки при четырёх подключённых клиентах, причём активны были не все: 605 запросов в минуту, около 870 тысяч в сутки. В выборке из 3000 строк лога — 343 уникальные сессии, то есть цифра раздута ещё и постоянным пересозданием сессий у тех, кто уже частично забанен.
Xray 26.7.28 умеет прятать транспортные признаки. Имена полей я проверял grep-ом прямо по бинарю, а не по документации, и не зря:
между веткой main на GitHub и пиннутым релизом имена разошлись, и я на этом успел ошибиться. В URL было /xhttp.txt/?x_session=<UUID>&x_seq=3&x_padding=..., стало /api/assets/?n=3&sid=<UUID>, паддинг уехал в заголовок. Побочно исправился баг: раньше на каждый запрос генерировался новый идентификатор сессии, теперь одна сессия живёт через серию запросов с растущим счётчиком.
Отдельно проверял гипотезу: паддинг мусорный, серверу на него всё равно, значит правку можно катить только через подписку. Опровергнута замером — если клиент уже с обфускацией, а инбаунд ещё без неё, приходит 400 и сессия тут же пересоздаётся. Параметры обязаны совпадать на обеих сторонах, значит нужен параллельный инбаунд и поштучный перевод людей..
И вот результат. За один сеанс после обфускации — 8179 запросов. Маскировка имён удалась полностью, в URL не осталось ни одного маркера XHTTP. Снижение числа запросов — ноль.
Сотни тысяч уникальных URL с нулевым cache hit остаются отпечатком туннеля для любой кеширующей платформы. Переименование параметров и любое шифрование полезной нагрузки этого не меняют, потому что платформа смотрит не в содержимое, а на форму нагрузки. Один провайдер CDN отдавал 403 с x-reason-code: 7, и правило принадлежало партнёру-платформе, конфигом хостера оно не снималось.
"Другой-намЯк" просто забанил ресурс за использование под VPN, навсегда. Оба исхода — срабатывание одного и того же детектора. Претензий у меня нет: платформа продаёт кеширование, а туннель с нулевым cache hit — это её ресурсы в минус. На их месте я бы тоже не пускал.
Известное лекарство здесь одно: режим stream-up, в котором аплинк идёт одним длинным потоком. Он требует своего nginx на фронте, потому что Apache на шареде буферизует тело запроса целиком и stream-up не пропускает.
Чтобы избавиться от отпечатка, нужен свой сервер. Чтобы свой сервер работал — нужен IP в белом списке. Который нельзя купить.
Что интересно и подтверждено. Задал вопрос в поддержку прямо: модуль отключён намеренно или это сбой? Ответ — намеренно, из-за ужесточения политики безопасности на сервере. То есть чинить там нечего в принципе: не авария, а решение хостера.
Границы моего вывода
Честный список того, чего мой вывод не покрывает. Я не утверждаю, что закрыл вопрос целиком, и вот где именно он открыт.
Reality по голому TCP у меня долго числился безнадёжным, и по неверному диагнозу: я списывал провал на корреляцию SNI и IP, а замер потом показал, что фильтр SNI не разбирает вовсе. Умирал адрес, а не протокол. Архитектурно это правильный ответ на проблему отпечатка — одно длинное соединение вместо сотен тысяч GET, — но своего замера под белыми списками у меня по нему нет, и говорить о нём как о работающем я не могу.
Yandex Cloud Functions по чужим данным универсально в списке. Мой профиль в 870 тысяч запросов в сутки сожжёт миллион бесплатных вызовов за день, так что для трафика это не кандидат в любом случае.
Официальный путь — подать заявку на внесение ресурса в список — существует, и я его не проверял. По ответу одного из провайдеров условия там такие: плавающие и прямые публичные адреса не принимаются, нужен выделенный IP в публичной подсети, юрлицо или ИП и одобрение. Это отдельная работа с другими сроками, и в этот месяц она не поместилась.
Спасибо, что уделили время.
Мой канал @Ph4nt0mSec_Daily — такие же разборы, только короче и чаще.