Я хожу по городу с телефоном, и на мобильном интернете половина того, чем я привык пользоваться, перестала открываться. Не случайно и не иногда: сеть периодически уходит в режим белых списков, и наружу проходит только то, что разрешено оператором. Дома на 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 — такие же разборы, только короче и чаще.