На прошлой неделе я заметил, что у меня ни с того ни с сего перестала открываться в браузере часть зарубежных сайтов. Поначалу это не выглядело как массовое явление: хотел зайти на prometheus.io/docs – а он не открывается по таймауту. Ну ладно, думаю – прилёг, с кем не бывает. День лежит, второй лежит – уже выглядит подозрительно. При этом в процессе гугления каких-то совсем других проблем заметил, что также подвисают некоторые блоги.
Посмотрел повнимательнее – curl открывает сайты бодро, Firefox – тоже, а вот в Яндекс Браузере по прежнему таймаут соединения без каких-либо объяснений причин в сетевой вкладке Developer Tools.
Ну что ж, Developer Tools ничего не говорят – пойдёмте смотреть в Wireshark, что там такого интересного происходит…
SNI, ESNI, ECH, WTF?
Для начала краткий ликбез: когда мы устанавливаем HTTPS-соединение, то, несмотря на то, что весь HTTP-запрос у нас шифруется, имя домена в TLS handshake идёт plain text’ом, т. к. оно необходимо для того, чтобы сообщить балансировщику, какой именно сайт вы хотите открыть (и для какого домена балансировщик, терминирующий TLS, должен выдать вам сертификат). Данное расширение TLS, в рамках которого передаётся имя домена, называется SNI (Server Name Indication).
Именно благодаря SNI Роскомнадзор может селективно блокировать HTTPS-ресурсы не по IP-адресам (задевая при этом ещё кучу других сайтов), а по доменным именам.
Так вот, давайте посмотрим, что нам расскажет Wireshark, когда мы набираем prometheus.io в адресной строке браузера.
Видим два DNS-запроса:
DNS Standard query 0x6d3e A prometheus.io
DNS Standard query 0x169c HTTPS prometheus.io
- Стандартная
A
-запись – тут всё понятно и никаких сюрпризов; - А вот вторая необычная –
HTTPS
-запись. Я о такой даже и не слышал – как интересно. :)
Вот ответ DNS-сервера:
prometheus.io: type HTTPS, class IN
Name: prometheus.io
Type: HTTPS (65) (HTTPS Specific Service Endpoints)
Class: IN (0x0001)
Time to live: 277 (4 minutes, 37 seconds)
Data length: 136
SvcPriority: 1
TargetName: <Root>
SvcParam: alpn=h3,h2
SvcParamKey: alpn (1)
SvcParamValue length: 6
ALPN length: 2
ALPN: h3
ALPN length: 2
ALPN: h2
SvcParam: ipv4hint=104.21.60.220,172.67.201.240
SvcParamKey: ipv4hint (4)
SvcParamValue length: 8
IP: 104.21.60.220
IP: 172.67.201.240
SvcParam: ech
SvcParamKey: ech (5)
SvcParamValue length: 71
ECHConfigList length: 69
ECHConfig: id=131 cloudflare-ech.com
SvcParam: ipv6hint=2606:4700:3030::6815:3cdc,2606:4700:3030::ac43:c9f0
SvcParamKey: ipv6hint (6)
SvcParamValue length: 32
IP: 2606:4700:3030::6815:3cdc
IP: 2606:4700:3030::ac43:c9f0
А вот наш TLS handshake с балансером, за которым находится prometheus.io:
TLSv1 Record Layer: Handshake Protocol: Client Hello
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 512
Handshake Protocol: Client Hello
Handshake Type: Client Hello (1)
Length: 508
Version: TLS 1.2 (0x0303)
...
Extension: server_name (len=23) name=cloudflare-ech.com
Type: server_name (0)
Length: 23
Server Name Indication extension
Server Name list length: 21
Server Name Type: host_name (0)
Server Name length: 18
Server Name: cloudflare-ech.com
...
Ого, всё ещё интереснее: несмотря на то, что мы идём на https://prometheus.io/, самому серверу мы в SNI указываем какой-то cloudflare-ech.com, который как раз нам выдал HTTPS
DNS-запрос.
Идём гуглить – и узнаём, что HTTPS
DNS-запись призвана:
- Уменьшить latency загрузки сайта, т. к. сразу предоставляет
A
иAAAA
-записи, а также список поддерживаемых протоколов (HTTP/2, HTTP/3). - Увеличивает безопасность соединения при первичном открытии сайта, когда в браузере ещё нет закешированного HSTS.
- ECH stands for Encrypted Client Hello.
Когда-то давно я читал про ESNI (Encrypted Server Name Indication), который призван решить проблему plain text’овости домена в TLS, и даже не раз приходилось слышать, что Роскомнадзор блокирует ESNI как раз по той причине, то он не позволяет фильтровать HTTPS-трафик. Но вот ECH (Encrypted Client Hello), который по сути является логическим развитием ESNI, как-то проходил мимо меня – как что-то из ещё неопределённого будущего.
Почитав про ECH, мы узнаём, что схема работает следующим образом:
- Браузер запрашивает HTTPS DNS-запись.
- Данная запись помимо IP-адресов включает в себя публичный домен-заглушку cloudflare-ech.com + открытый ключ.
- При установке соединения с сервером, браузер в SNI (в открытой части Client Hello) передаёт ни о чём не говорящий cloudflare-ech.com, а в закрытой – уже в зашифрованном виде, настоящий домен.
- РКН видит это, расстраивается, что ему не видно конечный домен – и блокирует соединение.
Ну а, собственно, проблемы начались из-за того, что Cloudflare, за которым находится просто бесчисленное множество сайтов, недавно начал массово включать ECH.
Как обойти
Если хочется сохранить доступ к сайтам, находящимся за Cloudflare, не привлекая внимания санитаров не заворачивая куда-либо абсолютно весь трафик до него, то первая мысль, которая приходит в голову – это пойти и отключить ECH в настройках браузера. Но браузеров много, устройств много, а помимо браузеров есть множество других программ, которые рано или поздно тоже начнут поддерживать ECH.
Если же хочется какого-то более универсального решения, то достаточно вспомнить о том, с чего всё началось, а именно – с HTTPS
-записи. Поэтому решение, которое выбрал я (по крайней мере пока) – это просто взять и заблокировать HTTPS
DNS-запись на своём роутере. В моём случае это решается добавлением filter-rr=HTTPS
в конфиг dnsmasq.
Имея такую конфигурацию, наш DNS-сервер, находящийся на роутере, будет reject’ить запросы HTTPS
-записей, и браузер будет получать только обычные A
и AAAA
-записи, что вынудит его работать по старинке без всяких ECH. РКН будет видеть, что вы действительно заходите на незаблокированный ресурс и не будет резать соединение.