Недавно я летел из Гонконга в Хитроу рейсом British Airways. Тот же самый маршрут я проделал в 2023 году, и помню, как в 14-часовом путешествии понадеялся на развлечения в самолёте. Однако на этот раз по дороге в Лондон у компании появилось интересное предложение: бесплатный WiFi для «мессенджеров» участникам «The British Airways Club».

Я был практически уверен, что не состою ни в каком подобном клубе (да и летел я эконом-классом), но оказалось, то это всего лишь название программы для часто пользующихся услугами компании. Довольно удобно было то, что можно зарегистрироваться в программе через портал авторизации, находясь прямо в воздухе; и хотя он требует адрес электронной почты, его не нужно верифицировать (то есть можно завершить регистрацию без доступа к Интернету).

После входа портал предложил мне «Начать сессию», и это действительно позволило мне общаться текстом. Я попробовал Whatsapp, Signal, Wechat и Discord. Первые три работали (но без поддержки изображений); Discord, как и ожидалось, не заработал. Совсем неплохо для бесплатного WiFi!

Откуда он знает?

Это первый вопрос, который возник у меня после проверки работы мессенджеров. На дворе 2025 год, всё должно шифроваться при передаче. Почему же система знает, что я использую Whatsapp или Discord? Поначалу я решил, что соединение как-то ограничивает ширину канала/передачу данных отдельных TCP-соединений, поэтому при отправке одного-двух сообщений они проходят, но что-то большее отклоняется.

Чтобы проверить эту гипотезу, я попробовал открыть на телефоне классический example.com. К сожалению, он не загрузился; значит, всё это устроено немного сложнее…

К счастью, у меня с собой был ноутбук, поэтому я подключился к WiFi, открыл в Devtools вкладку Network и для верности дополнил всё это Wireshark. После повторной регистрации для подключения к WiFi настало время экспериментов. При открытии чего-то наподобие example.com в Wireshark сразу после Client Hello отображался сброс TCP, поэтому я сразу подумал об SNI. Этот аспект спецификации TLS очень меня раздражал, потому что он широко используется Интернет-провайдерами в Индии для блокировки веб-сайтов (впрочем, для устранения этой проблемы есть ECH (который сам ранее был ESNI)).

Если говорить вкратце, то SNI раскрывает доменное имя КАЖДОГО веб-сайта, к которому подключается пользователь в TLS handshake, до создания туннеля! Хотя само содержимое того, что вы делаете, например, на totallynondodgywebsite.com, зашифровано, любой, следящий за трафиком, будет знать, к кому вы подключились (в том числе и Интернет-провайдеры). Я предположил, что у авиакомпании есть белый список доменов, используемых мессенджерами, и если они видят что-то другое, то сбрасывают подключение.

Примечание: когда я пытаюсь объяснять это, реакции людей сильно разнятся. Многие из моих далёких от техники друзей думают, что все действия без VPN видны кому угодно, а чуть более продвинутые знакомые считают, что видны URL (в том числе и параметры запросов), но не ответы. Кроме того, есть подмножество людей, веривших, что использование TLS означает шифрование всех данных между клиентом и сервером; они понятия не имели, что SNI создаёт утечку посещаемых ими доменов!

Проверяем нашу теорию

Хотя British Airways блокирует DNS-запросы ко всем публичным ресолверам (по крайней мере, к тем, которые я смог вспомнить), система всё-таки ресолвит любой переданный ей домен, том числе и записи MX, TXT, HTTPS. (Само по себе это может стать интересной сферой исследований, особенно потому, что DNS-ресолвинг может срабатывать до регистрации для доступа к бесплатному WiFi. Можно придумать что-нибудь наподобие произвольных поддоменов, представляющих полезную нагрузку запроса, и специальный сервер имён, возвращающий ответы через запись TXT или что-то подобное. Ну да ладно, двинемся дальше…).

Получив запись A моего личного сервера, я выполнил TLS handshake напрямую с IP-адресом без SNI. Он был сброшен British Airways; то есть отсутствие SNI тоже блокируется!

$ openssl s_client -connect 95.217.167.10:443
Connecting to 95.217.167.10
CONNECTED(00000003)
write:errno=104
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 302 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Protocol: TLSv1.3
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---

Дальше я попробовал протестировать некоторые SNI, которые могли бы удовлетворить систему. Я помнил, что Whatsapp для чего-то использует wa.me, поэтому решил проверить его. SNI работает так: сообщает серверу, к какому хосту мы хотим подключиться, чтобы он мог предоставить нужный сертификат TLS. В моём случае мой личный сервер не имел никаких сертификатов для wa.me, но NGINX, похоже, просто игнорирует SNI, если он не существует, и возвращает первый сертификат (мне так кажется; это может быть связано и с моей конфигурацией, но особо этот вопрос я не исследовал).

Однако, по сути, если я (клиент) не возражаю, можно выполнить TLS-соединение для любого произвольного сертификата, предлагаемого мне сервером, даже если в SNI я указал домен, который не контролирую (например, wa.me).

$ openssl s_client -connect 95.217.167.10:443 -servername wa.me
Connecting to 95.217.167.10
CONNECTED(00000003)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=R3
verify return:1
depth=0 CN=mijia.mywaifu.best
verify error:num=10:certificate has expired
notAfter=Jul 22 13:03:02 2023 GMT
verify return:1
depth=0 CN=mijia.mywaifu.best
notAfter=Jul 22 13:03:02 2023 GMT
verify return:1
---
Certificate chain
<выр��зано>

Ура! Использование SNI Whatsapp позволило обманом убедить British Airways, что я пользуюсь мессенджером, поэтому был создан TLS-туннель. Так как я подключён к серверу, чтобы убедиться, что это сработало, я написал в сокете запрос HTTP/1.1, воспользовавшись заголовком хоста реального веб-сайта моего инстанса NGINX

GET / HTTP/1.1
Host: saxrag.com

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 09 May 2025 19:14:46 GMT
Content-Type: text/html
Content-Length: 4968
Last-Modified: Wed, 09 Apr 2025 07:52:54 GMT
Connection: keep-alive
ETag: "67f62756-1368"
Cache-Control: no-cache
Accept-Ranges: bytes
<вырезано>

Мне успешно удалось запросить и получить в ответе мою домашнюю страницу! Все её 5 КиБ. Неплохо! Теперь нужно расширить этот способ, чтобы можно было заходить на любой веб-сайт…

От ненависти до любви

Нет, мои отношения с SNI нельзя назвать столь клишированными; я считаю, что мы всё ещё враги, зато он позволяет нам воспользоваться восхитительными возможностями. Если мне удастся убедить систему, что я подключаюсь к wa.me, то потенциально я смогу делать в этом соединении всё, что угодно (под маской «работы с мессенджером»). Итак, мои требования таковы:

  • Установить TLS-соединение при помощи SNI wa.me

  • Туннелировать произвольный трафик через это соединение

  • Делать всё это, не владея доменом wa.me и не контролируя его

Из моего предыдущего опыта реверс-инжиниринга и тому подобного я знал, что наиболее очевидным способом будет HTTPS-прокси. Это должен быть именно HTTPS, потому что именно соединение с прокси я и буду выдавать за Whatsapp. Если TLS handshake с HTTPS-прокси будет иметь SNI wa.me, то система должна пропустить его, после чего мы сможем выполнять реальные запросы через этот прокси.

К сожалению, я находился в воздухе, поэтому у меня не было удобного доступа к Интернету для управления моими серверами, а значит, подобное я настроить не мог; придётся проделать это на праздниках и протестировать на обратном рейсе. Я мог попробовать эмулировать ограничения British Airlines и так далее, находясь на земле, но я решил рискнуть.

Подготовка

Мне удалось найти один из моих VPS, который ещё не использовал порт 443. Допустим, публичный IP был 333.333.333.333 (да, я знаю, что октеты не могут быть больше 0xFF, так я решил спрятать свой IP). Затем я настроил на нём HTTP-прокси при помощи tinyproxy. Однако так мы создаём простой HTTP-прокси, слушающий 127.0.0.1:8080.

Для добавления слоя TLS я использовал stunnel. Для настройки TLS в stunnel я просто сгенерировал через openSSL несколько подписанных мной сертификатов, использовав стандартные параметры, за исключением common name (CN), в качестве которого я указал wa.me, потому что стремился обеспечить максимальную совместимость (например, чтобы клиент не отклонял соединение из-за неожиданного SNI относительно CN, и чтобы сервер знал, какой сертификат предоставлять).

openssl req -nodes -newkey ed25519 -keyout ssl.key -x509 -days 365 -out ssl.crt

Дополнение: на самом деле, в клиенте я решил игнорировать ошибки TLS (подписанный мной сертификат), а stunnel не волновал SNI, поэтому этот (CN) был не особо важен. Но в более легальных сценариях использования он определённо необходим!

Тестируем

Чтобы убедиться, что прокси работает, как задумано, я попробовал использовать его через curl непосредственно с IP:

$ curl -x https://user:pass@333.333.333.333:443 ifconfig.co -v
*   Trying 333.333.333.333:443...
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate
* closing connection #0
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.

Разумеется! Я просто случайным образом сгенерировал на моём VPS сертификаты, неподписанные «надёжным» CA. Что ж, мы можем попросить cURL игнорировать ошибки TLS для прокси при помощи флага --proxy-insecure, после чего всё заработает; ответом становится IP моего VPS.

Однако есть проблема — если я подключаюсь к прокси напрямую через IP, расширение SNI не задаётся, поэтому подключение будет заблокировано. Расширение SNI задаётся, когда подключение выполняется к домену, поэтому мне нужно сконфигурировать wa.me так, чтобы он указывал на 333.333.333.333. Разумеется, это можно сделать при помощи файла hosts, но в cURL есть и удобный хак в командной строке при помощи --resolve:

curl --resolve wa.me:443:333.333.333.333 -x https://username:password@wa.me ifconfig.co --proxy-insecure -v

Эта команда сообщает cURL, как ресолвить IP. При этом в Wireshark я могу видеть, что в качестве SNI установлен wa.me, и что соединение с прокси устанавливается успешно (разумеется, ошибки TLS, связанные с подписанным мной сертификатом, игнорируются). Неплохо, а теперь нужно ждать обратного полёта в Гонконг…

Тестируем на рейсе

Если я что-то напутал, то меня ждёт облом, ведь без доступа к Интернету я ничего не смогу исправить! Мой обратный рейс был в 19:35 местного времени, но я из-за раннего полёта из Эдинбурга я был на ногах уже в 04:00, после чего убивал время, исследуя рынки, дегустируя пиво и наблюдая за Гран-при Эмилии-Романьи. В заведении, которое я посетил, экраны были даже над писсуарами!

Несмотря на то, что я не спал уже примерно 16 часов, мне не терпелось проверить, сработало ли то, что я настроил. После взлёта я подключился к WiFi (на ноутбуке), зарегистрировался в программе лояльности British Airlines и активировал режим «Мессенджеры». Попробовав приведённую выше команду curl, я получил HTTP 200 от ifconfig.co с IP моего VPS; сработало! Вдобавок я попробовал при помощи cURL получить доступ к другим веб-сайтам, например, к example.com, google.com и так далее, чтобы убедиться, что всё в порядке.

Дальше мне предстояло расширить эту систему для доступа к вебу. К счастью, большинство современных браузеров поддерживает отправку трафика через HTTPS-прокси, а у Chromium даже есть флаг для отключения предупреждений о сертификатах TLS (благодаря чему он не будет жаловаться на мой подписанный сертификат, который, очевидно, не относится к настоящему wa.me).

Также мне пришлось изменить в файле hosts DNS-запись wa.me на 333.333.333.333, чтобы Chromium в TLS handshake установил в качестве SNI wa.me, но подключение выполнялось к моему VPS. Так как канал, скорее всего, будет довольно ограниченным (не только из-за того, что это Интернет в самолёте, но и из-за проксирования его через VPS в Нидерландах), я решил загрузить очень простой веб-сайт, состоящий целиком из текста: Hacker News.

Ура! Похоже, мы победили. Мне удалось зайти на HN через бесплатный WiFi British Airlines, предназначенный для мессенджеров! (Примечание: HTTP-запросы отображаются в Wireshark в незашифрованном тексте, потому что я использовал SSLKEYLOGFILE и настроил Wireshark так, чтобы он дешифровал TLS).

К сожалению, попытки загрузки более тяжёлых веб-сайтов заканчивались провалом: изображения в простых текстовых блогах загружались построчно. Что ж, по крайней мере, я испытал ностальгию по коммутируемому соединению!

Я предполагаю, что на бесплатном WiFi, наряду с проверками SNI, также обрезается ширина канала. Возможно, разработчики системы предвидели подобный способ обхода. С другой стороны, если это и есть та скорость Интернета, которая разблокируется на платном тарифе, то пользователей ждёт неприятный сюрприз...

Бонус: ECH

Выше я говорил о работе, проделанной для устранения утечек SNI: ECH. Это слишком объёмная тема для объяснения в моём посте, однако я рекомендую прочитать про него. Это довольно интересно! К тому же, вам станет понятнее данный раздел статьи.

У меня есть веб-сайт для тестирования ECH, поэтому перед рейсом я решил подготовиться получше. По сути, я создал ещё одну ECHConfig, для которой в качестве public_name указал wa.me.

В мире ECH публичный SNI нужен исключительно для того, чтобы сервер выполнил внешний ClientHello, а поскольку ECH-клиенты устанавливают публичный SNI на основании ECHConfig, я могу ввести в Firefox свой реальный домен, который всё равно будет использовать домен wa.me в качестве публичного SNI. После этого безопасно выполнится внутренний Client Hello, содержащий реальный SNI (rfc5746.mywaifu.best), а handshake завершится с «подлинным» сертификатом, подписанным CA для этого домена.

Это тоже сработало, даже без флагов игнорирования TLS, потому что реальный сертификат rfc5746.mywaifu.best был подписан «надёжным CA» (Let’s Encrypt). Что ещё интереснее, это сработало даже на нестандартном порту TLS: 7443! Не знаю точно, почему, но жаловаться не буду.

Примечание о ресолвинге ECHConfig

Обычно ECHConfig должны ресолвиться через зашифрованный DNS, например, DNS-over-HTTPS. Я думаю, что по умолчанию Firefox поступает именно так, но не уверен полностью, что именно так произошло на моём рейсе. Наверно, DoH должен был блокироваться в WiFi мессенджеров? Или, может быть, разработчики разрешили и SNI DoH, потому что новые телефоны по умолчанию используют его. Если кто-то из читателей вскоре собирается лететь на British Airlines, то проверьте и напишите мне!

SNI: не стоит слепо ему доверять

SNI (Server Name Indication), как и следует из названия — это своего рода «подсказка» от клиента серверу. Если кто-то контролирует обе стороны (клиент и сервер), то может указать любое поддельное значение, которое могут попробовать перехватить и анализировать промежуточные устройства (middlebox). Хотя эта система, к сожалению, работает в таких сферах применения, как цензура (когда провайдер или страна пытается блокировать конкретный веб-сайт), в сценариях наподобие распознавания угроз на неё полагаться нельзя; разработчики malware могут спуфить SNI при подключении к своим серверам C&C, потому что на самом деле он им не требуется, но такое будет выглядеть более невинно для промежуточных устройств.

Комментарии (2)


  1. randomsimplenumber
    28.10.2025 05:50

    Можно сделать бота для discord. Пусть хостится на земле, скачивает что нужно и передает клиенту на борт.


    1. NutsUnderline
      28.10.2025 05:50

      это если он доступен и такая его работа не зарежется