Очередное исследование методов блокировок ТСПУ
Приветствую, коллеги. Тема с блокировками серверов не обошла меня стороной - и эта статья - мое личное исследование блокировок на ТСПУ
Пара важных оговорок
исследование проводилось с использованием нейронных сетей. сама статья написана мною. если вы очень остро реагируете на любой LLM-generated контент, просьба отойти от экранов (нет, статья не слоп, только натуральная тупость)
я понимаю, что таких статей написано много, но, поскольку каждая статья так или иначе дополняет коллективную базу знаний о блокировках и способах их обхода, не считаю, что делаю бесполезную работу. хотя, естественно, за вами остается право меня заминусить
все IP адреса заменены на адреса для документации согласно RFC 5737:
203.0.113.10для RU,198.51.100.20для EU (естественно, нейронной сетью)
Итак, одним прекрасным вечером, у меня сломалось все. Вообще все. Все, что совершенно прекрасно жило несколько лет, обходило все блокировки РКН, кроме белых списков (ладно, естественно, знатоки, увидев мой конфиг xray, написали бы примерно пару тысяч замечаний в ревью, но оно работало), цвело и пахло - умерло. Найдя в закромах конфиг какой-то старой AmneziaWG, которую хостил мой друг (по совершенной случайности оно все еще работало), пошел разбираться, что пошло не так.
Параллельно выяснилось интересное - туннель оборвался, но коннект телеги до сих пор жил. Сначала, подумал, что что-то сломалось на мобильном интернете. Доехал домой, подключился к WiFi - нет, не только на мобильном. Пошел на всякие сайты, которые позволяют посмотреть доступность ресурсов из разных регионов РФ - доступность околонулевая. Приплыли :(
Иду к хостеру, пишу тикет с просьбой добавить второй IPv4 адрес. Параллельно, ищу VDS с приемлимым прайсом, неограниченным траффиком и адекватным хостером (под адекватностью, в данном случае, подразумевается хотя бы наличие ДЦ, малина возле роутера у владельца хостера). На самом деле, это тоже тот еще адок - найти подходящий сервис, который бы меня всем устраивал, мне не удалось. Хостер на тикет не отвечает, сервер не найден, а пользоваться Амнезией как-то не очень - она не умеет в нормальный Split Tunelling и, по сути, для РКН становится максимально простой целью для блокировки. Делать нечего, идем разбираться, через что можно гнать трафик.
Прочитав статью, которая сейчас висит в топе про построение VPN понимаю, что точно нужны два разных IP-адреса. Конечно, оптимальным путем было бы иметь два интерфейса на сервере, но, напоминаю, что хостер до сих пор не отвечает ¯\_(ツ)_/¯ (как только ответит, скорее всего, так и сделаю) Пришлось колхозить тоннельчик. Точный метод фикса раскрывать не буду (по понятным причинам), но отмечу, что RU ходит к EU через посредника (со своим IP, который в РФ еще не забанили)
Итак, предисловие закончилось - пора к исследованию. Тут стоит отметить, что русский сервер находится за NAT провайдера и у него на интерфейсе висит IP из локальной сети - когда как у EU сервера на интерфейсе его собтсвенный IP.
Для начала, исключим очевидное: прокопал iptables, nftables, ufw и прочие штуки, которые могут ограничить коннект между серверами. Очевидно, ничего блокирующего не нашел. Потом, исключил отсутствие подключения к интернету впринципе - и с RU, и с EU совершенно спокойно открываются все незаблокированные (относится только к RU) ресурсы (1.1.1.1:443, 8.8.8.8:443 итп)
Определяем симптомы
Собственно, единственное, что мы на данный момент знаем - какое-то подключение между какими-то серверами нормально не работает. У меня, как у инженера, первым делом возникла мысль сделать `ping EU` из RU и `ping RU` из EU. И именно здесь кроется самая большая подлянка - ICMP пинги ходят совершенно спокойно. Проверяем дальше
U=198.51.100.20
ping -c3 -W2 $U | tail -1
# rtt min/avg/max/mdev = 47.020/49.781/53.694/2.843 ms
for p in 22 80 443 1984 54321; do # тут были реальные порты используемых сервисов, но они заменены в целях безопасности
printf "tcp %s: " $p
timeout 5 bash -c "exec 3<>/dev/tcp/$U/$p" && echo open || echo "fail rc=$?"
done
# tcp 22: fail rc=124
# tcp 80: fail rc=124
# ... и так на всех портахА вы думали совсем без слопа будет?)
Пояснения от клода: rc=124 значит, что сработал timeout: соединение не отказано, а просто висит. Ответов нет, и RST тоже нет. Похоже на тихий дроп. Трюк с /dev/tcp удобен тем, что не требует ни nc, ни curl: это встроенная возможность bash.
Собственно, симптомы ясны - не ходит ни TCP, ни UDP. Но ходит ICMP. Отсюда - закономерный вопрос: а что еще ходит? И можно ли как-то настроить интерконнект через ICMP?
Методы исследований
Самым полезным приемом оказалось одновременное использование tcpdump с двух сторон. Тут я, наверное, никого не удивлю - и не буду мучать вас всякими неинтересными вещами о том, где именно режется траффик. Для самых любопытных - под катом будет кусок, который я целиком и полностью скопипастил из нейронки, но предупреждаю - это вы, скорее всего, итак знаете - если следите (хоть сколько-то) за методами блокировок на ТСПУ.
Скрытый текст
На RU, исходящие SYN к EU:
06:44:39.665 IP 10.0.0.7.40514 > 198.51.100.20.443: Flags [S], seq 645398567 ...
06:44:40.679 IP 10.0.0.7.40514 > 198.51.100.20.443: Flags [S], seq 645398567 ... (ретрансмит)
06:44:42.695 IP 10.0.0.7.40514 > 198.51.100.20.443: Flags [S], seq 645398567 ... (ретрансмит)На EU за то же время ни одного TCP-пакета от RU. Приходит только ICMP. А теперь наоборот, EU стучится к RU. На RU:
06:44:59.648 IP 198.51.100.20.58142 > 10.0.0.7.443: Flags [S] seq 3956941491
06:44:59.648 IP 10.0.0.7.443 > 198.51.100.20.58142: Flags [S.] seq 1622934467, ack 3956941492
06:45:00.659 IP 198.51.100.20.58142 > 10.0.0.7.443: Flags [S] (ретрансмит, SYN-ACK не дошёл)
06:45:00.659 IP 10.0.0.7.443 > 198.51.100.20.58142: Flags [S.]SYN от EU доходит, RU честно отвечает SYN-ACK, но этот SYN-ACK до EU не добирается, и EU повторяет SYN.
Вывод: фильтр срабатывает на пакеты с адресом назначения EU, идущие со стороны RU. Направление EU → RU не фильтруется. Состояние соединения фильтру безразлично: режется даже ответ на чужой SYN.
Где именно
Сравним трассировку обычным ICMP и TCP SYN на 443-й порт:
$ sudo mtr -n -T -P 443 -c 3 -r -w 198.51.100.20
1.|-- 10.0.0.1 0.0%
2.|-- ??? 100.0
3.|-- REDACTED. 0.0%
4.|-- REDACTED. 0.0%
5.|-- REDACTED. 0.0%
6.|-- REDACTED. 0.0%
7.|-- ??? 100.0 <- дальше ничего
$ sudo mtr -n -c 3 -r -w 198.51.100.20
...
6.|-- REDACTED. 0.0%
7.|-- ??? 100.0
8.|-- ??? 100.0
9.|-- REDACTED. 0.0% 47.9 ms <- уже сеть немецкого хостера
10.|-- REDACTED. 0.0%
11.|-- REDACTED. 0.0%
12.|-- REDACTED. 0.0% 47.7 msTCP и UDP исчезают сразу после шестого хопа, ещё внутри приватной адресации облака. ICMP идёт дальше, до сети хостера и самого сервера. Важная оговорка: mtr показывает, где перестают приходить ответы, а не где стоит фильтр. Но картина однозначная: пакеты пропадают на выходе из российского сегмента, до границы с зарубежной сетью.
Блокируют IP или всю сеть?
Постучимся с RU к соседям EU по /24 и к роутеру хостера:
198.51.100.100:80 open
198.51.100.100:443 open
198.51.100.108:80 open
198.51.100.108:443 open
198.51.100.108:22 refused <- RST пришёл, значит пакет дошёл
192.0.2.14:443 refused <- роутер хостера тоже отвечает
198.51.100.20:* timeout <- только наш серверСоседи по подсети отвечают нормально. Блокируется ровно один адрес.
Промежуточный итог
TCP и UDP к одному конкретному IP тихо дропаются.
ICMP проходит.
Работает только со стороны России, обратное направление чистое.
Фильтр стоит на выходе из российского сегмента, свои файрволы ни при чём.
Это классический почерк блокировки по IP-адресу (ТСПУ). Учитывая, что EU работает как VPN-сервер, попадание его адреса в список неудивительно.
А что все-таки пролезает?
С помощью технологий аутсорсинга мозга, были собраны пакеты разных протоколов на Python'овских raw socket'ах. Результаты одной таблицей:
Что отправляет RU | Дошло до EU |
|---|---|
TCP, любые порты | ✗ |
UDP: любые порты | ✗ |
IPIP (4), 6in4 (41), SCTP (132) | ✗ |
GRE, внутри IPv4 (TCP, UDP или ICMP) | ✗ |
GRE, внутри IPv6 | ✓ |
GRE, внутри Ethernet-кадр с IPv4 (gretap, | ✓ |
GRE с неизвестным ethertype и мусором внутри | ✓ |
ESP (голый IPsec, без NAT-T), AH, экспериментальный протокол 253 | ✓ |
ICMP echo | ✓ |
Оговорка: IPIP, 6in4 и SCTP не прошли ни в одну сторону, так что, скорее всего, их не пропускает NAT облака, а фильтр тут ни при чём.
Самое любопытное здесь происходит с GRE - протоколом инкапсуляции туннелей. Признаюсь честно - непосредственно с ним я особо не знаком, поэтому этот кусок статьи полагается на объяснения нейронки, который я попытался переварить. Если я в чем-то не прав, не стесняйтесь поправить меня в комментариях.
Собственно, если внутри GRE передать какие-либо IPv4 адреса, ТСПУ просто не пропустит такой пакет - независимо от того, какие адреса передать внутри (даже 10.xx и даже если внутри ICMP)
А вот если внутри лежат IPv6 или Ethernet пакеты, трафик проходит, но душится. Отсюда можно сделать вывод, что ТСПУ либо не всегда нормально умеет в IPv6, либо просто считает его непонятным пакетом и по какой-то неведомой причине пропускает наружу.
Еще интересно проявляется ограничитель с ICMP-пингами (в примере, отправлял 100 пакетов в секунду по 1028 байт). Из 300 запросов, EU получил 49, а ответов из них вернулось только 44. В обратную сторону, RU получил от EU все 100% запросов.
Больше всего это похоже на, как его назвал клод, порог с «штрафной скамейкой»: когда ты превышаешь некий порог, остальные запросы к этому адресу по этому протоколу режутся целиком. Косвенно это подтвердил самый первый прогон на ~25 Мбит/с: сначала GRE с IPv4 внутри, потом ESP. Ни один пакет не пережил пересылку, даже ESP, который при нормальном темпе успешно пролезает. Отсюда, есть гипотеза, что штраф общий для всех "непонятных" пакетов к конкретному адресу. Если пересчитать все это в полезную нагрузку, получатся доли мегабита, которых хватит в лучшем случае на работу по SSH.
Итоги
Прежде всего, подтверждаем вывод, что РКН начал активно использовать точечную блокировку VPN по IP (что уже давно не новость)
Точечная блокировка распространяется даже на маленькие VPN-серверы "для себя и для друга"
Пожалуй, самое интересное наблюдение - трафик внутрь РФ не блочится. Это можно попытаться использовать для обратного траффика - траффик на сервер гнать через посредника (или на второй интерфейс), а вот траффик с сервера гнать по условному UDP.
Скрипты и прочая полезная информация, осторожно, нейрослоп
Проба разных IP-протоколов. Запускается от root на отправляющей стороне, на принимающей в это время работает tcpdump -Q in -nli eth0 "ip and src <адрес отправителя>".
import socket, sys, time
dst = sys.argv[1]
# «Голые» IP-протоколы: GRE, IPIP, 6in4, ESP, AH, SCTP, экспериментальный 253
for proto, name in [(47, 'gre'), (4, 'ipip'), (41, '6in4'), (50, 'esp'),
(51, 'ah'), (132, 'sctp'), (253, 'exp253')]:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, proto)
for _ in range(3):
s.sendto(b'\x00' * 4 + b'probe-' + name.encode() + b'.' * 40, (dst, 0))
time.sleep(0.05)
# UDP на набор «популярных» портов
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for p in (53, 123, 443, 500, 1194, 3478, 4500, 33434, 51820):
for _ in range(2):
u.sendto(b'probe-udp', (dst, p))
time.sleep(0.03)Нагрузочный тест GRE с IPv6 внутри: метка прогона зашита в младший байт внутреннего адреса источника (fd00::<tag>).
import socket, struct, time, sys
dst = sys.argv[1]
g = socket.socket(socket.AF_INET, socket.SOCK_RAW, 47)
def gre_v6(tag, n):
src = b'\xfd' + b'\x00' * 14 + bytes([tag])
dst6 = b'\xfd' + b'\x00' * 14 + b'\x02'
udp = struct.pack('!HHHH', 40000, 51820, 8 + n, 0) + b'U' * n
ip6 = struct.pack('!IHBB16s16s', 0x60000000, len(udp), 17, 64, src, dst6)
return b'\x00\x00\x86\xdd' + ip6 + udp # GRE: флаги 0, тип IPv6
for tag, (pps, count) in enumerate([(100, 500), (1000, 2000), (4000, 6000)], start=11):
pkt, t0 = gre_v6(tag, 1300), time.time()
for i in range(count):
g.sendto(pkt, (dst, 0))
while time.time() < t0 + (i + 1) / pps:
pass
print(f'tag={tag}: {pps} pps x {count}')
time.sleep(1)Подсчёт на принимающей стороне:
for t in 11 12 13; do
echo "tag=$t: $(grep -c "fd00::$(printf %x $t)\." capture.txt)"
doneKioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.