Если вы держите свой обход блокировок, у вас почти наверняка есть белый список. Не как отдельная сущность, а как последнее правило в конфиге: разрешить то, разрешить это, а дальше reject. Появляется он в первый же день, когда доходит, что открытый прокси в интернете живёт до первой жалобы хостеру, и с этого дня к нему не возвращаются, потому что он не просит: узлы добавляются, конфиг раздаётся, туннель поднимается, мониторинг показывает зелёное.
У нас в этом списке было пять адресов при семи живых узлах, и сколько недель это продолжалось, я до сих пор сказать не могу.
Что тут вообще меряется
RCQ это мессенджер, который обходит блокировки сам, без отдельного VPN: внутри sing-box, снаружи Reality. Флот на 10 августа состоял из 14 эндпоинтов на семи машинах у четырёх провайдеров, подписанный конфиг версии 144 клиент забирает одним запросом и раскладывает по urltest, который дальше сам решает, чем ходить.
С версии 0.80~ трафик идёт в два прыжка. Вход знает, кто вы, и не знает, куда вы идёте, выход знает куда и не знает кто. Схема известная, к нам она приехала после того, как стало ясно, что один релей видит и адрес клиента, и остров, на который клиент стучится, то есть ту самую пару, ради разрыва которой всё затевалось.
Двухпрыжковая схема требует одной вещи, которой однопрыжковая не требует вообще. Вход должен уметь дозвониться до выхода. А вход это такая же запертая машина с reject в конце правил, и разрешён на ней тот, кто перечислен поимённо.
Как это выглядело
Смотрел я совсем другое. Платный узел, за который человек заплатил деньги, не хотел становиться выходом цепочки: клиент маршрут собирал, urltest его отбраковывал, трафик уходил через публичные машины. Гипотеза была про приоритеты в клиентском коде, я полез в клиент, потратил вечер и всё это время смотрел не туда.
Правила с живого входа, адреса здесь и дальше заменены на буквы:
остров-1/32 → directостров-2/32 → directузел-F/32 → directузел-G/32 → direct(всё остальное) → reject
Пять адресов: два наших острова и два релея. Остальных пяти узлов флота в списке нет, а четыре из них стоят у одного провайдера, так что посчитать, чем это оборачивается для цепочки, можно прямо по этой пятёрке.
Дальше выяснилось, что списки на машинах ещё и разные:

Семь опубликованных узлов, из которых взаимно достижимы трое. Для бесплатного пользователя, чей вход оказался в первой группе, выходов в природе существовало два, и онион, нарисованный на семь машин, гонял его трафик через три.
Почему это не всплыло
urltest не жалуется на мёртвую цепочку, он её просто не выбирает. В этом и состоит его работа: собрать список маршрутов, замерить каждый, взять живые. Если из двенадцати цепочек девять не встают, туннель поднимется по трём оставшимся, и в логах не появится ни строчки, потому что ничего не сломалось, а выбор из меньшего количества вариантов ничем не отличается от выбора из большего.
Канарейка ходит снаружи и проверяет, что релей отвечает. Релей отвечал. Внешний зонд проверяет, что остров жив, остров был жив, и оба прибора в этот момент работали правильно.
Был и третий, написанный ровно для таких случаев. relay-lockdown.sh --check сверяет правила с тем, что на машине должно быть разрешено: маскарадный хост, зеркала подписанного конфига, DoH-резолверы, хост пробы. Флот он не сверял, и поэтому на каждой из семи машин лежал скрипт, печатавший ok: masquerade host <...> and every named mirror are allowed и формально не врущий ни в одном слове.

Ни один прибор не был сломан. Просто ни один не смотрел в эту сторону, и человека, который держал бы в голове всю картину сразу, тоже не было.
Открытые прокси, которые мы продавали
Раз уж полез, посмотрел заодно два приватных узла. Это арендованные машины: организация платит, узел принадлежит ей одной, в публичной выдаче его нет никогда, иначе аренда теряет смысл. Существуют они по арифметике, а не ради выручки: аренда стоит $60 в месяц, железо под ней около $15, а обычный публичный узел обходится в $5. Один платящий оплачивает девять~ узлов для тех, кто платить не может, и это единственный известный нам способ вырасти с семи адресов, которые статичны месяцами и потому закрываются за вечер при желании цензора (утрирую).
У обоих не было секции route. Ни белого списка, ни reject, ничего вообще, то есть кто держал ключ арендатора, тот ходил через нашу машину куда угодно.
Риск здесь бытовой. Абуза прилетает хостеру, хостер сносит машину, а машина оплачена человеком, который её и покупал ради того, чтобы его не отрезали.
Мелочь про SNI, которая всплыла заодно
Скрипт лок-дауна берёт маскарадный хост из живого inbound, так что когда я прогнал его по всем девяти машинам, на двух в правилах обнаружился www.apple.com.
Он остался с тех времён, когда мы ещё не поняли, что имя, которым прикрывается Reality, обязано жить на том же ASN, что и адрес машины. Крупное популярное имя с адреса случайного облака выдаёт все соединения разом, и стойкость рукопожатия тут ни при чём, потому что выдаёт не рукопожатие, а несовпадение владельца имени с владельцем адреса: такой join строится одним запросом к данным, которые и так у всех есть. Рабочий приём мы нашли не сразу, и это тема на отдельную статью; здесь важно только то, что правилам на двух машинах про смену имени узнать было неоткуда.
Что теперь
--check сверяет белый список с релеями из подписанного конфига и печатает недостающие адреса. Формулировка в коде получилась длиннее самой проверки, и я оставил её как есть, потому что через полгода объяснять это будет некому:
fleet: the signed config publishes […] and this relay rejects them, so no onion chain can use it as an entry to those exits. Nothing reports this — the chains just quietly never form.
Захардкоженный запасной список, на который скрипт падает, когда зеркало недоступно, вырос с трёх адресов до семи. Приватные узлы туда не попадают: адрес арендованной машины не должен ездить внутри конфига публичной.
Дальше проверка встала в крон на все девять машин, раз в полчаса, и чинит найденное сама. Отчёт, который никуда не уходит, мы уже пробовали: писем с этих машин никто не шлёт, а лог читают тогда же, когда вспоминают про белые списки.
Отдельно пришлось решать, что делает автоматический прогон, и здесь симметричное решение оказалось опасным. Соблазн понятный: сверил, привёл в соответствие. Но вход у скрипта это конфиг, скачанный по сети, и все девять машин ходят за ним по одному адресу с одинаковым интервалом, поэтому обрезанный ответ, перечисляющий три релея вместо семи, за полчаса приведёт к тому, что весь флот согласованно закроется друг от друга. Автоматический прогон умеет только расширять список. Худший случай теперь это разрешённый адрес выведенной из строя машины, что не стоит ничего, а сужение осталось ручной операцией, за которой кто-то смотрит. Новый конфиг перед заменой живого проверяется sing-box check, и если проверка не прошла, не меняется ничего.
Чего я не понимаю до сих пор
Когда это началось. Списки не датированы, узлы добавлялись в разное время, а дрейф не оставляет следа в логах по определению: каждая машина в момент своего лок-дауна была настроена правильно и с тех пор не менялась, разъехалось не её состояние, а мир вокруг.
В шапке того же скрипта, задолго до всей этой истории, написано: ⚠ A SNAPSHOT, and a dangerous one — a relay locked down from this branch rejects every node added since, which shows up nowhere. Предупреждение было, крона не было, и разъехалось там, где написано, что разъедется.
Если у вас свой флот и на нём есть цепочки, проверять стоит одну вещь: знает ли каждый узел адреса всех остальных. У нас на этот вопрос семь машин отвечали OK, и трое из них говорили правду.